- 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>
167 lines
6.6 KiB
Rust
167 lines
6.6 KiB
Rust
//! The stock `Interface\FrameXML\BasicControls.xml` — the reference's second file, driven the way
|
|
//! an addon drives it.
|
|
//!
|
|
//! Nothing benilla ships calls `message`, `TEXT` or `_ERRORMESSAGE`; their only consumers are
|
|
//! third-party addons (26 call `TEXT`, ~10 genuinely call `message`, two replace `_ERRORMESSAGE`).
|
|
//! So every test here enters from Lua the way those addons do.
|
|
|
|
use benilla_ui::script::UiScript;
|
|
|
|
use super::test_ui::load_ui as load_xml;
|
|
|
|
/// Fonts then BasicControls — the manifest's own order, and the whole dependency this file has.
|
|
fn basic_controls() -> UiScript {
|
|
let mut s = UiScript::new().unwrap();
|
|
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
|
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
|
s.set_screen_size(1024.0, 768.0);
|
|
s.resolve();
|
|
s
|
|
}
|
|
|
|
/// **`message("...")` puts its text in the dialog and shows it** — the whole contract, and the
|
|
/// reason the frame has to exist rather than the function being a stub that prints somewhere.
|
|
#[test]
|
|
fn message_shows_the_script_errors_dialog_carrying_its_text() {
|
|
let s = basic_controls();
|
|
assert!(
|
|
!s.eval::<bool>("return ScriptErrors:IsVisible()").unwrap(),
|
|
"the dialog starts hidden (DialogBoxFrame's `hidden=true`)"
|
|
);
|
|
|
|
s.run(r#"message("Validation error: [nil]")"#).unwrap();
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
assert!(s.eval::<bool>("return ScriptErrors:IsVisible()").unwrap());
|
|
assert_eq!(
|
|
s.eval::<String>("return ScriptErrors_Message:GetText()")
|
|
.unwrap(),
|
|
"Validation error: [nil]"
|
|
);
|
|
|
|
// The ref guard: a second message while the dialog is up does NOT overwrite the first.
|
|
s.run(r#"message("second")"#).unwrap();
|
|
assert_eq!(
|
|
s.eval::<String>("return ScriptErrors_Message:GetText()")
|
|
.unwrap(),
|
|
"Validation error: [nil]",
|
|
"`if not ScriptErrors:IsVisible()` — the first error is the one you keep"
|
|
);
|
|
|
|
// And OKAY hides it, which is the only way it closes.
|
|
s.run("ScriptErrorsButton:Click()").unwrap();
|
|
assert!(!s.eval::<bool>("return ScriptErrors:IsVisible()").unwrap());
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|
|
|
|
/// `_ERRORMESSAGE` is the reference's own name for the body, it **returns its argument**, and it
|
|
/// is replaceable — which is the entire point for the two corpus addons that replace it.
|
|
#[test]
|
|
fn error_message_returns_its_argument_and_can_be_replaced() {
|
|
let s = basic_controls();
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return _ERRORMESSAGE("boom")"#).unwrap(),
|
|
"boom",
|
|
"the ref returns `message` — a caller may chain on it"
|
|
);
|
|
|
|
// ImprovedErrorFrame's shape: swap the global, then `message` must route through the swap.
|
|
s.run(
|
|
r#"Captured = nil
|
|
_ERRORMESSAGE = function(m) Captured = m return m end
|
|
message("routed")"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(s.eval::<String>("return Captured").unwrap(), "routed");
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|
|
|
|
/// The reference's closing line is transcribed since decision 1305: BasicControls installs
|
|
/// `seterrorhandler(_ERRORMESSAGE)`, so a script error pops the dialog like the real client's —
|
|
/// and the host channel stays sighted because the ENGINE records every caught error before the
|
|
/// dispatch ever runs (that dual channel is what dissolved the old divergence, whose guard this
|
|
/// test used to be).
|
|
#[test]
|
|
fn error_message_is_the_installed_handler_and_the_host_channel_stays_sighted() {
|
|
let mut s = basic_controls();
|
|
assert!(
|
|
s.eval::<bool>("return geterrorhandler() == _ERRORMESSAGE")
|
|
.unwrap(),
|
|
"the reference's own default handler is installed (1305)"
|
|
);
|
|
// An addon's pcall wrapper calls geterrorhandler()(msg) — with _ERRORMESSAGE installed that
|
|
// pops the dialog, exactly as it does in the real client.
|
|
s.run(r#"geterrorhandler()("reported")"#).unwrap();
|
|
assert!(
|
|
s.eval::<bool>("return ScriptErrors:IsVisible()").unwrap(),
|
|
"the dialog is the report now"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return ScriptErrors_Message:GetText()")
|
|
.unwrap(),
|
|
"reported"
|
|
);
|
|
// And an ENGINE-caught error still reaches `UiScript::errors()` — the instruments' channel —
|
|
// with the dispatch adding the dialog on top, not instead.
|
|
s.run("BrokenProbe = CreateFrame('Frame') BrokenProbe:RegisterEvent('B271_PROBE') BrokenProbe:SetScript('OnEvent', function() error('fault') end)")
|
|
.unwrap();
|
|
s.fire_event("B271_PROBE", vec![]);
|
|
assert!(
|
|
s.errors().iter().any(|e| e.contains("fault")),
|
|
"engine-caught errors stay on the host channel: {:?}",
|
|
s.errors()
|
|
);
|
|
s.dispatch_script_errors_to_handler();
|
|
assert!(
|
|
s.eval::<bool>("return ScriptErrors:IsVisible()").unwrap(),
|
|
"and the dispatch keeps the dialog up for the player"
|
|
);
|
|
}
|
|
|
|
/// `TEXT` is the identity function, and 26 corpus addons call it because shipped FrameXML does.
|
|
#[test]
|
|
fn text_is_the_identity_function_the_corpus_expects() {
|
|
let s = basic_controls();
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return TEXT("Level")"#).unwrap(),
|
|
"Level"
|
|
);
|
|
assert_eq!(s.eval::<i64>("return TEXT(7)").unwrap(), 7);
|
|
}
|
|
|
|
/// The dialog kit an addon inherits from directly: `DialogBoxFrame` plus the three named textures.
|
|
/// The OKAY button must hide *the caller's* frame, and `$parent` must resolve against the caller's
|
|
/// name — `MyDialogButton`, never `DialogBoxFrameButton`.
|
|
#[test]
|
|
fn a_dialog_box_from_the_template_names_and_closes_the_callers_frame() {
|
|
let s = basic_controls();
|
|
s.run(
|
|
r#"MyDialog = CreateFrame("Frame", "MyDialog", UIParent, "DialogBoxFrame")
|
|
MyDialog:SetWidth(384) MyDialog:SetHeight(128)
|
|
MyDialog:Show()"#,
|
|
)
|
|
.unwrap();
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
assert!(
|
|
s.eval::<bool>(r#"return getglobal("MyDialogButton") ~= nil"#)
|
|
.unwrap(),
|
|
"the caller's name won through $parent"
|
|
);
|
|
assert!(
|
|
s.eval::<bool>(r#"return getglobal("DialogBoxFrameButton") == nil"#)
|
|
.unwrap(),
|
|
"and the template's own name published nothing"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return getglobal("MyDialogButton"):GetText()"#)
|
|
.unwrap(),
|
|
"OKAY"
|
|
);
|
|
|
|
s.run("MyDialogButton:Click()").unwrap();
|
|
assert!(
|
|
!s.eval::<bool>("return MyDialog:IsVisible()").unwrap(),
|
|
"`this:GetParent():Hide()` closed the caller's frame, not the template's"
|
|
);
|
|
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
|
}
|