the inspect and talent windows are the reference's own

- the inspect and talent windows are the reference's own: `Blizzard_InspectUI` and `Blizzard_TalentUI` are LoadOnDemand addons whose real `.xml`/`.lua` live inside `patch.MPQ` — the loose `Interface\AddOns` folder holds only a `.pub` signature, which three notes in this tree had read as "packed, no source to migrate from", one wrong inference that kept five windows on hand-written transcriptions; 2300 lines of ours out, no engine work needed, and three things our talent transcription had quietly invented fell out (there was no level-10 gate, every tree scrolled where a shallow one does not, and a nil `TalentMicroButton` threw out of OnShow before the window painted)
- a nil unit token RAISES in 53 of 83 bindings: wow-re censused the registrar table at `0x850438` — `UnitName(nil)` raises and `UnitIsVisible(nil)` is quiet, one line apart in stock files, so there is no rule to derive, the table is the rule; ours answered a default and our own test had pinned it; both token positions of the seven two-token predicates are gated, `GetRaidTargetIndex` too, and `UnitIsAFK`/`UnitIsDND` are confirmed not to exist in 5875 at all (zero occurrences under a positive control) — we invented them, and they stay deliberately ungated
- `EditBox:SetNumber` did not exist (the only chain file calling it is `MoneyInputFrame.lua`, on the chain since the money kit): it is byte-identical to `SetText`, formats through `%.14g` in MSVC's CRT (`1e+020`, `1.#INF`), and `OnTextChanged` never fires from an edit — it fires from the dirty-word drain inside the box's own OnUpdate, coalesced to one per box per frame, where ours fired synchronously and `MoneyInputFrame_SetCopper` raised on it; `PanelTemplates_TabResize`'s `tab` argument is optional as the reference's is (stock macro tabs call it with `0`)
- the skull or star on your CURRENT target never appeared or cleared until you re-targeted — `RAID_TARGET_UPDATE` was never fired, and stock `TargetFrame.lua` hangs its repaint off it; a raid-to-party conversion now empties the mark board as `0x4ba550` does; and the group-loot roll popups were already on the chain inside stock `LootFrame.xml` — our 546-line `GroupLootFrame.xml` had been shadowing all four of them

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-02 17:06:18 +07:00
co-authored by Claude Fable 5.1
parent 7308046d60
commit 2bebb94471
41 changed files with 1400 additions and 3134 deletions
@@ -323,7 +323,12 @@ fn typing_the_confirm_word_enables_okay_and_untyping_it_disables_again() {
let mut s = setup();
drop_in_world(&mut s, 871, "Flurry Axe", 4);
// OKAY is enabled by the box's `OnTextChanged`, and that fire is deferred to the drain
// (decision 1831) — so what this answers is the button state the LAST DRAINED text produced,
// which is the state a player ever sees. Ticking here rather than after each write keeps the
// test reading as the sequence of edits it is about.
let enabled = |s: &mut UiScript| {
s.tick(0.0);
s.eval::<i64>("return StaticPopup1Button1:IsEnabled()")
.unwrap()
== 1
@@ -382,6 +387,7 @@ fn enter_in_the_box_destroys_only_once_okay_is_enabled() {
assert!(s.take_container_destroys().is_empty());
s.run(r#"StaticPopup1EditBox:SetText("DELETE")"#).unwrap();
s.tick(0.0); // the write only marks the box — the drain is what enables OKAY (1831)
assert!(s.key_input("ENTER"));
assert!(
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
@@ -7,35 +7,13 @@ use benilla_ui::script::{
DressUpIntent, ExtractedQuad, LootRollEntry, LootRollsState, QuadContent, ScriptValue, UiScript,
};
use super::test_ui::load_ui as load_xml;
use super::test_ui::{load_ui as load_xml, load_ui_no_warnings as load_xml_no_warnings};
/// Like [`load_xml`], but also demands zero loader WARNINGS — the bar this file's own assignment
/// set for `GroupLootFrame.xml` itself (a stale "unknown template" warning, e.g. the kind
/// LootFrame.xml's now-obsolete `GameFontNormalSmall` caveat would have produced, is exactly what
/// this catches). Not used for the prerequisite files below: Fonts/UiPanels/GameTooltip aren't this
/// assignment's to police and may carry warnings of their own.
fn load_xml_no_warnings(s: &UiScript, file: &str) -> usize {
let text = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("assets/ui")
.join(file),
)
.unwrap();
let doc = benilla_ui::framexml::parse(&text).unwrap();
let report = benilla_ui::loader::load(s, &doc, &|_| None);
assert!(
report.errors.is_empty(),
"{file}: loader errors: {:?}",
report.errors
);
assert!(
report.warnings.is_empty(),
"{file}: loader warnings: {:?}",
report.warnings
);
report.frames
}
/// The centre of the first texture quad whose path contains `needle` (a button's art), for clicking
/// it — the `loot_tests.rs` `icon_center` idiom.
fn quad_center(quads: &[ExtractedQuad], needle: &str) -> (f32, f32) {
@@ -61,10 +39,42 @@ fn text_color(quads: &[ExtractedQuad], t: &str) -> Option<[f32; 4]> {
})?
}
/// The four roll popups, off the player's chain.
///
/// They are declared in stock `LootFrame.xml` — `GroupLootFrameTemplate` plus `GroupLootFrame1..4`
/// — and that file has been on the manifest since the loot window migrated. Our own
/// `GroupLootFrame.xml` re-declared all five names on top of it and won by load order, so what
/// these tests exercised was our copy shadowing the chain's (decision 1838).
///
/// `UIParent.xml` comes with it because the `START_LOOT_ROLL` router lives there now — the
/// reference's own slot for it — where our file used to carry a dedicated hidden driver frame.
fn load_group_loot(s: &UiScript) {
load_xml(s, "UIParent.xml");
// `LootFrame.xml` brings the whole loot window, and its `GroupLootDropDown` calls
// `UIDropDownMenu_Initialize` from its own OnLoad — the dropdown kit is on the chain too.
load_xml(s, r"Interface\FrameXML\UIDropDownMenu.xml");
// `MAX_PARTY_MEMBERS` — `GroupLootDropDown_OnLoad` counts party rows, and the constant lives
// in stock `PartyMemberFrame.lua`. A real cross-file dependency the reference has too (its own
// toc loads PartyFrame well before LootFrame); the `.lua` alone is the minimal way to satisfy
// it here, because its whole top level is four constants and some function definitions — no
// frames, no side effects — where `PartyFrame.xml` would drag in the entire unit-frame cluster
// for one number.
load_xml(s, r"Interface\FrameXML\PartyMemberFrame.lua");
load_xml(s, r"Interface\FrameXML\LootFrame.xml");
}
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The loot window's own labels (`ITEMS`, `PREV`, `NEXT`) are GlobalStrings keys, and the
// loader warns on a key with no global behind it rather than failing — which is exactly the
// kind of warning `load_ui_no_warnings` is here to catch (decision 1838).
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "Fonts.xml"); // ITEM_QUALITY_COLORS + GameFontNormalSmall
// The loot window's slots inherit it — the same dependency the inspect window needed (1832).
load_xml(&s, r"Interface\FrameXML\ItemButtonTemplate.xml");
// `UIPanelCloseButton`, which the loot window's four close buttons inherit.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml"); // PASS/NEED/GREED + item hovers
@@ -131,13 +141,19 @@ fn rolls() -> LootRollsState {
/// and start hidden.
#[test]
fn shipped_group_loot_frame_loads_clean_and_starts_hidden() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
// Per instance: the frame + IconFrame + PassButton + RollButton + GreedButton + Timer (6),
// times 4, plus the one BenillaGroupLootFrameDriver event-listener frame.
assert_eq!(
load_xml_no_warnings(&s, "GroupLootFrame.xml"),
4 * 6 + 1,
"4 x (frame + icon + pass + need + greed + timer) + the START_LOOT_ROLL driver"
load_xml(&s, "UIParent.xml");
load_xml(&s, r"Interface\FrameXML\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\PartyMemberFrame.lua");
// The four roll popups arrive INSIDE the chain's loot window, so there is no exact frame count
// to assert any more: this used to read `4 * 6 + 1` — six regions per instance plus our own
// `BenillaGroupLootFrameDriver` — and both halves of that number were ours (decision 1838).
// What survives is the assignment that mattered: the file loads with **no warning of any
// kind**, which is what `load_ui_no_warnings` is for.
assert!(
load_xml_no_warnings(&s, r"Interface\FrameXML\LootFrame.xml") > 4,
"the loot window brought its frames"
);
for i in 1..=4 {
@@ -156,8 +172,9 @@ fn shipped_group_loot_frame_loads_clean_and_starts_hidden() {
/// click wiring end-to-end through `RollOnLoot`.
#[test]
fn start_loot_roll_claims_frames_in_order_and_paints_the_roll() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_xml(&s, "GroupLootFrame.xml");
load_group_loot(&s);
s.set_loot_rolls(rolls());
// Roll 7 (BoP, Epic) claims frame 1.
@@ -258,8 +275,9 @@ fn start_loot_roll_claims_frames_in_order_and_paints_the_roll() {
/// binds an epic to you on a single click. The five-line path below is the whole correction.
#[test]
fn the_bop_confirm_popup_lands_the_withheld_vote() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_xml(&s, "GroupLootFrame.xml");
load_group_loot(&s);
s.set_loot_rolls(rolls());
// The app fires this after draining the seam's confirm queue (`ui_loot_roll::drain_loot_rolls`).
@@ -319,8 +337,9 @@ fn the_bop_confirm_popup_lands_the_withheld_vote() {
/// `CANCEL_LOOT_ROLL` for a rollID hides that frame, and ONLY that one.
#[test]
fn cancel_loot_roll_hides_only_that_frame() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_xml(&s, "GroupLootFrame.xml");
load_group_loot(&s);
s.set_loot_rolls(rolls());
s.fire_event(
@@ -364,8 +383,9 @@ fn cancel_loot_roll_hides_only_that_frame() {
/// than crashing on the nils.
#[test]
fn in_flight_roll_does_not_error_and_falls_back() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_xml(&s, "GroupLootFrame.xml");
load_group_loot(&s);
s.set_loot_rolls(rolls());
s.fire_event(
@@ -382,9 +402,12 @@ fn in_flight_roll_does_not_error_and_falls_back() {
.unwrap());
assert_eq!(s.eval::<i64>("return GroupLootFrame1.rollID").unwrap(), 9);
// Blank name (not an error, not the literal "nil").
// The chain's FontString has NO text until something paints it, where our retired copy
// defaulted to `""` — so this reads Option and expects None (decision 1838).
assert_eq!(
s.eval::<String>("return GroupLootFrame1Name:GetText()")
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
None,
""
);
// No BoP decoration for an unresolved (default-false bindOnPickUp) roll.
@@ -392,13 +415,22 @@ fn in_flight_roll_does_not_error_and_falls_back() {
.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
.unwrap());
// The fallback icon painted rather than an empty/erroring SetTexture(nil).
// **No fallback icon — and that is the reference's behaviour, not a regression to fix here.**
// Our retired file painted `BENILLA_FALLBACK_ICON` (a question mark) whenever the texture was
// nil. The chain's `GroupLootFrame_OnShow` just calls `SetTexture(texture)` with whatever
// `GetLootRollItemInfo` gave it, so an unresolved item leaves the icon empty. The migration
// reverts our embellishment; whether that reads acceptably is a look call, recorded in 1838
// rather than re-authored back in.
s.resolve();
let has_fallback_icon = s.extract().iter().any(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.contains("INV_Misc_QuestionMark"))
});
assert!(has_fallback_icon, "in-flight roll shows the fallback icon");
assert!(
!has_fallback_icon,
"the reference paints no placeholder for an unresolved item"
);
// What DOES still hold: no error, and the dialog is up and usable.
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -415,9 +447,10 @@ fn in_flight_roll_does_not_error_and_falls_back() {
/// identity changes under an open frame — the snapshot finally arriving, or a late item template —
/// `UPDATE_LOOT_ROLL(rollID)` repaints it.
#[test]
fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
fn a_roll_that_opens_before_its_snapshot_stays_blank() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_xml(&s, "GroupLootFrame.xml");
load_group_loot(&s);
// The app's order: the event first, against a model that has no such roll at all.
s.fire_event(
@@ -428,10 +461,12 @@ fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
assert!(s
.eval::<bool>("return GroupLootFrame1:IsVisible()")
.unwrap());
// The chain's FontString has NO text until something paints it, where our retired copy
// defaulted to `""` — so this reads Option and expects None (decision 1838).
assert_eq!(
s.eval::<String>("return GroupLootFrame1Name:GetText()")
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
"",
None,
"nothing to paint yet — this is the state the director saw"
);
@@ -440,33 +475,44 @@ fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
s.fire_event("UPDATE_LOOT_ROLL", vec![ScriptValue::Int(7)]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// **The repaint does not happen, and this is the migration's one real loss.** Our retired file
// split the paint out of `OnShow` precisely so `UPDATE_LOOT_ROLL` could re-enter it; the
// reference has no such seam, because its `GetLootRollItemInfo` reads live C state that is
// already populated when `START_LOOT_ROLL` fires. Ours reads a pushed snapshot, and while
// `feed_loot_rolls` does push it BEFORE firing, the item template can still be in flight.
//
// The fix is app-side ordering, not a Lua shim: hold the roll until its template resolves.
// Decision 1838 carries that, and the engine question an adapter ran into on the way — a
// `Hide()`/`Show()` round trip re-fires `OnShow` from a plain chunk but not from inside an
// event handler.
assert_eq!(
s.eval::<String>("return GroupLootFrame1Name:GetText()")
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
"Staff of Jordan",
"the repaint fills in the name the OnShow could not know"
None,
"the name stays blank: the reference has no repaint path"
);
assert!(
s.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
!s.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
.unwrap(),
"and the BoP gold decoration, which OnShow also painted from nothing"
"and so does the BoP decoration"
);
s.resolve();
let quads = s.extract();
assert!(
quads.iter().any(|q| {
!quads.iter().any(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.contains("INV_Staff_12"))
}),
"and the real icon replaced the fallback"
"and no icon arrives either — same missing repaint, same one cause"
);
// An update for a roll this frame does not hold leaves it alone.
// An update for a roll this frame does not hold still leaves it alone and still does not
// error — the half of this test that survives the migration intact.
s.fire_event("UPDATE_LOOT_ROLL", vec![ScriptValue::Int(999)]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.eval::<String>("return GroupLootFrame1Name:GetText()")
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
"Staff of Jordan"
None
);
}
@@ -482,6 +528,7 @@ fn a_roll_that_opens_before_its_snapshot_repaints_when_it_lands() {
/// 42, pet 42.
#[test]
fn managed_positions_engage_for_the_bare_frame_name() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Fonts.xml");
@@ -492,7 +539,10 @@ fn managed_positions_engage_for_the_bare_frame_name() {
// taking every BenillaGroupLootFrame_* function down with it, not just the popup.
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GroupLootFrame.xml");
// `TOOLTIP_DEFAULT_COLOR`, which the chain's dropdown backdrops read in their OnLoad — the
// dropdown kit rides in with the loot window now (1838), so this bespoke setup needs it too.
load_xml(&s, "GameTooltip.xml");
load_group_loot(&s);
let bottom = |s: &UiScript| s.eval::<f64>("return GroupLootFrame1:GetBottom()").unwrap();
@@ -534,8 +584,9 @@ fn managed_positions_engage_for_the_bare_frame_name() {
/// (or a swallowed one from the dice) is exactly the regression worth catching.
#[test]
fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_xml(&s, "GroupLootFrame.xml");
load_group_loot(&s);
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink, the shared shift-insert helper
load_xml(&s, "DressUpFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
@@ -618,8 +669,9 @@ fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
/// to be nested inside the `IconFrame` **button**, not hung off the template root.
#[test]
fn the_roll_template_carries_the_reference_name_and_the_parts_addons_reach_for() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
load_xml(&s, "GroupLootFrame.xml");
load_group_loot(&s);
// The reference's name resolves — and, since 1253, a miss would RAISE rather than hand back a
// bare frame, so this call failing is this test failing.
@@ -664,6 +716,7 @@ fn the_roll_template_carries_the_reference_name_and_the_parts_addons_reach_for()
/// the eventual GroupLootFrame migration from shipping the loot bug a second time.
#[test]
fn the_stock_group_loot_frame_survives_an_in_flight_roll() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for f in super::test_ui::LOOT_UI {
@@ -367,13 +367,21 @@ fn shown_inspect_honor_page() -> UiScript {
s.run(RANK_GLOBALS).unwrap();
for file in [
"Fonts.xml",
// `TEXT`, the stock level line's formatter.
"BasicControls.xml",
"MoneyFrame.xml",
"UiPanels.xml",
// `InspectUnit`'s home since 1832.
"UIParent.xml",
"GameTooltip.xml",
// Before InspectFrame.xml — the honor page inherits this file's row templates and calls
// Before the inspect addon — its honor page inherits this file's row templates and calls
// its two shared painters, and `inherits=` resolves at load (the manifest's own order).
// The stock inspect slot buttons inherit this (1832).
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"Interface\\FrameXML\\HonorFrame.xml",
"InspectFrame.xml",
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
] {
load_xml(&s, file);
}
@@ -390,8 +398,7 @@ fn shown_inspect_honor_page() -> UiScript {
s.set_unit_reach(HashMap::from([("target".to_string(), reach(16.0))]));
s.set_inspect_honor(Some(inspect_reply()));
s.run(r#"InspectUnit("target")"#).unwrap();
s.run(r#"ToggleInspect("BenillaInspectHonorFrame")"#)
.unwrap();
s.run(r#"ToggleInspect("InspectHonorFrame")"#).unwrap();
s.resolve();
s
}
@@ -403,36 +410,30 @@ fn the_inspect_page_paints_the_reply_it_holds() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_inspect_honor_page();
for (frame, want) in [
("BenillaInspectHonorFrameCurrentHKValue", "17"),
("BenillaInspectHonorFrameCurrentDKValue", "2"),
("BenillaInspectHonorFrameYesterdayHKValue", "41"),
("BenillaInspectHonorFrameYesterdayContributionValue", "640"),
("BenillaInspectHonorFrameThisWeekHKValue", "123"),
("BenillaInspectHonorFrameThisWeekContributionValue", "1250"),
("BenillaInspectHonorFrameLastWeekHKValue", "420"),
("BenillaInspectHonorFrameLastWeekContributionValue", "8431"),
("BenillaInspectHonorFrameLastWeekStandingValue", "57"),
("BenillaInspectHonorFrameLifeTimeHKValue", "3907"),
("BenillaInspectHonorFrameLifeTimeDKValue", "12"),
(
"BenillaInspectHonorFrameLifeTimeRankValue",
"Lieutenant Commander",
),
("InspectHonorFrameCurrentHKValue", "17"),
("InspectHonorFrameCurrentDKValue", "2"),
("InspectHonorFrameYesterdayHKValue", "41"),
("InspectHonorFrameYesterdayContributionValue", "640"),
("InspectHonorFrameThisWeekHKValue", "123"),
("InspectHonorFrameThisWeekContributionValue", "1250"),
("InspectHonorFrameLastWeekHKValue", "420"),
("InspectHonorFrameLastWeekContributionValue", "8431"),
("InspectHonorFrameLastWeekStandingValue", "57"),
("InspectHonorFrameLifeTimeHKValue", "3907"),
("InspectHonorFrameLifeTimeDKValue", "12"),
("InspectHonorFrameLifeTimeRankValue", "Lieutenant Commander"),
] {
assert_eq!(text(&mut s, frame), want, "{frame}");
}
// The rank block: the target's CURRENT rank (12 → "Knight-Captain", badge 08), and the bar off
// the reply's own byte rather than the player's.
assert_eq!(
text(&mut s, "BenillaInspectHonorFrameCurrentPVPTitle"),
text(&mut s, "InspectHonorFrameCurrentPVPTitle"),
"Knight-Captain"
);
assert_eq!(
text(&mut s, "BenillaInspectHonorFrameCurrentPVPRank"),
"(Rank 8)"
);
assert_eq!(text(&mut s, "InspectHonorFrameCurrentPVPRank"), "(Rank 8)");
let bar = s
.eval::<f64>("return BenillaInspectHonorFrameProgressBar:GetValue()")
.eval::<f64>("return InspectHonorFrameProgressBar:GetValue()")
.unwrap();
assert!(
(bar - 191.0 / 255.0).abs() < 1e-6,
@@ -461,7 +462,7 @@ fn the_page_asks_only_when_it_holds_nothing() {
// Cleared — the app drops the reply when the inspected player changes. Re-showing the page
// asks, and paints nothing until an answer lands.
s.set_inspect_honor(None);
s.run(r#"BenillaInspectHonorFrame_OnShow()"#).unwrap();
s.run(r#"InspectHonorFrame_OnShow()"#).unwrap();
s.resolve();
assert_eq!(
s.take_inspect_honor_requests(),
+128 -50
View File
@@ -111,15 +111,39 @@ fn drawn(s: &mut UiScript, needle: &str) -> bool {
fn armed() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// `PLAYER_LEVEL`, the template the stock level line formats through — the reference keeps its
// strings here, and our retired file had the sentence written into it (1832).
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
load_xml(&s, "BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml");
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
// The stock slot buttons inherit `ItemButtonTemplate` — where their `$parentIconTexture`
// comes from, which `InspectPaperDollItemSlotButton_OnLoad` indexes unconditionally. Our own
// slot template declared its icon inline and needed no such file; the manifest has carried
// this entry far above the inspect window all along (decision 1832).
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
load_xml(&s, "InspectFrame.xml");
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
);
s.set_unit("target", Some(target_unit()));
s.set_inspect(Some(inspect_view("target")));
// 4 yards away (d² = 16) — comfortably inside the verified 100.0.
@@ -132,25 +156,51 @@ fn armed() -> UiScript {
fn shipped_inspect_frame_loads_clean() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
// `PLAYER_LEVEL`, the template the stock level line formats through — the reference keeps its
// strings here, and our retired file had the sentence written into it (1832).
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Fonts.xml");
// `TEXT`, which the stock `InspectPaperDollFrame_SetLevel` formats its level line through.
load_xml(&s, "BasicControls.xml");
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
// `InspectUnit` lives here now — the reference's own home for that name, and no longer in the
// window's own file (decision 1832). The manifest's order, and 49 other harnesses'.
load_xml(&s, "UIParent.xml");
load_xml(&s, "GameTooltip.xml");
// Before InspectFrame.xml, and required rather than tidy: this window's honor page inherits
// HonorFrame.xml's five row templates and `inherits=` resolves at LOAD, so without it the
// twelve honor rows materialize bare (decision 1512; the manifest states the same order).
// The stock slot buttons inherit `ItemButtonTemplate` — where their `$parentIconTexture`
// comes from, which `InspectPaperDollItemSlotButton_OnLoad` indexes unconditionally. Our own
// slot template declared its icon inline and needed no such file; the manifest has carried
// this entry far above the inspect window all along (decision 1832).
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\HonorFrame.xml");
load_xml(&s, "InspectFrame.xml");
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\Blizzard_InspectUI.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectPaperDollFrame.xml",
);
load_xml(
&s,
"Interface\\AddOns\\Blizzard_InspectUI\\InspectHonorFrame.xml",
);
// All 19 slots exist and carry their GetInventorySlotInfo id (1..=19, no ammo slot).
// The stock `InspectPaperDollItemSlotButton_OnLoad` puts it on the frame's own ID
// (`this:SetID(id)`); our retired file kept it in an `invSlotId` field of its own (1832).
for (name, id) in [
("BenillaInspectHeadSlot", 1),
("BenillaInspectBackSlot", 15),
("BenillaInspectMainHandSlot", 16),
("BenillaInspectRangedSlot", 18),
("BenillaInspectTabardSlot", 19),
("InspectHeadSlot", 1),
("InspectBackSlot", 15),
("InspectMainHandSlot", 16),
("InspectRangedSlot", 18),
("InspectTabardSlot", 19),
] {
assert_eq!(
s.eval::<i64>(&format!("return {name}.invSlotId")).unwrap(),
s.eval::<i64>(&format!("return {name}:GetID()")).unwrap(),
id,
"{name} inventory slot id"
);
@@ -169,8 +219,7 @@ fn inspect_unit_refuses_out_of_range() {
s.run(r#"InspectUnit("target")"#).unwrap();
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
assert!(
!s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap(),
!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
"out of range: the window must not open"
);
assert!(
@@ -182,8 +231,7 @@ fn inspect_unit_refuses_out_of_range() {
s.set_unit_reach(HashMap::from([("target".to_string(), reach(99.9))]));
s.run(r#"InspectUnit("target")"#).unwrap();
assert!(
s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap(),
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
"in range: the window opens"
);
assert_eq!(
@@ -330,22 +378,18 @@ fn shipped_inspect_frame_drives_end_to_end() {
let mut s = armed();
// Hidden at load, and no sound queued (never transitions on startup).
assert!(!s
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap());
assert!(!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
assert!(s.take_sounds().is_empty());
// The pane's OnLoad set the ref's own default facing (InspectModelFrame_OnLoad, 0.61).
assert!(
(s.inspect_yaw() - 0.61).abs() < 0.0001,
(s.model_pane_facing("InspectModelFrame") - 0.61).abs() < 0.0001,
"default facing 0.61, got {}",
s.inspect_yaw()
s.model_pane_facing("InspectModelFrame")
);
s.run(r#"InspectUnit("target")"#).unwrap();
assert!(s.errors().is_empty(), "open errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap());
assert!(s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
assert_eq!(
s.take_sounds(),
vec![SoundRequest::KitName("igCharacterInfoOpen".into())],
@@ -353,13 +397,13 @@ fn shipped_inspect_frame_drives_end_to_end() {
);
assert_eq!(
s.eval::<String>("return BenillaInspectNameText:GetText()")
s.eval::<String>("return InspectNameText:GetText()")
.unwrap(),
"Thargrim",
"the name line reads UnitName of the inspected token"
);
assert_eq!(
s.eval::<String>("return BenillaInspectLevelText:GetText()")
s.eval::<String>("return InspectLevelText:GetText()")
.unwrap(),
"Level 34 Dwarf Paladin",
"the level line reads the inspected unit, not the player"
@@ -406,21 +450,18 @@ fn shipped_inspect_frame_drives_end_to_end() {
// Rotate: the ref's sign convention (left subtracts, right adds 0.03) onto the booth yaw, with
// the kit each click.
s.run("BenillaInspectModelFrameRotateLeftButton:Click()")
.unwrap();
s.run("InspectModelRotateLeftButton:Click()").unwrap();
assert!(
(s.inspect_yaw() - 0.58).abs() < 0.0001,
(s.model_pane_facing("InspectModelFrame") - 0.58).abs() < 0.0001,
"rotate-left subtracts 0.03, got {}",
s.inspect_yaw()
s.model_pane_facing("InspectModelFrame")
);
s.run("BenillaInspectModelFrameRotateRightButton:Click()")
.unwrap();
s.run("BenillaInspectModelFrameRotateRightButton:Click()")
.unwrap();
s.run("InspectModelRotateRightButton:Click()").unwrap();
s.run("InspectModelRotateRightButton:Click()").unwrap();
assert!(
(s.inspect_yaw() - 0.64).abs() < 0.0001,
(s.model_pane_facing("InspectModelFrame") - 0.64).abs() < 0.0001,
"rotate-right adds 0.03, got {}",
s.inspect_yaw()
s.model_pane_facing("InspectModelFrame")
);
assert_eq!(
s.take_sounds(),
@@ -438,10 +479,8 @@ fn shipped_inspect_frame_drives_end_to_end() {
!s.take_inspect_clear(),
"ClearInspectPlayer not called while open"
);
s.run(r#"HideUIPanel(BenillaInspectFrame)"#).unwrap();
assert!(!s
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap());
s.run(r#"HideUIPanel(InspectFrame)"#).unwrap();
assert!(!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
assert_eq!(
s.take_sounds(),
vec![SoundRequest::KitName("igCharacterInfoClose".into())],
@@ -471,30 +510,69 @@ fn the_active_tab_is_inert_and_toggle_inspect_closes() {
// NB this engine's `IsEnabled` returns a plain bool, not the live API's 1/nil.
assert!(
!s.eval::<bool>("return BenillaInspectFrameTab1:IsEnabled() ~= 0")
!s.eval::<bool>("return InspectFrameTab1:IsEnabled() ~= 0")
.unwrap(),
"the selected tab is disabled (PanelTemplates_SelectTab), so its click is inert"
);
s.run("BenillaInspectFrameTab1:Click()").unwrap();
s.run("InspectFrameTab1:Click()").unwrap();
assert!(
s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap(),
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
"clicking the disabled active tab changes nothing"
);
// The reachable path: ToggleInspect on the page already showing closes the window.
s.run(r#"ToggleInspect("BenillaInspectPaperDollFrame")"#)
.unwrap();
s.run(r#"ToggleInspect("InspectPaperDollFrame")"#).unwrap();
assert!(
!s.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap(),
!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
"ToggleInspect on the showing page closes the window (ref ToggleInspect)"
);
// And again re-opens it, the other branch of the same function.
s.run(r#"ToggleInspect("BenillaInspectPaperDollFrame")"#)
.unwrap();
assert!(s
.eval::<bool>("return BenillaInspectFrame:IsVisible()")
.unwrap());
assert!(s.errors().is_empty(), "tab errors: {:?}", s.errors());
}
/// The other branch of `ToggleInspect` — re-opening after a close — **errors, and that is the
/// reference's own behaviour** (decision 1834 settled what 1832 had to leave open).
///
/// `InspectFrame_OnHide` sets `this.unit = nil`, so a re-open runs `InspectFrame_OnShow` with no
/// token. wow-re censused the whole binding table: `SetPortraitTexture 0x519ef0` gates its unit
/// argument at `0x519fb4` with `lua_isstring` and raises `Usage: SetPortraitTexture(texture,
/// "unit")`, and `luaL_error` does not return — so the reference raises here too, on its own
/// shipped code. The widget clear sits six instructions PAST the gate, so the portrait is not even
/// blanked; it is left exactly as it was.
///
/// In the reference the raise unwinds to the handler's `lua_pcall`, abandons the rest of `OnShow`,
/// and pops the ScriptErrors dialog through `_ERRORMESSAGE`. Ours collects it into `errors` and
/// abandons the handler the same way. So: the window shows, the portrait and name do not update,
/// and there is exactly one error — which is what this now asserts.
///
/// One correction from that census worth keeping: stock 1.12 ships **no** inspect keybind (0 of 234
/// `Binding name=` entries) and no caller of `ToggleInspect` outside the addon's own tab buttons,
/// which are children of the hidden `InspectFrame`. So on the stock UI's own paths this is
/// unreachable — only an addon or a macro gets here. It is still ours to reproduce.
#[test]
fn toggle_inspect_reopens_after_a_close_and_raises_like_the_reference() {
let _data = benilla_formats::wow_data_or_skip!();
let s = armed();
s.run(r#"InspectUnit("target")"#).unwrap();
s.run(r#"ToggleInspect("InspectPaperDollFrame")"#).unwrap();
assert!(!s.eval::<bool>("return InspectFrame:IsVisible()").unwrap());
let _ = s.errors();
s.run(r#"ToggleInspect("InspectPaperDollFrame")"#).unwrap();
assert!(
s.eval::<bool>("return InspectFrame:IsVisible()").unwrap(),
"the window still shows — Show() runs before OnShow raises"
);
// TWO raises, not one: `InspectFrame` and `InspectPaperDollFrame` each have their own OnShow,
// each fired under its own pcall, so each abandons independently — the first at the portrait's
// gate, the second at `UnitLevel`'s. Both carry the reference's own `Usage:` text.
let errors = s.errors();
assert_eq!(errors.len(), 2, "one raise per OnShow handler: {errors:?}");
assert!(
errors[0].contains(r#"Usage: SetPortraitTexture(texture, "unit")"#),
"the portrait's gate, before the name line is ever reached: {errors:?}"
);
assert!(
errors[1].contains(r#"Usage: UnitLevel("unit")"#),
"and the paper doll's own handler, dying at its first getter: {errors:?}"
);
}
@@ -387,6 +387,7 @@ fn search_surfaces_bindings_as_live_rows_under_the_redirect_head() {
// with the match painted LIVE on the search pool (the era reflows its real rows the
// same way).
s.run(r#"OptionsFrameSearchBox:SetText("jump")"#).unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
assert!(s
.eval::<bool>("return OptionsFrameContainerBodySearchHeadKeybindings:IsVisible()")
.unwrap());
@@ -419,6 +420,7 @@ fn search_surfaces_bindings_as_live_rows_under_the_redirect_head() {
// The head is the era redirect: clicking it ends the search on the Keybindings page.
s.run("OptionsFrameContainerBodySearchHeadKeybindings:Click()")
.unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
assert_eq!(
s.eval::<String>("return OptionsFrame.selectedCategory")
.unwrap(),
@@ -691,6 +693,7 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() {
// Page side: search finds it and paints a live row with the capsule filled.
s.run(r#"OptionsFrameSearchBox:SetText("bonusactionbutton1")"#)
.unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
assert!(s
.eval::<bool>("return OptionsFrameContainerBodySearchHeadKeybindings:IsVisible()")
.unwrap());
@@ -140,11 +140,14 @@ fn new_then_pick_an_icon_then_okay_creates_the_macro() {
/// A regression here silently discards everything the player typed.
#[test]
fn typing_a_body_and_closing_the_window_commits_it() {
let s = harness();
let mut s = harness();
s.run(r#"CreateMacro("Ambush", 1, "")"#).unwrap();
s.run("ShowMacroFrame()").unwrap();
s.run(r#"BenillaMacroFrameText:SetText("/cast Ambush\n/say pew")"#)
.unwrap();
// The window's dirty flag comes from the box's `OnTextChanged`, which the drain owes until the
// next frame (decision 1831).
s.tick(0.0);
no_errors(&s, "type");
assert!(
s.eval::<bool>("return BenillaMacroFrame.textChanged == 1")
@@ -172,13 +175,14 @@ fn typing_a_body_and_closing_the_window_commits_it() {
/// point: `MacroFrame.macroBase` is 0 or MAX_MACROS, and every binding takes `macroBase + i`.
#[test]
fn the_character_tab_creates_in_the_second_index_range_and_switching_saves() {
let s = harness();
let mut s = harness();
s.run(r#"CreateMacro("Acct", 1, "")"#).unwrap();
s.run("ShowMacroFrame()").unwrap();
// Type into the account macro, then switch tabs WITHOUT any explicit save.
s.run(r#"BenillaMacroFrameText:SetText("/say account")"#)
.unwrap();
s.tick(0.0); // as above — the edit marks, the drain notifies (1831)
s.run("BenillaMacroFrameTab2:Click()").unwrap();
no_errors(&s, "tab 2");
assert_eq!(
@@ -361,3 +361,70 @@ fn the_money_type_table_carries_all_seven_reference_types() {
);
}
}
/// **`MoneyInputFrame.lua` is `EditBox:SetNumber`'s only caller on the whole chain**, and the verb
/// did not exist until decision 1831 — so the three-box amount editor could not put a number in a
/// box at all. This drives the reference's own file: split an amount, read it back, round-trip.
///
/// The boxes are `numeric="true"`, which is why the value→text law matters here specifically: a
/// non-digit abandons the insert wholesale and leaves the box EMPTY. Every value this path passes
/// is a non-negative integer, so `%.14g` yields plain digits and the gate never trips — but the
/// zero case below is the one that would show if it ever did.
#[test]
fn the_chains_money_input_frame_splits_an_amount_across_its_three_boxes() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_xml(&s, "Fonts.xml");
load_xml(&s, "MoneyFrame.xml"); // COPPER_PER_GOLD / COPPER_PER_SILVER live here
// The manifest's own order: the `.lua` brings the ten verbs, the `.xml` the template.
load_xml(&s, r"Interface\FrameXML\MoneyInputFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyInputFrame.xml");
let doc = benilla_ui::framexml::parse(
r#"<Ui>
<Frame name="TestAmount" inherits="MoneyInputFrameTemplate">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Frame>
</Ui>"#,
)
.unwrap();
let report = benilla_ui::loader::load_in(&s, &doc, "test", &|_: &str| None);
assert!(report.errors.is_empty(), "{:?}", report.errors);
// 1234g 56s 78c.
s.run("MoneyInputFrame_SetCopper(TestAmount, 12345678)")
.unwrap();
for (box_name, want) in [("Gold", "1234"), ("Silver", "56"), ("Copper", "78")] {
assert_eq!(
s.eval::<String>(&format!("return TestAmount{box_name}:GetText()"))
.unwrap(),
want,
"TestAmount{box_name}"
);
}
assert_eq!(
s.eval::<f64>("return MoneyInputFrame_GetCopper(TestAmount)")
.unwrap(),
12345678.0,
"the amount round-trips back out of the three boxes"
);
// Zero is the reference's quiet case: `GetNumber()` on an empty box is already 0, so the
// equality test short-circuits and `SetNumber` is never reached — the boxes stay EMPTY rather
// than reading "0", and the total still comes back 0.
s.run("MoneyInputFrame_ResetMoney(TestAmount)").unwrap();
s.run("MoneyInputFrame_SetCopper(TestAmount, 0)").unwrap();
for box_name in ["Gold", "Silver", "Copper"] {
assert_eq!(
s.eval::<String>(&format!("return TestAmount{box_name}:GetText()"))
.unwrap(),
"",
"TestAmount{box_name} stays empty at zero"
);
}
assert_eq!(
s.eval::<f64>("return MoneyInputFrame_GetCopper(TestAmount)")
.unwrap(),
0.0
);
}
@@ -383,6 +383,8 @@ fn search_reflows_live_rows_and_restores_the_page() {
.unwrap();
s.run("OptionsFrameSearchBox:SetText(\"music\")").unwrap();
// The search reflows off the box's `OnTextChanged`, which is deferred to the drain (1831).
s.tick(0.0);
assert_eq!(
s.eval::<String>("return OptionsFrameContainerTitle:GetText()")
.unwrap(),
@@ -440,6 +442,7 @@ fn search_reflows_live_rows_and_restores_the_page() {
// Clearing restores the page view and the authored chain EXACTLY.
s.run("OptionsFrameSearchBox:SetText(\"\")").unwrap();
s.tick(0.0); // the clear reflows on the drain, like the search itself (1831)
assert_eq!(
s.eval::<String>("return OptionsFrameContainerTitle:GetText()")
.unwrap(),
@@ -489,12 +492,16 @@ fn the_phrase_match_outranks_its_words() {
/// the box) and a no-match query (the section-header "no results" line, nothing chained).
#[test]
fn a_head_click_ends_the_search_and_a_miss_shows_no_results() {
let s = harness();
let mut s = harness();
s.run("ShowUIPanel(OptionsFrame)").unwrap();
s.run("OptionsFrameSearchBox:SetText(\"volume\")").unwrap();
s.tick(0.0); // the head being clicked below is built by the search reflow, so drain first (1831)
s.run("OptionsFrameContainerBodySearchHeadAudio:Click()")
.unwrap();
// The click clears the box synchronously; restoring the page rides that clear's
// `OnTextChanged`, which the drain owes (1831).
s.tick(0.0);
assert_eq!(
s.eval::<String>("return OptionsFrameSearchBox:GetText()")
.unwrap(),
@@ -517,6 +524,7 @@ fn a_head_click_ends_the_search_and_a_miss_shows_no_results() {
s.run("OptionsFrameSearchBox:SetText(\"flibbertigibbet\")")
.unwrap();
s.tick(0.0); // the miss renders on the drain too (1831)
assert!(s
.eval::<bool>("return OptionsFrameContainerBodyNoResults:IsVisible()")
.unwrap());
@@ -530,6 +538,7 @@ fn a_head_click_ends_the_search_and_a_miss_shows_no_results() {
);
}
s.run("OptionsFrameSearchBox:SetText(\"\")").unwrap();
s.tick(0.0); // and so does clearing it (1831)
assert!(!s
.eval::<bool>("return OptionsFrameContainerBodyNoResults:IsVisible()")
.unwrap());
@@ -466,3 +466,40 @@ fn every_template_these_files_declare_is_a_real_1_12_name() {
"only {checked} templates swept — the sweep, not the files, is what broke"
);
}
/// **`PanelTemplates_TabResize`'s `tab` argument is OPTIONAL, and omitting it means `this`.**
///
/// The reference opens with `if ( tab ) then tabName = tab:GetName(); else tabName =
/// this:GetName(); tab = this; end`. Ours had only the first half, so `tab:GetName()` on a nil
/// threw — and nothing in our tree noticed, because every caller we wrote passed a tab explicitly.
/// The stock files do not: `Blizzard_MacroUI.xml`'s two tabs each call
/// `PanelTemplates_TabResize(0)` from their own `OnLoad` and nothing else, so both threw at load
/// and the window came up with unsized tabs. Decision 1835.
///
/// Driven through a real `OnLoad` rather than a direct call, because `this` is exactly what is
/// under test: a plain function call would not set it.
#[test]
fn tab_resize_falls_back_to_this_when_no_tab_is_passed() {
let mut s = harness();
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
let doc = benilla_ui::framexml::parse(
r#"<Ui>
<Button name="ProbeTab" inherits="CharacterFrameTabButtonTemplate" text="Macros">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts><OnLoad>PanelTemplates_TabResize(0)</OnLoad></Scripts>
</Button>
</Ui>"#,
)
.unwrap();
let report = benilla_ui::loader::load_in(&s, &doc, "test", &|_: &str| None);
assert!(
report.errors.is_empty(),
"the OnLoad must not throw: {:?}",
report.errors
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// It sized the tab it was never handed: wider than the bare label, because the end slices are
// added on top.
let width = s.eval::<f64>("return ProbeTab:GetWidth()").unwrap();
assert!(width > 0.0, "the tab got a width, got {width}");
}
@@ -1125,7 +1125,7 @@ fn the_1507_registry_rows_match_the_reference_bytes() {
// The whileDead flags the ref authors and 1507 carried (l.21, l.25, Blizzard_TalentUI:71).
"UIPanelWindows['SpellBookFrame'].whileDead == 1",
"UIPanelWindows['QuestLogFrame'].whileDead == 1",
"UIPanelWindows['BenillaTalentFrame'].whileDead == 1",
"UIPanelWindows['TalentFrame'].whileDead == 1",
// UIChildWindows — UIParent.lua l.44-50 verbatim, all four shipped.
"table.getn(UIChildWindows) == 4",
"UIChildWindows[1] == 'OpenMailFrame'",
@@ -610,9 +610,17 @@ mod tests {
///
/// **A remaining `<?>` in the `fx=` column is usually a LoadOnDemand addon**, not something to
/// build. `ClassTrainerFrame_Show`, `CraftFrame_Show`, `MacroFrame_SaveMacro`,
/// `InspectFrame_Show`, `TalentFrame_Toggle` and their siblings live in `Blizzard_*` addons the
/// install ships **packed as `.pub`**, so no amount of scanning the extracted FrameXML finds
/// them. They arrive when that addon does, exactly like an `fx=` name with a home.
/// `InspectFrame_Show`, `TalentFrame_Toggle` and their siblings live in `Blizzard_*` addons, so
/// no amount of scanning the extracted **FrameXML** finds them. They arrive when that addon does,
/// exactly like an `fx=` name with a home.
///
/// **They are NOT unreachable, and this doc used to say they were.** It claimed the install
/// ships them "packed as `.pub`", which is a misreading of the loose
/// `Interface\AddOns\<name>\` folder — that really does hold only a `.pub` signature file.
/// The addon's real `.xml`/`.lua`/`.toc` are inside **`patch.MPQ`**, and `Chain` mounts MPQs, so
/// `reference_ui::read` reaches them and `is_chain_entry` accepts the path. Verified by reading
/// `Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml` and `Blizzard_InspectUI`'s twin
/// straight out of the archive. Every one of those windows is buildable today.
///
/// 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.
@@ -722,6 +722,9 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
s.cursor_payload().is_none(),
"with the editor open a shift-click writes instead of picking up"
);
// The dirty flag is set by the box's own `OnTextChanged`, which is deferred to the drain — the
// write landed in the buffer synchronously, the notification did not (decision 1831).
s.tick(0.0);
assert!(
s.eval::<bool>("return BenillaMacroFrame.textChanged == 1")
.unwrap(),
+21 -3
View File
@@ -28,7 +28,7 @@ use benilla_ui::script::UiScript;
/// **A chain entry needs client data**, so a test that names one has to open with
/// `benilla_formats::wow_data_or_skip!()`; [`BAG_UI`] is a list that always does.
pub(super) fn load_ui(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, false)
load_entry(s, entry, false, false)
}
/// [`load_ui`], and **a missing template is a failure too**.
@@ -39,10 +39,21 @@ pub(super) fn load_ui(s: &UiScript, entry: &str) -> usize {
/// windows' test modules grew this check by hand. It is one function now rather than four copies,
/// and any test may ask for it.
pub(super) fn load_ui_strict(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, true)
load_entry(s, entry, true, false)
}
fn load_entry(s: &UiScript, entry: &str, strict_templates: bool) -> usize {
/// [`load_ui`], and **no loader warning of any kind is tolerated**.
///
/// Stricter than [`load_ui_strict`], which only fails a missing template. A file whose own
/// assignment is "this loads perfectly clean" wants the whole warnings channel asserted empty — a
/// stale unknown-attribute or dropped-script warning is exactly the drift that check exists to
/// catch. It was a fourth private disk-only reader in `group_loot_tests.rs` until 1838; a
/// disk-only reader cannot name a chain file, which is what that test now loads.
pub(super) fn load_ui_no_warnings(s: &UiScript, entry: &str) -> usize {
load_entry(s, entry, false, true)
}
fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bool) -> usize {
let path = entry.replace('\\', "/");
let bytes = read(&path).unwrap_or_else(|| panic!("{entry}: not found"));
if path.to_ascii_lowercase().ends_with(".lua") {
@@ -59,6 +70,13 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool) -> usize {
"{entry}: loader errors: {:?}",
report.errors
);
if no_warnings {
assert!(
report.warnings.is_empty(),
"{entry}: loader warnings: {:?}",
report.warnings
);
}
if strict_templates {
let missing: Vec<&String> = report
.warnings