the vendor and gossip windows are the reference's own
- the vendor window is the reference's own `MerchantFrame.xml`, which took three things in order: the vendor cursor, the coin kit's move, and `GameTooltip:SetMerchantCompareItem` — where `offset` is the ordinal among CANDIDATE slots (empty ones skipped without decrementing) and a candidate matches by item CLASS and never by entry (two of four RE workers read it as an entry match, which would have fired the compare tooltip only when you already wear the identical item, i.e. never, with the window looking finished); two visible changes ride with the reference's rows — the coin columns take `SmallMoneyFrameTemplate`'s own geometry, and only the ICON takes the mouse, so hovering a row's name shows nothing, which is the client's behaviour - the gossip window is the reference's own, and its deferral was wrong: the "2px rows" were a harness gap (a bare test VM with no text measurer, while the app always installs one), not an engine one; four things the reference does that ours did not are pinned — a hidden SPACER row between the quest groups, coded options are NOT greyed (a coded row is clickable, and the send path's missing code is named as its own gap), the row label is the Button's own text, and the scroll child is not grown - the CAMP/QUIT dialogs and the PARTY_INVITE dialog move to `StaticPopup`'s home — the fourth such rehome of a dialog that had lived in whichever window first needed it — and moving them found a real bug: `button1 = CANCEL` captured a nil at load because `CANCEL` was declared below the insertion point, so the popup engine fell back and put "Yes" on the camp dialog's cancel button - three instruments: the gap report stops counting commented-out calls (`ShowInventorySellCursor` blocked the character sheet on three commented lines — a whole RE round spent on an over-report), it runs the load itself and answers in ONE table (29 "free" became 24 actually unblocked), and a shadow report predicts which swaps will fail by intersecting the names our files define with the names the chain files in our own manifest define — 38 collisions across 5 files, ours winning every one Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -785,7 +785,7 @@ fn shipped_bag_frame_drives_end_to_end() {
|
||||
}
|
||||
}
|
||||
load_ui(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
||||
load_ui(&s, "MerchantFrame.xml");
|
||||
load_ui(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
assert_eq!(
|
||||
bar_frames, 16,
|
||||
|
||||
@@ -133,7 +133,7 @@ fn b_opens_the_backpack_alone_and_closes_every_bag() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
// Backpack (16) + one equipped bag in slot 2 (6). Bags 1/3/4 are left unset → 0 slots.
|
||||
@@ -214,7 +214,7 @@ fn shift_b_toggles_every_bag_at_once() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(
|
||||
0,
|
||||
@@ -295,7 +295,7 @@ fn bag_bar_buttons_light_while_their_bag_is_open() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(
|
||||
0,
|
||||
@@ -408,7 +408,7 @@ fn bag_tooltip_hangs_left_when_the_slot_sits_in_the_right_half() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -484,7 +484,7 @@ fn hovered_bag_tooltip_fills_itself_when_the_stats_land() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -587,7 +587,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -721,7 +721,7 @@ fn readable_letter_hover_shows_the_inspect_magnifier() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -791,7 +791,7 @@ fn drag_across_two_slots_queues_the_same_move_a_click_pickup_would() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -874,7 +874,7 @@ fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
let mut slots = std::collections::HashMap::new();
|
||||
@@ -946,7 +946,7 @@ fn equipped_bag_window_snug_fits_its_row_count() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
// (bag id, slot count, expected window height). 6 → 2 rows plus-two-top (72+32+10); 8 → 2 rows
|
||||
@@ -1005,7 +1005,7 @@ fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
|
||||
for file in BAG_UI {
|
||||
load_xml(s, file);
|
||||
}
|
||||
load_xml(s, "MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
@@ -1345,7 +1345,7 @@ fn bag_slot_cooldown_sweeps_through_the_xml() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.tick(100.0); // a nonzero clock epoch, like the engine cooldown tests
|
||||
|
||||
@@ -1418,7 +1418,7 @@ fn the_bar_bag_buttons_name_themselves_on_hover() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.resolve();
|
||||
|
||||
// The backpack: its label plus OPENALLBAGS's live key (default B; 0997's divergence note).
|
||||
@@ -1551,7 +1551,7 @@ fn keyring_surface(s: &UiScript) {
|
||||
for file in BAG_UI {
|
||||
load_xml(s, file);
|
||||
}
|
||||
load_xml(s, "MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(s, "Interface\\FrameXML\\UIErrorsFrame.xml");
|
||||
}
|
||||
|
||||
@@ -1823,7 +1823,7 @@ fn an_item_push_drops_its_icon_into_the_bag_that_took_it() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.resolve();
|
||||
|
||||
let shown = |s: &UiScript, name: &str| {
|
||||
@@ -2079,7 +2079,7 @@ fn an_addon_that_hooks_toggle_backpack_receives_the_click() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
// Bagnon's exact idiom: capture the original, replace the global.
|
||||
@@ -2135,7 +2135,7 @@ fn the_bag_slots_carry_the_references_names_and_icon_names() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
for i in 0..4 {
|
||||
assert!(
|
||||
@@ -2195,7 +2195,7 @@ fn the_backpack_buttons_ring_follows_its_own_window_through_real_clicks() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
// The reference's `ToggleBag` opens nothing for a container of size 0, so the backpack has to
|
||||
// be fed — our own windows existed statically and opened regardless.
|
||||
|
||||
@@ -73,7 +73,7 @@ fn bag_setup() -> UiScript {
|
||||
}
|
||||
// `ContainerFrame_Update` reads `MerchantFrame:IsShown()` on any slot the tooltip owns, and
|
||||
// `BankFrame` (BAG_UI's last entry) fills its purse through `BenillaMoney_Set`.
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const ROOM_UI: &[&str] = &[
|
||||
"UIDropDownMenu.xml",
|
||||
"UnitPopup.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\StackSplitFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
|
||||
@@ -113,10 +113,10 @@ fn an_error_toast_draws_over_an_open_panel_window() {
|
||||
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
|
||||
// ScrollTemplates.xml (the faux kit the list rides) + UIPanelTemplates.xml (the detail
|
||||
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
|
||||
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
|
||||
// ScrollTemplates.xml (the faux kit the list rides) + UIPanelTemplates.xml (the detail
|
||||
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
|
||||
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "QuestLogFrame.xml");
|
||||
|
||||
@@ -73,7 +73,7 @@ fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_Set, BankFrame's purse helper
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_Set, BankFrame's purse helper
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
|
||||
@@ -145,7 +145,7 @@ fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
s.set_money(0);
|
||||
@@ -233,7 +233,7 @@ fn escape_closes_an_open_stack_split_frame() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
|
||||
load_xml(&s, "ChatFrame.xml"); // …and ChatFrameEditBox, the shift fork's first test
|
||||
load_xml(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
||||
s.set_money(0);
|
||||
@@ -312,7 +312,7 @@ fn escape_ladder_cast_then_windows_then_target_one_eater_per_press() {
|
||||
// GameTooltip.xml (BAG_UI's, for the bag slots' tooltips) also carries TOOLTIP_DEFAULT_COLOR,
|
||||
// which the dropdown backdrop's OnLoad reads — so the kit can load straight after it.
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
||||
@@ -406,7 +406,7 @@ fn escape_ladder_targeting_rung_after_cast_before_windows() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
||||
|
||||
@@ -219,74 +219,11 @@ const KNOWN: &[Known] = &[
|
||||
},
|
||||
// ── We take the mouse where the reference does not ─────────────────────────────────────────
|
||||
//
|
||||
// The merchant rows: ours are one mouse-taking row each; the reference splits every row into
|
||||
// an inert container plus a `$parentItemButton` that takes the mouse on its behalf. Same
|
||||
// clicks, a different frame under them.
|
||||
Known {
|
||||
frame: "MerchantItem1",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem2",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem3",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem4",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem5",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem6",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem7",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem8",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem9",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem10",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem11",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantItem12",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
Known {
|
||||
frame: "MerchantBuyBackItem",
|
||||
flag: Flag::Mouse,
|
||||
why: "inherits BenillaMerchantItemTemplate",
|
||||
},
|
||||
// The merchant rows' divergence RETIRED (1751): our `MerchantFrame.xml` is gone and the
|
||||
// reference's own file is on the player's chain, so its rows are its rows. There were 13
|
||||
// entries here — twelve `MerchantItem<N>` plus `MerchantBuyBackItem` — saying ours took the
|
||||
// mouse on the row itself where the reference splits each row into an inert container plus a
|
||||
// `$parentItemButton`. That is exactly what this gate exists to notice going away.
|
||||
Known {
|
||||
frame: "WorldMapFrame",
|
||||
flag: Flag::Mouse,
|
||||
|
||||
@@ -199,7 +199,7 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
|
||||
#[test]
|
||||
fn escape_opens_the_menu_only_when_nothing_else_wants_the_press_and_then_closes_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = bag_harness_with(&[], &["MerchantFrame.xml"]);
|
||||
let mut s = bag_harness_with(&[], &["Interface\\FrameXML\\MerchantFrame.xml"]);
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(backpack()));
|
||||
|
||||
@@ -236,7 +236,7 @@ fn escape_opens_the_menu_only_when_nothing_else_wants_the_press_and_then_closes_
|
||||
#[test]
|
||||
fn the_clicked_form_closes_everything_and_opens_the_menu_in_one_go() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = bag_harness_with(&[], &["MerchantFrame.xml"]);
|
||||
let mut s = bag_harness_with(&[], &["Interface\\FrameXML\\MerchantFrame.xml"]);
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
||||
@@ -263,7 +263,7 @@ fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() {
|
||||
let mut s = bag_harness_with(
|
||||
&[],
|
||||
&[
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
// The loot window is the reference's own since 1751 — `test_ui::LOOT_UI` carries
|
||||
// what it needs and why, and PartyFrame's `MAX_PARTY_MEMBERS` is needed at LOAD.
|
||||
"UIDropDownMenu.xml",
|
||||
@@ -594,7 +594,7 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
|
||||
"UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"WorldMapFrame.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
// The loot window is the reference's own since 1751 — see `test_ui::LOOT_UI` for what
|
||||
// each of these buys; `PartyFrame`'s MAX_PARTY_MEMBERS is needed at LOAD time.
|
||||
"UnitPopup.xml",
|
||||
@@ -638,7 +638,10 @@ fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
|
||||
// ActionBar.xml goes AHEAD of the bag stack: it declares MainMenuBarArtFrame, which the bag
|
||||
// bar anchors into and seats itself above (`BenillaActionBarArt_SeatAbove`, nil-guarded in
|
||||
// BagFrame.xml precisely because most harnesses load no bar).
|
||||
let mut s = bag_harness_with(&["Cooldown.xml", "ActionBar.xml"], &["MerchantFrame.xml"]);
|
||||
let mut s = bag_harness_with(
|
||||
&["Cooldown.xml", "ActionBar.xml"],
|
||||
&["Interface\\FrameXML\\MerchantFrame.xml"],
|
||||
);
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(backpack()));
|
||||
s.resolve();
|
||||
|
||||
@@ -479,7 +479,7 @@ fn shipped_loot_pushed_to_center_by_merchant() {
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Loot opens onto the empty left slot.
|
||||
s.set_loot(Some(coin_and_two_items()));
|
||||
|
||||
@@ -52,18 +52,19 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The named virtual Font objects the re-skinned rows/title inherit through — loaded first at
|
||||
// runtime (ui_script's shipped list) so `inherits="GameFontNormalSmall"` resolves here too.
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
// The window's frame census: window 1 + item rows 12 (10 merchant + the 2 buyback-only) +
|
||||
// close 1 + row-price coin slots 36 + purse coin slots 3 + the buyback slot 1 + its 3 coins +
|
||||
// the repair pair 2 + the page pair 2 + the tab pair 2 = 63 (the title/quadrant art are
|
||||
// FontString/Texture layers, not frames).
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
// The STOCK file's frame census (1751). Ours materialized 63 — window, twelve rows, close, the
|
||||
// three-slot coin kit's 39 slots, the buyback slot, and the repair/page/tab pairs. The reference's
|
||||
// shape is different in kind, not just in count: every row is an inert container PLUS a
|
||||
// `$parentItemButton` that carries `ItemButtonTemplate`'s own children, and the prices are
|
||||
// `SmallMoneyFrameTemplate` instances rather than our `<prefix>Coin1..3` chain. A count is a
|
||||
// fingerprint of a file; this one is the reference's.
|
||||
assert_eq!(
|
||||
load_xml(&s, "MerchantFrame.xml"),
|
||||
63,
|
||||
"window + 12 rows + close + 39 coin slots + buyback slot + repair/page/tab pairs"
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"),
|
||||
90,
|
||||
"the stock file's own shape — see the census note above"
|
||||
);
|
||||
|
||||
// Hidden by default: no vendor icon on screen.
|
||||
@@ -155,18 +156,22 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
11,
|
||||
"10 merchant rows + the buyback slot render their socket plate"
|
||||
);
|
||||
// THREE, not two, and the third is the buyback slot. Our file dimmed an empty buyback socket
|
||||
// with the empty rows; the reference's `MerchantFrame_UpdateBuybackInfo` does not run the
|
||||
// empty-row arm on it at all, so it keeps the bright plate whether or not a sold item is
|
||||
// there. Same census, one socket moved sides.
|
||||
assert_eq!(
|
||||
socket_colors.iter().filter(|c| c[0] > 0.9).count(),
|
||||
2,
|
||||
"the 2 filled rows keep the socket full-bright"
|
||||
3,
|
||||
"the 2 filled rows and the buyback slot keep the socket full-bright"
|
||||
);
|
||||
assert_eq!(
|
||||
socket_colors
|
||||
.iter()
|
||||
.filter(|c| (c[0] - 0.4).abs() < 0.01)
|
||||
.count(),
|
||||
9,
|
||||
"the 8 empty rows + the empty buyback slot dim the socket to 0.4"
|
||||
8,
|
||||
"the 8 empty rows dim the socket to 0.4 (ref MerchantFrame.lua l.115)"
|
||||
);
|
||||
assert_eq!(
|
||||
quads
|
||||
@@ -335,11 +340,10 @@ fn shipped_merchant_frame_drives_end_to_end() {
|
||||
fn merchant_show_hide_plays_open_and_close_kits() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Hidden at load: no open sound (never transitions on startup).
|
||||
assert!(
|
||||
@@ -388,7 +392,7 @@ fn vendor_open_opens_the_backpack_and_layers_the_sound() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
equip_bag(&mut s, 0, "Backpack", 16);
|
||||
let _ = s.take_sounds(); // ignore anything from load (frames are hidden; nothing should)
|
||||
@@ -438,7 +442,7 @@ fn vendor_leaves_an_already_open_backpack_alone() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
equip_bag(&mut s, 0, "Backpack", 16);
|
||||
|
||||
@@ -486,7 +490,7 @@ fn vendor_opens_and_closes_all_equipped_bags() {
|
||||
for file in BAG_UI {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
equip_bag(&mut s, 0, "Backpack", 16);
|
||||
equip_bag(&mut s, 2, "Small Pouch", 6);
|
||||
@@ -519,11 +523,10 @@ fn vendor_opens_and_closes_all_equipped_bags() {
|
||||
fn merchant_switch_plays_close_then_open_and_queues_the_consumable_close() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Vendor A open.
|
||||
s.set_money(0);
|
||||
@@ -562,10 +565,9 @@ fn merchant_switch_plays_close_then_open_and_queues_the_consumable_close() {
|
||||
fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
// The quality→colour table is `ITEM_QUALITY_COLORS`, and Fonts.xml is its home — this used
|
||||
// to load the whole loot window for `BENILLA_LOOT_QUALITY_COLORS`, an alias our own
|
||||
// LootFrame.xml set to that same table. 1751 took that window to the chain and the alias went
|
||||
@@ -573,7 +575,7 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
// every FrameXML file loads before any hover fires, so load it here too — with the dropdown
|
||||
// kit its GroupLootDropDown initializes against at load (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
s.set_merchant(Some(MerchantState {
|
||||
items: vec![
|
||||
@@ -659,10 +661,17 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
assert!(!has_text(&quads, "Main Hand"), "no tooltip before hover");
|
||||
let sword_icon = rect_of_tex(&quads, "INV_Sword_04");
|
||||
|
||||
// Hover the row over its NAME PLATE (well right of the icon): the whole row is the button, but
|
||||
// the highlight must still cover only the 37px icon at the row's TOPLEFT.
|
||||
// Hover the ICON, and that is the whole change 1751 made here. Our `MerchantFrame.xml` made
|
||||
// the WHOLE ROW take the mouse — this hovered 60px right of the icon, over the name plate,
|
||||
// and expected the tooltip. The reference splits every row into an inert container plus a
|
||||
// `$parentItemButton` the size of the icon, which takes the mouse on its behalf; the twelve
|
||||
// `MerchantItem<N>` entries in the flag gate's KNOWN list said exactly that, and they retired
|
||||
// with our file.
|
||||
//
|
||||
// So on the reference's window, hovering a vendor row's NAME shows nothing. That is not a
|
||||
// regression to fix — it is what the client does.
|
||||
let (hx, hy) = (
|
||||
sword_icon.right + 60.0,
|
||||
(sword_icon.left + sword_icon.right) * 0.5,
|
||||
(sword_icon.bottom + sword_icon.top) * 0.5,
|
||||
);
|
||||
s.mouse_move(hx, hy);
|
||||
@@ -777,148 +786,21 @@ fn shipped_merchant_hover_scopes_highlight_and_anchors_item_tooltip() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Pin the real SmallMoneyFrame *shrink* (MoneyFrame.lua l.202 `SetWidth(GetTextWidth() +
|
||||
/// iconWidth)`, l.269 `frame:SetWidth`) that `BenillaMoney_Set` reproduces from the app's
|
||||
/// digit-advance feed ([`benilla_ui::script::UiScript::set_digit_advances`]): price groups pack
|
||||
/// number+coin with the real 4px spacing and start left-flush at the plate's left edge whatever
|
||||
/// the leading denomination (the "silver rows indent" bug), the purse packs the same
|
||||
/// right-to-left off its RIGHT anchor, and collapsed denominations HIDE (never a blank
|
||||
/// fixed-width box — the "copper-to-silver gap" bug).
|
||||
#[test]
|
||||
fn money_display_shrinks_to_content_and_stays_flush() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The app's synchronous number metrics (ui_script feeds NumberFontNormal's real advances once
|
||||
// per atlas bake; a uniform 6px here keeps the arithmetic below legible).
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
|
||||
s.set_money(12_345); // 1g 23s 45c → purse digits "1" / "23" / "45"
|
||||
s.set_merchant(Some(MerchantState {
|
||||
items: vec![
|
||||
MerchantItem {
|
||||
name: Some("Refreshing Spring Water".into()),
|
||||
texture: Some("Interface\\Icons\\INV_Drink_18".into()),
|
||||
price: 25, // copper only → "25"
|
||||
quantity: 1,
|
||||
num_available: -1,
|
||||
item_id: 159,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
MerchantItem {
|
||||
name: Some("Linen Bandage".into()),
|
||||
texture: Some("Interface\\Icons\\INV_Misc_Bandage_01".into()),
|
||||
price: 204, // 2s 4c → "2" + "4" (silver-led — the row that used to indent)
|
||||
quantity: 1,
|
||||
num_available: -1,
|
||||
item_id: 1251,
|
||||
stats: None,
|
||||
link: None,
|
||||
max_stack: Some(1),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}));
|
||||
s.fire_event("MERCHANT_SHOW", vec![]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
|
||||
let text_rect = |t: &str| {
|
||||
quads
|
||||
.iter()
|
||||
.find(|q| matches!(&q.content, QuadContent::Text { text: Some(x), .. } if x == t))
|
||||
.and_then(|q| q.rect)
|
||||
.unwrap_or_else(|| panic!("no text quad for {t:?}"))
|
||||
};
|
||||
let near = |a: f32, b: f32| (a - b).abs() < 0.01;
|
||||
|
||||
// The real collapse HIDES unused slots: exactly 6 coin icons are on screen — row 1's copper,
|
||||
// row 2's silver+copper, the purse's gold+silver+copper. (8 empty rows × 3 + all the unused
|
||||
// slots of the filled displays draw nothing.)
|
||||
let coin_icons = quads
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("UI-MoneyIcons"))
|
||||
})
|
||||
.count();
|
||||
assert_eq!(coin_icons, 6, "only the filled denominations draw a coin");
|
||||
|
||||
// Left-flush prices: both rows' leading numbers start at rowLeft + 46 (the name box's own
|
||||
// column — 2px of breathing room off the plate border the director asked for; the ref quote
|
||||
// was 44), whether the row leads with copper ("25") or silver ("2"). Row 1 sits at abs left
|
||||
// 24 (window 0 + 24), row 2 at 189 (24 + 153-wide row + 12 column gap).
|
||||
let p1 = text_rect("25");
|
||||
let p2 = text_rect("2");
|
||||
assert!(
|
||||
near(p1.left - 24.0, 46.0),
|
||||
"copper-led price starts at the plate edge, got {}",
|
||||
p1.left - 24.0
|
||||
);
|
||||
assert!(
|
||||
near(p2.left - 189.0, 46.0),
|
||||
"silver-led price starts at the SAME plate edge (the indent bug), got {}",
|
||||
p2.left - 189.0
|
||||
);
|
||||
|
||||
// The shrink + the real 4px MONEY_BUTTON_SPACING: row 2's copper number starts exactly
|
||||
// number-width + 13px coin + 4px gap after the silver number's left edge — no dead air from a
|
||||
// fixed-width slot (the gap bug). "2" is 6px wide, its coin 13, the gap 4 → "4" at +23.
|
||||
let p2c = text_rect("4");
|
||||
assert!(
|
||||
near(p2c.left, p2.right + 13.0 + 4.0),
|
||||
"silver→copper packs number+coin+4px, got {} vs {}",
|
||||
p2c.left,
|
||||
p2.right + 13.0 + 4.0
|
||||
);
|
||||
|
||||
// The purse packs the same but right-to-left, right-flush at window right -53 (the ref
|
||||
// MerchantMoneyFrame anchor -40 minus the template's built-in 13px right pad — see the purse
|
||||
// XML comment): copper "45" ends 13px (its coin) short of x=331, silver "23" ends its coin
|
||||
// 4px left of copper's slot, gold "1" likewise.
|
||||
let purse_c = text_rect("45");
|
||||
let purse_s = text_rect("23");
|
||||
let purse_g = text_rect("1");
|
||||
assert!(
|
||||
near(purse_c.right + 13.0, 384.0 - 53.0),
|
||||
"purse copper coin right-flush at window right -53, got {}",
|
||||
purse_c.right + 13.0
|
||||
);
|
||||
assert!(
|
||||
near(purse_c.left, purse_s.right + 13.0 + 4.0),
|
||||
"purse silver→copper packs rtl with the 4px gap, got {} vs {}",
|
||||
purse_c.left,
|
||||
purse_s.right + 13.0 + 4.0
|
||||
);
|
||||
assert!(
|
||||
near(purse_s.left, purse_g.right + 13.0 + 4.0),
|
||||
"purse gold→silver packs rtl with the 4px gap, got {} vs {}",
|
||||
purse_s.left,
|
||||
purse_g.right + 13.0 + 4.0
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// A sell changes only the coinage — no MERCHANT_UPDATE rides along. The purse repaints on the
|
||||
// real client's PLAYER_MONEY event (the app fires it whenever the pushed money changes).
|
||||
s.set_money(12_349); // +4c: the purse copper becomes "49"
|
||||
s.fire_event("PLAYER_MONEY", vec![]);
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
assert!(
|
||||
quads
|
||||
.iter()
|
||||
.any(|q| matches!(&q.content, QuadContent::Text { text: Some(t), .. } if t == "49")),
|
||||
"the merchant purse live-updates on PLAYER_MONEY"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
// ── `money_display_shrinks_to_content_and_stays_flush` RETIRED (1751) ────────────────────────
|
||||
//
|
||||
// It pinned `BenillaMoney_Set`'s geometry inside OUR `MerchantFrame.xml`: price groups left-flush
|
||||
// at `rowLeft + 46`, the purse right-flush at window-right −53, denominations packed with the real
|
||||
// 4px `MONEY_BUTTON_SPACING`. The 46 was a director call — 2px of breathing room off the plate
|
||||
// border where the reference quotes 44 — and it was a fact about a file we no longer ship.
|
||||
//
|
||||
// The reference's vendor rows use `SmallMoneyFrameTemplate` and `MoneyFrame_Update`, a different
|
||||
// kit with its own geometry. **The rules the test protected are not lost**: the shrink, the
|
||||
// collapse and the per-type `showSmallerCoins`/`fixedWidth` behaviour are `money_frame_tests.rs`'s
|
||||
// subject, against the kit that now draws these rows. What is gone is the merchant-specific pixel
|
||||
// geometry, which had nothing left to describe.
|
||||
//
|
||||
// **The vendor window's coin columns therefore look different now** — the reference's spacing, not
|
||||
// our approved 2px indent. Said here rather than left inside a changed constant.
|
||||
|
||||
/// The merchant/buyback tab pair drives the two pages (ref MerchantFrame_Update l.57-64 +
|
||||
/// UpdateMerchantInfo/UpdateBuybackInfo): the merchant page shows the single most-recent buyback
|
||||
@@ -929,11 +811,10 @@ fn money_display_shrinks_to_content_and_stays_flush() {
|
||||
fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(500);
|
||||
|
||||
let buyback_item = |name: &str, price: u32| MerchantItem {
|
||||
@@ -990,17 +871,19 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
.unwrap());
|
||||
|
||||
// The slot click buys back the most recent sale (slot 2 of 2).
|
||||
s.run("BenillaMerchantBuyBackItem_OnClick()").unwrap();
|
||||
// The stock row is a container plus a `$parentItemButton` that takes the mouse on its
|
||||
// behalf — the split the retired flag-gate entries used to record as our divergence.
|
||||
s.run("MerchantBuyBackItemItemButton:Click()").unwrap();
|
||||
assert_eq!(s.take_merchant_buybacks(), vec![2]);
|
||||
|
||||
// Repair-all queues its intent.
|
||||
s.run("BenillaMerchantRepairAllButton_OnClick()").unwrap();
|
||||
s.run("MerchantRepairAllButton:Click()").unwrap();
|
||||
assert!(s.take_repair_all());
|
||||
s.take_sounds();
|
||||
|
||||
// Tab 2: the buyback page — retitled, rows off GetBuybackItemInfo, merchant-only pieces down,
|
||||
// buyback art up, tab 2 selected (its Active slices showing).
|
||||
s.run("BenillaMerchantFrameTab_OnClick(2)").unwrap();
|
||||
s.run("MerchantFrameTab2:Click()").unwrap();
|
||||
assert!(s.errors().is_empty(), "tab errors: {:?}", s.errors());
|
||||
assert!(s
|
||||
.eval::<bool>("return MerchantNameText:GetText() == 'Merchant Buyback'")
|
||||
@@ -1026,12 +909,12 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
.unwrap());
|
||||
|
||||
// A row click on the buyback page buys that slot back.
|
||||
s.run("BenillaMerchantItem_OnClick(MerchantItem1, 'LeftButton')")
|
||||
s.run("MerchantItem1ItemButton:Click(\"LeftButton\")")
|
||||
.unwrap();
|
||||
assert_eq!(s.take_merchant_buybacks(), vec![1]);
|
||||
|
||||
// Back to tab 1: the merchant row returns, the buyback art drops.
|
||||
s.run("BenillaMerchantFrameTab_OnClick(1)").unwrap();
|
||||
s.run("MerchantFrameTab1:Click()").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return MerchantItem1Name:GetText() == 'Refreshing Spring Water' \
|
||||
@@ -1049,11 +932,10 @@ fn merchant_tabs_drive_buyback_page_and_repair_pair() {
|
||||
fn merchant_tabs_fit_their_labels() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_merchant(Some(MerchantState::default()));
|
||||
s.fire_event("MERCHANT_SHOW", vec![ScriptValue::Str("Vendor".into())]);
|
||||
@@ -1091,7 +973,7 @@ fn merchant_tabs_fit_their_labels() {
|
||||
|
||||
/// Hovering a buy-tab row arms the vendor **Buy** cursor (the grayed **UnableBuy** when you can't
|
||||
/// afford the row), swaps to **Inspect** while Ctrl is held, and clears on leave — the whole reason
|
||||
/// this window exists to a first-time visitor. Drives the real `BenillaMerchantItem_OnEnter` +
|
||||
/// this window exists to a first-time visitor. Drives the stock row button's own `<OnEnter>` +
|
||||
/// the frame's `OnUpdate` (a `tick`) exactly as the app does, since the coin is re-armed per frame.
|
||||
#[test]
|
||||
fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
@@ -1099,11 +981,10 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// A purse of 50c: row 1 (25c) is affordable, row 2 (100c) is not.
|
||||
s.set_money(50);
|
||||
@@ -1139,7 +1020,10 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
assert_eq!(s.ui_cursor(), None, "no override before any hover");
|
||||
|
||||
// Row 1 hover: OnEnter remembers the row, the frame's OnUpdate arms the coin — affordable → Buy.
|
||||
s.run("BenillaMerchantItem_OnEnter(MerchantItem1)").unwrap();
|
||||
// The stock `<OnEnter>` is inline on the row button, so it is fired the way the engine
|
||||
// fires it: `this` bound, then the compiled handler.
|
||||
s.run("this = MerchantItem1ItemButton MerchantItem1ItemButton:GetScript(\"OnEnter\")() this = nil")
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
@@ -1159,12 +1043,14 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
s.set_modifiers(false, false, false);
|
||||
|
||||
// Leaving clears the override (OnLeave ResetCursor + the itemHover poll stops).
|
||||
s.run("BenillaMerchantItem_OnLeave(MerchantItem1)").unwrap();
|
||||
s.run("this = MerchantItem1ItemButton MerchantItem1ItemButton:GetScript(\"OnLeave\")() this = nil")
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(s.ui_cursor(), None, "leaving the row resets the cursor");
|
||||
|
||||
// Row 2 hover: 100c against a 50c purse → the grayed UnableBuy.
|
||||
s.run("BenillaMerchantItem_OnEnter(MerchantItem2)").unwrap();
|
||||
s.run("this = MerchantItem2ItemButton MerchantItem2ItemButton:GetScript(\"OnEnter\")() this = nil")
|
||||
.unwrap();
|
||||
s.tick(0.016);
|
||||
assert_eq!(
|
||||
s.ui_cursor(),
|
||||
@@ -1188,11 +1074,10 @@ fn shipped_merchant_frame_arms_the_buy_cursor_on_hover() {
|
||||
fn trade_recipient_money_renders_the_digit_not_ellipsis() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MerchantFrame.xml"); // the BenillaMoney_* helpers
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // the BenillaMoney_* helpers
|
||||
load_xml(&s, "TradeFrame.xml");
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
|
||||
@@ -1244,13 +1129,12 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
|
||||
const WATER_LINK: &str = "|cffffffff|Hitem:159:0:0:0|h[Refreshing Spring Water]|h|r";
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml", // BenillaChatEdit_InsertLink, the shared shift-insert helper
|
||||
"GameTooltip.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
|
||||
@@ -734,7 +734,7 @@ fn interface_harness() -> UiScript {
|
||||
"ActionBar.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"Interface\\FrameXML\\BuffFrame.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"QuestFrame.xml",
|
||||
"QuestLogFrame.xml",
|
||||
],
|
||||
|
||||
@@ -42,6 +42,11 @@ fn frame_rect(quads: &[ExtractedQuad], w: f32, h: f32) -> benilla_ui::layout::Re
|
||||
fn shipped_gossip_frame_drives_end_to_end() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare
|
||||
// VM sizes every row from the previous frame's box. See
|
||||
// `shipped_gossip_rows_grow_to_their_wrapped_labels` for why this is a harness gap and not an
|
||||
// engine one.
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
@@ -54,9 +59,9 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
// button + the GOODBYE button. The greeting and the NPC-name banner are FontString layers (the
|
||||
// real GossipGreetingText ref l.241 / GossipFrameNpcNameText ref l.170) — not their own frames.
|
||||
assert_eq!(
|
||||
load_xml(&s, "GossipFrame.xml"),
|
||||
41,
|
||||
"window + greeting panel + scroll + bar (+2 arrows) + child + 32 rows + close + goodbye"
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml"),
|
||||
43,
|
||||
"the stock file's own shape (1751) — ours materialized 41"
|
||||
);
|
||||
|
||||
// Hidden by default: no gossip icon on screen.
|
||||
@@ -101,8 +106,18 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
.unwrap(),
|
||||
"Greetings, traveler. How may I help you?"
|
||||
);
|
||||
// Row 1 shown + enabled; row 2 shown + disabled (coded); row 3+ hidden (no quest rows this menu,
|
||||
// so the shared pool starts filling from option 1 at row 1 — decision 0088 §3).
|
||||
// Both rows shown and BOTH ENABLED — including the coded one, and that is a divergence
|
||||
// retiring rather than a regression.
|
||||
//
|
||||
// Our `GossipFrame.xml` greyed a coded option, on decision 0081's "coded options are greyed,
|
||||
// never selected". **1.12 does no such thing**: `GossipFrameOptionsUpdate` (ref l.111-128) has
|
||||
// no coded handling at all — it sets the text, resizes, sets the icon from the option TYPE and
|
||||
// shows. There is no `Disable()` anywhere in the file.
|
||||
//
|
||||
// The consequence is worth naming: a coded option is now clickable, and our
|
||||
// `GossipSelectOption` still sends no code (0081 v1). The server sees a select without the
|
||||
// code it asked for. That is a named gap in the send path, not something the window should be
|
||||
// lying about — the reference lets you click it.
|
||||
let states: (bool, bool, bool, bool) = s
|
||||
.eval(
|
||||
"return GossipTitleButton1:IsVisible(), GossipTitleButton1:IsEnabled() ~= 0,\n\
|
||||
@@ -111,8 +126,8 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
states,
|
||||
(true, true, false, false),
|
||||
"coded row disabled, extras hidden"
|
||||
(true, true, true, false),
|
||||
"the reference greys nothing; extras hidden"
|
||||
);
|
||||
|
||||
s.resolve();
|
||||
@@ -177,8 +192,14 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
let icon_rect = quads
|
||||
.iter()
|
||||
.find(|q| {
|
||||
// Case-folded, because the reference concatenates the option TYPE verbatim into
|
||||
// `Interface\GossipFrame\<Type>GossipIcon` and our `GetGossipOptions` answers the
|
||||
// lowercase Era icon name. The asset VFS folds case, so the art loads either way; only
|
||||
// the path STRING differs, and our own `GossipFrame.xml` used to capitalise it before
|
||||
// building the path. Whether 1.12's own binding answers lowercase or capitalised is an
|
||||
// open fidelity question, not something to settle by editing the feed to match a test.
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("VendorGossipIcon"))
|
||||
if p.to_ascii_lowercase().contains("vendorgossipicon"))
|
||||
})
|
||||
.and_then(|q| q.rect)
|
||||
.expect("vendor option icon visible after GOSSIP_SHOW");
|
||||
@@ -203,7 +224,7 @@ fn shipped_gossip_frame_drives_end_to_end() {
|
||||
|
||||
// The close button queues a close intent (the app clears state; here we drive the hide too)
|
||||
// through HideUIPanel, which vacates the left slot.
|
||||
s.run("BenillaGossipCloseButton_OnClick()").unwrap();
|
||||
s.run("GossipFrameCloseButton:Click()").unwrap();
|
||||
assert!(s.take_gossip_close());
|
||||
assert!(!s.eval::<bool>("return GossipFrame:IsVisible()").unwrap());
|
||||
assert!(
|
||||
@@ -231,7 +252,7 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
s.set_gossip(Some(GossipMenu {
|
||||
greeting: "A word, traveler.".into(),
|
||||
@@ -259,20 +280,24 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// Rows 1-2 carry the quest titles (active first, matching the order the menu supplied them —
|
||||
// benilla's seam already flattens available/active into one ordered list, ui_gossip.rs), row 3
|
||||
// the option, row 4+ hidden.
|
||||
let (r1_text, r1_vis, r2_text, r2_vis, r3_text, r3_vis, r4_vis): (
|
||||
let (r1_text, r1_vis, r2_vis, r3_text, r3_vis, r4_vis, r5_text, r5_vis, r6_vis): (
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
String,
|
||||
bool,
|
||||
bool,
|
||||
) = s
|
||||
.eval(
|
||||
"return GossipTitleButton1Label:GetText(), GossipTitleButton1:IsVisible(),\n\
|
||||
GossipTitleButton2Label:GetText(), GossipTitleButton2:IsVisible(),\n\
|
||||
GossipTitleButton3Label:GetText(), GossipTitleButton3:IsVisible(),\n\
|
||||
GossipTitleButton4:IsVisible()",
|
||||
"return GossipTitleButton1:GetText(), GossipTitleButton1:IsVisible(),\n\
|
||||
GossipTitleButton2:IsVisible(),\n\
|
||||
GossipTitleButton3:GetText(), GossipTitleButton3:IsVisible(),\n\
|
||||
GossipTitleButton4:IsVisible(),\n\
|
||||
GossipTitleButton5:GetText(), GossipTitleButton5:IsVisible(),\n\
|
||||
GossipTitleButton6:IsVisible()",
|
||||
)
|
||||
.unwrap();
|
||||
// **AVAILABLE quests lead, then ACTIVE** — the reference's own order (`GossipFrameUpdate` calls
|
||||
@@ -280,14 +305,24 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// this window started reading the reference's two list verbs instead of the single ordered
|
||||
// `GetGossipQuestInfo` benilla invented. The packet listed the active quest first; the window
|
||||
// does not.
|
||||
// **Each group is followed by a HIDDEN SPACER row, and the button index skips it** — the
|
||||
// reference's own layout, and something our `GossipFrame.xml` did not do. Both
|
||||
// `GossipFrameAvailableQuestsUpdate` and `…ActiveQuestsUpdate` end by hiding
|
||||
// `GossipTitleButton<buttonIndex>` and then incrementing past it (ref l.80-84, l.104-108), so
|
||||
// the groups are separated by a blank row's worth of space rather than butting together.
|
||||
//
|
||||
// With one available quest, one active and one option that lays out as:
|
||||
// 1 available · 2 hidden spacer · 3 active · 4 hidden spacer · 5 option · 6+ hidden.
|
||||
assert_eq!((r1_text.as_str(), r1_vis), ("A Threat Within", true));
|
||||
assert_eq!((r2_text.as_str(), r2_vis), ("Report to Goldshire", true));
|
||||
assert!(!r2_vis, "the available group's trailing spacer");
|
||||
assert_eq!((r3_text.as_str(), r3_vis), ("Report to Goldshire", true));
|
||||
assert!(!r4_vis, "the active group's trailing spacer");
|
||||
assert_eq!(
|
||||
(r3_text.as_str(), r3_vis),
|
||||
(r5_text.as_str(), r5_vis),
|
||||
("Let me browse your goods.", true),
|
||||
"the option row sits below both quest rows"
|
||||
"the option row sits below both quest groups"
|
||||
);
|
||||
assert!(!r4_vis, "no fourth row: 2 quests + 1 option");
|
||||
assert!(!r6_vis, "nothing past the option row");
|
||||
|
||||
// The per-row icon matches active vs available (ref l.75/99), verified via the resolved quads
|
||||
// rather than the Lua-side texture string, so the assertion also proves the rows actually paint.
|
||||
@@ -331,8 +366,8 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
// that back to the whole-menu position the app's queue speaks — 1, the packet's own order. The
|
||||
// two numbers being different for the same click is exactly what the single-list
|
||||
// `SelectGossipQuest` we retired could not express.
|
||||
s.run("BenillaGossipRow_OnClick(GossipTitleButton2)")
|
||||
.unwrap();
|
||||
// Button 3, not 2: the reference's hidden spacer sits at 2 (see the layout note above).
|
||||
s.run("GossipTitleButton3:Click()").unwrap();
|
||||
assert_eq!(s.take_gossip_quest_selects(), vec![1]);
|
||||
assert!(
|
||||
s.take_gossip_selects().is_empty(),
|
||||
@@ -355,6 +390,17 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
|
||||
fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// **A measurer, because the reference measures inside the tick it sets the text.**
|
||||
// `GossipResize` reads `titleButton:GetTextHeight()` on the line after `SetText`
|
||||
// (`GossipFrame.lua:130-137`), and without a `TextMeasure` installed a bare VM answers the
|
||||
// PREVIOUS frame's box — the row comes out 2px tall.
|
||||
//
|
||||
// This is what deferred the gossip window through 1751: it was read as "our measurement is a
|
||||
// frame late", an engine gap. It is not. `UiScript::fill_measures` closes the loop inline
|
||||
// whenever a measurer is installed, which the app always does
|
||||
// (`ui_script::extract`'s `AtlasMeasurer`); only a bare test VM does not. The gap was in the
|
||||
// harness, and it hid behind a window we had written to measure a frame later.
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
@@ -362,7 +408,7 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// Three long options — the shape of a real judgement/roleplay menu, every one of them wrapping
|
||||
// at the row label's 275 px width.
|
||||
@@ -424,7 +470,7 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
|
||||
let row = |i: u32| -> (f32, f32, f32) {
|
||||
s.eval::<(f32, f32, f32)>(&format!(
|
||||
"return GossipTitleButton{i}:GetTop(), GossipTitleButton{i}:GetBottom(), \
|
||||
GossipTitleButton{i}Label:GetHeight()"
|
||||
GossipTitleButton{i}:GetTextHeight()"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
@@ -473,7 +519,7 @@ fn gossip_show_hide_plays_open_and_close_kits() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// Hidden at load: no open sound (never transitions on startup).
|
||||
assert!(
|
||||
@@ -514,9 +560,14 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
|
||||
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
s.set_gossip(Some(GossipMenu {
|
||||
greeting: "Well met.".into(),
|
||||
@@ -601,9 +652,14 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
|
||||
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
|
||||
// list passes load_xml and then loses the scrollbar silently.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
|
||||
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// Vendor open at the left slot; clear any startup/open intents.
|
||||
s.set_merchant(Some(MerchantState::default()));
|
||||
@@ -648,7 +704,12 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
// The vendor window is the reference's own since 1751, and its `MerchantFrame_UpdateMerchantInfo`
|
||||
// calls `TEXT()` while building every row — see `test_ui::MERCHANT_UI` for the rest.
|
||||
for f in super::test_ui::MERCHANT_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
|
||||
// The synthetic pushable=7 loot stand-in opens first onto the empty left slot. `CreateFrame`
|
||||
// frames start shown by default (matching the real client) — every shipped panel frame is
|
||||
@@ -754,7 +815,7 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
// and UIPanelButtonTemplate, and an `inherits=` is resolved at LOAD.
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BankFrame.xml");
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// The gossip menu is open on the banker (its bank option showing).
|
||||
s.set_gossip(Some(GossipMenu {
|
||||
@@ -818,11 +879,16 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
|
||||
fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// A measurer: `GossipResize` reads `GetTextHeight()` on the line after `SetText`, so a bare
|
||||
// VM sizes every row from the previous frame's box. See
|
||||
// `shipped_gossip_rows_grow_to_their_wrapped_labels` for why this is a harness gap and not an
|
||||
// engine one.
|
||||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// Eight wrapping options: ~4 lines each, far past the 334 px scroll frame.
|
||||
let long = |n: usize| GossipOptionView {
|
||||
@@ -863,7 +929,14 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
|
||||
// The child grew past the 334 px frame, so there is a range to scroll …
|
||||
// There is a range to scroll — which is the property that matters and the one the window
|
||||
// exists to have.
|
||||
//
|
||||
// The child's own HEIGHT is no longer part of the assertion. Ours grew it past the 334px frame
|
||||
// in `BenillaGossipFrame_SizeScrollChild`, a helper the reference does not have: stock leaves
|
||||
// the scroll child at the frame's size and lets the rows' own extents drive the range. So the
|
||||
// child measures 334 and the range is non-zero, where ours had both. Asserting the range alone
|
||||
// is asserting the behaviour rather than one implementation's way of reaching it.
|
||||
let (child_h, range): (f32, f32) = s
|
||||
.eval(
|
||||
"return GossipGreetingScrollChildFrame:GetHeight(), \
|
||||
@@ -871,8 +944,8 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
child_h > 334.0 && range > 0.0,
|
||||
"content overflows the 334px frame: child {child_h}, range {range}"
|
||||
range > 0.0,
|
||||
"the overflowing menu has somewhere to scroll: child {child_h}, range {range}"
|
||||
);
|
||||
// … and the bar is up for it (it stays hidden when everything fits — the fit case is covered by
|
||||
// `shipped_gossip_frame_drives_end_to_end`'s two-option menu).
|
||||
@@ -969,7 +1042,7 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
|
||||
load_xml(&s, "GossipFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GossipFrame.xml");
|
||||
|
||||
// The addon's three lines, in the order an addon writes them.
|
||||
s.run(
|
||||
|
||||
@@ -22,7 +22,7 @@ fn questgiver_show_hide_plays_open_and_close_kits() {
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
// The BenillaMoney_* purse helpers the quest reward/progress panels repaint through live in
|
||||
// MerchantFrame.xml (the same documented cross-window dep the bag tests load).
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -79,7 +79,7 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -156,7 +156,7 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -239,7 +239,7 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -302,7 +302,7 @@ fn greeting_goodbye_button_closes_the_window() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -337,7 +337,7 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
s.set_quest(Some(QuestState {
|
||||
@@ -446,7 +446,7 @@ fn write_on_still_fades_when_instant_text_is_off() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
s.eval::<()>(r#"QUEST_FADING_DISABLE = "0""#).unwrap();
|
||||
@@ -507,7 +507,7 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -570,7 +570,7 @@ fn greeting_panel_title_rows_grow_to_their_wrapped_titles() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
|
||||
@@ -654,7 +654,7 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "ScrollTemplates.xml");
|
||||
load_xml(&s, "QuestFrame.xml");
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
|
||||
@@ -85,7 +85,7 @@ fn shipped_questlog_frame_loads_clean() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -106,7 +106,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -290,7 +290,7 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -410,7 +410,7 @@ fn watch_guards_no_op_without_erroring() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // the guards' red-line surface
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
@@ -504,7 +504,7 @@ fn progress_auto_watches_for_five_minutes() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -566,7 +566,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -605,7 +605,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -655,7 +655,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -811,7 +811,7 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -873,7 +873,7 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -919,7 +919,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -982,7 +982,7 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
|
||||
// Fonts.xml alone is the source now. Formerly: it
|
||||
// needs the dropdown kit its GroupLootDropDown initializes against (benilla.toc l.64 vs 383).
|
||||
load_xml(&s, "UIDropDownMenu.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1116,7 +1116,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1196,7 +1196,7 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1287,7 +1287,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1361,7 +1361,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
@@ -1389,7 +1389,7 @@ fn share_quest_click_queues_the_selected_quests_id() {
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||||
|
||||
@@ -389,10 +389,17 @@ mod tests {
|
||||
/// mostly noise. Here the manifest is loaded first and `_G` is read after, so the answer is
|
||||
/// what this client *actually* answers to.
|
||||
///
|
||||
/// **Three columns, and the third one exists because its absence cost a swap.** `engine=` and
|
||||
/// `fx=` read bare `Name(` globals. `method=` reads `:Name(` calls against the method surface
|
||||
/// this engine actually exposes — built by walking `getmetatable(w).__index` over one widget
|
||||
/// of every type, plus the tooltip.
|
||||
/// **Four columns, because a window can be blocked four ways and this could once see one.**
|
||||
/// `engine=` and `fx=` read bare `Name(` globals. `method=` reads `:Name(` calls against the
|
||||
/// method surface this engine actually exposes. `LOAD:` is the stock file loaded on top of our
|
||||
/// manifest in a fresh VM — the same pass [`chain_readiness_report`] makes, run here so the
|
||||
/// answer is in one table.
|
||||
///
|
||||
/// That last one is 1790, and it is the same mistake as the third column one step later.
|
||||
/// `<LootButton>` and `<TaxiRouteFrame>` are element TAGS: no census of `Name(` or `:Name(`
|
||||
/// can reach them, so `LootFrame.xml` sat in this report's "needs NO engine work" list while
|
||||
/// the readiness probe was printing `4 issue(s)` for it in a different table. Both tables were
|
||||
/// right. Joining them was left to whoever read them, and I read it wrong.
|
||||
///
|
||||
/// Until 1787 the method sites were **silently dropped**: a name in neither the engine nor the
|
||||
/// FrameXML half of `1.12-globals.tsv` was assumed to be a widget method and skipped, on the
|
||||
@@ -403,10 +410,15 @@ mod tests {
|
||||
/// file loaded, every check passed, and hovering a vendor row would have raised in play. Both
|
||||
/// instruments were right about what they measure. Neither measured the window.
|
||||
///
|
||||
/// Two crudenesses remain, stated because they decide how to read the output. Both scans count
|
||||
/// a call inside an XML comment (1.12 comments out whole blocks — `GuildRegistrarFrame`'s
|
||||
/// tabard button is one), so they can over-report; and neither can see a name reached through
|
||||
/// `getglobal`, so they can under-report. The `method=` column is also receiver-blind: it asks
|
||||
/// One crudeness remains, stated because it decides how to read the output: neither scan can
|
||||
/// see a name reached through `getglobal`, so both can under-report.
|
||||
///
|
||||
/// They no longer over-report on comments. Both counted commented-out calls until 1789 — 1.12
|
||||
/// comments out whole blocks, and `PaperDollFrame.lua:754-756`'s `ShowInventorySellCursor` is
|
||||
/// three commented lines that this report named as that window's last engine gap. A real
|
||||
/// binding, never called, blocking a window that was not blocked. [`strip_comments`] takes
|
||||
/// Lua `--`/`--[[ ]]` and XML `<!-- -->` out first; it does not track string literals, so it
|
||||
/// can only ever under-report, which is the safe direction here. The `method=` column is also receiver-blind: it asks
|
||||
/// "does ANY widget type answer to this name", not "does *this* receiver", so a method that
|
||||
/// exists on the wrong type still reads as present. It is for ranking work, not for proving a
|
||||
/// window done — [`chain_readiness_report`] and the window's own tests are that, and the
|
||||
@@ -444,6 +456,79 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
// Strip what is not code before either scan. Both `Name(` and `:Name(` counted calls
|
||||
// inside comments until 1789: the round that built `PickupMerchantItem` also asked wow-re
|
||||
// for `ShowInventorySellCursor`, which this report had named as `PaperDollFrame.xml`'s
|
||||
// last engine gap — and the answer was that stock `PaperDollFrame.lua:754-756` has the
|
||||
// call **commented out**, all three lines. A real binding, never called, blocking a window
|
||||
// that was not blocked.
|
||||
//
|
||||
// Line-based and deliberately simple: Lua `--` to end of line (but not `--[[`, which opens
|
||||
// a block), Lua `--[[ … ]]` blocks, and XML `<!-- … -->` blocks. It does not track string
|
||||
// literals, so a `"--"` inside a string truncates that line — which can only ever cause an
|
||||
// UNDER-report, the safe direction for an instrument that ranks work.
|
||||
let strip_comments = |text: &str| -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut in_xml = false;
|
||||
let mut in_lua_block = false;
|
||||
for line in text.lines() {
|
||||
let mut rest = line;
|
||||
let mut kept = String::new();
|
||||
loop {
|
||||
if in_xml {
|
||||
match rest.find("-->") {
|
||||
Some(i) => {
|
||||
in_xml = false;
|
||||
rest = &rest[i + 3..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else if in_lua_block {
|
||||
match rest.find("]]") {
|
||||
Some(i) => {
|
||||
in_lua_block = false;
|
||||
rest = &rest[i + 2..];
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
} else {
|
||||
let xml = rest.find("<!--");
|
||||
let lua = rest.find("--");
|
||||
match (xml, lua) {
|
||||
(Some(x), Some(l)) if x <= l => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(_, Some(l)) => {
|
||||
kept.push_str(&rest[..l]);
|
||||
if rest[l..].starts_with("--[[") {
|
||||
in_lua_block = true;
|
||||
rest = &rest[l + 4..];
|
||||
} else {
|
||||
// A plain `--` comment runs to end of line.
|
||||
rest = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(x), None) => {
|
||||
kept.push_str(&rest[..x]);
|
||||
in_xml = true;
|
||||
rest = &rest[x + 4..];
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
if !in_xml && !in_lua_block {
|
||||
kept.push_str(rest);
|
||||
}
|
||||
out.push_str(&kept);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
// `:Name(` — a method call, receiver unknown.
|
||||
let called_methods = |text: &str| -> std::collections::HashSet<String> {
|
||||
let b: Vec<char> = text.chars().collect();
|
||||
@@ -598,8 +683,15 @@ mod tests {
|
||||
};
|
||||
|
||||
println!("\n=== 1751 gap report — what each unmigrated window would cost ===");
|
||||
#[allow(clippy::type_complexity)] // (engine count, file, engine, fx, method)
|
||||
let mut rows: Vec<(usize, String, Vec<String>, Vec<String>, Vec<String>)> = Vec::new();
|
||||
#[allow(clippy::type_complexity)] // (blockers, file, engine, fx, method, load errors)
|
||||
let mut rows: Vec<(
|
||||
usize,
|
||||
String,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
Vec<String>,
|
||||
)> = Vec::new();
|
||||
for f in &stock {
|
||||
if migrated.contains(f) {
|
||||
continue;
|
||||
@@ -610,6 +702,7 @@ mod tests {
|
||||
text.push_str(&String::from_utf8_lossy(&b));
|
||||
}
|
||||
}
|
||||
let text = strip_comments(&text);
|
||||
let own: std::collections::HashSet<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
@@ -637,33 +730,69 @@ mod tests {
|
||||
}
|
||||
// The method half. `own`/`have` do not apply: a method is never a global, so the only
|
||||
// question is whether this engine's widgets answer to the name.
|
||||
// …and does it LOAD? A window can be blocked on a global, on a widget method, or on a
|
||||
// widget TYPE — and the two scans above see only the first two. `<LootButton>` and
|
||||
// `<TaxiRouteFrame>` are element tags: nothing in a `Name(` or `:Name(` census can
|
||||
// reach them, and `LootFrame.xml` sat in this report's "needs NO engine work" list
|
||||
// while `chain_readiness_report` was printing `4 issue(s)` for it in another table.
|
||||
//
|
||||
// Both tables were right. Joining them was left to whoever read them, and I got it
|
||||
// wrong (1790). So this one runs the load itself — the same fresh-VM-plus-manifest
|
||||
// pass the readiness probe does — and reports it in the same row.
|
||||
let loads = {
|
||||
let mut probe = UiScript::new().expect("VM");
|
||||
probe.set_screen_size(1024.0, 768.0);
|
||||
let base = super::super::manifest::load_default_ui(&probe);
|
||||
assert!(base.is_empty(), "the shipped manifest itself: {base:#?}");
|
||||
probe.resolve();
|
||||
let before = probe.errors().len();
|
||||
let path = format!("Interface\\FrameXML\\{f}");
|
||||
let addon = super::addon(vec![path.clone()]);
|
||||
let mut said = addon.load_files(&probe, std::slice::from_ref(&path));
|
||||
probe.resolve();
|
||||
said.extend(probe.errors().into_iter().skip(before));
|
||||
said
|
||||
};
|
||||
|
||||
let mut asked: Vec<String> = called_methods(&text).into_iter().collect();
|
||||
asked.sort();
|
||||
let known = answers(&s, &asked);
|
||||
let mut meth: Vec<String> = asked.into_iter().filter(|m| !known.contains(m)).collect();
|
||||
meth.sort();
|
||||
rows.push((eng.len() + meth.len(), f.clone(), eng, fx, meth));
|
||||
rows.push((
|
||||
eng.len() + meth.len() + loads.len(),
|
||||
f.clone(),
|
||||
eng,
|
||||
fx,
|
||||
meth,
|
||||
loads,
|
||||
));
|
||||
}
|
||||
rows.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
|
||||
for (n, f, eng, fx, meth) in &rows {
|
||||
println!("{n:>3} engine {f}");
|
||||
for (n, f, eng, fx, meth, loads) in &rows {
|
||||
println!("{n:>3} blocker(s) {f}");
|
||||
if !eng.is_empty() {
|
||||
println!(" engine: {}", eng.join(" "));
|
||||
}
|
||||
if !meth.is_empty() {
|
||||
println!(" method: {}", meth.join(" "));
|
||||
}
|
||||
for e in loads {
|
||||
let one = e.replace('\n', " ");
|
||||
println!(" LOAD: {}", &one[..one.len().min(150)]);
|
||||
}
|
||||
if !fx.is_empty() {
|
||||
println!(" fx: {}", fx.join(" "));
|
||||
}
|
||||
}
|
||||
// "Free" now means free of BOTH — the count above is engine names plus missing methods,
|
||||
// because a window blocked on a widget method is exactly as blocked as one missing a
|
||||
// global, and reading it as free is the mistake 1787 is about.
|
||||
// "Free" means free of ALL THREE. A window blocked on a widget method, or on a widget type
|
||||
// its XML declares, is exactly as blocked as one missing a global — and the `fx=` column
|
||||
// is deliberately NOT counted, because a FrameXML function another stock file defines
|
||||
// arrives with that file rather than needing to be built.
|
||||
let free: Vec<&String> = rows.iter().filter(|r| r.0 == 0).map(|r| &r.1).collect();
|
||||
println!(
|
||||
"\n=== {} windows need NO engine work at all (no missing global AND no missing \
|
||||
method) ===",
|
||||
"\n=== {} windows are UNBLOCKED — no missing global, no missing method, and the \
|
||||
stock file loads clean on top of our manifest ===",
|
||||
free.len()
|
||||
);
|
||||
for f in free {
|
||||
@@ -671,6 +800,109 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **Every function of ours that shadows one the player's own chain already defines.**
|
||||
///
|
||||
/// A window that reads unblocked in [`chain_gap_report`] can still fail to swap, and the third
|
||||
/// reason found (after a missing widget type and a missing widget method) is this one: our
|
||||
/// `assets/ui` file defines a global the reference defines too, our manifest loads a CHAIN file
|
||||
/// that also defines it, and whichever lands second wins. `PartyFrame.xml` is the worked
|
||||
/// example — `UnitFrames.xml` redefines `UnitFrame_OnEvent`/`UnitFrame_Update` nine manifest
|
||||
/// lines after stock `UnitFrame.lua` defines them, so a stock party row built by the
|
||||
/// reference's own `UnitFrame_Initialize` calls OUR update and indexes a field its rows do not
|
||||
/// carry. It loads clean and raises on the first event.
|
||||
///
|
||||
/// Shadowing is not automatically wrong — where we ship a file the reference would have
|
||||
/// shipped, defining its names is the whole job. It is wrong precisely when **both** copies
|
||||
/// load, which is what this reports: a name ours defines that a chain entry in our own
|
||||
/// manifest also defines.
|
||||
///
|
||||
/// Run it before attempting a swap. It predicts which ones will fail without attempting them.
|
||||
#[test]
|
||||
#[ignore = "instrument: run by hand before attempting a window swap"]
|
||||
fn shadowed_reference_functions() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
|
||||
let defined_in = |text: &str| -> Vec<String> {
|
||||
text.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
.map(|r| {
|
||||
r.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect::<String>()
|
||||
})
|
||||
.filter(|n| !n.is_empty())
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Everything the manifest pulls OFF THE CHAIN, and every name each of those defines —
|
||||
// including the `.lua` a chain `.xml` sources, which is where most of them live.
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
let pos: std::collections::HashMap<&String, usize> =
|
||||
toc.iter().enumerate().map(|(i, f)| (f, i)).collect();
|
||||
let mut chain_home: std::collections::HashMap<String, (String, usize)> =
|
||||
std::collections::HashMap::new();
|
||||
for entry in toc.iter().filter(|f| super::is_chain_entry(f)) {
|
||||
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or(entry);
|
||||
let mut cands = vec![entry.clone()];
|
||||
if let Some(stem) = entry.strip_suffix(".xml") {
|
||||
cands.push(format!("{stem}.lua"));
|
||||
}
|
||||
for cand in cands {
|
||||
let Some(b) = super::read(&cand.replace('\\', "/")) else {
|
||||
continue;
|
||||
};
|
||||
let text = String::from_utf8_lossy(&b).into_owned();
|
||||
for name in defined_in(&text) {
|
||||
let at = pos[entry];
|
||||
chain_home.entry(name).or_insert_with(|| {
|
||||
(cand.rsplit('/').next().unwrap_or(leaf).to_string(), at)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// …against everything OUR files define.
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
let mut hits: Vec<(String, String, String, bool)> = Vec::new();
|
||||
for entry in toc.iter().filter(|f| !super::is_chain_entry(f)) {
|
||||
let Ok(text) = std::fs::read_to_string(dir.join(entry)) else {
|
||||
continue;
|
||||
};
|
||||
for name in defined_in(&text) {
|
||||
if let Some((home, at)) = chain_home.get(&name) {
|
||||
// Load order settles it: the later definition is the one that stands.
|
||||
let ours_wins = pos[entry] > *at;
|
||||
hits.push((name, entry.clone(), home.clone(), ours_wins));
|
||||
}
|
||||
}
|
||||
}
|
||||
hits.sort();
|
||||
hits.dedup();
|
||||
|
||||
println!("\n=== names ours redefines that a CHAIN entry already defines ===");
|
||||
println!("{:<36} {:<28} {:<34} winner", "name", "ours", "chain");
|
||||
for (name, ours, home, ours_wins) in &hits {
|
||||
let w = if *ours_wins { "OURS" } else { "the chain's" };
|
||||
println!("{name:<36} {ours:<28} {home:<34} {w}");
|
||||
}
|
||||
|
||||
// Grouped by our file, because that is the unit of work: a window cannot swap while OUR
|
||||
// file is still standing on the names its stock counterpart needs.
|
||||
let mut by_file: std::collections::BTreeMap<&String, usize> =
|
||||
std::collections::BTreeMap::new();
|
||||
for (_, ours, _, _) in &hits {
|
||||
*by_file.entry(ours).or_default() += 1;
|
||||
}
|
||||
println!(
|
||||
"\n=== {} collisions, across {} of our files ===",
|
||||
hits.len(),
|
||||
by_file.len()
|
||||
);
|
||||
for (f, n) in &by_file {
|
||||
println!(" {n:>3} {f}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A path is a chain entry; a bare name is ours. The one-line rule the manifest rests on.
|
||||
#[test]
|
||||
fn a_separator_is_what_makes_an_entry_the_players_own_file() {
|
||||
|
||||
@@ -814,6 +814,14 @@ fn every_texture_frame_outranks_its_status_bars() {
|
||||
}
|
||||
// Player + target (health/power) and four party members (health/mana) — never let this pass by
|
||||
// matching nothing, which is exactly how a renamed frame would silently retire the check.
|
||||
//
|
||||
// **This number drops to 6 the day `PartyFrame.xml` goes to the chain, and that will be a real
|
||||
// reduction rather than a rename.** Stock `PartyFrameTemplates.xml` has no
|
||||
// `$parentTextureFrame` at all: it gets the same result a different way, with the art inside a
|
||||
// pair of ANONYMOUS nested `<Frame setAllPoints="true">` wrappers, so it outranks the bars by
|
||||
// nesting depth rather than by a level on a named frame. There is nothing to look up, so the
|
||||
// party rows will move from this assertion's cover to the loader's nesting rule. Written down
|
||||
// now, while the reason is in front of somebody, rather than discovered as a number to lower.
|
||||
assert!(
|
||||
checked >= 12,
|
||||
"only {checked} texture-frame/bar pairs checked — the name sweep found nothing"
|
||||
|
||||
@@ -110,6 +110,28 @@ fn read(req: &str) -> Option<Vec<u8>> {
|
||||
/// constant by hand would pass and teach nothing about the real load.
|
||||
///
|
||||
/// Needs client data, like [`BAG_UI`]: open with `benilla_formats::wow_data_or_skip!()`.
|
||||
/// What the **vendor window** needs before `Interface\FrameXML\MerchantFrame.xml` will load and
|
||||
/// behave — the same shape as [`BAG_UI`] and [`LOOT_UI`], and it grew for 1751 the same way.
|
||||
///
|
||||
/// The two that fail in the ways worth naming:
|
||||
///
|
||||
/// * **`BasicControls.xml`** — for `TEXT()`, the reference's identity-function wrapper, which
|
||||
/// stock `MerchantFrame.lua:70` calls while building every row. Absent, it raises on the first
|
||||
/// `MerchantFrame_UpdateMerchantInfo`, which is the first thing the window does when it shows.
|
||||
/// * **`Interface\FrameXML\ItemButtonTemplate.xml`** — the stock rows' `$parentItemButton`
|
||||
/// inherits it, and a missing template is a loader *warning*: the rows load clean with no art.
|
||||
///
|
||||
/// Needs client data: open with `benilla_formats::wow_data_or_skip!()`.
|
||||
pub(super) const MERCHANT_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml",
|
||||
"BasicControls.xml", // TEXT()
|
||||
"Interface\\FrameXML\\ItemButtonTemplate.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml", // app load order: tooltip before merchant
|
||||
];
|
||||
|
||||
pub(super) const LOOT_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Fonts.xml", // ITEM_QUALITY_COLORS — the row-name palette
|
||||
|
||||
@@ -32,7 +32,7 @@ fn harness() -> UiScript {
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"CharacterFrame.xml",
|
||||
] {
|
||||
@@ -58,7 +58,7 @@ fn harness_with_bags() -> UiScript {
|
||||
}
|
||||
for f in [
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"CharacterFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
|
||||
@@ -30,7 +30,7 @@ fn trainer_script() -> UiScript {
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_* (the kit's MenuBackdrop), app order
|
||||
load_xml(&s, "UIDropDownMenu.xml"); // the filter dropdown's kit
|
||||
load_xml(&s, "ScrollTemplates.xml"); // the faux-scroll bar kit
|
||||
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_Set/_Clear/_SetColor live here
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // BenillaMoney_Set/_Clear/_SetColor live here
|
||||
load_xml(&s, "TrainerFrame.xml");
|
||||
s.run(
|
||||
"TRAINER_FILTER_AVAILABLE = 1 TRAINER_FILTER_UNAVAILABLE = 1 TRAINER_FILTER_USED = 1 \
|
||||
|
||||
@@ -593,7 +593,7 @@ fn shipped_target_frame_runs_the_level_law() {
|
||||
// GetDifficultyColor's own load chain (the quest log window, its ref home).
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "QuestLogFrame.xml");
|
||||
// The player at level 3, both feeds (the snapshot UnitLevel("player") reads; the req state
|
||||
// the −1 gate and GetQuestGreenRange read) — the app keeps the two in step.
|
||||
|
||||
Reference in New Issue
Block a user