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