- `UnitHasRelicSlot` is real in 1.12 and benilla had no binding for it: `0x519e50` reads `ChrClasses.dbc` field 16 — set for Paladin, Shaman and Druid in `patch.MPQ`'s 17-column copy (the base `dbc.MPQ` copy has 16 columns and no such field, which is how the "always false in vanilla" belief survived twice in decisions and seven times in code) — and stock `PaperDollFrame.lua` calls it unconditionally, so the character sheet would have raised for EVERY class; a druid, paladin or shaman's sheet now hides the ammo slot, shows the relic plate where the bow was, tooltips "Relic", and reports ranged Attack/Damage/Power as N/A, all four branches transcribed from the reference - the loot window is the reference's own `LootFrame.xml`: `LootButton` is built as a real widget type rather than aliased (a Button plus one slot dword and one overridden OnClick — right-click loots exactly like left, no shift/ctrl/alt takes, a row whose Lua handler errored still loots, a fast double-click takes two slots), `LOOT_SLOT_CLEARED` fires per row again because the stock file hangs hide-in-place AND the emptied-page auto-advance off it, `EditBox:Insert(nil)` is a no-op as `lua_tostring` makes it, and an alias `GameTooltip.xml` had quietly become a reader of would have turned every item tooltip white, silently, had it just gone nil - the vendor cursor: `PickupMerchantItem` is two verbs behind one name, decided by what the cursor holds and never by the caller — a held bag item SELLS (index ignored, no merchant-open gate), otherwise the vendor row is grabbed as cursor mode 5, which is invisible to `CursorHasItem` (modes 1 and 9 only) so all thirteen stock gates stay closed while it is held; the drop is the buy, `CMSG_BUY_ITEM_IN_SLOT`, count 1, into a bag or an equipment slot, and every refusal is silent and clears the cursor; the four `Model_*` rotation handlers land in UIParent where the reference keeps them - the gap report reads `:Method(` sites now — it found `SetMerchantCompareItem`, which would have raised on the first vendor-row hover with every check green, and `LootButton:SetSlot`, and 28 "free" windows became 25; the shared coin kit moved out of the merchant window into `MoneyFrame.xml` so the eight windows that draw coins through it can reach it without loading a vendor Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
507 lines
21 KiB
Rust
507 lines
21 KiB
Rust
//! **The ESCAPE ladder** — `ToggleGameMenu`'s `elseif` chain (`UIParent.lua:1465-1497`) and the
|
|
//! host-glue precedence around it (a focused EditBox eats the key before the binding ever runs).
|
|
//!
|
|
//! The window these tests park in the ladder's way is a BAG, and since decision 1751 that is the
|
|
//! reference's own `ContainerFrame1..12`, executed off the player's installed patch chain — not
|
|
//! `BenillaBagFrame`, which no longer exists. Two consequences run through the file:
|
|
//!
|
|
//! * a bag test loads [`BAG_UI`] and opens with `wow_data_or_skip!`, because a chain entry needs
|
|
//! client data; and
|
|
//! * "is the bag open?" is asked of [`bag_open`] (the reference's own `IsBagOpen` scan), never of
|
|
//! a frame by name — the twelve windows are RECYCLED, so which one a bag lands in is a
|
|
//! coincidence and not a property.
|
|
//!
|
|
//! The ladder itself is unchanged by that swap: `CloseAllWindows` (ours, `UiPanels.xml`) sweeps
|
|
//! `ContainerFrame1..NUM_CONTAINER_FRAMES` where it used to sweep our five named windows.
|
|
|
|
use benilla_ui::script::{ContainerSlot, ContainerState, LootRow, LootState, UiScript};
|
|
|
|
use super::test_ui::{bag_open, bag_slot_button, click, load_ui as load_xml, BAG_UI};
|
|
|
|
/// A one-item backpack (slot 1 holds a resolved item, so it is pickable onto the cursor).
|
|
fn one_item_backpack() -> ContainerState {
|
|
let mut slots = std::collections::HashMap::new();
|
|
slots.insert(
|
|
1,
|
|
ContainerSlot {
|
|
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,
|
|
}
|
|
}
|
|
|
|
/// Load every window and drive the whole ESC-close path (UIParent.lua ToggleGameMenu → l.1491
|
|
/// CloseAllWindows): with the bag open, the loot window on
|
|
/// the left slot, and an item on the cursor, ESC closes the bag AND the panel slot, releases the
|
|
/// loot (OnHide → CloseLoot), and drops the held cursor. Also asserts the host-glue precedence: no
|
|
/// EditBox focused ⇒ `key_input("ESCAPE")` does not consume, so the app runs the binding.
|
|
///
|
|
/// The bag half is the reference's window now (1751), so what `CloseAllWindows` must sweep is a
|
|
/// `ContainerFrame` — the same assertion, asked through `IsBagOpen` instead of a frame name.
|
|
#[test]
|
|
fn escape_closes_bag_and_panel_releases_loot_and_clears_cursor() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for file in BAG_UI {
|
|
load_xml(&s, file);
|
|
}
|
|
// The loot window is the reference's own since 1751, and its dependency list grew with the
|
|
// swap — `LOOT_UI` carries the why for each entry. Loaded whole rather than cherry-picked:
|
|
// `PartyFrame.xml` is needed at LOAD (MAX_PARTY_MEMBERS) and skipping it fails loudly, but
|
|
// `ItemButtonTemplate` is only a warning and skipping it fails silently.
|
|
for file in super::test_ui::LOOT_UI {
|
|
load_xml(&s, file);
|
|
}
|
|
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
|
|
load_xml(&s, "MerchantFrame.xml"); // BenillaMoney_Set, BankFrame's purse helper
|
|
s.set_money(0);
|
|
s.set_container(0, Some(one_item_backpack()));
|
|
|
|
// Open the bag and the loot window; drain the open kits (not under test here).
|
|
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![]);
|
|
let _ = s.take_sounds();
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
|
|
// Pick up slot 1 onto the cursor.
|
|
s.run("PickupContainerItem(0, 1)").unwrap();
|
|
assert!(s.cursor_item().is_some(), "cursor holds the picked item");
|
|
assert!(bag_open(&s, 0), "bag is open before ESC");
|
|
assert!(
|
|
s.eval::<bool>("return GetLeftFrame():GetName() == \"LootFrame\"")
|
|
.unwrap(),
|
|
"loot holds the left panel slot before ESC"
|
|
);
|
|
|
|
// Precedence: no EditBox focused ⇒ ESCAPE is NOT consumed, so the host runs the binding.
|
|
assert!(
|
|
!s.key_input("ESCAPE"),
|
|
"no EditBox focused ⇒ ESC is not consumed by the box layer"
|
|
);
|
|
|
|
// The escape binding (what the host runs on the unconsumed ESC).
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
|
|
assert!(!bag_open(&s, 0), "ESC closed the bag");
|
|
assert!(
|
|
s.eval::<bool>("return GetLeftFrame() == nil").unwrap(),
|
|
"ESC vacated the panel slot"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("return LootFrame:IsVisible()").unwrap(),
|
|
"ESC hid the loot window"
|
|
);
|
|
assert!(
|
|
s.take_loot_close(),
|
|
"closing the loot fired the release (OnHide → CloseLoot)"
|
|
);
|
|
assert!(s.cursor_item().is_none(), "ESC dropped the held cursor");
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The other precedence side: a focused EditBox consumes ESCAPE (`key_input` returns true), so the
|
|
/// host's `!consumed` gate never runs the escape binding while typing — the bag stays open. Drives
|
|
/// the shipped chat box (the app's ENTER-to-open path is `focus_editbox`); its own OnEscapePressed
|
|
/// clears the focus (chat_tests covers the box's submit/close contract itself).
|
|
#[test]
|
|
fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for file in BAG_UI {
|
|
load_xml(&s, file);
|
|
}
|
|
load_xml(&s, "MerchantFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
|
load_xml(&s, "ChatFrame.xml");
|
|
s.set_money(0);
|
|
s.set_container(0, Some(one_item_backpack()));
|
|
|
|
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
|
assert!(
|
|
s.focus_editbox("ChatFrameEditBox"),
|
|
"the chat edit box focuses"
|
|
);
|
|
assert!(s.has_keyboard_focus());
|
|
|
|
// ESCAPE is consumed by the focused box — the host would NOT run the escape binding.
|
|
assert!(s.key_input("ESCAPE"), "a focused EditBox consumes ESCAPE");
|
|
// The box's OnEscapePressed cleared its own focus, but the bag window is untouched.
|
|
assert!(!s.has_keyboard_focus(), "the box cleared its focus");
|
|
assert!(
|
|
bag_open(&s, 0),
|
|
"the bag stays open (the escape binding never ran)"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The options rung (decision 0950; the ref's own `elseif OptionsFrame:IsVisible()` at
|
|
/// UIParent.lua l.1483-1484, BETWEEN the popup rung and the menu rung): ESC with the options
|
|
/// window up closes it — hide, not cancel-click: benilla applies changes live — and eats the
|
|
/// press, so the menu does NOT open on the same stroke. Only the NEXT press, with nothing left
|
|
/// to eat, opens the menu (one eater per press, the 0449 law).
|
|
#[test]
|
|
fn escape_closes_the_options_window_before_opening_the_menu() {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Fonts.xml");
|
|
load_xml(&s, "MoneyFrame.xml");
|
|
load_xml(&s, "UiPanels.xml");
|
|
load_xml(&s, "GameTooltip.xml");
|
|
load_xml(&s, "UIDropDownMenu.xml");
|
|
load_xml(&s, "ScrollTemplates.xml"); // the Keybindings page's faux-scroll kit (1008)
|
|
load_xml(&s, "KeyBindingsPage.xml");
|
|
load_xml(&s, "OptionsFrame.xml");
|
|
load_xml(&s, "GameMenuFrame.xml");
|
|
|
|
s.run("ShowUIPanel(OptionsFrame)").unwrap();
|
|
assert!(s.eval::<bool>("return OptionsFrame:IsVisible()").unwrap());
|
|
assert!(
|
|
!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
|
|
"the menu is down — the options rung is what must eat this press"
|
|
);
|
|
|
|
// Press 1: the options rung eats it — the window closes and the menu stays down.
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return OptionsFrame:IsVisible()").unwrap(),
|
|
"ESC closed the options window"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
|
|
"…and did NOT also open the menu — one eater per press"
|
|
);
|
|
|
|
// Press 2: nothing left to eat — the menu opens.
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
|
|
"the next press reaches the ladder's open-the-menu tail"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// ESC closes an open stack-split spinner (decision 0216 §6/slice 2, StackSplit.xml): this engine
|
|
/// has no plain-frame keyboard capture (the real client's own `StackSplitFrame` OnKeyDown ESCAPE
|
|
/// arm can't be driven), so the hook rides `ToggleGameMenu`'s shared chain instead — checked
|
|
/// right after the confirm popup and before the world map, both similarly transient overlays.
|
|
///
|
|
/// The spinner is opened by the REFERENCE's own shift fork now (1751,
|
|
/// `ContainerFrame.lua:567-577`), reached by a real SHIFT + left-click on a `ContainerFrame` item
|
|
/// button — which is why `ChatFrame.xml` is loaded: that fork's first test is
|
|
/// `ChatFrameEditBox:IsShown()` (a shift-click links the item into an open chat box instead of
|
|
/// splitting it), and the reference indexes that global unguarded.
|
|
#[test]
|
|
fn escape_closes_an_open_stack_split_frame() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for file in BAG_UI {
|
|
load_xml(&s, file);
|
|
}
|
|
load_xml(&s, "MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
|
|
load_xml(&s, "ChatFrame.xml"); // …and ChatFrameEditBox, the shift fork's first test
|
|
load_xml(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
|
s.set_money(0);
|
|
|
|
let mut slots = std::collections::HashMap::new();
|
|
slots.insert(
|
|
1,
|
|
ContainerSlot {
|
|
petition: None,
|
|
already_bound: false,
|
|
bar_placeable: true,
|
|
durability: None,
|
|
texture: Some("Interface\\Icons\\INV_Misc_Food_16".into()),
|
|
count: 5,
|
|
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(),
|
|
},
|
|
);
|
|
s.set_container(
|
|
0,
|
|
Some(ContainerState {
|
|
name: Some("Backpack".into()),
|
|
num_slots: 16,
|
|
slots,
|
|
}),
|
|
);
|
|
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
|
assert!(bag_open(&s, 0), "the backpack window is up");
|
|
|
|
// Open the spinner with a real SHIFT + left-click on the button holding slot 1 — asked of the
|
|
// buttons' own `GetID()`, never by index (`ContainerFrame_GenerateFrame` numbers them
|
|
// backwards). The click goes through the engine because the reference's handler reads `this`.
|
|
let slot1 = bag_slot_button(&s, 0, 1);
|
|
s.set_modifiers(true, false, false);
|
|
click(&mut s, &slot1, "LeftButton");
|
|
s.set_modifiers(false, false, false);
|
|
assert!(s.eval::<bool>("return StackSplitFrame:IsShown()").unwrap());
|
|
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return StackSplitFrame:IsShown()").unwrap(),
|
|
"ESC closed the split frame"
|
|
);
|
|
// ESC's unconditional cursor-clear (ToggleGameMenu's own first line) ran too — nothing was
|
|
// held anyway (the split branch already released it), so this just confirms no error.
|
|
assert!(s.cursor_item().is_none());
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The ESC ladder end-to-end (the ref's `ToggleGameMenu` order, `UIParent.lua:1482-1496`, one
|
|
/// eater per press — the director's two-press report, decision 0449): mid-cast with a menu
|
|
/// open, a bag open and a target, the press closes the MENU first (`CloseMenus`, `l.1488` —
|
|
/// the cast survives), the next only cancels the cast (`SpellStopCasting`, `l.1489`), the next
|
|
/// only closes the windows (`CloseAllWindows`, `l.1491`), and only then — nothing left to
|
|
/// eat — does one drop the target (`ClearTarget`, `l.1492`). The 1/nil returns are load-bearing
|
|
/// at every rung (an unconditional true anywhere would wedge the ladder; the artifact's
|
|
/// `elseif` chain is the proof each real binding is conditional).
|
|
#[test]
|
|
fn escape_ladder_cast_then_windows_then_target_one_eater_per_press() {
|
|
use benilla_ui::script::UnitState;
|
|
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for file in BAG_UI {
|
|
load_xml(&s, file);
|
|
}
|
|
// GameTooltip.xml (BAG_UI's, for the bag slots' tooltips) also carries TOOLTIP_DEFAULT_COLOR,
|
|
// which the dropdown backdrop's OnLoad reads — so the kit can load straight after it.
|
|
load_xml(&s, "UIDropDownMenu.xml");
|
|
load_xml(&s, "MerchantFrame.xml");
|
|
s.set_money(0);
|
|
s.set_container(0, Some(one_item_backpack()));
|
|
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
|
let _ = s.take_sounds();
|
|
assert!(bag_open(&s, 0));
|
|
s.set_unit(
|
|
"target",
|
|
Some(UnitState {
|
|
exists: true,
|
|
..Default::default()
|
|
}),
|
|
);
|
|
|
|
// Press 0 — mid-cast with a dropdown open (the ref's CloseMenus rung, l.1488): the menu
|
|
// closes and eats the press — the cast, the windows, and the target all survive.
|
|
s.set_casting(true);
|
|
s.run("DropDownList1:Show()").unwrap();
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return DropDownList1:IsShown()").unwrap(),
|
|
"ESC closed the open dropdown menu"
|
|
);
|
|
assert!(
|
|
!s.take_spell_stop(),
|
|
"the menu press must NOT reach SpellStopCasting — the cast runs on (ref order: \
|
|
CloseMenus sits before the cast rung)"
|
|
);
|
|
assert!(
|
|
bag_open(&s, 0),
|
|
"the menu press must not close windows either"
|
|
);
|
|
assert!(!s.take_target_clear());
|
|
|
|
// Press 1 — mid-cast (the app's per-frame IsCasting mirror): the cast dies, NOTHING else.
|
|
s.set_casting(true);
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
bag_open(&s, 0),
|
|
"ESC mid-cast is eaten by SpellStopCasting — the bag stays open"
|
|
);
|
|
assert!(
|
|
s.take_spell_stop(),
|
|
"the stop request queued for the app's local cancel"
|
|
);
|
|
assert!(
|
|
!s.take_target_clear(),
|
|
"the same press must NOT also drop the target (the raw-key double-fire 0449 retires)"
|
|
);
|
|
|
|
// Press 2 — the cancel resolved (next frame's mirror push): the windows close, target stays.
|
|
s.set_casting(false);
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
!bag_open(&s, 0),
|
|
"idle ESC falls through SpellStopCasting's nil to CloseAllWindows"
|
|
);
|
|
assert!(!s.take_spell_stop(), "no stray stop request when idle");
|
|
assert!(
|
|
!s.take_target_clear(),
|
|
"a press CloseAllWindows ate must not reach ClearTarget"
|
|
);
|
|
|
|
// Press 3 — nothing left to eat: the target drops.
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
s.take_target_clear(),
|
|
"the bare press reaches ClearTarget — the ladder's last rung"
|
|
);
|
|
|
|
// Press 4 — no target either: the chain runs out. In a full UI this press opens the game menu
|
|
// (decision 0674, `game_menu_tests`); this harness deliberately loads no GameMenuFrame.xml, so
|
|
// what it pins is the rung BELOW it — ClearTarget answering nil rather than eating the press.
|
|
s.set_unit("target", None);
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
!s.take_target_clear(),
|
|
"ClearTarget answers nil with no target — nothing queued"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The SpellStopTargeting rung (`UIParent.lua:1490`, decision 0792): ESC with the ground-target
|
|
/// cursor up cancels the targeting and ONLY the targeting — after the cast rung (the artifact's
|
|
/// order), before the window close. The 1/nil returns are load-bearing exactly like the cast
|
|
/// rung's: an idle press must fall straight through both.
|
|
#[test]
|
|
fn escape_ladder_targeting_rung_after_cast_before_windows() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for file in BAG_UI {
|
|
load_xml(&s, file);
|
|
}
|
|
load_xml(&s, "MerchantFrame.xml");
|
|
s.set_money(0);
|
|
s.set_container(0, Some(one_item_backpack()));
|
|
s.run("MainMenuBarBackpackButton:Click()").unwrap();
|
|
let _ = s.take_sounds();
|
|
assert!(bag_open(&s, 0));
|
|
|
|
// SpellIsTargeting() mirrors the pushed state — the PetFrame-family readers' predicate.
|
|
assert!(!s.eval::<bool>("return SpellIsTargeting()").unwrap_or(false));
|
|
s.set_spell_targeting(true);
|
|
assert!(s.eval::<bool>("return SpellIsTargeting()").unwrap());
|
|
|
|
// Press 1 — targeting, no cast in flight: SpellStopTargeting eats the press; the bag stays.
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
s.take_stop_targeting(),
|
|
"the stop-targeting request queued for the app's cancel"
|
|
);
|
|
assert!(
|
|
!s.take_spell_stop(),
|
|
"the cast rung answered nil — nothing casting"
|
|
);
|
|
assert!(
|
|
bag_open(&s, 0),
|
|
"ESC while targeting must NOT also close windows"
|
|
);
|
|
|
|
// Press 2 — BOTH casting and targeting pushed (unreachable app state, but the chain's order
|
|
// is the artifact's law): the cast rung sits first (l.1489 before l.1490) and eats alone.
|
|
s.set_casting(true);
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(s.take_spell_stop(), "the cast rung eats first");
|
|
assert!(
|
|
!s.take_stop_targeting(),
|
|
"the same press must not also cancel the targeting"
|
|
);
|
|
|
|
// Press 3 — idle (the app cleared the mode and the cast resolved): both rungs answer nil
|
|
// and the press falls through to CloseAllWindows.
|
|
s.set_casting(false);
|
|
s.set_spell_targeting(false);
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(!s.take_stop_targeting(), "no stray trigger when idle");
|
|
assert!(
|
|
!bag_open(&s, 0),
|
|
"the idle press falls through both spell rungs to CloseAllWindows"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// **`UISpecialFrames` — an addon's own ESC list** (decision 1206).
|
|
///
|
|
/// `tinsert(UISpecialFrames, "MyFrame")` is *the* 1.12 idiom for "ESC should close my window", and
|
|
/// 34 corpus call sites use it. With the table absent that line was
|
|
/// `bad argument #1 to 'tinsert' (table expected, got nil)` — 11 addons dead at load, the
|
|
/// second-largest wall in the survey.
|
|
///
|
|
/// The walk is the reference's own (`UIParent.lua` l.947-954, inside `CloseWindows`): a plain
|
|
/// `Hide`, never `HideUIPanel`, because these frames hold no panel slot.
|
|
#[test]
|
|
fn an_addon_frame_registered_in_uispecialframes_closes_on_escape() {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Fonts.xml");
|
|
load_xml(&s, "MoneyFrame.xml");
|
|
load_xml(&s, "UiPanels.xml");
|
|
load_xml(&s, "GameMenuFrame.xml");
|
|
|
|
// The addon's three lines, verbatim in shape.
|
|
s.run(
|
|
r#"
|
|
MyAddonWindow = CreateFrame("Frame", "MyAddonWindow", UIParent)
|
|
MyAddonWindow:Show()
|
|
tinsert(UISpecialFrames, "MyAddonWindow")
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
assert!(s.eval::<bool>("return MyAddonWindow:IsVisible()").unwrap());
|
|
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return MyAddonWindow:IsVisible()").unwrap(),
|
|
"ESC closed the addon's window"
|
|
);
|
|
// ...and it ATE the press: the menu must not also open, or ESC would close the window and
|
|
// raise the menu in one keystroke.
|
|
assert!(
|
|
!s.eval::<bool>("return GameMenuFrame:IsVisible()").unwrap(),
|
|
"CloseAllWindows returning truthy is what stops the chain (UIParent.lua l.1491)"
|
|
);
|
|
|
|
// A name in the table that resolves to nothing is skipped, not an error — the reference's own
|
|
// `if (frame and frame:IsVisible())`.
|
|
s.run(r#"tinsert(UISpecialFrames, "NoSuchFrameAnywhere") ToggleGameMenu()"#)
|
|
.unwrap();
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|