Files
benilla-octo/crates/benilla-app/src/ui_script/panel_template_tests.rs
T
SamandClaude Fable 5.1 404708859a every dialog is the reference's own, and the trainer loads on demand
- every dialog is the reference's own: `StaticPopup.xml` + `StaticPopup.lua` load off the player's chain, and 0308's engine with its 37 dialogs leaves `UiPanels.xml`; all 76 of the reference's dialogs are the stock table's, 28 of which no file of ours had ever registered (the resurrect family among them); the nine hidden listener frames that existed only because benilla once had no `UIParent_OnEvent` come home to it verbatim from UIParent.lua, and the seven files that were nothing but a dialog and its driver are deleted; what the swap surfaced: the reference capitalises the countdown's unit word ("5 Minutes until release") where ours had lowercased it, and ESC in the typed delete confirm's box is swallowed — the entry names no escape handler; arms for events this engine never fires (PLAYER_SKINNED, TRADE_REQUEST) wait on their feeds rather than being armed blind (1960)
- the social window and the unit menus are the reference's own files: `FriendsFrame.xml` loads off the chain and our transcription is deleted — its twelve dialogs re-homed, `FRIENDLIST_SHOW` fired as the answer to an explicit `ShowFriends()`, and `Get`/`SetLookingForGroup` a client-side flag until their bodies are carved; `UnitPopup.xml` loads off the chain, our row catalogue and its driver frame go, and nine verbs are built for it — `HasFullControl` off the player-control flag the reference reads at `0x51a158`, `UnitPlayerOrPetInParty`/`InRaid` over a new unit owner, and the six pet one-shots, each pressing the bar's own reaction or command slot word through the same latch and `CMSG_PET_ACTION`, confirmed by wow-re's carve; the Guild Leave/Promote confirms 1958 believed nothing registered were our social file's all along, and the duplicate went with it (1958, 1959)
- the trainer loads on demand: our `TrainerFrame.xml` is deleted and no row replaces it — `Blizzard_TrainerUI` loads off the player's own chain through the reference's `ClassTrainerFrame_LoadUI`, and the LoadOnDemand `Blizzard_*` addons are registry rows read off the chain; the timing IS the mechanism: the list builder writes the state-filter mask to 3 on every `SMSG_TRAINER_LIST` (`0x4d75d9`) and the stock addon pushes its saved filters exactly once, on its own ADDON_LOADED — which fires on the session's first trainer, after that packet, so the saved filters win that open and an eager load would never have applied them; the loader holds an anchor to a sibling not built yet and applies it once the subtree exists; and `UNIT_FLAGS` fires only on a change between two snapshots — wow-re's trio found the per-field watch bridge runs no notify pass on the create leg, correcting 1953 (1957)
- the additive window, built and withdrawn the same hour: under `BLEND_ADD` every additive particle draw commutes with every other, so same-key additive draws can be regrouped exactly — built in the effect lane's merge walk and measured on the crowd rig (Stormwind, forty partybots): the adjacency merge already folds about half the effect items and the regroup found two more folds in a frame of ninety-three, so the pre-regroup walk is what runs; what stays: an `fx=` items/merged census on the FPS probe line, a `DRESS_PART` line per drawn part of the self body on the dress census, and the regression the A/B uncovered: 1948's stock chat migration dropped the drain's plain-line sender, so every `WOW_PROBE_CHAT` rig — the crowd rig's forty `.partybot add`s among them — had gone silent on the server; a probe's slash-less line reaches the wire as SAY again (1955)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 20:57:24 +07:00

585 lines
24 KiB
Rust

//! The shipped `assets/ui/UIPanelTemplates.xml` + `assets/ui/OptionsFrameTemplates.xml` — the
//! reference's SHARED widget kit, driven the way an addon drives it.
//!
//! **These are not tests of a window.** Nothing benilla ships instantiates a single template in
//! either file: their only consumer is a third-party addon writing
//! `CreateFrame(kind, name, parent, "SomeTemplate")`, which decision 1203 made work and which
//! `addon_harness` then ranked — 69 corpus call sites on `UICheckButtonTemplate` alone, 185 sites
//! in total naming templates we had never declared. So every test here goes in through
//! `CreateFrame`'s fourth argument, from Lua, with a caller-chosen name, because that is the entire
//! surface these files exist to serve.
//!
//! What they guard, in the order the harness ranked them:
//!
//! - **The name.** `CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")` must
//! publish `MyCheckText`, never `UICheckButtonTemplateText` — `getglobal(this:GetName().."Text")`
//! is the next line every one of those 69 sites writes. 1203 pinned the *mechanism*; this pins it
//! through the real templates, including the two-level `$parent` case
//! (`MyScrollScrollBarScrollUpButton`).
//! - **The art.** Each state texture in those files carries the reference's `inherits=` *and* the
//! art that `inherits=` resolves to, because our loader does not expand `inherits=` in
//! state-texture position (the deviation is stated at the head of `UIPanelTemplates.xml`). The
//! extract assertions below are that deviation's falsifier: strip the inline `file=` back to the
//! reference's `inherits=`-only form and every checkbox and panel button in the corpus goes
//! invisible with nothing erroring.
//! - **The behaviour.** `SetChecked`/`GetChecked` across a click, the close button hiding its
//! parent, the edit box round-tripping text, the scroll frame's range reaching its bar.
//! - **The names are 1.12's.** The last test re-reads both files against
//! `reference/1.12-globals.tsv` — a template we invent under a plausible-looking name is a name
//! an addon can never have meant.
use benilla_ui::script::UiScript;
use super::test_ui::load_ui as load_xml;
fn ui_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui")
}
/// The manifest prefix these two files sit on: fonts, `UIParent` (the parent every addon passes),
/// `HideUIPanel` (the close button's OnClick), the tooltip (the options widgets' hover) and the
/// scroll kit (`ScrollFrame_OnLoad`), then the two files under test in manifest order.
fn harness() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"Interface\\FrameXML\\Fonts.xml",
"UIParent.xml",
"MoneyFrame.xml",
"UiPanels.xml",
"GameTooltip.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
"OptionsFrameTemplates.xml",
] {
load_xml(&s, file);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
}
/// Every texture path the resolved frame tree actually draws.
fn drawn_textures(s: &mut UiScript) -> Vec<String> {
s.resolve();
s.extract()
.into_iter()
.filter_map(|q| match q.content {
benilla_ui::script::QuadContent::Texture { path: Some(p), .. } => Some(p),
_ => None,
})
.collect()
}
/// **The test this whole file exists for.** An addon's own line, verbatim, and then the global it
/// reads back on the next line.
///
/// The failure this pins is not "no checkbox": it is a checkbox whose label region is called
/// `UICheckButtonTemplateText`, so `getglobal(this:GetName().."Text")` is nil, so the addon's
/// `SetText` dies — with the `CreateFrame` itself having succeeded and returned an object.
#[test]
fn a_check_button_from_the_template_names_its_label_after_the_caller() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run(r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")"#)
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(
s.eval::<bool>(r#"return getglobal("MyCheckText") ~= nil"#)
.unwrap(),
"the label publishes under the CALLER's name — this is the line an addon writes next"
);
assert!(
s.eval::<bool>(r#"return getglobal("UICheckButtonTemplateText") == nil"#)
.unwrap(),
"and never under the template's"
);
// The 69-call-site idiom, end to end.
s.run(r#"getglobal("MyCheckText"):SetText("Show my thing")"#)
.unwrap();
assert_eq!(
s.eval::<String>(r#"return getglobal("MyCheckText"):GetText()"#)
.unwrap(),
"Show my thing"
);
assert_eq!(
s.eval::<(f64, f64)>("return MyCheck:GetWidth(), MyCheck:GetHeight()")
.unwrap(),
(32.0, 32.0),
"the template's own 32x32"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The checkbox **paints**, and it paints the reference's art.
///
/// This is the falsifier for `UIPanelTemplates.xml`'s stated deviation: the reference declares its
/// state textures with `inherits=` alone, our loader does not expand `inherits=` there, and a
/// transcription that copied the reference literally would create a frame with no state textures at
/// all — no error, no warning, 69 invisible checkboxes.
#[test]
fn the_templated_check_button_draws_the_reference_checkbox_art() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")
MyCheck:SetPoint("TOPLEFT", 20, -20)"#,
)
.unwrap();
assert!(
s.eval::<bool>("return MyCheck:GetNormalTexture() ~= nil")
.unwrap(),
"the state-texture slot exists at all"
);
let drawn = drawn_textures(&mut s);
assert!(
drawn
.iter()
.any(|p| p == r"Interface\Buttons\UI-CheckBox-Up"),
"the unchecked box is on screen: {drawn:?}"
);
assert!(
!drawn
.iter()
.any(|p| p == r"Interface\Buttons\UI-CheckBox-Check"),
"and the tick is not, until it is checked"
);
s.run("MyCheck:SetChecked(1)").unwrap();
let drawn = drawn_textures(&mut s);
assert!(
drawn
.iter()
.any(|p| p == r"Interface\Buttons\UI-CheckBox-Check"),
"checked draws the tick: {drawn:?}"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// A click toggles, and the addon's `OnClick` sees the NEW state — the widget contract every
/// options panel in the corpus is written against (`if this:GetChecked() then …`).
#[test]
fn a_templated_check_button_toggles_before_its_on_click_runs() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run(
r#"MyCheck = CreateFrame("CheckButton", "MyCheck", UIParent, "UICheckButtonTemplate")
MySeen = {}
MyCheck:SetScript("OnClick", function()
table.insert(MySeen, this:GetChecked() and 1 or 0)
end)"#,
)
.unwrap();
assert!(
!s.eval::<bool>("return MyCheck:GetChecked() and true or false")
.unwrap(),
"a fresh box is unchecked"
);
s.run("MyCheck:Click()").unwrap();
assert!(s
.eval::<bool>("return MyCheck:GetChecked() and true or false")
.unwrap());
s.run("MyCheck:Click()").unwrap();
assert!(!s
.eval::<bool>("return MyCheck:GetChecked() and true or false")
.unwrap());
assert_eq!(
s.eval::<Vec<i64>>("return MySeen").unwrap(),
vec![1, 0],
"the handler read the post-toggle state both times, not the pre-toggle one"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UIPanelButtonTemplate` — 19 corpus call sites, and the shared original of nine private copies
/// in our own `assets/ui`. Its label is a `<ButtonText>` rather than a layer FontString, so the
/// addon idiom is `btn:SetText(...)` and the published global is still `$parentText`.
#[test]
fn a_panel_button_from_the_template_labels_and_paints() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyBtn = CreateFrame("Button", "MyBtn", UIParent, "UIPanelButtonTemplate")
MyBtn:SetWidth(90) MyBtn:SetHeight(22)
MyBtn:SetPoint("TOPLEFT", 20, -20)
MyBtn:SetText("Okay")"#,
)
.unwrap();
assert_eq!(
s.eval::<String>(r#"return getglobal("MyBtnText"):GetText()"#)
.unwrap(),
"Okay",
"<ButtonText name=\"$parentText\"> publishes against the caller"
);
assert!(s
.eval::<bool>(r#"return getglobal("UIPanelButtonTemplateText") == nil"#)
.unwrap());
let drawn = drawn_textures(&mut s);
assert!(
drawn
.iter()
.any(|p| p == r"Interface\Buttons\UI-Panel-Button-Up"),
"the button face is on screen: {drawn:?}"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UIPanelCloseButton` — 8 call sites across 4 corpus addons, and the one template here whose
/// whole point is its script: `HideUIPanel(this:GetParent())`, resolved at click time against
/// `UiPanels.xml`.
#[test]
fn the_templated_close_button_hides_the_frame_it_sits_on() {
let _data = benilla_formats::wow_data_or_skip!();
let s = harness();
s.run(
r#"MyPanel = CreateFrame("Frame", "MyPanel", UIParent)
MyPanel:SetWidth(200) MyPanel:SetHeight(100)
MyPanel:SetPoint("CENTER")
MyPanel:Show()
MyClose = CreateFrame("Button", "MyClose", MyPanel, "UIPanelCloseButton")
MyClose:SetPoint("TOPRIGHT")"#,
)
.unwrap();
assert_eq!(
s.eval::<(f64, f64)>("return MyClose:GetWidth(), MyClose:GetHeight()")
.unwrap(),
(32.0, 32.0)
);
assert!(s.eval::<bool>("return MyPanel:IsShown()").unwrap());
s.run("MyClose:Click()").unwrap();
assert!(
!s.eval::<bool>("return MyPanel:IsShown()").unwrap(),
"the template's own OnClick reached HideUIPanel"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `InputBoxTemplate` — 5 call sites across 3 corpus addons. Its three border slices are `<Layers>`
/// regions (so they publish by name), and its direct-child `<FontString>` is the box's *text*
/// region, not a layer.
#[test]
fn an_input_box_from_the_template_carries_its_border_and_takes_text() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyBox = CreateFrame("EditBox", "MyBox", UIParent, "InputBoxTemplate")
MyBox:SetWidth(120) MyBox:SetHeight(20)
MyBox:SetPoint("TOPLEFT", 20, -20)
MyBox:SetText("hello")"#,
)
.unwrap();
for slice in ["Left", "Right", "Middle"] {
assert!(
s.eval::<bool>(&format!(r#"return getglobal("MyBox{slice}") ~= nil"#))
.unwrap(),
"the {slice} border slice publishes against the caller's name"
);
}
assert_eq!(s.eval::<String>("return MyBox:GetText()").unwrap(), "hello");
let drawn = drawn_textures(&mut s);
assert!(
drawn
.iter()
.any(|p| p == r"Interface\Common\Common-Input-Border"),
"the input border draws: {drawn:?}"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UIPanelScrollFrameTemplate` — 6 call sites across 2 corpus addons, and the template with the
/// deepest naming problem: its scroll bar's arrow buttons are `$parentScrollUpButton` inside
/// `$parentScrollBar`, so a caller named `MyScroll` must reach
/// `MyScrollScrollBarScrollUpButton` through **two** levels of `$parent` — which is precisely what
/// `ScrollFrame_OnLoad` getglobals on the way past.
///
/// The `UpdateScrollChildRect` leg is also the falsifier for the `<ThumbTexture>` publication:
/// `ScrollFrame_OnScrollRangeChanged` reaches the thumb through
/// `getglobal(bar:GetName().."ThumbTexture")`, which resolves only because our loader publishes a
/// named `<ThumbTexture>` as a global. If that publication regressed, this call would raise on a
/// nil index and `s.errors()` would not be empty.
///
/// (This used to claim the opposite — that we diverged to `GetThumbTexture()` "because our loader
/// never publishes a named `<ThumbTexture>`", and called it "this file's one Lua divergence". It
/// was already false when written: the XML has used `getglobal` throughout, at HEAD and now.
/// Corrected rather than deleted, because a doc comment asserting a divergence that does not exist
/// is exactly how someone later introduces a real one by "restoring" it.)
#[test]
fn a_scroll_frame_from_the_template_wires_its_bar_two_parents_deep() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyScroll = CreateFrame("ScrollFrame", "MyScroll", UIParent, "UIPanelScrollFrameTemplate")
MyScroll:SetWidth(290) MyScroll:SetHeight(80)
MyScroll:SetPoint("TOPLEFT", 20, -20)"#,
)
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
for child in [
"ScrollBar",
"ScrollBarScrollUpButton",
"ScrollBarScrollDownButton",
] {
assert!(
s.eval::<bool>(&format!(r#"return getglobal("MyScroll{child}") ~= nil"#))
.unwrap(),
"MyScroll{child} — the caller's name won through every $parent level"
);
}
// ScrollFrame_OnLoad ran off the template: empty range, both arrows greyed, offset 0.
assert_eq!(
s.eval::<(f64, f64)>("return MyScrollScrollBar:GetMinMaxValues()")
.unwrap(),
(0.0, 0.0)
);
assert!(!s
.eval::<bool>("return MyScrollScrollBarScrollUpButton:IsEnabled() ~= 0")
.unwrap());
assert_eq!(s.eval::<i64>("return MyScroll.offset").unwrap(), 0);
// A scroll child taller than the window, then the range change the reference's own handler
// consumes — the leg that touches the thumb.
s.run(
r#"MyScrollChild = CreateFrame("Frame", "MyScrollChild", MyScroll)
MyScrollChild:SetWidth(290) MyScrollChild:SetHeight(192)
MyScroll:SetScrollChild(MyScrollChild)"#,
)
.unwrap();
s.resolve();
s.run("MyScroll:UpdateScrollChildRect()").unwrap();
assert!(
s.errors().is_empty(),
"ScrollFrame_OnScrollRangeChanged ran clean: {:?}",
s.errors()
);
assert_eq!(
s.eval::<(f64, f64)>("return MyScrollScrollBar:GetMinMaxValues()")
.unwrap(),
(0.0, 112.0),
"192px of content in an 80px window — the bar's range is the overflow"
);
assert!(
s.eval::<bool>("return MyScrollScrollBarScrollDownButton:IsEnabled() ~= 0")
.unwrap(),
"there is somewhere to scroll to, so the down arrow woke"
);
// And the frame's own <OnVerticalScroll> seats the bar and re-enables the up arrow.
s.run("MyScroll:SetVerticalScroll(48)").unwrap();
assert_eq!(
s.eval::<f64>("return MyScrollScrollBar:GetValue()")
.unwrap(),
48.0
);
assert!(s
.eval::<bool>("return MyScrollScrollBarScrollUpButton:IsEnabled() ~= 0")
.unwrap());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `UIOptionsCheckButtonTemplate` — 8 call sites, all in one addon's options panel, and a
/// two-generation chain: it overrides the size of `OptionsCheckButtonTemplate`, which adds the hit
/// rect and the click sound to `UICheckButtonTemplate`. The chain has to resolve through
/// `CreateFrame`'s fourth argument the same way it resolves through XML `inherits=`.
#[test]
fn the_options_check_button_resolves_its_whole_inheritance_chain() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.run(
r#"MyOpt = CreateFrame("CheckButton", "MyOpt", UIParent, "UIOptionsCheckButtonTemplate")"#,
)
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.eval::<(f64, f64)>("return MyOpt:GetWidth(), MyOpt:GetHeight()")
.unwrap(),
(26.0, 26.0),
"UIOptionsFrame.xml's own size override, the LAST link in the chain"
);
let (_, right, _, _) = s
.eval::<(f64, f64, f64, f64)>("return MyOpt:GetHitRectInsets()")
.unwrap();
assert_eq!(
right, -100.0,
"OptionsCheckButtonTemplate's label-catching hit rect, the MIDDLE link"
);
assert!(
s.eval::<bool>(r#"return getglobal("MyOptText") ~= nil"#)
.unwrap(),
"and UICheckButtonTemplate's label, the ROOT link — still named after the caller"
);
// The middle link's OnClick is the reference's option-toggle sound pair.
let _ = s.take_sounds();
s.run("MyOpt:Click()").unwrap();
assert!(
!s.take_sounds().is_empty(),
"clicking an options checkbox makes the reference's toggle sound"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **No invented names.** Every `virtual="true"` template these two files declare is a name the
/// real 1.12.1 client has, read from `reference/1.12-globals.tsv`.
///
/// Scoped to these two files on purpose: the rest of `assets/ui` is full of deliberately
/// benilla-shaped template names (`BenillaScrollBarTemplate`, `BenillaMacroPanelButtonTemplate`,
/// `OptionsRedButtonTemplate`), and a whole-tree sweep would be asserting something else. These
/// two files make the opposite claim — *these are the reference's own names, which is why an addon
/// can find them* — so that claim is the one worth gating. A template renamed to something
/// plausible-but-absent here is a template no addon will ever name.
#[test]
fn every_template_these_files_declare_is_a_real_1_12_name() {
let _data = benilla_formats::wow_data_or_skip!();
let tsv =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../reference/1.12-globals.tsv");
let text = std::fs::read_to_string(&tsv).unwrap_or_else(|e| {
panic!(
"reading {}: {e} — regenerate with scripts/gen-reference-globals.py",
tsv.display()
)
});
let known: std::collections::HashSet<&str> = text
.lines()
.filter(|l| !l.starts_with('#'))
.filter_map(|l| l.split('\t').next())
.collect();
let mut checked = 0;
// OUR files only. This reads the source tree directly, so it cannot name a chain entry — and
// it has no reason to: asserting that the reference's own file declares 1.12 names is
// tautological. What it guards is us inventing one (decision 1841).
// Our remaining file only. `UIPanelTemplates.xml` was ours until 1846 put the whole kit on the
// chain; what is left to guard is the one template we still declare.
for file in ["OptionsFrameTemplates.xml"] {
let src = std::fs::read_to_string(ui_dir().join(file)).unwrap();
let doc = benilla_ui::framexml::parse(&src).unwrap();
for item in &doc.items {
let benilla_ui::framexml::TopLevel::Template(el) = item else {
continue;
};
let name = el.name().expect("every template here is named");
assert!(
known.contains(name),
"{file} declares '{name}', which is not a 1.12 global — \
a template the reference does not have is one no addon can name"
);
checked += 1;
}
}
assert!(
checked >= 1,
"only {checked} templates swept — the sweep, not the files, is what broke"
);
}
/// **`PanelTemplates_TabResize`'s `tab` argument is OPTIONAL, and omitting it means `this`.**
///
/// The reference opens with `if ( tab ) then tabName = tab:GetName(); else tabName =
/// this:GetName(); tab = this; end`. Ours had only the first half, so `tab:GetName()` on a nil
/// threw — and nothing in our tree noticed, because every caller we wrote passed a tab explicitly.
/// The stock files do not: `Blizzard_MacroUI.xml`'s two tabs each call
/// `PanelTemplates_TabResize(0)` from their own `OnLoad` and nothing else, so both threw at load
/// and the window came up with unsized tabs. Decision 1835.
///
/// Driven through a real `OnLoad` rather than a direct call, because `this` is exactly what is
/// under test: a plain function call would not set it.
#[test]
fn tab_resize_falls_back_to_this_when_no_tab_is_passed() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
let doc = benilla_ui::framexml::parse(
r#"<Ui>
<Button name="ProbeTab" inherits="CharacterFrameTabButtonTemplate" text="Macros">
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts><OnLoad>PanelTemplates_TabResize(0)</OnLoad></Scripts>
</Button>
</Ui>"#,
)
.unwrap();
let report = benilla_ui::loader::load_in(&s, &doc, "test", &|_: &str| None);
assert!(
report.errors.is_empty(),
"the OnLoad must not throw: {:?}",
report.errors
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// It sized the tab it was never handed: wider than the bare label, because the end slices are
// added on top.
let width = s.eval::<f64>("return ProbeTab:GetWidth()").unwrap();
assert!(width > 0.0, "the tab got a width, got {width}");
}
/// **The window tab's structural clamp, driven the way the client drives it — from `OnUpdate`.**
///
/// 1002's law is that a tab may never grow past its window's drawn right edge, whatever fit the
/// instance asked for. `BenillaTabButton_OnUpdate` enforces it by comparing the fitted width
/// against the room, and it used to read that width from `PanelTemplates_TabResize`'s return —
/// a benilla-only line on OUR copy of that verb.
///
/// 1837 put `Interface\FrameXML\UIPanelTemplates.lua` on the chain BELOW `UiPanels.xml`, so the
/// reference's `PanelTemplates_TabResize` is the one that runs, and it returns nothing. The
/// comparison became `nil > number`: a raise, once per tab per width change, with the clamp
/// silently off the air and the tab already sized by the unclamped first call.
///
/// The existing tab test drives `OnLoad` and never ticks, which is why nothing caught it.
#[test]
fn a_tab_never_grows_past_its_windows_right_edge() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = harness();
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
// A deliberately narrow window and a long label: the natural fit does not fit.
let doc = benilla_ui::framexml::parse(
r#"<Ui>
<Frame name="ProbeWindow">
<Size><AbsDimension x="160" y="200"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Frames>
<Button name="ProbeTab" inherits="CharacterFrameTabButtonTemplate"
text="A Very Long Tab Label Indeed">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-10"/></Offset></Anchor>
</Anchors>
</Button>
</Frames>
</Frame>
</Ui>"#,
)
.unwrap();
let report = benilla_ui::loader::load_in(&s, &doc, "test", &|_: &str| None);
assert!(report.errors.is_empty(), "load: {:?}", report.errors);
// Two ticks: the first lands the measure, the second settles against it.
s.tick(0.1);
s.tick(0.1);
assert!(
s.errors().is_empty(),
"the settle must not raise — the clamp reads the fit back, not a return value: {:?}",
s.errors()
);
let (left, width) = s
.eval::<(f64, f64)>("return ProbeTab:GetLeft(), ProbeTab:GetWidth()")
.unwrap();
let right = s.eval::<f64>("return ProbeWindow:GetRight()").unwrap();
assert!(
left + width <= right + 0.5,
"the tab ran past its window: left {left} + width {width} > right {right}"
);
}