Files
benilla-octo/crates/benilla-app/src/ui_script/help_frame_tests.rs
T
SamandClaude Fable 5.1 058ca4b6c8 every dialog is the reference's own, and the trainer loads on demand
- every dialog is the reference's own: `StaticPopup.xml` + `StaticPopup.lua` load off the player's chain, and 0308's engine with its 37 dialogs leaves `UiPanels.xml`; all 76 of the reference's dialogs are the stock table's, 28 of which no file of ours had ever registered (the resurrect family among them); the nine hidden listener frames that existed only because benilla once had no `UIParent_OnEvent` come home to it verbatim from UIParent.lua, and the seven files that were nothing but a dialog and its driver are deleted; what the swap surfaced: the reference capitalises the countdown's unit word ("5 Minutes until release") where ours had lowercased it, and ESC in the typed delete confirm's box is swallowed — the entry names no escape handler; arms for events this engine never fires (PLAYER_SKINNED, TRADE_REQUEST) wait on their feeds rather than being armed blind (1960)
- the social window and the unit menus are the reference's own files: `FriendsFrame.xml` loads off the chain and our transcription is deleted — its twelve dialogs re-homed, `FRIENDLIST_SHOW` fired as the answer to an explicit `ShowFriends()`, and `Get`/`SetLookingForGroup` a client-side flag until their bodies are carved; `UnitPopup.xml` loads off the chain, our row catalogue and its driver frame go, and nine verbs are built for it — `HasFullControl` off the player-control flag the reference reads at `0x51a158`, `UnitPlayerOrPetInParty`/`InRaid` over a new unit owner, and the six pet one-shots, each pressing the bar's own reaction or command slot word through the same latch and `CMSG_PET_ACTION`, confirmed by wow-re's carve; the Guild Leave/Promote confirms 1958 believed nothing registered were our social file's all along, and the duplicate went with it (1958, 1959)
- the trainer loads on demand: our `TrainerFrame.xml` is deleted and no row replaces it — `Blizzard_TrainerUI` loads off the player's own chain through the reference's `ClassTrainerFrame_LoadUI`, and the LoadOnDemand `Blizzard_*` addons are registry rows read off the chain; the timing IS the mechanism: the list builder writes the state-filter mask to 3 on every `SMSG_TRAINER_LIST` (`0x4d75d9`) and the stock addon pushes its saved filters exactly once, on its own ADDON_LOADED — which fires on the session's first trainer, after that packet, so the saved filters win that open and an eager load would never have applied them; the loader holds an anchor to a sibling not built yet and applies it once the subtree exists; and `UNIT_FLAGS` fires only on a change between two snapshots — wow-re's trio found the per-field watch bridge runs no notify pass on the create leg, correcting 1953 (1957)
- the additive window, built and withdrawn the same hour: under `BLEND_ADD` every additive particle draw commutes with every other, so same-key additive draws can be regrouped exactly — built in the effect lane's merge walk and measured on the crowd rig (Stormwind, forty partybots): the adjacency merge already folds about half the effect items and the regroup found two more folds in a frame of ninety-three, so the pre-regroup walk is what runs; what stays: an `fx=` items/merged census on the FPS probe line, a `DRESS_PART` line per drawn part of the self body on the dress census, and the regression the A/B uncovered: 1948's stock chat migration dropped the drain's plain-line sender, so every `WOW_PROBE_CHAT` rig — the crowd rig's forty `.partybot add`s among them — had gone silent on the server; a probe's slash-less line reaches the wire as SAY again (1955)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 20:57:24 +07:00

215 lines
8.2 KiB
Rust

//! The GM help window (decision 1673, HelpFrame.xml): the category list the DBC feeds it, the two
//! faces of `UPDATE_TICKET`, the queue gate, the ticket toast, and the three dialogs.
//!
//! Written as the **falsification** pass over the transcription rather than a demonstration of it:
//! every test is named after one claim the window makes, and each was checked to fail when the
//! claim is broken. The load-bearing one is
//! [`clicking_a_category_files_a_ticket_under_that_categorys_dbc_id`] — the id travels from
//! `GMTicketCategory.dbc` through a button, a page, and the editor onto the wire, and a break
//! anywhere in that chain files every ticket under the wrong heading with nothing on screen to
//! show for it.
use benilla_ui::script::{GmTicketIntent, ScriptValue, UiScript};
use super::test_ui::load_ui as load_xml;
/// The window, its dependencies, and the catalog the app pushes — the real ten `GMTicketCategory`
/// rows, so a test that walks the list is walking the shipped data.
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for file in [
"Interface\\FrameXML\\Fonts.xml",
"Interface\\FrameXML\\BasicControls.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"ScrollTemplates.xml",
"MoneyFrame.xml",
"GameTooltip.xml",
// Before UiPanels.xml: the shared StaticPopup carries a `SmallMoneyFrameTemplate` coin
// row, whose OnLoad calls `SmallMoneyFrame_OnLoad` — the TOC's own order (1580's
// talent-wipe fixture hit this first).
"MoneyFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"UiPanels.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
// The stock file's `HelpFrame_OnShow` calls `UpdateMicroButtons()` before
// `GetGMStatus()`, so without the micro row the OnShow raises and the status ask never
// happens. Ours never called it. MicroMenu needs the bar's button kit under it.
"Cooldown.xml",
"Interface\\FrameXML\\ActionButtonTemplate.xml",
"Interface\\FrameXML\\TextStatusBar.lua",
"Interface\\FrameXML\\TextStatusBar.xml",
"UIParent.xml",
"Interface\\FrameXML\\MainMenuBar.xml",
"Interface\\FrameXML\\ActionBarFrame.xml",
"Interface\\FrameXML\\BonusActionBarFrame.xml",
"MicroMenu.xml",
// `TicketStatusFrame_OnEvent` re-anchors TemporaryEnchantFrame before it arms the repoll
// timer, so without BuffFrame the handler raises and `refreshTime` is never set. This is
// the one ordering requirement benilla.toc already calls out for this window.
"Interface\\FrameXML\\BuffFrame.xml",
"Interface\\FrameXML\\HelpFrame.xml",
] {
load_xml(&s, file);
}
s.set_gm_ticket_categories(vec![
(1, "Stuck".into()),
(2, "Behavior/Harassment".into()),
(3, "Guild".into()),
(4, "Item".into()),
(5, "Environmental".into()),
(6, "Non-Quest/Creep".into()),
(7, "Quest/Quest NPC".into()),
(8, "Technical".into()),
(9, "Account/Billing".into()),
(10, "Character".into()),
]);
s
}
/// The `UPDATE_TICKET` argument list the app's feed builds for an open ticket — category first,
/// text second, exactly as `ui_gm_ticket::update_ticket_args` orders it. Kept in sync by being
/// written the same way in both places; if they ever disagree, this file's tests are what notices.
fn open_ticket_args(
category: i64,
text: &str,
age: f64,
oldest: f64,
update: f64,
) -> Vec<ScriptValue> {
vec![
ScriptValue::Int(category),
ScriptValue::Str(text.into()),
ScriptValue::Number(age),
ScriptValue::Number(oldest),
ScriptValue::Number(update),
ScriptValue::Int(0),
ScriptValue::Int(0),
]
}
/// **`UPDATE_TICKET`'s two faces.** With a ticket the editor becomes an editor (Save Changes /
/// Exit); with the bare `arg1 = 0` it goes back to being a form (Submit / Cancel). The zero leg is
/// the one that would silently rot: it is the ordinary answer, so a window stuck in edit mode
/// looks fine until you try to file a second ticket.
#[test]
fn an_open_ticket_turns_the_form_into_an_editor_and_a_zero_turns_it_back() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ShowUIPanel(HelpFrame) HelpFrame_ShowFrame(\"OpenTicket\")")
.unwrap();
s.fire_event(
"UPDATE_TICKET",
open_ticket_args(7, "Where is this NPC?", 0.25, 2.5, 0.01),
);
assert_eq!(
s.eval::<String>("return HelpFrameOpenTicketText:GetText()")
.unwrap(),
"Where is this NPC?",
"arg2 is the description"
);
assert_eq!(
s.eval::<i64>("return HelpFrameOpenTicket.ticketType")
.unwrap(),
7,
"arg1 is the category"
);
assert_eq!(
s.eval::<i64>("return HelpFrameOpenTicket.hasTicket")
.unwrap(),
1
);
// And now the ordinary answer.
s.fire_event("UPDATE_TICKET", vec![ScriptValue::Int(0)]);
assert_eq!(
s.eval::<String>("return HelpFrameOpenTicketText:GetText()")
.unwrap(),
"",
"the editor empties"
);
assert!(
s.eval::<bool>("return HelpFrameOpenTicket.hasTicket == nil")
.unwrap(),
"and stops believing it has a ticket"
);
}
/// The toast follows the ticket: up while one is open, gone when it is not. It is the only thing
/// on screen that says a ticket exists at all once the window is closed.
#[test]
fn the_ticket_toast_follows_the_ticket() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.fire_event(
"UPDATE_TICKET",
open_ticket_args(1, "Stuck.", 0.1, 0.2, 0.01),
);
assert!(
s.eval::<bool>("return TicketStatusFrame:IsVisible()")
.unwrap(),
"a ticket raises the toast"
);
s.fire_event("UPDATE_TICKET", vec![ScriptValue::Int(0)]);
assert!(
!s.eval::<bool>("return TicketStatusFrame:IsVisible()")
.unwrap(),
"and abandoning it takes the toast away"
);
}
/// The toast's own poll is what keeps a long wait honest: `TicketStatus_OnUpdate` re-asks the
/// server every `GMTICKET_CHECK_INTERVAL`, and not before. This is the reason the app counts
/// answers instead of diffing them, so it is worth a test on this side too.
#[test]
fn the_toast_repolls_the_server_only_after_the_full_interval() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.fire_event(
"UPDATE_TICKET",
open_ticket_args(1, "Stuck.", 0.1, 0.2, 0.01),
);
let _ = s.take_gm_ticket_intents();
s.run("TicketStatus_OnUpdate(599)").unwrap();
assert!(s.take_gm_ticket_intents().is_empty(), "not yet");
s.run("TicketStatus_OnUpdate(2)").unwrap();
assert_eq!(
s.take_gm_ticket_intents(),
vec![GmTicketIntent::Ask],
"600s elapsed — re-ask"
);
s.run("TicketStatus_OnUpdate(1)").unwrap();
assert!(
s.take_gm_ticket_intents().is_empty(),
"and the clock restarts"
);
}
/// `ToggleHelpFrame` is the micro button's whole wiring, and opening the window asks the server
/// for the queue status — without that ask the gate above would run on a stale assumption for the
/// life of the session.
#[test]
fn toggling_the_window_opens_it_and_asks_for_the_queue_status() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
s.run("ToggleHelpFrame()").unwrap();
assert!(s.eval::<bool>("return HelpFrame:IsVisible()").unwrap());
assert_eq!(
s.take_gm_ticket_intents(),
vec![GmTicketIntent::AskStatus],
"OnShow calls GetGMStatus — the gate must not run on an assumption"
);
// This is the one test that asserts the OnShow traffic itself; the others drain it away first.
assert!(
s.eval::<bool>("return HelpFrameHome:IsVisible()").unwrap(),
"and it opens on Home"
);
s.run("ToggleHelpFrame()").unwrap();
assert!(!s.eval::<bool>("return HelpFrame:IsVisible()").unwrap());
}