Files
benilla-octo/crates/benilla-ui/src/script/tests/anchors.rs
T
SamandClaude Fable 5 9662553528 addons come alive: Bagnon draws, hovers and clicks
- Lua 5.0's generic-for and nested long strings are restored in a forked VM — 60 corpus addons stopped dying on one deleted opcode
- a day of demand-driven carving takes the 218-addon corpus from 24 addons surviving a session start to 114, with 144 loading without a single error
- Bagnon draws a real bag and answers real gestures — hover tooltip, pickup, use, drag — because our frames wear the reference's names, and the reference's own ContainerFrame.lua runs off the player's install instead of being transcribed
- the physics broad phase leaves: nothing in the client reads a contact, and it was measured at up to 41% of main-thread samples in real sessions

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

238 lines
8.9 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.
//! SetPoint + resolve → GetWidth/GetHeight + rect.
use super::common::script;
use crate::layout::Rect;
// Regression: an *explicit* `nil` relativeTo must still consume its argument slot, so the
// relativePoint + offsets that follow line up. The `SetPoint("P", nil, "P", x, y)` form is the
// common FrameXML idiom for "anchor to the screen at an offset"; a leading nil that failed to
// advance the cursor silently dropped the offsets (screen-anchored frames pinned to the corner).
#[test]
fn setpoint_explicit_nil_relative_to_keeps_offsets() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local f = CreateFrame("Frame", "Nil")
f:SetPoint("TOPLEFT", nil, "TOPLEFT", 40, -40)
f:SetSize(300, 200)
"#,
)
.unwrap();
s.resolve();
let rect = s
.extract()
.iter()
.find_map(|q| match q.target {
crate::order::ZTarget::Frame(_) => q.rect,
_ => None,
})
.expect("resolved frame rect");
// screen [0,0,600,800], TOPLEFT+(40,-40): left 40, top 560, size 300×200.
assert_eq!(rect, Rect::new(360.0, 40.0, 560.0, 340.0));
}
#[test]
fn setpoint_resolve_size_and_rect() {
let mut s = script();
s.set_screen_size(800.0, 600.0); // screen rect [bottom 0, left 0, top 600, right 800]
s.run(
r#"
local f = CreateFrame("Frame", "Sized")
f:SetPoint("TOPLEFT", 10, -5) -- relativeTo = screen (default), relativePoint = TOPLEFT
f:SetSize(200, 50)
"#,
)
.unwrap();
s.resolve();
let (w, h): (f32, f32) = s
.eval("return Sized:GetWidth(), Sized:GetHeight()")
.unwrap();
assert_eq!(w, 200.0);
assert_eq!(h, 50.0);
// Hand-computed (layout.md oracle): TOPLEFT anchored to screen [0,0,600,800] at (10,-5), size
// 200×50 → Rect(bottom 545, left 10, top 595, right 210).
let quads = s.extract();
let frame_rect = quads
.iter()
.find_map(|q| match q.target {
crate::order::ZTarget::Frame(_) => q.rect,
_ => None,
})
.expect("resolved frame rect");
assert_eq!(frame_rect, Rect::new(545.0, 10.0, 595.0, 210.0));
}
#[test]
fn getwidth_falls_back_to_explicit_size_before_resolve() {
let s = script();
// No SetPoint ⇒ unresolvable; GetWidth returns the explicit SetSize value.
let w: f32 = s
.eval(r#"local f = CreateFrame("Frame"); f:SetWidth(123); return f:GetWidth()"#)
.unwrap();
assert_eq!(w, 123.0);
}
// A *named* relativeTo that doesn't resolve falls back to the parent/owner — the client's
// behavior — but must SAY so: the silent version misdirected ItemTextFrame's scrollbar track
// onto the parchment (an XML forward reference; anchors resolve at SetPoint time). Both the
// frame and the region SetPoint paths warn.
#[test]
fn setpoint_unresolved_name_warns() {
let mut s = script();
s.run(
r#"
local f = CreateFrame("Frame", "Orphan")
f:SetPoint("TOPLEFT", "NoSuchFrame", "TOPLEFT", 0, 0)
local t = f:CreateTexture(nil, "ARTWORK")
t:SetPoint("TOPRIGHT", "NoSuchRegion")
"#,
)
.unwrap();
let w = s.take_warnings();
assert!(
w.iter()
.any(|w| w.contains("Orphan") && w.contains("NoSuchFrame")),
"frame path: {w:#?}"
);
assert!(
w.iter()
.any(|w| w.contains("Orphan") && w.contains("NoSuchRegion")),
"region path: {w:#?}"
);
// A resolvable name stays silent.
s.run(r#"CreateFrame("Frame", "Target"); Orphan:SetPoint("BOTTOMLEFT", "Target", "TOPLEFT")"#)
.unwrap();
assert!(s.take_warnings().is_empty());
}
/// **A region whose OWNER has no rect still resolves from its own anchors.**
///
/// `layout.rs`'s region pass used to `continue` when `resolved` held nothing for the owner frame.
/// But the owner's rect is only the *fallback* for axes the region's own anchors do not pin — a
/// region anchored fully to some other frame needs nothing from its owner, and the reference
/// resolves it. Skipping made an ordinary addon shape silently invisible: a bare container frame
/// (`CreateFrame("Frame", n, parent)` with no size and no `SetPoint`) holding a region anchored
/// elsewhere.
///
/// Found by reproducing the director's MapCoords report. Its world-map readout computed the right
/// string every single frame — `Cursor Coords: … Player Coords: …` — and was never given a
/// position, with no error raised anywhere. Three of its four frames are this shape.
///
/// The control matters as much as the subject: a SIZED owner must be unaffected, or the fix has
/// traded one wrong layout for another.
#[test]
fn a_region_resolves_even_when_its_owner_frame_has_no_rect() {
let mut s = crate::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run(
r#"
host = CreateFrame("Frame", "AnchorHost")
host:SetWidth(400) host:SetHeight(200)
host:SetPoint("BOTTOMLEFT", 100, 50)
-- the shape that was invisible: owner with NO size and NO SetPoint
bare = CreateFrame("Frame", "BareOwner")
mark = bare:CreateTexture("BareMark", "ARTWORK")
mark:SetWidth(20) mark:SetHeight(10)
mark:SetPoint("BOTTOMLEFT", host, "BOTTOMLEFT", 5, 7)
-- control: identical region, owner that DOES resolve
sized = CreateFrame("Frame", "SizedOwner")
sized:SetWidth(10) sized:SetHeight(10) sized:SetPoint("CENTER")
ctl = sized:CreateTexture("SizedMark", "ARTWORK")
ctl:SetWidth(20) ctl:SetHeight(10)
ctl:SetPoint("BOTTOMLEFT", host, "BOTTOMLEFT", 5, 7)
"#,
)
.unwrap();
s.resolve();
// The owner genuinely has no rect — that part is correct and must stay true.
assert_eq!(
s.eval::<Option<f32>>("return BareOwner:GetLeft()").unwrap(),
None,
"an unpositioned frame has no rect; the fix must not invent one for it"
);
// ...and its region resolves anyway, from its own anchor: host's BOTTOMLEFT (100,50) + (5,7).
assert_eq!(
s.eval::<Option<f32>>("return BareMark:GetLeft()").unwrap(),
Some(105.0),
"a fully-anchored region needs nothing from its owner"
);
assert_eq!(
s.eval::<Option<f32>>("return BareMark:GetBottom()")
.unwrap(),
Some(57.0)
);
// The control is unmoved.
assert_eq!(
s.eval::<Option<f32>>("return SizedMark:GetLeft()").unwrap(),
Some(105.0),
"a sized owner's region must be unaffected by the fix"
);
}
/// **Geometry answers inside the same call stack that moved it** — the seam that killed 97 addons.
///
/// A handler creates a frame, anchors it, shows it and then measures it, all before any resolve
/// pass can run. `Dewdrop-2.0.lua` — embedded in ~65 corpus addons — does exactly this in its menu
/// `Open`: `local left = frame:GetLeft()` (l.1942) then `curX - left - width/2` (l.1960). Our
/// getters read a cache filled by the per-frame `resolve()`, so `left` was nil and the arithmetic
/// died. **97 of the 108 addons that drew and then raised on being touched died on that one line**,
/// with no missing verb anywhere.
///
/// The readers settle the graph on demand now. The second half of the test is the one that keeps
/// it honest: a *further* move inside the same stack must also be visible, or the fix is just a
/// one-shot warm-up.
#[test]
fn geometry_answers_within_the_call_stack_that_moved_it() {
let mut s = crate::script::UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// Dewdrop's shape, in one eval — no resolve() between the writes and the read.
let (left, bottom): (Option<f32>, Option<f32>) = s
.eval(
r#"
local f = CreateFrame("Frame", "MenuLike", UIParent)
f:SetWidth(100) f:SetHeight(50)
f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 40, 60)
f:Show()
return f:GetLeft(), f:GetBottom()
"#,
)
.unwrap();
assert_eq!(
(left, bottom),
(Some(40.0), Some(60.0)),
"a frame must report its geometry in the stack that anchored it"
);
// ...and a LATER move in the same stack is visible too — not a stale first answer.
let moved: Option<f32> = s
.eval(
r#"
MenuLike:ClearAllPoints()
MenuLike:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 200, 60)
return MenuLike:GetLeft()
"#,
)
.unwrap();
assert_eq!(moved, Some(200.0), "the settle must re-run after each move");
// Width/height and the centre reader take the same path.
assert_eq!(
s.eval::<(f32, f32)>("return MenuLike:GetWidth(), MenuLike:GetHeight()")
.unwrap(),
(100.0, 50.0)
);
assert_eq!(
s.eval::<(Option<f32>, Option<f32>)>("return MenuLike:GetCenter()")
.unwrap(),
(Some(250.0), Some(85.0))
);
}