the mailbox takes typing again, and events carry their real arguments

- the mail window's send tab takes typing again: the subject, body and money boxes were taking no input while the recipient box swallowed every key, and a sent letter clears its subject and item icon instead of putting them straight back (2145)
- seven events fire the way the reference does: UNIT_DYNAMIC_FLAGS fires at all, swapping two identical stacks between bank slots fires PLAYERBANKSLOTS_CHANGED twice, the pet sheet's training points repaint, and ITEM_LOCK_CHANGED loses its invented arguments (2140)
- nine widget methods 1.12 addons probe for are in: ShaguTweaks stops recolouring textures the reference leaves alone, pfUI and _Nameplates stop raising on a tooltip's GetAnchorType, and 63 AceEvent addons get RegisterAllEvents (2142)
- six post-1.12 methods leave the widget surface (SetSize, SetShown, HasFocus, SetFormattedText and two tooltip verbs), so an addon reaching for a later client's API finds what a real 1.12 client gives it (2142, 2146)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-11 20:52:50 +07:00
co-authored by Claude Fable 5.1
parent 3f3cf21afa
commit fed2c96ec8
100 changed files with 3837 additions and 869 deletions
@@ -1182,7 +1182,7 @@ fn the_split_frame_raises_over_a_same_stratum_window() {
local w = CreateFrame("Frame", "FakeBagnon", UIParent)
w:SetFrameStrata("HIGH")
w:SetPoint("BOTTOMLEFT", 0, 0)
w:SetSize(1024, 768)
w:SetWidth(1024); w:SetHeight(768)
local bg = w:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\FakeBagnonBG")
bg:SetAllPoints()
@@ -1395,7 +1395,13 @@ fn bagnon_forevers_records_survive_the_logout_boundary() {
// The logout boundary: the despawn frame (no self store) and then the shutdown's own events.
// The record the write persists must be the bags the player actually had.
let mut memory = crate::ui_items::feed::FeedMemory::default();
crate::ui_items::feed::apply_container_source(&mut s, &mut memory, None, [0; 10], Vec::new());
crate::ui_items::feed::apply_container_source(
&mut s,
&mut memory,
None,
Default::default(),
Vec::new(),
);
s.fire_event("PLAYER_LEAVING_WORLD", Vec::new());
s.fire_event("PLAYER_LOGOUT", Vec::new());
let (size, item) = s
@@ -1038,7 +1038,7 @@ fn the_duration_line_reads_the_real_global_strings() {
s.run(
r#"
BENILLA_ANCHOR = CreateFrame("Button", "BF9")
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetSize(10, 10)
BENILLA_ANCHOR:SetPoint("CENTER", 0, 0); BENILLA_ANCHOR:SetWidth(10); BENILLA_ANCHOR:SetHeight(10)
BENILLA_TIP = CreateFrame("GameTooltip", "TT9")
"#,
)
@@ -335,9 +335,9 @@ fn a_rare_payload_raises_the_typed_confirm_with_okay_disabled() {
.unwrap(),
"hasEditBox raises the narrow box"
);
assert!(
s.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
.unwrap(),
assert_eq!(
s.focused_editbox_name().as_deref(),
Some("StaticPopup1EditBox"),
"the entry's OnShow focuses the box, so the player can type straight away"
);
assert_eq!(
@@ -478,18 +478,19 @@ fn no_on_the_typed_confirm_clears_and_leaves_the_box_empty_for_next_time() {
fn escape_in_the_typed_confirms_box_is_swallowed_as_the_reference_leaves_it() {
let mut s = setup();
drop_in_world(&mut s, 871, "Flurry Axe", 4);
assert!(s
.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
.unwrap());
assert_eq!(
s.focused_editbox_name().as_deref(),
Some("StaticPopup1EditBox")
);
assert!(s.key_input("ESCAPE"), "the focused box consumes ESCAPE");
assert!(
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
"the dialog stays: the entry names no escape handler"
);
assert!(
s.eval::<bool>("return StaticPopup1EditBox:HasFocus()")
.unwrap(),
assert_eq!(
s.focused_editbox_name().as_deref(),
Some("StaticPopup1EditBox"),
"and the box keeps focus"
);
assert!(s.cursor_item().is_some(), "the item is still on the cursor");
@@ -0,0 +1,385 @@
//! **The event ARGUMENT-shape gate** — `reference/1.12-events.tsv` against what benilla actually
//! pushes at each fire site (decision 2140).
//!
//! Two gates already guard the event seam by NAME. [`super::reference_ui`]'s
//! `every_event_a_chain_file_registers_has_a_producer` asks whether something fires what a stock
//! file listens for (1889); `every_event_we_fire_is_an_event_the_reference_has` asks whether a
//! name we fire is a 1.12 name at all (1883). **Neither has ever looked at the arguments**, and an
//! event name is a plain string at both ends — so a fire with the wrong argument shape is silent
//! on both sides. It is 1842's finding one API over: the right name, the wrong shape.
//!
//! It was not hypothetical. Three live divergences on the first run:
//!
//! * `PLAYERBANKSLOTS_CHANGED` — two producers in the reference with **different** shapes
//! (`0x5ddd6e` argless, `0x4c728d` `"player"`), one shape here.
//! * `UNIT_PET_EXPERIENCE` and `UNIT_PET_TRAINING_POINTS` — argless here, `%s` there. The second
//! one reaches `PetPaperDollFrame_OnEvent` only through its final `elseif ( arg1 == "pet" )`
//! catch-all, so an argless fire reached **nobody**: the stock pet page never repainted its
//! training points off the event at all.
//!
//! ## What is compared, and what the table can and cannot say
//!
//! The oracle is wow-re's two byte-level censuses, vendored by `scripts/gen-reference-events.py`.
//! Its `conf` column carries the gate rule per row, exactly as `1.12-shapes.tsv` does:
//!
//! * `exact` — every producer that reaches the event has a known shape. **This is the column this
//! gate reads.**
//! * `advisory` — a contributing site declares more varargs than its caller pushes (three sites,
//! image-wide). Not gated: a faithful client must not copy a bug that hands Lua undefined
//! values.
//! * `none` — no producer family reaches it in that census. **Not a claim that it has none** (the
//! census's own header says so in capitals), so a `none` row is skipped, never failed. That is
//! where the whole `CHAT_MSG_*` family sits: those reach Lua through a formatted wrapper the
//! two-helper census does not see.
//!
//! ## Two deliberate narrowings
//!
//! **Only a fire whose event name is a literal is measured.** `ui_unit.rs` builds the ten power
//! events with `format!("UNIT_{}", power_token(…))`, and there is no name to key on at the call
//! site. Those are covered one gate over — `reference_ui`'s `CONSTRUCTED` list — and their single
//! `tok()` argument is the same shape all ten share.
//!
//! **Only a literal `ScriptValue` element's KIND is asserted.** An argument built from a variable
//! (`vec![ScriptValue::Int(bag)]` is literal enough — the *variant* is what types it, not the
//! value) is typed by its variant; anything that is not a `ScriptValue::` constructor at all
//! counts toward the arity and matches any kind. Arity is the assertion that catches the bug
//! class; kinds are the free extra.
use std::collections::BTreeMap;
/// One row of `reference/1.12-events.tsv`.
struct Row {
/// The alternatives, each a per-slot kind list. `%s` -> `s`, `%d`/`%u`/`%f` -> `n`.
shapes: Vec<Vec<char>>,
producers: String,
}
fn table() -> BTreeMap<String, Row> {
let tsv = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../reference/1.12-events.tsv"
);
let text = std::fs::read_to_string(tsv).expect("reference/1.12-events.tsv");
let mut out = BTreeMap::new();
for line in text.lines() {
if line.starts_with('#') || line.starts_with("name\t") {
continue;
}
let f: Vec<&str> = line.split('\t').collect();
// name ids producers arg_formats conf note
if f.len() < 5 || f[4] != "exact" {
continue;
}
let shapes = f[3]
.split('|')
.map(|alt| {
if alt == "()" {
return Vec::new();
}
alt.split('%')
.skip(1)
.map(|d| match d.chars().next() {
Some('s') => 's',
_ => 'n',
})
.collect()
})
.collect();
out.insert(
f[0].to_string(),
Row {
shapes,
producers: f[2].to_string(),
},
);
}
out
}
/// Split `body` at its top-level commas — `vec![…]`'s elements, with nested calls left alone.
fn elements(body: &str) -> Vec<String> {
let mut out = Vec::new();
let (mut depth, mut cur, mut in_str, mut esc) = (0i32, String::new(), false, false);
for c in body.chars() {
if in_str {
cur.push(c);
if esc {
esc = false;
} else if c == '\\' {
esc = true;
} else if c == '"' {
in_str = false;
}
continue;
}
match c {
'"' => {
in_str = true;
cur.push(c);
}
'(' | '[' | '{' => {
depth += 1;
cur.push(c);
}
')' | ']' | '}' => {
depth -= 1;
cur.push(c);
}
',' if depth == 0 => {
out.push(std::mem::take(&mut cur));
}
_ => cur.push(c),
}
}
if !cur.trim().is_empty() {
out.push(cur);
}
out.into_iter()
.map(|e| e.trim().to_string())
.filter(|e| !e.is_empty())
.collect()
}
/// The kind an argument expression pushes: `s` string, `n` number, `b` boolean, `x` nil,
/// `?` not determinable from the source.
fn kind_of(expr: &str) -> char {
let e = expr.trim_start_matches('&').trim();
for (variant, k) in [
("ScriptValue::Str", 's'),
("ScriptValue::Int", 'n'),
("ScriptValue::Number", 'n'),
("ScriptValue::Bool", 'b'),
("ScriptValue::Nil", 'x'),
] {
if e.starts_with(variant) {
return k;
}
}
'?'
}
/// Every `(event, argument kinds, where)` benilla fires with a literal name.
fn fire_sites() -> Vec<(String, Vec<char>, String)> {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root")
.join("crates");
let mut out = Vec::new();
let mut stack = vec![root];
while let Some(dir) = stack.pop() {
for e in std::fs::read_dir(&dir).into_iter().flatten().flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
continue;
}
if p.extension().is_none_or(|x| x != "rs") {
continue;
}
// A test's own synthetic fires are not a surface we ship — the same rule
// `every_event_we_fire_is_an_event_the_reference_has` holds, plus the inline
// `#[cfg(test)]` tail a file-name rule cannot see.
if p.to_string_lossy().contains("test") {
continue;
}
let whole = std::fs::read_to_string(&p).unwrap_or_default();
let text = whole
.split_once("#[cfg(test)]")
.map_or(whole.as_str(), |(head, _)| head);
// Split so this file cannot match its own scanner (the 1883 gate's lesson).
for call in [
concat!("fire_event", "("),
concat!("fire_event_into", "("),
concat!("pending_events.push", "(("),
] {
let mut from = 0;
while let Some(i) = text[from..].find(call) {
let at = from + i + call.len();
from = at;
let rest = text[at..].trim_start();
let rest = rest.strip_prefix("lua,").map_or(rest, str::trim_start);
let Some(body) = rest.strip_prefix('"') else {
continue;
};
let Some(end) = body.find('"') else { continue };
let (name, after) = (&body[..end], &body[end + 1..]);
let Some(args) = after.trim_start().strip_prefix(',') else {
continue;
};
let args = args.trim_start();
let kinds = if let Some(v) = args.strip_prefix("vec![") {
let mut depth = 1i32;
let mut close = v.len();
for (k, c) in v.char_indices() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
close = k;
break;
}
}
_ => {}
}
}
elements(&v[..close]).iter().map(|e| kind_of(e)).collect()
} else if args.starts_with("Vec::new()") {
Vec::new()
} else {
// An argument list built elsewhere — arity unknowable from here.
continue;
};
out.push((name.to_string(), kinds, p.display().to_string()));
}
}
}
}
out
}
/// **Every event benilla fires must carry the arguments the reference's producer pushes.**
#[test]
fn every_event_we_fire_carries_the_reference_s_arguments() {
let table = table();
assert!(
table.len() > 250,
"the events table gave only {} gateable rows — it stopped being read",
table.len()
);
let sites = fire_sites();
assert!(
sites.len() > 200,
"the walker found only {} literal fire sites — it stopped matching, which would make this \
gate silently vacuous",
sites.len()
);
let mut checked = 0usize;
let mut wrong: Vec<String> = Vec::new();
for (name, kinds, at) in &sites {
let Some(row) = table.get(name) else { continue };
checked += 1;
let fits = row.shapes.iter().any(|want| {
want.len() == kinds.len()
&& want
.iter()
.zip(kinds)
.all(|(w, g)| *g == '?' || *g == *w || (*w == 'n' && *g == 'n'))
});
if !fits {
let shapes: Vec<String> = row
.shapes
.iter()
.map(|s| format!("({})", s.iter().collect::<String>()))
.collect();
wrong.push(format!(
"{name} fired as ({}) from {at} — the reference's {} producer(s) push {}",
kinds.iter().collect::<String>(),
row.producers,
shapes.join(" | ")
));
}
}
assert!(
checked >= 175,
"only {checked} fires were measurable against the table — coverage collapsed"
);
wrong.sort();
wrong.dedup();
assert!(
wrong.is_empty(),
"{} fire site(s) push the wrong argument shape. An event name is a plain string at both \
ends, so nothing else in the build can see this:\n {}",
wrong.len(),
wrong.join("\n ")
);
}
/// **Every event the reference's unit-field bridge produces must have a producer here.**
///
/// This is the half that let `UNIT_DYNAMIC_FLAGS` sit unfired for the whole life of the client.
/// [`super::reference_ui::every_event_a_chain_file_registers_has_a_producer`] asks whether
/// anything fires what a **stock chain file** listens for — and *nothing in 1.12 FrameXML
/// registers `UNIT_DYNAMIC_FLAGS`, so the hole was invisible to it by construction. The corpus is
/// where the demand was (`CT_UnitFrames/CT_TargetFrame.xml:200`, `TipBuddy/TipBuddy.lua:17`), and
/// the corpus is not in this repo.
///
/// So this gate asks the question from the REFERENCE's side instead, over the one family where
/// that is bounded and exact: `0x51bbb0` registers one watch per named unit-window field, so
/// every event id below `0xb6` that has a name in the client's own table is produced by that
/// bridge, always with the unit token. Twenty-five names, one producer function on our side
/// ([`crate::ui_unit::fire_transitions`] and the two pet-stat feeds), and no judgment call about
/// which of them "should" exist — the reference fires all of them.
///
/// A CENSUS with a declared set, like 1889's: what must not happen is a new name appearing.
#[test]
fn every_unit_field_bridge_event_has_a_producer() {
/// Produced by the reference's bridge, by nothing here. Each needs a reason, and an entry
/// that comes into production fails the gate so the list cannot outlive its fix.
const UNPRODUCED: &[(&str, &str)] = &[(
"UNIT_LOYALTY",
"field 138, the hunter pet's loyalty level. benilla's pet-stat feed (`ui_pet_stats`) \
reads happiness and training points off `PET_BYTES` but never the loyalty field, and no \
window it feeds shows a loyalty line — `PetPaperDollFrame` does not register it either. \
Unbuilt, not wrong: the day the pet page grows the line, this entry goes with it",
)];
let table = table();
let bridge: Vec<&String> = table
.iter()
.filter(|(_, r)| r.producers.contains("unit-field-bridge"))
.map(|(n, _)| n)
.collect();
assert!(
bridge.len() >= 25,
"only {} bridge events in the table — it stopped being read",
bridge.len()
);
let fired: std::collections::HashSet<String> = fire_sites()
.into_iter()
.map(|(n, _, _)| n)
.chain(
// The ten power events are built with `format!("UNIT_{}", power_token(…))`, so no
// literal exists to scan for — the same declared family `reference_ui`'s CONSTRUCTED
// list carries, and an entry there is a promise that something fires it.
[
"MANA",
"RAGE",
"FOCUS",
"ENERGY",
"HAPPINESS",
"MAXMANA",
"MAXRAGE",
"MAXFOCUS",
"MAXENERGY",
"MAXHAPPINESS",
]
.iter()
.map(|p| format!("UNIT_{p}")),
)
.collect();
let expected: std::collections::HashSet<&str> = UNPRODUCED.iter().map(|(e, _)| *e).collect();
let missing: Vec<&str> = bridge
.iter()
.map(|n| n.as_str())
.filter(|n| !fired.contains(*n) && !expected.contains(n))
.collect();
assert!(
missing.is_empty(),
"the reference fires these off a named unit descriptor field and benilla fires them \
nowhere. No stock file need listen for one — the corpus does, and an addon that \
registers an event nothing produces hears silence with no error anywhere:\n {missing:?}"
);
let now_fired: Vec<&str> = UNPRODUCED
.iter()
.map(|(e, _)| *e)
.filter(|e| fired.contains(*e))
.collect();
assert!(
now_fired.is_empty(),
"these now HAVE a producer — take them out of UNPRODUCED so the list keeps meaning what \
it says: {now_fired:?}"
);
}
+10 -10
View File
@@ -1980,9 +1980,9 @@ mod clip_plumb_tests {
r#"
local frame = CreateFrame("ScrollFrame", "SF")
frame:SetPoint("TOPLEFT", 0, -100) -- screen top 768 -> frame top 668
frame:SetSize(300, 200) -- frame bottom 300, right 300
frame:SetWidth(300); frame:SetHeight(200) -- frame bottom 300, right 300
local child = CreateFrame("Frame", "Child")
child:SetSize(300, 600)
child:SetWidth(300); child:SetHeight(600)
frame:SetScrollChild(child)
local marker = child:CreateTexture(nil, "ARTWORK")
marker:SetTexture(1, 0, 0) -- pathless colored quad: no BLP asset needed
@@ -2070,7 +2070,7 @@ mod clip_plumb_tests {
r#"
local plain = CreateFrame("Frame", "Plain")
plain:SetPoint("TOPLEFT", 0, 0)
plain:SetSize(50, 50)
plain:SetWidth(50); plain:SetHeight(50)
local m = plain:CreateTexture(nil, "ARTWORK")
m:SetTexture(0, 1, 0)
m:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
@@ -2134,7 +2134,7 @@ mod align_tests {
for i = 1, 2 do
local f = CreateFrame("Frame", "F" .. i)
f:SetPoint("TOPLEFT", 0, 0)
f:SetSize(50, 50)
f:SetWidth(50); f:SetHeight(50)
for j = 1, 2 do
local t = f:CreateTexture(nil, "ARTWORK")
t:SetTexture(i / 4, j / 4, 0)
@@ -2271,7 +2271,7 @@ mod extract_gate_tests {
r#"
local plain = CreateFrame("Frame", "Plain")
plain:SetPoint("TOPLEFT", 0, 0)
plain:SetSize(50, 50)
plain:SetWidth(50); plain:SetHeight(50)
marker = plain:CreateTexture(nil, "ARTWORK")
marker:SetTexture(1, 0, 0)
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
@@ -2379,11 +2379,11 @@ mod extract_gate_tests {
r#"
local doll = CreateFrame("PlayerModel", "CharacterModelFrame")
doll:SetPoint("TOPLEFT", 0, 0)
doll:SetSize(233, 224)
doll:SetWidth(233); doll:SetHeight(224)
doll:SetUnit("player")
local stray = CreateFrame("PlayerModel", "SomeAddonsModelPane")
stray:SetPoint("TOPLEFT", 300, 0)
stray:SetSize(233, 224)
stray:SetWidth(233); stray:SetHeight(224)
stray:SetUnit("player")
"#,
);
@@ -2501,7 +2501,7 @@ mod extract_gate_tests {
r#"
local plain = CreateFrame("Frame", "Plain")
plain:SetPoint("TOPLEFT", 0, 0)
plain:SetSize(50, 50)
plain:SetWidth(50); plain:SetHeight(50)
marker = plain:CreateTexture(nil, "ARTWORK")
marker:SetTexture(0, 0, 1)
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
@@ -2560,7 +2560,7 @@ mod extract_gate_tests {
r#"
local plain = CreateFrame("Frame", "Plain")
plain:SetPoint("TOPLEFT", 0, 0)
plain:SetSize(50, 50)
plain:SetWidth(50); plain:SetHeight(50)
marker = plain:CreateTexture(nil, "ARTWORK")
marker:SetTexture(0, 1, 0)
marker:SetAllPoints() -- templateless Lua region: no implicit anchor (1310)
@@ -2587,7 +2587,7 @@ mod extract_gate_tests {
local function box(name, x, r, g, b)
local f = CreateFrame("Frame", name)
f:SetPoint("TOPLEFT", x, 0)
f:SetSize(50, 50)
f:SetWidth(50); f:SetHeight(50)
local t = f:CreateTexture(nil, "ARTWORK")
t:SetTexture(r, g, b)
t:SetAllPoints()
+5
View File
@@ -920,6 +920,11 @@ mod panel_template_tests;
#[cfg(test)]
mod shape_gate;
/// The event ARGUMENT-shape gate (decision 2140) — the same question one API over: the two event
/// gates in `reference_ui` compare names, and nothing compared what a fire site pushes.
#[cfg(test)]
mod event_shape_gate;
/// The reference's BasicControls.xml — TEXT/message/_ERRORMESSAGE and the ScriptErrors dialog,
/// none of which benilla itself calls: every test enters from Lua the way an addon does.
#[cfg(test)]
@@ -771,7 +771,7 @@ fn shipped_panel_slot_pushable_promotes_to_center() {
r#"
local loot = CreateFrame("Frame", "LootFrame")
loot:Hide()
loot:SetSize(50, 50)
loot:SetWidth(50); loot:SetHeight(50)
ShowUIPanel(loot)
"#,
)
@@ -1257,8 +1257,8 @@ fn a_dead_player_opens_only_whiledead_windows() {
// Stand-ins for two shipped rows: GossipFrame (no whileDead) and QuestLogFrame (whileDead=1).
// Bare frames are enough — the guard runs before any seat is chosen.
s.run(
r#"local g = CreateFrame("Frame", "GossipFrame") g:SetSize(50, 50) g:Hide()
local q = CreateFrame("Frame", "QuestLogFrame") q:SetSize(50, 50) q:Hide()
r#"local g = CreateFrame("Frame", "GossipFrame") g:SetWidth(50); g:SetHeight(50) g:Hide()
local q = CreateFrame("Frame", "QuestLogFrame") q:SetWidth(50); q:SetHeight(50) q:Hide()
ShowUIPanel(GossipFrame)"#,
)
.unwrap();
@@ -1321,9 +1321,9 @@ fn a_frame_arriving_at_center_puts_the_child_windows_away() {
// holds left, TradeFrame (pushable 1) then ARRIVES at center (UIParent.lua l.734-741's
// else-arm: the incumbent outranks nobody, the newcomer settles at center).
s.run(
r#"local m = CreateFrame("Frame", "OpenMailFrame") m:SetSize(50, 50)
local a = CreateFrame("Frame", "MerchantFrame") a:SetSize(50, 50) a:Hide()
local b = CreateFrame("Frame", "TradeFrame") b:SetSize(50, 50) b:Hide()
r#"local m = CreateFrame("Frame", "OpenMailFrame") m:SetWidth(50); m:SetHeight(50)
local a = CreateFrame("Frame", "MerchantFrame") a:SetWidth(50); a:SetHeight(50) a:Hide()
local b = CreateFrame("Frame", "TradeFrame") b:SetWidth(50); b:SetHeight(50) b:Hide()
ShowUIPanel(MerchantFrame)
ShowUIPanel(TradeFrame)"#,
)
@@ -1344,7 +1344,7 @@ fn a_frame_arriving_at_center_puts_the_child_windows_away() {
s.run(
r#"HideUIPanel(TradeFrame) HideUIPanel(MerchantFrame)
OpenMailFrame:Show()
local l = CreateFrame("Frame", "LootFrame") l:SetSize(50, 50) l:Hide()
local l = CreateFrame("Frame", "LootFrame") l:SetWidth(50); l:SetHeight(50) l:Hide()
ShowUIPanel(LootFrame)
ShowUIPanel(MerchantFrame)"#,
)
@@ -1296,7 +1296,8 @@ fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not
}
/// The SHARED item tooltip on a quest reward row (the director's "same tooltip as vendor items"):
/// hovering the reward fires the row's OnEnter → `SetItemById` → the ask-once store. First hover
/// hovering the reward fires the row's OnEnter → `SetQuestLogItem` → the shared id-keyed render
/// → the ask-once store. First hover
/// (store cold) shows the fallback name line AND records the ask; after the app's push, a re-hover
/// renders the full stat head — the identical lines a vendor row/bag slot gets for this item.
#[test]
@@ -559,7 +559,13 @@ fn spending_an_arrow_starts_quivers_reload_through_the_real_item_feed() {
)]))
};
let mut memory = FeedMemory::default();
apply_container_source(&mut s, &mut memory, quiver(200), [0; 10], Vec::new());
apply_container_source(
&mut s,
&mut memory,
quiver(200),
Default::default(),
Vec::new(),
);
s.fire_event("START_AUTOREPEAT_SPELL", Vec::new());
s.tick(0.1);
@@ -568,7 +574,13 @@ fn spending_an_arrow_starts_quivers_reload_through_the_real_item_feed() {
// The server tells us the stack is one lighter. That is a fired shot, and the only way the
// addon can ever know it.
apply_container_source(&mut s, &mut memory, quiver(199), [0; 10], Vec::new());
apply_container_source(
&mut s,
&mut memory,
quiver(199),
Default::default(),
Vec::new(),
);
s.tick(0.1);
let (_, reloading, reload_left, _) = shot_state(&mut s);
@@ -97,7 +97,7 @@ fn app_frame_ticked(s: &mut UiScript, dt: f32) {
fn install_changing_tooltip(s: &UiScript, owner: &str, func: &str) {
s.run(&format!(
r#"
local a = CreateFrame("Button", "{owner}"); a:SetPoint("CENTER", 0, 0); a:SetSize(10, 10)
local a = CreateFrame("Button", "{owner}"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
{func}_n = 0
function {func}()
{func}_n = {func}_n + 1
@@ -534,7 +534,7 @@ fn a_tooltip_line_flipping_wrapped_to_plain_costs_no_graph_derivation() {
s.run(
r#"
FlipOwner = CreateFrame("Button", "FlipOwner"); FlipOwner:SetPoint("CENTER", 0, 0)
FlipOwner:SetSize(10, 10)
FlipOwner:SetWidth(10); FlipOwner:SetHeight(10)
flip_n = 0
-- The two shapes a hover alternates between. The trailing `1` on the wrap arm is
-- `AddLine`'s positional wrapText flag (the byte-pinned 0x531630 signature) — it is what
@@ -606,7 +606,7 @@ fn a_hover_sweep_across_owners_costs_no_graph_derivation() {
r#"
for i = 1, 12 do
local b = CreateFrame("Button", "SweepOwner" .. i)
b:SetPoint("CENTER", 0, 0); b:SetSize(10, 10)
b:SetPoint("CENTER", 0, 0); b:SetWidth(10); b:SetHeight(10)
end
sweep_n = 0
-- A different owner AND a different line shape every step: the two halves of a real sweep
@@ -679,7 +679,7 @@ fn an_action_bar_hover_sweep_costs_no_graph_derivation() {
r#"
for i = 1, 12 do
local b = CreateFrame("Button", "BarOwner" .. i)
b:SetPoint("CENTER", 0, 0); b:SetSize(36, 36)
b:SetPoint("CENTER", 0, 0); b:SetWidth(36); b:SetHeight(36)
end
bar_n = 0
-- `GameTooltip_SetDefaultAnchor`'s body, which is what every action button actually runs:
@@ -754,7 +754,7 @@ fn a_bag_addon_hover_sweep_costs_no_graph_derivation() {
r#"
for i = 1, 12 do
local b = CreateFrame("Button", "BagOwner" .. i)
b:SetPoint("CENTER", 0, 0); b:SetSize(37, 37)
b:SetPoint("CENTER", 0, 0); b:SetWidth(37); b:SetHeight(37)
end
bag_n = 0
function bag_hover()
+32 -4
View File
@@ -419,13 +419,37 @@ const WIDGET_PROBES: &[(&str, &str, &str)] = &[
("0x87abb0", "ColorSelect", "PGColor"),
("0x87b960", "MessageFrame", "PGMessage"),
("0x87b5c0", "ScrollingMessageFrame", "PGScrollMsg"),
("0x878948", "PlayerModel", "PGModel"),
("0x878948", "Model", "PGModel"),
// ── The eleven tables this list did NOT cover until 2142 ──────────────────────────────────
// Twelve of the reference's twenty-three widget tables were mapped, so 11 classes — 190 of
// its 481 widget rows, `GameTooltip`'s 46 among them — were skipped by BOTH the arity gate
// and the kind gate, silently and by omission rather than by a stated narrowing. The
// identities are the corpus census's (2142), each read off its table's own distinctive
// members rather than guessed: `SetOwner`/`AddDoubleLine` is GameTooltip's, `CopyFontObject`
// the Font object's, `PingLocation` the Minimap's, `Dress`/`TryOn` DressUpModel's,
// `CycleVariation` TabardModel's, `StartMovie` MovieFrame's, `SetSlot` LootButton's.
("0x854198", "GameTooltip", "PGTip"),
("0x84c538", "Minimap", "PGMinimap"),
("0x84ee40", "TabardModel", "PGTabard"),
("0x84f190", "DressUpModel", "PGDress"),
// `PlayerModel` is `Model` plus three verbs, so its own table needs its own instance — the
// `0x878948` row above is now the plain `Model` it always was.
("0x84f1fc", "PlayerModel", "PGPlayerModel"),
("0x87ab4c", "MovieFrame", "PGMovie"),
("0x847ce4", "LootButton", "PGLoot"),
// The base map every widget misses into (its 19 names are exactly `script/mod.rs`'s
// `REGION_MAP_METHODS`). It has no class of its own, so it is probed on a Frame — which is
// what "its own class" means for a base: every widget answers it.
("0x87c9b8", "Frame", "PGRegionBase"),
];
/// The two region classes, which are made by a frame rather than by `CreateFrame`.
const REGION_PROBES: &[(&str, &str)] = &[
("0x87c128", "PGFrame:CreateTexture('PGTex')"),
("0x87c1d8", "PGFrame:CreateFontString('PGFS')"),
// The FONT OBJECT (`CreateFont`), not a FontString — 22 methods of its own, and the third
// thing here that `CreateFrame` cannot make (2142).
("0x87c7c8", "CreateFont('PGFontObject')"),
];
/// **The widget half of the gate** (decision 1843) — the half that would have caught 1840's
@@ -461,7 +485,7 @@ fn every_widget_method_answers_the_reference_s_return_arity() {
}
}
assert!(
made.len() >= 8,
made.len() >= 20,
"only {} widget classes could be instantiated — the probe set is broken",
made.len()
);
@@ -511,7 +535,10 @@ fn every_widget_method_answers_the_reference_s_return_arity() {
}
assert!(
checked >= 60,
// 121 at 2142, when the probe set went from 12 of the reference's 23 widget tables to
// all 23. The floor it replaces was 60 — set at the coverage of 1843's day, and never
// raised, so eleven whole classes could go unprobed without this number moving.
checked >= 110,
"the widget gate measured only {checked} methods — it has stopped covering anything"
);
assert!(
@@ -788,7 +815,8 @@ fn every_widget_method_answers_the_reference_s_return_kinds() {
}
assert!(
checked >= 40,
// 119 at 2142 — the same widening. The floor it replaces was 40.
checked >= 110,
"the widget kind gate measured only {checked} methods — it has stopped covering anything"
);
assert!(
@@ -681,7 +681,9 @@ fn no_shipped_script_sets_a_global_string_key_as_display_text() {
// — so XML comment blocks are tracked across lines and Lua `--` lines are dropped.
fn sink_literals(line: &str) -> Vec<&str> {
let mut out = Vec::new();
for sink in [":SetText(", ":SetFormattedText(", ":SetButtonText("] {
// `:SetFormattedText(` used to sit between these two and never could have matched —
// it is not a 1.12 verb, so no shipped script writes it (2142 retired ours).
for sink in [":SetText(", ":SetButtonText("] {
let mut rest = line;
while let Some(at) = rest.find(sink) {
rest = &rest[at + sink.len()..];