Files
benilla-octo/crates/benilla-app/src/ui_script/shipped_xml_tests.rs
T
SamandClaude Fable 5 5de1f3e7b8 feign death plays dead everywhere, and the attack keys go exclusive
- feign death plays dead for real: the body collapses and stays down, both bars read 0 and DEAD, the nameplate drops, the combat hum goes quiet
- melee and Auto Shot are exclusive now: starting one stops the other, even mid-swing
- another client's addon chatter no longer prints as party chat
- right-click presses look pressed, rage and happiness read on their real scale, and the login boxes blink their caret

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:00:00 +07:00

251 lines
12 KiB
Rust

//! Whole-directory guards over the shipped `assets/ui/*.xml` — the coverage the per-window test
//! modules structurally cannot give, since each of those loads only the file it is about.
/// EVERY shipped `assets/ui/*.xml` parses — not just the ones a window test happens to load.
///
/// The per-window tests each load their own file, which left real holes: `CraftFrame.xml` and
/// `TradeSkillFrame.xml` ship in [`super::load_default_ui`]'s manifest and had NO parse coverage at
/// all, because the manifest is an inline array no test walks. A malformed comment in either (XML
/// forbids `--` inside `<!-- -->`, which a prose edit hits easily) would have reached a real run
/// untouched by a green suite.
///
/// Deliberately parse-only, not `loader::load`: loading one file out of manifest order reports
/// legitimate errors for templates its predecessors define, so a load-sweep would have to duplicate
/// the load order to say anything. Parsing is order-free and catches the whole well-formedness
/// class on its own.
#[test]
fn every_shipped_ui_xml_parses() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
let mut checked = 0;
for entry in std::fs::read_dir(&dir).expect("assets/ui").flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "xml") {
let text = std::fs::read_to_string(&path).expect("read");
if let Err(e) = benilla_ui::framexml::parse(&text) {
panic!("{}: {e}", path.display());
}
checked += 1;
}
}
// 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 >= 40,
"only {checked} xml files swept — sweep broke"
);
}
/// The WHOLE manifest LOADS, in its real order, with zero loader errors — the check the parse
/// sweep above structurally cannot give, and the one the app itself never made.
///
/// [`super::load_default_ui`] only ever *logged* its errors, so a broken manifest entry reached a
/// real run behind a log line nobody greps: a mistyped file name, a frame name colliding with a
/// later window's, a template used before its definer, an `<Include>` that resolves to nothing.
/// Nor could a capture run have caught it — captures skip the manifest entirely unless
/// `WOW_CAPTURE_UI=1`. This is that assertion, over the array the app really walks, so a new
/// window's entry is covered the moment it is added rather than when someone remembers to test it.
#[test]
fn the_whole_shipped_manifest_loads_without_errors() {
let mut s = benilla_ui::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
let failures = super::load_default_ui(&s);
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
s.resolve();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// **Loading the UI makes no sound.** Materializing the shipped manifest is bookkeeping — nothing
/// has opened, so nothing may be heard (decision 1033).
///
/// The defect this pins: a dropdown's `OnLoad` calls `UIDropDownMenu_Initialize`, which —
/// faithfully, ref `UIDropDownMenu.lua` l.49-52 — *calls the init function immediately*. For the
/// unit popups that init function reaches `UnitPopup_ShowMenu`, whose last line is
/// `PlaySound("igMainMenuOpen")`. The ref never gets there at load: `UnitPopup_HideButtons` leaves
/// nothing but CANCEL shown for a unit that does not exist, tripping the "only one item, don't show
/// the menu" early-out. Ours was missing that hide for FOLLOW and INSPECT (ref l.304-307/316-319),
/// so all four party dropdowns rang on startup — four copies of the menu-open tack stacked in one
/// frame, on the login screen.
///
/// Deliberately asserted over the WHOLE manifest rather than the popup: any future window that
/// plays a sound from a load-time handler is the same bug, and this is where it gets caught. Sound
/// is on by default now (decision 1026), so a load-time sound is something the director hears on
/// every single launch.
#[test]
fn loading_the_shipped_ui_queues_no_sounds() {
let mut s = benilla_ui::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
let failures = super::load_default_ui(&s);
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
s.resolve();
assert_eq!(
s.take_sounds(),
vec![],
"loading the UI played a sound — a load-time handler is ringing; see \
UnitPopup_HideButtons / UIDropDownMenu_Initialize (decision 1033)"
);
}
/// **Every `text=` in the shipped UI is answerable against the REAL `GlobalStrings.lua`** — the
/// tripwire for the defect that put "CREATE_MACROS" across the macro window's title bar (0991).
///
/// `text=` is a global-string LOOKUP, not a literal (wow-re rf28 l.36/l.115 → `FrameScript_GetText
/// 0x703bf0`). The loader didn't do the lookup at all, so **23 key-shaped values across six
/// windows** were rendering as their own key names — and nothing anywhere said so. Two halves, both
/// needed:
///
/// - a **key-shaped** value (`SCREAMING_SNAKE`) must resolve to a non-empty string, or it reaches a
/// real screen as its own key;
/// - a **literal** value ("Send Mail") must NOT collide with a real global, because the loader's
/// deliberate literal-fallback divergence means a collision would silently swap the words on
/// screen for a localized string nobody asked for.
///
/// Skips without client data (the real string table is the whole point — a hand-kept copy of the
/// keys would rot into agreeing with itself).
#[test]
fn every_shipped_text_attribute_answers_against_the_real_global_strings() {
let data = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../WoW/Data");
if !data.is_dir() {
eprintln!("skipping: vanilla client not present at {}", data.display());
return;
}
let mut chain = benilla_formats::open_chain(&data).expect("open chain");
let src = chain
.read_file("Interface\\FrameXML\\GlobalStrings.lua")
.expect("GlobalStrings.lua in the chain");
let s = benilla_ui::script::UiScript::new().expect("VM");
s.run(&String::from_utf8_lossy(&src)).expect("runs clean");
// The loader's own shape test, restated here so the two can't drift apart silently.
let key_shaped = |v: &str| {
v.len() >= 2
&& v.chars().any(|c| c.is_ascii_uppercase())
&& v.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
};
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
let mut keys = 0;
for entry in std::fs::read_dir(&dir).expect("assets/ui").flatten() {
let path = entry.path();
if path.extension().is_none_or(|e| e != "xml") {
continue;
}
let text = std::fs::read_to_string(&path).expect("read");
let file = path.file_name().unwrap_or_default().to_string_lossy();
for value in text
.match_indices("text=\"")
.filter_map(|(i, m)| text[i + m.len()..].split('"').next())
{
let resolved = s.lua().globals().get::<String>(value).ok();
if key_shaped(value) {
assert!(
resolved.is_some_and(|t| !t.is_empty()),
"{file}: text=\"{value}\" is shaped like a GlobalStrings key but the real \
GlobalStrings.lua has no such string — it would render as its own key name"
);
keys += 1;
} else {
assert!(
resolved.is_none(),
"{file}: the literal text=\"{value}\" collides with a real GlobalStrings key — \
the loader would silently show that string's value instead of these words"
);
}
}
}
// Never let the sweep pass by matching nothing: 23 key-shaped values across six windows is the
// floor as of 0991, and a regex that stops matching is exactly how this guard would retire.
assert!(keys >= 23, "only {keys} key-shaped text= values swept");
}
/// **The `$parentTextureFrame` idiom's contract, over the whole shipped UI**: a frame whose art is
/// meant to cap a unit frame must sit at a strictly HIGHER frame level than that frame's status
/// bars — because frame level is the only key term above the draw layer, and the layer would
/// otherwise lift an ARTWORK bar fill straight over BACKGROUND art.
///
/// Decision 0884 pinned the layer as bucket-wide and above the frame. Two files had been getting
/// this right for the wrong reason — `UnitFrames.xml` and `PartyFrame.xml` both *declared* the
/// TextureFrame after the bars and leaned on the retired key's insertion-order tie-break — so both
/// inverted the instant the key was corrected, and both reached the director's screen. The
/// reference spends a real frame level on this in both of its own spellings (`TargetFrame.lua`
/// l.32-34's explicit `SetFrameLevel(textureFrame-1)`, `PlayerFrame.xml` l.50-52's two anonymous
/// nesting frames); benilla now uses the first, everywhere.
///
/// Name-driven on purpose, so a *new* copy of the idiom is covered the day it is written rather
/// than when someone remembers to test it. A geometric sweep was run once over this same UI —
/// every substantially-overlapping cross-frame quad pair at one `(strata, level)`, i.e. every pair
/// the draw layer alone orders — and found no third instance; the surviving 45 pairs were all
/// benign (action-bar and bag-slot chrome adjacency, text over art, and a bar fill over its own
/// frame's trough, which is the intended order). It is not kept as a gate: frozen, that list would
/// churn on every action-bar edit, and a noisy gate is a gate nobody reads.
#[test]
fn every_texture_frame_outranks_its_status_bars() {
use benilla_ui::order::unpack;
let mut s = benilla_ui::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
let failures = super::load_default_ui(&s);
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
// Every unit-frame family painting at once: the frames hide themselves without a unit, and a
// hidden frame emits no quads to read a level from.
for unit in ["player", "target", "party1", "party2", "party3", "party4"] {
s.set_unit(
unit,
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Someone".into()),
health: 60,
max_health: 100,
level: 60,
power_type: 0,
power: 60,
max_power: 100,
..benilla_ui::script::UnitState::default()
}),
);
}
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
s.resolve();
// owner frame name → its (strata, level), read off the packed draw key the renderer sorts by.
let mut level_of: std::collections::BTreeMap<String, (u8, u16)> =
std::collections::BTreeMap::new();
for q in s.extract() {
if let Some(name) = s.quad_owner_name(q.target) {
let p = unpack(q.z);
level_of.insert(name, (p.strata, p.level));
}
}
let mut checked = 0;
for (texture_frame, &(tf_strata, tf_level)) in &level_of {
let Some(base) = texture_frame.strip_suffix("TextureFrame") else {
continue;
};
for suffix in ["HealthBar", "PowerBar", "ManaBar"] {
let bar = format!("{base}{suffix}");
let Some(&(bar_strata, bar_level)) = level_of.get(&bar) else {
continue;
};
assert_eq!(
bar_strata, tf_strata,
"{bar} and {texture_frame} must share a strata for the level to decide"
);
assert!(
tf_level > bar_level,
"{texture_frame} (level {tf_level}) must outrank {bar} (level {bar_level}): \
tied, the draw layer lifts the bar's ARTWORK fill over the frame's BACKGROUND art"
);
checked += 1;
}
}
// Player + target (health/power) and four party members (health/mana) — never let this pass by
// matching nothing, which is exactly how a renamed frame would silently retire the check.
assert!(
checked >= 12,
"only {checked} texture-frame/bar pairs checked — the name sweep found nothing"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}