Files
benilla-octo/crates/benilla-app/src/ui_script/static_popup_tests.rs
T
gromandClaude Opus 5 04a3f68009 Nine reds where the realm changed the data under a transcribed assertion
None of these were defects; each is a value, a row index or a semantic the realm moved and the
test still asserted stock 1.12's:

- `RAID_CLASS_COLORS.SHAMAN` is the later blue `|cff0070de` (`Fonts.xml:50`), not a copy of
  PALADIN's pink — 1.12 reused the row and the realm did not.
- `ChatTypeInfo` has 106 keys, the extra being the realm's `HARDCORE`, and ten of them are sticky
  rather than five: it marks `CHANNEL`, `OFFICER`, `RAID_WARNING` and `WHISPER` too. Sticky is what
  the edit box keeps between sends, so that list is typing behaviour a player feels.
- `ChatMenu`'s Macro row is eleventh: `HARDCORE_MESSAGE` went in fourth (`ChatFrame.lua:2371`).
- A friend row is coloured three ways (`FriendsFrame.lua:186-197`) — name and class from
  `RAID_CLASS_COLORS[TW_CLASS_TOKEN[class]]`, level from `GetDifficultyColor`. `TW_CLASS_TOKEN`
  lives in the realm's `Globals.lua`, so that row joins `SOCIAL_UI`; without it the window draws
  uncoloured, which is a different window than the client paints.
- A guild row goes offline by **alpha 0.55** on all four columns (`FriendsFrame.lua:526`), keeping
  its class and difficulty inks, where 1.12 wrote a flat grey.
- The minimap clock plate is four lines — `Clock`, then a Zone/Server/Local time triple
  (`GameTime.lua:50-56`) — not one bare time.
- `ERR_TAXINOPATHS` uses a straight ASCII apostrophe; the test looked for U+2019 and so never
  matched a line that was being drawn correctly all along.
- `DropDownList1/2`'s OnHide **wipes** `OPEN_DROPDOWNMENUS[n]` instead of dropping it
  (`UIDropDownMenu.xml:17`, `:28`), so the row is emptied in place and an addon's reference to it
  stays live. Asserted both halves.
- `CONFIRM_PET_UNLEARN` gained an `OnUpdate` that hides it unless `CheckPetUntrainerDist()` answers
  (`StaticPopup.lua:1290-1294`), so the dialog needs that verb seated before the first tick.
- The spellbook shows its Spellbook tab with no pet at all (the realm sets tab 1 up on both arms of
  `hasPetSpells`), and it **fixed** both pet-paging quirks this test had pinned as "DO NOT FIX":
  `SpellBook_GetSpellID`'s pet arm carries the page term and Next writes the pet book's own page.

Also corrected a test of mine from the last batch: `IsEnabled()` answers the number 0, which is
truthy in Lua, so it is compared and not tested — as the stock UI itself does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-31 13:13:33 -06:00

312 lines
11 KiB
Rust

//! The stock StaticPopup engine (`StaticPopup.lua`): timeout, StartDelay, cancels, Escape, the
//! countdown text and addressing an instance by data.
use benilla_ui::script::UiScript;
use super::test_ui::load_ui as load_xml;
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");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
s
}
/// Expiry runs OnCancel with "timeout", then hides the dialog (`StaticPopup.lua:1716`).
#[test]
fn timeout_expires_into_a_timeout_cancel() {
benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run(
r#"reason = "unset"
StaticPopupDialogs["TEST_TIMEOUT"] = {
text = "expiring", button1 = "OK", timeout = 2,
OnCancel = function(data, r) reason = r or "none" end,
}
StaticPopup_Show("TEST_TIMEOUT")"#,
)
.unwrap();
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
s.tick(1.0);
assert!(
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"still counting"
);
s.tick(1.5);
assert!(
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"expiry hides the dialog"
);
assert_eq!(
s.eval::<String>("return reason").unwrap(),
"timeout",
"expiry runs OnCancel with reason 'timeout'"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// StartDelay holds button1 disabled, then swaps the real text in (`StaticPopup.lua:1764`); only
/// the kinds named at `StaticPopup.lua:1778`, RECOVER_CORPSE among them, render its countdown.
#[test]
fn start_delay_gates_button1_then_swaps_the_text_in() {
benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run(
r#"StaticPopupDialogs["RECOVER_CORPSE"] = {
StartDelay = function() return 2 end,
delayText = "%d %s until resurrection",
text = "Resurrect now?",
button1 = "Accept", timeout = 0, whileDead = 1,
}
StaticPopup_Show("RECOVER_CORPSE")"#,
)
.unwrap();
assert!(s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap());
assert!(
!s.eval::<bool>("return StaticPopup1Button1:IsEnabled() ~= 0")
.unwrap(),
"button1 starts disabled under StartDelay"
);
s.tick(0.5);
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"2 Seconds until resurrection",
"the delayText countdown renders (ceil of 1.5s)"
);
s.tick(1.6);
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"Resurrect now?",
"delay expiry swaps the real text in"
);
assert!(
s.eval::<bool>("return StaticPopup1Button1:IsEnabled() ~= 0")
.unwrap(),
"delay expiry enables button1"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Showing a dialog cancels a visible `cancels` kind with "override" (`StaticPopup.lua:1475`).
#[test]
fn showing_a_dialog_cancels_its_named_victim_with_override() {
benilla_formats::wow_data_or_skip!();
let s = setup();
s.run(
r#"victim_reason = "unset"
StaticPopupDialogs["TEST_VICTIM"] = {
text = "victim", button1 = "OK", timeout = 0,
OnCancel = function(data, r) victim_reason = r or "none" end,
}
StaticPopupDialogs["TEST_CANCELLER"] = {
text = "canceller", button1 = "OK", timeout = 0,
cancels = "TEST_VICTIM",
}
StaticPopup_Show("TEST_VICTIM")
StaticPopup_Show("TEST_CANCELLER")"#,
)
.unwrap();
assert_eq!(
s.eval::<String>("return victim_reason").unwrap(),
"override",
"the victim's OnCancel ran with reason 'override'"
);
assert_eq!(
s.eval::<String>("return StaticPopup1.which").unwrap(),
"TEST_CANCELLER",
"the canceller owns the (single) dialog instance now"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Escape closes only `hideOnEscape` dialogs (`StaticPopup.lua:1879`).
#[test]
fn escape_skips_dialogs_without_hide_on_escape() {
benilla_formats::wow_data_or_skip!();
let s = setup();
s.run(
r#"StaticPopupDialogs["TEST_STICKY"] = {
text = "cannot escape me", button1 = "OK", timeout = 0, whileDead = 1,
}
StaticPopup_Show("TEST_STICKY")"#,
)
.unwrap();
s.run("ToggleGameMenu()").unwrap();
assert!(
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"a non-hideOnEscape dialog ignores ESC (the DEATH popup law)"
);
s.run(
r#"StaticPopup_Hide("TEST_STICKY")
StaticPopupDialogs["TEST_STICKY"].hideOnEscape = 1
StaticPopup_Show("TEST_STICKY")"#,
)
.unwrap();
s.run("ToggleGameMenu()").unwrap();
assert!(
!s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"the escapable variant closes"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// DEATH's text re-renders every tick, in minutes from 60 s up (`StaticPopup.lua:1727`).
#[test]
fn the_death_countdown_text_rerenders_each_tick() {
benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run(
r#"StaticPopupDialogs["DEATH"] = {
text = "%d %s until release",
button1 = "Release Spirit",
OnShow = function()
this.timeleft = 90
end,
timeout = 0, whileDead = 1,
}
StaticPopup_Show("DEATH")"#,
)
.unwrap();
s.tick(0.1);
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"2 Minutes until release",
"above 60s renders ceil-minutes"
);
s.tick(31.0);
assert_eq!(
s.eval::<String>("return StaticPopup1Text:GetText()")
.unwrap(),
"59 Seconds until release",
"below 60s renders seconds"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `StaticPopup_FindVisible` matches data only for a `multiple` kind (`StaticPopup.lua:1421`);
/// `StaticPopup_Hide` given data hides only the instance carrying it (`StaticPopup.lua:1706`).
#[test]
fn hide_and_find_address_one_instance_by_data_only_for_a_multiple_dialog() {
benilla_formats::wow_data_or_skip!();
let s = setup();
s.run(
r#"
StaticPopupDialogs["T_MULTI"] = { text = "multi", button1 = "Ok", multiple = 1, timeout = 0 }
StaticPopupDialogs["T_ONE"] = { text = "one", button1 = "Ok", timeout = 0 }
a = StaticPopup_Show("T_MULTI") a.data = "alpha"
b = StaticPopup_Show("T_MULTI") b.data = "beta"
c = StaticPopup_Show("T_ONE") c.data = "gamma"
"#,
)
.unwrap();
assert!(s.errors().is_empty(), "{:?}", s.errors());
// Both ways round, so finding the first instance alone cannot pass.
for (data, want) in [("alpha", "a"), ("beta", "b")] {
assert!(
s.eval::<bool>(&format!(
"return StaticPopup_FindVisible(\"T_MULTI\", {data:?}) == {want}"
))
.unwrap(),
"FindVisible picks the instance carrying {data:?}"
);
}
assert!(s
.eval::<bool>(r#"return StaticPopup_FindVisible("T_MULTI", "nobody") == nil"#)
.unwrap());
s.run(r#"StaticPopup_Hide("T_MULTI", "alpha")"#).unwrap();
assert!(s
.eval::<bool>("return not a:IsShown() and b:IsShown()")
.unwrap());
// No data hides every instance of the kind.
s.run(r#"StaticPopup_Hide("T_MULTI")"#).unwrap();
assert!(s.eval::<bool>("return not b:IsShown()").unwrap());
assert!(s
.eval::<bool>(r#"return StaticPopup_FindVisible("T_ONE", "wrong") == c"#)
.unwrap());
// An unknown kind answers nil (`StaticPopup.lua:1416`).
assert!(s
.eval::<bool>(r#"return StaticPopup_FindVisible("T_NOPE") == nil"#)
.unwrap());
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// Five stock `UIParent.lua` event arms raise their dialogs, which call engine verbs.
#[test]
fn the_verb_dialogs_open_from_their_events_and_call_their_verbs() {
benilla_formats::wow_data_or_skip!();
use benilla_ui::script::ScriptValue;
let mut s = setup();
load_xml(&s, r"Interface\FrameXML\UIParent.xml"); // the arms
s.set_money(50_000);
// The realm gave this dialog an `OnUpdate` that hides it the moment `CheckPetUntrainerDist()`
// stops answering (`StaticPopup.lua:1290-1294`) — stock 1.12 had no such arm, so the dialog
// used to stand on its own. The verb has to answer before the first tick or the dialog is
// gone before anything can click it.
s.set_pet_untrainer_pending(true);
s.fire_event("CONFIRM_PET_UNLEARN", vec![ScriptValue::Int(12_345)]);
s.tick(0.0);
assert_eq!(
s.eval::<String>("return StaticPopup1.which").unwrap(),
"CONFIRM_PET_UNLEARN"
);
assert_eq!(
s.eval::<f64>("return StaticPopup1MoneyFrame.staticMoney")
.unwrap(),
12_345.0
);
s.run("StaticPopup1Button1:Click()").unwrap();
assert_eq!(s.take_pet_unlearn_confirms(), 1);
s.set_instance_boot_secs(30);
s.fire_event("INSTANCE_BOOT_START", vec![]);
s.tick(0.0);
assert!(s
.eval::<bool>("return StaticPopup_Visible(\"INSTANCE_BOOT\") ~= nil")
.unwrap());
s.fire_event("INSTANCE_BOOT_STOP", vec![]);
s.tick(0.0);
assert!(!s
.eval::<bool>("return StaticPopup_Visible(\"INSTANCE_BOOT\") ~= nil")
.unwrap());
s.set_area_spirit_healer(true, 20);
s.fire_event("AREA_SPIRIT_HEALER_IN_RANGE", vec![]);
s.tick(0.0);
assert!(s
.eval::<bool>("return StaticPopup_Visible(\"AREA_SPIRIT_HEAL\") ~= nil")
.unwrap());
// The shipped dialog accepts on show and its one button cancels (`StaticPopup.lua:1224`).
assert!(
s.take_area_spirit_accepts() >= 1,
"showing the dialog IS the accept"
);
s.run("StaticPopup_OnClick(StaticPopup_FindVisible(\"AREA_SPIRIT_HEAL\"), 1)")
.unwrap();
assert_eq!(
s.take_area_spirit_accepts(),
0,
"the button cancels; it does not accept again"
);
s.fire_event("AREA_SPIRIT_HEALER_OUT_OF_RANGE", vec![]);
s.tick(0.0);
assert!(!s
.eval::<bool>("return StaticPopup_Visible(\"AREA_SPIRIT_HEAL\") ~= nil")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}