Read the test that reports errors first: 20 more, from four causes

The guild and friends clusters — 26 tests between them — were one nil function.
`guild_tests::driving_the_guild_windows_raises_no_script_errors` exists to name
its errors, and all four of them were the same line: `attempt to call global
'GetDifficultyColor' (a nil value)` at `FriendsFrame.lua:523`, reached from four
entry points. `GetDifficultyColor` is FrameXML's, not a client global
(`reference/1.12-globals.tsv` types it so), and `QuestLogFrame.lua` defines it —
ten toc rows above `FriendsFrame.xml`. The raise took the rest of the roster
update with it, including the `GuildPlayerStatusFrame:Show()` that ends that
branch, which is how it read as "the guild tab opens and the player pane is not
visible": an assertion about visibility with no hint of a nil function behind it.
One prerequisite row took guild 17 → 1 and friends 9 → 1.

The reputation, honor and skills pages (20 more) shared a fixture gap: their
player `UnitState` had a level, a race and a class and **no name**. The realm's
`CharacterFrame_OnShow` formats `gsub(UnitPVPName("player"), "\n.+", "")`, and
`UnitPVPName` answers nil for a nameless unit, so every one of those pages raised
`bad argument #1 to 'gsub'` on the way up and never finished showing.

The inspect page's arena tab raised three more: its team buttons inherit
`ArenaTeamTemplate`, which is a FrameXML row of the realm's own
(`Turtle_ArenaUI\Turtle_ArenaUITemplates.xml`) and not in the addon's templates
file, so the inherits was skipped, `$parentText` was never built, and the OnLoad's
`_G[this:GetName().."Text"]:SetText("(2v2)")` had nothing to index. That fixture
already said "Before the inspect addon, as `inherits=` resolves at load" — it was
simply missing this one.

And five expected a rank title without its trailing space. The realm's
`GlobalStrings.lua` gives every rank one — `PVP_RANK_8_1 = "Master Sergeant "`,
where stock 1.12's copy has none — because its own callers concatenate a name
after the title. The titles are now read off `PVP_RANK_<rank>_<team>` through one
helper, so the realm owns its own strings; that also turned up a wrong index
hiding behind the stale spelling (Grand Marshal is rank 18, not 14).

136 → 116, nothing newly failing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-28 22:41:36 -06:00
co-authored by Claude Opus 5
parent bae75b9d3f
commit 1aa3418fdc
4 changed files with 84 additions and 7 deletions
@@ -32,6 +32,19 @@ fn state() -> HonorState {
/// The character window, whose fifth tab this page is; [`super::test_ui::CHARACTER_UI`] carries
/// `HonorFrame.xml`, as `PaperDollFrame.lua:103` writes into it on every show.
/// The realm's own `PVP_RANK_<rank>_<team>` string.
///
/// Never a copy written here: the realm's `GlobalStrings.lua` gives every rank a **trailing
/// space** (`PVP_RANK_8_1 = "Master Sergeant "`, where stock 1.12's `patch.MPQ` copy has none),
/// because its own callers concatenate a name after the title. Pinning the stock spelling made
/// five tests in this file assert against a string the realm does not ship.
fn rank_title(s: &UiScript, rank: u32, team: u32) -> String {
s.eval::<String>(&format!(
"return PVP_RANK_{rank}_{team} or \"<no PVP_RANK_{rank}_{team}>\""
))
.expect("the realm's own rank string")
}
fn load_page(s: &UiScript) {
for file in super::test_ui::CHARACTER_UI {
super::test_ui::load_ui_strict(s, file);
@@ -43,6 +56,12 @@ fn load_page(s: &UiScript) {
fn alliance_player() -> UnitState {
UnitState {
exists: true,
// Named: the window this page lives in formats the player's name on every show —
// `CharacterFrame_OnShow` does `gsub(UnitPVPName("player"), "\n.+", "")`
// (`CharacterFrame.lua:85`) — and `UnitPVPName` answers nil for a unit with no name, so a
// nameless fixture raises `bad argument #1 to 'gsub'` and takes the rest of that OnShow
// with it.
name: Some("Tester".into()),
level: 60,
sex: 2,
faction_group: Some("Alliance".into()),
@@ -93,7 +112,7 @@ fn every_figure_lands_in_its_own_row() {
("HonorFrameLifeTimeHKValue", "3907"),
("HonorFrameLifeTimeDKValue", "12"),
// The highest rank's title (14), not the current one (12).
("HonorFrameLifeTimeRankValue", "Lieutenant Commander"),
("HonorFrameLifeTimeRankValue", &rank_title(&s, 14, 1)),
] {
assert_eq!(text(&mut s, frame), want, "{frame}");
}
@@ -105,7 +124,10 @@ fn every_figure_lands_in_its_own_row() {
fn the_title_is_the_internal_rank_and_the_badge_is_the_visual_one() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = shown_honor_page();
assert_eq!(text(&mut s, "HonorFrameCurrentPVPTitle"), "Knight-Captain");
assert_eq!(
text(&mut s, "HonorFrameCurrentPVPTitle"),
rank_title(&s, 12, 1)
);
assert_eq!(text(&mut s, "HonorFrameCurrentPVPRank"), "(Rank 8)");
assert!(
s.eval::<bool>("return HonorFramePvPIcon:IsShown()")
@@ -178,12 +200,12 @@ fn a_gm_flagged_player_keeps_his_rank_title_and_loses_only_the_faction_group() {
s.resolve();
assert_eq!(
text(&mut s, "HonorFrameLifeTimeRankValue"),
"Grand Marshal",
rank_title(&s, 18, 1),
"the Highest Rank row — the reported symptom"
);
assert_eq!(
text(&mut s, "HonorFrameCurrentPVPTitle"),
"Grand Marshal",
rank_title(&s, 18, 1),
"and the title under the badge, which reads the same key"
);
assert!(
@@ -278,7 +300,22 @@ fn the_real_global_strings_name_the_rank() {
load_page(&s);
s.set_honor(Some(state()));
for (team, want) in [(1i8, "Knight-Captain"), (0, "Legionnaire")] {
// The expected title comes off the realm's **own** `PVP_RANK_<rank>_<team>` string, not a copy
// written here: the realm's `GlobalStrings.lua` gives every rank a **trailing space**
// (`PVP_RANK_8_1 = "Master Sergeant "`, where stock 1.12's `patch.MPQ` copy has none), because
// its own callers concatenate a name after the title. Pinning the stock spelling here made
// five tests in this file assert against a string the realm does not ship.
//
// Rank 12 is the fixture's (`state`), and the team digit is what this is really testing: the
// same rank reads as the Alliance title for team 1 and the Horde one for team 0.
for team in [1i8, 0] {
let want = s
.eval::<String>(&format!(
"return PVP_RANK_12_{} or \"<no PVP_RANK_12_{}>\"",
if team == 1 { 1 } else { 0 },
if team == 1 { 1 } else { 0 }
))
.expect("the realm's own rank string");
s.set_unit(
"player",
Some(UnitState {
@@ -361,6 +398,13 @@ fn shown_inspect_honor_page() -> UiScript {
// `ItemButtonTemplate`, its honor rows `HonorFrame.xml`'s row templates.
"Interface\\FrameXML\\ItemButtonTemplate.xml",
"Interface\\FrameXML\\HonorFrame.xml",
// …and its **arena** page's three team buttons inherit `ArenaTeamTemplate`, which is a
// FrameXML row of the realm's (`Turtle_ArenaUI\Turtle_ArenaUITemplates.xml`, toc row 109)
// rather than anything in the addon's own templates file. Without it the inherits is
// skipped, so `$parentText` is never built, and the OnLoad's
// `_G[this:GetName().."Text"]:SetText("(2v2)")` (`InspectArenaFrame.xml:71`) raises
// `attempt to index field '?'` — three times, once per team.
r"Interface\FrameXML\Turtle_ArenaUI\Turtle_ArenaUITemplates.xml",
] {
load_xml(&s, file);
}
@@ -398,14 +442,14 @@ fn the_inspect_page_paints_the_reply_it_holds() {
("InspectHonorFrameLastWeekStandingValue", "57"),
("InspectHonorFrameLifeTimeHKValue", "3907"),
("InspectHonorFrameLifeTimeDKValue", "12"),
("InspectHonorFrameLifeTimeRankValue", "Lieutenant Commander"),
("InspectHonorFrameLifeTimeRankValue", &rank_title(&s, 14, 1)),
] {
assert_eq!(text(&mut s, frame), want, "{frame}");
}
// The target's current rank, 12, and the bar from the reply's own byte.
assert_eq!(
text(&mut s, "InspectHonorFrameCurrentPVPTitle"),
"Knight-Captain"
rank_title(&s, 12, 1)
);
assert_eq!(text(&mut s, "InspectHonorFrameCurrentPVPRank"), "(Rank 8)");
let bar = s
@@ -63,6 +63,12 @@ fn load_page(s: &UiScript) {
fn player(level: u32) -> UnitState {
UnitState {
exists: true,
// Named, because the window this page lives in formats the player's name on **every**
// show: `CharacterFrame_OnShow` does `gsub(UnitPVPName("player"), "\n.+", "")`
// (`CharacterFrame.lua:85`), and `UnitPVPName` answers nil for a unit with no name — so a
// nameless fixture raises `bad argument #1 to 'gsub'` and takes the rest of that OnShow
// with it, including the sub-frame show this page needs.
name: Some("Tester".into()),
level,
race: Some("Human".into()),
race_file: Some("Human".into()),
@@ -53,6 +53,12 @@ fn shown_skills_page_with(measurer: Option<Box<dyn benilla_ui::script::TextMeasu
"player",
Some(UnitState {
exists: true,
// Named: the window this page lives in formats the player's name on every show —
// `CharacterFrame_OnShow` does `gsub(UnitPVPName("player"), "\n.+", "")`
// (`CharacterFrame.lua:85`) — and `UnitPVPName` answers nil for a unit with no name, so a
// nameless fixture raises `bad argument #1 to 'gsub'` and takes the rest of that OnShow
// with it.
name: Some("Tester".into()),
level: 60,
race: Some("Tauren".into()),
race_file: Some("Tauren".into()),
@@ -457,6 +463,12 @@ fn the_list_reaches_its_last_row_at_the_bars_end() {
"player",
Some(UnitState {
exists: true,
// Named: the window this page lives in formats the player's name on every show —
// `CharacterFrame_OnShow` does `gsub(UnitPVPName("player"), "\n.+", "")`
// (`CharacterFrame.lua:85`) — and `UnitPVPName` answers nil for a unit with no name, so a
// nameless fixture raises `bad argument #1 to 'gsub'` and takes the rest of that OnShow
// with it.
name: Some("Tester".into()),
level: 60,
race: Some("Human".into()),
race_file: Some("Human".into()),
@@ -141,6 +141,21 @@ const PREREQS: &[(&str, &[&str])] = &[
// window 0, and `FloatingChatFrame_Update` hands `FCF_SetWindowName` a frame it cannot name —
// two loader errors, and **137 of this crate's tests** died on them, every one that seats any
// part of the chat UI.
// `GetDifficultyColor` is a **FrameXML** function (`reference/1.12-globals.tsv` types it
// `framexml`, not a client global) and `QuestLogFrame.lua` is what defines it — ten toc rows
// above this one. `FriendsFrame.lua:523` calls it for every roster row's level colour, from
// four different entry points (`FriendsFrame_Update`, `FriendsFrame_OnEvent`,
// `FriendsFrameGuildStatusButton_OnClick`, `GuildFrameGuildListToggleButton_OnClick`), and the
// raise takes the rest of the pane's update with it — including the
// `GuildPlayerStatusFrame:Show()` at the end of that branch (`:579`), which is how it reads as
// "the guild tab opens and the player pane is not visible".
//
// The `.lua` alone, not `QuestLogFrame.xml`: the functions are what is missing, and the window
// would drag its own prerequisites in for frames no guild test looks at.
(
"friendsframe.xml",
&[r"Interface\FrameXML\QuestLogFrame.lua"],
),
(
"floatingchatframe.xml",
&[r"Interface\FrameXML\ChatFrame.xml"],