Files
benilla-octo/crates/benilla-app/src/ui_script/minimap_tests.rs
T
SamandClaude Fable 5.1 86b155d6e7 bindings answer the reference's shapes, and the panel kit goes stock
- four return shapes were wrong — `UnitPowerType` returns ONE value in 5875 (the Era `(type, "RAGE")` pair does not exist, and its miss arm pushes the NUMBER 0, which `UnitFrame.lua:122` indexes `ManaBarColor` with), `UnitName` returns `(name, server)`, `GetTexCoord` returns EIGHT (ours four — a shape that exists nowhere, with zero call sites in FrameXML, so only the binary could have caught it), `GetLootMethod` answers `freeforall` ungrouped — the fifth right-name-wrong-arity finding in two days, so wow-re's `binding-shapes.tsv` (1722 rows over all 82 registrar tables) is vendored and a gate probes every binding's arity against it: 313 probes, eight divergences (seven global, one widget); `GetBuildInfo` pushed a fourth value no 1.12 client does (458 corpus sites take one, the four that destructure a fourth read nil on the real client), `GetAddOnInfo` RAISES on a numeric index, `Model:GetFogColor` is a packed `0xAARRGGBB` dword with no unset state, and the five miss branches are the reference's exact tuples
- the panel-template kit is the reference's own `UIPanelTemplates` pair (our 18 templates were a strict subset — `TabButtonTemplate` and the `ScrollingEdit_*` family arrive, which unblocks the macro window and HelpFrame), and the swap found two structural facts: `FauxScrollFrameTemplate` is a `<ScrollFrame>` while every instance we declared was a `<Frame>` (fifteen elements across twelve files), and an instance `<OnLoad>` REPLACES the template's — four FriendsFrame lists set `updateFunc` in their own OnLoad, so `ScrollFrame_OnLoad` never ran and guild/friends/ignore/who had been silently painting nothing behind a `frame.offset or 0`
- the options widget kit (`OptionFrameBoxTemplate`, `OptionsButtonTemplate`, `OptionsCheckButtonTemplate`, `OptionsSliderTemplate`) comes off the chain too, giving back the reference's own per-widget hover; one template of ours stays because `UIOptionsFrame.xml` is not built here, and the lesson is general — when a file SHRINKS rather than dies, every harness that named it gains the chain entry beside it, not instead of it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:06:36 +07:00

319 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The shipped `MinimapCluster.xml` driven engine-only: the +/- zoom buttons must re-sync their
//! enabled state when the active zoom index switches (stepping inside/outside a WMO flips to the
//! other, independent level). Regression guard for the director-caught stale-button bug (2026-07-09):
//! `ZoomIn` greyed from an outdoor max-zoom stayed greyed indoors at the middle default level.
use benilla_ui::script::UiScript;
use super::test_ui::load_ui as load_xml;
fn enabled(s: &UiScript, button: &str) -> bool {
s.eval::<bool>(&format!("return {button}:IsEnabled() ~= 0"))
.unwrap()
}
#[test]
fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// Host globals the cluster's OnLoad/clicks lean on that a bare engine doesn't install.
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Fonts.xml");
// The shipped load order provides GameTooltip before the cluster; Minimap_Update's tooltip
// half (the PVP tint slice, decision 0287) touches it from OnLoad on.
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
// OnLoad seeds the +/- state from the live zoom. The CVar default is 3 — a middle level, so
// BOTH buttons start enabled (the old hardcoded `MinimapZoomOut:Disable()` assumed zoom 0).
assert_eq!(s.eval::<u8>("return Minimap:GetZoom()").unwrap(), 3);
assert!(enabled(&s, "MinimapZoomIn"));
assert!(enabled(&s, "MinimapZoomOut"));
// Outdoors, zoom fully in (index 5 = max): the click handler greys ZoomIn.
s.run("Minimap:SetZoom(5)").unwrap();
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert!(!enabled(&s, "MinimapZoomIn"), "at max zoom ZoomIn disables");
assert!(enabled(&s, "MinimapZoomOut"));
// Step INSIDE: the API now routes to the indoor index, still at its own untouched default 3.
// The app fires MINIMAP_UPDATE_ZOOM on the transition; the cluster must re-sync to the new level.
s.set_minimap_inside(true);
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert_eq!(
s.eval::<u8>("return Minimap:GetZoom()").unwrap(),
3,
"indoors reads the separate indoor index"
);
assert!(
enabled(&s, "MinimapZoomIn"),
"the stale outdoor max-zoom greying must clear — this is the reported bug"
);
assert!(enabled(&s, "MinimapZoomOut"));
// Indoors, zoom fully out (index 0 = widest): ZoomOut greys.
s.run("Minimap:SetZoom(0)").unwrap();
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert!(
!enabled(&s, "MinimapZoomOut"),
"at min zoom ZoomOut disables"
);
assert!(enabled(&s, "MinimapZoomIn"));
// Step back OUTSIDE: the outdoor index is exactly where we left it (5 = max), so ZoomIn is
// greyed again and the indoor min-zoom greying of ZoomOut is gone.
s.set_minimap_inside(false);
s.fire_event("MINIMAP_UPDATE_ZOOM", vec![]);
assert_eq!(s.eval::<u8>("return Minimap:GetZoom()").unwrap(), 5);
assert!(!enabled(&s, "MinimapZoomIn"), "outdoor max-zoom survived");
assert!(enabled(&s, "MinimapZoomOut"));
}
/// The tracking icon (ref-Minimap.xml l.109-174, transcribed verbatim): hidden at load, and the
/// verbatim OnEvent follows `GetTrackingTexture()` across `PLAYER_AURAS_CHANGED` — the event the
/// aura feed fires beside `UNIT_AURA` on every display-state change (`ui_aura`). Show with a
/// tracking state pushed, hide when it clears.
#[test]
fn tracking_frame_follows_get_tracking_texture_across_player_auras_changed() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::TrackingState;
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Fonts.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
let vis = |s: &UiScript| {
s.eval::<bool>("return MiniMapTrackingFrame:IsVisible()")
.unwrap()
};
assert!(!vis(&s), "no tracking at load — the frame starts hidden");
// The feed pushes a tracking aura (a miner's Find Minerals) and fires the rebuild event.
s.set_tracking(Some(TrackingState {
spell_id: 2580,
name: Some("Find Minerals".into()),
icon: Some("Interface\\Icons\\Trade_Mining".into()),
cancelable: true,
}));
s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
assert!(vis(&s), "a live tracking texture shows the icon");
// Tracking cancelled/expired: the same event path hides it.
s.set_tracking(None);
s.fire_event("PLAYER_AURAS_CHANGED", vec![]);
assert!(!vis(&s), "no tracking texture hides the frame again");
}
/// A session with the minimap cluster + the time-of-day indicator (`GameTime.xml`) loaded, the
/// game clock parked at `hour:minute` — the shape `crate::minimap::feed_game_time` pushes.
fn game_time_session(hour: u32, minute: u32) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
s.run(&format!(
"__benilla_game_hour = {hour}; __benilla_game_minute = {minute}"
))
.unwrap();
// The player's own strings and locale rather than a hand-typed copy of three of them: the
// window is the reference's file since 1751 window 5, and `TIME_TWENTYFOURHOURS` and
// `TwentyFourHourTime` are exactly what it reads. `Localization.xml` only DEFINES
// `LocalizeFrames`; the reference calls it from `UIParent_OnEvent`'s VARIABLES_LOADED arm, and
// this session has no UIParent, so it calls it directly.
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\Localization.xml");
s.run("LocalizeFrames()").unwrap();
load_xml(&s, "Fonts.xml");
// `TEXT()`, which the reference's own tooltip and label formatting passes every string through.
load_xml(&s, "BasicControls.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
s
}
/// `GameTimeTexture`'s current texcoord window, as the `(left, right, top, bottom)` rect this
/// file reasons in.
///
/// `GetTexCoord` answers EIGHT values since decision 1840 — `ULx, ULy, LLx, LLy, URx, URy, LRx,
/// LRy` — so the rect is positions 1, 5, 2, 4. Folded here rather than at each call site because
/// the window really is axis-aligned and every assertion below is about its edges.
fn tod_window(s: &UiScript) -> (f64, f64, f64, f64) {
let (ulx, uly, _, lly, urx, ..): (f64, f64, f64, f64, f64, f64, f64, f64) =
s.eval("return GameTimeTexture:GetTexCoord()").unwrap();
(ulx, urx, uly, lly)
}
/// `GameTimeFrame_Update`'s law, exactly: the 50-px window over the 128×64 UI-TOD-Indicator
/// sits on the LEFT half (the sun) through the game day and slides +0.5 to the RIGHT half (the
/// moon) outside it — night is before 5:30 AM or from 9:00 PM, boundaries included exactly as
/// the ref compares (`< DAWN or >= DUSK`).
#[test]
fn game_time_frame_slides_the_sun_moon_window_on_the_game_clock() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = game_time_session(10, 30);
// OnLoad seeded `timeOfDay = 0` and 10:30 ≠ 0, so the very first update already seated the
// window — no OnUpdate tick needed for the initial state.
let day = (0.0, 50.0 / 128.0, 0.0, 50.0 / 64.0);
assert_eq!(tod_window(&s), day, "mid-morning shows the sun half");
// 21:00 exactly is night (`>= DUSK`): the OnUpdate re-read slides the window +0.5.
s.run("__benilla_game_hour = 21; __benilla_game_minute = 0")
.unwrap();
s.tick(0.016);
assert_eq!(
tod_window(&s),
(0.5, 0.5 + 50.0 / 128.0, 0.0, 50.0 / 64.0),
"9:00 PM sharp is the moon half"
);
// 5:29 is still night; 5:30 exactly is day (`< DAWN`).
s.run("__benilla_game_hour = 5; __benilla_game_minute = 29")
.unwrap();
s.tick(0.016);
assert_eq!(tod_window(&s).0, 0.5, "5:29 AM is still the moon");
s.run("__benilla_game_minute = 30").unwrap();
s.tick(0.016);
assert_eq!(tod_window(&s), day, "5:30 AM sharp flips to the sun");
}
/// Hovering the indicator through the REAL pointer path (hit-test → OnEnter) shows the game-time
/// tooltip, live-updates it while owned (the `IsOwned` refresh branch), and hides it on leave.
/// This also pins the two loader-side pieces this frame leans on: the `<Scripts>` walker's
/// mouse auto-enable (the frame declares no enableMouse, like the reference — without the law the
/// hit-test never captures) and the `<HitRectInsets>` hull.
#[test]
fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = game_time_session(21, 7);
s.resolve();
// Hover the middle of the frame's hit rect: the resolved rect inset by (l=6, r=0, t=5, b=10).
let (l, r, t, b) = (
s.eval::<f32>("return GameTimeFrame:GetLeft()").unwrap(),
s.eval::<f32>("return GameTimeFrame:GetRight()").unwrap(),
s.eval::<f32>("return GameTimeFrame:GetTop()").unwrap(),
s.eval::<f32>("return GameTimeFrame:GetBottom()").unwrap(),
);
let (x, y) = ((l + 6.0 + r) * 0.5, (b + 10.0 + t - 5.0) * 0.5);
s.mouse_move(x, y);
assert!(
s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"OnEnter owns the tooltip (the Scripts-walker auto-enable capturing at {x},{y})"
);
// TwentyFourHourTime = 1 (enGB LocalizeFrames, per GameTime.xml's header): 21:07, not 9:07 PM.
let text = |s: &UiScript| {
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap()
};
assert_eq!(text(&s), "21:07");
// The minute ticks while hovered: GameTimeFrame_Update's IsOwned branch refreshes in place.
s.run("__benilla_game_minute = 8").unwrap();
s.tick(0.016);
assert_eq!(text(&s), "21:08", "the owned tooltip follows the clock");
// Leave: a point past the left inset — outside the hit hull but still inside the raw 50×50
// rect — must ALSO leave; the insets are part of the reference geometry.
s.mouse_move(l + 2.0, y);
assert!(
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"the 6-px left inset band is not hoverable"
);
}
/// **The ping's click path, driven by a real mouse event** (decision 1596).
///
/// The point of the test is the *path*, not the arithmetic: `Minimap_OnClick` is only reached if
/// the widget is mouse-enabled, hit-tests, and its `OnMouseUp` fires — and what it parks has to be
/// centre-relative UI units, because that is the contract the app's conversion is written against.
/// Calling `Minimap_OnClick()` from `s.run` would prove none of it (1234 §2).
#[test]
fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Fonts.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
s.resolve();
let cx = s
.eval::<f32>("local x = Minimap:GetCenter(); return x")
.unwrap();
let cy = s
.eval::<f32>("local _, y = Minimap:GetCenter(); return y")
.unwrap();
assert!(cx > 0.0 && cy > 0.0, "the widget resolved: ({cx}, {cy})");
// Nothing is parked until somebody clicks.
assert_eq!(s.take_minimap_ping_request(), None);
// A click 20 UI units right and 12 up of the centre — inside the 70-unit disc.
s.mouse_button(cx + 20.0, cy + 12.0, "LeftButton", true);
s.mouse_button(cx + 20.0, cy + 12.0, "LeftButton", false);
let (dx, dy) = s
.take_minimap_ping_request()
.expect("OnMouseUp → Minimap_OnClick → PingLocation");
assert!((dx - 20.0).abs() < 0.01, "x right of centre: {dx}");
assert!((dy - 12.0).abs() < 0.01, "y UP from centre: {dy}");
// Draining is a drain: the app must not see the same click twice.
assert_eq!(s.take_minimap_ping_request(), None);
// A click that misses the widget entirely never reaches the handler.
s.mouse_button(10.0, 10.0, "LeftButton", true);
s.mouse_button(10.0, 10.0, "LeftButton", false);
assert_eq!(s.take_minimap_ping_request(), None, "off-widget is no ping");
}
/// `Minimap:GetPingPosition()` answers **nil** with no ping, and the live pair once the app
/// publishes one — never `(0, 0)` for "there isn't one", which a caller cannot tell from a ping
/// under its own feet.
#[test]
fn get_ping_position_is_nil_until_there_is_a_ping() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run("function GetMinimapZoneText() return '' end")
.unwrap();
s.run("function PlaySound() end").unwrap();
load_xml(&s, "Fonts.xml");
load_xml(&s, "GameTooltip.xml");
load_xml(&s, "MinimapCluster.xml");
assert_eq!(
s.eval::<Option<f32>>("return (Minimap:GetPingPosition())")
.unwrap(),
None
);
s.set_minimap_ping(Some((0.25, -0.125)));
let x = s
.eval::<f32>("local x = Minimap:GetPingPosition(); return x")
.unwrap();
let y = s
.eval::<f32>("local _, y = Minimap:GetPingPosition(); return y")
.unwrap();
assert!(
(x - 0.25).abs() < 1e-6 && (y + 0.125).abs() < 1e-6,
"{x} {y}"
);
s.set_minimap_ping(None);
assert_eq!(
s.eval::<Option<f32>>("return (Minimap:GetPingPosition())")
.unwrap(),
None
);
}