Nine reds where the realm changed the data under a transcribed assertion

None of these were defects; each is a value, a row index or a semantic the realm moved and the
test still asserted stock 1.12's:

- `RAID_CLASS_COLORS.SHAMAN` is the later blue `|cff0070de` (`Fonts.xml:50`), not a copy of
  PALADIN's pink — 1.12 reused the row and the realm did not.
- `ChatTypeInfo` has 106 keys, the extra being the realm's `HARDCORE`, and ten of them are sticky
  rather than five: it marks `CHANNEL`, `OFFICER`, `RAID_WARNING` and `WHISPER` too. Sticky is what
  the edit box keeps between sends, so that list is typing behaviour a player feels.
- `ChatMenu`'s Macro row is eleventh: `HARDCORE_MESSAGE` went in fourth (`ChatFrame.lua:2371`).
- A friend row is coloured three ways (`FriendsFrame.lua:186-197`) — name and class from
  `RAID_CLASS_COLORS[TW_CLASS_TOKEN[class]]`, level from `GetDifficultyColor`. `TW_CLASS_TOKEN`
  lives in the realm's `Globals.lua`, so that row joins `SOCIAL_UI`; without it the window draws
  uncoloured, which is a different window than the client paints.
- A guild row goes offline by **alpha 0.55** on all four columns (`FriendsFrame.lua:526`), keeping
  its class and difficulty inks, where 1.12 wrote a flat grey.
- The minimap clock plate is four lines — `Clock`, then a Zone/Server/Local time triple
  (`GameTime.lua:50-56`) — not one bare time.
- `ERR_TAXINOPATHS` uses a straight ASCII apostrophe; the test looked for U+2019 and so never
  matched a line that was being drawn correctly all along.
- `DropDownList1/2`'s OnHide **wipes** `OPEN_DROPDOWNMENUS[n]` instead of dropping it
  (`UIDropDownMenu.xml:17`, `:28`), so the row is emptied in place and an addon's reference to it
  stays live. Asserted both halves.
- `CONFIRM_PET_UNLEARN` gained an `OnUpdate` that hides it unless `CheckPetUntrainerDist()` answers
  (`StaticPopup.lua:1290-1294`), so the dialog needs that verb seated before the first tick.
- The spellbook shows its Spellbook tab with no pet at all (the realm sets tab 1 up on both arms of
  `hasPetSpells`), and it **fixed** both pet-paging quirks this test had pinned as "DO NOT FIX":
  `SpellBook_GetSpellID`'s pet arm carries the page term and Next writes the pet book's own page.

Also corrected a test of mine from the last batch: `IsEnabled()` answers the number 0, which is
truthy in Lua, so it is compared and not tested — as the stock UI itself does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-31 13:13:33 -06:00
co-authored by Claude Opus 5
parent d49f8daad8
commit 04a3f68009
11 changed files with 164 additions and 44 deletions
+16 -2
View File
@@ -428,7 +428,15 @@ fn chat_type_info_has_the_references_shape() {
let count: i64 = s
.eval("local n = 0 for _ in pairs(ChatTypeInfo) do n = n + 1 end return n")
.unwrap();
assert_eq!(count, 105, "the reference declares 105 keys");
assert_eq!(
count, 106,
"the reference declares 106 keys — 1.12's 105 plus the realm's own HARDCORE row"
);
assert!(
s.eval::<bool>("return ChatTypeInfo.HARDCORE ~= nil")
.unwrap(),
"the extra key is HARDCORE, the realm's death-announce type, not a stray of ours"
);
let sticky: String = s
.eval(
@@ -436,7 +444,13 @@ fn chat_type_info_has_the_references_shape() {
table.insert(t, k) end end table.sort(t) return table.concat(t, \" \")",
)
.unwrap();
assert_eq!(sticky, "BATTLEGROUND GUILD PARTY RAID SAY");
// Ten, not 1.12's five: the realm marks `CHANNEL`, `OFFICER`, `RAID_WARNING`, `WHISPER` and
// its own `HARDCORE` sticky too (`ChatFrame.lua`'s `ChatTypeInfo` block). Sticky is what the
// edit box keeps between sends, so this list is the typing behaviour a player feels.
assert_eq!(
sticky,
"BATTLEGROUND CHANNEL GUILD HARDCORE OFFICER PARTY RAID RAID_WARNING SAY WHISPER"
);
// REPLY and COMBAT_ERROR are FrameXML's own, absent from the engine's 94-entry registry, so
// both have id 0, but their colors differ: the `UPDATE_CHAT_COLOR` arm copies WHISPER's
@@ -58,19 +58,35 @@ fn hiding_a_list_clears_its_open_menu_registry_entry() {
)
.unwrap();
// The realm **wipes** the row rather than dropping it: both lists' OnHide run
// `wipe(OPEN_DROPDOWNMENUS[n])` (`UIDropDownMenu.xml:17`, `:28`), where stock 1.12 assigned
// nil. The table identity therefore outlives the menu, and what clears is its contents —
// which is what `UnitPopup_OnUpdate`'s walk reads, since it keys off `.which`.
let table_ids: (String, String) = s
.eval("return tostring(OPEN_DROPDOWNMENUS[1]), tostring(OPEN_DROPDOWNMENUS[2])")
.unwrap();
s.run("DropDownList2:Hide();").unwrap();
assert!(
s.eval::<bool>("return OPEN_DROPDOWNMENUS[2] == nil")
s.eval::<bool>("return next(OPEN_DROPDOWNMENUS[2]) == nil")
.unwrap(),
"level 2's registry entry survived its list hiding"
);
s.run("DropDownList1:Hide();").unwrap();
assert!(
s.eval::<bool>("return OPEN_DROPDOWNMENUS[1] == nil")
s.eval::<bool>("return next(OPEN_DROPDOWNMENUS[1]) == nil")
.unwrap(),
"level 1's registry entry survived its list hiding"
);
assert_eq!(
s.eval::<(String, String)>(
"return tostring(OPEN_DROPDOWNMENUS[1]), tostring(OPEN_DROPDOWNMENUS[2])"
)
.unwrap(),
table_ids,
"wiped in place, not replaced — an addon holding a reference keeps seeing the live row"
);
}
/// `UIDropDownMenu.lua:16` declares the height nil; list 1's OnLoad sets it from Button1's font
@@ -152,15 +152,19 @@ fn friend_rows_render_the_online_and_offline_templates() {
"FRIENDLIST_UPDATE",
);
// The realm colours an online row three ways (`FriendsFrame.lua:186-197`): the name and the
// class in `RAID_CLASS_COLORS[TW_CLASS_TOKEN[class]].hex` — rogue yellow `|cfffff569` — and
// the level in `GetDifficultyColor(level).hex`, which against this fixture's level-less
// player reads as the hostile red `|cffff1919`. Stock 1.12 wrote all three plain.
assert_eq!(
s.eval::<String>("return FriendsFrameFriendButton1ButtonTextNameLocation:GetText()")
.unwrap(),
"Onerogue |cffffffff- Elwynn Forest|r <AFK>"
"|cfffff569Onerogue|r |cffffffff- Elwynn Forest|r <AFK>"
);
assert_eq!(
s.eval::<String>("return FriendsFrameFriendButton1ButtonTextInfo:GetText()")
.unwrap(),
"Level 60 Rogue"
"Level |cffff191960|r |cfffff569Rogue|r"
);
assert_eq!(
s.eval::<String>("return FriendsFrameFriendButton2ButtonTextNameLocation:GetText()")
@@ -280,15 +280,34 @@ fn an_offline_member_keeps_its_columns_and_only_greys() {
);
assert_eq!(text(&s, "GuildFrameButton3Class"), "Mage");
// The realm greys an offline row with **alpha**, not with a grey ink
// (`FriendsFrame.lua:526`, `local alpha = online and 1 or 0.55`, applied to all four columns).
// Its colours are the class's and the level's difficulty, online or not — stock 1.12 wrote
// gold names and flat `(0.5, 0.5, 0.5)` for an offline one.
assert_eq!(
colour(&s, "GuildFrameButton3Name"),
(0.5, 0.5, 0.5),
"offline rows go flat grey"
(0.41, 0.8, 0.94),
"a mage keeps mage blue offline (`RAID_CLASS_COLORS.MAGE`)"
);
for column in ["Name", "Zone", "Level", "Class"] {
let faded = s
.eval::<f64>(&format!("return GuildFrameButton3{column}:GetAlpha()"))
.unwrap();
assert!(
(faded - 0.55).abs() < 1e-6,
"{column} fades with the row, which is the whole of being offline here: {faded}"
);
assert_eq!(
s.eval::<f64>(&format!("return GuildFrameButton1{column}:GetAlpha()"))
.unwrap(),
1.0,
"{column} of an online row is opaque"
);
}
assert_eq!(
colour(&s, "GuildFrameButton1Name"),
(1.0, 0.82, 0.0),
"an online name keeps the gold"
(0.78, 0.61, 0.43),
"and an online warrior is warrior tan, not 1.12's gold"
);
}
@@ -201,17 +201,28 @@ fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
"OnEnter owns the tooltip (the Scripts-walker auto-enable capturing at {x},{y})"
);
// The enGB `LocalizeFrames` sets `TwentyFourHourTime = 1` (`Localization.lua:20`): 21:07.
let text = |s: &UiScript| {
s.eval::<String>("return GameTooltipTextLeft1:GetText()")
// The realm's clock plate is four lines, not stock 1.12's one bare time:
// `GAMETIME_TITLE` then a `Zone time:` / `Server time:` / `Local time:` triple of double
// lines (`GameTime.lua:50-56`). The enGB `LocalizeFrames` sets `TwentyFourHourTime = 1`
// (`Localization.lua:20`), so the zone row reads 21:07.
let text = |s: &UiScript, line: &str| {
s.eval::<String>(&format!("return GameTooltipText{line}:GetText()"))
.unwrap()
};
assert_eq!(text(&s), "21:07");
assert_eq!(text(&s, "Left1"), "Clock");
assert_eq!(text(&s, "Left2"), "Zone time:");
assert_eq!(text(&s, "Right2"), "21:07");
assert_eq!(text(&s, "Left3"), "Server time:");
assert_eq!(text(&s, "Left4"), "Local time:");
// While owned, `GameTimeFrame_Update` refreshes the tooltip (`GameTime.lua:20-22`).
// While owned, `GameTimeFrame_Update` refreshes the tooltip (`GameTime.lua:44-45`).
s.run("__benilla_game_minute = 8").unwrap();
s.tick(0.016);
assert_eq!(text(&s), "21:08", "the owned tooltip follows the clock");
assert_eq!(
text(&s, "Right2"),
"21:08",
"the owned tooltip follows the clock"
);
// Inside the raw 50x50 rect but in the left inset: not hoverable.
s.mouse_move(l + 2.0, y);
@@ -397,13 +397,36 @@ fn the_pet_tab_switches_books_and_renders_the_pets_spells() {
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
s.set_spellbook(book());
// ── No pet: no toggle row, and no pet book (`SpellBookFrame.lua:11-14`) ──────────────────
// ── No pet: the Spellbook tab alone, and no pet book ────────────────────────────────────
s.run("ToggleSpellBook(BOOKTYPE_SPELL)").unwrap();
assert!(s.errors().is_empty(), "open errors: {:?}", s.errors());
// The realm sets up tab 1 on **both** arms of its `hasPetSpells` fork
// (`SpellBookFrame.lua:211-216`), and `SpellBookFrame_SetTabType` ends in `tabButton:Show()`
// (`:253`). Stock 1.12 built the row only when there was a pet book to switch to, so the
// whole row stayed hidden; here the Spellbook tab is always up, and disabled because it is
// the open book.
assert!(
!s.eval::<bool>("return SpellBookFrameTabButton1:IsVisible()")
s.eval::<bool>("return SpellBookFrameTabButton1:IsVisible()")
.unwrap(),
"with no pet spells the ref hides the whole toggle row"
"the Spellbook tab shows with no pet"
);
assert_eq!(
s.eval::<String>("return SpellBookFrameTabButton1:GetText()")
.unwrap(),
"Spellbook"
);
// `IsEnabled()` answers the number 0, not false — 0 is truthy in Lua, which is why the stock
// UI compares it (`== 0`, `FriendsFrame.lua:404`) instead of testing it.
assert_eq!(
s.eval::<i64>("return SpellBookFrameTabButton1:IsEnabled()")
.unwrap(),
0,
"the tab for the open book is disabled (`:249-250`)"
);
assert!(
!s.eval::<bool>("return SpellBookFrameTabButton2:IsVisible()")
.unwrap(),
"…and there is no second tab without a pet book behind it"
);
s.run("ToggleSpellBook(BOOKTYPE_PET)").unwrap();
assert!(
@@ -548,26 +571,35 @@ fn the_pet_tab_switches_books_and_renders_the_pets_spells() {
"a right-click on the pet page must not also cast"
);
// ── Two reference quirks, pinned ───────────────────────────────────────────────────────────
// The pet page ignores its page number: `SpellBook_GetSpellID`'s pet arm is a bare `return id`
// (`SpellBookFrame.lua:460-461`).
// ── Two 1.12 quirks the realm fixed ───────────────────────────────────────────────────────
// Stock `SpellBook_GetSpellID`'s pet arm was a bare `return id`, so the pet page number was
// never applied; the realm's is `id + SPELLS_PER_PAGE * (page - 1)` like the spell arm
// (`SpellBookFrame.lua:532-537`). Page 2 therefore starts at the thirteenth pet spell.
assert_eq!(s.eval::<i64>("return SpellBook_GetSpellID(1)").unwrap(), 1);
s.run(r#"SPELLBOOK_PAGENUMBERS["pet"] = 2"#).unwrap();
assert_eq!(
s.eval::<i64>("return SpellBook_GetSpellID(1)").unwrap(),
1,
"DO NOT FIX: the ref's pet arm has no page term"
1 + s.eval::<i64>("return SPELLS_PER_PAGE").unwrap(),
"the realm's pet arm carries the page term"
);
// …and Next writes `SPELLBOOK_PAGENUMBERS[selectedSkillLine]` on both books
// (`SpellBookFrame.lua:423`), so the pet page stays put.
// And Next writes the pet book's own page rather than the selected skill line's
// (`:490-500`), so paging works — which also means the arrows gate on the pet book's length:
// two pet spells is one page, so Next is disabled and a click cannot move it.
s.run(r#"SPELLBOOK_PAGENUMBERS["pet"] = 1"#).unwrap();
s.run("SpellBook_UpdatePageArrows()").unwrap();
assert_eq!(
s.eval::<i64>("return SpellBookNextPageButton:IsEnabled()")
.unwrap(),
0,
"one page of pet spells, so there is nowhere to turn to"
);
click(&mut s, "SpellBookNextPageButton", "LeftButton");
assert!(s.errors().is_empty(), "page errors: {:?}", s.errors());
assert_eq!(
s.eval::<i64>(r#"return SPELLBOOK_PAGENUMBERS["pet"]"#)
.unwrap(),
1,
"DO NOT FIX: the ref writes SPELLBOOK_PAGENUMBERS[selectedSkillLine] on both books"
1
);
assert_eq!(
s.eval::<String>("return SpellButton1SpellName:GetText()")
@@ -255,6 +255,11 @@ fn the_verb_dialogs_open_from_their_events_and_call_their_verbs() {
let mut s = setup();
load_xml(&s, r"Interface\FrameXML\UIParent.xml"); // the arms
s.set_money(50_000);
// The realm gave this dialog an `OnUpdate` that hides it the moment `CheckPetUntrainerDist()`
// stops answering (`StaticPopup.lua:1290-1294`) — stock 1.12 had no such arm, so the dialog
// used to stand on its own. The verb has to answer before the first tick or the dialog is
// gone before anything can click it.
s.set_pet_untrainer_pending(true);
s.fire_event("CONFIRM_PET_UNLEARN", vec![ScriptValue::Int(12_345)]);
s.tick(0.0);
assert_eq!(
@@ -132,7 +132,10 @@ fn no_single_hop_destination_posts_the_error_and_closes() {
let quads = s.extract();
let has_refusal = quads.iter().any(|q| match &q.content {
benilla_ui::script::QuadContent::Text { text: Some(t), .. } => {
t.contains("don\u{2019}t know any flight locations")
// A straight ASCII apostrophe: the realm's `GlobalStrings.lua` writes
// `"You don't know any flight locations connected to this one."`, not the curly
// U+2019 some of its other strings use.
t.contains("don't know any flight locations")
}
_ => false,
});
@@ -689,6 +689,11 @@ pub(super) const CHARACTER_UI: &[&str] = &[
pub(super) const SOCIAL_UI: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
// The realm's own globals (toc row 2, right after `GlobalStrings.lua`). `TW_CLASS_TOKEN` is
// the localized-class-name → token map its `FriendsFrame.lua:187-192` and
// `GuildFrame.lua` colour rows through; without it those rows silently draw uncoloured, which
// is a different window than the one the client paints.
r"Interface\FrameXML\Globals.lua",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\BasicControls.xml",
r"Interface\FrameXML\UIParent.xml",
@@ -49,25 +49,27 @@ fn chat_menu_rows_left_align_their_label_and_right_align_their_shortcut() {
assert!(s.errors().is_empty(), "opening it raises: {:?}", s.errors());
s.resolve();
// Macro is the tenth row `ChatMenu_OnLoad` adds (`ChatFrame.lua:2301`).
// Macro is the **eleventh** row `ChatMenu_OnLoad` adds (`ChatFrame.lua:2378`): the realm
// inserted its own `HARDCORE_MESSAGE` row fourth (`:2371`), which pushed everything below it
// down one, so stock 1.12's tenth seat now holds Voice Emote.
assert_eq!(
s.eval::<String>("return ChatMenuButton10:GetText()")
s.eval::<String>("return ChatMenuButton11:GetText()")
.unwrap(),
"Macro"
);
assert_eq!(
s.eval::<String>("return ChatMenuButton10ShortcutText:GetText()")
s.eval::<String>("return ChatMenuButton11ShortcutText:GetText()")
.unwrap(),
"/macro"
);
// `UIMenu_Initialize` fixes the row at 104 wide (`UIMenu.lua:16`).
let (row_l, row_r) = edges(&mut s, "ChatMenuButton10");
let (row_l, row_r) = edges(&mut s, "ChatMenuButton11");
assert_eq!(row_r - row_l, 104.0, "UIMENU_BUTTON_WIDTH");
// The label's implicit anchor is LEFT to LEFT (0, 0), from `<NormalFont justifyH="LEFT"/>`.
let (p, rp, x, y): (String, String, f32, f32) = s
.eval(
"local p, _, rp, x, y = ChatMenuButton10:GetFontString():GetPoint(1) \
"local p, _, rp, x, y = ChatMenuButton11:GetFontString():GetPoint(1) \
return p, rp, x, y",
)
.unwrap();
@@ -76,10 +78,10 @@ fn chat_menu_rows_left_align_their_label_and_right_align_their_shortcut() {
("LEFT", "LEFT", 0.0, 0.0),
"the label is anchored by the normal font's justify"
);
let (label_l, label_r) = edges(&mut s, "ChatMenuButton10:GetFontString()");
let (label_l, label_r) = edges(&mut s, "ChatMenuButton11:GetFontString()");
assert_eq!(label_l, row_l, "the label starts at the row's left edge");
// The template anchors the shortcut string RIGHT.
let (short_l, short_r) = edges(&mut s, "ChatMenuButton10ShortcutText");
let (short_l, short_r) = edges(&mut s, "ChatMenuButton11ShortcutText");
assert_eq!(short_r, row_r, "the shortcut ends at the row's right edge");
// With 8 px glyphs "Macro" spans 40 and "/macro" 48 of 104; a centred label (32..72) overlaps.
assert!(
@@ -157,15 +157,24 @@ fn raid_class_colors_is_the_references_own_nine() {
"keyed by UnitClass's uppercase second return, which is how addons index it"
);
// SHAMAN has PALADIN's colour in the reference; not a typo to fix.
// Stock 1.12 gave SHAMAN a copy of PALADIN's pink (shamans being Horde-only, the table
// reused the row); the realm's own `Fonts.xml:50` gives it the later blue instead, so the two
// rows are distinct here. Transcribed from the player's file, not corrected by us either way.
assert_eq!(
s.eval::<(f64, f64, f64)>(
"local p, h = RAID_CLASS_COLORS.PALADIN, RAID_CLASS_COLORS.SHAMAN \
return p.r - h.r, p.g - h.g, p.b - h.b"
)
.unwrap(),
(0.0, 0.0, 0.0),
"SHAMAN shares PALADIN's pink in 1.12 — transcribed, not corrected"
s.eval::<(f64, f64, f64)>("local c = RAID_CLASS_COLORS.SHAMAN return c.r, c.g, c.b")
.unwrap(),
(0.0, 0.44, 0.87)
);
assert_eq!(
s.eval::<String>("return RAID_CLASS_COLORS.SHAMAN.hex")
.unwrap(),
"|cff0070de",
"and the `hex` every addon concatenates matches the three floats"
);
assert_eq!(
s.eval::<(f64, f64, f64)>("local c = RAID_CLASS_COLORS.PALADIN return c.r, c.g, c.b")
.unwrap(),
(0.96, 0.55, 0.73)
);
assert_eq!(
s.eval::<(f64, f64, f64)>("local c = RAID_CLASS_COLORS.HUNTER return c.r, c.g, c.b")