- the help window is the reference's own `HelpFrame.xml`, and ours was less than half its size, so this is mostly arrival: the category list, the per-category GM and harassment pages, the stuck page, the bug report, the PvP policy line; four things we had built go with the swap — one-click filing from Home, the Home unstick button, the downed-queue refusal message, and ticket abandonment, a capability loss rather than a look change, since `AbandonTicket` exists in neither the binding table nor the 1.12 FrameXML corpus and any return of it goes on top of the stock window; `GMSURVEY_DISPLAY` arrives with no producer, a real 1.12 event (`SignalEvent` `0x5e797b`) whose whole UI is a LoadOnDemand addon not on our chain; and the stock file needs two orderings ours did not, the micro-button row before its GM status ask and the buff frame before its repoll timer (1889, superseding 1687)
- the cast bar, the action-button regions and the font registry are the reference's own files too: our `CastingBar.xml` was `CastingBarFrame.xml` under another name, with the literal "Failed" where the reference reads the `FAILED` GlobalString (1886); `ActionButtonTemplate.xml` seats above our `ActionBar.xml` because the later-registered template wins, and it declares zero scripts, so the regions migrate while the handlers stay ours until `ActionButton.lua`'s verbs do (1887); `Fonts.xml` is manifest entry 0, all 51 fonts match by name, and its one real difference, `ITEM_QUALITY_COLORS`, moves to UIParent where the reference declares it (1888); the harnesses were the real cost — those reading our files off disk could not name a chain file and now read through the manifest's own rule — and the event-producer gate, having cried wolf at the cast bar's `SPELLCAST_INTERRUPTED` fired through a variable, now matches the fired tuple's shape
- of the 189 events benilla fires, three are not in the client — `BAG_UPDATE_DELAYED` (a Classic Era name), `LOOT_UPDATE` and `UPDATE_LOOT_ROLL` (invented here, no listener anywhere) — found against the reference binary's own string table, and a gate now fails any fire of a name the client cannot dispatch unless it wears a `BENILLA_` prefix (1883); two payloads the reference gives that we did not: `UNIT_HAPPINESS` carries its unit token, without which no `UNIT_*` consumer reads it, and `CHARACTER_POINTS_CHANGED` carries the point deltas `ChatFrame.lua` compares unguarded, so nil raised (1884); and `PLAYER_LEVEL_UP` carries all nine — level, health and mana gain, talent points (not on the wire: the client derives `(newLevel >= 10) ? 1 : 0` at `0x5e407c`) and the five stat gains — matched to the ding by level, a demotion's absent gains sent as zeros since a nil raises where a zero reads as no line
- the chain gap report could not run — one of its six probes had no player seated where a stock addon formats `UnitName("player")` at load, the other five each carrying an identical copy-pasted seed — and the repaired instrument prices each window's cost: 13 stock files load clean with nothing missing (the action bars, the cast bar, the fonts, the help, friends, game-menu and sound-options windows among them) and the near ones name their missing verb, with "loads clean" still not "swapped" (1885); and `UpdateNameplates`, recorded as the missing engine verb blocking the UIParent keystone, is FrameXML — defined in `UIOptionsFrame.lua`, absent from the binding table, its whole body four nameplate verbs that are already ours — so it is twelve lines transcribed into our options frame, the reference's own Show-then-Hide no-op included (1890 corrects 1878)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
167 lines
6.5 KiB
Rust
167 lines
6.5 KiB
Rust
//! The shipped `assets/ui/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());
|
|
}
|