The three multibar reds were one duplicate load, not a loader defect

`_G["MultiBarBottomLeftButton1NormalTexture"]` really was a different object
from that button's `GetNormalTexture()`, with both answering that same
`GetName()`, so every tint written through the global — the realm's own
`ActionButton_ShowGrid`, and pfUI constantly — landed on a region nothing drew.
I wrote that up as a state-texture publish naming the wrong region at two levels
of template inheritance and left three tests red on it.

It was neither the publish nor the depth. **These tests loaded
`MultiActionBars.xml` twice**: once through `load_action_bar`, where toc order
puts it, and again in each test's own list, left behind when the first was added
— 15 tests. Two loads build two sets of MultiBar buttons, and the two `_G`
publishes disagree about a collision: a frame's is non-overwriting (`0x701bd0`),
so `MultiBarBottomLeftButton1` stayed the *first* set's button, while the
loader's state-texture publish is a plain `globals().set`, so the region name
became the *second* set's texture. Every write through the global reached the
hidden second set.

What rules the loader out is now its own test: `expand`'s merge concatenates
children, and only the base of the real chain declares a `<NormalTexture>` —
`ActionBarButtonTemplate` and `MultiBar1ButtonTemplate` declare only `<Scripts>`,
read off the chain — so there is exactly one declaration at any depth, and a
synthetic three-level chain answers `named == live` at every level, top-level or
child of a named parent.

The other two reds in that file were a misread of the reference, in the same
place twice: `ShapeshiftBar_Update`'s last line is
`UIParent_ManageFramePositions()`, and the pass hides all three shelf strips
under `if ( MultiBarBottomLeft:IsShown() )`. So a form learned while the
bottom-left bar is up shows the middle strip and takes it down again inside one
call — it is never seen across the row, where both tests asserted it would be
"until the next pass".

17/17 in that module now. The publish asymmetry is left alone and written down
where it was found: no single load can reach it, the reference's region-name path
is not disassembled, and matching frames on a guess is not a fix.

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:07:00 -06:00
co-authored by Claude Opus 5
parent 27c75776c6
commit 48c27d45bf
2 changed files with 112 additions and 37 deletions
@@ -275,7 +275,6 @@ fn shipped_stance_bar_drives_end_to_end() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
show_bars(&s, &[1]);
assert!(
s.eval::<bool>("return ShapeshiftBarFrame ~= nil and ShapeshiftButton10 ~= nil")
@@ -550,7 +549,6 @@ fn the_vertical_multibars_exist_hidden_on_the_reference_pages() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
for bar in ["MultiBarRight", "MultiBarLeft"] {
assert!(
@@ -605,7 +603,6 @@ fn every_extra_bar_stays_down_until_its_own_toggle_is_set() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
const BARS: [&str; 4] = [
"MultiBarBottomLeft",
@@ -682,7 +679,6 @@ fn raising_a_bottom_bar_moves_the_managed_bottom_stack() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Interface\\FrameXML\\CastingBarFrame.xml");
// Read the row's y off the anchor, so a hidden cast bar answers too.
@@ -741,7 +737,6 @@ fn viewable_action_bar_pages_follow_the_bar_toggles() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
let viewable = |s: &UiScript| {
s.eval::<String>(
@@ -803,7 +798,6 @@ fn the_grid_option_holds_the_extra_bars_empty_wells_open() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
show_bars(&s, &[1]);
let well = |s: &UiScript| {
@@ -884,7 +878,6 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
s.set_action(
1,
Some(ActionSlot {
@@ -939,21 +932,26 @@ fn a_held_payload_ghosts_the_empty_wells_it_opens() {
23,
"11 empty main wells swap art + MultiBarBottomLeft's 12 appear"
);
// **Known red, and precisely located.** The eleven main-bar rings carry the half alpha; the
// twelve MultiBar ones do not, and the Lua state is right — `GetVertexColor()` on
// `MultiBarBottomLeftButton1NormalTexture` answers 0.5. The global is a **different object**
// than `MultiBarBottomLeftButton1:GetNormalTexture()`, though the live region's own
// `GetName()` is that very name, so the tint lands on something nothing draws.
// This assertion was red for a while on a wrong diagnosis worth leaving written down, because
// the symptom is a good imitation of a loader bug. The twelve MultiBar rings were not taking
// the tint, and `_G["MultiBarBottomLeftButton1NormalTexture"]` was a **different object** from
// `MultiBarBottomLeftButton1:GetNormalTexture()` while both answered that same `GetName()` —
// which reads exactly like the state-texture publish naming the wrong region, and which I
// wrote up as a template-depth defect in `loader::widgets`.
//
// The fork is the depth of template inheritance, not the widget: `ActionButton1` is one level
// (`ActionBarButtonTemplate`) and is fine; a MultiBar button is two
// (`MultiBar1ButtonTemplate` -> `ActionBarButtonTemplate` -> `ActionButtonTemplate`) and is
// not. Neither of the two outer templates declares a `<NormalTexture>` of its own, so the
// duplicate comes out of the loader's expansion, not the markup.
// It was neither. **This test loaded `MultiActionBars.xml` twice** — once through
// `load_action_bar`, where it belongs by toc order, and once again in its own list, left
// behind when the first was added. Two loads build two sets of MultiBar buttons, and the two
// `_G` publishes disagree about a collision: a frame's is non-overwriting (`0x701bd0`), so
// `MultiBarBottomLeftButton1` stayed the **first** set's button, while the loader's
// state-texture publish is a plain `globals().set`, so the region name became the **second**
// set's texture. Every write through the global landed on the hidden second set.
//
// Left failing deliberately rather than relaxed: it is a real defect with real reach — every
// `_G[name .. "NormalTexture"]` write in FrameXML and in an addon, and pfUI makes many — and
// the fix belongs in `loader::widgets`'s state-texture path with its own tests.
// A minimal three-level template chain does *not* reproduce it (`loader/tests.rs`), which is
// what finally killed the depth story. The inconsistency between the two publishes is real
// but out of reach of any single load, so it is recorded here rather than guessed at: the
// reference's region path is not disassembled, and making regions non-overwriting to match
// frames would be a change with no evidence behind it.
assert!(
ghosts.iter().all(|a| *a == 0.5),
"every grid ring is the ref's half-alpha ghost, not an opaque plate: {ghosts:?}"
@@ -1022,7 +1020,6 @@ fn a_bar_toggle_sends_the_byte_its_globals_pack_to() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
let _ = s.take_action_bar_toggle_sends();
// The Options row's sequence: assign, update the bars, send.
@@ -1099,7 +1096,6 @@ fn the_shipped_setter_passes_exactly_four_arguments() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
// Lua 5.0's `arg.n`: `...` as a value is not in the 1.12 client's grammar.
s.run(
@@ -1145,7 +1141,6 @@ fn the_stance_bar_sits_where_the_pass_puts_it() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
s.set_shapeshift_forms(vec![ShapeshiftFormView {
@@ -1206,7 +1201,6 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
@@ -1270,15 +1264,21 @@ fn the_stance_shelf_follows_the_bottom_left_bar() {
}
assert_eq!(ring(&s), 50.0, "raised rings are the ref's 50");
// A third form learned while raised: `ShapeshiftBar_Update` shows the middle strip regardless
// (BonusActionBarFrame.lua:169-175) and nothing runs the pass on that event, so the strip
// draws over the row until the next pass (UIParent.lua:1706-1717).
// A third form learned while raised. `ShapeshiftBar_Update`'s form-count arm *does* show the
// middle strip — and then its own last line runs the manage pass, which takes it straight back
// down because the bottom-left bar is up (`BonusActionBarFrame.lua`'s `ShapeshiftBar_Update`
// ends `ShapeshiftBar_UpdateState(); UIParent_ManageFramePositions();`, and the pass's
// shapeshift arm hides all three strips under `if ( MultiBarBottomLeft:IsShown() )`). So the
// strip never draws across the row, not even for a frame: the show and the hide are inside one
// call. This assertion used to read the other way, on a note that "nothing runs the pass on
// that event" — the file's last line says otherwise, and that was the whole of this red.
s.set_shapeshift_forms(vec![form(2457), form(71), form(2458)]);
s.fire_event("UPDATE_SHAPESHIFT_FORMS", vec![]);
assert!(
shown(&s, "ShapeshiftBarMiddle"),
"Update shows the strip; only the pass takes it down"
!shown(&s, "ShapeshiftBarMiddle"),
"Update shows the strip and its own trailing pass hides it again"
);
// And an explicit pass is idempotent on that state.
s.run("UIParent_ManageFramePositions()").unwrap();
assert!(!shown(&s, "ShapeshiftBarMiddle"));
assert_eq!(ring(&s), 50.0);
@@ -1318,7 +1318,6 @@ fn the_stance_shelf_is_as_long_as_the_form_count() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
show_bars(&s, &[]); // unraised: the shelf art is the state under test
@@ -1418,7 +1417,6 @@ fn an_extra_bars_empty_well_keeps_its_bound_hotkey_label() {
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
show_bars(&s, &[1]);
s.set_action(
61,
@@ -1500,7 +1498,6 @@ fn the_middle_strip_tiles_along_its_length_only() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
show_bars(&s, &[]); // bars down: the shelf is the state under test
let form = |id: u32| ShapeshiftFormView {
@@ -1567,7 +1564,6 @@ fn a_forms_state_change_leaves_the_shelf_down_over_the_raised_bar() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_action_bar(&s);
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
// A recorder for the two events a push may announce.
s.run(
"STANCE_LOG = {} local f = CreateFrame('Frame') \
@@ -1721,8 +1717,11 @@ fn a_forms_state_change_leaves_the_shelf_down_over_the_raised_bar() {
);
assert_eq!(log(&s), "");
// A fourth stance learned is a list edge: `ShapeshiftBar_Update` shows the strip over the
// raised bar until the next pass.
// A fourth stance learned is a list edge. `ShapeshiftBar_Update` shows the strip for the form
// count and then runs the manage pass on its own last line, which takes it down again under
// the raised bar — so the shelf is never seen across the row. Same correction as
// `the_stance_shelf_follows_the_bottom_left_bar`'s: the "until the next pass" reading ignored
// `ShapeshiftBar_Update`'s trailing `UIParent_ManageFramePositions()`.
assert_eq!(
push_forms(
&mut s,
@@ -1738,8 +1737,8 @@ fn a_forms_state_change_leaves_the_shelf_down_over_the_raised_bar() {
);
assert_eq!(log(&s), "UPDATE_SHAPESHIFT_FORMS");
assert!(
shown(&s, "ShapeshiftBarMiddle"),
"the learn edge: Update shows the strip"
!shown(&s, "ShapeshiftBarMiddle"),
"the learn edge shows the strip and hides it again in the same call"
);
s.run("UIParent_ManageFramePositions()").unwrap();
assert!(!shown(&s, "ShapeshiftBarMiddle"));
+76
View File
@@ -14,6 +14,82 @@ mod loader_tests {
framexml::parse(text).expect("valid FrameXML")
}
/// A state texture's `$parent` name points at the live slot however deep the template chain
/// is, and whether the button is top-level or a named parent's child.
///
/// Written after a wrong diagnosis: `_G["MultiBarBottomLeftButton1NormalTexture"]` really was
/// a different object from that button's `GetNormalTexture()`, and the obvious reading was
/// that `expand`'s merge duplicates the base's `<NormalTexture>` once per level, so the
/// publish and the final slot drift apart. It does not — merge concatenates children and only
/// the base of the real chain (`ActionButtonTemplate`) declares one, so there is exactly one
/// declaration at any depth. The real cause was the test loading `MultiActionBars.xml` twice
/// (`multibar_stance_tests::a_held_payload_ghosts_the_empty_wells_it_opens` carries the
/// write-up). This test is what rules the loader out, so it stays.
#[test]
fn a_state_textures_name_follows_the_live_slot_at_any_template_depth() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(800.0, 600.0);
let doc = parse(
r#"<Ui>
<CheckButton name="DepthBase" virtual="true">
<Size><AbsDimension x="36" y="36"/></Size>
<NormalTexture name="$parentNormalTexture" file="Interface\Base">
<Size><AbsDimension x="66" y="66"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</NormalTexture>
</CheckButton>
<!-- Two more levels that declare no state texture of their own, as
ActionBarButtonTemplate and MultiBar1ButtonTemplate do not. -->
<CheckButton name="DepthMid" virtual="true" inherits="DepthBase">
<Scripts><OnLoad>this.mid = 1</OnLoad></Scripts>
</CheckButton>
<CheckButton name="DepthOuter" virtual="true" inherits="DepthMid">
<Scripts><OnLoad>this.outer = 1</OnLoad></Scripts>
</CheckButton>
<CheckButton name="OneHop" inherits="DepthBase">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</CheckButton>
<CheckButton name="ThreeHop" inherits="DepthOuter">
<Anchors><Anchor point="TOPRIGHT"/></Anchors>
</CheckButton>
<Frame name="DepthHost">
<Anchors><Anchor point="BOTTOMLEFT"/></Anchors>
<Frames>
<CheckButton name="$parentKid" inherits="DepthOuter">
<Anchors><Anchor point="CENTER"/></Anchors>
</CheckButton>
</Frames>
</Frame>
</Ui>"#,
);
let report = load(&s, &doc, &no_files);
assert!(report.errors.is_empty(), "{:?}", report.errors);
for button in ["OneHop", "ThreeHop", "DepthHostKid"] {
assert!(
s.eval::<bool>(&format!(
"return _G['{button}NormalTexture'] == _G['{button}']:GetNormalTexture()"
))
.unwrap(),
"{button}: the published name is not the live normal texture"
);
assert_eq!(
s.eval::<String>(&format!(
"return _G['{button}']:GetNormalTexture():GetName()"
))
.unwrap(),
format!("{button}NormalTexture"),
"{button}: the live region does not answer its own published name"
);
// And the paint reached it, so this is not two agreeing empties.
assert_eq!(
s.eval::<String>(&format!("return _G['{button}NormalTexture']:GetTexture()"))
.unwrap(),
"Interface\\Base",
"{button}: the base template's art"
);
}
}
/// `CMinimap::LoadXML 0x4ee2b0`: `minimapArrowModel` goes to engine children 1-8 (`0x4ee170`),
/// `minimapPlayerModel` to child 9 alone (`[Minimap+0x338]`, `0x4ee260`); without the
/// attributes the defaults are `0x84c768` and `0x8453c0`. `Minimap.xml:97` sets both.