Files
benilla-octo/crates/benilla-app/src/ui_script/perf_bar_tests.rs
T
SamandClaude Fable 5.1 eadacac998 a hundred stale claims retire, and text= was never a divergence
- a comment that justifies code by an engine difference is a claim with an expiry date, and the tree had ~96 expired ones across `assets/ui`, `ui_script/` and `benilla-ui` — a hygiene pass, no behaviour change: dangling `Benilla*` names with zero definitions (`BenillaMoney_*` at thirteen sites after 1937/1962 converged the coin cluster onto the stock `SmallMoneyFrameTemplate`; `BenillaGossipRow_Resize`, `BenillaErrorsFrame_AddMessage`, the digit feed 1285 deleted, `BenillaMerchantItem_OnClick` and three more), "the shipped `assets/ui/<X>.xml`" for thirty-odd files the directory no longer holds — it holds nine — each site now naming the chain file that actually loads, `UiPanels.xml` in the present tense after 1988 took the panel manager and the dialog engine stock, and expired self-deprecation: SimpleHTML "a widget kind we do not have" (1337 built it), "benilla has no console" (1948 bound `ConsoleExec`), `Model`'s "seven verbs not published yet" and "`ClearFog` is a guess" (all 23 are published, the carve sits 500 lines below, `UNBUILT` has been `[&str; 0]` since 2027), `frame_flag_gate`'s doc describing four groups of accepted mouse divergences over a `KNOWN` list that is empty (the empty array and its enforcement stay), five comments saying key delivery is not gated on `keyboard_enabled` where `script::keyboard`'s walk filters on exactly that field (1319), `SetRaidTarget` called a verb this house lacks beside an assert that its intent IS queued (1820), two questlog tests deferring to a screenshot for `GetPoint`/`IsVisible` the regions already answer, `benilla.toc`'s "trade still needs four engine verbs" (all four bound, 1966) and its ping `Model`s "this engine draws no pixels for" (2013 renders `MinimapPing.mdx` itself), and `rig_anim`'s `set_paused` naming a booth caller it does not have; and the code the prose was propping up goes with it: `GameMenuFrame.xml`'s six `<OnLoad>` label setters, because `text=` on a `<Button>` is a global-string lookup at the loader's attribute step (0995, built 61 seconds before the file was written) — the buttons carry `text="LOGOUT"`/`"EXIT_GAME"`/`"MACROS"`/`"RETURN_TO_GAME"`/`"MAIN_MENU"` as the reference's own do; `ScriptLogFrame`'s five authored close-button lines, now `inherits="UIPanelCloseButton"` off the chain's `UIPanelTemplates.xml` seated 757 lines above it (1846); and the editbox's 22-line "gap" doc claiming only the self-acquire `autoFocus` path is built and its `false` default unverified — path 1 is 1686's `visibility_focus`, and the default is `true`, byte-read off `flags = 1` at `0x779a29`/`0x779a2e`, so the test named `autofocus_does_not_focus_on_show…` is renamed for what it actually pins (2044)
- the `text=` divergence never existed: `resolve_text`'s doc had claimed since 0068 that falling back to the raw attribute on a GlobalStrings miss was "one deliberate divergence" from a reference that draws the empty string, and read at the byte it is the reference's own behaviour — `FrameScript_GetText 0x703bf0` does return a pre-seeded empty string (`0x882748`) when the global is not a string, but that return never reaches a label, because all three `text=` readers image-wide test it and substitute the raw attribute: `Button::LoadXML` at `0x778c07` (`0x778c31 mov eax,esi`), `CSimpleFontString::LoadXML` at `0x771006` byte-identically, and `0x7292a6`, which pushes `edi` on the empty arm; so the loader changes nothing, the two test comments that had called `PetLevelText`'s "Level level race class" placeholder and `CLOSE` our divergence now name the reference's arm (a real 1.12 client draws them too), and what IS ours is the extra warning on a key-shaped miss (0983 → 0991), a strict addition; beside it, `GetEffectiveScale`'s root factor is 1 not because "benilla has no `uiScale` CVar" — 1804 registered one, default 0.9 — but because the dial is applied at the raster seam, `768/uiScale` UI units of virtual-screen height rather than a scale on `UIParent`, so every coordinate the VM hands Lua is already in those units and the reference's `GetCursorPosition()/GetEffectiveScale()` transcriptions convert correctly with a root of 1 (2044)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:39:30 +07:00

330 lines
13 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 stock `Interface\FrameXML\ActionBarFrame.xml`'s performance ("ping") meter — the tinted bar
//! in the main bar's last empty recess — over the real files, never a stub.
//!
//! What these guard, in order: the ref geometry that puts the bar IN the recess (the whole point of
//! the slice); the LOW-strata draw order that makes it show *through* the bar art instead of over
//! it; the latency→color law and its 10 s poll; and the hover tooltip's live number.
use benilla_ui::script::{QuadContent, UiScript};
use super::test_ui::load_ui as load_xml;
/// The bar on a 1024×768 screen. 1024 wide is deliberate: the 1024-wide bar then spans x 0..1024,
/// so every ref offset below is also an absolute screen coordinate.
fn harness(extra: &[&str]) -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, "Interface\\FrameXML\\Cooldown.xml");
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
for f in extra {
load_xml(&s, f);
}
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s
}
/// The meter's own paint quad (its `MainMenuBarPerformanceBar` texture) and where it sits in the
/// painter's order.
fn bar_quad(s: &mut UiScript) -> (usize, [f32; 4], Option<[f32; 4]>) {
s.resolve();
let quads = s.extract();
let i = quads
.iter()
.position(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.contains("UI-MainMenuBar-PerformanceBar"))
})
.expect("the performance bar's texture quad");
let rect = quads[i].rect.expect("a resolved rect");
let color = match &quads[i].content {
QuadContent::Texture { color, .. } => *color,
_ => unreachable!(),
};
(i, [rect.left, rect.bottom, rect.right, rect.top], color)
}
/// ref-MainMenuBar.xml l.344-364: a 16×64 frame at the bar's BOTTOMRIGHT +(-227,-10), carrying a
/// 20×66 texture off its TOPRIGHT. On a 1024-wide bar that is x 781..797 for the frame and
/// x 777..797 for the texture — the empty recess between the last micro button (which ends at 763)
/// and the bag cluster. Both hang below the bar's own bottom, which is what drops the texture's
/// grey column into the slot rather than floating it above.
#[test]
fn the_meter_sits_in_the_bar_recess_the_reference_leaves_for_it() {
let mut s = harness(&[]);
s.resolve();
let (left, bottom, w, h) = s
.eval::<(f64, f64, f64, f64)>(
"return MainMenuBarPerformanceBarFrame:GetLeft(), MainMenuBarPerformanceBarFrame:GetBottom(), \
MainMenuBarPerformanceBarFrame:GetWidth(), MainMenuBarPerformanceBarFrame:GetHeight()",
)
.unwrap();
assert_eq!((w, h), (16.0, 64.0), "frame size");
assert_eq!(left, 781.0, "1024 − 227 − 16");
assert_eq!(bottom, -10.0, "hangs 10 below the bar's bottom");
// The texture overspills the frame: 20 wide off the TOPRIGHT ⇒ 4 further left, and 66 tall from
// the frame's top (54) ⇒ 2 further down.
let (_, rect, _) = bar_quad(&mut s);
assert_eq!(
rect,
[777.0, -12.0, 797.0, 54.0],
"[left, bottom, right, top]"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// `frameStrata="LOW"` is the mechanism, not decoration: the bar art is a transparent WINDOW over
/// this slot, so the meter has to paint UNDERNEATH it. At the default MEDIUM the 20-wide texture
/// would paint over the metal surround that frames the 10-wide slot.
#[test]
fn the_meter_paints_under_the_bar_art_it_shows_through() {
let mut s = harness(&[]);
let (meter, _, _) = bar_quad(&mut s);
let quads = s.extract();
let art = quads
.iter()
.position(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.contains("UI-MainMenuBar-Dwarf"))
})
.expect("the bar's own dwarf art");
assert!(
meter < art,
"the meter must draw before (under) the bar art — it is seen through the art's \
transparent slot, not over it"
);
// And the hover button is above everything, the ref's own HIGH-strata split: the LOW frame
// beneath the art can't take the mouse itself.
assert_eq!(
s.eval::<String>("return MainMenuBarPerformanceBarFrameButton:GetFrameStrata()")
.unwrap(),
"HIGH"
);
assert_eq!(
s.eval::<String>("return MainMenuBarPerformanceBarFrame:GetFrameStrata()")
.unwrap(),
"LOW",
"the parent's LOW must not have been dragged up by its HIGH child"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// ref-MainMenuBar.xml l.375-392: the poll runs every `PERFORMANCEBAR_UPDATE_INTERVAL` seconds and
/// tints the bar green under 300 ms, yellow to 600, red beyond. 1.12 never scales the bar's HEIGHT
/// — color is the entire readout, which is why this test asserts the tint and the rect separately.
#[test]
fn the_meter_tints_by_latency_on_the_reference_thresholds() {
let mut s = harness(&[]);
// The first tick polls immediately (updateInterval starts at 0), so an unmeasured connection
// reads 0 ms and shows green rather than sitting untinted grey.
s.tick(0.016);
let (_, _, color) = bar_quad(&mut s);
assert_eq!(color, Some([0.0, 1.0, 0.0, 1.0]), "0 ms ⇒ green");
for (latency, want, band) in [
(299, [0.0, 1.0, 0.0, 1.0], "under LOW ⇒ green"),
(301, [1.0, 1.0, 0.0, 1.0], "past LOW ⇒ yellow"),
(601, [1.0, 0.0, 0.0, 1.0], "past MEDIUM ⇒ red"),
] {
s.set_latency_ms(Some(latency));
poll_beat(&mut s);
let (_, _, color) = bar_quad(&mut s);
assert_eq!(color, Some(want), "{latency} ms: {band}");
}
// A latency that crosses a threshold does NOT repaint before the next poll beat — the ref's
// own 10 s cadence, and the reason a 30 s ping's jitter can't strobe the bar.
s.set_latency_ms(Some(0));
s.tick(1.0);
let (_, _, color) = bar_quad(&mut s);
assert_eq!(
color,
Some([1.0, 0.0, 0.0, 1.0]),
"still red until the beat"
);
poll_beat(&mut s);
let (_, _, color) = bar_quad(&mut s);
assert_eq!(
color,
Some([0.0, 1.0, 0.0, 1.0]),
"the beat repaints it green"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The drawn colour of the tooltip's explanation line — found by its TEXT, so it can't be confused
/// with the label above it. `None` if that line isn't being painted at all.
fn newbie_line_color(s: &mut UiScript) -> Option<[f32; 4]> {
let want = s
.eval::<String>("return NEWBIE_TOOLTIP_LATENCY")
.expect("the string is declared with the meter");
s.resolve();
s.extract().iter().find_map(|q| match &q.content {
QuadContent::Text {
text: Some(t),
color,
..
} if *t == want => Some(*color),
_ => None,
})?
}
/// Advance exactly one poll beat. The ref's OnUpdate spends the whole remaining interval on one
/// tick and only polls on the tick that finds it non-positive, so a beat is always two ticks longer
/// than the interval: the first drains it, the second crosses zero and re-arms it at 10.
fn poll_beat(s: &mut UiScript) {
s.tick(11.0);
s.tick(11.0);
}
/// `GetNetStats()` is the whole seam: the app pushes the averaged RTT, the meter reads it. The two
/// bandwidth returns are the named gap (benilla tallies no throughput) and must stay NUMBERS, so
/// arithmetic on them can't error.
#[test]
fn get_net_stats_reports_the_pushed_latency() {
let mut s = harness(&[]);
assert_eq!(
s.eval::<(f64, f64, f64)>("return GetNetStats()").unwrap(),
(0.0, 0.0, 0.0),
"nothing measured yet"
);
s.set_latency_ms(Some(42));
assert_eq!(
s.eval::<(f64, f64, f64)>("return GetNetStats()").unwrap(),
(0.0, 0.0, 42.0)
);
// Back to unmeasured (a disconnect clears the app's ring) ⇒ 0, the reference's own reading for
// a connection it has no sample for.
s.set_latency_ms(None);
assert_eq!(
s.eval::<f64>("local _, _, ms = GetNetStats() return ms")
.unwrap(),
0.0
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The hover tooltip (ref l.395-406): the HIGH-strata button owns the mouse over the meter, and its
/// tooltip reads the live number. The held-open plate re-reads it on the same 10 s beat.
///
/// The plate is the ref's TWO-line `GameTooltip_AddNewbieTip` — detailed tips ship ON in 1.12
/// (`SHOW_NEWBIE_TIPS = "1"`, ref UIOptionsFrame.lua l.100), so the explanation under the number is
/// the DEFAULT hover, not an opt-in. Decision 0661; this test is the guard on that, because a
/// regression here (the helper's branch flipping, the string going missing) shows up only as a
/// tooltip that is quietly one line short.
#[test]
fn hovering_the_meter_shows_the_live_latency() {
let mut s = harness(&[r"Interface\FrameXML\UIParent.xml"]);
s.set_latency_ms(Some(42));
// 1.12 ships detailed tips ON — `SHOW_NEWBIE_TIPS = "1"` is UIOptionsFrame_Init's (ref
// UIOptionsFrame.lua l.100; ours sits in OptionsFrame.xml's uvar block, 1968), and a harness
// without the options file says so itself, the way the reference's tooltip would read it.
s.run("SHOW_NEWBIE_TIPS = \"1\"").unwrap();
s.resolve();
assert_eq!(
s.hit_test_name(789.0, 20.0).as_deref(),
Some("MainMenuBarPerformanceBarFrameButton"),
"the button over the meter takes the mouse"
);
s.run("this = MainMenuBarPerformanceBarFrameButton MainMenuBarPerformanceBarFrameButton:GetScript(\"OnEnter\")()")
.unwrap();
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap(),
"Latency: 42ms"
);
assert_eq!(
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
2,
"the number, then the explanation — the ref's newbie branch"
);
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft2:GetText()")
.unwrap(),
s.eval::<String>("return NEWBIE_TOOLTIP_LATENCY").unwrap(),
"line 2 is the ref's own NEWBIE_TOOLTIP_LATENCY, verbatim"
);
// The newbie branch seats the plate at the default screen corner, NOT beside the meter — the
// `default` flag GameTooltip_SetDefaultAnchor stamps is the observable half of that.
assert_eq!(
s.eval::<i64>("return GameTooltip.default").unwrap(),
1,
"the default-corner anchor, not ANCHOR_RIGHT off the button"
);
// …and the explanation draws in NORMAL_FONT_COLOR, the gold that separates it from the white
// label above it.
assert_eq!(
newbie_line_color(&mut s),
Some([1.0, 0.82, 0.0, 1.0]),
"NORMAL_FONT_COLOR (Fonts.xml l.37)"
);
// Still hovering, and the latency moved: the next poll beat rewrites the plate in place.
s.set_latency_ms(Some(7));
poll_beat(&mut s);
assert_eq!(
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
.unwrap(),
"Latency: 7ms"
);
assert_eq!(
s.eval::<i64>("return GameTooltip:NumLines()").unwrap(),
2,
"the refresh rebuilds BOTH lines — a held-open plate never decays to one"
);
s.run("this = MainMenuBarPerformanceBarFrameButton MainMenuBarPerformanceBarFrameButton:GetScript(\"OnLeave\")()").unwrap();
assert!(
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"leaving hides the plate"
);
// …and the poll no longer touches it.
s.set_latency_ms(Some(999));
poll_beat(&mut s);
assert!(
!s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"an unhovered meter never re-opens the tooltip"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The meter is a frame slot in the bar's own child list — a regression guard on the frame count
/// the bar's end-to-end test pins, kept here so the two move together.
#[test]
fn the_meter_adds_its_two_frames_to_the_bar() {
let s = harness(&[]);
for name in [
"MainMenuBarPerformanceBarFrame",
"MainMenuBarPerformanceBarFrameButton",
] {
assert!(
s.eval::<bool>(&format!("return {name} ~= nil")).unwrap(),
"{name} must exist"
);
}
// The button is the frame's child (the ref's `parent=` attribute, expressed as nesting).
assert_eq!(
s.eval::<String>("return MainMenuBarPerformanceBarFrameButton:GetParent():GetName()")
.unwrap(),
"MainMenuBarPerformanceBarFrame"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}