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:
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user