the pet bar and skills tab go stock, and the raid's profile is flat

- the pet action bar is the reference's own `PetActionBarFrame.xml`; our 559-line transcription is deleted, and the reference's bar brings what it had left out — the slide, the Ctrl-held and locked gates on show/hide, the hotkey pass whose string-versus-number comparison leaves every corner empty — with the three pet dialogs moved verbatim into the popup slice; one adapter draws the autocast shine (the reference's `<Model>` draws nothing in this engine, so each button gets the token-texture shine at the reference's own 1.2); the stock file registers four events nothing fired, built rather than listed — `PET_BAR_UPDATE_COOLDOWN` when the ten cooldown triples alone move, `PLAYER_CONTROL_LOST`/`GAINED` on the edge of the control flag (`0x4958e0`'s change-gated fire), `PLAYER_FARSIGHT_FOCUS_CHANGED` on the edge of the farsight descriptor, and `UNIT_FLAGS` on a change of the raw flags (its exact fire law a dispatched question) (1953)
- the skills tab is the reference's own `SkillFrame.xml` at its slot between ReputationFrame and HonorFrame; our 874-line transcription is deleted, the `UNLEARN_SKILL` dialog moves verbatim into the popup slice, and `CancelSkillUps` is built — the page's live Close button calls it before hiding, so every close reaches it; it is the reset leg of the temp-point table (`0x4d3e30`, no arguments, no returns) over a table this client keeps empty by construction, so the reset moves nothing, which is the binding's whole effect on this data; `AddSkillUp`, `RemoveSkillUp` and `BuySkillTier` sit on buttons the reference shows only for skill points or step/rank costs no 1.12 server sends and stay named as open gaps; and the expand tab's fit is the reference's load-time law (`GetTextWidth()+45` once, in OnLoad), so the transcription's zero-measure guard and re-fit are gone (1956)
- a 45-bot Blackrock raid circling with sound on holds 60 fps with the main thread at 6–7 ms while the HUD's ms readout says 17–18: that number is the sum of every thread's CPU per frame (main, render, compute pool, audio), not the frame time — the raid costs +6 ms spread over four threads with no thread owning it; two suspects refuted by measurement: mimalloc as the process allocator (interleaved: no change on any thread, reverted the same hour) and page faults (59 a frame on the probe, tens of microseconds, not the render thread's 2.1 ms of kernel time, which is the Metal driver's own work and scales with draws); so the probe counts draws per phase family (transparent outnumbers opaque, mostly the buff wave's particles and nameplates) and `WOW_PROBE_SHARED_SKIN=1` prices cross-body instancing before it is built: nothing moved, because bevy instances only identical meshes and a geared body's merged geoset set is its own mesh (1954)
- three fixes, each removing work a 15 s sample named: booth cameras skip cluster assignment (no bevy light ever reaches a booth, and the default config rebuilt an empty cluster grid per booth view per frame — 0.2 ms of the raid's main thread), the rig palette's watermark clone reuses the row buffer from two publishes ago instead of calloc'ing the 650 KB table every publish (safe because the GPU only ever receives dirty ranges), and prop probes upload the dirty span rather than every allocated row (circling a city streams props in and out most frames, and each change re-sent hundreds of KB for one row); no bucket of the main thread's profile is above a millisecond — the raid's +2.5 ms there is forty-five bodies' worth of every per-entity lane at ~55 µs a body, and the one lever left that is all ours, one texture array per body folding ~9 draws to ~3, is sized at ~1.5 ms and not started (1954)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-04 20:56:17 +07:00
co-authored by Claude Fable 5.1
parent 2ec92beeed
commit a43e2c9a88
27 changed files with 854 additions and 1615 deletions
@@ -77,7 +77,9 @@ fn shipped_xml() -> Vec<(String, String)> {
})
.collect();
out.sort();
assert!(out.len() >= 38, "only {} xml files swept", out.len());
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751), so
// the floor sits well under the count rather than one step above it (1956).
assert!(out.len() >= 20, "only {} xml files swept", out.len());
out
}
@@ -603,10 +603,11 @@ fn the_shipped_frames_carry_the_references_flags() {
}
assert!(
// 1948 retired our ChatFrame.xml and 1952 our SpellBookFrame.xml (their frames are the
// reference's own now), which took the paired count from the low 400s to the low 360s;
// the floor guards the pairing, not the census.
compared > 350,
// 1948 retired our ChatFrame.xml, 1952 our SpellBookFrame.xml, 1953 our PetActionBar.xml
// and 1956 our SkillFrame.xml (their frames are the reference's own now), which took the
// paired count from the low 400s to the high 310s; the floor guards the pairing, not the
// census.
compared > 300,
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
);
assert!(
@@ -1,4 +1,4 @@
//! The pet action bar (`PetActionBar.xml`) driven end to end through the REAL shipped XML
//! The pet action bar (the reference's `PetActionBarFrame.xml`, 1953) driven end to end through the REAL shipped XML
//! (decision 0982) — the `multibar_stance_tests` pattern: a self-contained loader, then the
//! whole chain from a pushed slot list to the quads it actually paints.
@@ -9,33 +9,42 @@ use super::test_ui::load_ui as load_xml;
/// The pet bar's own load prerequisites, in manifest order: UiPanels (`SetDesaturation`, the
/// disabled-bar grey), UIParent (the managed bottom stack its OnShow/OnHide re-fires), Cooldown
/// (`CooldownFrame_SetTimer`), ActionBar (the `MainMenuBar` anchor target) — then the bar.
fn load_pet_bar(s: &UiScript) {
/// The reference's pet bar needs what the manifest loads before it: the action-bar chain
/// (`ActionButton_UpdateState`, `MainMenuBar` as its parent, `CooldownFrame_SetTimer`), the
/// options window's uvars for `MultiActionBars.lua`, `SetDesaturation` and `GetBindingText` from
/// our UIParent.lua slices, `TEXT` — and the chat window, whose edit box `ShowPetActionBar`
/// raises over the sliding bar (PetActionBarFrame.lua l.175). One chain, in the manifest's order,
/// for every test here (1953).
pub(super) fn load_pet_bar(s: &UiScript) {
for file in [
"MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"PetActionBar.xml",
"UiPanels.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"ScrollTemplates.xml",
"KeyBindingsPage.xml",
"OptionsFrame.xml",
"Interface\\FrameXML\\MultiActionBars.xml",
"Interface\\FrameXML\\PetActionBarFrame.xml",
"PetActionBarAdapters.xml",
"Interface\\FrameXML\\UIMenu.xml",
"Interface\\FrameXML\\ChatFrame.xml",
"Interface\\FrameXML\\FloatingChatFrame.xml",
] {
load_xml(s, file);
}
@@ -44,7 +53,7 @@ fn load_pet_bar(s: &UiScript) {
/// GlobalStrings is loaded from the MPQ at runtime, not by the loader — a token slot's `name` is
/// a KEY into it, so the tests declare the two keys they read. (That the real keys exist in the
/// shipped file is a separate fact, asserted in `ui_pet`'s own tests by name.)
fn declare_token_strings(s: &UiScript) {
pub(super) fn declare_token_strings(s: &UiScript) {
s.run(
"PET_ACTION_ATTACK = 'Attack' \
PET_MODE_DEFENSIVE = 'Defensive'",
@@ -59,7 +68,7 @@ const CLAW_WORD: u32 = 0xC100_0BC2;
/// A hunter's bar as the server actually sends it: Attack (a lit command token), an empty spell
/// slot, Claw (a spell with autocast running), and Defensive (a lit reaction token).
fn hunter_slots() -> Vec<PetActionView> {
pub(super) fn hunter_slots() -> Vec<PetActionView> {
let mut slots = vec![PetActionView::default(); 10];
slots[0] = PetActionView {
name: Some("PET_ACTION_ATTACK".into()),
@@ -208,10 +217,13 @@ fn the_shipped_pet_bar_drives_end_to_end() {
(72.0, 56.0, 102.0, 86.0)
);
// The pet goes: the bar hides again, shine marker and all.
// The pet goes: the bar slides out over the reference's PETACTIONBAR_SLIDETIME (0.09 s,
// `PetActionBarFrame_OnUpdate`) and hides at the end of the slide, shine marker and all.
s.set_pet_actions(false, true, true, Vec::new());
s.fire_event("PET_BAR_UPDATE", vec![]);
s.tick(0.05);
for _ in 0..3 {
s.tick(0.05);
}
s.resolve();
let gone = s.extract();
assert_eq!(textures(&gone, "Interface\\PetActionBar\\UI-PetBar"), 0);
@@ -367,47 +379,11 @@ fn a_disabled_bar_greys_rather_than_hides() {
fn pet_bar_row(with_multibar: bool) -> (usize, f32) {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"UIParent.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"GameTooltip.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
] {
load_xml(&s, file);
}
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
load_xml(&s, "ScrollTemplates.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, "KeyBindingsPage.xml");
load_xml(&s, "OptionsFrame.xml");
load_xml(&s, "Interface\\FrameXML\\MultiActionBars.xml");
load_pet_bar(&s);
if with_multibar {
s.run("SHOW_MULTI_ACTIONBAR_1 = 1 MultiActionBar_Update()")
.unwrap();
}
load_xml(&s, "PetActionBar.xml");
declare_token_strings(&s);
s.set_pet_actions(true, true, true, hunter_slots());
@@ -670,40 +646,6 @@ fn the_keybind_pair_pushes_and_casts_without_the_clicks_forks() {
// law, the no-seam continuity pin) live with the drawing now: `crate::autocast_shine`'s own
// test module — the script layer no longer moves a single spark (decision 1383).
/// The pet bar plus the three files its TOOLTIP needs, in the TOC's own order: Fonts.xml (the
/// colour codes that wrap the binding), GameTooltip.xml (the plate), and UIParent.xml — already a
/// prerequisite for the managed stack — for `GetBindingText`, which renders the key.
fn load_pet_bar_with_tooltip(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
"MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"UIParent.xml",
"GameTooltip.xml",
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
// two template files its check boxes inherit through (1875).
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\OptionsFrameTemplates.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"PetActionBar.xml",
] {
load_xml(s, file);
}
}
/// A hunter bar hovered through the REAL gesture, with the real binding registry and the real CVar
/// table behind it — `mouse_move` runs the shipped `<OnEnter>` with `this` bound, which is the only
/// way this fork gets exercised the way nazriel exercised it.
@@ -722,7 +664,7 @@ fn hovered_pet_bar() -> UiScript {
// `GetCVar("UberTooltips")` answers "1" (its byte-read registrar value) rather than nil.
s.register_bindings(&crate::bindings::registry_commands());
s.register_cvars(crate::cvars::registered_pairs());
load_pet_bar_with_tooltip(&s);
load_pet_bar(&s);
declare_token_strings(&s);
s.set_pet_actions(true, true, true, hunter_slots());
s.fire_event("PET_BAR_UPDATE", vec![]);
@@ -1479,6 +1479,33 @@ mod tests {
keyring tooltips through `ContainerFrameAdapters.xml`'s wrapper (0765), which is \
why nothing has hit it — the wrapper answers first for our own rows.",
),
(
"SkillFrame.xml",
"BuySkillTier",
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's LearnSkillButton calls it, and \
that button shows only while `UnitCharacterPoints`'s second value or a row's \
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
skill-point wire exists; not built (1956).",
),
(
"SkillFrame.xml",
"AddSkillUp",
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's RightArrow calls it, and \
that button shows only while `UnitCharacterPoints`'s second value or a row's \
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
skill-point wire exists; not built (1956).",
),
(
"SkillFrame.xml",
"RemoveSkillUp",
"a 5875 binding (wow-re `bindings.md`: marshals and delegates to a C++ \
method/net-send) of the pre-1.12 skill-point purchase UI. the detail bar's LeftArrow calls it, and \
that button shows only while `UnitCharacterPoints`'s second value or a row's \
step/rank cost is non-zero — which no 1.12 server sends. Unreachable until the \
skill-point wire exists; not built (1956).",
),
(
"ClassTrainerFrameTemplates.xml",
"ClassTrainerSkillButton_OnClick",
@@ -2070,10 +2097,6 @@ mod tests {
// built; none is 1819-shaped, because no PAIR is split (a half-fired pair is the tell).
const UNPRODUCED: &[(&str, &str)] = &[
("BAG_OPEN", "ContainerFrame.lua"),
(
"CVAR_UPDATE",
"TextStatusBar.lua — a CVar change does not repaint the bar text",
),
("DISPLAY_SIZE_CHANGED", "the four paperdoll files"),
(
"GMSURVEY_DISPLAY",
@@ -2177,8 +2200,13 @@ mod tests {
stack.push(p);
} else if p.extension().is_some_and(|x| x == "rs") {
let text = std::fs::read_to_string(&p).unwrap_or_default();
const CALLS: [&str; 2] =
[concat!("fire_event", "("), concat!("fire_event_into", "(")];
// The engine's deferred lane (`pending_events.push((name, args))`,
// `cursor.rs`) is a fire too — the pet grid pair rides it (1953).
const CALLS: [&str; 3] = [
concat!("fire_event", "("),
concat!("fire_event_into", "("),
concat!("pending_events.push", "(("),
];
let mut fires_indirectly = false;
for call in CALLS {
let mut from = 0;
@@ -30,7 +30,9 @@ fn every_shipped_ui_xml_parses() {
// Never let the sweep pass by finding nothing — a moved assets dir would otherwise turn this
// into a test that guards zero files while staying green.
assert!(
checked >= 38, // 41 until 1944 took the two quest files stock
// A sanity floor for the walk, not a census: `assets/ui` retires file by file (1751),
// so the floor sits well under the count rather than one step above it (1956).
checked >= 20,
"only {checked} xml files swept — sweep broke"
);
}
@@ -123,6 +125,23 @@ fn loading_the_shipped_ui_queues_no_sounds() {
/// a white slab over its autocast ring and another under its tab row. Every gate was green.
///
/// The shape half runs everywhere; the resolve half needs client data and skips without it (a
/// The reference's pet bar, up with the hunter fixture (Claw autocasting), settled past its
/// slide — the pet half of the two shine pins below.
fn pet_bar_vm() -> benilla_ui::script::UiScript {
let mut s = benilla_ui::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
super::pet_bar_tests::load_pet_bar(&s);
super::pet_bar_tests::declare_token_strings(&s);
s.set_pet_actions(true, true, true, super::pet_bar_tests::hunter_slots());
s.fire_event("PET_BAR_UPDATE", vec![]);
for _ in 0..3 {
s.tick(0.05);
}
s.resolve();
assert!(s.errors().is_empty(), "pet bar errors: {:?}", s.errors());
s
}
/// The autocast **corner brackets** (`UI-AutoCastableOverlay`), per template — the sibling pin to
/// the shine's, and the same invariant: not a magic size, but the **bracket square against its
/// button**, which is the thing an eye actually reads.
@@ -199,6 +218,12 @@ fn the_autocast_brackets_reach_each_buttons_corners() {
.eval("return SpellButton1AutoCastable:GetWidth(), SpellButton1:GetWidth()")
.unwrap();
found.push(("SpellButton1AutoCastable".into(), overlay * ART, button));
// The pet button's overlay is the reference's own template (1953) — measured the same way.
let s = pet_bar_vm();
let (overlay, button): (f32, f32) = s
.eval("return PetActionButton1AutoCastable:GetWidth(), PetActionButton1:GetWidth()")
.unwrap();
found.push(("PetActionButton1AutoCastable".into(), overlay * ART, button));
assert_eq!(found.len(), 2, "expected two autocast overlays: {found:?}");
for (name, brackets, button) in &found {
assert!(
@@ -316,6 +341,23 @@ fn the_shine_tokens_ask_for_the_rims_we_meant() {
let scale = crate::autocast_shine::token_model_scale(&token).expect("a shine token");
let view: f32 = s.eval("return SpellButton1Shine:GetWidth()").unwrap();
found.push(("SpellButton1Shine".into(), 0.02 * 1280.0 * scale, view));
// The pet button's shine is PetActionBarAdapters.xml's, over the whole button (1953): the
// token off the shown marker's quad, the viewport off that quad's own rect.
let s = pet_bar_vm();
let (token, view) = s
.extract()
.into_iter()
.find_map(|q| match (&q.content, q.rect) {
(benilla_ui::script::QuadContent::Texture { path: Some(p), .. }, Some(r))
if p.starts_with(crate::autocast_shine::SHINE_TOKEN) =>
{
Some((p.clone(), r.right - r.left))
}
_ => None,
})
.expect("the pet bar's shine marker (Claw autocasts in the hunter fixture)");
let scale = crate::autocast_shine::token_model_scale(&token).expect("a shine token");
found.push(("PetActionButton1Shine".into(), 0.02 * 1280.0 * scale, view));
assert_eq!(
found.len(),
2,
@@ -797,7 +839,8 @@ fn no_shipped_script_sets_a_global_string_key_as_display_text() {
}
assert!(offenders.is_empty(), "{}", offenders.join("\n"));
// The sweep must never pass by finding nothing to sweep.
assert!(swept >= 38, "only {swept} xml files swept — sweep broke");
// The same walk floor as above (1956).
assert!(swept >= 20, "only {swept} xml files swept — sweep broke");
}
/// **The `$parentTextureFrame` idiom's contract, over the whole shipped UI**: a frame whose art is
@@ -1394,7 +1437,9 @@ fn every_declared_parent_really_attaches() {
let declared = shipped_frame_parents();
assert!(
declared.len() > 30, // 67 before 1938 took the three bar files (25 of them) stock
// A sanity floor for the scan, not a census — the declarations retire with the files
// that carry them (1751); 67 before 1938, 30 after 1956.
declared.len() >= 15,
"only {} parent declarations found — the scan broke",
declared.len()
);
@@ -57,8 +57,18 @@ fn skill(
/// silently never got built (`SkillDetailScrollFrame` inherits `UIPanelScrollFrameTemplate`).
/// [`super::test_ui::load_ui_strict`] is that lesson made into a check.
fn shown_skills_page() -> UiScript {
shown_skills_page_with(None)
}
/// [`shown_skills_page`] with a text measurer seated BEFORE the files load — the state the app's
/// VM is in at world entry (the glue screens' frames seat it), which a stock `OnLoad` that measures
/// text relies on.
fn shown_skills_page_with(measurer: Option<Box<dyn benilla_ui::script::TextMeasure>>) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
if let Some(m) = measurer {
s.set_text_measurer(m);
}
for f in super::test_ui::CHARACTER_UI {
super::test_ui::load_ui_strict(&s, f);
}
@@ -201,7 +211,7 @@ fn a_single_rank_line_paints_gray_with_no_rank_text() {
);
// Selecting the single-rank row paints the detail pane the same way (the shared PaintBar).
s.run("SetSelectedSkill(2) BenillaSkillFrame_Update()")
s.run("SetSelectedSkill(2) SkillFrame_UpdateSkills()")
.unwrap();
assert_eq!(
s.eval::<String>("return SkillDetailStatusBarSkillRank:GetText() or \"\"")
@@ -458,30 +468,20 @@ fn the_collapse_all_fold_wears_the_row_font_and_the_references_seat() {
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
}
/// The tab's **fit law** (ref `SkillFrameExpandButtonFrame`'s OnLoad, l.312-316:
/// `SetWidth(GetTextWidth()+45)`). benilla seats the VM's font engine at the frame boundary, so
/// the XML-load call reads 0 — the guard leaves the declared width standing, and the first Update
/// with a measurer fits the tab. Both states are pinned here because only the second one is the
/// reference's, and only the first is what a cold load sees.
/// The tab's **fit law** (ref `SkillFrameExpandButtonFrame`'s OnLoad:
/// `SetWidth(SkillFrameCollapseAllButton:GetTextWidth()+45)`), which the reference applies ONCE,
/// at load, with its font engine already up. The app's VM is in that state at world entry (the
/// glue screens seat the measurer on it), so the test seats one first; the transcription used to
/// guard a 0 measure and re-fit on the first Update, which the reference never does (1956).
#[test]
fn the_expand_tab_fits_its_label_once_a_measure_answers() {
fn the_expand_tab_fits_its_label_at_load() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_skills_page();
assert_eq!(
s.eval::<f64>("return SkillFrameExpandButtonFrame:GetWidth()")
.unwrap(),
54.0,
"unmeasured, the declared width stands — a 0 measure must not squash the tab to 45"
);
s.set_text_measurer(Box::new(super::FixedWidthFont(7.0)));
s.run("BenillaSkillFrame_Update()").unwrap();
s.resolve();
let mut s = shown_skills_page_with(Some(Box::new(super::FixedWidthFont(7.0))));
assert_eq!(
s.eval::<f64>("return SkillFrameExpandButtonFrame:GetWidth()")
.unwrap(),
7.0 * 3.0 + 45.0,
"then the ref's own law: the label's width + 45"
"the ref's own law at load: the ALL label's width + 45"
);
// The middle slab is the span between the two caps, so the fit reaches the art for free.
let (mid_l, mid_r, cap_r_l) = s
@@ -496,5 +496,13 @@ fn the_expand_tab_fits_its_label_once_a_measure_answers() {
66.0 - 16.0,
"and carries the whole span minus the two caps"
);
// An Update leaves the fit alone — the reference sizes the tab at load and never again.
s.run("SkillFrame_UpdateSkills()").unwrap();
s.resolve();
assert_eq!(
s.eval::<f64>("return SkillFrameExpandButtonFrame:GetWidth()")
.unwrap(),
66.0
);
assert!(s.errors().is_empty(), "errors: {:?}", s.errors());
}
+1 -1
View File
@@ -284,7 +284,7 @@ pub(super) const CHARACTER_UI: &[&str] = &[
// bag row. It comes with `ContainerFrame.xml`, manifest 585 against the pane's 693 (1875).
"Interface\\FrameXML\\ContainerFrame.xml",
r"Interface\FrameXML\ReputationFrame.xml",
"SkillFrame.xml",
"Interface\\FrameXML\\SkillFrame.xml",
"Interface\\FrameXML\\HonorFrame.xml",
];
@@ -457,9 +457,10 @@ fn solo_target_inspect_click_reaches_inspect_unit() {
// ── The PET menu (decision 1066; report B219) ───────────────────────────────────────────────────
/// The pet menu's own prefix. Two things join the popup prefix: `UiPanels.xml` (the StaticPopup
/// engine, because two of the four rows go behind a dialog) and `PetActionBar.xml`, which is where
/// those three dialogs are registered — with the pet arc, not with the rows. `Cooldown.xml` and
/// The pet menu's own prefix. What joins the popup prefix is `UiPanels.xml` — the StaticPopup
/// engine, because two of the four rows go behind a dialog, and since 1953 where the pet arc's
/// three dialogs are registered (they rode our pet-bar file until it became the reference's).
/// `Cooldown.xml` and
/// `ActionBar.xml` are the pet bar's own load deps, not this menu's.
fn load_pet_menu_frames(s: &UiScript) {
for file in [
@@ -487,7 +488,6 @@ fn load_pet_menu_frames(s: &UiScript) {
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"PetActionBar.xml",
] {
load_xml(s, file);
}