//! `CreateFrame(kind, name, parent, inherits)` — the **runtime** template path //! ([`crate::loader::apply_template`]). //! //! `local b = CreateFrame("Button", "MyButton", UIParent, "UIPanelButtonTemplate")` is the single //! most common line in the addon corpus, and until the loader's post-`CreateFrame` steps were //! factored into `Loader::decorate` the fourth argument was dropped on the floor: the addon got a //! bare frame with no art, no regions, no scripts, and a warning on a channel it never reads. //! //! The load-bearing property every test here turns on is that **the caller's own name wins** — a //! template instantiated as `Mine` publishes `MineTexture`, never `ThatTemplateTexture`, because //! `getglobal("MineTexture")` is what the addon's next line calls. use super::common::script; use crate::script::UiScript; /// Register templates by loading a small FrameXML document — the same door an addon's own `.xml` /// comes through, so nothing here is a test-only shortcut into the registry. fn register(s: &UiScript, xml: &str) { let doc = crate::framexml::parse(xml).expect("valid FrameXML"); let report = crate::loader::load(s, &doc, &|_| None); assert!( report.errors.is_empty(), "fixture document failed to load: {:?}", report.errors ); } /// The whole shape at once: a virtual template's ``, ``, `` texture and /// `` all reach a frame built by `CreateFrame`'s fourth argument — and the region /// is named against **the instance**, not the template. #[test] fn a_runtime_template_brings_size_anchors_regions_and_a_fired_onload() { let mut s = script(); s.set_screen_size(800.0, 600.0); register( &s, r#" ProbeLoadedAs = self:GetName(); ProbeLoadWidth = self:GetWidth() "#, ); s.run(r#"Mine = CreateFrame("Frame", "Mine", Host, "ProbeTemplate")"#) .unwrap(); assert_eq!( s.eval::<(f32, f32)>("return Mine:GetWidth(), Mine:GetHeight()") .unwrap(), (160.0, 40.0), "the template's " ); // The template's own substitute `$parent` against the frame's PARENT (rf27) — the // instance's enclosing frame, not the instance itself. let (point, rel, rel_point, x, y): (String, String, String, f32, f32) = s .eval( "local p, r, rp, ox, oy = Mine:GetPoint(1) \ return p, r:GetName(), rp, ox, oy", ) .unwrap(); assert_eq!( (point.as_str(), rel.as_str(), rel_point.as_str(), x, y), ("TOPLEFT", "Host", "BOTTOMLEFT", 7.0, -3.0) ); // The region carries the INSTANCE's name. This is the whole point: `MineTexture` is what an // addon reaches for, and `ProbeTemplateTexture` would be useless to it. assert!( s.eval::(r#"return getglobal("MineTexture") ~= nil"#) .unwrap(), "the template's $parentTexture resolved against the caller's name" ); assert!( s.eval::(r#"return getglobal("ProbeTemplateTexture") == nil"#) .unwrap(), "nothing may be named after the template" ); // OnLoad fired inside CreateFrame, after the frame was decorated — the reference's ordering, // and what `local f = CreateFrame(...); f:Foo()` on the next line assumes. assert_eq!(s.eval::("return ProbeLoadedAs").unwrap(), "Mine"); assert_eq!(s.eval::("return ProbeLoadWidth").unwrap(), 160.0); assert!(s.take_errors().is_empty()); assert!(s.take_warnings().is_empty()); } /// A template's nested `` child is named against the caller too, and is really parented to /// the new frame — the `$parent` chain composes through `CreateFrame` exactly as through XML. #[test] fn a_nested_frames_child_is_named_against_the_caller() { let mut s = script(); register( &s, r#" "#, ); s.run(r#"Outer = CreateFrame("Frame", "Outer", nil, "NestTemplate")"#) .unwrap(); assert_eq!( s.eval::("return OuterInner:GetParent():GetName()") .unwrap(), "Outer" ); assert_eq!( s.eval::<(f32, f32)>("return OuterInner:GetWidth(), OuterInner:GetHeight()") .unwrap(), (11.0, 12.0) ); // Two levels deep, so the chain is composing rather than substituting once. assert_eq!( s.eval::("return OuterInnerDeep:GetParent():GetName()") .unwrap(), "OuterInner" ); assert!(s .eval::(r#"return getglobal("NestTemplateInner") == nil"#) .unwrap()); assert!(s.take_errors().is_empty()); assert!(s.take_warnings().is_empty()); } /// The template argument is **one name**, on this path exactly as on the XML one — and a chain /// through a single name still resolves. /// /// This asserted the opposite until the registry lookup was carved. `"TemplA, TemplB"` is not a /// list, it is a literal name nothing declared: 1.12's `CreateFrame` reaches the same `0x6ee6f0` /// the XML loader does (call site `0x7061dd`) with the string Lua handed it, and the loader has no /// splitter anywhere — comma lists are a **later**-client feature. Corroborated by the corpus: /// 6842 `inherits=` across 282 vanilla XML files contain **zero** comma lists, while the modern /// Blizzard UI source is full of them. #[test] fn a_comma_list_is_one_name_and_a_single_name_still_chains() { let mut s = script(); register( &s, r#" "#, ); // A single name still walks its whole chain. This is what keeps the narrowing honest: it // removes the list, not the inheritance. s.run(r#"One = CreateFrame("Frame", "One", nil, "TemplA")"#) .unwrap(); assert_eq!( s.eval::<(f32, f32)>("return One:GetWidth(), One:GetHeight()") .unwrap(), (50.0, 60.0) ); assert_eq!(s.eval::("return One:GetAlpha()").unwrap(), 0.25); assert!(s.take_errors().is_empty()); assert!(s.take_warnings().is_empty()); // The comma form is one literal name, nothing declared it, so it RAISES — naming the whole // unsplit string, which is the proof it was never split. let err = s .run(r#"Both = CreateFrame("Frame", "Both", nil, "TemplA, TemplB")"#) .expect_err("a comma list is one name, and it misses"); let text = err.to_string(); assert!( text.contains("TemplA, TemplB"), "the miss must name the literal it looked up, unsplit: {text}" ); assert!( s.eval::(r#"return getglobal("Both") == nil"#) .unwrap(), "a missed template must leave no frame behind" ); } /// The case fold, with the four names the corpus actually mis-cases. /// /// The compare is `SStrCmpI` → `_strnicmp`, and the trap that decides it is the bucket hash: /// `SStrHash 0x64b3f0` uppercases before mixing, so a mis-cased name lands in the same bucket and /// the stored-hash pre-check passes rather than short-circuiting. ASCII-only, like every other fold /// in this engine. #[test] fn a_template_name_is_matched_case_insensitively() { let mut s = script(); register( &s, r#" "#, ); // Exactly the shape `CT_RaidAssist/CT_RAOptions.xml` ships: `RA` written `Ra`. s.run(r#"Cased = CreateFrame("Frame", "Cased", nil, "CT_RaCheckButtonTemplate")"#) .unwrap(); assert_eq!( s.eval::<(f32, f32)>("return Cased:GetWidth(), Cased:GetHeight()") .unwrap(), (50.0, 60.0) ); assert!(s.take_errors().is_empty()); assert!(s.take_warnings().is_empty()); } /// An unresolvable template name **raises, and creates nothing**. /// /// This test asserted the exact opposite — *"the reference creates the frame anyway, and an addon's /// next line already assumes it did"* — until the bytes were read. `0x7061dd` looks the name up and /// on NULL falls into `luaL_error(L, "CreateFrame(): Couldn't find inherited node \"%s\"")`, which /// **never returns**: `luaG_errormsg` and `luaD_throw` contain no `ret` between them and end in /// `longjmp`, so the `xor eax,eax; ret` that follows the call is dead code. (That trailing `ret` is /// a fact about `luaL_error`'s `int` return type in C, not about reachability — which is exactly /// how the old claim was arrived at.) /// /// Both flavours of "unusable" raise, because the registry only ever holds `virtual="true"` /// elements, so a name declared without it misses the lookup identically to a name nothing declared. /// /// The ordering is asserted too: the miss precedes construction and name publication (`0x706208` / /// `0x70622d`), so nothing partial is left behind — no frame, and no global. #[test] fn an_unresolvable_template_raises_and_creates_nothing() { let s = script(); register(&s, r#""#); for (frame, template) in [("Orphan", "NoSuchTemplate"), ("Plainer", "PlainFrame")] { let err = s .run(&format!( r#"{frame} = CreateFrame("Button", "{frame}", nil, "{template}")"# )) .expect_err("an unresolvable template must raise"); let text = err.to_string(); assert!( text.contains("Couldn't find inherited node") && text.contains(template), "the raise must carry the reference's message and the name: {text}" ); // Nothing partial: no global published, and the name never reached the arena. assert!( s.eval::(&format!(r#"return getglobal("{frame}") == nil"#)) .unwrap(), "{frame} was published despite the miss" ); } // And it is ordinary Lua propagation — `pcall` catches it, which is what keeps one bad // CreateFrame inside a handler from taking the client down (the widget dispatcher runs every // handler under `lua_pcall`). assert!( !s.eval::( r#"return (pcall(CreateFrame, "Button", "Caught", nil, "NoSuchTemplate"))"# ) .unwrap(), "the raise must be pcall-catchable" ); } /// The two shape mismatches, neither of which may panic or drop the frame. /// /// A **region** template (a virtual ``) asked for as a frame: its frame-shaped content /// still lands, its region-only content cannot, and the message says which. /// /// A **kind that disagrees with the template's tag**: the frame keeps the kind `CreateFrame` was /// given — no template can retype a frame that already exists — so the ` "#, ); // 1 · a region template on a frame. s.run(r#"FromRegion = CreateFrame("Frame", "FromRegion", nil, "RegionTemplate")"#) .unwrap(); assert_eq!( s.eval::<(f32, f32)>("return FromRegion:GetWidth(), FromRegion:GetHeight()") .unwrap(), (33.0, 44.0), "the frame-shaped half of a region template still applies" ); let warnings = s.take_warnings(); assert!( warnings .iter() .any(|w| w.contains("RegionTemplate") && w.contains("REGION template")), "the region/frame mismatch must be named: {warnings:?}" ); assert!(s.take_errors().is_empty()); // 2 · a Button template asked for as a Frame. s.run(r#"AsFrame = CreateFrame("Frame", "AsFrame", nil, "ProbeButtonTemplate")"#) .unwrap(); assert_eq!( s.eval::("return AsFrame:GetWidth()").unwrap(), 90.0, "the generic half of the template still applies" ); assert!( s.eval::(r#"return getglobal("AsFrameText") == nil"#) .unwrap(), "a Frame has no ButtonText slot, so nothing was invented for it" ); let warnings = s.take_warnings(); assert!( warnings .iter() .any(|w| w.contains("ProbeButtonTemplate") && w.contains("