- a comment that justifies code by an engine difference is a claim with an expiry date, and the tree had ~96 expired ones across `assets/ui`, `ui_script/` and `benilla-ui` — a hygiene pass, no behaviour change: dangling `Benilla*` names with zero definitions (`BenillaMoney_*` at thirteen sites after 1937/1962 converged the coin cluster onto the stock `SmallMoneyFrameTemplate`; `BenillaGossipRow_Resize`, `BenillaErrorsFrame_AddMessage`, the digit feed 1285 deleted, `BenillaMerchantItem_OnClick` and three more), "the shipped `assets/ui/<X>.xml`" for thirty-odd files the directory no longer holds — it holds nine — each site now naming the chain file that actually loads, `UiPanels.xml` in the present tense after 1988 took the panel manager and the dialog engine stock, and expired self-deprecation: SimpleHTML "a widget kind we do not have" (1337 built it), "benilla has no console" (1948 bound `ConsoleExec`), `Model`'s "seven verbs not published yet" and "`ClearFog` is a guess" (all 23 are published, the carve sits 500 lines below, `UNBUILT` has been `[&str; 0]` since 2027), `frame_flag_gate`'s doc describing four groups of accepted mouse divergences over a `KNOWN` list that is empty (the empty array and its enforcement stay), five comments saying key delivery is not gated on `keyboard_enabled` where `script::keyboard`'s walk filters on exactly that field (1319), `SetRaidTarget` called a verb this house lacks beside an assert that its intent IS queued (1820), two questlog tests deferring to a screenshot for `GetPoint`/`IsVisible` the regions already answer, `benilla.toc`'s "trade still needs four engine verbs" (all four bound, 1966) and its ping `Model`s "this engine draws no pixels for" (2013 renders `MinimapPing.mdx` itself), and `rig_anim`'s `set_paused` naming a booth caller it does not have; and the code the prose was propping up goes with it: `GameMenuFrame.xml`'s six `<OnLoad>` label setters, because `text=` on a `<Button>` is a global-string lookup at the loader's attribute step (0995, built 61 seconds before the file was written) — the buttons carry `text="LOGOUT"`/`"EXIT_GAME"`/`"MACROS"`/`"RETURN_TO_GAME"`/`"MAIN_MENU"` as the reference's own do; `ScriptLogFrame`'s five authored close-button lines, now `inherits="UIPanelCloseButton"` off the chain's `UIPanelTemplates.xml` seated 757 lines above it (1846); and the editbox's 22-line "gap" doc claiming only the self-acquire `autoFocus` path is built and its `false` default unverified — path 1 is 1686's `visibility_focus`, and the default is `true`, byte-read off `flags = 1` at `0x779a29`/`0x779a2e`, so the test named `autofocus_does_not_focus_on_show…` is renamed for what it actually pins (2044) - the `text=` divergence never existed: `resolve_text`'s doc had claimed since 0068 that falling back to the raw attribute on a GlobalStrings miss was "one deliberate divergence" from a reference that draws the empty string, and read at the byte it is the reference's own behaviour — `FrameScript_GetText 0x703bf0` does return a pre-seeded empty string (`0x882748`) when the global is not a string, but that return never reaches a label, because all three `text=` readers image-wide test it and substitute the raw attribute: `Button::LoadXML` at `0x778c07` (`0x778c31 mov eax,esi`), `CSimpleFontString::LoadXML` at `0x771006` byte-identically, and `0x7292a6`, which pushes `edi` on the empty arm; so the loader changes nothing, the two test comments that had called `PetLevelText`'s "Level level race class" placeholder and `CLOSE` our divergence now name the reference's arm (a real 1.12 client draws them too), and what IS ours is the extra warning on a key-shaped miss (0983 → 0991), a strict addition; beside it, `GetEffectiveScale`'s root factor is 1 not because "benilla has no `uiScale` CVar" — 1804 registered one, default 0.9 — but because the dial is applied at the raster seam, `768/uiScale` UI units of virtual-screen height rather than a scale on `UIParent`, so every coordinate the VM hands Lua is already in those units and the reference's `GetCursorPosition()/GetEffectiveScale()` transcriptions convert correctly with a root of 1 (2044) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
507 lines
22 KiB
Rust
507 lines
22 KiB
Rust
//! The delete-item confirm popup driver (decision 0216 §3, UiPanels.xml's
|
|
//! `BenillaDeleteItemConfirmDriver`) — Lua wiring the Rust-side cursor tests can't reach: the
|
|
//! world-drop's `DELETE_ITEM_CONFIRM` showing the ref's `DELETE_ITEM` StaticPopup entry (decision
|
|
//! 0308 §3's engine) with the real `DELETE_ITEM`/`YES`/`NO` GlobalStrings, its Yes/No/ESC routing
|
|
//! to `DeleteCursorItem`/`ClearCursor`, and the entry's own `OnUpdate` auto-hide poll.
|
|
//!
|
|
//! Plus the `arg2 >= 3` fork the driver grew in 1743 (ref `UIParent.lua:344-352`): a RARE-or-better
|
|
//! payload raises `DELETE_GOOD_ITEM` instead, whose OKAY stays disabled until the player types
|
|
//! `DELETE_ITEM_CONFIRM_STRING` into its edit box. The two fixtures are real 1.12 rows (vmangos
|
|
//! `item_template`): **Tough Jerky** 117, quality 1 — the plain arm; **Flurry Axe** 871, quality 4 —
|
|
//! the typed arm.
|
|
|
|
use benilla_ui::script::{ContainerSlot, ContainerState, UiScript};
|
|
|
|
use super::test_ui::{bag_open, bag_slot_button, load_ui as load_xml, BAG_UI};
|
|
|
|
/// A one-item, one-slot backpack holding `item_id`/`name` at `quality`, so the confirm text and
|
|
/// the wire's destroy count are exercisable end to end. Quality is what forks the driver, so it is
|
|
/// the fixture's only real parameter.
|
|
fn one_item_backpack(item_id: u32, name: &str, quality: u32) -> 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: 5,
|
|
quality: Some(quality),
|
|
item_id,
|
|
link: Some(format!("|cffffffff|Hitem:{item_id}|h[{name}]|h|r")),
|
|
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 popup engine and its driver, and nothing else: `UiPanels.xml` owns both `StaticPopup*` and
|
|
/// `BenillaDeleteItemConfirmDriver`, and every test but the repaint one below only ever asks about
|
|
/// the dialog. It carried `MerchantFrame.xml` + `Cooldown.xml` + `BagFrame.xml` until 1751 for one
|
|
/// reason — the repaint test used to count `BenillaBagFrame_Update` — and that test now brings the
|
|
/// reference's own bag stack itself ([`bag_setup`]), so the whole tail went with it.
|
|
fn setup() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
|
|
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
|
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
|
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
|
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
|
|
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
|
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
|
// The stock DELETE_GOOD_ITEM's OnHide hands focus back to `ChatFrameEditBox` (1960).
|
|
load_xml(&s, r"Interface\FrameXML\UIMenu.xml");
|
|
load_xml(&s, r"Interface\FrameXML\ChatFrame.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrop
|
|
load_xml(&s, r"Interface\FrameXML\UIDropDownMenu.xml");
|
|
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
|
load_xml(&s, r"Interface\FrameXML\FloatingChatFrame.xml"); // declares ChatFrameEditBox
|
|
s.set_money(0);
|
|
s
|
|
}
|
|
|
|
/// [`setup`] plus the reference's bag stack — the harness for the one test that has to watch a bag
|
|
/// WINDOW repaint. The windows are `ContainerFrame1..12` off the player's chain now (1751), so
|
|
/// this needs client data and the caller opens with `wow_data_or_skip!`.
|
|
fn bag_setup() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
for file in BAG_UI {
|
|
load_xml(&s, file);
|
|
}
|
|
// `ContainerFrame_Update` reads `MerchantFrame:IsShown()` on any slot the tooltip owns.
|
|
// `BankFrame`'s purse is an ordinary `SmallMoneyFrameTemplate` off the chain's own
|
|
// `MoneyFrame.xml` (1937/1962), so it is not a merchant dependency.
|
|
load_xml(&s, "ScrollTemplates.xml"); // our scroll kit + the placeholder icon
|
|
load_xml(&s, "Interface\\FrameXML\\CharacterFrameTemplates.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
|
s.set_money(0);
|
|
s
|
|
}
|
|
|
|
/// Pick up the fixture item and click-carry it into the world — a completed LEFT CLICK (press +
|
|
/// release, both over nothing; 0218's byte-verified trigger) fires the world-drop
|
|
/// `DELETE_ITEM_CONFIRM(name, quality)` the driver listens for.
|
|
fn pick_up_and_drop_in_world(s: &mut UiScript) {
|
|
drop_in_world(s, 117, "Tough Jerky", 1);
|
|
}
|
|
|
|
/// The same world drop for an arbitrary fixture item — 1743's fork reads `arg2` (the quality), so
|
|
/// every test below picks the arm it wants by choosing the item.
|
|
fn drop_in_world(s: &mut UiScript, item_id: u32, name: &str, quality: u32) {
|
|
s.set_container(0, Some(one_item_backpack(item_id, name, quality)));
|
|
s.run("PickupContainerItem(0, 1)").unwrap();
|
|
assert!(s.cursor_item().is_some(), "fixture: the item is held");
|
|
// Off past every frame (the bag sits bottom-right; off-screen negative is always clear).
|
|
s.mouse_button(-50.0, -50.0, "LeftButton", true);
|
|
assert!(
|
|
s.mouse_button(-50.0, -50.0, "LeftButton", false),
|
|
"a world drop consumes the completed click"
|
|
);
|
|
s.tick(0.01); // flush the queued DELETE_ITEM_CONFIRM into the driver's OnEvent
|
|
}
|
|
|
|
/// The popup shows with the real 1.12 `DELETE_ITEM`/`YES`/`NO` GlobalStrings text, and Yes runs
|
|
/// `DeleteCursorItem()` — the item leaves the cursor and queues its wire destroy.
|
|
#[test]
|
|
fn delete_item_confirm_shows_the_real_strings_and_yes_deletes() {
|
|
let mut s = setup();
|
|
pick_up_and_drop_in_world(&mut s);
|
|
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"the confirm popup shows on DELETE_ITEM_CONFIRM"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1Text:GetText()")
|
|
.unwrap(),
|
|
"Do you want to destroy Tough Jerky?",
|
|
"the real GlobalStrings DELETE_ITEM text, formatted with the item name"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1Button1:GetText()")
|
|
.unwrap(),
|
|
"Yes"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1Button2:GetText()")
|
|
.unwrap(),
|
|
"No"
|
|
);
|
|
|
|
// Yes → StaticPopup_OnClick(dialog, 1) → the entry's OnAccept → DeleteCursorItem(): the
|
|
// payload clears and the destroy queues (count 0 = the fixture's whole 5-stack was picked up).
|
|
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"Yes hides the popup"
|
|
);
|
|
assert!(s.cursor_item().is_none(), "DeleteCursorItem cleared it");
|
|
assert_eq!(s.take_container_destroys(), vec![(0, 1, 0)]);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// No runs the entry's OnCancel (`ClearCursor()`) instead — the item is dropped, not destroyed (no
|
|
/// wire send) — and the bag REPAINTS off the clear's `ITEM_LOCK_CHANGED`, un-darkening the source
|
|
/// slot. The repaint is the 0218 stuck-darkening regression: the popup's No is a clear the bag
|
|
/// never clicked through, so only the event wiring (the container window's own ITEM_LOCK_CHANGED
|
|
/// registration) can reach it.
|
|
///
|
|
/// **What 1751 changed, and what it did not.** The repaint counted here used to be
|
|
/// `BenillaBagFrame_Update`; the window is the reference's `ContainerFrame` now, so the body is
|
|
/// its own `ContainerFrame_Update(frame)` (`ContainerFrame.lua:234`) off the player's chain. Two
|
|
/// consequences, both faithful: the window has to be OPEN (`ContainerFrame_OnEvent` gates the
|
|
/// repaint on `this:IsShown()` at l.39-42, where ours repainted a hidden window too), and the
|
|
/// darkening the repaint undoes is the reference's own `SetItemButtonDesaturated(button, locked)`
|
|
/// at l.245 — so the slot's greyscale flag is asserted directly here rather than only through the
|
|
/// repaint that clears it.
|
|
#[test]
|
|
fn delete_item_confirm_no_clears_without_destroying() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = bag_setup();
|
|
s.set_container(0, Some(one_item_backpack(117, "Tough Jerky", 1)));
|
|
s.run("ToggleBackpack()").unwrap();
|
|
assert!(bag_open(&s, 0), "the backpack window is up");
|
|
// Asked of the buttons' own GetID: `ContainerFrame_GenerateFrame` numbers a window's buttons
|
|
// BACKWARDS, so slot 1 is the LAST `…Item<j>`, and index arithmetic would pin a coincidence.
|
|
let slot1 = bag_slot_button(&s, 0, 1);
|
|
|
|
pick_up_and_drop_in_world(&mut s);
|
|
assert!(
|
|
desaturated(&mut s, &slot1),
|
|
"held on the cursor, the source slot draws dark (ref SetItemButtonDesaturated(_, locked))"
|
|
);
|
|
|
|
// Count repaints from here — the No-click path must trigger one via the event, not a click.
|
|
s.run(
|
|
"repaints = 0\n\
|
|
local real = ContainerFrame_Update\n\
|
|
ContainerFrame_Update = function(...) repaints = repaints + 1; return real(...) end",
|
|
)
|
|
.unwrap();
|
|
|
|
s.run("StaticPopup_OnClick(StaticPopup1, 2)").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"No hides the popup"
|
|
);
|
|
assert!(s.cursor_item().is_none(), "ClearCursor cleared it");
|
|
assert!(s.take_container_destroys().is_empty(), "No never destroys");
|
|
s.tick(0.01); // ITEM_LOCK_CHANGED fires from the pending queue at the next tick
|
|
assert!(
|
|
s.eval::<i64>("return repaints").unwrap() >= 1,
|
|
"the clear's ITEM_LOCK_CHANGED repaints the bag (the stuck-darkened slot, 0218)"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("local _, _, locked = GetContainerItemInfo(0, 1) return locked")
|
|
.unwrap(),
|
|
"the source slot reads unlocked again"
|
|
);
|
|
assert!(
|
|
!desaturated(&mut s, &slot1),
|
|
"…and the repaint un-darkened it — the 0218 stuck-dark slot"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// Is `button`'s icon drawn greyscale? The renderer-facing answer, off the quad stream, because
|
|
/// the engine publishes `Texture:SetDesaturated` with no getter to ask — and a *drawn* grey is
|
|
/// what "the slot is stuck dark" means to the player anyway.
|
|
fn desaturated(s: &mut UiScript, button: &str) -> bool {
|
|
s.resolve();
|
|
s.extract()
|
|
.into_iter()
|
|
.filter(|q| s.quad_owner_name(q.target).as_deref() == Some(button))
|
|
.any(|q| match &q.content {
|
|
benilla_ui::script::QuadContent::Texture {
|
|
path: Some(p),
|
|
desaturated,
|
|
..
|
|
} => p.contains("INV_Misc_Food_16") && *desaturated,
|
|
_ => false,
|
|
})
|
|
}
|
|
|
|
/// ESC routes through the ref's `StaticPopup_EscapePressed` — DELETE_ITEM is `hideOnEscape`, so
|
|
/// its OnCancel runs with reason "clicked" exactly as a real No-click would. (`ToggleGameMenu`'s
|
|
/// existing unconditional `ClearCursor()` already empties the cursor before the popup branch, so
|
|
/// this mainly proves the popup itself closes.)
|
|
#[test]
|
|
fn escape_closes_the_delete_confirm_popup() {
|
|
let mut s = setup();
|
|
pick_up_and_drop_in_world(&mut s);
|
|
|
|
s.run("ToggleGameMenu()").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"ESC closes the confirm popup"
|
|
);
|
|
assert!(s.cursor_item().is_none());
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The reference's DELETE_ITEM `OnUpdate` auto-hide poll (StaticPopup.lua:669-673), now VERBATIM
|
|
/// as the registry entry's own OnUpdate: if the cursor empties by any OTHER path (not a popup
|
|
/// button — say, a same-slot click cancelling a fresh pickup) while the confirm is still showing,
|
|
/// the popup auto-hides on the next tick. The poll lives inside the DELETE_ITEM entry, so a
|
|
/// DIFFERENT visible dialog is untouched by cursor traffic.
|
|
#[test]
|
|
fn the_delete_entry_polls_itself_hidden_and_other_dialogs_are_untouched() {
|
|
let mut s = setup();
|
|
pick_up_and_drop_in_world(&mut s);
|
|
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
|
|
|
// The cursor empties via ClearCursor() directly (not a popup button) — the entry's own
|
|
// OnUpdate poll hides the still-showing confirm on the next tick.
|
|
s.run("ClearCursor()").unwrap();
|
|
s.tick(0.01);
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"the DELETE_ITEM OnUpdate poll auto-hides once the cursor is empty"
|
|
);
|
|
|
|
// An unrelated registered dialog must NOT be auto-hidden by cursor traffic — only the
|
|
// DELETE_ITEM entry polls, and its OnUpdate only runs while DELETE_ITEM is the shown `which`.
|
|
s.run(
|
|
r#"StaticPopupDialogs["TEST_UNRELATED"] = { text = "unrelated?", button1 = "Yes",
|
|
button2 = "No", timeout = 0, whileDead = 1 }
|
|
StaticPopup_Show("TEST_UNRELATED")"#,
|
|
)
|
|
.unwrap();
|
|
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
|
s.fire_event("CURSOR_UPDATE", vec![]);
|
|
s.tick(0.01);
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"an unrelated dialog is never touched by the delete-confirm poll"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The RARE-or-better fork (ref `UIParent.lua:346-350`, `arg2 >= 3`): a quality-4 payload raises
|
|
/// `DELETE_GOOD_ITEM`, not `DELETE_ITEM` — with the second GlobalString, the edit box up and
|
|
/// focused, and OKAY **disabled** until the confirm word is typed.
|
|
#[test]
|
|
fn a_rare_payload_raises_the_typed_confirm_with_okay_disabled() {
|
|
let mut s = setup();
|
|
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
|
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1.which").unwrap(),
|
|
"DELETE_GOOD_ITEM",
|
|
"quality 4 forks to the typed-confirmation variant"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1Text:GetText()")
|
|
.unwrap(),
|
|
"Do you want to destroy Flurry Axe?\n\nType \"DELETE\" into the field to confirm.",
|
|
"the real GlobalStrings DELETE_GOOD_ITEM text, formatted with the item name"
|
|
);
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1EditBox:IsShown()")
|
|
.unwrap(),
|
|
"hasEditBox raises the narrow box"
|
|
);
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
|
|
.unwrap(),
|
|
"the entry's OnShow focuses the box, so the player can type straight away"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<i64>("return StaticPopup1Button1:IsEnabled()")
|
|
.unwrap(),
|
|
0,
|
|
"OKAY starts disabled — nothing has been typed yet"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The typed gate itself (ref `EditBoxOnTextChanged`, `StaticPopup.lua:718-723`): only the exact
|
|
/// `DELETE_ITEM_CONFIRM_STRING` enables OKAY, it is compared through `strupper` so lower case
|
|
/// passes, and backing away from the word disables it again. Then OKAY destroys.
|
|
#[test]
|
|
fn typing_the_confirm_word_enables_okay_and_untyping_it_disables_again() {
|
|
let mut s = setup();
|
|
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
|
|
|
// OKAY is enabled by the box's `OnTextChanged`, and that fire is deferred to the drain
|
|
// (decision 1831) — so what this answers is the button state the LAST DRAINED text produced,
|
|
// which is the state a player ever sees. Ticking here rather than after each write keeps the
|
|
// test reading as the sequence of edits it is about.
|
|
let enabled = |s: &mut UiScript| {
|
|
s.tick(0.0);
|
|
s.eval::<i64>("return StaticPopup1Button1:IsEnabled()")
|
|
.unwrap()
|
|
== 1
|
|
};
|
|
|
|
s.run(r#"StaticPopup1EditBox:SetText("DELET")"#).unwrap();
|
|
assert!(!enabled(&mut s), "a prefix of the word is not the word");
|
|
|
|
// Typed character by character through the engine's own input path, lower case: the ref
|
|
// compares through strupper, so this arms OKAY exactly as shouting it would.
|
|
s.run(r#"StaticPopup1EditBox:SetText("")"#).unwrap();
|
|
for c in ["d", "e", "l", "e", "t", "e"] {
|
|
assert!(s.char_input(c), "the focused box takes the keystroke");
|
|
}
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1EditBox:GetText()")
|
|
.unwrap(),
|
|
"delete"
|
|
);
|
|
assert!(
|
|
enabled(&mut s),
|
|
"the ref compares through strupper, so lower case passes"
|
|
);
|
|
|
|
s.run(r#"StaticPopup1EditBox:SetText("deletex")"#).unwrap();
|
|
assert!(!enabled(&mut s), "one char past the word disables again");
|
|
|
|
s.run(r#"StaticPopup1EditBox:SetText("DELETE")"#).unwrap();
|
|
assert!(enabled(&mut s));
|
|
|
|
// OKAY now does what the plain arm's does: DeleteCursorItem().
|
|
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"OKAY hides the popup"
|
|
);
|
|
assert!(s.cursor_item().is_none(), "DeleteCursorItem cleared it");
|
|
assert_eq!(s.take_container_destroys(), vec![(0, 1, 0)]);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// Enter in the box is the ref's `EditBoxOnEnterPressed` (`StaticPopup.lua:712-717`): it destroys
|
|
/// only while OKAY is enabled, so a reflexive Enter over a half-typed word does nothing at all.
|
|
#[test]
|
|
fn enter_in_the_box_destroys_only_once_okay_is_enabled() {
|
|
let mut s = setup();
|
|
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
|
|
|
s.run(r#"StaticPopup1EditBox:SetText("DEL")"#).unwrap();
|
|
assert!(s.key_input("ENTER"), "the focused box consumes ENTER");
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"Enter with OKAY disabled is inert — the dialog stays up"
|
|
);
|
|
assert!(s.cursor_item().is_some(), "and the item is still held");
|
|
assert!(s.take_container_destroys().is_empty());
|
|
|
|
s.run(r#"StaticPopup1EditBox:SetText("DELETE")"#).unwrap();
|
|
s.tick(0.0); // the write only marks the box — the drain is what enables OKAY (1831)
|
|
assert!(s.key_input("ENTER"));
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"Enter with OKAY enabled destroys and hides"
|
|
);
|
|
assert!(s.cursor_item().is_none());
|
|
assert_eq!(s.take_container_destroys(), vec![(0, 1, 0)]);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// NO on the typed variant is the plain one's cancel — `ClearCursor()`, no wire send — and the
|
|
/// entry's `OnHide` empties the box so the next raise opens blank (the ref's own reason: a word
|
|
/// left over from last time would be armed before the player read the dialog).
|
|
#[test]
|
|
fn no_on_the_typed_confirm_clears_and_leaves_the_box_empty_for_next_time() {
|
|
let mut s = setup();
|
|
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
|
s.run(r#"StaticPopup1EditBox:SetText("DELETE")"#).unwrap();
|
|
|
|
s.run("StaticPopup_OnClick(StaticPopup1, 2)").unwrap();
|
|
assert!(!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
|
|
assert!(s.cursor_item().is_none(), "ClearCursor cleared it");
|
|
assert!(s.take_container_destroys().is_empty(), "No never destroys");
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1EditBox:GetText()")
|
|
.unwrap(),
|
|
"",
|
|
"OnHide empties the box"
|
|
);
|
|
|
|
// Raise it again: blank box, OKAY disabled — the armed state did not survive.
|
|
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1EditBox:GetText()")
|
|
.unwrap(),
|
|
""
|
|
);
|
|
assert_eq!(
|
|
s.eval::<i64>("return StaticPopup1Button1:IsEnabled()")
|
|
.unwrap(),
|
|
0,
|
|
"and OKAY is disabled again"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// ESC inside the typed confirm's focused box is SWALLOWED, the reference's own behaviour (1960):
|
|
/// the stock `DELETE_GOOD_ITEM` entry defines no `EditBoxOnEscapePressed`, so the box's
|
|
/// `StaticPopup_EditBoxOnEscapePressed` finds nothing to call and the dialog stays up with the
|
|
/// item still on the cursor — the player answers with No. (ADD_FRIEND and its kin hide on ESC
|
|
/// because their entries say so; this one does not.) Our engine used to fall through to a
|
|
/// cancel here, which the earlier form of this test pinned.
|
|
#[test]
|
|
fn escape_in_the_typed_confirms_box_is_swallowed_as_the_reference_leaves_it() {
|
|
let mut s = setup();
|
|
drop_in_world(&mut s, 871, "Flurry Axe", 4);
|
|
assert!(s
|
|
.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
|
|
.unwrap());
|
|
|
|
assert!(s.key_input("ESCAPE"), "the focused box consumes ESCAPE");
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
|
"the dialog stays: the entry names no escape handler"
|
|
);
|
|
assert!(
|
|
s.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
|
|
.unwrap(),
|
|
"and the box keeps focus"
|
|
);
|
|
assert!(s.cursor_item().is_some(), "the item is still on the cursor");
|
|
assert!(s.take_container_destroys().is_empty(), "ESC never destroys");
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|
|
|
|
/// The plain arm keeps its own edit box HIDDEN — the fork is not cosmetic, and a common-quality
|
|
/// destroy must not grow a field to type into. The control for every test above.
|
|
#[test]
|
|
fn the_plain_arm_shows_no_edit_box() {
|
|
let mut s = setup();
|
|
pick_up_and_drop_in_world(&mut s); // Tough Jerky, quality 1
|
|
|
|
assert_eq!(
|
|
s.eval::<String>("return StaticPopup1.which").unwrap(),
|
|
"DELETE_ITEM"
|
|
);
|
|
assert!(
|
|
!s.eval::<bool>("return StaticPopup1EditBox:IsShown()")
|
|
.unwrap(),
|
|
"no hasEditBox on the plain entry"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<i64>("return StaticPopup1Button1:IsEnabled()")
|
|
.unwrap(),
|
|
1,
|
|
"and YES is live immediately"
|
|
);
|
|
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
|
}
|