Two real macro-window bugs, and three expectations the realm had moved on from

The macro window's four reds were four separate things, two of them bugs a
player meets.

**Every macro made through the icon chooser had a blank icon.** This realm
rewrote `Blizzard_MacroUI` to keep bare upper-cased icon names in its own
`MACRO_ICON_FILENAMES` — `UpdateMacroIconFilenames` strips `INTERFACE\ICONS\`
off `GetMacroIconInfo` — and `MacroPopupOkayButton_OnClick` hands that name
straight to `CreateMacro`. `icon_path` took any string as a finished path, so
`GetMacroInfo` answered `ABILITY_AMBUSH` and `MacroFrame_Update`'s
`macroIcon:SetTexture(texture)` resolved nothing. The engine composes the prefix
itself: `Interface\Icons\` is at `0x44ca64`, inside `UIMacros.cpp`'s own string
block between the `DeleteMacro`/`GetMacroInfo`/`GetNumMacros`/`CreateMacro`
registration names and `MACRONODE`, which is what says the macro code owns it.
A number is still a chooser index, and a string that already carries a separator
is still a path.

**Selecting a macro with no icon raised and took the whole details pane down.**
`Blizzard_MacroUI.lua:84-85` feeds `GetMacroInfo`'s texture to `SetTexture` and
to `strupper` with no guard, so a nil answered there is
`bad argument #1 to 'strupper' (string expected, got nil)` inside
`MacroFrame_Update` — no editor, no body box, no character counter. The
reference cannot be answering nil, or its own UI would raise the same way: a
macro with no icon of its own now reads as the client's own
`INV_Misc_QuestionMark` (`0x447fe4`, same UI block), which is also the name the
realm's chooser puts in row 1.

The other three were stale readings of a file the realm has rewritten, each
checked against the chain copy rather than guessed:

- the character counter is `MacroFrameCharLimitText:SetText(this:GetNumLetters()
  .. "/" .. this:GetMaxLetters())` in the realm's own XML — "21/510", with no
  `MACROFRAME_CHAR_LIMIT` and no "Characters Used" tail;
- the chooser shows `GetNumMacroIcons() + 1` rows, because
  `UpdateMacroIconFilenames` inserts `INV_MISC_QUESTIONMARK` at index 1 ahead of
  anything the engine offers, so every list index is one past the engine's;
- both tabs call `PanelTemplates_TabResize(0)` with no cap and read
  `text="GENERAL"`/`text="CHARACTER"`, where stock passed -15, capped tab 2 at
  150 and formatted the player's name into it. That test was *about* the name,
  so it now checks the row is independent of it instead.

195 failures from 203, no regressions (`comm -13` over the sorted names).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-29 15:33:50 -06:00
co-authored by Claude Opus 5
parent 48c27d45bf
commit 76e022ba78
2 changed files with 144 additions and 34 deletions
+64 -30
View File
@@ -119,7 +119,12 @@ fn new_then_pick_an_icon_then_okay_creates_the_macro() {
);
s.run(r#"MacroPopupEditBox:SetText("Ambush")"#).unwrap();
s.run("MacroPopupButton1:Click()").unwrap();
// Button **2**, not 1: this realm's `UpdateMacroIconFilenames` inserts
// `INV_MISC_QUESTIONMARK` at index 1 before it walks the spellbook and `GetMacroIconInfo`,
// so the pushed list starts at 2. Each button carries its list index as its `SetID`, so the
// click selects `ABILITY_AMBUSH` — the name the realm's UI keeps, upper-cased with
// `INTERFACE\ICONS\` stripped off.
s.run("MacroPopupButton2:Click()").unwrap();
assert!(
s.eval::<bool>("return MacroPopupOkayButton:IsEnabled() ~= 0")
.unwrap(),
@@ -138,7 +143,13 @@ fn new_then_pick_an_icon_then_okay_creates_the_macro() {
.eval::<(String, String)>("local n, t = GetMacroInfo(1) return n, t")
.unwrap();
assert_eq!(name, "Ambush");
assert_eq!(tex, "Interface\\Icons\\Ability_Ambush");
assert_eq!(
tex, "Interface\\Icons\\ABILITY_AMBUSH",
"`MacroPopupOkayButton_OnClick` hands `CreateMacro` the bare name its own list holds, and \
the engine composes `Interface\\Icons\\` onto it (`0x44ca64`, in `UIMacros.cpp`'s string \
block) — the case is the UI's, and texture lookup is case-insensitive. Storing the bare \
name verbatim left every macro made here with a blank icon."
);
assert!(
!s.eval::<bool>("return MacroPopupFrame:IsVisible()")
.unwrap(),
@@ -169,10 +180,16 @@ fn typing_a_body_and_closing_the_window_commits_it() {
.unwrap(),
"OnTextChanged marks the window dirty"
);
// The realm's own `Blizzard_MacroUI.xml` writes this counter itself —
// `MacroFrameCharLimitText:SetText(this:GetNumLetters() .. "/" .. this:GetMaxLetters())` in
// `MacroFrameText`'s `OnTextChanged` — and never reads `MACROFRAME_CHAR_LIMIT`, so there is no
// "Characters Used" tail and the cap is the `letters="510"` its own EditBox declares. (Our
// `install_macro_letters_patch` raises both for an install whose FrameXML still says 255; on
// this chain it has nothing left to do.)
assert_eq!(
s.eval::<String>("return MacroFrameCharLimitText:GetText()")
.unwrap(),
"21/255 Characters Used"
"21/510"
);
s.run(r#"HideUIPanel(MacroFrame)"#).unwrap();
@@ -314,8 +331,17 @@ fn the_icon_chooser_shows_the_pushed_list_and_hides_its_tail() {
s.run("MacroNewButton:Click()").unwrap();
no_errors(&s, "popup");
// The engine's list is the three the app pushed; the chooser's is **four**, because this
// realm's `UpdateMacroIconFilenames` inserts `INV_MISC_QUESTIONMARK` at index 1 before it
// walks the spellbook and `GetMacroIconInfo`. So the shown count is `GetNumMacroIcons() + 1`
// and every list index is one past the engine's.
assert_eq!(s.eval::<i64>("return GetNumMacroIcons()").unwrap(), 3);
for i in 1..=3 {
assert_eq!(
s.eval::<String>("return MACRO_ICON_FILENAMES[1]").unwrap(),
"INV_MISC_QUESTIONMARK",
"the realm's own first row, ahead of anything the engine offers"
);
for i in 1..=4 {
assert!(
s.eval::<bool>(&format!("return MacroPopupButton{i}:IsVisible()"))
.unwrap(),
@@ -323,7 +349,7 @@ fn the_icon_chooser_shows_the_pushed_list_and_hides_its_tail() {
);
}
assert!(
!s.eval::<bool>("return MacroPopupButton4:IsVisible()")
!s.eval::<bool>("return MacroPopupButton5:IsVisible()")
.unwrap(),
"past the end of the list the button hides — not a blank square"
);
@@ -339,21 +365,34 @@ fn the_icon_chooser_shows_the_pushed_list_and_hides_its_tail() {
.unwrap(),
"the picked icon is checked"
);
// The name the realm's list holds for that row, which is what `CreateMacro` is handed: the
// second pushed path, stripped and upper-cased.
assert_eq!(
s.eval::<String>("return MacroPopupFrame.selectedIconTexture")
.unwrap(),
"ABILITY_BACKSTAB"
);
}
/// Both tabs pass `PanelTemplates_TabResize` a -15 padding and tab 2, whose label carries the
/// player name, a 150 cap; a short and a long name run both arms of the cap.
/// Both tabs size themselves off their own label, with **no padding and no cap**: this realm's
/// `Blizzard_MacroUI.xml` calls `PanelTemplates_TabResize(0)` in each tab's `OnLoad` (`:502`,
/// `:524`), where stock 1.12 passed `-15` and gave tab 2 a 150-unit cap.
///
/// Its labels changed with it — `text="GENERAL"` and `text="CHARACTER"`, global-string keys, where
/// stock formatted `CHARACTER_SPECIFIC_MACROS` with `UnitName("player")`. So the tab row no longer
/// grows with the player's name at all, which is what this test used to be about: it ran a short
/// and a long name through the cap's two arms. Both names are kept, now as a check that the row is
/// *independent* of them.
#[test]
fn the_two_tabs_fit_inside_the_window() {
let _data = benilla_formats::wow_data_or_skip!();
/// `sideWidths`: twice `TabButtonTemplate`'s 16-unit end slice (`UIPanelTemplates.lua:41`).
/// `sideWidths`: twice `TabButtonTemplate`'s 16-unit end slice (`UIPanelTemplates.lua:42`).
const SIDES: f64 = 32.0;
/// The stock padding on both tabs (`Blizzard_MacroUI.xml:488`, `:511`).
const PAD: f64 = -15.0;
/// The stock cap on tab 2 (`Blizzard_MacroUI.xml:511`).
const CAP: f64 = 150.0;
/// What this realm passes (`Blizzard_MacroUI.xml:502`, `:524`).
const PAD: f64 = 0.0;
for (name, cap_should_bind) in [("Probe", false), ("Bartholomewthelongnamed", true)] {
let mut rows = Vec::new();
for name in ["Probe", "Bartholomewthelongnamed"] {
let mut s = harness_named(name);
s.run("ShowMacroFrame()").unwrap();
select_first(&s);
@@ -371,25 +410,15 @@ fn the_two_tabs_fit_inside_the_window() {
assert_eq!(
w1,
label1 + PAD + SIDES,
"tab 1 is text − 15 + the end slices"
"tab 1 is its text plus the slices"
);
let uncapped = label2 + PAD + SIDES;
if cap_should_bind {
assert_eq!(
w2,
CAP + PAD + SIDES,
"the reference's 150 cap binds on a long name"
);
assert!(
uncapped > CAP + PAD + SIDES,
"…and the name really was over it"
);
} else {
assert_eq!(w2, uncapped, "a short name is under the cap");
}
assert_eq!(w2, label2 + PAD + SIDES, "tab 2 the same, with no cap");
assert!(
w2 <= CAP + PAD + SIDES,
"the clamp may tighten the reference's cap, never loosen it"
!s.eval::<bool>(&format!(
"return string.find(MacroFrameTab2Text:GetText(), '{name}') ~= nil"
))
.unwrap(),
"tab 2's label carries the player name, which this realm's `text=\"CHARACTER\"` does not"
);
// The tab ends inside the plate the player sees, not merely inside the 384-unit frame.
@@ -405,7 +434,12 @@ fn the_two_tabs_fit_inside_the_window() {
"the tab row ({left1} + {w1} + {w2}) runs off the 384-wide window"
);
no_errors(&s, "tab fit");
rows.push((w1, w2));
}
assert_eq!(
rows[0], rows[1],
"the row must not move with the player's name any more"
);
}
#[test]