skill-ups announce again, and the stack split fills its window
- a real skill-up prints its line; the lines the client keeps silent — class specs, racials, mounts — stay silent, off the game's own flag - the stack-split dialog draws whole: its plate fills the window - the four bag-bar buttons speak the reference's names all the way down, so Bagnon's overrides catch them and the backpack button lights while its window is open - regions anchor at creation — the client's implicit-anchor law replaces a pile of draw-time fallbacks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,8 @@ fn bag_bar_icons_draw_above_the_action_bar_art() {
|
||||
/// The backpack open/close kits (ContainerFrame.lua ContainerFrame_OnShow/OnHide, l.140 / l.120):
|
||||
/// showing the window queues igBackPackOpen, hiding it queues igBackPackClose — and nothing queues
|
||||
/// at load (the frame is authored hidden="true", so it never transitions on startup). Driven through
|
||||
/// `BenillaBagToggle_OnClick`, the exact path both the bag button and the 'B' binding use.
|
||||
/// `BenillaBagToggle_OnClick` — the toggle body the bag button's click wrapper calls (the 'B'
|
||||
/// binding runs the bare `ToggleBackpack()`, the same one hop deeper).
|
||||
#[test]
|
||||
fn backpack_toggle_plays_open_and_close_kits() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -1002,6 +1003,60 @@ fn shift_click_on_a_stack_opens_the_split_frame() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// B180 — the split dialog's parchment plate fills the whole 172×96 frame. The reference authors
|
||||
/// the plate 256×32 with NO anchors, a vestigial size the real client never renders (the TexCoords
|
||||
/// crop exactly 172×96 out of the 256×128 art — the frame's own size, a complete panel drawn 1:1
|
||||
/// over it). Our sized-anchor-less fallback draws CENTERED at the authored size instead, which
|
||||
/// squashed the dialog to a 32-tall bar: the count and arrows floated above it and Okay/Cancel
|
||||
/// straddled its bottom edge. The plate's authored size is dropped (StackSplit.xml says why); this
|
||||
/// pins the render so the vestigial size cannot come back.
|
||||
#[test]
|
||||
fn the_split_frame_plate_fills_the_dialog() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let (x, y) = open_backpack_with_a_five_stack(&mut s);
|
||||
|
||||
s.set_modifiers(true, false, false);
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
s.set_modifiers(false, false, false);
|
||||
s.resolve();
|
||||
|
||||
let (left, right, top, bottom) = s
|
||||
.eval::<(f64, f64, f64, f64)>(
|
||||
"return StackSplitFrame:GetLeft(), StackSplitFrame:GetRight(), \
|
||||
StackSplitFrame:GetTop(), StackSplitFrame:GetBottom()",
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
(right - left - 172.0).abs() < 0.5 && (top - bottom - 96.0).abs() < 0.5,
|
||||
"the dialog frame is 172×96, got {}×{}",
|
||||
right - left,
|
||||
top - bottom
|
||||
);
|
||||
let plate = s
|
||||
.extract()
|
||||
.into_iter()
|
||||
.find(|q| {
|
||||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||||
if p.contains("UI-MoneyFrame"))
|
||||
})
|
||||
.expect("the plate is on screen");
|
||||
let r = plate.rect.expect("the plate has a rect");
|
||||
for (edge, got, want) in [
|
||||
("left", r.left, left as f32),
|
||||
("right", r.right, right as f32),
|
||||
("top", r.top, top as f32),
|
||||
("bottom", r.bottom, bottom as f32),
|
||||
] {
|
||||
assert!(
|
||||
(got - want).abs() < 0.5,
|
||||
"plate {edge} = {got}, frame {edge} = {want} — the plate must fill the dialog"
|
||||
);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Okay in the split spinner only picks the split carry up (ref/cursor.rs `SplitContainerItem` —
|
||||
/// a pickup, not a self-contained move); a SUBSEQUENT placement is what actually queues the
|
||||
/// `ContainerMove` with `count: Some(n)`, drained the same way any other container move is.
|
||||
@@ -1834,3 +1889,73 @@ fn the_bag_slots_carry_the_references_names_and_icon_names() {
|
||||
"Bagnon captures this OnClick to replace the bag behaviour"
|
||||
);
|
||||
}
|
||||
|
||||
/// The backpack button's checked law after the stated divergence (BenillaBagToggle_OnClick's
|
||||
/// comment): checked belongs to the WINDOWS' OnShow/OnHide writes, and the button's XML click
|
||||
/// wrapper only undoes the CheckButton widget's pre-handler flip. The killer case is the click
|
||||
/// that never touches the backpack window — backpack shut, another bag's window open →
|
||||
/// ToggleBackpack sees an open window and CLOSES ALL; the backpack never transitions, nothing
|
||||
/// writes its button, and only the wrapper's undo keeps the widget flip from leaving it
|
||||
/// lit-while-shut (the job the ref's native-window scan tail used to do).
|
||||
#[test]
|
||||
fn a_close_others_click_leaves_the_backpack_button_dark() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"UiPanels.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"ActionBar.xml",
|
||||
"BagFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
s.set_money(0);
|
||||
s.set_container(
|
||||
1,
|
||||
Some(benilla_ui::script::ContainerState {
|
||||
name: Some("Pouch".into()),
|
||||
num_slots: 6,
|
||||
slots: std::collections::HashMap::new(),
|
||||
}),
|
||||
);
|
||||
let checked = |s: &mut UiScript| {
|
||||
s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
|
||||
.unwrap()
|
||||
};
|
||||
// Bag 1's window alone is open; the backpack window and its button are dark.
|
||||
s.run("ToggleBag(1)").unwrap();
|
||||
assert!(!s.eval::<bool>("return BenillaBagFrame:IsShown()").unwrap());
|
||||
assert!(!checked(&mut s));
|
||||
|
||||
// A REAL click on the backpack button (the widget flip fires only on real input): close-all.
|
||||
s.resolve();
|
||||
let r: Vec<f32> = s
|
||||
.eval(
|
||||
"local f = MainMenuBarBackpackButton \
|
||||
return { f:GetLeft() + f:GetWidth() / 2, f:GetBottom() + f:GetHeight() / 2 }",
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(r[0], r[1]);
|
||||
s.mouse_button(r[0], r[1], "LeftButton", true);
|
||||
s.mouse_button(r[0], r[1], "LeftButton", false);
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaBagFrame1:IsShown()").unwrap(),
|
||||
"the click closes the other bag's window (ToggleBackpack's close-all arm)"
|
||||
);
|
||||
assert!(
|
||||
!checked(&mut s),
|
||||
"no backpack transition happened, so the button must stay dark — the widget flip undone"
|
||||
);
|
||||
|
||||
// The second real click opens the backpack (plus the equipped bag): its OnShow writes the light.
|
||||
s.mouse_button(r[0], r[1], "LeftButton", true);
|
||||
s.mouse_button(r[0], r[1], "LeftButton", false);
|
||||
assert!(s.eval::<bool>("return BenillaBagFrame:IsShown()").unwrap());
|
||||
assert!(
|
||||
checked(&mut s),
|
||||
"open ⇒ lit, written by the window's OnShow"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -1391,3 +1391,158 @@ fn bagnon_forevers_records_survive_the_logout_boundary() {
|
||||
);
|
||||
assert_eq!(item, "4496", "the logout boundary must not erase the slot");
|
||||
}
|
||||
|
||||
/// **Bug 6 — the director's "smaller bags open their own little bags".** Bagnon's whole
|
||||
/// interception model is the global override (`Bagnon_Core/core/Overrides.lua`: `ToggleBag = …`)
|
||||
/// plus a `SetScript("OnClick")` wrap on our bar buttons whose replacement calls the ORIGINAL
|
||||
/// handler back for a plain click. Both layers end at whatever the original OnClick does — and
|
||||
/// ours toggled `BenillaBagFrame<N>` directly instead of calling `ToggleBag`, so every slot-button
|
||||
/// click walked straight past both of Bagnon's hooks and opened the native window (the backpack
|
||||
/// button was fixed in an earlier pass; the four slots were not). The ref's own
|
||||
/// `BagSlotButton_OnClick` (MainMenuBarBagButtons.lua l.3-21) calls the GLOBAL `ToggleBag`, and
|
||||
/// its checked tail scans only the NATIVE ContainerFrames — so with Bagnon holding the bags a
|
||||
/// slot click toggles Bagnon's one window and the slot button stays unlit. Both halves pinned
|
||||
/// here with a real mouse click, an equipped bag in slot 2, and Bagnon fully hooked.
|
||||
#[test]
|
||||
fn a_bag_slot_click_toggles_bagnon_not_the_native_window() {
|
||||
let root = corpus_or_skip!();
|
||||
let mut s = open_bagnon(&root);
|
||||
|
||||
// An equipped bag behind CharacterBag1Slot (bagId 2): the inventory surface (the bag ITEM in
|
||||
// inv slot 21) and its container — both fed before any click, the schedule's own order.
|
||||
let mut inv: benilla_ui::script::InventorySlots = Default::default();
|
||||
inv[21] = Some(benilla_ui::script::InvSlotView {
|
||||
item_id: 4497,
|
||||
count: 1,
|
||||
quality: 1,
|
||||
name: Some("Small Green Pouch".into()),
|
||||
link: Some("|cffffffff|Hitem:4497:0:0:0|h[Small Green Pouch]|h|r".into()),
|
||||
icon: Some("Interface\\Icons\\INV_Misc_Bag_10_Green".into()),
|
||||
..Default::default()
|
||||
});
|
||||
s.set_inventory_slots(inv);
|
||||
s.set_container(
|
||||
2,
|
||||
Some(ContainerState {
|
||||
name: Some("Small Green Pouch".into()),
|
||||
num_slots: 6,
|
||||
slots: Default::default(),
|
||||
}),
|
||||
);
|
||||
s.fire_event("BAG_UPDATE", vec![benilla_ui::script::ScriptValue::Int(2)]);
|
||||
s.resolve();
|
||||
|
||||
// Bagnon is open (the fixture); a REAL click on the slot button must CLOSE Bagnon — the
|
||||
// toggle routed through the override — and must never show the native window.
|
||||
// Both interception layers must be live, and the click exercises both: the SetScript wrap
|
||||
// (Bagnon.lua's Bagnon_AddBagHooks — its replacement calls our CAPTURED original back with
|
||||
// no args, so `self` inside the XML wrapper must still resolve on the re-entrant call) and
|
||||
// the global ToggleBag override the original then reaches.
|
||||
assert!(
|
||||
s.eval::<bool>("return CharacterBag1Slot:GetScript('OnClick') == BagnonBlizBag_OnClick")
|
||||
.unwrap_or(false),
|
||||
"Bagnon's SetScript OnClick wrap must be in place on the slot button"
|
||||
);
|
||||
let (x, y) = centre_of(&mut s, "CharacterBag1Slot");
|
||||
s.mouse_move(x, y);
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
assert!(
|
||||
!s.eval::<bool>("return Bagnon:IsVisible() or false")
|
||||
.unwrap(),
|
||||
"the slot click must reach Bagnon's ToggleBag override and close its window"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaBagFrame2:IsVisible() or false")
|
||||
.unwrap(),
|
||||
"the native bag window must NOT open — the click belongs to the addon's override"
|
||||
);
|
||||
|
||||
// Click again: Bagnon back open, native still shut, and the slot button unlit — the ref's
|
||||
// checked tail reads only the native window (its ContainerFrame scan), which Bagnon never
|
||||
// shows.
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
for _ in 0..2 {
|
||||
s.tick(0.1);
|
||||
}
|
||||
assert!(
|
||||
s.eval::<bool>("return Bagnon:IsVisible() or false")
|
||||
.unwrap(),
|
||||
"the second click re-opens Bagnon through the same override"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaBagFrame2:IsVisible() or false")
|
||||
.unwrap(),
|
||||
"the native window stays shut on the re-open too"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return CharacterBag1Slot:GetChecked() and true or false")
|
||||
.unwrap(),
|
||||
"the slot button stays unlit with an addon holding the bags (the ref scan is native-only)"
|
||||
);
|
||||
let raised = s.errors();
|
||||
let unexplained: Vec<&String> = raised
|
||||
.iter()
|
||||
.filter(|e| !e.contains("PutItemIn") && !e.contains("PickupBagFromSlot"))
|
||||
.collect();
|
||||
assert!(
|
||||
unexplained.is_empty(),
|
||||
"the click round-trip raised: {unexplained:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **Bug 7 — the director's unlit backpack button.** Bagnon lights `MainMenuBarBackpackButton`
|
||||
/// from its window's OnShow/OnHide (`Bagnon.lua:33/39` — `SetChecked(1/0)`), and OnShow dispatch
|
||||
/// is synchronous (wow-re `onshow-onhide-dispatch-order.md`), so whoever writes checked AFTER the
|
||||
/// toggle wins. Two of our seats overwrote it: the button's old click tail re-derived checked
|
||||
/// from the NATIVE backpack window (the ref's own scan — which leaves the button unlit on the
|
||||
/// real client's click path too, a ref wart our stated divergence closes), and the `B` binding
|
||||
/// ran the BUTTON handler instead of the ref's bare `ToggleBackpack()` body, dragging that tail
|
||||
/// onto the one path the real client lights. Now: a real click on the button ends lit while
|
||||
/// Bagnon's window is open and unlit when it closes, and the bare-global path (the binding's
|
||||
/// body) does the same.
|
||||
#[test]
|
||||
fn the_backpack_button_lights_while_bagnon_holds_the_bags() {
|
||||
let root = corpus_or_skip!();
|
||||
let mut s = open_bagnon(&root);
|
||||
|
||||
// The fixture opened Bagnon through the bare global (the binding's own body): OnShow's
|
||||
// SetChecked(1) must be standing — nothing runs after it on that path.
|
||||
assert!(
|
||||
s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
|
||||
.unwrap(),
|
||||
"the bare ToggleBackpack() path (the B binding) must leave the button lit"
|
||||
);
|
||||
|
||||
// A REAL click on the button: auto-toggle flips, the wrapper's undo repairs it, Bagnon's
|
||||
// OnHide writes the truth — closed and unlit.
|
||||
let (x, y) = centre_of(&mut s, "MainMenuBarBackpackButton");
|
||||
s.mouse_move(x, y);
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
assert!(
|
||||
!s.eval::<bool>("return Bagnon:IsVisible() or false")
|
||||
.unwrap(),
|
||||
"the click must toggle Bagnon closed through its override"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
|
||||
.unwrap(),
|
||||
"closed ⇒ unlit (Bagnon's OnHide write is the last word)"
|
||||
);
|
||||
|
||||
// And the second click: open again, lit again — the director's report, closed.
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
s.mouse_button(x, y, "LeftButton", false);
|
||||
assert!(
|
||||
s.eval::<bool>("return Bagnon:IsVisible() or false")
|
||||
.unwrap(),
|
||||
"the second click re-opens Bagnon"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return MainMenuBarBackpackButton:GetChecked() and true or false")
|
||||
.unwrap(),
|
||||
"open ⇒ lit (Bagnon's OnShow write is the last word)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -826,6 +826,7 @@ mod clip_plumb_tests {
|
||||
frame:SetScrollChild(child)
|
||||
local marker = child:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture(1, 0, 0) -- pathless colored quad: no BLP asset needed
|
||||
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -911,6 +912,7 @@ mod clip_plumb_tests {
|
||||
plain:SetSize(50, 50)
|
||||
local m = plain:CreateTexture(nil, "ARTWORK")
|
||||
m:SetTexture(0, 1, 0)
|
||||
m:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -970,6 +972,7 @@ mod extract_gate_tests {
|
||||
plain:SetSize(50, 50)
|
||||
marker = plain:CreateTexture(nil, "ARTWORK")
|
||||
marker:SetTexture(1, 0, 0)
|
||||
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user