Files
benilla-octo/crates/benilla-ui/src/script/tests/measure.rs
T
SamandClaude Fable 5 820c406049 the interface labels announce their writes, and the idle tick goes quiet
- a text write names its own label, so the per-frame measure sweep drains a short ledger instead of hashing every font string twice — ~0.3 ms a frame at the Stormwind gates, gone
- the idle interface tick stops re-writing settled state: buff durations and the weapon-enchant frame write only on change, and the fade and unit-popup drivers park until woken
- the interface extract skips a hidden region before cloning its row, not after
- WOW_UPLOAD_BUDGET=<MB> brackets the arrival burst's upload pacing — a lever for measuring world entry, not a fix yet

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

573 lines
24 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 measure round-trip + frame→region anchors, end to end.
use super::common::script;
use crate::script::*;
/// The measure round-trip + frame→region anchors, end to end: a height-less FontString reports a
/// [`MeasureRequest`]; the host answer becomes its implicit size; and a FRAME anchored to that
/// FontString by name binds to its measured bottom in resolve's second round — the real gossip
/// structure (option rows hang off the greeting's laid-out height, ref-GossipFrame.xml l.258-261).
#[test]
fn measured_fontstring_height_feeds_frame_anchors() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
local g = w:CreateFontString("Greeting", "ARTWORK")
g:SetText("a long greeting that wraps")
g:SetWidth(270)
g:SetPoint("TOPLEFT", 33, -91)
local row = CreateFrame("Button", "Row1")
row:SetSize(300, 16)
row:SetPoint("TOPLEFT", "Greeting", "BOTTOMLEFT", -10, -20)
"#,
)
.unwrap();
s.resolve();
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1, "one height-less FontString wants measuring");
let r = &reqs[0];
assert_eq!(r.wrap_width, Some(270.0));
assert_eq!(r.text, "a long greeting that wraps");
// Host answers: 3 wrapped lines of 16px ⇒ 48 tall.
s.set_measured_text_unwrapped(&[(r.id, 250.0, 48.0, r.key)]);
s.resolve();
assert!(
s.fontstrings_needing_measure().is_empty(),
"cache key satisfied — no re-measure on a quiet frame"
);
let quads = s.extract();
// Greeting: TOPLEFT of Win +(33,-91) ⇒ top 409 (win top 500), measured height 48 ⇒ bottom 361.
let g = quads
.iter()
.find_map(|q| match &q.content {
QuadContent::Text { text: Some(t), .. } if t.starts_with("a long") => q.rect,
_ => None,
})
.expect("greeting rect");
assert_eq!((g.top, g.bottom, g.left), (409.0, 361.0, 33.0));
// Row1: TOPLEFT → Greeting BOTTOMLEFT +(-10,-20) ⇒ top 341, left 23 — bound in round 2.
let row = quads
.iter()
.find_map(|q| match q.target {
crate::order::ZTarget::Frame(_) => q.rect.filter(|r| (r.width() - 300.0).abs() < 0.1),
_ => None,
})
.expect("row rect");
assert_eq!((row.top, row.left), (341.0, 23.0));
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A stored measure serves ONLY the current text: after `SetText` to a DIFFERENT string, the old
/// string's width must not leak through `GetWidth`/`GetStringWidth` — the whisper-header cursor
/// bug: the chat edit box ran `SetTextInsets(15 + header:GetWidth(), …)` on a type switch
/// (Say → "Tell Alice:") and its `w > 1` settle gate passed with the PREVIOUS header's measure,
/// latching the caret inside the new header. The metric read is key-checked
/// ([`crate::script::RegionData`]'s measure key): a changed string reads 0 until its own measure
/// lands, so poll-until-nonzero callers converge on the RIGHT width.
#[test]
fn a_changed_text_reads_zero_until_its_own_measure_lands() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
local h = w:CreateFontString("Header", "ARTWORK")
h:SetText("Say: ")
h:SetPoint("LEFT", 13, 0)
"#,
)
.unwrap();
s.resolve();
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1);
let r = reqs[0].clone();
assert_eq!(r.text, "Say: ");
s.set_measured_text_unwrapped(&[(r.id, 30.0, 16.0, r.key)]);
assert_eq!(
s.eval::<f64>(r#"return getglobal("Header"):GetWidth()"#)
.unwrap(),
30.0
);
// The type switch: same region, new text. The old measure must NOT serve.
s.run(r#"getglobal("Header"):SetText("Tell Alice: ")"#)
.unwrap();
assert_eq!(
s.eval::<f64>(r#"return getglobal("Header"):GetWidth()"#)
.unwrap(),
0.0,
"a stale measure must not serve for changed text"
);
// The round-trip re-measures the new string; the true width serves.
s.resolve();
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1, "the changed text wants re-measuring");
let r2 = reqs[0].clone();
assert_eq!(r2.text, "Tell Alice: ");
assert_ne!(r2.key, r.key, "the key tracks the text");
s.set_measured_text_unwrapped(&[(r2.id, 72.0, 16.0, r2.key)]);
assert_eq!(
s.eval::<f64>(r#"return getglobal("Header"):GetWidth()"#)
.unwrap(),
72.0
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A zero-WIDTH FontString with an explicit height auto-sizes its width to the measured line —
/// the reference label idiom (`<Size x="0" y="16"/>` anchored TOPRIGHT→TOPLEFT: MailFrame's
/// "From:"/"Subject:" labels end at the anchor and grow leftward, and the value string anchored
/// LEFT→label RIGHT starts past them, never overlapping). Gating the measure on height alone
/// left these rects zero-width — "From" and the sender name painted on top of each other.
#[test]
fn zero_width_fontstring_autosizes_to_its_line() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(384, 512)
local label = w:CreateFontString("FromLabel", "ARTWORK")
label:SetText("From:")
label:SetSize(0, 16)
label:SetPoint("TOPRIGHT", "Win", "TOPLEFT", 114, -45)
local value = w:CreateFontString("FromValue", "ARTWORK")
value:SetText("Thrall")
value:SetSize(110, 0)
value:SetPoint("LEFT", "FromLabel", "RIGHT", 5, 0)
"#,
)
.unwrap();
s.resolve();
let reqs = s.fontstrings_needing_measure();
let label_req = reqs
.iter()
.find(|r| r.text == "From:")
.expect("the zero-width label asks for a measure");
assert_eq!(
label_req.wrap_width, None,
"width 0 = unwrapped single line"
);
let value_req = reqs.iter().find(|r| r.text == "Thrall").expect("value");
let answers = [
(label_req.id, 40.0, 16.0, label_req.key),
(value_req.id, 45.0, 16.0, value_req.key),
];
s.set_measured_text_unwrapped(&answers);
s.resolve();
// Label: right edge pinned at Win left +114, measured width 40 ⇒ [74, 114].
let (l_left, l_right, l_w): (f32, f32, f32) = s
.eval("return FromLabel:GetLeft(), FromLabel:GetRight(), FromLabel:GetStringWidth()")
.unwrap();
assert_eq!((l_left, l_right, l_w), (74.0, 114.0, 40.0));
// Value: starts 5 past the label's real right edge — no overlap.
let v_left: f32 = s.eval("return FromValue:GetLeft()").unwrap();
assert_eq!(v_left, 119.0);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The frame-scale seam (0219 §2's divergence, closed): a `SetScale`'d frame's quads carry its
/// `effective_scale` out to the renderer — the rect is already scale-multiplied by layout, and
/// the renderer needs the same factor for the GLYPH raster size — and the measure round-trip
/// rides it too: the request names the scale (the host measures at the drawn size), the cache
/// key holds it (a re-scale re-measures), all before any quad draws stale text.
#[test]
fn frame_scale_rides_the_quads_and_the_measure_key() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(400, 300)
w:SetScale(0.8)
local t = w:CreateFontString("ScaledLabel", "ARTWORK")
t:SetText("Options")
t:SetPoint("TOPLEFT", 10, -10)
"#,
)
.unwrap();
s.resolve();
// The measure request carries the owner's effective scale.
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1);
let r = &reqs[0];
assert_eq!(r.scale, 0.8, "request names the drawn-size scale");
let old_key = r.key;
let (id, key) = (r.id, r.key);
s.set_measured_text_unwrapped(&[(id, 50.0, 16.0, key)]);
s.resolve();
assert!(s.fontstrings_needing_measure().is_empty(), "cache warm");
// Every quad of the scaled frame carries the scale — frame slot and region alike.
let quads = s.extract();
let label = quads
.iter()
.find(|q| matches!(&q.content, QuadContent::Text { text: Some(t), .. } if t == "Options"))
.expect("label quad");
assert_eq!(label.scale, 0.8);
// The label's rect is scale-multiplied by layout (width 50 × 0.8 = 40) — the quad's scale is
// for the glyph raster, not a second rect multiply.
let lr = label.rect.expect("label rect");
assert!((lr.width() - 40.0).abs() < 0.01, "width {}", lr.width());
// A re-scale invalidates the measure key: the same text re-measures at the new drawn size.
s.run(r#"Win:SetScale(1.25)"#).unwrap();
s.resolve();
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1, "SetScale re-measures");
assert_eq!(reqs[0].scale, 1.25);
assert_ne!(reqs[0].key, old_key, "key carries the scale");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The host's raster-environment invalidation ([`UiScript::invalidate_text_measures`]): a warm
/// FontString measure cache re-requests after the call — same content, same key (the key hashes
/// content, not the host's seam scale; the STALENESS is the host's to declare, which is the whole
/// seam). This is the engine half of the fullscreen-truncation fix: measures answered under one
/// seam scale kept satisfying fit tests run under another, and the ellipsis ate fitting text
/// ("Contr...", director 2026-08-04; reproduced end-to-end by `WOW_RESIZE`).
#[test]
fn invalidate_text_measures_reopens_the_round_trip() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local w = CreateFrame("Frame", "Win")
w:SetPoint("TOPLEFT", 0, -100); w:SetSize(400, 300)
local t = w:CreateFontString("Label", "ARTWORK")
t:SetText("Keybindings")
t:SetPoint("TOPLEFT", 10, -10)
"#,
)
.unwrap();
s.resolve();
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1);
let (id, key) = (reqs[0].id, reqs[0].key);
s.set_measured_text_unwrapped(&[(id, 74.0, 12.0, key)]);
s.resolve();
assert!(s.fontstrings_needing_measure().is_empty(), "cache warm");
assert_eq!(
s.eval::<f64>("return Label:GetStringWidth()").unwrap(),
74.0
);
s.invalidate_text_measures();
// The measured extent is gone (GetStringWidth back to its unmeasured 0)…
assert_eq!(
s.eval::<f64>("return Label:GetStringWidth()").unwrap(),
0.0,
"stale measure dropped, not served"
);
// …and the round-trip reopens with the SAME content key — the request is the host's cue to
// re-measure under its new environment; nothing about the region itself changed.
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1, "re-requests after invalidation");
assert_eq!(
reqs[0].key, key,
"content key unchanged — only the answer was stale"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// **`GetStringWidth` is the NATURAL width — never the box, never the wrapped extent** (wow-re
/// `fontstring-overflow.md`, "The measurement echo": the reference re-measures the raw text with no
/// wrap constraint, so "Lua sees the natural, unwrapped, un-truncated width at the DRAWN size").
///
/// This is the distinction whose absence made the reference's own `PanelTemplates_TabResize` a
/// feedback loop in this engine (decision 0997): the kit sized a tab from `GetStringWidth`, set that
/// width on the label, and read its own output back next frame — a tab that changed width every
/// single frame. Three separate things are pinned here because each one was wrong on its own:
///
/// 1. a region with a DECLARED width still gets a measure request (it used to be skipped: no
/// auto-sized axis, no request — so a constrained label could never learn its natural width);
/// 2. `GetStringWidth` answers with the natural width, while `GetWidth` keeps echoing the laid-out
/// extent that auto-size depends on;
/// 3. the answer does not move when the declared width does.
#[test]
fn get_string_width_is_the_natural_width_not_the_box() {
let mut s = UiScript::new().unwrap();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
Win = CreateFrame("Frame")
Win:SetWidth(400) Win:SetHeight(300)
Win:SetPoint("TOPLEFT", 0, 0)
Label = Win:CreateFontString("Label")
Label:SetText("a string that is wider than its box")
Label:SetWidth(60) Label:SetHeight(13)
Label:SetPoint("TOPLEFT", 10, -10)
"#,
)
.unwrap();
s.resolve();
// 1 · Both axes are declared, so nothing about the LAYOUT needs a measure — and the request is
// issued anyway, because `GetStringWidth` has no other way to learn the natural width.
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1, "a fully-sized FontString still measures");
let r = &reqs[0];
assert_eq!(r.wrap_width, Some(60.0), "the request carries the box");
// The host answers with both: laid out inside 60, natural 200.
s.set_measured_text(&[(r.id, 60.0, 39.0, 200.0, r.key)]);
s.resolve();
// 2 · The two getters answer different questions.
assert_eq!(
s.eval::<f64>("return Label:GetStringWidth()").unwrap(),
200.0,
"GetStringWidth is the natural, unwrapped extent"
);
assert_eq!(
s.eval::<f64>("return Label:GetWidth()").unwrap(),
60.0,
"GetWidth is the laid-out box the auto-size path depends on"
);
// 3 · Narrowing the box re-opens the round trip (the key carries the wrap) and, once answered,
// leaves the natural width exactly where it was. This is the loop that used to close.
s.run("Label:SetWidth(30)").unwrap();
s.resolve();
let reqs = s.fontstrings_needing_measure();
assert_eq!(reqs.len(), 1, "a new box is a new measure key");
s.set_measured_text(&[(reqs[0].id, 30.0, 91.0, 200.0, reqs[0].key)]);
s.resolve();
assert_eq!(
s.eval::<f64>("return Label:GetStringWidth()").unwrap(),
200.0,
"the string did not change, so neither did its natural width"
);
}
// ─────────────────────────────────────────────────────────────────────────────────────────────
// The SYNCHRONOUS measure — a host font engine installed into the VM ([`TextMeasure`])
// ─────────────────────────────────────────────────────────────────────────────────────────────
/// A stand-in font engine: every character is `PER_CHAR` wide and every line `LINE_H` tall, wrapped
/// greedily at the request's wrap width. Deliberately not the real one — what these tests are about
/// is *when* the answer arrives, and a fake with arithmetic anyone can do in their head is what
/// makes the assertions readable. The real engine's own numbers are the app's to test.
struct BlockFont;
const PER_CHAR: f32 = 7.0;
const LINE_H: f32 = 14.0;
impl TextMeasure for BlockFont {
fn measure(&mut self, req: &MeasureRequest) -> (f32, f32, f32) {
let natural = req.text.chars().count() as f32 * PER_CHAR;
match req.wrap_width {
Some(w) if w > 0.0 && natural > w => {
let rows = (natural / w).ceil();
(w, rows * LINE_H, natural)
}
_ => (natural, LINE_H, natural),
}
}
}
/// **The director's bug, at the engine.** `SetText` then `GetStringWidth` in ONE tick must answer
/// with the string's width — the shape `Bagnon_Forever/database/ui.lua:58-59` writes over every
/// saved character (`button:SetText(player)` then `button:GetTextWidth() + 40`), and the shape the
/// reference's own `SmallMoneyFrame` writes (MoneyFrame.lua l.202).
///
/// Without an engine installed this is 0 until the host's round-trip lands at extract — a frame
/// later. That is what sized Bagnon's character dropdown to 40px with seven names hanging out of it.
#[test]
fn a_width_read_in_the_tick_that_set_the_text_is_not_zero() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_text_measurer(Box::new(BlockFont));
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
-- the corpus idiom: set, then measure, in one statement sequence
fs:SetText("Onewarrior")
Answer = fs:GetStringWidth()
"#,
)
.unwrap();
assert_eq!(
s.eval::<f32>("return Answer").unwrap(),
"Onewarrior".len() as f32 * PER_CHAR,
"GetStringWidth must answer inside the tick that set the text"
);
}
/// The same read WITHOUT an engine stays 0 and stays served by the round-trip — the pre-existing
/// behaviour, still exactly itself. Every engine test and every headless run is this VM, so the
/// synchronous path may never be a correctness precondition for anything.
#[test]
fn with_no_engine_installed_the_round_trip_is_still_the_only_answer() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetText("Onewarrior")
Answer = fs:GetStringWidth()
"#,
)
.unwrap();
assert_eq!(s.eval::<f32>("return Answer").unwrap(), 0.0);
assert_eq!(
s.fontstrings_needing_measure().len(),
1,
"and the request is still queued for the host"
);
}
/// A FontString's **box** is right in the frame its text was set, not the frame after: `resolve`
/// closes the round-trip itself when an engine is installed, so a caller reading `GetWidth()` (the
/// laid-out extent, not the natural one) gets this string's number and not the previous string's.
#[test]
fn resolve_closes_the_round_trip_when_an_engine_is_installed() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_text_measurer(Box::new(BlockFont));
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetWidth(35) -- a declared width ⇒ the two extents differ
fs:SetText("Onewarrior") -- 70 natural, wraps to 2 rows inside 35
"#,
)
.unwrap();
s.resolve();
assert!(
s.fontstrings_needing_measure().is_empty(),
"the engine answered its own request during resolve"
);
assert_eq!(
s.eval::<Vec<f32>>(
"return { Label:GetWidth(), Label:GetHeight(), Label:GetStringWidth() }"
)
.unwrap(),
vec![35.0, LINE_H * 2.0, 70.0],
"laid-out extent from the box, natural width unwrapped — the two must not be conflated"
);
}
/// The same-tick measure and the batch round-trip write the **same cache**, so asking twice costs
/// one measure — and a caller polling `GetStringWidth` every frame does not re-measure a string
/// that has not changed.
#[test]
fn a_synchronous_measure_satisfies_the_batch_request_too() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.set_text_measurer(Box::new(BlockFont));
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetText("hello")
Answer = fs:GetStringWidth()
"#,
)
.unwrap();
assert_eq!(s.eval::<f32>("return Answer").unwrap(), 5.0 * PER_CHAR);
assert!(
s.fontstrings_needing_measure().is_empty(),
"the Lua read already filled the cache the batch pass keys on"
);
// …and a new string invalidates it exactly as it always did.
s.run("Label:SetText('a longer label')").unwrap();
assert_eq!(
s.eval::<f32>("return Label:GetStringWidth()").unwrap(),
"a longer label".len() as f32 * PER_CHAR,
"a stale measure is not this string's metric"
);
}
/// The measure LEDGER's completeness guard: `fontstrings_needing_measure` no longer walks the
/// roster on the armed path — a measure-input write the ledger never hears about is a label
/// whose box silently never updates again. The exposing sequence is SETTLE (drain the ledger
/// dry), then one silent write, then sweep: enrolled, the sweep names exactly that region;
/// missed, it returns empty and this test is the red light. (Watched red with the `SetText`
/// enrollment removed — the earlier tests in this file stay green through that removal, because
/// their regions ride the ledger's own re-enrollment of still-unanswered requests.)
#[test]
fn a_settled_region_is_refound_after_each_measure_input_write() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local f = CreateFrame("Frame", "Host")
f:SetPoint("TOPLEFT"); f:SetSize(400, 300)
local fs = f:CreateFontString("Label", "ARTWORK")
fs:SetPoint("TOPLEFT")
fs:SetText("one")
"#,
)
.unwrap();
let writes: &[(&str, &str)] = &[
("SetText", "Label:SetText('two')"),
("SetFormattedText", "Label:SetFormattedText('n=%d', 3)"),
(
"SetFont",
r#"Label:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")"#,
),
("SetTextHeight", "Label:SetTextHeight(20)"),
("SetWidth (the wrap width)", "Label:SetWidth(123)"),
// The conservative lane: a scale write cannot name descendants one by one, so it must
// put the ledger back on the whole-roster walk — which refinds Label (scale is in the
// measure key).
("SetScale (conservative)", "Host:SetScale(2.0)"),
];
for (label, lua) in writes {
// Settle: answer whatever is pending, then prove the sweep is dry.
loop {
let reqs = s.fontstrings_needing_measure();
if reqs.is_empty() {
break;
}
let answers: Vec<_> = reqs.iter().map(|r| (r.id, 10.0, 16.0, r.key)).collect();
s.set_measured_text_unwrapped(&answers);
}
assert!(s.fontstrings_needing_measure().is_empty(), "settled");
s.run(lua).unwrap();
let reqs = s.fontstrings_needing_measure();
assert_eq!(
reqs.len(),
1,
"{label}: a silent measure-input write must re-surface its region to the sweep"
);
}
// The button label lane writes through its own binding (`Button:SetText` routes to the
// ButtonText region) — same guard, second write path.
s.run(
r#"
local b = CreateFrame("Button", "Btn")
b:SetPoint("TOPLEFT", 0, -50); b:SetSize(80, 22)
"#,
)
.unwrap();
while !{
let reqs = s.fontstrings_needing_measure();
let answers: Vec<_> = reqs.iter().map(|r| (r.id, 10.0, 16.0, r.key)).collect();
s.set_measured_text_unwrapped(&answers);
reqs.is_empty()
} {}
s.run("Btn:SetText('Go')").unwrap();
let reqs = s.fontstrings_needing_measure();
assert_eq!(
reqs.len(),
1,
"Button:SetText: the label region must be re-surfaced to the sweep"
);
assert_eq!(reqs[0].text, "Go");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}