the loading screen lets nothing through, and the micro buttons go stock

- units could be hovered through the loading screen — the sword cursor gave it away: the world pick chain has no state gate, one of fifty-one input sites, so a gate per consumer is a rule that regresses; the cut is at the source instead — one `PreUpdate` system empties the button planes, drains the raw queues, zeroes the accumulators and blanks the window's cursor position while the cover is up (the alt-tabbed-away state every consumer already handles); a press under the cover never happened, a held button is released once, a gesture in flight is cancelled, and a wall test counts the channels; the reference does the same — its raise registers a veto stub (`0x406a50`) on five event-bus categories — and one gap closed with it: the reference parks the plain arrow and drops any item overlay before the screen goes up, where benilla kept a held item's icon over the loading art (1986, 1990)
- the micro-button row is the reference's own `MainMenuBarMicroButtons.xml`, our transcription deleted — no engine verb was missing, and the whole engine-side cost is one alias, `SoundOptionsFrame = OptionsFrame`, because the stock file reads the reference's three options windows and ours is the one window for all three; what the stock file brings back: `UpdateTalentButton`'s own −2 re-seat (the five buttons after the talent button sit 1 px further right — the reference's own pixel), the raw binding token in the hover label (`(C)`, `(SHIFT-P)`), the sixty-second talent-button pulse on a ding while the sheet is closed (0304's scope-out retired), the keyring arm, and "Abilities" on the spellbook hover for a character with no spells; `BenillaActionBarArt_SeatAbove` goes with the file — a child is born one level above its parent (`SetParent` `0x76ab65`), the seat it set by hand (1987)
- hovering the player arrow on the stock world map showed an empty tooltip: narrower than 4:3 the map is scaled under 1, and our `MouseIsOver` had dropped the reference's division by `GetEffectiveScale` as "always 1" — the frame getters answer in the frame's own units, `GetCursorPosition` in the screen's; the division is back, and the region getters divide by their owner's scale too; and the ready check times out: the stock `UIParent.xml` ticks `CheckReadyCheckTime` every frame, the one unbound name in that file's way, so the mechanism behind it is built — a 30 s deadline and a "has not answered" flag per roster entry, the leader getting no popup while members print "%s has initiated a ready check" and get it, and on expiry `RAID_MEMBERS_AFK` with the unanswered names or `READY_CHECK_NO_AFK`, the last answer closing the check at once (1985, 1989)
- a review of the floor grind closed eight stale-input and bypassed-side-effect siblings the gates had left open: a never-placed card places on a still frame, the dormant-emitter skip sits below the emitter's lifetime blocks, PVS changes (not only spawns) un-still the gates, the appear ramp's retirement and a far-side unmark come due, a host born under a parked camera is judged, the projection counts as a still input, a fresh palette range is zeroed, the stable feed runs once more after a close, the unit publisher's viewer query is filtered, solo UI materials are reused by key, and the draw-list cache compares exactly; and three doc links the chat window's stock move (1948) left dangling point at what replaced them — the colour table and the tell ring are the stock Lua's now — which is what had the doc-links gate (1925) red on main

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 21:01:38 +07:00
co-authored by Claude Fable 5.1
parent e800974806
commit bc1a2428dd
59 changed files with 2135 additions and 772 deletions
@@ -414,7 +414,7 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"Interface\\FrameXML\\QuestFrame.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
"Interface\\FrameXML\\QuestLogFrame.xml",
] {
load_xml(&s, f);
@@ -131,7 +131,7 @@ fn an_error_toast_draws_over_an_open_panel_window() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// A left-slot panel open, and the toast raised after it — the order that must not decide.
@@ -574,10 +574,10 @@ fn the_shipped_frames_carry_the_references_flags() {
// frames are the reference's own now), which took the paired count from the low 400s to
// the high 150s; 1966 (TradeFrame), 1968 (GameTooltip), 1969 (DressUpFrame) and 1970
// (MailFrame) to the high 80s, and 1980 (WorldMapFrame, whose 50-odd blip frames were
// most of what was left) to the high teens. The floor guards the pairing, not the
// census — it comes down with every window that migrates, and reaches zero with the
// last file of ours that declares a reference-named frame.
compared > 10,
// most of what was left) to the high teens, and 1987 (the micro row) to nine. The floor
// guards the pairing, not the census — it comes down with every window that migrates,
// and reaches zero with the last file of ours that declares a reference-named frame.
compared > 5,
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
);
assert!(
@@ -577,7 +577,7 @@ fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
r"Interface\FrameXML\WorldMapFrame.xml",
]);
@@ -638,7 +638,7 @@ fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
r"Interface\FrameXML\WorldMapFrame.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
// The loot window is the reference's own since 1751 — see `test_ui::LOOT_UI` for what
@@ -38,7 +38,7 @@ fn setup() -> UiScript {
"Interface\\FrameXML\\StaticPopup.xml",
// The stock file's `HelpFrame_OnShow` calls `UpdateMicroButtons()` before
// `GetGMStatus()`, so without the micro row the OnShow raises and the status ask never
// happens. Ours never called it. MicroMenu needs the bar's button kit under it.
// happens. Ours never called it. The row needs the bar's button kit under it.
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
@@ -47,7 +47,7 @@ fn setup() -> UiScript {
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
// `TicketStatusFrame_OnEvent` re-anchors TemporaryEnchantFrame before it arms the repoll
// timer, so without BuffFrame the handler raises and `refreshTime` is never set. This is
// the one ordering requirement benilla.toc already calls out for this window.
@@ -108,7 +108,7 @@ fn harness_with(player: &str) -> UiScript {
r"Interface\FrameXML\UIPanelTemplates.xml",
// `ClassTrainerListScrollFrameTemplate` — the icon chooser's scroll frame inherits it.
r"Interface\FrameXML\ClassTrainerFrameTemplates.xml",
"MicroMenu.xml", // stock `MacroFrame_OnShow`/`_OnHide` drive the micro button
r"Interface\FrameXML\MainMenuBarMicroButtons.xml", // stock `MacroFrame_OnShow`/`_OnHide` drive the micro button
] {
super::test_ui::load_ui(&s, file);
}
@@ -1,85 +1,119 @@
//! The shipped `assets/ui/MicroMenu.xml` — the eight micro buttons in the main bar's right-hand
//! recess — loaded behind `ActionBar.xml` (their anchor target `MainMenuBarArtFrame` and the
//! `BenillaActionBarArt_SeatAbove` seating helper) into a bare engine.
//! The stock `Interface\FrameXML\MainMenuBarMicroButtons.xml` — the eight micro buttons in the
//! main bar's right-hand recess — off the player's chain behind the stock bar (their anchor
//! target `MainMenuBarArtFrame`) into a bare engine (decision 1987).
//!
//! What these guard, in order: the row's ref geometry (29×58 at art-frame BOTTOMLEFT +(552,2), a
//! 26 px stride) — which is the whole point of the slice, since the gap the row fills is exactly
//! that panel of empty bar art; the `HitRectInsets` header that keeps a button's transparent top
//! from eating the mouse; `UpdateMicroButtons` tracking a panel's visibility; and
//! `UpdateTalentButton`'s level-10 gate closing the row up behind the hidden talent button.
//! What these guard, in order: the row's geometry (29×58 at art-frame BOTTOMLEFT +(552,2), a
//! 26 px stride, and the reference's own 1 px nudge once the talent gate has run); the
//! `HitRectInsets` header that keeps a button's transparent top from eating the mouse; the
//! character button's portrait crop; `UpdateTalentButton`'s level-10 gate closing the row up
//! behind the hidden talent button; the two-line hover with the bound key in its label; and,
//! under the whole shipped manifest, `UpdateMicroButtons` following a real panel and the
//! level-up pulse the reference hangs on the talent button.
use benilla_ui::script::{QuadContent, ScriptValue, TexCoords, UiScript};
use benilla_ui::script::{QuadContent, ScriptValue, TexCoords, UiScript, UnitState};
/// Load `ActionBar.xml` then `MicroMenu.xml` into a 1024×768 engine, asserting both load clean.
/// 1024 wide is deliberate: the 1024-wide bar then centers at x=0, so every ref offset below is
/// The eight, in row order.
const ROW: [&str; 8] = [
"CharacterMicroButton",
"SpellbookMicroButton",
"TalentMicroButton",
"QuestLogMicroButton",
"SocialsMicroButton",
"WorldMapMicroButton",
"MainMenuMicroButton",
"HelpMicroButton",
];
/// The stock bar, then the stock row, into a 1024×768 engine, asserting both load clean. 1024
/// wide is deliberate: the 1024-wide bar then centers at x=0, so every reference offset below is
/// also an absolute screen coordinate.
fn harness() -> UiScript {
harness_with(&[])
}
/// …with more shipped files behind the row — the tooltip test needs the real plate.
fn harness_with(extra: &[&str]) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
let files: Vec<&str> = [
for file in [
"Interface\\FrameXML\\Fonts.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"UIParent.xml",
// The eight labels and their NEWBIE_TOOLTIP_* lines, read at each button's OnLoad…
"Interface\\FrameXML\\GlobalStrings.lua",
// …through `TEXT()`.
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
]
.into_iter()
.chain(extra.iter().copied())
.collect();
// Through `test_ui::load_ui`, not off disk: the list above now names a chain file, and a
// reader that joins `assets/ui` cannot resolve one (the wall 1838 hit, and 1887 again).
for file in files {
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
] {
super::test_ui::load_ui(&s, file);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
}
/// The row's geometry, quoted from ref-MainMenuBarMicroButtons.xml: CharacterMicroButton at the
/// art frame's BOTTOMLEFT +(552,2), each button 29×58, the rest chained BOTTOMLEFT-to-previous-
/// BOTTOMRIGHT +(-3,0) ⇒ a 26 px stride, so the eight run x 552..757 — inside the 1024-wide bar,
/// between the page arrows (x≈522) and the bag cluster at the far right.
/// Set the player's level and fire the `UNIT_LEVEL` the talent gate listens for.
fn set_player_level(s: &mut UiScript, level: u32) {
s.set_unit(
"player",
Some(UnitState {
exists: true,
level,
..Default::default()
}),
);
s.fire_event("UNIT_LEVEL", vec![ScriptValue::Str("player".into())]);
}
fn left_of(s: &UiScript, name: &str) -> f64 {
s.eval::<f64>(&format!("return {name}:GetLeft()"))
.unwrap_or_else(|e| panic!("{name}: {e}"))
}
/// The row's geometry, the reference's own numbers: CharacterMicroButton at the art frame's
/// BOTTOMLEFT +(552,2), each button 29×58, the rest chained BOTTOMLEFT-to-previous-BOTTOMRIGHT
/// +(-3,0) ⇒ a 26 px stride, so the eight run x 552..757 as declared — between the page arrows
/// (x≈522) and the bag cluster at the far right. **As declared** is the operative phrase: the XML
/// seats the quest-log button at −3 and `UpdateTalentButton` re-seats it at −2
/// (`MainMenuBarMicroButtons.lua:139`), so the first level pass at 10+ nudges the tail right by
/// one pixel. A real row is 1 px wider past its first `PLAYER_ENTERING_WORLD` than its XML
/// says; that is the client's, and it is asserted here as the reference's law rather than
/// sanded off (our retired file wrote −3 both ways).
#[test]
fn the_micro_row_sits_where_the_reference_puts_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
// Past level 10 so all eight are shown and the row is unbroken.
set_player_level(&mut s, 10);
s.resolve();
let names = [
"CharacterMicroButton",
"SpellbookMicroButton",
"TalentMicroButton",
"QuestLogMicroButton",
"SocialsMicroButton",
"WorldMapMicroButton",
"MainMenuMicroButton",
"HelpMicroButton",
];
for (i, name) in names.iter().enumerate() {
// As declared, before any level pass: a clean 26 px stride.
for (i, name) in ROW.iter().enumerate() {
let (left, bottom, w, h) = s
.eval::<(f64, f64, f64, f64)>(&format!(
"return {name}:GetLeft(), {name}:GetBottom(), {name}:GetWidth(), {name}:GetHeight()"
))
.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!((w, h), (29.0, 58.0), "{name} size");
assert_eq!(left, 552.0 + 26.0 * i as f64, "{name} left edge");
assert_eq!(
left,
552.0 + 26.0 * i as f64,
"{name} left edge, as declared"
);
assert_eq!(bottom, 2.0, "{name} sits 2 above the bar's bottom");
}
// Past level 10 the gate re-seats the quest-log button at −2: the tail moves 1 px right.
set_player_level(&mut s, 60);
s.resolve();
for (i, name) in ROW.iter().enumerate().skip(3) {
assert_eq!(
left_of(&s, name),
553.0 + 26.0 * i as f64,
"{name} after the talent gate's own −2"
);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -88,8 +122,8 @@ fn the_micro_row_sits_where_the_reference_puts_it() {
/// bar's XP strip and the sliver of world above it.
#[test]
fn the_transparent_top_of_a_micro_button_does_not_capture_the_mouse() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_player_level(&mut s, 10);
s.resolve();
// Mid-button horizontally (552 + 14), inside the art band (y 2..42) ⇒ captures.
@@ -98,7 +132,7 @@ fn the_transparent_top_of_a_micro_button_does_not_capture_the_mouse() {
Some("CharacterMicroButton"),
"the art band takes the mouse"
);
// Same column, in the dead 18-unit header (y 42..60) ⇒ nothing of ours captures it.
// Same column, in the dead 18-unit header (y 42..60) ⇒ nothing of the row captures it.
assert_ne!(
s.hit_test_name(566.0, 50.0).as_deref(),
Some("CharacterMicroButton"),
@@ -107,13 +141,14 @@ fn the_transparent_top_of_a_micro_button_does_not_capture_the_mouse() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The character button's face: the region binds the SAME `"player"` portrait slot the unit frame
/// samples, and carries the ref's crop window out to the renderer. Both halves matter — the crop is
/// what makes an 18×25 rectangle show a face instead of the whole square bake squashed into it, and
/// the extract path used to pin portrait quads to full UVs and drop it (fixed with this slice).
/// Pushing the button swaps the window and dims it (ref CharacterMicroButton_SetPushed).
/// The character button's face: `MicroButtonPortrait` binds the SAME `"player"` portrait slot
/// the unit frame samples, and carries the reference's crop window out to the renderer. Both
/// halves matter — the crop is what makes an 18×25 rectangle show a face instead of the whole
/// square bake squashed into it. Pushing the button swaps the window and dims it
/// (`CharacterMicroButton_SetPushed`, `MainMenuBarMicroButtons.lua:109-117`).
#[test]
fn the_character_button_carries_the_player_portrait_through_the_reference_crop() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.resolve();
@@ -136,8 +171,8 @@ fn the_character_button_carries_the_player_portrait_through_the_reference_crop()
assert_eq!(unit, "player");
assert!(
circular,
"the ref's round stencil lives in the bake's own UV space, so the crop below yields a \
rectangular slice OF a masked face — not an ellipse fitted to this 18x25 region"
"the reference's round stencil lives in the bake's own UV space, so the crop below yields \
a rectangular slice OF a masked face — not an ellipse fitted to this 18x25 region"
);
let round4 = |c: TexCoords| match c {
TexCoords::Rect(e) => e.map(|v| (v * 10_000.0).round() / 10_000.0),
@@ -146,7 +181,7 @@ fn the_character_button_carries_the_player_portrait_through_the_reference_crop()
assert_eq!(
round4(coords.expect("a crop window")),
[0.2, 0.8, 0.0666, 0.9],
"the normal window (ref CharacterMicroButton_SetNormal)"
"the normal window (CharacterMicroButton_SetNormal)"
);
s.run("CharacterMicroButton_SetPushed()").unwrap();
@@ -155,7 +190,7 @@ fn the_character_button_carries_the_player_portrait_through_the_reference_crop()
assert_eq!(
round4(coords.expect("a crop window")),
[0.2666, 0.8666, 0.0, 0.8333],
"the held-down window (ref CharacterMicroButton_SetPushed)"
"the held-down window (CharacterMicroButton_SetPushed)"
);
assert_eq!(
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
@@ -166,50 +201,12 @@ fn the_character_button_carries_the_player_portrait_through_the_reference_crop()
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UpdateMicroButtons` (ref MainMenuBarMicroButtons.lua l.20-84): a button is PUSHED exactly
/// while its panel is up. Driven here with a stand-in frame under the real panel's name rather
/// than the whole character window — this is the visibility contract, not that window's.
#[test]
fn a_micro_button_pushes_while_its_panel_is_open() {
let s = harness();
s.run(
r#"
local f = CreateFrame("Frame", "CharacterFrame")
f:SetPoint("CENTER", 0, 0); f:SetSize(100, 100); f:Hide()
"#,
)
.unwrap();
let state = |s: &UiScript| {
s.eval::<String>("return CharacterMicroButton:GetButtonState()")
.unwrap()
};
s.run("UpdateMicroButtons()").unwrap();
assert_eq!(state(&s), "NORMAL", "closed panel ⇒ button up");
s.run("CharacterFrame:Show(); UpdateMicroButtons()")
.unwrap();
assert_eq!(state(&s), "PUSHED", "open panel ⇒ button held down");
s.run("CharacterFrame:Hide(); UpdateMicroButtons()")
.unwrap();
assert_eq!(state(&s), "NORMAL", "closed again ⇒ button pops back");
// A button whose panel doesn't exist at all (the three unwired ones, and any panel a test
// harness didn't load) never pushes — and never errors.
assert_eq!(
s.eval::<String>("return SocialsMicroButton:GetButtonState()")
.unwrap(),
"NORMAL"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UpdateTalentButton` (ref l.133-141): under level 10 the talent button is hidden and the quest
/// log button slides onto its seat, so the row has no hole in it; at 10 the button returns and the
/// tail shifts back out by one stride.
/// `UpdateTalentButton` (l.133-141): under level 10 the talent button is hidden and the quest
/// log button slides onto its seat, so the row has no hole in it; at 10 the button returns and
/// the tail shifts back out — by one stride less two, the gate's own −2.
#[test]
fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
set_player_level(&mut s, 9);
@@ -220,13 +217,9 @@ fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
"no talents before 10"
);
// The quest log button takes the talent button's own seat — slot 3, not slot 4.
assert_eq!(left_of(&s, "QuestLogMicroButton"), 552.0 + 26.0 * 2.0);
assert_eq!(
s.eval::<f64>("return QuestLogMicroButton:GetLeft()")
.unwrap(),
552.0 + 26.0 * 2.0
);
assert_eq!(
s.eval::<f64>("return HelpMicroButton:GetLeft()").unwrap(),
left_of(&s, "HelpMicroButton"),
552.0 + 26.0 * 6.0,
"the whole tail moves up one slot with it"
);
@@ -239,51 +232,84 @@ fn the_talent_button_appears_at_level_ten_and_the_row_closes_up_below_it() {
"the button returns at 10"
);
assert_eq!(
s.eval::<f64>("return QuestLogMicroButton:GetLeft()")
.unwrap(),
552.0 + 26.0 * 3.0
left_of(&s, "QuestLogMicroButton"),
553.0 + 26.0 * 3.0,
"…and the tail goes back out, at the gate's own −2"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The hover (ref MainMenuBarMicroButton's OnEnter, l.13): every button's plate is the ref's
/// TWO-line `GameTooltip_AddNewbieTip` — the label, then that button's own `NEWBIE_TOOLTIP_*`
/// explanation in gold. 1.12 ships detailed tips ON (`SHOW_NEWBIE_TIPS = "1"`, ref
/// UIOptionsFrame.lua l.100), so the paragraph is the DEFAULT hover; this file used to open-code
/// the tips-off branch and show a bare label. Decision 0661.
///
/// Checked across all eight, because the explanation is threaded per button through OnLoad and a
/// single missing argument is invisible until someone hovers that one button.
/// The hover (the template's OnEnter, `MainMenuBarMicroButtons.xml:12-14`): every button's plate
/// is the reference's TWO-line `GameTooltip_AddNewbieTip` — the label, then that button's own
/// `NEWBIE_TOOLTIP_*` explanation in gold. 1.12 ships detailed tips ON (`SHOW_NEWBIE_TIPS = "1"`,
/// `UIOptionsFrame.lua:100`; ours sits in OptionsFrame.xml's uvar block), so the paragraph is the
/// DEFAULT hover (decision 0661). Through the engine's own hover, because the stock OnEnter reads
/// the firing frame off `this`. For the bound ones the label carries the key the way the
/// reference prints it — `GetBindingKey`'s raw token in `NORMAL_FONT_COLOR_CODE` parentheses
/// (`MicroButtonTooltipText`, l.11-18) — re-read on `UPDATE_BINDINGS`, which is the event
/// `bindings.rs` fires after every (re)load of the table.
#[test]
fn every_micro_button_hovers_with_its_reference_explanation() {
let mut s = harness_with(&["UIParent.xml"]);
set_player_level(&mut s, 10);
// 1.12 ships detailed tips ON — `SHOW_NEWBIE_TIPS = "1"` is UIOptionsFrame_Init's (ref
// UIOptionsFrame.lua l.100; ours sits in OptionsFrame.xml's uvar block, 1968), and a harness
// without the options file says so itself, the way the reference's tooltip would read it.
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
// A key the character button's label can carry: the registry, then the reference's own
// `SetBinding`, then the event the row re-reads its labels on.
s.register_bindings(&crate::bindings::registry_commands());
assert_eq!(
s.eval::<Option<i64>>("return SetBinding(\"C\", \"TOGGLECHARACTER0\")")
.unwrap(),
Some(1)
);
s.fire_event("UPDATE_BINDINGS", vec![]);
s.resolve();
for (button, newbie) in [
("CharacterMicroButton", "NEWBIE_TOOLTIP_CHARACTER"),
("SpellbookMicroButton", "NEWBIE_TOOLTIP_SPELLBOOK"),
("TalentMicroButton", "NEWBIE_TOOLTIP_TALENTS"),
("QuestLogMicroButton", "NEWBIE_TOOLTIP_QUESTLOG"),
("SocialsMicroButton", "NEWBIE_TOOLTIP_SOCIAL"),
("WorldMapMicroButton", "NEWBIE_TOOLTIP_WORLDMAP"),
("MainMenuMicroButton", "NEWBIE_TOOLTIP_MAINMENU"),
("HelpMicroButton", "NEWBIE_TOOLTIP_HELP"),
for (button, label, newbie) in [
(
"CharacterMicroButton",
"CHARACTER_BUTTON",
"NEWBIE_TOOLTIP_CHARACTER",
),
// The spellbook's own OnEnter (l.79-87) picks its label by `PlayerHasSpells()`.
(
"SpellbookMicroButton",
"PlayerHasSpells() and SPELLBOOK_ABILITIES_BUTTON or ABILITYBOOK_BUTTON",
"NEWBIE_TOOLTIP_SPELLBOOK",
),
(
"TalentMicroButton",
"TALENTS_BUTTON",
"NEWBIE_TOOLTIP_TALENTS",
),
(
"QuestLogMicroButton",
"QUESTLOG_BUTTON",
"NEWBIE_TOOLTIP_QUESTLOG",
),
(
"SocialsMicroButton",
"SOCIAL_BUTTON",
"NEWBIE_TOOLTIP_SOCIAL",
),
(
"WorldMapMicroButton",
"WORLDMAP_BUTTON",
"NEWBIE_TOOLTIP_WORLDMAP",
),
(
"MainMenuMicroButton",
"MAINMENU_BUTTON",
"NEWBIE_TOOLTIP_MAINMENU",
),
("HelpMicroButton", "HELP_BUTTON", "NEWBIE_TOOLTIP_HELP"),
] {
// A hover is a handler: the reference's helper reads the firing frame off `this`.
s.run(&format!("this = {button} BenillaMicroButton_OnEnter(this)"))
.unwrap();
super::test_ui::hover(&mut s, button);
assert_eq!(
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
2,
"{button}: the label, then the explanation"
);
// Line 1 is the button's own label (plus a `|cff…(key)|r` suffix on the five bound ones).
let label = s.eval::<String>(&format!("return {button}.label")).unwrap();
let label = s.eval::<String>(&format!("return {label}")).unwrap();
let line1 = s
.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap();
@@ -295,26 +321,130 @@ fn every_micro_button_hovers_with_its_reference_explanation() {
s.eval::<String>("return GameTooltipTextLeft2:GetText()")
.unwrap(),
s.eval::<String>(&format!("return {newbie}")).unwrap(),
"{button}: line 2 is the ref's {newbie}, verbatim"
"{button}: line 2 is the reference's {newbie}, verbatim"
);
assert_eq!(
s.eval::<i64>("return GameTooltip.default").unwrap(),
1,
"{button}: the default-corner anchor, not ANCHOR_RIGHT off the button"
);
super::test_ui::unhover(&mut s);
}
// The bound one carries its key the reference's way: the raw token, gold, in parentheses,
// after a space — `MicroButtonTooltipText`'s own concatenation.
super::test_ui::hover(&mut s, "CharacterMicroButton");
let line1 = s
.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap();
assert_eq!(
line1,
s.eval::<String>(
"return CHARACTER_BUTTON .. \" \" .. NORMAL_FONT_COLOR_CODE .. \"(C)\" .. FONT_COLOR_CODE_CLOSE"
)
.unwrap(),
"the key read on UPDATE_BINDINGS"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Set the player's level and fire the `UNIT_LEVEL` the talent gate listens for.
fn set_player_level(s: &mut UiScript, level: u32) {
/// Under the whole shipped manifest — the only VM in which every panel `UpdateMicroButtons`
/// (l.20-84) reads exists — a button is PUSHED exactly while its panel is up, the character
/// button's face dims with it, and the level-up pulse the reference hangs on the talent button
/// (`TalentMicroButton_OnEvent`, l.119-124: sixty seconds, unless the character sheet is open)
/// is live — 0304's "no microbutton pulse" scope-out, retired by the file that owns the pulse.
#[test]
fn a_micro_button_pushes_while_its_panel_is_open_and_the_talent_button_pulses_on_a_ding() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// A player always exists by the time the manifest loads (1051/1848) — with a race and a
// class, which the sheet's own `PaperDollFrame_SetLevel` formats into its title on show.
s.set_unit(
"player",
Some(benilla_ui::script::UnitState {
Some(UnitState {
exists: true,
level,
name: Some("Probefour".into()),
level: 60,
race: Some("Night Elf".into()),
race_file: Some("NightElf".into()),
class: Some("Warrior".into()),
class_file: Some("WARRIOR".into()),
sex: 2,
is_player: true,
player_controlled: true,
..Default::default()
}),
);
s.fire_event("UNIT_LEVEL", vec![ScriptValue::Str("player".into())]);
// The reference's strings, which the app runs ahead of the manifest (lifecycle.rs) — the
// ding's chat lines below format `SPELL_STAT0_NAME`..`4` through them.
super::test_ui::load_ui(&s, "Interface\\FrameXML\\GlobalStrings.lua");
assert!(super::load_default_ui(&s).is_empty());
s.resolve();
let state = |s: &UiScript, button: &str| {
s.eval::<String>(&format!("return {button}:GetButtonState()"))
.unwrap()
};
let face = |s: &UiScript| {
s.eval::<f64>("return MicroButtonPortrait:GetAlpha()")
.unwrap()
};
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
assert_eq!(face(&s), 1.0);
// The button's own click (its OnClick, l.49-51) — the sheet's OnShow calls back.
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
assert_eq!(
state(&s, "CharacterMicroButton"),
"PUSHED",
"open sheet ⇒ button held down"
);
assert_eq!(
face(&s),
0.5,
"…and the face dims (CharacterMicroButton_SetPushed)"
);
s.run("ToggleCharacter(\"PaperDollFrame\")").unwrap();
assert_eq!(state(&s, "CharacterMicroButton"), "NORMAL");
assert_eq!(face(&s), 1.0);
// The reference's own full-screen panel; its OnShow/OnHide call back the same way.
s.run("ToggleWorldMap()").unwrap();
assert_eq!(state(&s, "WorldMapMicroButton"), "PUSHED");
s.run("ToggleWorldMap()").unwrap();
assert_eq!(state(&s, "WorldMapMicroButton"), "NORMAL");
// A ding with the sheet closed: sixty seconds on the pulse list (UiPanels.xml's
// SetButtonPulse, the reference's UIParent.lua machinery). The nine args are the ones
// `ui_unit` fires (level, health, power, talent points, five stats).
s.fire_event(
"PLAYER_LEVEL_UP",
vec![
ScriptValue::Int(61),
ScriptValue::Int(12),
ScriptValue::Int(0),
ScriptValue::Int(1),
ScriptValue::Int(1),
ScriptValue::Int(1),
ScriptValue::Int(1),
ScriptValue::Int(0),
ScriptValue::Int(0),
],
);
assert_eq!(
s.eval::<f64>("return TalentMicroButton.pulseTimeLeft")
.unwrap(),
60.0,
"the sixty-second pulse"
);
assert!(
s.eval::<bool>(
"for _, b in ipairs(PULSEBUTTONS) do if b == TalentMicroButton then return true end end \
return false"
)
.unwrap(),
"on the pulse list"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
@@ -796,7 +796,7 @@ fn interface_harness() -> UiScript {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"Interface\\FrameXML\\QuestFrame.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
"Interface\\FrameXML\\QuestLogFrame.xml",
],
);
@@ -692,7 +692,7 @@ fn tooltip_line1(s: &UiScript) -> String {
///
/// The exact string is pinned, colour codes and all, because the ref's concatenation puts the
/// colour BEFORE the space (`Attack|cffffd200 (CTRL-1)|r`, not `Attack |cffffd200(CTRL-1)|r` —
/// which is what MicroMenu.xml's different-shaped version of the same idea produces).
/// which is what the stock `MicroButtonTooltipText` — the same idea, the other shape — produces).
#[test]
fn token_tooltips_name_their_binding_and_pet_spells_do_not() {
let mut s = hovered_pet_bar();
@@ -44,7 +44,7 @@ fn harness() -> UiScript {
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
load_xml(&s, "Interface\\FrameXML\\QuestTimerFrame.xml");
s
@@ -105,7 +105,7 @@ fn shipped_questlog_frame_loads_clean() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
}
@@ -139,7 +139,7 @@ fn shipped_questlog_frame_drives_end_to_end() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -349,7 +349,7 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
load_xml(&s, "UIParent.xml");
@@ -498,7 +498,7 @@ fn watch_guards_no_op_without_erroring() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
load_xml(&s, "UIParent.xml");
@@ -619,7 +619,7 @@ fn progress_auto_watches_for_five_minutes() {
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -705,7 +705,7 @@ fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
assert_eq!(s.eval::<String>("return AUTO_QUEST_WATCH").unwrap(), "1");
@@ -761,7 +761,7 @@ fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
let mut state = eight_entries();
@@ -844,7 +844,7 @@ fn empty_quest_log_hides_rows_and_disables_abandon() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
@@ -909,7 +909,7 @@ fn reward_rows_follow_the_refs_two_per_row_layout() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
let mut state = eight_entries();
@@ -1098,7 +1098,7 @@ fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -1173,7 +1173,7 @@ fn wheel_over_the_detail_pane_changes_vertical_scroll() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -1238,7 +1238,7 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(overflowing_entry());
@@ -1320,7 +1320,7 @@ fn reward_row_hover_serves_the_shared_item_tooltip() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(eight_entries());
@@ -1475,7 +1475,7 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
@@ -1574,7 +1574,7 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
@@ -1684,7 +1684,7 @@ fn share_quest_needs_both_a_sharable_selection_and_a_party() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// Solo, quest 1 sharable and selected by the auto-selection.
@@ -1771,7 +1771,7 @@ fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(QuestLogState::default());
@@ -1812,7 +1812,7 @@ fn share_quest_click_queues_the_selected_quests_id() {
load_xml(&s, "UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
s.set_quest_log(entries_sharable(&[1, 5]));
@@ -80,6 +80,7 @@ fn push_raid(s: &mut UiScript, raid: Vec<RaidMemberInfo>) {
members,
leader_index: 0,
leader_guid: 0, // the player leads; their guid is unset in this fixture
own_guid: 0,
raid,
loot_method: "group".into(),
master_looter: None,
@@ -1116,12 +1116,13 @@ mod tests {
/// do not ship under its own name usually has a counterpart of ours under a different one, and
/// both would declare the same frames.
///
/// That half doubles as the map nothing else holds: `FloatingChatFrame.xml` is our `ChatFrame.xml`,
/// `MainMenuBarMicroButtons.xml` is our `MicroMenu.xml`, `StaticPopup.xml` is our
/// `UiPanels.xml`. (The row this map used to lead with — `PlayerFrame.xml`/`TargetFrame.xml`/
/// `PetFrame.xml` all being our one `UnitFrames.xml` — is retired: those four are the
/// reference's own files now, 1751.) `chain_gap_report` calls several of those unblocked, and they are —
/// individually. They just cannot load beside the file of ours already holding their names.
/// That half used to double as the map nothing else held — `FloatingChatFrame.xml` was our
/// `ChatFrame.xml`, `MainMenuBarMicroButtons.xml` our `MicroMenu.xml`, `StaticPopup.xml` our
/// `UiPanels.xml`'s dialog half, `PlayerFrame.xml`/`TargetFrame.xml`/`PetFrame.xml` our one
/// `UnitFrames.xml`. Every one of those is the reference's own file now (1751; the micro row
/// last, 1987), so the frame half names nothing today. It stays because a stock window
/// declaring a frame one of ours still holds is the first thing a swap has to rule out —
/// such a pair can load individually and cannot load side by side.
///
/// Run it before attempting a swap. It predicts which ones will fail without attempting them.
#[test]
@@ -1419,8 +1420,8 @@ mod tests {
/// FrameXML function is invisible to both, because loading a file never runs the body that
/// calls it: `LootFrame.xml` shipped load-clean and raised at `LootFrame.lua:85` the first
/// time it met real data, and stock `CharacterFrame.xml` would have raised on the first tab
/// HOVER, because `MicroButtonTooltipText` did not exist here (it does now — `MicroMenu.xml`,
/// our `MainMenuBarMicroButtons.xml` counterpart, is where the reference declares it).
/// HOVER, because `MicroButtonTooltipText` did not exist here (it does now — the stock
/// `MainMenuBarMicroButtons.xml` declares it, off the chain since 1987).
///
/// So: for every chain `.xml` in the manifest, census the bare `Name(` call sites across it
/// and its `.lua`, subtract what those two define themselves, keep the names the reference's
@@ -625,10 +625,10 @@ fn every_archive_path_a_shipped_lua_chunk_names_survives_its_own_escaping() {
}
}
}
// Never let the sweep pass by matching nothing (19 after 1971, 8 after 1980; the floor
// follows the census down as windows migrate).
// Never let the sweep pass by matching nothing (19 after 1971, 8 after 1980, 4 after 1987;
// the floor follows the census down as windows migrate).
assert!(
paths.len() >= 5,
paths.len() >= 4,
"only {} archive paths swept out of the shipped Lua",
paths.len()
);
@@ -1375,9 +1375,10 @@ fn a_cinematic_leaves_nothing_of_the_interface_on_screen() {
let before = names.iter().filter(|n| visible(&s, n)).count();
assert!(
// 50 until 1938 took the three bar files stock, 20 until 1974 took the minimap cluster
// (its 22 named frames were most of what this sweep counted); the floor follows the
// census of OUR files down, and 12 is what is left — the glue and dev frames.
before > 10,
// (its 22 named frames were most of what this sweep counted), 12 until 1987 took the
// micro row; the floor follows the census of OUR files down, and 2 is what is left —
// the glue and dev frames.
before >= 2,
"only {before} frames visible before the cinematic — the sweep found no interface to \
hide, so it would pass no matter what the cascade did"
);
@@ -1448,8 +1449,9 @@ fn every_declared_parent_really_attaches() {
let declared = shipped_frame_parents();
assert!(
// A sanity floor for the scan, not a census — the declarations retire with the files
// that carry them (1751); 67 before 1938, 30 after 1956, 14 after 1970.
declared.len() >= 10,
// that carry them (1751); 67 before 1938, 30 after 1956, 14 after 1970, 2 after 1987
// (the micro row's eight were most of what was left).
declared.len() >= 2,
"only {} parent declarations found — the scan broke",
declared.len()
);
@@ -114,7 +114,7 @@ pub(super) fn spellbook_ui(w: f32, h: f32) -> UiScript {
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
"Interface\\FrameXML\\SpellBookFrame.xml",
"SpellBookAdapters.xml",
] {
@@ -703,7 +703,7 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
r"Interface\FrameXML\UIPanelTemplates.xml",
// The icon chooser's scroll frame inherits `ClassTrainerListScrollFrameTemplate`.
r"Interface\FrameXML\ClassTrainerFrameTemplates.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
+50 -6
View File
@@ -70,6 +70,14 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bo
"{entry}: loader errors: {:?}",
report.errors
);
if path
.rsplit('/')
.next()
.is_some_and(|leaf| leaf.eq_ignore_ascii_case("MainMenuBarMicroButtons.xml"))
{
s.run(MICRO_BUTTON_STAND_INS)
.expect("the micro-button stand-ins");
}
if no_warnings {
assert!(
report.warnings.is_empty(),
@@ -92,6 +100,39 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bo
report.frames
}
/// **What a kit owes the stock micro-button row.** `UpdateMicroButtons`
/// (`MainMenuBarMicroButtons.lua:20-84`) reads ten panels, `KeyRingButton` and `IsBagOpen`
/// UNGUARDED — the reference's own contract, because in the client every one of them is up
/// before anything can be shown. A kit is a prefix of the manifest and stops wherever its window
/// does, so [`load_entry`] wraps the function the moment the row loads: on its FIRST call — a
/// panel's OnShow, by which time the kit is complete — the wrapper seats a hidden, unnamed
/// stand-in under each name the kit never declared (a hidden frame is what a closed panel
/// answers), then runs the reference's own body. First use rather than load time, and unnamed,
/// because both registries keep the first writer: the arena's name map
/// (`named_registry_is_non_overwriting`) and `_G` itself — a named frame is published
/// non-overwriting (RF-0023: a `lua_gettable` nil check on GLOBALSINDEX, which is why a
/// metatable fallback would block the real publish just the same). A stand-in seated before the
/// real declaration shadows the real frame for good; the first cut did exactly that, 126
/// failures, every tabbed panel's `numTabs` read off the stand-in. `fire_chat_login`'s
/// `UIOptionsFrame` stand-in is the same move for the chat files. Seated here rather than at the
/// kits' seventy-odd consumers because the dependency is the row's, not any one window's — and
/// the shipped manifest never needs it. `tests/common/mod.rs` carries the same chunk for the
/// integration tests, which cannot reach this module (decision 1987).
pub(super) const MICRO_BUTTON_STAND_INS: &str = r#"
local real = UpdateMicroButtons
function UpdateMicroButtons()
for _, name in ipairs({ "CharacterFrame", "SpellBookFrame", "QuestLogFrame", "GameMenuFrame",
"OptionsFrame", "SoundOptionsFrame", "UIOptionsFrame", "FriendsFrame", "WorldMapFrame",
"HelpFrame" }) do
if not getglobal(name) then local f = CreateFrame("Frame") f:Hide() setglobal(name, f) end
end
if not KeyRingButton then KeyRingButton = CreateFrame("Button") KeyRingButton:Hide() end
if not KEYRING_CONTAINER then KEYRING_CONTAINER = -2 end
if not IsBagOpen then function IsBagOpen() return nil end end
return real()
end
"#;
/// One file's bytes, from whichever store its path names — the chain for a path, this crate's
/// `assets/ui` for a bare name. Also the `<Include>` / `<Script file=>` provider, which is why it
/// takes an already-resolved path in either space.
@@ -222,9 +263,12 @@ pub(super) const LOOT_UI: &[&str] = &[
/// `HonorLevelText` and `HonorGuildText` "while we're at it" (`PaperDollFrame.lua:100`/`:120`).
/// Nothing touches them at load, so leaving this out loads clean and then raises the first time
/// the window is SHOWN. It sits below the character block in the manifest for the same reason.
/// * **`MicroMenu.xml`** — `UpdateMicroButtons` (called by both `CharacterFrame_OnShow` and
/// `_OnHide`) and `MicroButtonTooltipText` (all five tab hovers). A tab hover is the only thing
/// that reaches the second one, so its absence is invisible until a test hovers a tab.
/// * **`MainMenuBarMicroButtons.xml`** — `UpdateMicroButtons` (called by both
/// `CharacterFrame_OnShow` and `_OnHide`) and `MicroButtonTooltipText` (all five tab hovers).
/// A tab hover is the only thing that reaches the second one, so its absence is invisible
/// until a test hovers a tab. The reference's own file since 1987; the panels its
/// `UpdateMicroButtons` reads unguarded and this kit stops short of are
/// [`MICRO_BUTTON_STAND_INS`]'s.
///
/// Needs client data, like its three siblings: open with `benilla_formats::wow_data_or_skip!()`.
pub(super) const CHARACTER_UI: &[&str] = &[
@@ -272,7 +316,7 @@ pub(super) const CHARACTER_UI: &[&str] = &[
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
// The two page files these three need before the window can be OPENED, which is not the same
// as before it can load: `CHARACTERFRAME_SUBFRAMES` lists all five pages by name and
// `CharacterFrame_ShowSubFrame` calls `getglobal(value):Hide()` on each one it is not showing
@@ -331,7 +375,7 @@ pub(super) const SOCIAL_UI: &[&str] = &[
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\ChatFrame.xml",
@@ -393,7 +437,7 @@ pub(super) const BAG_UI: &[&str] = &[
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// `UpdateMicroButtons` — the KEYRING's own OnShow/OnHide calls it (ContainerFrame.lua l.117,
// l.137), because in the reference the keyring's existence moves the micro-button row.
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The dialog engine, after the UIPanelCloseButton it inherits (1960).
@@ -71,7 +71,7 @@ fn trainer_script_base() -> UiScript {
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
"MicroMenu.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
"Interface\\FrameXML\\ClassTrainerFrameTemplates.xml",
] {
@@ -284,3 +284,42 @@ fn the_font_path_globals_are_the_references_own_four() {
"|cffffff9a"
);
}
/// `MouseIsOver` divides the cursor by the frame's effective scale, as the reference does
/// (UIParent.lua l.1389-1390): a frame scaled to 0.5 whose screen footprint holds the cursor
/// answers 1 — with the division written out (the old "the scale is the constant 1" note) it
/// answered nil under the very cursor that had just entered it (decision 1985).
#[test]
fn mouse_is_over_reads_a_scaled_frame_in_its_own_units() {
let mut s = ui_parent();
s.run(
r#"Scaled = CreateFrame("Frame", "Scaled", UIParent) Scaled:SetWidth(200) Scaled:SetHeight(100)
Scaled:SetPoint("BOTTOMLEFT", 100, 100) Scaled:SetScale(0.5)"#,
)
.unwrap();
s.resolve();
// The frame's screen footprint is its own-unit box times its effective scale (offsets and
// size both scale); the probe sits at its centre, then just past its right edge.
let (l, r, b, t, eff) = s
.eval::<(f64, f64, f64, f64, f64)>(
"return Scaled:GetLeft(), Scaled:GetRight(), Scaled:GetBottom(), Scaled:GetTop(), Scaled:GetEffectiveScale()",
)
.unwrap();
assert!(
(eff - 0.5).abs() < 1e-6 && (r - l - 200.0).abs() < 1e-3,
"own units: {l}..{r} at {eff}"
);
let (cx, cy) = (((l + r) * 0.5 * eff) as f32, ((b + t) * 0.5 * eff) as f32);
s.mouse_move(cx, cy);
assert_eq!(
s.eval::<Option<i64>>("return MouseIsOver(Scaled)").unwrap(),
Some(1),
"the cursor at the scaled footprint's centre ({cx}, {cy})"
);
s.mouse_move((r * eff) as f32 + 10.0, cy);
assert_eq!(
s.eval::<Option<i64>>("return MouseIsOver(Scaled)").unwrap(),
None,
"past the scaled footprint (inside the unscaled one) is outside"
);
}
@@ -469,6 +469,7 @@ fn left_clicking_the_player_frame_targets_self() {
// The player's own guid, which this fixture leaves unset — spelled out because a bare 0
// is also the reference's "ungrouped" sentinel and this party has a member.
leader_guid: 0,
own_guid: 0,
raid: Vec::new(),
loot_method: "group".into(),
master_looter: None,
@@ -551,6 +552,7 @@ fn raid_mark_clicks_through_the_nested_level() {
}],
leader_index: 0, // we lead — the mark rows are leader-gated
leader_guid: 0, // the player's own guid; this fixture leaves it unset
own_guid: 0,
raid: Vec::new(),
loot_method: "group".into(),
master_looter: None,
@@ -663,7 +665,7 @@ fn shipped_target_frame_runs_the_level_law() {
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "MicroMenu.xml");
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.xml");
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
// The player at level 3, both feeds (the snapshot UnitLevel("player") reads; the req state
// the −1 gate and GetQuestGreenRange read) — the app keeps the two in step.
@@ -1270,6 +1272,7 @@ fn the_party_art_paints_over_the_bars() {
}],
leader_index: 0,
leader_guid: 0,
own_guid: 0,
raid: Vec::new(),
loot_method: "group".into(),
master_looter: None,
@@ -1999,6 +2002,7 @@ fn the_player_frame_wears_the_leader_and_master_looter_icons() {
leader_index,
// Follows `leader_index`: 0 = the player (unset here), else the member who leads.
leader_guid: if leader_index == 0 { 0 } else { 0x7A17 },
own_guid: 0,
raid: Vec::new(),
loot_method: method.into(),
master_looter,
@@ -537,3 +537,54 @@ fn the_maps_own_furniture_survives_the_hide_that_showing_it_performs() {
assert_eq!(visible(&s, "BlackoutWorld"), 0, "with the blackout down");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **Hovering the player arrow on a scaled map names the player** (director report: an empty
/// tooltip). On a window narrower than 4:3 the stock `SetupFullscreenScale` scales the map
/// under 1 (0.802 here); `WorldMapUnit_OnEnter` then asks `MouseIsOver(WorldMapPlayer)` for the
/// text, which read the cursor against the blip's own-unit edges without the reference's scale
/// division and answered nil — a tooltip with no lines (decision 1985).
#[test]
fn hovering_the_player_blip_on_a_scaled_map_names_the_player() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(821.0, 768.0);
s.set_unit(
"player",
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Probefour".into()),
level: 60,
..Default::default()
}),
);
let failures = super::load_default_ui(&s);
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
s.resolve();
s.run("ShowUIPanel(WorldMapFrame)").unwrap();
s.resolve();
assert!(
s.eval::<f64>("return WorldMapFrame:GetScale()").unwrap() < 0.99,
"the narrow window scales the map under 1"
);
s.set_world_map_feed(None, Some((0.5, 0.5)), 0.0, None, Vec::new(), Vec::new());
update(&mut s);
s.resolve();
let (px, py, eff) = s
.eval::<(f64, f64, f64)>(
"local x, y = WorldMapPlayer:GetCenter() return x, y, WorldMapPlayer:GetEffectiveScale()",
)
.unwrap();
s.mouse_move((px * eff) as f32, (py * eff) as f32);
assert_eq!(
s.hit_test_name((px * eff) as f32, (py * eff) as f32)
.as_deref(),
Some("WorldMapPlayer")
);
assert!(s.eval::<bool>("return WorldMapTooltip:IsShown()").unwrap());
assert_eq!(
s.eval::<String>("return WorldMapTooltipTextLeft1:GetText()")
.unwrap(),
"Probefour"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}