71 more key commands, and the quest windows wear the reference's names

- the binding registry goes 130 → 201 of the client's 228 live commands: a coverage gate makes registered ∪ recorded-absent exactly the 1.12 surface, each absence naming the Lua globals whose arrival refutes it — which is how 37 rows whose mechanism had already landed came home (the keyring, the pet book, the raid tab, the six action-bar pages, both vertical multibars) — and then four mechanisms bring 71 more: the five camera views with the reference's default table and its sixteen camera CVars read out of the binary (NextView/PrevView do NOT wrap), self-cast riding UseAction's third argument with autoSelfCast a real CVar at last, the friendly target cone / assist / last-hostile as one cycler with a side (the two sides use different deadness tests on purpose, and assist neither requires an assistable target nor swings), and REPLY2 — reply to the last person you told; the friendly cone also stops borrowing the enemy side's in-combat score bonus, which would have shoved a healer targeting you to the head of every cycle
- the key names close their gaps: NUMPADEQUALS, F13-F24 and BUTTON6+ were keys the binding window listed and the file saved that the dispatcher could never press — named both ways now, with the namer and the codec structurally unable to disagree again; platform= becomes the OS gate it really is (off a Mac build the iTunes-remote category never even exists), and the Mac 1.12 slice settles that its F13 IS print-screen while F14/F15 map to UNKNOWN on purpose and stay unbindable; and the last-enemy stamp gains the four conjuncts it shipped without — you resolve, are not dead, not a ghost, not mounted, and the new target is alive-or-feigning — so TargetLastEnemy can no longer land somewhere the real client's never would
- the quest windows wear the reference's names: ~40 of the quest window's globals were ours (the reference's name usually sitting in the comment beside them), its two six-item reward pools merge into the reference's single ten-item run with the update function ported line-for-line, the four panels and the quest log take REAL scroll frames (the thumb drags now), the gossip window gets its greeting panel, the two mail panes' inert painted-at-rest scrollbars become real ones, the Skills detail pane nests as the reference has it, and nine private constants take their real names; the one that broke a feature detect: our QuestLogCount — a name 1.12 doesn't publish — made pfUI conclude it was running on TBC and die in that branch
- an EditBox is born with its constructor's five regions (the text, three selection quads and the caret), so GetRegions answers the reference's own count and authored regions start at index 6 — and a bare <NormalTexture/> now builds a live blank texture, since the element is what creates the region, not its file=; OpacityFrame lands, its first customer having arrived; and the equip / auto-equip / use bind confirms complete the soulbind arc — three different predicates with no quality leg (borrowing the loot arm's would have silenced the confirm on every white bind-on-equip item), Accept re-issuing the original action with the reference's suppress flag because 1.12 ships no confirm packet, and the soulstone use going through the same gate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-31 20:16:49 +07:00
co-authored by Claude Fable 5
parent 542c2ac04b
commit 8ae42cc7ab
69 changed files with 6829 additions and 1479 deletions
@@ -1,5 +1,11 @@
use benilla_ui::script::{ActionSlot, QuadContent, ScriptValue, UiScript};
/// The queued action ids alone — `take_action_uses` carries `UseAction`'s self-cast modifier
/// beside the id since 1745, and every assertion in this file is about the id.
fn action_ids(s: &mut UiScript) -> Vec<u32> {
s.take_action_uses().into_iter().map(|u| u.action).collect()
}
/// Load the real `assets/ui/ActionBar.xml` (the shipped default bar) into a bare engine and
/// drive it with a synthetic action snapshot — the slice-1 chain minus Bevy: template
/// expansion over 12 instances, the vanilla bonus-page formula, icon paint on events, empty
@@ -95,7 +101,7 @@ fn shipped_action_bar_drives_end_to_end() {
// center is (8+18, 4+18) = (26, 22).
s.mouse_button(26.0, 22.0, "LeftButton", true);
s.mouse_button(26.0, 22.0, "LeftButton", false);
assert_eq!(s.take_action_uses(), vec![73]);
assert_eq!(action_ids(&mut s), vec![73]);
// The keybinding entry (the app's key feed runs `ActionButtonDown/Up(i)` on the two
// key edges — the ref's ACTIONBUTTONn binding, ActionButton.lua:15-45): UP fires UseAction
@@ -123,7 +129,7 @@ fn shipped_action_bar_drives_end_to_end() {
"PUSHED"
);
s.run("ActionButtonUp(2)").unwrap();
assert_eq!(s.take_action_uses(), vec![74], "key '2' fires action 74");
assert_eq!(action_ids(&mut s), vec![74], "key '2' fires action 74");
assert_eq!(depressed(&s), 0, "key UP restores the normal state");
// Stance drops (offset 0): the bar re-pages to actions 1..12 — all empty here, icons clear.
@@ -335,7 +341,7 @@ fn a_right_click_on_an_action_button_uses_the_action() {
s.mouse_button(26.0, 22.0, "RightButton", true);
s.mouse_button(26.0, 22.0, "RightButton", false);
assert_eq!(
s.take_action_uses(),
action_ids(&mut s),
vec![1],
"right-click queues the same UseAction a left-click does"
);
@@ -1439,11 +1445,24 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
// Keys route to the overlay immediately (ref ActionButton.lua:15-45's IsShown fork).
s.run("ActionButtonDown(1) ActionButtonUp(1)").unwrap();
assert_eq!(
s.take_action_uses(),
action_ids(&mut s),
vec![73],
"a key pressed mid-slide already drives the bonus page"
);
// …and the same button with the SELF-CAST modifier. `SELFACTIONBUTTON1`-`12` (`ALT-1`…`ALT-=`)
// are `ActionButtonUp(id, 1)` and nothing else, so this is the whole of what those twelve
// bindings do that the plain twelve do not (1745).
s.run("ActionButtonDown(1) ActionButtonUp(1, 1)").unwrap();
assert_eq!(
s.take_action_uses()
.into_iter()
.map(|u| (u.action, u.on_self))
.collect::<Vec<_>>(),
vec![(73, true)],
"ActionButtonUp's onSelf reaches UseAction's third argument"
);
// Half the slide: the replica is half-risen (top = 0.5 * 43 over the bar's bottom edge at
// y=0), the paint suppression still holds, still silent.
s.tick(0.075);
@@ -1536,7 +1555,7 @@ fn bonus_bar_slides_up_with_sound_and_down_without() {
// A key mid-descent still drives the old form's page — ref GetPagedID's lastBonusBar
// stand-in while the frame is still shown.
s.run("ActionButtonDown(1) ActionButtonUp(1)").unwrap();
assert_eq!(s.take_action_uses(), vec![97]);
assert_eq!(action_ids(&mut s), vec![97]);
s.tick(0.2);
assert!(
!s.eval::<bool>("return BonusActionBarFrame:IsShown()")
@@ -665,6 +665,10 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "ScrollTemplates.xml");
// Not optional, though this list ran without it: a MISSING template is a loader *warning*, not
// an error, so an under-loaded list passes load_xml's assert and then fails later on geometry
// that silently never got built. SkillDetailScrollFrame inherits UIPanelScrollFrameTemplate.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "CharacterFrame.xml");
load_xml(&s, "SkillFrame.xml");
s.set_unit("player", Some(player_unit()));
@@ -772,10 +776,10 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
// name; accepting queues the CMSG_UNLEARN_SKILL intent BY SKILL ID and removes nothing
// locally (the server's SetSkill(id,0,0) round trip owns the removal).
assert!(
shown(&mut s, "SkillDetailUnlearnButton"),
shown(&mut s, "SkillDetailStatusBarUnlearnButton"),
"the unlearn button shows for a profession"
);
s.run("SkillDetailUnlearnButton:Click()").unwrap();
s.run("SkillDetailStatusBarUnlearnButton:Click()").unwrap();
assert!(
shown(&mut s, "StaticPopup1"),
"the UNLEARN_SKILL confirm opens"
@@ -802,7 +806,7 @@ fn tab_round_trip_with_a_selected_skill_by_point() {
let defense = text_center(&mut s, "Defense");
click(&mut s, defense);
assert!(
!shown(&mut s, "SkillDetailUnlearnButton"),
!shown(&mut s, "SkillDetailStatusBarUnlearnButton"),
"no unlearn button for Defense"
);
// Restore the profession selection so the round trip below leaves familiar state.
@@ -130,6 +130,11 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "Cooldown.xml");
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_* — QuestLogDetail's reward money row
// ScrollTemplates.xml (the faux kit the list rides) + UIPanelTemplates.xml (the detail
// pane's UIPanelScrollFrameTemplate). A MISSING template is a loader *warning*, so an
// under-loaded list passes and then dies on the first FauxScrollFrame_Update.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
// A left-slot panel open, and the toast raised after it — the order that must not decide.
@@ -317,15 +317,9 @@ const KNOWN: &[Known] = &[
why: "as FriendsFrameFriendsScrollFrame",
},
Known {
frame: "OpenMailScrollFrame",
frame: "QuestLogListScrollFrame",
flag: Flag::Mouse,
why: "our open-mail body is flat art plus a FontString — the render approximation \
MailFrame.xml names; nothing scrolls it on either side",
},
Known {
frame: "SendMailScrollFrame",
flag: Flag::Mouse,
why: "as OpenMailScrollFrame",
why: "as FriendsFrameFriendsScrollFrame",
},
// ── id ─────────────────────────────────────────────────────────────────────────────────────
//
@@ -200,14 +200,18 @@ fn the_capture_flow_binds_steals_and_refuses_like_112() {
.unwrap(),
"Unbind arms with the selection"
);
// The host hands back a canonical chord: F binds into slot 1, W's old seat; UP survives
// The host hands back a canonical chord: J binds into slot 1, W's old seat; UP survives
// in slot 2; the capture disarms; the table is LIVE and the bind COMMITTED (Save queued —
// 1008's live-commit law, where 0997 waited for Okay).
s.run(r#"KeyBindings_OnHostKey("F")"#).unwrap();
//
// **`J`, not `F`** — this leg is about the plain bind, so its key has to be one nothing else
// holds, and `F` stopped being one when 1745 registered `ASSISTTARGET` on its byte-real
// default. The steal path has its own leg below (`T`), which is where a taken key belongs.
s.run(r#"KeyBindings_OnHostKey("J")"#).unwrap();
assert!(!s.bind_capture_armed(), "a completed bind disarms");
assert!(s
.eval::<bool>(
r#"local k1, k2 = GetBindingKey("MOVEFORWARD"); return k1 == "F" and k2 == "UP""#
r#"local k1, k2 = GetBindingKey("MOVEFORWARD"); return k1 == "J" and k2 == "UP""#
)
.unwrap());
assert_eq!(
@@ -710,3 +714,34 @@ fn the_pet_lane_is_registered_under_the_action_bar_header() {
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **Every registry body is real Lua.** 167 rows carry a Lua chunk as a Rust string literal, and
/// nothing compiled them: a typo in one shipped as a command that errors on its first press, in a
/// file whose tests all live one layer up at the page. `loadstring` is the whole check — running
/// them needs the FrameXML the harness deliberately does not load — and it is exactly the check
/// that a hand-typed string wants.
#[test]
fn every_command_body_compiles() {
use crate::bindings::commands::Kind;
let s = UiScript::new().unwrap();
for spec in SPECS {
let bodies: Vec<&str> = match &spec.kind {
Kind::Held | Kind::Host => Vec::new(),
Kind::Edge(body) => vec![*body],
Kind::EdgeUpDown(down, up) => vec![*down, *up],
};
for body in bodies {
s.run(&format!(
"local f, err = loadstring({body:?}); if not f then BenillaBodyError = {name:?} .. \": \" .. err end",
body = body,
name = spec.name
))
.unwrap_or_else(|e| panic!("{}: {e}", spec.name));
}
}
let bad: Vec<String> = s.errors();
assert!(bad.is_empty(), "script errors: {bad:?}");
s.run("if BenillaBodyError then error(BenillaBodyError) end")
.unwrap_or_else(|e| panic!("a command body is not valid Lua: {e}"));
}
@@ -165,13 +165,22 @@ fn shipped_multibars_drive_end_to_end() {
// a bonus page is active (the ref's fork: only the main bar re-pages).
s.mouse_button(26.0, 75.0, "LeftButton", true);
s.mouse_button(26.0, 75.0, "LeftButton", false);
assert_eq!(s.take_action_uses(), vec![61]);
assert_eq!(
s.take_action_uses()
.into_iter()
.map(|u| u.action)
.collect::<Vec<_>>(),
vec![61]
);
s.set_bonus_bar_offset(1);
s.fire_event("UPDATE_BONUS_ACTIONBAR", vec![]);
s.mouse_button(26.0, 75.0, "LeftButton", true);
s.mouse_button(26.0, 75.0, "LeftButton", false);
assert_eq!(
s.take_action_uses(),
s.take_action_uses()
.into_iter()
.map(|u| u.action)
.collect::<Vec<_>>(),
vec![61],
"a bonus page never re-pages a multibar"
);
+53 -22
View File
@@ -55,14 +55,18 @@ fn shipped_gossip_frame_drives_end_to_end() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// The window + its scroll frame (bar + child) + the 32-row shared pool (quest rows and option
// rows both draw from it, decision 0088 §3 — the reference's own NUMGOSSIPBUTTONS) + the close
// button + the GOODBYE button. The greeting and the NPC-name banner are FontString layers (the
// real GossipGreetingText ref l.241 / GossipFrameNpcNameText ref l.170) — not their own frames.
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
// The window + its scroll frame (bar + child) + the 32-row shared pool (quest rows and option
// rows both draw from it, decision 0088 §3 — the reference's own NUMGOSSIPBUTTONS) + the close
// button + the GOODBYE button. The greeting and the NPC-name banner are FontString layers (the
// real GossipGreetingText ref l.241 / GossipFrameNpcNameText ref l.170) — not their own frames.
assert_eq!(
load_xml(&s, "GossipFrame.xml"),
40,
"window + scroll + bar (+2 arrows) + child + 32 rows + close + goodbye"
41,
"window + greeting panel + scroll + bar (+2 arrows) + child + 32 rows + close + goodbye"
);
// Hidden by default: no gossip icon on screen.
@@ -111,8 +115,8 @@ fn shipped_gossip_frame_drives_end_to_end() {
// so the shared pool starts filling from option 1 at row 1 — decision 0088 §3).
let states: (bool, bool, bool, bool) = s
.eval(
"return GossipRow1:IsVisible(), GossipRow1:IsEnabled() ~= 0,\n\
GossipRow2:IsEnabled() ~= 0, GossipRow3:IsVisible()",
"return GossipTitleButton1:IsVisible(), GossipTitleButton1:IsEnabled() ~= 0,\n\
GossipTitleButton2:IsEnabled() ~= 0, GossipTitleButton3:IsVisible()",
)
.unwrap();
assert_eq!(
@@ -233,6 +237,10 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "GossipFrame.xml");
s.set_gossip(Some(GossipMenu {
@@ -269,10 +277,10 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
bool,
) = s
.eval(
"return GossipRow1Label:GetText(), GossipRow1:IsVisible(),\n\
GossipRow2Label:GetText(), GossipRow2:IsVisible(),\n\
GossipRow3Label:GetText(), GossipRow3:IsVisible(),\n\
GossipRow4:IsVisible()",
"return GossipTitleButton1Label:GetText(), GossipTitleButton1:IsVisible(),\n\
GossipTitleButton2Label:GetText(), GossipTitleButton2:IsVisible(),\n\
GossipTitleButton3Label:GetText(), GossipTitleButton3:IsVisible(),\n\
GossipTitleButton4:IsVisible()",
)
.unwrap();
assert_eq!((r1_text.as_str(), r1_vis), ("Report to Goldshire", true));
@@ -323,7 +331,8 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() {
// Click quest row 2 (available, "A Threat Within") → SelectGossipQuest(2) queues the seam's
// 1-based quest-row position; the app maps it to the quest id + guid and sends
// CMSG_QUESTGIVER_QUERY_QUEST (decision 0088 §3).
s.run("BenillaGossipRow_OnClick(GossipRow2)").unwrap();
s.run("BenillaGossipRow_OnClick(GossipTitleButton2)")
.unwrap();
assert_eq!(s.take_gossip_quest_selects(), vec![2]);
assert!(
s.take_gossip_selects().is_empty(),
@@ -349,6 +358,10 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "GossipFrame.xml");
// Three long options — the shape of a real judgement/roleplay menu, every one of them wrapping
@@ -410,8 +423,8 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() {
let row = |i: u32| -> (f32, f32, f32) {
s.eval::<(f32, f32, f32)>(&format!(
"return GossipRow{i}:GetTop(), GossipRow{i}:GetBottom(), \
GossipRow{i}Label:GetHeight()"
"return GossipTitleButton{i}:GetTop(), GossipTitleButton{i}:GetBottom(), \
GossipTitleButton{i}Label:GetHeight()"
))
.unwrap()
};
@@ -456,6 +469,10 @@ fn gossip_show_hide_plays_open_and_close_kits() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "GossipFrame.xml");
// Hidden at load: no open sound (never transitions on startup).
@@ -493,6 +510,10 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
load_xml(&s, "MerchantFrame.xml");
@@ -575,6 +596,10 @@ fn displacing_an_npc_window_ends_the_displaced_session() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "GossipFrame.xml");
load_xml(&s, "GameTooltip.xml"); // app load order: tooltip before merchant
load_xml(&s, "MerchantFrame.xml");
@@ -719,6 +744,10 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() {
load_xml(&s, "MerchantFrame.xml");
load_xml(&s, "BankFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the shared scroll kit the window rides
// UIPanelScrollFrameTemplate lives here, and the gossip scroll frame inherits it. NOT
// optional: a missing template is a loader *warning*, not an error, so an under-loaded
// list passes load_xml and then loses the scrollbar silently.
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "GossipFrame.xml");
// The gossip menu is open on the banker (its bank option showing).
@@ -786,6 +815,7 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, "GossipFrame.xml");
// Eight wrapping options: ~4 lines each, far past the 334 px scroll frame.
@@ -830,8 +860,8 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
// The child grew past the 334 px frame, so there is a range to scroll …
let (child_h, range): (f32, f32) = s
.eval(
"return GossipGreetingScrollChild:GetHeight(), \
GossipGreetingScroll:GetVerticalScrollRange()",
"return GossipGreetingScrollChildFrame:GetHeight(), \
GossipGreetingScrollFrame:GetVerticalScrollRange()",
)
.unwrap();
assert!(
@@ -841,7 +871,7 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
// … and the bar is up for it (it stays hidden when everything fits — the fit case is covered by
// `shipped_gossip_frame_drives_end_to_end`'s two-option menu).
assert!(
s.eval::<bool>("return GossipGreetingScrollBar:IsVisible()")
s.eval::<bool>("return GossipGreetingScrollFrameScrollBar:IsVisible()")
.unwrap(),
"the scrollbar shows once the menu overflows"
);
@@ -852,7 +882,7 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
// out with: it must be the scroll frame's own rect, and rows past the bottom must be entirely
// outside it (nothing of them survives the clip).
let (frame_top, frame_bottom): (f32, f32) = s
.eval("return GossipGreetingScroll:GetTop(), GossipGreetingScroll:GetBottom()")
.eval("return GossipGreetingScrollFrame:GetTop(), GossipGreetingScrollFrame:GetBottom()")
.unwrap();
let painted_rows =
|s: &mut UiScript| -> Vec<(benilla_ui::layout::Rect, benilla_ui::layout::Rect)> {
@@ -885,9 +915,9 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
// Scrolling pans the content up under that clip by exactly the scroll amount. Rects are y-up
// (`ExtractedQuad::rect`), so "up" means row 1's top EDGE VALUE grows as it slides off the top.
let first_top =
|s: &mut UiScript| -> f32 { s.eval::<f32>("return GossipRow1:GetTop()").unwrap() };
|s: &mut UiScript| -> f32 { s.eval::<f32>("return GossipTitleButton1:GetTop()").unwrap() };
let before = first_top(&mut s);
s.run("BenillaScroll_Step(GossipGreetingScroll, 100)")
s.run("BenillaScroll_Step(GossipGreetingScrollFrame, 100)")
.unwrap();
s.resolve();
let after = first_top(&mut s);
@@ -897,7 +927,7 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() {
);
// The bar followed the scroll (SyncBar off OnVerticalScroll), and the rows are still clipped.
assert_eq!(
s.eval::<f32>("return GossipGreetingScrollBar:GetValue()")
s.eval::<f32>("return GossipGreetingScrollFrameScrollBar:GetValue()")
.unwrap(),
100.0,
"the bar seats at the scroll offset"
@@ -932,6 +962,7 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml"); // UIPanelScrollFrameTemplate — see the note above
load_xml(&s, "GossipFrame.xml");
// The addon's three lines, in the order an addon writes them.
@@ -5,7 +5,9 @@
//! of these test: `UNIT_PET` names the OWNER (`arg1 == "player"`, wow-re §9), every other `UNIT_*`
//! names the pet itself, and a frame that mixes the two repaints off the player's health.
use benilla_ui::script::{AuraState, QuadContent, ScriptValue, UiScript, UnitState};
use benilla_ui::script::{
AuraState, QuadContent, ScriptValue, SelectionRequest, UiScript, UnitState,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error (the unit-frame tests'
/// loader).
@@ -322,11 +324,14 @@ fn left_clicking_the_pet_frame_targets_it() {
s.fire_event("UNIT_PET", vec![ScriptValue::Str("player".into())]);
s.run("PetFrame_OnClick(\"LeftButton\")").unwrap();
assert_eq!(s.take_target_requests(), vec!["pet".to_string()]);
assert_eq!(
s.take_selection_requests(),
vec![SelectionRequest::Unit("pet".into())]
);
// The right button is the deferred PET menu — it must do nothing at all, not target.
s.run("PetFrame_OnClick(\"RightButton\")").unwrap();
assert!(s.take_target_requests().is_empty());
assert!(s.take_selection_requests().is_empty());
}
/// **All three legs of `PetFrame_OnClick` survive the click** — B208's "dropping food from the bag
@@ -394,7 +399,7 @@ fn every_leg_of_the_pet_frame_click_reaches_a_live_binding() {
"a held item + a pet click queues the drop"
);
// …and it is the DROP leg, not the target leg — the reference's if/elseif is exclusive.
assert!(s.take_target_requests().is_empty());
assert!(s.take_selection_requests().is_empty());
}
/// The GlobalStrings the happiness tooltip resolves by key. Loaded from the MPQ in production; the
+44 -49
View File
@@ -100,10 +100,10 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
load_xml(&s, "QuestFrame.xml");
let panels = [
"QuestGreetingPanel",
"QuestDetailPanel",
"QuestProgressPanel",
"QuestRewardPanel",
"QuestFrameGreetingPanel",
"QuestFrameDetailPanel",
"QuestFrameProgressPanel",
"QuestFrameRewardPanel",
];
let is_shown =
|s: &UiScript, name: &str| s.eval::<bool>(&format!("return {name}:IsShown()")).unwrap();
@@ -122,12 +122,12 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
for name in panels {
assert_eq!(
is_shown(&s, name),
name == "QuestGreetingPanel",
name == "QuestFrameGreetingPanel",
"only the greeting panel is shown after QUEST_GREETING"
);
}
assert_eq!(
s.eval::<String>("return QuestNpcNameText:GetText()")
s.eval::<String>("return QuestFrameNpcNameText:GetText()")
.unwrap(),
"Deputy Willem"
);
@@ -148,7 +148,7 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
for name in panels {
assert_eq!(
is_shown(&s, name),
name == "QuestDetailPanel",
name == "QuestFrameDetailPanel",
"only the detail panel is shown after QUEST_DETAIL"
);
}
@@ -158,10 +158,13 @@ fn panel_events_show_exactly_one_child_panel_and_hide_the_others() {
);
}
/// The reward/choice grid, ported from the ref's shared `QuestFrameItems_Update` (the SAME idiom the
/// quest log's own reward rows already prove out) — now on REAL per-panel widgets
/// (`QuestDetailChoice*`/`QuestDetailReward*`), exercised on the Detail panel with the
/// fixture shape the task's capture drives: 2 choices + 1 fixed reward + money.
/// The reward grid, ported from the ref's shared `QuestFrameItems_Update` — on ONE numbered pool
/// per panel (`QuestDetailItem1..10`), the reference's own shape: choices first, then the
/// reward-spell row, then fixed rewards, each positioned at runtime. The fixture is the one the
/// task's capture drives — 2 choices + 1 fixed reward + money — so the pool reads
/// `Item1`/`Item2` = the choices, `Item3` = the fixed reward, `Item4`+ unused. It used to run on
/// two 6-deep pools named `QuestDetailChoice*`/`QuestDetailReward*`, names the reference does not
/// have and a split the ref's own function cannot be written against.
#[test]
fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
let mut s = UiScript::new().unwrap();
@@ -198,17 +201,11 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return QuestDetailChoice1:IsShown()")
.unwrap());
assert!(s
.eval::<bool>("return QuestDetailChoice2:IsShown()")
.unwrap());
assert!(!s
.eval::<bool>("return QuestDetailChoice3:IsShown()")
.unwrap());
assert!(s.eval::<bool>("return QuestDetailItem1:IsShown()").unwrap());
assert!(s.eval::<bool>("return QuestDetailItem2:IsShown()").unwrap());
assert!(!s.eval::<bool>("return QuestDetailItem4:IsShown()").unwrap());
assert_eq!(
s.eval::<String>("return QuestDetailChoice1Name:GetText()")
s.eval::<String>("return QuestDetailItem1Name:GetText()")
.unwrap(),
"Worn Sword"
);
@@ -223,14 +220,10 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
"You will also receive:",
"choices present -> the ref's \"also\" wording (QuestFrame.lua:461-462)"
);
assert!(s
.eval::<bool>("return QuestDetailReward1:IsShown()")
.unwrap());
assert!(!s
.eval::<bool>("return QuestDetailReward2:IsShown()")
.unwrap());
assert!(s.eval::<bool>("return QuestDetailItem3:IsShown()").unwrap());
assert!(!s.eval::<bool>("return QuestDetailItem4:IsShown()").unwrap());
assert_eq!(
s.eval::<String>("return QuestDetailReward1Name:GetText()")
s.eval::<String>("return QuestDetailItem3Name:GetText()")
.unwrap(),
"Militia Hammer"
);
@@ -244,7 +237,7 @@ fn detail_panel_reward_grid_follows_the_refs_two_per_row_layout() {
// select). They DO carry an OnClick now (`QuestItem_OnClick`, the ref's own
// QuestItemTemplate script — the ctrl/shift fork, decisions 1059/1060), but it has no select
// arm at all: an unmodified click must not raise and must not set an itemChoice.
s.run("QuestDetailChoice1:Click()").ok();
s.run("QuestDetailItem1:Click()").ok();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.eval::<i64>("return QuestFrame.itemChoice").unwrap(),
@@ -289,26 +282,26 @@ fn reward_panel_choice_click_selects_and_completes_with_zero_based_index() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
!s.eval::<bool>("return QuestRewardChoiceHighlight:IsShown()")
!s.eval::<bool>("return QuestRewardItemHighlight:IsShown()")
.unwrap(),
"no row picked yet"
);
// Completing without a choice picked is a no-op (ref: QuestChooseRewardError guard).
s.run("QuestRewardCompleteButton:Click()").unwrap();
s.run("QuestFrameCompleteQuestButton:Click()").unwrap();
assert!(
s.take_quest_actions().is_empty(),
"must pick a choice first"
);
s.run("QuestRewardChoice2:Click()").unwrap();
s.run("QuestRewardItem2:Click()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
s.eval::<bool>("return QuestRewardChoiceHighlight:IsShown()")
s.eval::<bool>("return QuestRewardItemHighlight:IsShown()")
.unwrap(),
"picking a choice row shows the highlight"
);
s.run("QuestRewardCompleteButton:Click()").unwrap();
s.run("QuestFrameCompleteQuestButton:Click()").unwrap();
assert_eq!(
s.take_quest_actions(),
vec![benilla_ui::script::QuestAction::Reward(1)],
@@ -341,7 +334,7 @@ fn greeting_goodbye_button_closes_the_window() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s.eval::<bool>("return QuestFrame:IsVisible()").unwrap());
s.run("QuestGreetingGoodbyeButton:Click()").unwrap();
s.run("QuestFrameGreetingGoodbyeButton:Click()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
!s.eval::<bool>("return QuestFrame:IsVisible()").unwrap(),
@@ -375,10 +368,10 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return QuestAcceptButton:IsShown()")
.eval::<bool>("return QuestFrameAcceptButton:IsShown()")
.unwrap());
assert!(s
.eval::<bool>("return QuestDeclineButton:IsShown()")
.eval::<bool>("return QuestFrameDeclineButton:IsShown()")
.unwrap());
s.resolve();
@@ -434,7 +427,7 @@ fn detail_panel_action_buttons_resolve_to_real_onscreen_rects() {
"instant text keeps the writing sound: one scratch on the wake tick"
);
assert!(s
.eval::<bool>("return QuestAcceptButton:IsEnabled() ~= 0")
.eval::<bool>("return QuestFrameAcceptButton:IsEnabled() ~= 0")
.unwrap());
assert_eq!(
s.eval::<f32>("return QuestDetailTextAlphaFrame:GetAlpha()")
@@ -494,7 +487,7 @@ fn write_on_still_fades_when_instant_text_is_off() {
"the write-on scratches the WriteQuest quill each tick"
);
assert!(!s
.eval::<bool>("return QuestAcceptButton:IsEnabled() ~= 0")
.eval::<bool>("return QuestFrameAcceptButton:IsEnabled() ~= 0")
.unwrap());
// Half a second more runs the 13-char gradient off (40 chars/s): Accept wakes and the block
@@ -502,7 +495,7 @@ fn write_on_still_fades_when_instant_text_is_off() {
s.tick(0.5);
assert!(s.errors().is_empty(), "tick errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return QuestAcceptButton:IsEnabled() ~= 0")
.eval::<bool>("return QuestFrameAcceptButton:IsEnabled() ~= 0")
.unwrap());
let alpha = s
.eval::<f32>("return QuestDetailTextAlphaFrame:GetAlpha()")
@@ -546,7 +539,7 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.eval::<String>("return getglobal('QuestNpcNameText'):GetText() or ''")
s.eval::<String>("return getglobal('QuestFrameNpcNameText'):GetText() or ''")
.unwrap(),
"Marshal McBride",
"the panel-open event's arg1 lands in the title bar"
@@ -568,13 +561,14 @@ fn npc_name_reaches_the_title_bar_on_open_and_on_refresh() {
);
// The late-name path: open with an empty name, the refresh brings it.
s.run("getglobal('QuestNpcNameText'):SetText('')").unwrap();
s.run("getglobal('QuestFrameNpcNameText'):SetText('')")
.unwrap();
s.fire_event(
"QUEST_ITEM_UPDATE",
vec![ScriptValue::Str("Marshal McBride".into())],
);
assert_eq!(
s.eval::<String>("return getglobal('QuestNpcNameText'):GetText() or ''")
s.eval::<String>("return getglobal('QuestFrameNpcNameText'):GetText() or ''")
.unwrap(),
"Marshal McBride",
"the QUEST_ITEM_UPDATE refresh also carries the name"
@@ -718,14 +712,14 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
// The regression this fork could break: a PLAIN click still picks the choice (the ref's third
// arm). Row 2 is picked here so the modified clicks below — aimed at row 1 — would visibly
// move it if they leaked into the select arm.
s.run("QuestRewardChoice2:Click()").unwrap();
s.run("QuestRewardItem2:Click()").unwrap();
assert_eq!(
s.eval::<i64>("return QuestFrame.itemChoice").unwrap(),
2,
"a plain click still selects the reward choice"
);
assert!(s
.eval::<bool>("return QuestRewardChoiceHighlight:IsShown()")
.eval::<bool>("return QuestRewardItemHighlight:IsShown()")
.unwrap());
assert!(
s.take_dressup_intents().is_empty(),
@@ -735,7 +729,7 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
// SHIFT + chat open → the row's full escaped link, and the selection is untouched.
assert!(s.focus_editbox("ChatFrameEditBox"));
s.set_modifiers(true, false, false);
s.run("QuestRewardChoice1:Click()").unwrap();
s.run("QuestRewardItem1:Click()").unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
s.eval::<String>("return ChatFrameEditBox:GetText()")
@@ -749,10 +743,11 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
"the shift arm returns — it must NOT also select the clicked choice"
);
// A FIXED reward row (plain QuestItemTemplate → QuestItem_OnClick) posts too.
// A FIXED reward row posts too — same pool, same template now, and the select arm is gated
// by `button.kind == "choice"` rather than by which pool the row came from.
s.run("ChatFrameEditBox:SetText(\"\")").unwrap();
s.set_modifiers(true, false, false);
s.run("QuestRewardReward1:Click()").unwrap();
s.run("QuestRewardItem3:Click()").unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
s.eval::<String>("return ChatFrameEditBox:GetText()")
@@ -763,7 +758,7 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
// CTRL → the dressing room wearing the clicked reward, and STILL no reselect. The intent pair
// is ordered: Dress (the room was closed, so it re-dresses in the player's own gear) then TryOn.
s.set_modifiers(false, true, false);
s.run("QuestRewardChoice1:Click()").unwrap();
s.run("QuestRewardItem1:Click()").unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
s.take_dressup_intents(),
@@ -102,6 +102,12 @@ fn shipped_questlog_frame_loads_clean() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
}
@@ -117,6 +123,12 @@ fn shipped_questlog_frame_drives_end_to_end() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -145,7 +157,8 @@ fn shipped_questlog_frame_drives_end_to_end() {
.unwrap()
.contains("Quest 1"));
assert_eq!(
s.eval::<String>("return QuestLogCount:GetText()").unwrap(),
s.eval::<String>("return QuestLogQuestCount:GetText()")
.unwrap(),
"Quests: |cffffffff8/20|r"
);
assert_eq!(s.eval::<i64>("return GetQuestLogSelection()").unwrap(), 1);
@@ -163,7 +176,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
);
// The reward row (QuestFrame.xml's item-row pattern, reused) + its money.
assert_eq!(
s.eval::<String>("return QuestLogReward1Name:GetText()")
s.eval::<String>("return QuestLogItem1Name:GetText()")
.unwrap(),
"Militia Hammer"
);
@@ -192,7 +205,9 @@ fn shipped_questlog_frame_drives_end_to_end() {
.unwrap(),
""
);
assert!(!s.eval::<bool>("return QuestLogChoice1:IsShown()").unwrap());
// No choices, so the pool's first slot IS the fixed reward and the SECOND is the first
// unused one. (One pool now — the ref's shape; there is no separate "choice row 1".)
assert!(!s.eval::<bool>("return QuestLogItem2:IsShown()").unwrap());
// Wheel DOWN over an actual list ROW (delta -1) advances the faux-scroll offset by one: row 1 now
// shows entry 2's title. Aiming over a row (not the empty margin) is the case that matters — a spin
@@ -292,6 +307,12 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -407,6 +428,12 @@ fn watch_guards_no_op_without_erroring() {
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
load_xml(&s, "ErrorsFrame.xml"); // the guards' red-line surface
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -494,6 +521,12 @@ fn progress_auto_watches_for_five_minutes() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -550,6 +583,12 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
assert_eq!(s.eval::<String>("return AUTO_QUEST_WATCH").unwrap(), "1");
@@ -583,6 +622,12 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
@@ -592,13 +637,13 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
// A bare FontString region has no Show/Hide/IsVisible in this engine (only Frames/Buttons do —
// `region.rs`'s method table) — the empty-state message is a toggled SetText, read back here.
assert_eq!(
s.eval::<String>("return QuestLogEmptyText:GetText()")
s.eval::<String>("return QuestLogNoQuestsText:GetText()")
.unwrap(),
"Your quest log is empty."
);
assert!(!s.eval::<bool>("return QuestLogTitle1:IsVisible()").unwrap());
assert!(!s
.eval::<bool>("return QuestLogAbandonButton:IsEnabled() ~= 0")
.eval::<bool>("return QuestLogFrameAbandonButton:IsEnabled() ~= 0")
.unwrap());
// The Description header is now Lua-managed (BenillaQuestLogDetail_Clear blanks it) rather than
// a static `text=` — it used to float "Description" over the empty-log parchment with no
@@ -609,7 +654,8 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
""
);
assert_eq!(
s.eval::<String>("return QuestLogCount:GetText()").unwrap(),
s.eval::<String>("return QuestLogQuestCount:GetText()")
.unwrap(),
"Quests: |cffffffff0/20|r"
);
}
@@ -626,6 +672,12 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
let mut state = eight_entries();
@@ -669,16 +721,18 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// Both choice rows show, the rest of the 6-row cap stays hidden.
assert!(s.eval::<bool>("return QuestLogChoice1:IsShown()").unwrap());
assert!(s.eval::<bool>("return QuestLogChoice2:IsShown()").unwrap());
assert!(!s.eval::<bool>("return QuestLogChoice3:IsShown()").unwrap());
// 2 choices then 1 fixed reward, in ONE pool: Item1/Item2 are the choices, Item3 the reward,
// Item4 the first unused slot.
assert!(s.eval::<bool>("return QuestLogItem1:IsShown()").unwrap());
assert!(s.eval::<bool>("return QuestLogItem2:IsShown()").unwrap());
assert!(!s.eval::<bool>("return QuestLogItem4:IsShown()").unwrap());
assert_eq!(
s.eval::<String>("return QuestLogChoice1Name:GetText()")
s.eval::<String>("return QuestLogItem1Name:GetText()")
.unwrap(),
"Worn Sword"
);
assert_eq!(
s.eval::<String>("return QuestLogChoice2Name:GetText()")
s.eval::<String>("return QuestLogItem2Name:GetText()")
.unwrap(),
"Worn Mace"
);
@@ -700,10 +754,10 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
);
// One fixed reward shows, chained under the receive text (its own SetPoint target).
assert!(s.eval::<bool>("return QuestLogReward1:IsShown()").unwrap());
assert!(!s.eval::<bool>("return QuestLogReward2:IsShown()").unwrap());
assert!(s.eval::<bool>("return QuestLogItem3:IsShown()").unwrap());
assert!(!s.eval::<bool>("return QuestLogItem4:IsShown()").unwrap());
assert_eq!(
s.eval::<String>("return QuestLogReward1Name:GetText()")
s.eval::<String>("return QuestLogItem3Name:GetText()")
.unwrap(),
"Militia Hammer"
);
@@ -715,7 +769,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
);
}
/// One entry whose selected detail overflows the 261px-tall `QuestLogDetailScroll`: 10
/// One entry whose selected detail overflows the 261px-tall `QuestLogDetailScrollFrame`: 10
/// objective lines (each a FIXED 12px slot, `QuestLogObjective1`'s own XML comment) plus a
/// long description and a reward row push the child's summed height (`BenillaQuestLogDetail_
/// ResizeChild`) well past the pane — load-bearing for the scroll/clip tests below even
@@ -761,8 +815,8 @@ fn overflowing_entry() -> QuestLogState {
}
/// The ScrollFrame clip (decision 0112 §4): every quad in the moved detail chain's subtree —
/// `QuestLogDetailChild` and its descendants (here, a reward row's icon-slot texture) —
/// carries `QuestLogDetailScroll`'s own resolved rect as its clip, so content that overflows
/// `QuestLogDetailScrollChildFrame` and its descendants (here, a reward row's icon-slot texture) —
/// carries `QuestLogDetailScrollFrame`'s own resolved rect as its clip, so content that overflows
/// the pane never draws past its bottom edge; a sibling entirely outside the scroll child (the
/// window's own book-icon chrome) stays unclipped, mirroring `scrollframe.rs`'s own unit test for
/// the mechanism, just through the shipped window instead of a synthetic fixture.
@@ -774,6 +828,12 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -830,6 +890,12 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -843,14 +909,14 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
let (x, y) = (scroll_rect.left + 10.0, scroll_rect.top - 10.0);
assert_eq!(
s.eval::<f32>("return QuestLogDetailScroll:GetVerticalScroll()")
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
.unwrap(),
0.0
);
s.mouse_wheel(x, y, -1.0); // WoW convention: -1 = down
assert!(s.errors().is_empty(), "wheel errors: {:?}", s.errors());
let after = s
.eval::<f32>("return QuestLogDetailScroll:GetVerticalScroll()")
.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
.unwrap();
assert!(
after > 0.0,
@@ -870,6 +936,12 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -877,9 +949,10 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s.resolve(); // GetVerticalScrollRange (SetVerticalScroll's clamp) reads resolved rects.
s.run("QuestLogDetailScroll:SetVerticalScroll(10)").unwrap();
s.run("QuestLogDetailScrollFrame:SetVerticalScroll(10)")
.unwrap();
assert_eq!(
s.eval::<f32>("return QuestLogDetailScroll:GetVerticalScroll()")
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
.unwrap(),
10.0
);
@@ -888,7 +961,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
s.fire_event("QUEST_LOG_UPDATE", vec![]);
assert!(s.errors().is_empty(), "refresh errors: {:?}", s.errors());
assert_eq!(
s.eval::<f32>("return QuestLogDetailScroll:GetVerticalScroll()")
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
.unwrap(),
10.0,
"QUEST_LOG_UPDATE's data-refresh path must not yank the scroll position (pin §5)"
@@ -899,7 +972,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
.unwrap();
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
assert_eq!(
s.eval::<f32>("return QuestLogDetailScroll:GetVerticalScroll()")
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
.unwrap(),
0.0,
"a manual reselect snaps the detail pane's scroll back to the top"
@@ -925,6 +998,12 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
load_xml(&s, "UIDropDownMenu.xml");
load_xml(&s, "LootFrame.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -1053,6 +1132,12 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
@@ -1062,13 +1147,13 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
s.run("ToggleQuestLog()").unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
s.eval::<bool>("return QuestLogReward1:IsShown()").unwrap(),
s.eval::<bool>("return QuestLogItem1:IsShown()").unwrap(),
"the fixture's one fixed reward row is laid out"
);
let _ = s.take_dressup_intents();
// A PLAIN click is inert: no room, no chat text, no error (the row has no select arm).
s.run("QuestLogReward1:Click()").unwrap();
s.run("QuestLogItem1:Click()").unwrap();
assert!(
s.take_dressup_intents().is_empty(),
"a plain click never opens the dressing room"
@@ -1084,7 +1169,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
// SHIFT + chat open → the reward's full escaped link (ref l.547-550).
assert!(s.focus_editbox("ChatFrameEditBox"));
s.set_modifiers(true, false, false);
s.run("QuestLogReward1:Click()").unwrap();
s.run("QuestLogItem1:Click()").unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
s.eval::<String>("return ChatFrameEditBox:GetText()")
@@ -1099,7 +1184,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
// CTRL → the room wearing the reward (ref l.543-546): re-dress first, then try it on.
s.set_modifiers(false, true, false);
s.run("QuestLogReward1:Click()").unwrap();
s.run("QuestLogItem1:Click()").unwrap();
s.set_modifiers(false, false, false);
assert_eq!(
s.take_dressup_intents(),
@@ -1126,6 +1211,12 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
@@ -1210,6 +1301,12 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
// Solo, quest 1 sharable and selected by the auto-selection.
@@ -1226,7 +1323,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
"quest 1 carries the sharable bit"
);
assert!(
!s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
!s.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
.unwrap(),
"solo: sharable is not enough"
);
@@ -1235,7 +1332,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
s.set_party(party(2));
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
assert!(
s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
s.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
.unwrap(),
"PARTY_MEMBERS_CHANGED alone re-tests the button"
);
@@ -1249,7 +1346,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
"quest 2 has no sharable bit"
);
assert!(
!s.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
!s.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
.unwrap(),
"in a party: an unsharable selection is still dark"
);
@@ -1258,12 +1355,12 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
s.run("SelectQuestLogEntry(1)").unwrap();
s.run("BenillaQuestLogFrame_Update()").unwrap();
assert!(s
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
.unwrap());
s.set_party(PartyState::default());
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
assert!(!s
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
.unwrap());
}
@@ -1278,13 +1375,19 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
s.set_party(party(4));
s.run("ToggleQuestLog()").unwrap();
assert!(!s
.eval::<bool>("return QuestLogPushQuestButton:IsEnabled() ~= 0")
.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -1300,6 +1403,12 @@ fn share_quest_click_queues_the_selected_quests_id() {
load_xml(&s, "MoneyFrame.xml");
load_xml(&s, "UiPanels.xml");
load_xml(&s, "MerchantFrame.xml");
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
// silently.
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, "UIPanelTemplates.xml");
load_xml(&s, "QuestLogFrame.xml");
s.set_quest_log(entries_sharable(&[1, 5]));
@@ -1308,7 +1417,7 @@ fn share_quest_click_queues_the_selected_quests_id() {
s.run("SelectQuestLogEntry(5)").unwrap();
s.run("BenillaQuestLogFrame_Update()").unwrap();
s.run("QuestLogPushQuestButton:Click()").unwrap();
s.run("QuestFramePushQuestButton:Click()").unwrap();
assert_eq!(s.take_quest_log_pushes(), vec![5]);
assert!(s.take_quest_log_pushes().is_empty(), "drained");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
@@ -12,7 +12,8 @@
//! sweep. Each test below fails on exactly one of those.
use benilla_ui::script::{
PartyMemberInfo, PartyRequest, PartyState, RaidMemberInfo, SavedInstanceInfo, UiScript,
PartyMemberInfo, PartyRequest, PartyState, RaidMemberInfo, SavedInstanceInfo, SelectionRequest,
UiScript,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error **or unknown-template
@@ -600,7 +601,10 @@ fn left_clicking_a_row_targets_its_raid_token() {
open_raid_tab(&mut s);
push_raid(&mut s, twelve());
s.run("RaidGroupButton7:Click(\"LeftButton\")").unwrap();
assert_eq!(s.take_target_requests(), vec!["raid7".to_string()]);
assert_eq!(
s.take_selection_requests(),
vec![SelectionRequest::Unit("raid7".into())]
);
}
/// Right-clicking a row opens the RAID menu, and each of its four rows appears for exactly the
@@ -64,12 +64,17 @@ fn skill(
fn shown_skills_page() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// UIPanelTemplates.xml is NOT optional even though this list ran without it for a year: a
// MISSING template is a loader *warning*, not an error, so an under-loaded list passes the
// assert in load_xml and then fails later on geometry that silently never got built.
// SkillDetailScrollFrame inherits UIPanelScrollFrameTemplate from that file.
for f in [
"Fonts.xml",
"MoneyFrame.xml",
"UiPanels.xml",
"GameTooltip.xml",
"ScrollTemplates.xml",
"UIPanelTemplates.xml",
"CharacterFrame.xml",
"SkillFrame.xml",
] {
@@ -14,8 +14,8 @@
//! `TargetFrame_OnUpdate`'s one-compare reconcile is for, and it has a test.
use benilla_ui::script::{
AuraState, PartyMemberInfo, PartyState, QuadContent, RaidMemberInfo, ScriptValue, UiScript,
UnitState,
AuraState, PartyMemberInfo, PartyState, QuadContent, RaidMemberInfo, ScriptValue,
SelectionRequest, UiScript, UnitState,
};
/// Load one shipped `assets/ui/<file>`, panicking on any loader error (the unit-frame tests').
@@ -378,11 +378,14 @@ fn the_left_click_targets_the_unit() {
let mut s = load_tot();
switch_on(&mut s);
s.run(r#"TargetofTarget_OnClick("LeftButton")"#).unwrap();
assert_eq!(s.take_target_requests(), vec!["targettarget".to_string()]);
assert_eq!(
s.take_selection_requests(),
vec![SelectionRequest::Unit("targettarget".into())]
);
// A right click is not a menu here: the reference gives this frame none.
s.run(r#"TargetofTarget_OnClick("RightButton")"#).unwrap();
assert!(s.take_target_requests().is_empty());
assert!(s.take_selection_requests().is_empty());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -1,4 +1,6 @@
use benilla_ui::script::{QuadContent, ScriptValue, SoundRequest, UiScript, UnitState};
use benilla_ui::script::{
QuadContent, ScriptValue, SelectionRequest, SoundRequest, UiScript, UnitState,
};
/// Load one shipped `assets/ui/<file>` (the panel tests' loader, duplicated to stay
/// self-contained), panicking on any loader error.
@@ -383,7 +385,7 @@ fn left_clicking_the_player_frame_targets_self() {
s.fire_event("UNIT_HEALTH", vec![ScriptValue::Str("player".into())]);
s.resolve();
assert!(
s.take_target_requests().is_empty(),
s.take_selection_requests().is_empty(),
"no request before any click"
);
@@ -394,7 +396,7 @@ fn left_clicking_the_player_frame_targets_self() {
s.mouse_button(cx as f32, cy as f32, "RightButton", true);
s.mouse_button(cx as f32, cy as f32, "RightButton", false);
assert!(
s.take_target_requests().is_empty(),
s.take_selection_requests().is_empty(),
"right-click queues no target"
);
// Solo, every SELF row is gated off (only CANCEL survives) — the ref opens no menu. 1.12 has
@@ -408,8 +410,8 @@ fn left_clicking_the_player_frame_targets_self() {
s.mouse_button(cx as f32, cy as f32, "LeftButton", true);
s.mouse_button(cx as f32, cy as f32, "LeftButton", false);
assert_eq!(
s.take_target_requests(),
vec!["player"],
s.take_selection_requests(),
vec![SelectionRequest::Unit("player".into())],
"left-click queues a self-target"
);