Files
benilla-octo/crates/benilla-ui/src/script/tests/events.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

175 lines
6.5 KiB
Rust

//! RegisterEvent + fire_event via BOTH conventions (RF-0025).
use super::common::script;
use crate::script::*;
#[test]
fn fire_event_both_conventions_in_one_handler() {
let mut s = script();
s.run(
r#"
local f = CreateFrame("Frame", "EF")
f:RegisterEvent("UNIT_HEALTH")
f:SetScript("OnEvent", function(self, event, ...)
r_this_eq_self = (this == self) -- legacy `this` global == modern `self`
r_event_global = event -- modern `event` arg
r_event_eq = (event == _G.event) -- == legacy `event` global
r_arg1_eq = (arg1 == select(1, ...)) -- legacy `arg1` == modern select(1,...)
r_arg1 = arg1
r_arg2 = select(2, ...)
end)
"#,
)
.unwrap();
s.fire_event(
"UNIT_HEALTH",
vec![ScriptValue::Str("player".into()), ScriptValue::Int(42)],
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
assert!(s.eval::<bool>("return r_this_eq_self").unwrap());
assert_eq!(
s.eval::<String>("return r_event_global").unwrap(),
"UNIT_HEALTH"
);
assert!(s.eval::<bool>("return r_event_eq").unwrap());
assert!(s.eval::<bool>("return r_arg1_eq").unwrap());
assert_eq!(s.eval::<String>("return r_arg1").unwrap(), "player");
assert_eq!(s.eval::<i64>("return r_arg2").unwrap(), 42);
}
#[test]
fn globals_are_restored_after_firing_nesting_safe() {
let mut s = script();
s.run(
r#"
this, event, arg1 = "outer_this", "outer_event", "outer_arg1"
local f = CreateFrame("Frame", "NF")
f:RegisterEvent("E")
f:SetScript("OnEvent", function() end)
"#,
)
.unwrap();
s.fire_event("E", vec![ScriptValue::Str("x".into())]);
// After firing, the prior global values must be restored (RF-0025 set-then-restore).
let (t, e, a): (String, String, String) = s.eval("return this, event, arg1").unwrap();
assert_eq!(
(t.as_str(), e.as_str(), a.as_str()),
("outer_this", "outer_event", "outer_arg1")
);
}
#[test]
fn handler_errors_are_collected_not_panicked() {
let mut s = script();
s.run(
r#"
local f = CreateFrame("Frame", "BoomF")
f:RegisterEvent("E")
f:SetScript("OnEvent", function() error("boom") end)
"#,
)
.unwrap();
s.fire_event("E", vec![]);
let errs = s.errors();
assert_eq!(errs.len(), 1, "{errs:?}");
assert!(errs[0].contains("boom"), "{errs:?}");
}
/// The cross-frame dispatch ORDER law (wow-re `event-dispatch-order.md`, VERIFIED): the client's
/// per-event listener list is tail-appended (`0x7052d0`) and walked head-first (`0x703e50`) —
/// **FIFO: registration order = firing order**. Duplicate registration keeps the original
/// position (`0x702264` dup ret); unregister+re-register moves to the tail. The ZoneText frames
/// depend on this: both write PVPInfoTextString on one event — the last writer decides.
#[test]
fn events_fire_in_registration_order_fifo() {
let mut s = script();
s.run(
r#"
order = ""
local a = CreateFrame("Frame", "FA")
local b = CreateFrame("Frame", "FB")
local c = CreateFrame("Frame", "FC")
a:RegisterEvent("E"); b:RegisterEvent("E"); c:RegisterEvent("E")
a:SetScript("OnEvent", function() order = order .. "A" end)
b:SetScript("OnEvent", function() order = order .. "B" end)
c:SetScript("OnEvent", function() order = order .. "C" end)
"#,
)
.unwrap();
s.fire_event("E", vec![]);
assert_eq!(s.eval::<String>("return order").unwrap(), "ABC");
// Duplicate registration keeps A's position (the client's dup early-ret).
s.run("FA:RegisterEvent('E'); order = ''").unwrap();
s.fire_event("E", vec![]);
assert_eq!(s.eval::<String>("return order").unwrap(), "ABC");
// Unregister + re-register moves B to the TAIL (the node is freed, the re-add appends).
s.run("FB:UnregisterEvent('E'); FB:RegisterEvent('E'); order = ''")
.unwrap();
s.fire_event("E", vec![]);
assert_eq!(s.eval::<String>("return order").unwrap(), "ACB");
}
/// **`HasScript` answers "can this widget CARRY that kind", not "does it have one set".**
///
/// That distinction is the verb's whole purpose, and every corpus caller depends on it: they ask
/// before hooking, precisely when nothing is set yet.
///
/// ```lua
/// if parent:HasScript("OnMouseDown") then -- Tablet-2.0.lua:2409
/// local script = parent:GetScript("OnMouseDown")
/// parent:SetScript("OnMouseDown", function() … end)
/// end
/// ```
///
/// It was the top session-start blocker — 32 of 39 `attempt to call method` failures were this one
/// name — and implementing it took survivors from 41 to 69.
///
/// The known over-permission is asserted too, so it is a recorded divergence rather than a
/// discovery: our table is flat where the reference's is per widget type, so a plain Frame answers
/// true for a Button-only kind. Exact for the base kinds, which is what the corpus asks about.
#[test]
fn has_script_reports_the_kind_is_supported_not_that_one_is_set() {
let s = script();
s.run(r#"f = CreateFrame("Frame", "HasScriptProbe")"#)
.unwrap();
// True with NOTHING set — the case every caller is actually in.
assert!(
s.eval::<bool>(r#"return f:HasScript("OnMouseDown")"#)
.unwrap(),
"a frame must report it can carry OnMouseDown before one is set"
);
assert!(
!s.eval::<bool>(r#"return f:HasScript("OnNotARealScript")"#)
.unwrap(),
"an unknown kind is false, not true"
);
// Tablet's exact idiom, run end to end: guard, read the (absent) handler, install one, fire it.
let fired: bool = s
.eval(
r#"
RAN = false
if f:HasScript("OnMouseDown") then
local prev = f:GetScript("OnMouseDown")
f:SetScript("OnMouseDown", function() RAN = true end)
end
f:GetScript("OnMouseDown")()
return RAN
"#,
)
.unwrap();
assert!(fired, "the guarded hook must install and run");
// The recorded divergence: flat table, so a Frame says true for a Button-only kind. The
// reference says false. Pinned so making SCRIPT_KINDS per-type has to come here and decide.
assert!(
s.eval::<bool>(r#"return f:HasScript("OnClick")"#).unwrap(),
"over-permissive by design today — see the comment at the binding"
);
}