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:
@@ -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}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user