//! 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 `` 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); // The in-game UI materializes on world entry (1051), so a player always exists by the time the // manifest loads — and the stock macro window's character tab formats `UnitName("player")` // into its label inside its own OnLoad. A manifest load with no player is a state the client // never reaches (decision 1848). s.set_unit( "player", Some(benilla_ui::script::UnitState { exists: true, name: Some("Probefour".into()), level: 60, ..Default::default() }), ); 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); // The in-game UI materializes on world entry (1051), so a player always exists by the time the // manifest loads — and the stock macro window's character tab formats `UnitName("player")` // into its label inside its own OnLoad. A manifest load with no player is a state the client // never reaches (decision 1848). s.set_unit( "player", Some(benilla_ui::script::UnitState { exists: true, name: Some("Probefour".into()), level: 60, ..Default::default() }), ); 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 texture path the shipped UI names RESOLVES in the real archives** — the tripwire for a /// mis-typed `file=`, which the renderer reports by drawing a plain WHITE QUAD and nothing else. /// /// A sprite that fails to resolve is `None` all the way to `ui_pass`, where a texture-less quad /// samples the shared 1×1 white image (that fallback is what makes flat-shaded quads batch, so it /// cannot itself be made loud). The bug it hid (1046): six `file=` attributes in /// `SpellBookFrame.xml` were written with **doubled** separators — `Interface\\SpellBook\\…`, the /// Lua escaping, in an XML attribute where a backslash is already literal. `normalize_path` only /// folds case and slashes, so the doubled key missed the archive hash and the pet book shipped with /// 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 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. /// /// The art fills only the middle `33/64` of the texture, so the drawn size decides where the /// brackets land. The reference gets this right on the pet button (58 x 0.5156 = 29.9 on 30) and /// loose on the spell book (60 x 0.5156 = 30.9 on 37 — three units inside each edge). Decision /// 1393 carries the pet button's ratio across; this test is what keeps it carried, and what will /// catch a third template added with a borrowed number. #[test] fn the_autocast_brackets_reach_each_buttons_corners() { use benilla_ui::framexml::{Element, TopLevel}; /// The fraction of `UI-AutoCastableOverlay.blp` the bracket art actually covers, measured off /// the shipped BLP (`benilla-extract blp`): art bbox 15..47 of 64. const ART: f32 = 33.0 / 64.0; fn dim(el: &Element, tag: &str, axis: &str) -> Option { el.children .iter() .find(|c| c.tag.eq_ignore_ascii_case(tag)) .and_then(|n| n.children.first()) .and_then(|d| { d.attrs() .iter() .find(|(k, _)| k.eq_ignore_ascii_case(axis)) .map(|(_, v)| v.clone()) }) .and_then(|v| v.parse().ok()) } /// Walk a template, carrying the nearest enclosing button size down to the overlay. fn walk(el: &Element, button: Option, out: &mut Vec<(String, f32, f32)>) { let button = dim(el, "Size", "x") .filter(|_| el.tag.ends_with("Button")) .or(button); let name = el .attrs() .iter() .find(|(k, _)| k.eq_ignore_ascii_case("name")) .map(|(_, v)| v.clone()) .unwrap_or_default(); if name.ends_with("AutoCastable") { if let (Some(size), Some(btn)) = (dim(el, "Size", "x"), button) { out.push((name.clone(), size * ART, btn)); } } for child in &el.children { walk(child, button, out); } } let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui"); let mut found = Vec::new(); for entry in std::fs::read_dir(&dir).expect("assets/ui") { let path = entry.expect("entry").path(); if path.extension().is_none_or(|e| e != "xml") { continue; } let src = std::fs::read_to_string(&path).expect("read"); for item in benilla_ui::framexml::parse(&src).expect("parse").items { match item { TopLevel::Font(el) | TopLevel::Template(el) | TopLevel::Instance(el) => { walk(&el, None, &mut found) } TopLevel::Include(_) | TopLevel::Script(_) => {} } } } assert_eq!(found.len(), 2, "expected two autocast overlays: {found:?}"); for (name, brackets, button) in &found { assert!( (brackets / button - 0.997).abs() < 0.01, "{name}: a {brackets:.1}-unit bracket square on a {button}-unit button is {:.3}x — the \ pet button's is 0.997x, which is what puts brackets IN the corners (decision 1393)", brackets / button ); } } /// Every shipped autocast-shine token, and the rim/viewport ratio each one asks for — the one /// place the widget's geometry is decided, so the one place to pin it. /// /// The pet button is the REFERENCE's own numbers (`setAllPoints` on 30x30 at `scale="1.2"`), and /// its ratio is why that button reads as a rim: the rim square is 1.024x its viewport, so it runs /// ON the edge and the widget's scissor halves every star (1387/1391). /// /// The spell book is **one deliberate deviation** (decision 1392). The reference writes /// `scale="1.22"` into a 36-unit viewport — a 0.87x rim that floats clear of the edge, is never /// clipped, and washes the icon; the real 1.12 client looks the same way (director-checked), so /// this is taste, not fidelity. We write 1.44 to borrow the pet button's ratio. This test is what /// stops that drifting, or being "corrected" back to 1.22 by someone who only read the ref. #[test] fn the_shine_tokens_ask_for_the_rims_we_meant() { use benilla_ui::framexml::{Element, TopLevel}; fn walk(el: &Element, out: &mut Vec<(String, f32, f32)>) { for (key, value) in el.attrs() { let scale = crate::autocast_shine::token_model_scale(value); let is_token = key.eq_ignore_ascii_case("file") && value.starts_with(crate::autocast_shine::SHINE_TOKEN); if let (true, Some(scale)) = (is_token, scale) { let view = el .children .iter() .find(|c| c.tag.eq_ignore_ascii_case("Size")) .and_then(|sz| sz.children.first()) .and_then(|d| { d.attrs() .iter() .find(|(k, _)| k.eq_ignore_ascii_case("x")) .map(|(_, v)| v.to_string()) }) .and_then(|v| v.parse::().ok()) // No means setAllPoints on the pet button, which is 30x30. .unwrap_or(30.0); out.push(( el.attrs() .iter() .find(|(k, _)| k.eq_ignore_ascii_case("name")) .map(|(_, v)| v.to_string()) .unwrap_or_default(), 0.02 * 1280.0 * scale, view, )); } } for child in &el.children { walk(child, out); } } let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui"); let mut found = Vec::new(); for entry in std::fs::read_dir(&dir).expect("assets/ui") { let path = entry.expect("entry").path(); if path.extension().is_none_or(|e| e != "xml") { continue; } let src = std::fs::read_to_string(&path).expect("read"); for item in benilla_ui::framexml::parse(&src).expect("parse").items { match item { TopLevel::Font(el) | TopLevel::Template(el) | TopLevel::Instance(el) => { walk(&el, &mut found) } TopLevel::Include(_) | TopLevel::Script(_) => {} } } } assert_eq!( found.len(), 2, "expected exactly two shine tokens: {found:?}" ); for (name, rim, view) in &found { assert!( (rim / view - 1.024).abs() < 1e-3, "{name}: a {rim}-unit rim in a {view}-unit viewport is {:.3}x, not the pet button's \ 1.024x — a rim that does not reach its own viewport edge is never clipped, and reads \ as a wash rather than a rim (decisions 1387/1392)", rim / view ); } } /// hand-kept list of real file names would rot into agreeing with itself — the `text=` sweep's /// argument below). Resolution goes through the renderer's own `sprite_candidates`, not a copy of it — /// including its `.blp`/`.tga` fallback, so the sweep accepts exactly what the renderer accepts. #[test] fn every_shipped_texture_path_resolves_in_the_client_archives() { use benilla_ui::framexml::{Element, TopLevel}; // `file=` on a texture element is an ARCHIVE path; `