Files
benilla-octo/crates/benilla-app/tests/trade_frame.rs
T
Sam SungandClaude Opus 5.5 a7e6fc2301 Comments and strings: the passes' leftovers, and a region's template matched as one name (#391)
The comment and string passes' last leftovers, and one lookup made to match the reference.

**Comments that stated a wrong reference fact**, now stating the right one:
- A cast failure clears the GCD, and a failed cooldown-on-event spell also drops its parked record (`0x6e73cc`).
- `SetUnit` is `0x505d70`; `0x476cb0` is the component clone it reaches.
- Interior sound follows the character, where the listener sits, not the camera.
- Template inheritance re-runs `LoadXML` on the one object against each template (`0x76985c`), and reads `name` and `id` from the instance alone (`0x769770`).
- On the ghost-to-alive edge the reference clears its corpse cache and sends nothing (`0x5ee990`, `0x491f50`).
- The name cache has no high-bit miss. An empty-name answer evicts the record in the reference (`0x55f6f0`).
- The combat-log deferred queue (`0xc4e208`) waits only on an item name. A unit name never waits.
- A flying spline's segments are paced by curve length (`0x454320`, `0x453760`), not by chord length.
- A stop within 3 yd of its stop point keeps the arrow nocked (`0x6019e4`).
- A missile's sound id is at `CMissile+0x40`.
- A nil or unresolved `AssistUnit` token is a gap against `0xb8` ERR_GENERIC_NO_TARGET, not a deviation.

**Strings:**
- Test, log and tool messages lose leftover history wording and placeholder labels.
- The extractor's scan summaries describe the current tint and sequence-clock paths.
- The smoke script's failure text carries no private numbers.

**Line continuations:** twelve log, panic and assert strings get back the `\` continuations they had lost. Each printed a run of spaces mid-message. Fixes #106.

**One lookup:** a region's `inherits=` is looked up as one verbatim name, matched case-insensitively (`0x6ee6f0`, `_strnicmp` at `0x6ee747`), as `framexml::expand` already did for frames. The gate before it split on commas and matched case-sensitively, so a texture or font string whose `inherits=` spelled its template in another case never received the template. `a_region_takes_a_mis_cased_template` fails before this change and passes after.

**Tests:** the quest tests stop loading three FrameXML files that no quest file uses.

**In game:** nothing changes for the stock UI. Log lines read as one sentence, and an addon texture that names its template in another case now gets it.

Verified with `scripts/check.sh` on the changed crates, plus the loader and quest tests with the install present.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 08:57:47 +07:00

348 lines
12 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 `TradeFrame.xml` through the engine: pushes a synthetic two-sided offer, opens
//! the window with `TRADE_SHOW`, and asserts the Lua paints both columns, the money and the accept
//! glows that follow `TRADE_ACCEPT_UPDATE(my, his)`.
mod common;
use benilla_ui::script::{ScriptValue, TradeSideState, TradeSlotItem, TradeState, UiScript};
/// The trade window's load prefix, in `assets/ui/benilla.toc` order.
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
// `MoneyInputFrameTemplate` and the `MoneyInputFrame_*` functions the window calls.
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
"Interface\\FrameXML\\GameTooltip.xml",
// The slot updates go through `ItemButtonTemplate.lua`'s `SetItemButton*` helpers.
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")),
}
}
/// We offer Linen Cloth ×5 and 1g 23s 45c; the partner offers Silk Cloth and 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 the editable input
"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")` (`TradeFrame.lua:43`); the app points the "npc"
// unit at the partner.
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"
);
// Slotted into the left panel by its `UIPanelWindows` row (`UIParent.lua:27`); unregistered,
// `ShowUIPanel` only calls `Show()` and the window never lands on screen.
assert_eq!(
s.eval::<String>("return GetLeftFrame() and GetLeftFrame():GetName() or ''")
.unwrap(),
"TradeFrame",
"TRADE_SHOW slots the window into the left panel"
);
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 (`SetText(nil)`) and 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 `UnitName("NPC")`, set up above.
assert_eq!(
s.eval::<String>("return TradeFrameRecipientNameText:GetText()")
.unwrap(),
"Thrall"
);
// `TARGET_TRADE` collapses (`MoneyFrame.lua:69`), so 5s shows the silver coin alone.
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();
// 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![]);
assert_eq!(
s.eval::<String>("return TradePlayerItem7Name:GetText()")
.unwrap(),
// `TRADEFRAME_NOT_MODIFIED_TEXT` (`TradeFrame.lua:64`).
"|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![]);
// `TradeFrame_Update` hides all four highlights.
assert!(!s
.eval::<bool>("return TradeHighlightRecipient:IsShown()")
.unwrap());
// The partner accepts: their column and 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.
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 hides the window, and its OnHide's `CloseTrade` queues the close.
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());
// A server-driven `TRADE_CLOSED` 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());
}
/// `PLAYER_TRADE_MONEY` fills the three boxes from `GetPlayerTradeMoney()` without re-offering,
/// and a keystroke offers the copper total through `SetTradeMoney`. vmangos never echoes our own
/// gold (it sends it to the partner only, `TradeData.cpp:118`); the app sets it client-side and
/// fires the event.
#[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
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"
);
}
// The programmatic fill must not re-offer.
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 event fills 1g 23s 45c into the boxes"
);
assert_eq!(
s.take_trade_money(),
None,
"the guarded reflect does not re-offer"
);
// Lift the affordability clamp (`TradeFrame.lua:164`) for a typed offer.
s.run("GetMoney = function() return 100000000 end").unwrap();
s.run("TradePlayerInputMoneyFrameGold:SetText('2')")
.unwrap();
s.tick(0.0); // the deferred OnTextChanged drains here
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());
}
/// With an empty cursor, clicking our filled slot clears it (`ClickTradeButton`), while the
/// partner's column calls `ClickTargetTradeButton`, which queues nothing (`TradeFrame.xml:101`).
#[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())); // both slot 1s are filled
s.fire_event("TRADE_SHOW", vec![]);
let _ = s.take_trade_clear_items();
s.run("TradePlayerItem1ItemButton:Click()").unwrap();
assert_eq!(
s.take_trade_clear_items(),
vec![1],
"clicking our filled slot clears it (ClickTradeButton)"
);
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());
}