Files
benilla-octo/crates/benilla-app/src/ui_script/game_menu_tests.rs
T
Sam SungandClaude Opus 5.5 eba558a5d2 Comments in benilla-app, the UI script layer: one line of fact each, no history, no record numbers (#184)
Part of #41: the first of five `benilla-app` pieces. This one is the UI script layer: `ui_script` with its test harnesses, the text engine and layout (`ui_text`), the UI pass and the UI model panes. Its comments are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 22,041 comment lines become 7,133.

- **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal.
- **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them:
  - On a fresh login the reference fires `PLAYER_LOGIN` and `PLAYER_ENTERING_WORLD` from the player's create (`0x5deb60` → `0x4908c0`); only a `/reload` goes through `0x490168`. The comments now state both paths.
  - `ComputeStep` (`0x5ca2d0`) applies negative pair kerns and rounds up. It does have a kerning term.
  - A fallback icon global and the retired `UiPanels.xml` panels no longer exist. The comments that named them now describe the stock FrameXML mechanism that replaced them.
  - Stock FrameXML line citations are checked against the stock 1.12 files and corrected, and several misattributed files are fixed (`MainMenuBarArtFrame` lives in `MainMenuBar.xml`).
  - Dozens more turned up on the way: docs sitting on the wrong item, removed functions, stale counts and links.
- **Deviation labels.** "Deviation:" now marks only a choice with a reason. An unbuilt behaviour says so plainly.
- **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Five second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the code, the stock files, vmangos and the reference.

Behaviour differences the rewrite surfaced are filed as their own issues (#183 among them). The comments here state what the code does. String literals are untouched; they are #92.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 08:20:21 +07:00

767 lines
28 KiB
Rust

//! Tests for our `GameMenuFrame.xml`, the frame ESC opens. The bags in its way are the stock
//! `ContainerFrame1..12`, recycled, so tests ask [`bag_open`] rather than naming a frame.
use benilla_ui::script::{
ContainerSlot, ContainerState, LootRow, LootState, SessionRequest, SoundRequest, UiScript,
};
use super::test_ui::{bag_open, load_ui as load_xml, BAG_UI};
/// The menu over what it needs (`GameMenuButtonTemplate` is `UIPanelTemplates.xml:403`), with
/// `extra` in its way, deduped so no file loads twice.
fn harness_with(extra: &[&str]) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
let files: Vec<&str> = [
"Interface\\FrameXML\\Fonts.xml",
// Before the menu: its `parent="UIParent"` resolves at load.
r"Interface\FrameXML\UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
]
.into_iter()
.chain(extra.iter().copied())
.chain(std::iter::once("GameMenuFrame.xml"))
.collect();
let mut loaded = std::collections::HashSet::new();
for file in files {
if loaded.insert(file) {
load_xml(&s, file);
}
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
}
fn harness() -> UiScript {
harness_with(&[])
}
/// [`harness_with`] over the stock bag stack, with `before` ahead of it and `after` behind.
fn bag_harness_with(before: &[&str], after: &[&str]) -> UiScript {
let files: Vec<&str> = before
.iter()
.copied()
.chain(BAG_UI.iter().copied())
.chain(after.iter().copied())
.collect();
harness_with(&files)
}
/// Top to bottom. Deviation: the 1.15 era client's ESC ladder, because its settings screens
/// replace 1.12's; without the AddOns rung, as the character-select screen is the only addon UI.
const LADDER: [&str; 7] = [
"GameMenuButtonOptions",
"GameMenuButtonEditMode",
"GameMenuButtonSupport",
"GameMenuButtonMacros",
"GameMenuButtonLogout",
"GameMenuButtonQuit",
"GameMenuButtonContinue",
];
/// The era layout (MainMenuFrameTemplates: padding 32 top and 28 elsewhere, 20-unit section
/// gaps) over seven 144x21 rungs: 200x267, gaps after Options, after Macros and before Return.
#[test]
fn the_menu_has_the_era_frame_and_button_ladder() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
s.resolve();
let (w, h) = s
.eval::<(f64, f64)>("return GameMenuFrame:GetWidth(), GameMenuFrame:GetHeight()")
.unwrap();
assert_eq!((w, h), (200.0, 267.0), "the era frame size");
let top = s.eval::<f64>("return GameMenuFrame:GetTop()").unwrap();
let left = s.eval::<f64>("return GameMenuFrame:GetLeft()").unwrap();
const TOPS: [f64; 7] = [32.0, 73.0, 94.0, 115.0, 156.0, 177.0, 218.0];
for (name, down) in LADDER.iter().zip(TOPS) {
let (bw, bh, btop, bleft) = s
.eval::<(f64, f64, f64, f64)>(&format!(
"return {name}:GetWidth(), {name}:GetHeight(), {name}:GetTop(), {name}:GetLeft()"
))
.unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(
(bw, bh),
(144.0, 21.0),
"{name} is a GameMenuButtonTemplate"
);
assert!(
(btop - (top - down)).abs() < 0.001,
"{name} top: expected {} down {down}, got {}",
top - down,
btop
);
assert!(
(bleft - (left + 28.0)).abs() < 0.001,
"{name} sits at the era 28-unit left padding"
);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
#[test]
fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
for name in ["GameMenuButtonEditMode", "GameMenuButtonSupport"] {
assert!(
!s.eval::<bool>(&format!("return {name}:IsEnabled() ~= 0"))
.unwrap(),
"{name} has no panel behind it and must read that way"
);
}
for name in [
"GameMenuButtonOptions",
"GameMenuButtonMacros",
"GameMenuButtonLogout",
"GameMenuButtonQuit",
"GameMenuButtonContinue",
] {
assert!(
s.eval::<bool>(&format!("return {name}:IsEnabled() ~= 0"))
.unwrap(),
"{name} is live"
);
}
// Edit Mode reads the era's `HUD_EDIT_MODE_MENU` and Continue 1.12's `RETURN_TO_GAME`; Options
// is a literal, as 1.12's `OPTIONS_MENU` is "Options Menu".
assert_eq!(
s.eval::<String>("return GameMenuButtonEditMode:GetText()")
.unwrap(),
"Edit Mode"
);
assert_eq!(
s.eval::<String>("return GameMenuButtonContinue:GetText()")
.unwrap(),
"Return to Game"
);
assert_eq!(
s.eval::<String>("return GameMenuButtonOptions:GetText()")
.unwrap(),
"Options"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `ToggleGameMenu`'s ESC arm (`UIParent.lua:1482-1496`).
#[test]
fn escape_opens_the_menu_only_when_nothing_else_wants_the_press_and_then_closes_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = bag_harness_with(
&[],
&[
"ScrollTemplates.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
],
);
s.set_money(0);
s.set_container(0, Some(backpack()));
// With a window open, `CloseAllWindows` eats the press.
s.run("MainMenuBarBackpackButton:Click()").unwrap();
let _ = s.take_sounds();
s.run("ToggleGameMenu()").unwrap();
assert!(!bag_open(&s, 0), "the press closed the bag");
assert!(
!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
"and did NOT also open the menu — one eater per press"
);
let _ = s.take_sounds();
s.run("ToggleGameMenu()").unwrap();
assert!(s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
assert!(s
.take_sounds()
.contains(&SoundRequest::KitName("igMainMenuOpen".into())));
s.run("ToggleGameMenu()").unwrap();
assert!(!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
assert!(s
.take_sounds()
.contains(&SoundRequest::KitName("igMainMenuQuit".into())));
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `ToggleGameMenu(1)`, the micro button's form (`UIParent.lua:1466-1480`), is a plain toggle.
#[test]
fn the_clicked_form_closes_everything_and_opens_the_menu_in_one_go() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = bag_harness_with(
&[],
&[
"ScrollTemplates.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
],
);
s.set_money(0);
s.set_container(0, Some(backpack()));
s.run("MainMenuBarBackpackButton:Click()").unwrap();
assert!(bag_open(&s, 0));
s.run("ToggleGameMenu(1)").unwrap();
assert!(!bag_open(&s, 0), "the click closed the bag");
assert!(
s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
"…and opened the menu in the SAME click"
);
s.run("ToggleGameMenu(1)").unwrap();
assert!(!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The menu is a native-center panel: showing it closes the panels and bags
/// (`UIParent.lua:698-704`), and while it holds the center `CanOpenPanels` refuses the rest.
#[test]
fn the_open_menu_takes_the_screen_and_refuses_every_other_panel() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = bag_harness_with(
&[],
&[
"ScrollTemplates.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
// The stock loot window, as `test_ui::LOOT_UI`.
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\UnitFrame.xml",
"Interface\\FrameXML\\BuffFrame.xml",
"Interface\\FrameXML\\PartyFrame.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"Interface\\FrameXML\\LootFrame.xml",
],
);
s.set_money(0);
s.set_container(0, Some(backpack()));
s.run("MainMenuBarBackpackButton:Click()").unwrap();
s.set_loot(Some(LootState {
fishing: false,
master_candidates: Vec::new(),
rows: vec![Some(LootRow {
item_id: 0,
name: Some("Wool Cloth".into()),
texture: Some("Interface\\Icons\\INV_Fabric_Wool_01".into()),
quantity: 1,
quality: Some(1),
is_coin: false,
link: None,
random_property_id: 0,
})],
}));
s.fire_event("LOOT_OPENED", vec![]);
assert!(s
.eval::<bool>("return GetLeftFrame():GetName() == \"LootFrame\"")
.unwrap());
s.run("ToggleGameMenu(1)").unwrap();
assert!(
s.eval::<bool>("return GetLeftFrame() == nil").unwrap(),
"the panel slot vacated on the way in"
);
assert!(!bag_open(&s, 0), "and the bags closed (CloseAllBags)");
assert!(
s.eval::<bool>("return GetCenterFrame():GetName() == \"GameMenuFrame\"")
.unwrap(),
"the menu holds the CENTER slot, not the left one"
);
assert!(
!s.eval::<bool>("return CanOpenPanels() and true or false")
.unwrap(),
"a native-center frame is up: nothing may open"
);
s.run("ShowUIPanel(LootFrame)").unwrap();
assert!(
!s.eval::<bool>("return LootFrame:IsVisible()").unwrap(),
"ShowUIPanel refuses a left-area panel behind the menu"
);
s.run("HideUIPanel(GameMenuFrame)").unwrap();
s.run("ShowUIPanel(LootFrame)").unwrap();
assert!(s.eval::<bool>("return LootFrame:IsVisible()").unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Logout and Exit Game queue alike here: whether a finished logout ends the process is app-side.
#[test]
fn the_live_buttons_queue_their_intents_and_play_their_kits() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run("ToggleGameMenu()").unwrap();
let _ = s.take_sounds();
s.run("GameMenuButtonContinue:Click()").unwrap();
assert!(!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
assert!(s
.take_sounds()
.contains(&SoundRequest::KitName("igMainMenuContinue".into())));
assert!(s.take_session_requests().is_empty());
s.run("ToggleGameMenu()").unwrap();
let _ = s.take_sounds();
s.run("GameMenuButtonLogout:Click()").unwrap();
assert_eq!(s.take_session_requests(), vec![SessionRequest::Logout]);
assert!(s
.take_sounds()
.contains(&SoundRequest::KitName("igMainMenuLogout".into())));
assert!(!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
s.run("ToggleGameMenu()").unwrap();
let _ = s.take_sounds();
s.run("GameMenuButtonQuit:Click()").unwrap();
assert_eq!(s.take_session_requests(), vec![SessionRequest::Quit]);
assert!(s
.take_sounds()
.contains(&SoundRequest::KitName("igMainMenuQuit".into())));
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `PLAYER_CAMPING` shows CAMP (`UIParent.lua:304-307`, `StaticPopup.lua:564-583`), whose `%d %s`
/// text the popup engine's countdown branch writes, not the entry (`StaticPopup.lua:1727-1758`).
#[test]
fn player_camping_opens_a_counting_dialog_whose_early_close_cancels() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
assert_eq!(
s.eval::<String>("return StaticPopup_Visible(\"CAMP\") or \"\"")
.unwrap(),
"StaticPopup1",
"the camp dialog took an instance"
);
// One tick of the countdown fills the text from `CAMP_TIMER`.
s.run("StaticPopup_OnUpdate(StaticPopup1, 0.5)").unwrap();
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"20 Seconds until logout",
"the countdown text is the engine's, from the server's 20 s clock"
);
// Cancel is the only button: 1.12 comments out `CAMP_NOW` (`StaticPopup.lua:567`).
assert_eq!(
s.eval::<String>("return StaticPopup1Button1:GetText()")
.unwrap(),
"Cancel"
);
assert!(
!s.eval::<bool>("return StaticPopup1Button2:IsShown()")
.unwrap(),
"no second button"
);
// An early close cancels from both OnAccept and OnHide, as in the reference; harmless twice.
s.run("StaticPopup1Button1:Click()").unwrap();
assert!(
s.take_session_requests()
.contains(&SessionRequest::CancelLogout),
"the early close called the logout off"
);
assert!(s
.eval::<bool>("return StaticPopup_Visible(\"CAMP\") == nil")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The engine zeroes `timeleft` before hiding (`StaticPopup.lua:1716-1722`), and the entry's OnHide
/// cancels only while `timeleft > 0`.
#[test]
fn a_countdown_that_expires_does_not_cancel_the_logout() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
s.run("StaticPopup_OnUpdate(StaticPopup1, 25)").unwrap();
assert!(
s.eval::<bool>("return StaticPopup_Visible(\"CAMP\") == nil")
.unwrap(),
"the dialog closed itself at zero"
);
assert!(
!s.take_session_requests()
.contains(&SessionRequest::CancelLogout),
"…and did NOT call off the logout it was counting"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// QUIT adds "Exit now" (`ForceQuit`); `LOGOUT_CANCEL`, the server's cancel ack, hides CAMP and
/// QUIT alike (`UIParent.lua:312-316`).
#[test]
fn player_quiting_offers_exit_now_and_logout_cancel_closes_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_QUITING", vec![]);
s.run("StaticPopup_OnUpdate(StaticPopup1, 0.5)").unwrap();
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"20 Seconds until exit"
);
assert_eq!(
s.eval::<String>("return StaticPopup1Button1:GetText()")
.unwrap(),
"Exit now"
);
assert_eq!(
s.eval::<String>("return StaticPopup1Button2:GetText()")
.unwrap(),
"Cancel"
);
s.run("StaticPopup1Button1:Click()").unwrap();
assert!(s
.take_session_requests()
.contains(&SessionRequest::ForceQuit));
s.fire_event("PLAYER_CAMPING", vec![]);
assert!(s
.eval::<bool>("return StaticPopup_Visible(\"CAMP\") ~= nil")
.unwrap());
let _ = s.take_session_requests();
s.fire_event("LOGOUT_CANCEL", vec![]);
assert!(s
.eval::<bool>("return StaticPopup_Visible(\"CAMP\") == nil")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The stock OnShow guard (`GameMenuFrame.xml:134-140`) disables both while CAMP or QUIT counts.
/// Opened with the micro button, because ESC during a countdown dismisses the dialog instead.
#[test]
fn logout_and_exit_read_disabled_while_a_countdown_runs() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
s.run("ToggleGameMenu(1)").unwrap();
assert!(
!s.eval::<bool>("return GameMenuButtonLogout:IsEnabled() ~= 0")
.unwrap(),
"Logout is dead while the camp timer runs"
);
assert!(!s
.eval::<bool>("return GameMenuButtonQuit:IsEnabled() ~= 0")
.unwrap());
s.run("HideUIPanel(GameMenuFrame)").unwrap();
s.fire_event("LOGOUT_CANCEL", vec![]);
s.run("ToggleGameMenu(1)").unwrap();
assert!(s
.eval::<bool>("return GameMenuButtonLogout:IsEnabled() ~= 0")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `StaticPopup_EscapePressed` is the first rung (`UIParent.lua:1482`) and CAMP is `hideOnEscape`,
/// so ESC dismisses the dialog, whose OnHide cancels the logout.
#[test]
fn escape_during_a_countdown_cancels_it_and_does_not_open_the_menu() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.fire_event("PLAYER_CAMPING", vec![]);
s.run("ToggleGameMenu()").unwrap();
assert!(
s.eval::<bool>("return StaticPopup_Visible(\"CAMP\") == nil")
.unwrap(),
"the press dismissed the countdown"
);
assert!(
s.take_session_requests()
.contains(&SessionRequest::CancelLogout),
"…which called the logout off"
);
assert!(
!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
"and the same press did NOT also open the menu — one eater per press"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The map is a stock `area = "full"` panel (`UIParent.lua:30`), so the menu blocks it, and on
/// close it must vacate the full-screen slot, or the stale slot refuses every later panel.
#[test]
fn the_world_map_cannot_open_behind_the_menu_and_gives_its_slot_back() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness_with(&[
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml", // the map's zone pickers initialize at OnLoad
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The map calls `UpdateMicroButtons` unguarded (`WorldMapFrame.xml:599`, `:606`).
"Interface\\FrameXML\\Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
r"Interface\FrameXML\WorldMapFrame.xml",
]);
s.run("ToggleWorldMap()").unwrap();
assert!(s.eval::<bool>("return WorldMapFrame:IsVisible()").unwrap());
assert!(s
.eval::<bool>("return GetFullScreenFrame():GetName() == \"WorldMapFrame\"")
.unwrap());
s.run("ToggleWorldMap()").unwrap();
assert!(!s.eval::<bool>("return WorldMapFrame:IsVisible()").unwrap());
assert!(
s.eval::<bool>("return GetFullScreenFrame() == nil")
.unwrap(),
"the map vacated the full-screen slot"
);
// The M binding and the micro button both call `ToggleWorldMap`.
s.run("ToggleGameMenu(1)").unwrap();
assert!(s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap());
s.run("ToggleWorldMap()").unwrap();
assert!(
!s.eval::<bool>("return WorldMapFrame:IsVisible()").unwrap(),
"the map must not open behind the game menu"
);
assert!(s
.eval::<bool>("return GetFullScreenFrame() == nil")
.unwrap());
s.run("ToggleGameMenu(1)").unwrap();
s.run("ToggleWorldMap()").unwrap();
assert!(s.eval::<bool>("return WorldMapFrame:IsVisible()").unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// With the map holding the full screen nothing else opens (`UIParent.lua:668-675`), and ESC
/// closes the map without also opening the menu.
#[test]
fn nothing_opens_behind_the_world_map_and_escape_closes_it_first() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness_with(&[
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
// The action bar the micro buttons sit on: the map calls `UpdateMicroButtons` unguarded.
"Interface\\FrameXML\\Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
r"Interface\FrameXML\MainMenuBarMicroButtons.xml",
r"Interface\FrameXML\WorldMapFrame.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
// The stock loot window, as `test_ui::LOOT_UI`.
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which UnitPopup.lua reads at file scope
"Interface\\FrameXML\\UnitPopup.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\UnitFrame.xml",
"Interface\\FrameXML\\BuffFrame.xml",
"Interface\\FrameXML\\PartyFrame.xml",
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"Interface\\FrameXML\\LootFrame.xml",
]);
s.run("ToggleWorldMap()").unwrap();
s.run("ShowUIPanel(LootFrame)").unwrap();
assert!(
!s.eval::<bool>("return LootFrame:IsVisible()").unwrap(),
"a left-area panel must not open behind the full-screen map"
);
s.run("ToggleGameMenu()").unwrap();
assert!(!s.eval::<bool>("return WorldMapFrame:IsVisible()").unwrap());
assert!(
!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
"…and did not also open the menu"
);
assert!(s
.eval::<bool>("return GetFullScreenFrame() == nil")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Checked at the quad level, as `IsEnabled()` cannot tell greyed from gone: a disabled button
/// draws no state texture without a DisabledTexture (`SetState`, `0x779790`), and each bag icon is
/// a layer texture that `Disable_BagButtons` desaturates (`MainMenuBarBagButtons.lua:102-113`).
#[test]
fn the_bag_row_greys_under_the_menu_without_any_of_it_disappearing() {
let _data = benilla_formats::wow_data_or_skip!();
// The action bar, whose `MainMenuBarArtFrame` parents the bag bar, ahead of the bag stack.
let mut s = bag_harness_with(
&[
"Interface\\FrameXML\\Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
],
&[
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\MerchantFrame.xml",
],
);
s.set_money(0);
s.set_container(0, Some(backpack()));
s.resolve();
let art = |s: &UiScript, owner: &str| -> Vec<(String, bool)> {
s.extract()
.into_iter()
.filter(|eq| s.quad_owner_name(eq.target).as_deref() == Some(owner))
.filter_map(|eq| match &eq.content {
benilla_ui::script::QuadContent::Texture {
path: Some(p),
desaturated,
..
} => Some((p.clone(), *desaturated)),
_ => None,
})
.collect()
};
for owner in [
"MainMenuBarBackpackButton",
"CharacterBag0Slot",
"CharacterBag1Slot",
"CharacterBag2Slot",
"CharacterBag3Slot",
] {
assert!(
!art(&s, owner).is_empty(),
"{owner} draws art before the menu opens"
);
}
s.run("ToggleGameMenu(1)").unwrap();
s.resolve();
for owner in [
"MainMenuBarBackpackButton",
"CharacterBag0Slot",
"CharacterBag1Slot",
"CharacterBag2Slot",
"CharacterBag3Slot",
] {
let drawn = art(&s, owner);
assert!(
!drawn.is_empty(),
"{owner} must still DRAW under the open menu — greyed is not gone"
);
}
// The backpack icon keeps its art and carries `SetDesaturation`'s greyscale flag.
let toggle = art(&s, "MainMenuBarBackpackButton");
assert!(
toggle
.iter()
.any(|(p, _)| p == "Interface\\Buttons\\Button-Backpack-Up"),
"the backpack image is still on the bar: {toggle:?}"
);
assert!(
toggle
.iter()
.any(|(p, grey)| p == "Interface\\Buttons\\Button-Backpack-Up" && *grey),
"…and it is greyed, not full-bright: {toggle:?}"
);
s.run("ToggleGameMenu(1)").unwrap();
s.resolve();
let toggle = art(&s, "MainMenuBarBackpackButton");
assert!(
toggle
.iter()
.any(|(p, grey)| p == "Interface\\Buttons\\Button-Backpack-Up" && !*grey),
"closing the menu restores full colour: {toggle:?}"
);
assert!(
s.eval::<bool>("return MainMenuBarBackpackButton:IsEnabled() ~= 0")
.unwrap(),
"…and the button works again"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// A one-item backpack.
fn backpack() -> ContainerState {
let mut slots = std::collections::HashMap::new();
slots.insert(
1,
ContainerSlot {
duration_ms: None,
petition: None,
already_bound: false,
bar_placeable: true,
durability: None,
texture: Some("Interface\\Icons\\INV_Misc_Food_16".into()),
count: 1,
quality: Some(1),
item_id: 117,
link: Some("|cffffffff|Hitem:117|h[Tough Jerky]|h|r".into()),
locked: false,
equip_slots: Vec::new(),
cooldown: None,
readable: false,
creator: None,
flags: 0,
enchants: Vec::new(),
},
);
ContainerState {
name: Some("Backpack".into()),
num_slots: 16,
slots,
}
}
/// The menu wears the options window's `ERA_WINDOW_SCALE` on show, as the era client draws the two
/// at one density; loaded with OptionsFrame.xml, which defines it.
#[test]
fn the_menu_rides_the_shared_era_window_scale() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness_with(&[
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
]);
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
s.resolve();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
let ran = s.eval::<f64>("return BENILLA_MENU_ONSHOW or -99").unwrap();
println!("handler ran probe: {ran}");
let k = s.eval::<f64>("return GameMenuFrame:GetScale()").unwrap();
let want = s.eval::<f64>("return ERA_WINDOW_SCALE").unwrap();
assert!((k - want).abs() < 1e-6, "menu scale {k} != knob {want}");
assert!((want - 0.78).abs() < 1e-6, "the knob itself moved: {want}");
}