- 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>
231 lines
10 KiB
Rust
231 lines
10 KiB
Rust
//! `CreateFrame`'s fourth argument against the **real** `assets/ui` templates — the corpus idiom,
|
|
//! end to end.
|
|
//!
|
|
//! The unit tests for the runtime template path live in
|
|
//! `benilla-ui/src/script/tests/create_frame_template.rs` and drive synthetic templates. This one
|
|
//! exists because the thing an addon actually types is
|
|
//!
|
|
//! ```lua
|
|
//! local tab = CreateFrame("Button", "MyTab", UIParent, "TabButtonTemplate")
|
|
//! getglobal("MyTab".."Text"):SetText("Hi")
|
|
//! ```
|
|
//!
|
|
//! against a template *we* wrote, loaded the way the client loads it — six hundred lines of real
|
|
//! FrameXML above it, a font registry, an anchor graph — not a four-line fixture. Every assertion
|
|
//! below is a global an addon would reach for.
|
|
|
|
mod common;
|
|
|
|
use benilla_ui::script::UiScript;
|
|
|
|
/// The prefix of `benilla.toc`'s load order these templates need: the font registry, the panel kit,
|
|
/// UIParent (the parent every addon passes), and the faux-scroll kit.
|
|
///
|
|
/// **Two of the templates under test are the REFERENCE's since 1860** — `FauxScrollFrameTemplate`
|
|
/// and `TabButtonTemplate` were ours until the dead-copy sweep, and both now come off the player's
|
|
/// chain from `Interface\FrameXML\UIPanelTemplates.xml`, seated below `UIParent.xml` exactly as
|
|
/// the manifest seats it. So this list carries the chain pair and the loader below has to be able
|
|
/// to READ a chain entry, which a disk-only provider under `assets/ui` cannot.
|
|
const FILES: &[&str] = &[
|
|
"Interface\\FrameXML\\Fonts.xml",
|
|
// A REGRESSION GUARD, not a dependency (1923). FadingFrame.xml's entire body is a single
|
|
// `<Script file="FadingFrame.lua"/>`, so it loads correctly ONLY if that relative reference
|
|
// resolves against the document's own directory. Load it through the path-less `loader::load`
|
|
// and the base is "", the ref stays bare, the provider misses, and the assert below fires —
|
|
// which is exactly what it did before this file passed the path.
|
|
//
|
|
// It is self-contained on BOTH axes, and both had to be checked: zero `inherits=`, AND its Lua
|
|
// has no file-scope statements at all — only function definitions. The first guard tried here
|
|
// was CombatFeedback.xml, which also has zero `inherits=` but whose Lua calls `TEXT()` at file
|
|
// scope (l.7); that global is BasicControls.xml's, so it needed a dependency after all.
|
|
// `UnitFrame.lua` fails the same way (`TEXT(MANA)`). Structural dependencies are not the only
|
|
// kind.
|
|
"Interface\\FrameXML\\FadingFrame.xml",
|
|
r"Interface\FrameXML\MoneyFrame.lua",
|
|
r"Interface\FrameXML\MoneyFrame.xml",
|
|
r"Interface\FrameXML\UIParent.xml",
|
|
r"Interface\FrameXML\UIPanelTemplates.lua",
|
|
r"Interface\FrameXML\UIPanelTemplates.xml",
|
|
"Interface\\FrameXML\\GlobalStrings.lua",
|
|
"Interface\\FrameXML\\BasicControls.xml", // `TEXT`, which StaticPopup.lua reads at file scope
|
|
"Interface\\FrameXML\\LocaleProperties.lua",
|
|
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
|
|
"ScrollTemplates.xml",
|
|
];
|
|
|
|
fn load_ui(script: &UiScript) {
|
|
for file in FILES {
|
|
common::load_ui(script, file);
|
|
}
|
|
}
|
|
|
|
/// The line an addon writes, and the globals it reads on the next one.
|
|
///
|
|
/// `TabButtonTemplate` (`UIPanelTemplates.xml`) is a `<Button>` carrying a `<Size>`, six `<Layers>`
|
|
/// slices, a `<ButtonText name="$parentText">`, a `<HighlightTexture
|
|
/// name="$parentHighlightTexture">`, the three state fonts and an `<OnUpdate>` — i.e. every
|
|
/// decoration pass at once. Instantiating it as `BenillaTemplateProbeTab` must publish
|
|
/// `BenillaTemplateProbeTab*`, and must publish nothing named after the template.
|
|
#[test]
|
|
fn a_real_template_reaches_an_addon_through_create_frame() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_ui(&s);
|
|
// Clear anything the UI load itself had to say; what follows is this call's alone.
|
|
let _ = s.take_warnings();
|
|
let _ = s.take_errors();
|
|
|
|
s.run(
|
|
r#"ProbeTab = CreateFrame("Button", "BenillaTemplateProbeTab", UIParent, "TabButtonTemplate")"#,
|
|
)
|
|
.expect("the corpus idiom must not error");
|
|
|
|
// The template's authored `<Size>` is 115x32 in BOTH our retired copy and the reference's, but
|
|
// the reference's `<OnLoad>` calls `PanelTemplates_TabResize(0)` — it FITS the tab to its text
|
|
// on load, and a freshly created tab has none, so what survives is the two end caps. Ours left
|
|
// the fit to an OnUpdate settle (1004), which is why this used to read the pre-fit 115. The
|
|
// height is untouched by the fit and still the template's (1860).
|
|
let (w, h) = s
|
|
.eval::<(f32, f32)>("return ProbeTab:GetWidth(), ProbeTab:GetHeight()")
|
|
.unwrap();
|
|
assert_eq!(h, 32.0, "the template's own <Size> height");
|
|
let caps = s
|
|
.eval::<f32>("return 2 * BenillaTemplateProbeTabLeft:GetWidth()")
|
|
.unwrap();
|
|
// `caps` plus the empty label's one-unit floor (`FONTSTRING_MIN_SPAN`) — the point is that it
|
|
// collapsed to its end caps, nowhere near the authored 115.
|
|
assert!(
|
|
w >= caps && w <= caps + 1.5,
|
|
"an unlabelled tab fits down to its two end caps: {w} vs {caps}"
|
|
);
|
|
assert_eq!(
|
|
s.eval::<String>("return ProbeTab:GetParent():GetName()")
|
|
.unwrap(),
|
|
"UIParent",
|
|
"the parent argument, not the template's idea of one"
|
|
);
|
|
|
|
// The globals an addon addresses the parts by — every one named against the INSTANCE. The
|
|
// reference's own kit does exactly this (`getglobal(tabName.."Text")` in PanelTemplates).
|
|
for suffix in [
|
|
"Text", // <ButtonText name="$parentText">
|
|
"HighlightTexture", // <HighlightTexture name="$parentHighlightTexture">
|
|
"Left", // <Layers> slices
|
|
"Middle",
|
|
"Right",
|
|
"LeftDisabled",
|
|
] {
|
|
assert!(
|
|
s.eval::<bool>(&format!(
|
|
r#"return getglobal("BenillaTemplateProbeTab{suffix}") ~= nil"#
|
|
))
|
|
.unwrap(),
|
|
"BenillaTemplateProbeTab{suffix} must exist"
|
|
);
|
|
assert!(
|
|
s.eval::<bool>(&format!(
|
|
r#"return getglobal("TabButtonTemplate{suffix}") == nil"#
|
|
))
|
|
.unwrap(),
|
|
"nothing may be published under the TEMPLATE's name (TabButtonTemplate{suffix})"
|
|
);
|
|
}
|
|
|
|
// The label is a real FontString the addon can drive.
|
|
s.run(r#"getglobal("BenillaTemplateProbeTabText"):SetText("Hi")"#)
|
|
.unwrap();
|
|
assert_eq!(
|
|
s.eval::<String>(r#"return getglobal("BenillaTemplateProbeTabText"):GetText()"#)
|
|
.unwrap(),
|
|
"Hi"
|
|
);
|
|
|
|
// The template's <Scripts> reached the frame. **`OnLoad`, not `OnUpdate`, since 1860** — the
|
|
// OnUpdate was our own text-fit settle (1004), and the reference's `TabButtonTemplate` declares
|
|
// exactly one handler: an `<OnLoad>` that calls `PanelTemplates_TabResize(0)` and sizes the
|
|
// highlight. That it ran is what the collapsed width above already proves.
|
|
assert!(
|
|
s.eval::<bool>(r#"return ProbeTab:GetScript("OnLoad") ~= nil"#)
|
|
.unwrap(),
|
|
"the template's OnLoad is installed"
|
|
);
|
|
|
|
assert!(s.take_errors().is_empty());
|
|
assert_eq!(
|
|
s.take_warnings(),
|
|
Vec::<String>::new(),
|
|
"a template that resolves cleanly has nothing to report"
|
|
);
|
|
}
|
|
|
|
/// The template the corpus actually asks for, and the handler that proves the naming rule.
|
|
///
|
|
/// `FauxScrollFrameTemplate` is the most-instantiated template in the 218-addon vanilla corpus that
|
|
/// benilla declares at all — 15 `CreateFrame` call sites across AckisRecipeList, FonzAppraiser,
|
|
/// Leader, Optional and oRA2. Its `<OnLoad>` is `ScrollFrame_OnLoad`, whose very first line is
|
|
///
|
|
/// ```lua
|
|
/// getglobal(this:GetName() .. "ScrollBarScrollDownButton"):Disable()
|
|
/// ```
|
|
///
|
|
/// so the handler **dies on a nil index** unless the caller's own name won all the way down: the
|
|
/// template's nested `<Slider name="$parentScrollBar">` had to become `<caller>ScrollBar`, and
|
|
/// *that* slider's own inherited `$parentScrollDownButton` had to become
|
|
/// `<caller>ScrollBarScrollDownButton`. Two levels of `$parent`, composed through a runtime
|
|
/// template, checked by shipped code rather than by an assertion we wrote to match.
|
|
#[test]
|
|
fn the_corpus_favourite_template_composes_parent_two_levels_deep() {
|
|
let _data = benilla_formats::wow_data_or_skip!();
|
|
let mut s = UiScript::new().unwrap();
|
|
s.set_screen_size(1024.0, 768.0);
|
|
load_ui(&s);
|
|
let _ = s.take_warnings();
|
|
let _ = s.take_errors();
|
|
|
|
s.run(
|
|
// **A `ScrollFrame`, not a `Frame`, since 1860.** `FauxScrollFrameTemplate` is now the
|
|
// reference's own and the reference declares it as a `<ScrollFrame>` carrying a
|
|
// `<ScrollChild>`; `framexml::merge` takes the OVERRIDING node's tag, so asking for a
|
|
// "Frame" keeps the frame a Frame and the ScrollFrame-only parts — the scroll child among
|
|
// them — cannot apply. Our retired copy was a plain `<Frame>`, which is why the corpus
|
|
// idiom used to read that way here.
|
|
r#"Scroller = CreateFrame("ScrollFrame", "BenillaTemplateProbeScroll", UIParent, "FauxScrollFrameTemplate")"#,
|
|
)
|
|
.expect("the corpus's most-used template must not error");
|
|
|
|
// `ScrollFrame_OnLoad` ran to completion — it set `this.offset`, its last statement, which it
|
|
// cannot reach if either getglobal above returned nil.
|
|
assert_eq!(
|
|
s.eval::<f32>("return Scroller.offset").unwrap(),
|
|
0.0,
|
|
"the template's OnLoad ran to its last line"
|
|
);
|
|
for suffix in [
|
|
"ScrollBar",
|
|
"ScrollBarScrollUpButton",
|
|
"ScrollBarScrollDownButton",
|
|
] {
|
|
assert!(
|
|
s.eval::<bool>(&format!(
|
|
r#"return getglobal("BenillaTemplateProbeScroll{suffix}") ~= nil"#
|
|
))
|
|
.unwrap(),
|
|
"BenillaTemplateProbeScroll{suffix} must exist"
|
|
);
|
|
}
|
|
assert_eq!(
|
|
s.eval::<String>("return BenillaTemplateProbeScrollScrollBar:GetParent():GetName()")
|
|
.unwrap(),
|
|
"BenillaTemplateProbeScroll"
|
|
);
|
|
assert!(
|
|
s.eval::<bool>(r#"return getglobal("FauxScrollFrameTemplateScrollBar") == nil"#)
|
|
.unwrap(),
|
|
"nothing may be published under the template's own name"
|
|
);
|
|
|
|
assert!(s.take_errors().is_empty());
|
|
assert_eq!(s.take_warnings(), Vec::<String>::new());
|
|
}
|