diff --git a/crates/benilla-app/assets/ui/GameMenuAdapters.xml b/crates/benilla-app/assets/ui/GameMenuAdapters.xml
new file mode 100644
index 00000000..6962cddf
--- /dev/null
+++ b/crates/benilla-app/assets/ui/GameMenuAdapters.xml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/crates/benilla-app/assets/ui/GameMenuFrame.xml b/crates/benilla-app/assets/ui/GameMenuFrame.xml
deleted file mode 100644
index 955fa369..00000000
--- a/crates/benilla-app/assets/ui/GameMenuFrame.xml
+++ /dev/null
@@ -1,216 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -- ERA_WINDOW_SCALE (OptionsFrame.xml) stands in for the era's UIParent scale. A
- -- second OnShow on this frame would replace this one.
- this:SetScale(ERA_WINDOW_SCALE or 1)
- if UpdateMicroButtons then UpdateMicroButtons() end
- if Disable_BagButtons then Disable_BagButtons() end
-
-
- if UpdateMicroButtons then UpdateMicroButtons() end
- if Enable_BagButtons then Enable_BagButtons() end
-
-
-
-
diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml
index dd9c8f4b..38fb1b0c 100644
--- a/crates/benilla-app/assets/ui/OptionsFrame.xml
+++ b/crates/benilla-app/assets/ui/OptionsFrame.xml
@@ -1256,7 +1256,7 @@
-
+
diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc
index 6b27b073..8e1bad2e 100644
--- a/crates/benilla-app/assets/ui/benilla.toc
+++ b/crates/benilla-app/assets/ui/benilla.toc
@@ -1,7 +1,6 @@
# The core interface, in load order: the reference's own `FrameXML.toc` files, off the player's
-# patch chain, and one bare filename of ours from `assets/ui`. Entry 0, Fonts.xml, loads alone
-# into every boot VM, from Startup on; the rest loads on entering the world, then benilla's layer
-# (`layer.toc`), then the third-party addons.
+# patch chain. Entry 0, Fonts.xml, loads alone into every boot VM, from Startup on; the rest loads
+# on entering the world, then benilla's layer (`layer.toc`), then the third-party addons.
# The font objects thus exist before the login screen; the reference loads Fonts.xml with the
# rest of FrameXML.
# `inherits=`, `parent=` and a named `relativeTo=` resolve only against names declared above; a
@@ -54,6 +53,9 @@ Interface\FrameXML\ComboFrame.xml
Interface\FrameXML\UIPanelTemplates.lua
Interface\FrameXML\UIPanelTemplates.xml
+# Inherits `GameMenuButtonTemplate`. It sources UIParent.lua again, as at the reference's own row,
+# which rebuilds UIParent.lua's tables; no row between here and UIParent.xml writes into them.
+Interface\FrameXML\GameMenuFrame.xml
# The reference's three options windows, loaded hidden so addons find their frames by name.
# Deviation: the player opens the layer's own options window, because 1.12's settings screens are
# much worse to use; the stock one holds `OptionsFrame`.
@@ -148,7 +150,3 @@ Interface\FrameXML\HelpFrame.xml
Interface\FrameXML\CinematicFrame.xml
Interface\FrameXML\TabardFrame.xml
Interface\FrameXML\TutorialFrame.xml
-# Ours, in place of the stock GameMenuFrame.xml until the layer reshapes the stock menu (#553).
-# Last: it wraps `ToggleGameMenu`, `IsOptionFrameOpen` and `UpdateMicroButtons` to count
-# `BenillaOptionsFrame`.
-GameMenuFrame.xml
diff --git a/crates/benilla-app/assets/ui/layer.toc b/crates/benilla-app/assets/ui/layer.toc
index 17843450..141bdb68 100644
--- a/crates/benilla-app/assets/ui/layer.toc
+++ b/crates/benilla-app/assets/ui/layer.toc
@@ -11,6 +11,8 @@ ScrollTemplates.xml
KeyBindingsPage.xml
# `BenillaOptionsFrame`; its rows read the option globals the stock options windows set.
OptionsFrame.xml
+# The ESC menu's era look on the stock `GameMenuFrame`, whose Options rung opens ours.
+GameMenuAdapters.xml
# Wraps the stock bag verbs; the keyring is a container (id -2).
ContainerFrameAdapters.xml
# The pet page's autocast widgets in the pet action button's proportions.
diff --git a/crates/benilla-app/src/ui_script/escape_tests.rs b/crates/benilla-app/src/ui_script/escape_tests.rs
index 4027a194..7dcf8e73 100644
--- a/crates/benilla-app/src/ui_script/escape_tests.rs
+++ b/crates/benilla-app/src/ui_script/escape_tests.rs
@@ -181,10 +181,11 @@ fn escape_closes_the_options_window_before_opening_the_menu() {
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
+ load_xml(&s, r"Interface\FrameXML\GameMenuFrame.xml");
load_xml(&s, "ScrollTemplates.xml"); // the Keybindings page's faux-scroll kit
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
- load_xml(&s, "GameMenuFrame.xml");
+ load_xml(&s, "GameMenuAdapters.xml");
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
assert!(s
@@ -469,7 +470,8 @@ fn an_addon_frame_registered_in_uispecialframes_closes_on_escape() {
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
- load_xml(&s, "GameMenuFrame.xml");
+ load_xml(&s, r"Interface\FrameXML\GameMenuFrame.xml");
+ load_xml(&s, "GameMenuAdapters.xml");
s.run(
r#"
diff --git a/crates/benilla-app/src/ui_script/frame_flag_gate.rs b/crates/benilla-app/src/ui_script/frame_flag_gate.rs
index 5eec88f3..f5fcec0c 100644
--- a/crates/benilla-app/src/ui_script/frame_flag_gate.rs
+++ b/crates/benilla-app/src/ui_script/frame_flag_gate.rs
@@ -401,7 +401,7 @@ fn the_shipped_frames_carry_the_references_flags() {
assert!(
// The floor guards the pairing, not the census: the paired count falls as each of our
// files declaring a reference-named frame retires.
- compared > 5,
+ compared >= 1,
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
);
assert!(
diff --git a/crates/benilla-app/src/ui_script/game_menu_addon_tests.rs b/crates/benilla-app/src/ui_script/game_menu_addon_tests.rs
new file mode 100644
index 00000000..257b948a
--- /dev/null
+++ b/crates/benilla-app/src/ui_script/game_menu_addon_tests.rs
@@ -0,0 +1,389 @@
+//! The stock ESC menu's rungs under the addons that reshape it (ShaguTweaks, MCP, pfUI), with the
+//! layer on and without it: every stock rung stays in the chain, anchored `TOP`, so an addon's
+//! insert lays out as on 1.12.1. Without the layer, the menu is the stock one.
+
+use std::path::Path;
+
+use benilla_ui::script::UiScript;
+
+use crate::local_state::test_env::ENV_LOCK;
+
+/// The production in-game load with the ESC menu open: the layer on unless `stock_ui`, and the
+/// AddOns root filled by `addons`. Fails on any load error or raise.
+fn open_menu_with(tag: &str, stock_ui: bool, addons: impl FnOnce(&Path)) -> UiScript {
+ let (mut s, failures) = super::layer_tests::production_load_with(tag, stock_ui, "", addons);
+ assert!(failures.is_empty(), "load failures: {failures:#?}");
+ s.run("ShowUIPanel(GameMenuFrame)").unwrap();
+ s.resolve();
+ assert!(s.eval::("return GameMenuFrame:IsVisible()").unwrap());
+ assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+ s
+}
+
+/// An addon of `files`, each a name and its text, in the AddOns root.
+fn write_addon(root: &Path, name: &str, files: &[(&str, &str)]) {
+ let dir = root.join(name);
+ std::fs::create_dir_all(&dir).unwrap();
+ for (file, body) in files {
+ std::fs::write(dir.join(file), body).unwrap();
+ }
+}
+
+/// The corpus addon `name`, copied whole into the AddOns root.
+fn copy_addon(corpus: &Path, root: &Path, name: &str) {
+ let dir = root.join(name);
+ std::fs::create_dir_all(&dir).unwrap();
+ for entry in std::fs::read_dir(corpus.join(name)).unwrap().flatten() {
+ if entry.file_type().unwrap().is_file() {
+ std::fs::copy(entry.path(), dir.join(entry.file_name())).unwrap();
+ }
+ }
+}
+
+/// A shown rung's `(top, bottom)` down from the frame's top, and whether it is centred in it.
+fn rung(s: &UiScript, name: &str) -> (f64, f64, bool) {
+ let (top, bottom, left, right, ftop, fleft, fright) = s
+ .eval::<(f64, f64, f64, f64, f64, f64, f64)>(&format!(
+ "local f = GameMenuFrame return {name}:GetTop(), {name}:GetBottom(), \
+ {name}:GetLeft(), {name}:GetRight(), f:GetTop(), f:GetLeft(), f:GetRight()"
+ ))
+ .unwrap_or_else(|e| panic!("{name}: {e}"));
+ let centred = ((left - fleft) - (fright - right)).abs() < 1e-3;
+ (ftop - top, ftop - bottom, centred)
+}
+
+/// The shown rungs of the open menu, top to bottom: the children of `GameMenuFrame` that draw.
+fn shown_rungs(s: &UiScript) -> Vec {
+ let mut names: Vec = s
+ .eval(
+ "local out = {} for _, c in ipairs({ GameMenuFrame:GetChildren() }) do \
+ if c:IsVisible() then table.insert(out, c:GetName()) end end return out",
+ )
+ .unwrap();
+ names.sort_by(|a, b| rung(s, a).0.total_cmp(&rung(s, b).0));
+ names
+}
+
+/// No two shown rungs overlap, each is centred, and each is 21 tall.
+fn assert_a_clean_ladder(s: &UiScript) -> Vec {
+ let names = shown_rungs(s);
+ for pair in names.windows(2) {
+ let (above, below) = (rung(s, &pair[0]), rung(s, &pair[1]));
+ assert!(
+ below.0 >= above.1 - 1e-3,
+ "{} (down {}..{}) overlaps {} (down {}..{})",
+ pair[1],
+ below.0,
+ below.1,
+ pair[0],
+ above.0,
+ above.1
+ );
+ }
+ for name in &names {
+ let (top, bottom, centred) = rung(s, name);
+ assert!(
+ (bottom - top - 21.0).abs() < 1e-3,
+ "{name} is {} tall",
+ bottom - top
+ );
+ assert!(centred, "{name} is off centre in the menu");
+ }
+ names
+}
+
+/// ShaguTweaks' insert (`config.lua:313-324`), restated in its own calls: narrow and deepen the
+/// menu, hang a new rung under Interface Options, then hang Key Bindings under the new rung.
+const SHAGU_ADVANCED_OPTIONS: &str = r#"
+local panel = CreateFrame("Frame", nil, UIParent)
+panel:Hide()
+GameMenuFrame:SetWidth(GameMenuFrame:GetWidth() - 10)
+GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + 10)
+local rung = CreateFrame("Button", "GameMenuButtonAdvancedOptions", GameMenuFrame, "GameMenuButtonTemplate")
+rung:SetPoint("TOP", GameMenuButtonUIOptions, "BOTTOM", 0, -1)
+rung:SetText("Advanced Options")
+rung:SetScript("OnClick", function() HideUIPanel(GameMenuFrame); panel:Show() end)
+GameMenuButtonKeybindings:ClearAllPoints()
+GameMenuButtonKeybindings:SetPoint("TOP", rung, "BOTTOM", 0, -1)
+"#;
+
+fn shagu(root: &Path) {
+ write_addon(
+ root,
+ "ZZShagu",
+ &[
+ ("ZZShagu.toc", "## Interface: 11200\nconfig.lua\n"),
+ ("config.lua", SHAGU_ADVANCED_OPTIONS),
+ ],
+ );
+}
+
+/// ShaguTweaks anchors its Advanced Options under `GameMenuButtonUIOptions` and re-anchors
+/// `GameMenuButtonKeybindings` under it (`GameMenuFrame.xml:78`, `:93`): it loads without a raise,
+/// the button lands 1 under the Options rung, and every rung below moves down by its height and
+/// the gap, 22, as the stock rungs do on the stock menu.
+#[test]
+fn shagutweaks_advanced_options_lands_under_the_options_rung_and_pushes_the_rest_down() {
+ let _data = benilla_formats::wow_data_or_skip!();
+ let _l = ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ for stock_ui in [false, true] {
+ let tag = if stock_ui { "shagu-stock" } else { "shagu" };
+ let base = open_menu_with(&format!("{tag}-base"), stock_ui, |_| {});
+ let s = open_menu_with(tag, stock_ui, shagu);
+ let before = shown_rungs(&base);
+ let after = assert_a_clean_ladder(&s);
+
+ let options = rung(&s, "GameMenuButtonOptions");
+ let advanced = rung(&s, "GameMenuButtonAdvancedOptions");
+ // Under the Options rung on ours, as the era menu has no Sound or UI Options rung.
+ let above = if stock_ui {
+ "GameMenuButtonUIOptions"
+ } else {
+ "GameMenuButtonOptions"
+ };
+ assert!(
+ (advanced.0 - (rung(&s, above).1 + 1.0)).abs() < 1e-3,
+ "stock_ui {stock_ui}: Advanced Options at {advanced:?}, 1 under {above}"
+ );
+ let at = after
+ .iter()
+ .position(|n| n == "GameMenuButtonAdvancedOptions")
+ .expect("the button is shown");
+ assert_eq!(after[at - 1], above, "stock_ui {stock_ui}: {after:?}");
+ let mut expected = before.clone();
+ expected.insert(at, "GameMenuButtonAdvancedOptions".into());
+ assert_eq!(after, expected, "stock_ui {stock_ui}: the ladder");
+ for name in &after[at + 1..] {
+ let moved = (rung(&s, name).0 - options.0)
+ - (rung(&base, name).0 - rung(&base, "GameMenuButtonOptions").0);
+ assert!(
+ (moved - 22.0).abs() < 1e-3,
+ "stock_ui {stock_ui}: {name} moved {moved} down, not 22"
+ );
+ }
+ }
+}
+/// MCP's AddOns button (`MCP.xml:108-127`) anchors `TOP` under Macros, sets Logout's `TOP` under
+/// itself without `ClearAllPoints` and grows the menu by 25: Logout's one `TOP` anchor is replaced,
+/// so Logout, Exit Game and Return to Game move down under it, as on the stock menu.
+#[test]
+fn mcps_addons_button_lands_under_macros_and_pushes_logout_down() {
+ let _data = benilla_formats::wow_data_or_skip!();
+ let corpus = benilla_formats::addon_corpus_or_skip!();
+ let _l = ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ for stock_ui in [false, true] {
+ let tag = if stock_ui { "mcp-stock" } else { "mcp" };
+ let base = open_menu_with(&format!("{tag}-base"), stock_ui, |_| {});
+ let s = open_menu_with(tag, stock_ui, |root| copy_addon(&corpus, root, "MCP"));
+ let after = assert_a_clean_ladder(&s);
+
+ let macros = rung(&s, "GameMenuButtonMacros");
+ let addons = rung(&s, "GameMenuButtonAddOns");
+ let logout = rung(&s, "GameMenuButtonLogout");
+ assert!(
+ (addons.0 - (macros.1 + 1.0)).abs() < 1e-3,
+ "stock_ui {stock_ui}: AddOns {addons:?} 1 under Macros {macros:?}"
+ );
+ assert!(
+ (logout.0 - (addons.1 + 1.0)).abs() < 1e-3,
+ "stock_ui {stock_ui}: Logout {logout:?} 1 under AddOns {addons:?}"
+ );
+ let at = after
+ .iter()
+ .position(|n| n == "GameMenuButtonAddOns")
+ .unwrap();
+ assert_eq!(after[at - 1], "GameMenuButtonMacros");
+ assert_eq!(
+ after[at + 1..],
+ [
+ "GameMenuButtonLogout",
+ "GameMenuButtonQuit",
+ "GameMenuButtonContinue"
+ ]
+ );
+ let height = |s: &UiScript| s.eval::("return GameMenuFrame:GetHeight()").unwrap();
+ assert!(
+ (height(&s) - height(&base) - 25.0).abs() < 1e-3,
+ "stock_ui {stock_ui}: the menu grows by MCP's 25"
+ );
+ let span = |s: &UiScript, a: &str, b: &str| rung(s, b).0 - rung(s, a).0;
+ for (a, b) in [
+ ("GameMenuButtonLogout", "GameMenuButtonQuit"),
+ ("GameMenuButtonQuit", "GameMenuButtonContinue"),
+ ] {
+ assert!(
+ (span(&s, a, b) - span(&base, a, b)).abs() < 1e-3,
+ "stock_ui {stock_ui}: {b} still follows {a}"
+ );
+ }
+ }
+}
+
+/// pfUI's game-menu skin (`skins/blizzard/game_menu.lua`) in its shape: it narrows the menu by 30,
+/// finds the unnamed MAIN_MENU title among the regions, adds a button at the top, shifts
+/// `GameMenuButtonOptions`' own point down 22 through `GetPoint`, and skins every stock rung that
+/// exists. The rungs stay centred in the narrower frame, as the stock ones do, with no raise.
+const PFUI_SHAPED_SKIN: &str = r#"
+GameMenuFrame:SetWidth(GameMenuFrame:GetWidth() - 30)
+GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + 6)
+
+local title
+for _, region in ipairs({ GameMenuFrame:GetRegions() }) do
+ if region:GetObjectType() == "FontString" and region:GetDrawLayer() == "ARTWORK"
+ and region:GetText() and string.find(region:GetText(), MAIN_MENU, 1) then
+ title = region
+ end
+end
+title:SetTextColor(1, 1, 1, 1)
+title:ClearAllPoints()
+title:SetPoint("TOP", GameMenuFrame, "TOP", 0, 16)
+
+local button = CreateFrame("Button", "GameMenuButtonPFUI", GameMenuFrame, "GameMenuButtonTemplate")
+button:SetPoint("TOP", 0, -10)
+button:SetText("pfUI Config")
+
+local point, relativeTo, relativePoint, xOffset, yOffset = GameMenuButtonOptions:GetPoint()
+GameMenuButtonOptions:SetPoint(point, relativeTo, relativePoint, xOffset, yOffset - 22)
+
+ZZPFUI_SKINNED = 0
+for _, rung in pairs({ GameMenuButtonOptions, GameMenuButtonSoundOptions, GameMenuButtonUIOptions,
+ GameMenuButtonKeybindings, GameMenuButtonMacros, GameMenuButtonRatings, GameMenuButtonLogout,
+ GameMenuButtonQuit, GameMenuButtonContinue }) do
+ if rung then ZZPFUI_SKINNED = ZZPFUI_SKINNED + 1 end
+end
+"#;
+
+#[test]
+fn a_pfui_shaped_skin_keeps_every_rung_centred_in_the_narrowed_menu() {
+ let _data = benilla_formats::wow_data_or_skip!();
+ let _l = ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ for stock_ui in [false, true] {
+ let tag = if stock_ui { "pfui-stock" } else { "pfui" };
+ let s = open_menu_with(tag, stock_ui, |root| {
+ write_addon(
+ root,
+ "ZZpfUI",
+ &[
+ ("ZZpfUI.toc", "## Interface: 11200\nskin.lua\n"),
+ ("skin.lua", PFUI_SHAPED_SKIN),
+ ],
+ );
+ });
+ let after = assert_a_clean_ladder(&s);
+ assert_eq!(
+ after[0], "GameMenuButtonPFUI",
+ "stock_ui {stock_ui}: {after:?}"
+ );
+ assert_eq!(
+ after[1], "GameMenuButtonOptions",
+ "stock_ui {stock_ui}: {after:?}"
+ );
+ assert_eq!(
+ s.eval::("return ZZPFUI_SKINNED").unwrap(),
+ 8,
+ "stock_ui {stock_ui}: every stock rung is there to skin"
+ );
+ }
+}
+
+/// Booted without the layer, as `WOW_STOCK_UI=1` does, the ESC menu is the stock one: its eight
+/// rungs at the stock file's geometry (`GameMenuFrame.xml:5-185`), each opening its stock window.
+#[test]
+fn the_stock_ui_menu_is_the_stock_menu_and_every_rung_opens_its_stock_window() {
+ use benilla_ui::script::SessionRequest;
+ let _data = benilla_formats::wow_data_or_skip!();
+ let _l = ENV_LOCK
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let mut s = open_menu_with("stock-menu", true, |_| {});
+ let rungs = assert_a_clean_ladder(&s);
+ assert_eq!(
+ rungs,
+ [
+ "GameMenuButtonOptions",
+ "GameMenuButtonSoundOptions",
+ "GameMenuButtonUIOptions",
+ "GameMenuButtonKeybindings",
+ "GameMenuButtonMacros",
+ "GameMenuButtonLogout",
+ "GameMenuButtonQuit",
+ "GameMenuButtonContinue",
+ ]
+ );
+ assert_eq!(
+ s.eval::<(f64, f64)>("return GameMenuFrame:GetWidth(), GameMenuFrame:GetHeight()")
+ .unwrap(),
+ (195.0, 246.0)
+ );
+ // Options' centre 37 under the top, then 1-unit gaps, and 16 above Return to Game.
+ let tops: Vec = rungs.iter().map(|n| rung(&s, n).0).collect();
+ let want = [26.5, 48.5, 70.5, 92.5, 114.5, 136.5, 158.5, 195.5];
+ for ((name, got), want) in rungs.iter().zip(&tops).zip(want) {
+ assert!((got - want).abs() < 1e-3, "{name} at {got}, not {want}");
+ }
+ let labels: Vec = s
+ .eval(
+ "local out = {} for _, n in ipairs({ 'Options', 'SoundOptions', 'UIOptions', \
+ 'Keybindings', 'Macros', 'Logout', 'Quit', 'Continue' }) do \
+ table.insert(out, getglobal('GameMenuButton' .. n):GetText()) end return out",
+ )
+ .unwrap();
+ let globals: Vec = s
+ .eval(
+ "return { VIDEOOPTIONS_MENU, SOUNDOPTIONS_MENU, UIOPTIONS_MENU, KEY_BINDINGS, MACROS, \
+ LOGOUT, EXIT_GAME, RETURN_TO_GAME }",
+ )
+ .unwrap();
+ assert_eq!(labels, globals);
+ assert!(!s
+ .eval::("return BenillaGameMenuButtonEditMode ~= nil")
+ .unwrap());
+
+ for (rung, window) in [
+ ("Options", "OptionsFrame"),
+ ("SoundOptions", "SoundOptionsFrame"),
+ ("UIOptions", "UIOptionsFrame"),
+ ("Keybindings", "KeyBindingFrame"),
+ ("Macros", "MacroFrame"),
+ ] {
+ s.run("HideUIPanel(GameMenuFrame) ShowUIPanel(GameMenuFrame)")
+ .unwrap();
+ s.run(&format!("GameMenuButton{rung}:Click()")).unwrap();
+ assert!(
+ s.eval::(&format!(
+ "return {window} ~= nil and {window}:IsVisible() and true or false"
+ ))
+ .unwrap(),
+ "{rung} opens {window}"
+ );
+ s.run(&format!("HideUIPanel({window})")).unwrap();
+ // The stock video window's own show and hide read video CVars and verbs benilla does not
+ // register yet (`OptionsFrame.lua:146`, `:260`); what the menu does is the click above.
+ let errors = s.take_errors();
+ assert!(
+ errors
+ .iter()
+ .all(|e| rung == "Options" && e.contains("OptionsFrame.lua")),
+ "{rung}: {errors:?}"
+ );
+ }
+ for (rung, request) in [
+ ("Logout", SessionRequest::Logout),
+ ("Quit", SessionRequest::Quit),
+ ] {
+ s.run("ShowUIPanel(GameMenuFrame)").unwrap();
+ let _ = s.take_session_requests();
+ s.run(&format!("GameMenuButton{rung}:Click()")).unwrap();
+ assert_eq!(s.take_session_requests(), vec![request], "{rung}");
+ }
+ s.run("ShowUIPanel(GameMenuFrame) GameMenuButtonContinue:Click()")
+ .unwrap();
+ assert!(!s.eval::("return GameMenuFrame:IsVisible()").unwrap());
+ assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
+}
diff --git a/crates/benilla-app/src/ui_script/game_menu_tests.rs b/crates/benilla-app/src/ui_script/game_menu_tests.rs
index 02a3d4b8..48363b2a 100644
--- a/crates/benilla-app/src/ui_script/game_menu_tests.rs
+++ b/crates/benilla-app/src/ui_script/game_menu_tests.rs
@@ -1,4 +1,5 @@
-//! Tests for our `GameMenuFrame.xml`, the frame ESC opens. The bags in its way are the stock
+//! Tests for the frame ESC opens: the stock `GameMenuFrame.xml` as the layer's
+//! `GameMenuAdapters.xml` reshapes it. 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::{
@@ -7,8 +8,8 @@ use benilla_ui::script::{
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.
+/// The stock menu over what it needs (`GameMenuButtonTemplate` is `UIPanelTemplates.xml:403`),
+/// with `extra` in its way, then the layer's reshaping, 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);
@@ -24,10 +25,12 @@ fn harness_with(extra: &[&str]) -> UiScript {
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
+ // Ahead of `extra`, as the core loads before the layer: it sources UIParent.lua again.
+ r"Interface\FrameXML\GameMenuFrame.xml",
]
.into_iter()
.chain(extra.iter().copied())
- .chain(std::iter::once("GameMenuFrame.xml"))
+ .chain(std::iter::once("GameMenuAdapters.xml"))
.collect();
let mut loaded = std::collections::HashSet::new();
for file in files {
@@ -58,8 +61,8 @@ fn bag_harness_with(before: &[&str], after: &[&str]) -> UiScript {
/// 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",
+ "BenillaGameMenuButtonEditMode",
+ "BenillaGameMenuButtonSupport",
"GameMenuButtonMacros",
"GameMenuButtonLogout",
"GameMenuButtonQuit",
@@ -114,7 +117,10 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
let s = harness();
s.run("ShowUIPanel(GameMenuFrame)").unwrap();
- for name in ["GameMenuButtonEditMode", "GameMenuButtonSupport"] {
+ for name in [
+ "BenillaGameMenuButtonEditMode",
+ "BenillaGameMenuButtonSupport",
+ ] {
assert!(
!s.eval::(&format!("return {name}:IsEnabled() ~= 0"))
.unwrap(),
@@ -134,10 +140,10 @@ fn the_unbacked_entries_are_disabled_and_the_rest_are_live() {
"{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".
+ // Edit Mode is the era's label 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::("return GameMenuButtonEditMode:GetText()")
+ s.eval::("return BenillaGameMenuButtonEditMode:GetText()")
.unwrap(),
"Edit Mode"
);
diff --git a/crates/benilla-app/src/ui_script/keybindings_tests.rs b/crates/benilla-app/src/ui_script/keybindings_tests.rs
index ab529494..df517194 100644
--- a/crates/benilla-app/src/ui_script/keybindings_tests.rs
+++ b/crates/benilla-app/src/ui_script/keybindings_tests.rs
@@ -35,10 +35,12 @@ pub(crate) fn harness() -> UiScript {
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
+ // Before our files, as the core loads before the layer: it sources UIParent.lua again.
+ r"Interface\FrameXML\GameMenuFrame.xml",
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
- "GameMenuFrame.xml",
+ "GameMenuAdapters.xml",
] {
// Strict for ours: an unknown template only warns, so a skinless window would pass.
if file == "KeyBindingsPage.xml" || file == "OptionsFrame.xml" {
diff --git a/crates/benilla-app/src/ui_script/layer_tests.rs b/crates/benilla-app/src/ui_script/layer_tests.rs
index a812cb70..b2b59ee7 100644
--- a/crates/benilla-app/src/ui_script/layer_tests.rs
+++ b/crates/benilla-app/src/ui_script/layer_tests.rs
@@ -28,17 +28,34 @@ f:SetScript("OnEvent", function() table.insert(ADDON_LOADED_LOG, arg1) end)
/// A VM after the production in-game load, with one third-party addon, `ZZOrder`, in a hermetic
/// AddOns root; the layer on unless `stock_ui`. Returns the VM and the load's failures.
fn production_load(tag: &str, stock_ui: bool) -> (UiScript, Vec) {
+ let before = format!("{DEFINE_LOG}\n{ADDON_LOADED_LOG}");
+ production_load_with(tag, stock_ui, &before, |root| {
+ let dir = root.join("ZZOrder");
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join("ZZOrder.toc"), "## Interface: 11200\nprobe.lua\n").unwrap();
+ std::fs::write(
+ dir.join("probe.lua"),
+ "ZZORDER_SAW_LAYER = BenillaOptionsFrame ~= nil\n",
+ )
+ .unwrap();
+ })
+}
+
+/// The production in-game load, the layer on unless `stock_ui`, over a hermetic AddOns root that
+/// `addons` fills; `before` runs ahead of the load. The caller holds [`ENV_LOCK`]. Returns the VM
+/// and the load's failures.
+pub(super) fn production_load_with(
+ tag: &str,
+ stock_ui: bool,
+ before: &str,
+ addons: impl FnOnce(&std::path::Path),
+) -> (UiScript, Vec) {
let tmp = std::env::temp_dir().join(format!("benilla-layer-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let home = tmp.join("benilla-config");
- let dir = home.join("AddOns").join("ZZOrder");
- std::fs::create_dir_all(&dir).unwrap();
- std::fs::write(dir.join("ZZOrder.toc"), "## Interface: 11200\nprobe.lua\n").unwrap();
- std::fs::write(
- dir.join("probe.lua"),
- "ZZORDER_SAW_LAYER = BenillaOptionsFrame ~= nil\n",
- )
- .unwrap();
+ let root = home.join("AddOns");
+ std::fs::create_dir_all(&root).unwrap();
+ addons(&root);
let _c = EnvGuard::unset("WOW_CAPTURE");
let _h = EnvGuard::set("BENILLA_HOME", home.to_str().unwrap());
@@ -50,6 +67,8 @@ fn production_load(tag: &str, stock_ui: bool) -> (UiScript, Vec) {
exists: true,
name: Some("Probesix".into()),
level: 60,
+ class: Some("Warrior".into()),
+ class_file: Some("WARRIOR".into()),
..Default::default()
}),
);
@@ -57,8 +76,7 @@ fn production_load(tag: &str, stock_ui: bool) -> (UiScript, Vec) {
s.note_addon_info_reply(&[]);
s.register_cvars(crate::cvars::registered_pairs());
let mut failures = super::load_font_registry(&s);
- s.run(DEFINE_LOG).unwrap();
- s.run(ADDON_LOADED_LOG).unwrap();
+ s.run(before).unwrap();
// The layer passed in, not set through `WOW_STOCK_UI`: every test in this process reads it.
failures.extend(super::manifest::load_ingame_ui_with(
&mut s,
@@ -109,12 +127,10 @@ fn the_layer_loads_after_every_stock_file_and_before_the_addons() {
assert!(failures.is_empty(), "load failures: {failures:#?}");
let log: Vec = s.eval("return DEFINE_LOG").unwrap();
- // The core's last stock row (TutorialFrame.lua), the core's own last row (GameMenuFrame.xml),
- // the layer's first file (ScrollTemplates.xml) and last (FrameXMLFixes.xml is all
- // redefinitions, so ScriptLogFrame.xml), then the addon.
+ // The core's last row (TutorialFrame.lua), the layer's first file (ScrollTemplates.xml) and
+ // last (FrameXMLFixes.xml is all redefinitions, so ScriptLogFrame.xml), then the addon.
let order = [
"TutorialFrame_OnHide",
- "GameMenuButton_Pending",
"BenillaScrollBar_Step",
"BenillaScriptLog_Toggle",
"ZZORDER_SAW_LAYER",
@@ -139,7 +155,7 @@ fn the_layer_loads_after_every_stock_file_and_before_the_addons() {
|| n.strip_prefix("arg")
.is_some_and(|d| d.len() == 1 && d.as_bytes()[0].is_ascii_digit())
};
- let late: Vec<&String> = log[at[2]..]
+ let late: Vec<&String> = log[at[1]..]
.iter()
.filter(|n| stock.contains(n.as_str()) && !dispatch(n))
.collect();
@@ -235,6 +251,7 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() {
"BenillaScrollBar_Step",
"KeyBindings_OnHostKey",
"BenillaOptionsFrame_SelectCategory",
+ "BenillaGameMenuButtonEditMode",
"BENILLA_BAG_WAS_OPEN",
"BenillaScriptLog_Toggle",
] {
@@ -245,12 +262,11 @@ fn the_stock_ui_switch_boots_the_core_without_the_layer() {
}
assert!(!s.eval::("return BenillaOptionsFrame ~= nil").unwrap());
assert!(!s.eval::("return ZZORDER_SAW_LAYER").unwrap());
- // The host's calls into the layer, and the game menu's Options rung.
+ // The host's calls into the layer.
for body in [
super::ERRORS_TOGGLE.to_string(),
super::ERRORS_CLEAR.to_string(),
super::host_key_capture("CTRL-J"),
- "GameMenuButtonOptions:Click()".to_string(),
] {
s.run(&body).unwrap_or_else(|e| panic!("{body}: {e}"));
}
diff --git a/crates/benilla-app/src/ui_script/mod.rs b/crates/benilla-app/src/ui_script/mod.rs
index dea47e2d..f65c87f6 100644
--- a/crates/benilla-app/src/ui_script/mod.rs
+++ b/crates/benilla-app/src/ui_script/mod.rs
@@ -817,6 +817,8 @@ mod tooltip_template_tests;
#[cfg(test)]
mod escape_tests;
+#[cfg(test)]
+mod game_menu_addon_tests;
#[cfg(test)]
mod game_menu_tests;
diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs
index 5deca590..32663df5 100644
--- a/crates/benilla-app/src/ui_script/options_tests.rs
+++ b/crates/benilla-app/src/ui_script/options_tests.rs
@@ -34,6 +34,8 @@ fn harness_on(mut s: UiScript) -> UiScript {
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
+ // Before our files, as the core loads before the layer: it sources UIParent.lua again.
+ r"Interface\FrameXML\GameMenuFrame.xml",
// The stock options kit, hidden: `UIOptionsFrame_Init` assigns the option globals our
// rows capture at OnLoad, and these declare the tables the Graphics rows and census read.
r"Interface\FrameXML\OptionsFrameTemplates.xml",
@@ -42,7 +44,7 @@ fn harness_on(mut s: UiScript) -> UiScript {
"ScrollTemplates.xml", // the page scroll and the Keybindings list
"KeyBindingsPage.xml", // the Keybindings page's templates and script
"OptionsFrame.xml",
- "GameMenuFrame.xml",
+ "GameMenuAdapters.xml",
] {
// Our window loads strict: a missing template there fails instead of only warning.
if loaded(&s, file) {
diff --git a/crates/benilla-app/src/ui_script/reference_ui.rs b/crates/benilla-app/src/ui_script/reference_ui.rs
index 1453d9d0..62f99b86 100644
--- a/crates/benilla-app/src/ui_script/reference_ui.rs
+++ b/crates/benilla-app/src/ui_script/reference_ui.rs
@@ -1322,10 +1322,9 @@ mod tests {
);
}
- /// The core's chain rows are the reference's own `FrameXML.toc` rows, every one, but the stock
- /// `GameMenuFrame.xml`, which ours stands in for until the layer reshapes the stock menu. A
- /// row the stock toc lacks runs a file the reference does not, or twice: `UIParent.xml:4`
- /// sources `LocaleProperties.lua`, which has no row.
+ /// The core's chain rows are the reference's own `FrameXML.toc` rows, every one. A row the
+ /// stock toc lacks runs a file the reference does not, or twice: `UIParent.xml:4` sources
+ /// `LocaleProperties.lua`, which has no row.
#[test]
fn the_cores_chain_rows_are_the_stock_tocs_rows() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -1337,7 +1336,6 @@ mod tests {
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
- .filter(|l| !l.eq_ignore_ascii_case("GameMenuFrame.xml"))
.map(|l| format!("interface\\framexml\\{}", l.to_ascii_lowercase()))
.collect();
let mut core: Vec = super::super::addons::Addon::core()
diff --git a/crates/benilla-app/src/ui_script/shipped_xml_tests.rs b/crates/benilla-app/src/ui_script/shipped_xml_tests.rs
index 0e423a60..f4397ea0 100644
--- a/crates/benilla-app/src/ui_script/shipped_xml_tests.rs
+++ b/crates/benilla-app/src/ui_script/shipped_xml_tests.rs
@@ -486,7 +486,7 @@ fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
};
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
- let mut keys = 0;
+ let mut swept = 0;
for entry in std::fs::read_dir(&dir).expect("assets/ui").flatten() {
let path = entry.path();
if path.extension().is_none_or(|e| e != "xml") {
@@ -505,7 +505,6 @@ fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
"{file}: text=\"{value}\" is shaped like a GlobalStrings key but the real \
GlobalStrings.lua has no such string — it would render as its own key name"
);
- keys += 1;
} else {
assert!(
resolved.is_none(),
@@ -513,10 +512,11 @@ fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
the loader would silently show that string's value instead of these words"
);
}
+ swept += 1;
}
}
// A floor, so the sweep cannot pass by matching nothing.
- assert!(keys >= 1, "only {keys} key-shaped text= values swept");
+ assert!(swept >= 1, "only {swept} text= values swept");
}
/// No shipped script hands a GlobalStrings key to a text sink as its words: `SetText` cannot tell
@@ -1096,7 +1096,7 @@ fn every_declared_parent_really_attaches() {
let declared = shipped_frame_parents();
assert!(
// A sanity floor for the scan, not a census.
- declared.len() >= 2,
+ !declared.is_empty(),
"only {} parent declarations found — the scan broke",
declared.len()
);
@@ -1445,7 +1445,7 @@ fn the_stock_sound_options_window_loads_hidden_and_the_alias_is_gone() {
);
// `IsOptionFrameOpen` names the stock options windows, all hidden; it sees ours through the
- // wrapper `GameMenuFrame.xml` installs.
+ // wrapper `GameMenuAdapters.xml` installs.
assert!(
s.eval::("return IsOptionFrameOpen() and true or false")
.unwrap(),
@@ -1503,7 +1503,7 @@ fn the_stock_sound_window_turns_a_box_off_and_stores_a_slider_as_lua_text() {
/// The stock Video Options window loads hidden (`OptionsFrame.xml:5`) and owns the `OptionsFrame`
/// name; ours is `BenillaOptionsFrame`, which the ESC menu opens and the stock "options window
-/// open?" consumers see only through the `GameMenuFrame.xml` wrappers.
+/// open?" consumers see only through the `GameMenuAdapters.xml` wrappers.
#[test]
fn the_stock_video_options_window_loads_hidden_and_owns_its_own_name() {
let _data = benilla_formats::wow_data_or_skip!();
@@ -1989,8 +1989,7 @@ fn the_stock_options_windows_load_and_save_are_reachable_for_addons() {
}
/// benilla's own interface is one layer, and it does not grow: `layer.toc` names exactly these
-/// files, all ours (none off the chain), and `assets/ui` holds nothing else but the two manifests
-/// and the core's one file. A window is migrated, not authored: point `benilla.toc` at
+/// files, all ours (none off the chain), and `assets/ui` holds nothing else but the two manifests. A window is migrated, not authored: point `benilla.toc` at
/// `Interface\FrameXML\.xml`, delete ours, and build the engine verbs the stock file
/// calls; what changes the stock UI goes in one of these files.
#[test]
@@ -1999,6 +1998,7 @@ fn the_layer_does_not_grow() {
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
+ "GameMenuAdapters.xml",
"ContainerFrameAdapters.xml",
"SpellBookAdapters.xml",
"ScriptLogFrame.xml",
@@ -2020,44 +2020,28 @@ fn the_layer_does_not_grow() {
found.sort();
let mut shipped: Vec = LAYER
.iter()
- .chain(&["benilla.toc", "layer.toc", "GameMenuFrame.xml"])
+ .chain(&["benilla.toc", "layer.toc"])
.map(|s| s.to_string())
.collect();
shipped.sort();
assert_eq!(
found, shipped,
- "assets/ui holds the two manifests, the layer's files and the core's one file, nothing else"
+ "assets/ui holds the two manifests and the layer's files, nothing else"
);
}
-/// The core is the stock UI: `benilla.toc` names the reference's own files off the chain and one
-/// file of ours, `GameMenuFrame.xml`, which stands in for the stock one until the layer reshapes
-/// the stock menu, and whose row says so.
+/// The core is the stock UI: `benilla.toc` names the reference's own files off the chain and no
+/// file of ours.
#[test]
-fn the_core_toc_names_no_file_of_ours_but_the_game_menu() {
+fn the_core_toc_names_no_file_of_ours() {
let core = super::addons::Addon::core().toc.files;
let ours: Vec<&String> = core
.iter()
.filter(|f| !super::reference_ui::is_chain_entry(f))
.collect();
- assert_eq!(
- ours,
- ["GameMenuFrame.xml"],
- "benilla.toc names a file of ours: benilla's own interface is the layer (layer.toc), \
- which loads after every stock file"
- );
- let toc = super::content::read(super::manifest::MANIFEST).expect("benilla.toc");
- let lines: Vec<&str> = toc.lines().collect();
- let at = lines
- .iter()
- .position(|l| l.trim() == "GameMenuFrame.xml")
- .expect("the row");
assert!(
- lines[..at]
- .iter()
- .rev()
- .take_while(|l| l.starts_with('#'))
- .any(|l| l.contains("#553")),
- "GameMenuFrame.xml's row names the issue that retires it"
+ ours.is_empty(),
+ "benilla.toc names a file of ours, {ours:?}: benilla's own interface is the layer \
+ (layer.toc), which loads after every stock file"
);
}
diff --git a/crates/benilla-app/src/ui_script/test_ui.rs b/crates/benilla-app/src/ui_script/test_ui.rs
index b8773108..ac4c75c6 100644
--- a/crates/benilla-app/src/ui_script/test_ui.rs
+++ b/crates/benilla-app/src/ui_script/test_ui.rs
@@ -130,9 +130,16 @@ fn load_entry(s: &UiScript, entry: &str, strict_templates: bool, no_warnings: bo
s.run(MICRO_BUTTON_STAND_INS)
.expect("the micro-button stand-ins");
}
- if leaf.eq_ignore_ascii_case("UIParent.xml") && super::reference_ui::is_chain_entry(&path) {
+ // The stock GameMenuFrame.xml sources UIParent.lua again, which drops the stand-ins' wrappers.
+ if (leaf.eq_ignore_ascii_case("UIParent.xml") || leaf.eq_ignore_ascii_case("GameMenuFrame.xml"))
+ && super::reference_ui::is_chain_entry(&path)
+ {
s.run(UIPARENT_STAND_INS).expect("the UIParent stand-ins");
}
+ if leaf.eq_ignore_ascii_case("GameMenuFrame.xml") && super::reference_ui::is_chain_entry(&path)
+ {
+ s.run(GAME_MENU_STAND_INS).expect("the game menu stand-ins");
+ }
if leaf.eq_ignore_ascii_case("UIOptionsFrame.xml") {
s.run(UIOPTIONS_STAND_INS)
.expect("the stock options window's stand-ins");
@@ -241,7 +248,7 @@ pub(super) const UIPARENT_STAND_INS: &str = r#"
-- names are real, and all three options windows are the REFERENCE's own files,
-- loaded hidden — including `OptionsFrame`, the video window, which used to be our own
-- window's name. Ours is `BenillaOptionsFrame` now and is not in this list: it is not a name
- -- the reference indexes, and the wrappers in `GameMenuFrame.xml` are what tell these two
+ -- the reference indexes, and the wrappers in `GameMenuAdapters.xml` are what tell these two
-- functions about it. A KIT is a prefix of the manifest and may load none of the four, which
-- is what these stand-ins are for.
local function benilla_seat_options()
@@ -263,6 +270,15 @@ pub(super) const UIPARENT_STAND_INS: &str = r#"
end
"#;
+/// What a kit owes stock `GameMenuFrame.xml`: its OnShow and OnHide (`GameMenuFrame.xml:188-195`)
+/// call the micro-button and bag-bar verbs unguarded, from files a kit may stop short of. Plain
+/// global writes, so the real definitions replace them in any order.
+pub(super) const GAME_MENU_STAND_INS: &str = r#"
+ UpdateMicroButtons = UpdateMicroButtons or function() end
+ Disable_BagButtons = Disable_BagButtons or function() end
+ Enable_BagButtons = Enable_BagButtons or function() end
+"#;
+
/// What a kit owes stock `UIOptionsFrame.xml`: its `VARIABLES_LOADED` arm
/// (`UIOptionsFrame.lua:193-227`) calls six functions from files a kit may not load, and a kit
/// fires the event itself. Functions, so seating them at load is safe in any order.
diff --git a/crates/benilla-app/tests/common/mod.rs b/crates/benilla-app/tests/common/mod.rs
index 4cfa350b..bbb704ce 100644
--- a/crates/benilla-app/tests/common/mod.rs
+++ b/crates/benilla-app/tests/common/mod.rs
@@ -42,7 +42,10 @@ pub fn load_ui(script: &UiScript, entry: &str) {
.run(MICRO_BUTTON_STAND_INS)
.expect("the micro-button stand-ins");
}
- if leaf.eq_ignore_ascii_case("UIParent.xml") && entry.contains('\\') {
+ // The stock GameMenuFrame.xml sources UIParent.lua again, which drops the stand-ins' wrappers.
+ if (leaf.eq_ignore_ascii_case("UIParent.xml") || leaf.eq_ignore_ascii_case("GameMenuFrame.xml"))
+ && entry.contains('\\')
+ {
script
.run(UIPARENT_STAND_INS)
.expect("the UIParent stand-ins");
@@ -123,7 +126,7 @@ const UIPARENT_STAND_INS: &str = r#"
-- names are real, and all three options windows are the REFERENCE's own files,
-- loaded hidden — including `OptionsFrame`, the video window, which used to be our own
-- window's name. Ours is `BenillaOptionsFrame` now and is not in this list: it is not a name
- -- the reference indexes, and the wrappers in `GameMenuFrame.xml` are what tell these two
+ -- the reference indexes, and the wrappers in `GameMenuAdapters.xml` are what tell these two
-- functions about it. A KIT is a prefix of the manifest and may load none of the four, which
-- is what these stand-ins are for.
local function benilla_seat_options()