dialogs come to the front, and the red line names what it needs

- dialogs carry the reference's toplevel bit, so the stack-split window opens above Bagnon instead of behind it
- the failure line fills its blank — "Requires Blacksmith Hammer", not "Requires" — and Opening casts stop printing a label the spell itself marks "no text"
- the pet bar's Attack/Follow/Stay tooltips name their keybinding, and typed text reaches the focused frame through the client's own delivery walk
- AtlasLoot parses again: our Lua keeps 5.0's compatibility semicolon

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-08-14 19:38:18 +07:00
co-authored by Claude Fable 5
parent 606a72bde5
commit 2c1ffca2ca
57 changed files with 2163 additions and 275 deletions
@@ -221,3 +221,65 @@ fn a_nested_long_string_parses_as_lua_50_does() {
.expect("nested long string must parse");
assert_eq!(v, "a [[b]] c");
}
/// **5.0's constructor compat-semicolon, restored** — the third dialect divergence (decision 1315).
///
/// Lua 5.0's `constructor()` opens its field loop with `testnext(ls, ';') /* compatibility
/// only */` — a Lua 4.0 leftover (4.0 separated a constructor's list part from its record part
/// with `;`) that silently eats ONE extra `;` per separator position inside a table constructor.
/// 5.1 deleted the line. AtlasLoot's `ButtonRegistry.lua` writes `Back_Title = AL["Factions"];;`
/// twenty times; the real client loads it (director-verified on the live 1.12.1 client), stock
/// 5.1 answered "unexpected symbol near ';'" and the addon's registry global never existed (B266).
#[test]
fn a_double_semicolon_inside_a_constructor_parses_as_lua_50_does() {
let s = script();
// The exact corpus shape: a record field, its `;` separator, and the compat extra.
let out: String = s
.eval(
r#"
local AL = { Factions = "Factions" }
local reg = {
["REP1"] = { Title = "The Aldor"; Back_Page = "REPMENU"; Back_Title = AL["Factions"];; };
}
return reg["REP1"].Back_Title
"#,
)
.expect("the ButtonRegistry shape must parse");
assert_eq!(out, "Factions");
// The compat skip runs at loop TOP, so a lone `;` before `}` — legal 5.0, an empty record
// part in 4.0 terms — is eaten the same way.
assert_eq!(s.eval::<i64>("local t = {;} return 1").unwrap(), 1);
// List context too: the separator may be `;` and the extra one is still absorbed.
let n: i64 = s.eval("local t = { 1;; 2 } return t[1] + t[2]").unwrap();
assert_eq!(n, 3);
}
/// **The restoration is exactly one semicolon wide, and constructor-only.** Both halves of the
/// boundary are the client's: 5.0 eats ONE extra `;` per position (a third dies in `listfield`),
/// and statement-level `;;` is rejected by 5.0 and 5.1 alike — the half wow-5875-re byte-verified
/// in the client's `luaL_loadbuffer` first (`system/ui/scratch/lua-dialect.md`).
#[test]
fn the_compat_semicolon_is_one_wide_and_statement_level_stays_rejected() {
let s = script();
let err = s
.eval::<i64>("local t = { 1;;; 2 } return t[1]")
.unwrap_err()
.to_string();
assert!(
err.contains("unexpected symbol"),
"a THIRD semicolon must still be a syntax error, as on 5.0: {err}"
);
let err = s
.eval::<i64>("local x = 1;; return x")
.unwrap_err()
.to_string();
assert!(
err.contains("unexpected symbol"),
"statement-level `;;` must stay rejected — that half is byte-verified: {err}"
);
}
+4 -7
View File
@@ -598,13 +598,10 @@ fn the_unfired_script_kinds_still_raise_rather_than_silently_accepting() {
s.run(r#"Raiser = CreateFrame("EditBox", "RaiserBox")"#)
.unwrap();
for name in [
// The FLAG exists (`EnableKeyboard`/`IsKeyboardEnabled`, 1252); what is still absent is the
// keyboard INDEX and the strata walk that would deliver a key to a frame carrying it, so
// these three stay unfired (wow-re `scripts-auto-enable.md` kinds 0/1). Being enabled is
// not being a handler — the carve is explicit that the two are separable.
"OnKeyDown",
"OnKeyUp",
"OnChar",
// `OnKeyDown`/`OnKeyUp`/`OnChar` are NOT in this list any more: decision 1319 built the
// delivery walk, so they are fired and therefore accepted — the rule this test enforces,
// applied in the other direction. Their law lives in `script::tests::keyboard`.
//
// Caret geometry is host-side; its four float args would all be zero.
"OnCursorChanged",
// 2.0's secure-frame system — no such slot exists in any 1.12 resolver.
@@ -0,0 +1,186 @@
//! The frame keyboard delivery law (decision 1319; wow-re
//! `system/ui/scratch/frame-key-script-delivery.md`, §5 trio — VERIFIED).
//!
//! Each test pins one clause that a plausible-but-wrong implementation gets backwards.
use super::common::script;
/// The bucket gate (§3.2): membership is the **keyboard-enabled flag**, not the presence of a
/// script. A Lua-created frame auto-enables nothing (the reference's `SetScript` doesn't either),
/// so its handler is unreachable until `EnableKeyboard(true)` puts it in the walk.
#[test]
fn a_key_script_alone_does_not_put_a_frame_in_the_walk() {
let mut s = script();
s.run(
r#"
got = nil
f = CreateFrame("Frame", "KbUnenabled")
f:SetScript("OnChar", function() got = arg1 end)
"#,
)
.unwrap();
assert!(
!s.char_input("7"),
"an unenabled frame is not in the bucket"
);
assert_eq!(s.eval::<Option<String>>("return got").unwrap(), None);
s.run("f:EnableKeyboard(true)").unwrap();
assert!(
s.char_input("7"),
"enabled: now in the walk, and it consumes"
);
assert_eq!(
s.eval::<Option<String>>("return got").unwrap().as_deref(),
Some("7")
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The existence gate's asymmetry (§3): a frame carrying **only** an `OnKeyUp` consumes every
/// key-down and runs nothing. This is the clause an implementation "tidies away" — and doing so
/// silently un-suppresses that frame's keybindings.
#[test]
fn only_an_onkeyup_still_swallows_the_key_down() {
let mut s = script();
s.run(
r#"
downs = 0
f = CreateFrame("Frame", "KbUpOnly")
f:EnableKeyboard(true)
f:SetScript("OnKeyUp", function() downs = downs + 1 end)
"#,
)
.unwrap();
assert!(s.key_input("ESCAPE"), "the OnKeyUp slot alone consumes");
assert_eq!(
s.eval::<i64>("return downs").unwrap(),
0,
"…and fires nothing"
);
// Neither slot: declines outright.
s.run("f:SetScript(\"OnKeyUp\", nil)").unwrap();
assert!(!s.key_input("ESCAPE"), "no key slot at all: declines");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The walk order (§2): **strata descending first**, and only then level. A LOW frame can never
/// take a key from a HIGH one, however high its level — the same "a raise never changes stratum"
/// shape the toplevel law has.
#[test]
fn the_walk_is_strata_then_level_then_registration() {
let mut s = script();
s.run(
r#"
winner = nil
function mk(name, strata, level)
local f = CreateFrame("Frame", name)
f:SetFrameStrata(strata)
f:SetFrameLevel(level)
f:EnableKeyboard(true)
f:SetScript("OnKeyDown", function() winner = name end)
return f
end
-- registered first, but the LOWEST stratum: must lose despite its huge level
mk("KbLow", "LOW", 99)
mk("KbHigh", "HIGH", 1)
"#,
)
.unwrap();
assert!(s.key_input("ESCAPE"));
assert_eq!(
s.eval::<String>("return winner").unwrap(),
"KbHigh",
"strata beats level"
);
// Within one stratum: higher level wins, and equal levels keep registration order.
s.run(
r#"
winner = nil
mk("KbHighTop", "HIGH", 5)
mk("KbHighTie", "HIGH", 5)
"#,
)
.unwrap();
assert!(s.key_input("ESCAPE"));
assert_eq!(
s.eval::<String>("return winner").unwrap(),
"KbHighTop",
"level 5 beats level 1; the earlier of the two 5s wins the tie"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// **At most one frame consumes** (§2, "Delivery"): the walk stops at the first consumer, so a
/// second keyboard frame below it never sees the key.
#[test]
fn the_first_consumer_ends_the_walk() {
let mut s = script();
s.run(
r#"
seen = {}
function mk(name, strata)
local f = CreateFrame("Frame", name)
f:SetFrameStrata(strata)
f:EnableKeyboard(true)
f:SetScript("OnChar", function() table.insert(seen, name) end)
end
mk("KbTip", "TOOLTIP")
mk("KbDlg", "DIALOG")
"#,
)
.unwrap();
assert!(s.char_input("x"));
assert_eq!(s.eval::<i64>("return table.getn(seen)").unwrap(), 1);
assert_eq!(s.eval::<String>("return seen[1]").unwrap(), "KbTip");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A hidden frame is in no bucket (the link gate the walk shares with the draw order), so it
/// neither fires nor consumes — and the frame below it gets the key instead.
#[test]
fn a_hidden_frame_is_not_in_the_walk() {
let mut s = script();
s.run(
r#"
winner = nil
function mk(name, strata)
local f = CreateFrame("Frame", name)
f:SetFrameStrata(strata)
f:EnableKeyboard(true)
f:SetScript("OnChar", function() winner = name end)
return f
end
top = mk("KbHidden", "TOOLTIP")
mk("KbBelow", "DIALOG")
top:Hide()
"#,
)
.unwrap();
assert!(s.char_input("q"));
assert_eq!(s.eval::<String>("return winner").unwrap(), "KbBelow");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// A raising handler must not eat the key or abort the walk's caller: the error is recorded, the
/// frame still consumed (consumption is the C++ gate's, never the handler's — §3.1).
#[test]
fn a_raising_handler_still_consumes_and_is_recorded() {
let mut s = script();
s.run(
r#"
f = CreateFrame("Frame", "KbBoom")
f:EnableKeyboard(true)
f:SetScript("OnChar", function() error("boom") end)
"#,
)
.unwrap();
assert!(s.char_input("z"), "the gate consumed regardless");
assert!(
s.errors().iter().any(|e| e.contains("boom")),
"the raise is recorded: {:?}",
s.errors()
);
}
@@ -17,6 +17,7 @@ mod font_object;
mod frame_api;
mod generic_for;
mod input;
mod keyboard;
mod layout_gate;
mod measure;
mod minimap;