Files
benilla-octo/crates/benilla-app/tests/trade_frame.rs
T
SamandClaude Fable 5.1 eadacac998 a hundred stale claims retire, and text= was never a divergence
- a comment that justifies code by an engine difference is a claim with an expiry date, and the tree had ~96 expired ones across `assets/ui`, `ui_script/` and `benilla-ui` — a hygiene pass, no behaviour change: dangling `Benilla*` names with zero definitions (`BenillaMoney_*` at thirteen sites after 1937/1962 converged the coin cluster onto the stock `SmallMoneyFrameTemplate`; `BenillaGossipRow_Resize`, `BenillaErrorsFrame_AddMessage`, the digit feed 1285 deleted, `BenillaMerchantItem_OnClick` and three more), "the shipped `assets/ui/<X>.xml`" for thirty-odd files the directory no longer holds — it holds nine — each site now naming the chain file that actually loads, `UiPanels.xml` in the present tense after 1988 took the panel manager and the dialog engine stock, and expired self-deprecation: SimpleHTML "a widget kind we do not have" (1337 built it), "benilla has no console" (1948 bound `ConsoleExec`), `Model`'s "seven verbs not published yet" and "`ClearFog` is a guess" (all 23 are published, the carve sits 500 lines below, `UNBUILT` has been `[&str; 0]` since 2027), `frame_flag_gate`'s doc describing four groups of accepted mouse divergences over a `KNOWN` list that is empty (the empty array and its enforcement stay), five comments saying key delivery is not gated on `keyboard_enabled` where `script::keyboard`'s walk filters on exactly that field (1319), `SetRaidTarget` called a verb this house lacks beside an assert that its intent IS queued (1820), two questlog tests deferring to a screenshot for `GetPoint`/`IsVisible` the regions already answer, `benilla.toc`'s "trade still needs four engine verbs" (all four bound, 1966) and its ping `Model`s "this engine draws no pixels for" (2013 renders `MinimapPing.mdx` itself), and `rig_anim`'s `set_paused` naming a booth caller it does not have; and the code the prose was propping up goes with it: `GameMenuFrame.xml`'s six `<OnLoad>` label setters, because `text=` on a `<Button>` is a global-string lookup at the loader's attribute step (0995, built 61 seconds before the file was written) — the buttons carry `text="LOGOUT"`/`"EXIT_GAME"`/`"MACROS"`/`"RETURN_TO_GAME"`/`"MAIN_MENU"` as the reference's own do; `ScriptLogFrame`'s five authored close-button lines, now `inherits="UIPanelCloseButton"` off the chain's `UIPanelTemplates.xml` seated 757 lines above it (1846); and the editbox's 22-line "gap" doc claiming only the self-acquire `autoFocus` path is built and its `false` default unverified — path 1 is 1686's `visibility_focus`, and the default is `true`, byte-read off `flags = 1` at `0x779a29`/`0x779a2e`, so the test named `autofocus_does_not_focus_on_show…` is renamed for what it actually pins (2044)
- the `text=` divergence never existed: `resolve_text`'s doc had claimed since 0068 that falling back to the raw attribute on a GlobalStrings miss was "one deliberate divergence" from a reference that draws the empty string, and read at the byte it is the reference's own behaviour — `FrameScript_GetText 0x703bf0` does return a pre-seeded empty string (`0x882748`) when the global is not a string, but that return never reaches a label, because all three `text=` readers image-wide test it and substitute the raw attribute: `Button::LoadXML` at `0x778c07` (`0x778c31 mov eax,esi`), `CSimpleFontString::LoadXML` at `0x771006` byte-identically, and `0x7292a6`, which pushes `edi` on the empty arm; so the loader changes nothing, the two test comments that had called `PetLevelText`'s "Level level race class" placeholder and `CLOSE` our divergence now name the reference's arm (a real 1.12 client draws them too), and what IS ours is the extra warning on a key-shaped miss (0983 → 0991), a strict addition; beside it, `GetEffectiveScale`'s root factor is 1 not because "benilla has no `uiScale` CVar" — 1804 registered one, default 0.9 — but because the dial is applied at the raster seam, `768/uiScale` UI units of virtual-screen height rather than a scale on `UIParent`, so every coordinate the VM hands Lua is already in those units and the reference's `GetCursorPosition()/GetEffectiveScale()` transcriptions convert correctly with a root of 1 (2044)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:39:30 +07:00

370 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Drives the stock `Interface\FrameXML\TradeFrame.xml` through the engine (decision 0592 P1) — the
//! trade twin of `mail_frame.rs`: it loads the same file chain the app does (cut to the trade
//! window's dependency prefix), pushes a synthetic two-sided offer, opens the window with the app's
//! own `TRADE_SHOW` event, and asserts the transcribed Lua actually paints — the named regions
//! exist, both columns populate from a fed `TradeState`, the money coin trios render, and the
//! accept glow tracks the `TRADE_ACCEPT_UPDATE(my, his)` args. This is the machine gate for the XML
//! (a Lua error / missing global / wrong region name fails here); the director's eye judges only the *look*.
mod common;
use benilla_ui::script::{ScriptValue, TradeSideState, TradeSlotItem, TradeState, UiScript};
/// The trade window's load prefix — the app's own order (`ui_script/mod.rs`), members only.
/// MerchantFrame.xml rides along because TradeFrame.xml reuses its global `BenillaMoney_*` coin
/// helpers (the two gold displays), so a load error in either fails here.
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// The money entry comes off the chain since 1882 — `MoneyInputFrameTemplate` and the
// `MoneyInputFrame_*` verbs the window's OnLoad calls. Seated straight after MoneyFrame.xml,
// which is benilla.toc's own order.
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
r"Interface\FrameXML\UIParent.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml", // the dialog engine (1960)
"Interface\\FrameXML\\GameTooltip.xml",
// The stock slot updates go through ItemButtonTemplate.lua's SetItemButton* helpers (1966).
r"Interface\FrameXML\ItemButtonTemplate.xml",
"Interface\\FrameXML\\TradeFrame.xml",
];
fn load_ui(script: &UiScript) {
for file in FILES {
common::load_ui(script, file);
}
}
fn item(item_id: u32, name: &str, count: u32, quality: u32) -> TradeSlotItem {
TradeSlotItem {
item_id,
name: Some(name.to_string()),
texture: Some("Interface\\Icons\\INV_Fabric_Linen_01".to_string()),
count,
quality: Some(quality),
enchantment: None,
link: Some(format!("|cffffffff|Hitem:{item_id}:0:0:0|h[{name}]|h|r")),
}
}
/// A two-sided offer: we offer Linen Cloth ×5 + 1g23s45c; the partner offers Silk Cloth + 5s.
fn state() -> TradeState {
let mut player = TradeSideState {
gold: 12_345,
..Default::default()
};
player.slots[0] = Some(item(2589, "Linen Cloth", 5, 1));
let mut target = TradeSideState {
gold: 500,
..Default::default()
};
target.slots[0] = Some(item(4306, "Silk Cloth", 1, 2));
TradeState {
player,
target,
partner_name: Some("Thrall".into()),
}
}
#[test]
fn trade_frame_loads_and_key_regions_exist() {
let _data = benilla_formats::wow_data_or_skip!();
let s = UiScript::new().unwrap();
load_ui(&s);
for name in [
"TradeFrame",
"TradePlayerItem1",
"TradePlayerItem1ItemButton",
"TradePlayerItem7", // the enchant slot
"TradeRecipientItem1",
"TradeRecipientItem7",
"TradeFrameTradeButton",
"TradeFrameCancelButton",
"TradePlayerInputMoneyFrameGold", // our gold is now the editable input (P2)
"TradeRecipientMoneyFrameCopperButton",
"TradeHighlightPlayer",
"TradeHighlightRecipientEnchant",
] {
assert!(
s.eval::<bool>(&format!("return getglobal('{name}') ~= nil"))
.unwrap(),
"region {name} should exist"
);
}
// The window is hidden until TRADE_SHOW.
assert!(!s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
}
#[test]
fn trade_show_opens_and_both_columns_populate() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
// The stock header reads `UnitName("NPC")` — the partner rides the shared "npc" booth the
// app's session feed points at them (1966).
s.set_unit(
"npc",
Some(benilla_ui::script::UnitState {
exists: true,
name: Some("Thrall".into()),
..Default::default()
}),
);
s.fire_event("TRADE_SHOW", vec![]);
assert!(
s.eval::<bool>("return TradeFrame:IsShown()").unwrap(),
"the window opens on TRADE_SHOW"
);
// ...and it is SLOTTED into the left panel, not merely Show()n unpositioned. Without the
// UIPanelWindows["TradeFrame"] row, ShowUIPanel takes its unregistered branch — a bare
// frame:Show() with no SetLeftFrame placement — which still flips IsShown() but leaves the
// window off its panel slot, so it never lands on screen (the live "OPEN_WINDOW arrived on both
// clients, yet no window" bug). This is the assertion that catches that missing registration.
assert_eq!(
s.eval::<String>("return GetLeftFrame() and GetLeftFrame():GetName() or ''")
.unwrap(),
"TradeFrame",
"TRADE_SHOW slots the window into the left panel"
);
// Our slot 1 and the partner's slot 1 painted their names + icons from the fed state.
assert_eq!(
s.eval::<String>("return TradePlayerItem1Name:GetText()")
.unwrap(),
"Linen Cloth"
);
assert_eq!(
s.eval::<String>("return TradeRecipientItem1Name:GetText()")
.unwrap(),
"Silk Cloth"
);
assert!(
s.eval::<bool>("return TradePlayerItem1ItemButtonIconTexture:IsShown()")
.unwrap(),
"a filled slot shows its icon"
);
// An empty slot clears its name (the stock's `SetText(nil)`) + hides its icon.
assert!(s
.eval::<Option<String>>("return TradePlayerItem2Name:GetText()")
.unwrap()
.unwrap_or_default()
.is_empty());
assert!(!s
.eval::<bool>("return TradePlayerItem2ItemButtonIconTexture:IsShown()")
.unwrap());
// The partner's name paints from GetTradePartnerName().
assert_eq!(
s.eval::<String>("return TradeFrameRecipientNameText:GetText()")
.unwrap(),
"Thrall"
);
// The partner's read-only money rendered exactly one coin for 5s: TARGET_TRADE is a COLLAPSING
// type (MoneyTypeInfo.collapse), so gold and copper hide and silver alone stands — the
// reference's own rule, and the one this fixture was written to show. Our own gold is the
// editable input, exercised in `player_money_input_reflects_then_offers`.
assert!(s
.eval::<bool>(
"return TradeRecipientMoneyFrameSilverButton:IsShown() \
and not TradeRecipientMoneyFrameGoldButton:IsShown() \
and not TradeRecipientMoneyFrameCopperButton:IsShown()"
)
.unwrap());
assert!(s.take_errors().is_empty(), "clean repaint");
}
#[test]
fn enchant_slot_shows_the_not_traded_note() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
let mut st = state();
// Park an item in our enchant slot (index 6 = slot 7).
st.player.slots[6] = Some(item(6217, "Copper Rod", 1, 1));
s.set_trade(Some(st));
s.fire_event("TRADE_SHOW", vec![]);
// Slot 7 with an item but no enchant shows the coloured "will not be traded" note.
assert_eq!(
s.eval::<String>("return TradePlayerItem7Name:GetText()")
.unwrap(),
// The stock wording, TRADEFRAME_NOT_MODIFIED_TEXT (ours had read "Will Not Be Traded").
"|cffffffffItem not yet modified|r"
);
assert!(s.take_errors().is_empty());
}
#[test]
fn accept_update_drives_the_column_glows() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
s.fire_event("TRADE_SHOW", vec![]);
// Highlights start hidden (TradeFrame_Update hides all four).
assert!(!s
.eval::<bool>("return TradeHighlightRecipient:IsShown()")
.unwrap());
// The partner accepts: their column + enchant glow show, ours stay hidden.
s.fire_event(
"TRADE_ACCEPT_UPDATE",
vec![ScriptValue::Int(0), ScriptValue::Int(1)],
);
assert!(s
.eval::<bool>("return TradeHighlightRecipient:IsShown()")
.unwrap());
assert!(s
.eval::<bool>("return TradeHighlightRecipientEnchant:IsShown()")
.unwrap());
assert!(!s
.eval::<bool>("return TradeHighlightPlayer:IsShown()")
.unwrap());
// We accept: our glow shows and the Trade button disables (the reference's own-accept lock).
s.fire_event(
"TRADE_ACCEPT_UPDATE",
vec![ScriptValue::Int(1), ScriptValue::Int(0)],
);
assert!(s
.eval::<bool>("return TradeHighlightPlayer:IsShown()")
.unwrap());
assert!(!s
.eval::<bool>("return TradeFrameTradeButton:IsEnabled() ~= 0")
.unwrap());
assert!(s.take_errors().is_empty());
}
#[test]
fn closing_the_window_queues_the_cancel() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
s.fire_event("TRADE_SHOW", vec![]);
let _ = s.take_trade_close();
// The X button hides the window → OnHide → CloseTrade queues the local close/cancel verb.
s.run("TradeFrameCloseButton:Click()").unwrap();
assert!(!s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
assert!(s.take_trade_close(), "closing queued the CloseTrade intent");
assert!(s.take_errors().is_empty());
// TRADE_CLOSED from the server-driven path also hides it.
s.fire_event("TRADE_SHOW", vec![]);
assert!(s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
s.fire_event("TRADE_CLOSED", vec![]);
assert!(!s.eval::<bool>("return TradeFrame:IsShown()").unwrap());
}
#[test]
fn trade_button_click_queues_accept() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state()));
s.fire_event("TRADE_SHOW", vec![]);
let _ = s.take_trade_accept();
s.run("TradeFrameTradeButton:Click()").unwrap();
assert!(s.take_trade_accept(), "the Trade button queues AcceptTrade");
assert!(s.take_errors().is_empty());
}
/// The editable player money input (decision 0592 P2): the server's PLAYER_TRADE_MONEY echo reflects
/// the accepted gold into the three boxes without re-offering (the diff-guarded SetCopper), and a
/// keystroke offers the running copper total through SetTradeMoney → the app's SET_TRADE_GOLD.
#[test]
fn player_money_input_reflects_then_offers() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_money(1_000_000); // SetTradeMoney is purse-gated (1965)
load_ui(&s);
s.set_trade(Some(state())); // state().player.gold == 12345 (1g 23s 45c)
s.fire_event("TRADE_SHOW", vec![]);
// The three numeric boxes exist and start empty.
for box_ in ["Gold", "Silver", "Copper"] {
assert!(
s.eval::<bool>(&format!(
"return getglobal('TradePlayerInputMoneyFrame{box_}') ~= nil"
))
.unwrap(),
"money box {box_} exists"
);
}
// Reflection: PLAYER_TRADE_MONEY fills the boxes from GetPlayerTradeMoney(), guarded so the
// programmatic fill does NOT re-offer (no spurious SET_TRADE_GOLD).
let _ = s.take_trade_money();
s.fire_event("PLAYER_TRADE_MONEY", vec![]);
assert_eq!(
s.eval::<(String, String, String)>(
"return TradePlayerInputMoneyFrameGold:GetText(), \
TradePlayerInputMoneyFrameSilver:GetText(), \
TradePlayerInputMoneyFrameCopper:GetText()"
)
.unwrap(),
("1".into(), "23".into(), "45".into()),
"the echo reflects 1g 23s 45c into the boxes"
);
assert_eq!(
s.take_trade_money(),
None,
"the guarded reflect does not re-offer"
);
// A genuine keystroke offers the running total (bypass the affordability clamp — a bare harness
// has no purse).
s.run("GetMoney = function() return 100000000 end").unwrap();
s.run("TradePlayerInputMoneyFrameGold:SetText('2')")
.unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here (decision 1831)
assert_eq!(
s.take_trade_money(),
Some(2 * 10000 + 23 * 100 + 45),
"typing offers the copper total via SetTradeMoney"
);
assert!(s.take_errors().is_empty());
}
/// The trade slot buttons route through the shared drop handler (decision 0592 P2): clicking OUR
/// filled slot clears it (ClickTradeButton → CLEAR_TRADE_ITEM), while the partner's column is inert
/// (ClickTargetTradeButton). Exercises the real button OnClick → BenillaTradeSlot_OnClick →
/// player/recipient name-dispatch, without needing a cursor payload (the empty-cursor clear path).
#[test]
fn slot_click_routes_player_to_clear_and_recipient_to_inert() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_trade(Some(state())); // player slot 1 + recipient slot 1 are both filled
s.fire_event("TRADE_SHOW", vec![]);
let _ = s.take_trade_clear_items();
// Our filled slot 1 clicked with an empty cursor → a clear routed through ClickTradeButton.
s.run("TradePlayerItem1ItemButton:Click()").unwrap();
assert_eq!(
s.take_trade_clear_items(),
vec![1],
"clicking our filled slot clears it (ClickTradeButton)"
);
// The partner's filled slot is read-only — the click hits ClickTargetTradeButton, which queues
// nothing on either channel.
s.run("TradeRecipientItem1ItemButton:Click()").unwrap();
assert!(
s.take_trade_clear_items().is_empty() && s.take_trade_set_items().is_empty(),
"the partner slot is inert (ClickTargetTradeButton)"
);
assert!(s.take_errors().is_empty());
}