- the target-of-target frame and its option: 1.12's frame with its five display modes, fed one hop off the selected unit's own target field through the same read `/assist` runs, in four feeds (snapshot and transitions, the debuff buttons, the portrait booth, TargetUnit's click arm); ships off, as 1.12 does, on its own pair of saved globals - talents can be unlearned again: the trainer's `MSG_TALENT_WIPE_CONFIRM` had no handler since the wipe flow was deferred, so the gossip line closed the menu and did nothing — the question is an NPC session (walking away retracts it), the popup shows the cost on the StaticPopup money variant, Accept answers it behind the client's own purse check - Sprint's sparkles hang in the air behind you along the trail instead of collapsing into your torso: ride-vs-trail is the emitter's own file flag 0x10, not the host class — the finding wow-re returned eighteen days ago and nobody folded; the particle dump names the two flag bits it had been mis-reporting - `/sleep` takes both movement masks (the strictest posture), correcting the day's sit gate which let you lie down on a lake bottom while `/sit` was refused; a Lua-driven selection (TargetUnit on an attackable token) starts the swing exactly as a click does, through the one select path Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
725 lines
27 KiB
Rust
725 lines
27 KiB
Rust
//! Drives the REAL `assets/ui/MailFrame.xml` through the engine (decision 0544 P1/P2) — the mail
|
||
//! twin of `tradeskill_frame.rs`: it loads the same file chain the app does (cut to the mail
|
||
//! window's dependency prefix), pushes a synthetic inbox, opens the window with the app's own
|
||
//! `MAIL_SHOW`/`MAIL_INBOX_UPDATE` events, and asserts the transcribed Lua actually paints — the
|
||
//! named regions exist, the rows populate from a fed `MailState`, the paging math is right, and the
|
||
//! unread/read row state tracks the wire `wasRead` flag.
|
||
|
||
use benilla_ui::script::{MailInboxRow, MailInvoice, MailState, UiScript};
|
||
|
||
const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
|
||
|
||
/// The mail window's load prefix — the app's own order (`ui_script/mod.rs`), members only.
|
||
/// MerchantFrame.xml rides along because MailFrame.xml reuses its global `BenillaMoney_*` coin
|
||
/// helpers (postage display), so a load error in either fails here.
|
||
const FILES: [&str; 6] = [
|
||
"Fonts.xml",
|
||
"MoneyFrame.xml",
|
||
"UiPanels.xml",
|
||
"GameTooltip.xml",
|
||
"MerchantFrame.xml",
|
||
"MailFrame.xml",
|
||
];
|
||
|
||
fn load_ui(script: &UiScript) {
|
||
let dir = std::path::Path::new(UI_DIR);
|
||
let provider = |req: &str| -> Option<Vec<u8>> {
|
||
let norm = req.replace('\\', "/");
|
||
let base = norm.rsplit('/').next().unwrap_or(&norm);
|
||
std::fs::read(dir.join(&norm))
|
||
.or_else(|_| std::fs::read(dir.join(base)))
|
||
.ok()
|
||
};
|
||
for file in FILES {
|
||
let text = std::fs::read_to_string(dir.join(file)).unwrap_or_else(|e| {
|
||
panic!("reading {file}: {e}");
|
||
});
|
||
let doc = benilla_ui::framexml::parse(&text).unwrap_or_else(|e| {
|
||
panic!("parsing {file}: {e}");
|
||
});
|
||
let report = benilla_ui::loader::load(script, &doc, &provider);
|
||
assert!(
|
||
report.errors.is_empty(),
|
||
"{file} loaded with errors: {:#?}",
|
||
report.errors
|
||
);
|
||
}
|
||
}
|
||
|
||
/// One inbox row with the fields the window paints.
|
||
fn row(sender: &str, subject: &str, was_read: bool, item_id: u32, cod: u32) -> MailInboxRow {
|
||
MailInboxRow {
|
||
package_icon: (item_id != 0).then(|| "Interface\\Icons\\INV_Misc_Bag_08".to_string()),
|
||
stationery_icon: Some("Interface\\Icons\\INV_Misc_Note_01".to_string()),
|
||
sender: Some(sender.to_string()),
|
||
subject: subject.to_string(),
|
||
money: 0,
|
||
cod,
|
||
days_left: 29.0,
|
||
item_count: if item_id != 0 { 2 } else { 0 },
|
||
was_read,
|
||
was_returned: false,
|
||
text_created: false,
|
||
can_reply: true,
|
||
is_gm: false,
|
||
body: Some("body".into()),
|
||
stationery_texture: "STATIONERYTEST".into(),
|
||
is_invoice: false,
|
||
invoice: None,
|
||
has_body: true,
|
||
item_id,
|
||
item_name: (item_id != 0).then(|| "Linen Cloth".to_string()),
|
||
item_texture: (item_id != 0).then(|| "Interface\\Icons\\INV_Fabric_Linen_01".to_string()),
|
||
item_quality: (item_id != 0).then_some(1),
|
||
can_delete: false,
|
||
item_random_property_id: 0,
|
||
}
|
||
}
|
||
|
||
/// A one-page inbox (2 mails).
|
||
fn small_inbox() -> MailState {
|
||
MailState {
|
||
inbox: vec![
|
||
row("Thrall", "Warchief's orders", false, 2589, 0),
|
||
row("Jaina", "A letter", true, 0, 0),
|
||
],
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn mail_frame_loads_and_key_regions_exist() {
|
||
let s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
// The window, both tabs' bodies, a row, the open-letter toplevel, and the send-tab widgets.
|
||
for name in [
|
||
"MailFrame",
|
||
"InboxFrame",
|
||
"MailItem1",
|
||
"MailItem1Button",
|
||
"MailItem7",
|
||
"SendMailFrame",
|
||
"SendMailNameEditBox",
|
||
"SendMailMailButton",
|
||
"OpenMailFrame",
|
||
"OpenMailReplyButton",
|
||
] {
|
||
assert!(
|
||
s.eval::<bool>(&format!("return getglobal('{name}') ~= nil"))
|
||
.unwrap(),
|
||
"region {name} should exist"
|
||
);
|
||
}
|
||
// The window is hidden until MAIL_SHOW.
|
||
assert!(!s.eval::<bool>("return MailFrame:IsShown()").unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn mail_show_opens_and_inbox_populates() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(small_inbox()));
|
||
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
assert!(
|
||
s.eval::<bool>("return MailFrame:IsShown()").unwrap(),
|
||
"the window opens on MAIL_SHOW"
|
||
);
|
||
// MAIL_SHOW's CheckInbox() queued the inbox-refresh intent.
|
||
assert!(s.take_mail_check_inbox(), "MAIL_SHOW fires CheckInbox");
|
||
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
assert_eq!(s.eval::<i64>("return GetInboxNumItems()").unwrap(), 2);
|
||
// Row 1 painted its sender + subject from the fed state.
|
||
assert_eq!(
|
||
s.eval::<String>("return MailItem1Sender:GetText()")
|
||
.unwrap(),
|
||
"Thrall"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return MailItem1Subject:GetText()")
|
||
.unwrap(),
|
||
"Warchief's orders"
|
||
);
|
||
// The row's clickable child button is shown for a populated row, hidden past the 2 mails (the
|
||
// reference row FRAME stays shown — only $parentButton toggles; ref InboxFrame_Update l.120/180).
|
||
assert!(s.eval::<bool>("return MailItem1Button:IsShown()").unwrap());
|
||
assert!(!s.eval::<bool>("return MailItem3Button:IsShown()").unwrap());
|
||
|
||
// No script errors escaped the event-driven repaints.
|
||
assert!(s.take_errors().is_empty(), "clean repaint");
|
||
}
|
||
|
||
#[test]
|
||
fn paging_math_enables_next_only_when_overflowing() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
// 9 mails → 2 pages of 7.
|
||
let mut inbox = Vec::new();
|
||
for i in 0..9 {
|
||
inbox.push(row(&format!("S{i}"), &format!("subj{i}"), false, 0, 0));
|
||
}
|
||
s.set_mail(Some(MailState { inbox }));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
|
||
// Page 1: prev disabled, next enabled.
|
||
assert!(!s
|
||
.eval::<bool>("return InboxPrevPageButton:IsEnabled()")
|
||
.unwrap());
|
||
assert!(s
|
||
.eval::<bool>("return InboxNextPageButton:IsEnabled()")
|
||
.unwrap());
|
||
// Turn the page: prev enabled, next disabled (only 2 mails on page 2).
|
||
s.run("InboxNextPage()").unwrap();
|
||
assert!(s
|
||
.eval::<bool>("return InboxPrevPageButton:IsEnabled()")
|
||
.unwrap());
|
||
assert!(!s
|
||
.eval::<bool>("return InboxNextPageButton:IsEnabled()")
|
||
.unwrap());
|
||
}
|
||
|
||
/// The unread/read row *coloring* (SetTextColor / SetVertexColor) has no engine getter to assert
|
||
/// against, so the harness verifies the other observable per-row state the same Update paints: the
|
||
/// COD tag shows on a COD mail and hides on a plain one, and the plain row's money field is nil.
|
||
#[test]
|
||
fn cod_tag_shows_on_a_cod_mail() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(MailState {
|
||
inbox: vec![
|
||
row("Auctioneer", "COD parcel", false, 2589, 5000), // COD 50s
|
||
row("Jaina", "A letter", true, 0, 0), // no COD
|
||
],
|
||
}));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
|
||
assert!(
|
||
s.eval::<bool>("return MailItem1ButtonCOD:IsShown()")
|
||
.unwrap(),
|
||
"the COD mail shows its coin tag"
|
||
);
|
||
assert!(
|
||
!s.eval::<bool>("return MailItem2ButtonCOD:IsShown()")
|
||
.unwrap(),
|
||
"the plain mail hides the COD tag"
|
||
);
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn opening_a_letter_shows_the_open_frame_and_queues_the_body() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(small_inbox()));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
let _ = s.take_mail_opens();
|
||
|
||
// Click row 1: the check button toggles on → open the letter.
|
||
// A programmatic click toggles the check button on, then fires OnClick (this = the button).
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
assert!(
|
||
s.eval::<bool>("return OpenMailFrame:IsShown()").unwrap(),
|
||
"the open-letter frame shows"
|
||
);
|
||
// The sender/subject/body painted; GetInboxText queued the open (mark-read + body ask).
|
||
assert_eq!(
|
||
s.eval::<String>("return OpenMailSender:GetText()").unwrap(),
|
||
"Thrall"
|
||
);
|
||
assert!(s.take_mail_opens().contains(&1), "opening queued the row");
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
/// **The letter and the centre seat exclude each other, in BOTH arrival orders** (decision 1520,
|
||
/// director-reported and ref-checked): with the mailbox at the left slot and the character sheet
|
||
/// pushed to centre beside it (its pushable=2 row), clicking a mail item must EVICT the sheet —
|
||
/// the letter's OnShow is the ref's own (`if GetCenterFrame() then HideUIPanel(...)`, ref
|
||
/// MailFrame.xml l.1903-1907) — not open underneath it, which is what a bare Show did. The
|
||
/// reverse order is 1507's child-window loop: a frame arriving at centre puts the letter away.
|
||
/// Whichever comes second wins the space; they can never stack. A bare stand-in carries the
|
||
/// CharacterFrame row (the real file isn't in this harness's chain).
|
||
#[test]
|
||
fn a_letter_and_the_centre_occupant_evict_each_other() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(small_inbox()));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
let _ = s.take_mail_opens();
|
||
|
||
// The director's setup: mailbox left, character sheet pushed to centre beside it.
|
||
s.run(
|
||
r#"local c = CreateFrame("Frame", "CharacterFrame") c:SetSize(50, 50) c:Hide()
|
||
ShowUIPanel(CharacterFrame)"#,
|
||
)
|
||
.unwrap();
|
||
assert!(
|
||
s.eval::<bool>(
|
||
"return GetLeftFrame():GetName() == 'MailFrame' \
|
||
and GetCenterFrame():GetName() == 'CharacterFrame'"
|
||
)
|
||
.unwrap(),
|
||
"mail holds left, the sheet was pushed to centre"
|
||
);
|
||
|
||
// Click a mail item: the letter opens AND the sheet is evicted — one thing beside the mailbox.
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
assert!(s.take_errors().is_empty());
|
||
assert!(
|
||
s.eval::<bool>("return OpenMailFrame:IsShown()").unwrap(),
|
||
"the letter opened"
|
||
);
|
||
assert!(
|
||
!s.eval::<bool>("return CharacterFrame:IsShown()").unwrap(),
|
||
"the centre occupant was evicted, not covered (the reported bug)"
|
||
);
|
||
assert!(
|
||
s.eval::<bool>("return GetCenterFrame() == nil").unwrap(),
|
||
"the eviction is a plain vacate — nothing slides"
|
||
);
|
||
assert!(
|
||
s.eval::<bool>("return MailFrame:IsShown() and GetLeftFrame():GetName() == 'MailFrame'")
|
||
.unwrap(),
|
||
"the mailbox itself is untouched"
|
||
);
|
||
|
||
// The reverse arrival: re-opening the sheet over the open letter puts the LETTER away
|
||
// (1507's child-window loop) — the same exclusion, other direction.
|
||
s.run("ShowUIPanel(CharacterFrame)").unwrap();
|
||
assert!(s.take_errors().is_empty());
|
||
assert!(
|
||
s.eval::<bool>("return CharacterFrame:IsShown()").unwrap()
|
||
&& !s.eval::<bool>("return OpenMailFrame:IsShown()").unwrap(),
|
||
"a frame arriving at centre hides the letter"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn reply_switches_to_send_tab_prefilled() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(small_inbox()));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
// A programmatic click toggles the check button on, then fires OnClick (this = the button).
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
|
||
s.run("OpenMail_Reply()").unwrap();
|
||
// The send tab is now shown, the recipient prefilled, the subject "RE: "-prefixed.
|
||
assert!(s.eval::<bool>("return SendMailFrame:IsShown()").unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return SendMailNameEditBox:GetText()")
|
||
.unwrap(),
|
||
"Thrall"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return SendMailSubjectEditBox:GetText()")
|
||
.unwrap(),
|
||
"RE: Warchief's orders"
|
||
);
|
||
}
|
||
|
||
/// Closing an ordinary letter (no money, no item, textCreated=false) must NOT delete it — the
|
||
/// reference OnHide rule (MailFrame.lua l.256-272) only purges a fully-taken husk.
|
||
#[test]
|
||
fn closing_a_plain_letter_does_not_delete_it() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(MailState {
|
||
inbox: vec![row("One", "test", false, 0, 0)],
|
||
}));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
let _ = s.take_mail_opens();
|
||
let _ = s.take_mail_deletes();
|
||
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
assert!(s.eval::<bool>("return OpenMailFrame:IsShown()").unwrap());
|
||
s.run("OpenMailCancelButton:Click()").unwrap();
|
||
assert!(s.take_errors().is_empty(), "no Lua errors on close");
|
||
let deletes = s.take_mail_deletes();
|
||
assert!(
|
||
deletes.is_empty(),
|
||
"closing a plain read letter must NOT delete it, got deletes: {deletes:?}"
|
||
);
|
||
}
|
||
|
||
/// Closing a fully-taken husk (no money, no item, textCreated=TRUE) deletes it — the reference
|
||
/// OnHide purge. This is also the live "mail vanished on close" moment against vmangos: the server
|
||
/// stamps an EMPTY-BODY player mail MAIL_CHECK_MASK_COPIED (`MailHandler.cpp` l.421,
|
||
/// `req->body.empty() ? MAIL_CHECK_MASK_COPIED : MAIL_CHECK_MASK_HAS_BODY`), which IS the wire's
|
||
/// textCreated bit — so a subject-only letter with nothing attached auto-purges when closed, in
|
||
/// the real 1.12 client exactly as here.
|
||
#[test]
|
||
fn closing_a_taken_husk_deletes_it() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
let mut husk = row("One", "test", false, 0, 0);
|
||
husk.text_created = true;
|
||
s.set_mail(Some(MailState { inbox: vec![husk] }));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
let _ = s.take_mail_opens();
|
||
let _ = s.take_mail_deletes();
|
||
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
s.run("OpenMailCancelButton:Click()").unwrap();
|
||
assert!(s.take_errors().is_empty(), "no Lua errors on close");
|
||
assert_eq!(
|
||
s.take_mail_deletes(),
|
||
vec![1],
|
||
"the husk purges on close (reference OnHide, MailFrame.lua l.256-272)"
|
||
);
|
||
}
|
||
|
||
/// The expiry text pluralizes like the reference (GetText("DAYS_ABBR"): "Day"/"Days").
|
||
#[test]
|
||
fn expiry_text_pluralizes_days() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
let mut one_day = row("Two", "b", false, 0, 0);
|
||
one_day.days_left = 1.7;
|
||
s.set_mail(Some(MailState {
|
||
inbox: vec![row("One", "a", false, 0, 0), one_day],
|
||
}));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
assert_eq!(
|
||
s.eval::<String>("return MailItem1ExpireTime:GetText()")
|
||
.unwrap(),
|
||
"|cff20ff2029 Days|r"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return MailItem2ExpireTime:GetText()")
|
||
.unwrap(),
|
||
"|cff20ff201 Day|r"
|
||
);
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
/// The letter button — "make a permanent copy" (ref OpenMail_Update l.364-376): a mail whose body
|
||
/// is takeable (`item_text_id != 0`) and not yet copied shows it; clicking queues the
|
||
/// `TakeInboxTextItem` intent (→ `CMSG_MAIL_CREATE_TEXT_ITEM`). With money enclosed too, both
|
||
/// buttons show and the caption reads "Take Attachments:".
|
||
#[test]
|
||
fn letter_button_shows_for_a_body_letter_and_click_queues_the_copy() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
let mut mail = row("One", "asd", false, 0, 0);
|
||
mail.money = 10000;
|
||
s.set_mail(Some(MailState { inbox: vec![mail] }));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
assert!(
|
||
s.eval::<bool>("return OpenMailLetterButton:IsShown()")
|
||
.unwrap(),
|
||
"a takeable, not-yet-copied body shows the letter button"
|
||
);
|
||
assert!(s
|
||
.eval::<bool>("return OpenMailMoneyButton:IsShown()")
|
||
.unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return OpenMailAttachmentText:GetText()")
|
||
.unwrap(),
|
||
"Take Attachments:"
|
||
);
|
||
s.run("OpenMailLetterButton:Click()").unwrap();
|
||
assert_eq!(
|
||
s.take_mail_take_texts(),
|
||
vec![1],
|
||
"the click queues the permanent-copy intent"
|
||
);
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
/// Once the body is copied (`textCreated`, the wire COPIED bit) the letter button hides — the same
|
||
/// flag whose OnHide purge then deletes the husk on close.
|
||
#[test]
|
||
fn letter_button_hides_once_copied() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
let mut mail = row("One", "asd", false, 0, 0);
|
||
mail.money = 10000;
|
||
mail.text_created = true;
|
||
s.set_mail(Some(MailState { inbox: vec![mail] }));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
assert!(!s
|
||
.eval::<bool>("return OpenMailLetterButton:IsShown()")
|
||
.unwrap());
|
||
assert!(s
|
||
.eval::<bool>("return OpenMailMoneyButton:IsShown()")
|
||
.unwrap());
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
/// Hovering the coins shows the plain money tooltip (ref OpenMailMoneyButton OnEnter l.1823-1829).
|
||
#[test]
|
||
fn money_button_hover_shows_the_amount_tooltip() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
let mut mail = row("One", "asd", false, 0, 0);
|
||
mail.money = 10000;
|
||
s.set_mail(Some(MailState { inbox: vec![mail] }));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
|
||
s.run("BenillaOpenMailMoneyButton_OnEnter(OpenMailMoneyButton)")
|
||
.unwrap();
|
||
assert!(
|
||
s.eval::<bool>("return GameTooltip:IsShown()").unwrap(),
|
||
"the money tooltip shows on hover"
|
||
);
|
||
assert!(
|
||
s.eval::<bool>("return GameTooltipMoneyCoin1:IsShown()")
|
||
.unwrap(),
|
||
"the coin row rendered (SetTooltipMoney path)"
|
||
);
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
/// The inbox page label stays EMPTY: the ref XML declares InboxCurrentPage (l.329) but no
|
||
/// reference Lua ever writes it — the real 1.12 window shows nothing there ("Page 1" was our
|
||
/// invention, now removed).
|
||
#[test]
|
||
fn inbox_page_label_stays_empty_like_the_reference() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(small_inbox()));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
let text = s
|
||
.eval::<Option<String>>("return InboxCurrentPage:GetText()")
|
||
.unwrap();
|
||
assert!(
|
||
text.as_deref().unwrap_or("").is_empty(),
|
||
"InboxCurrentPage must stay unwritten (got {text:?})"
|
||
);
|
||
}
|
||
|
||
/// A runtime-shown child `<Frame>` renders its own `<Layers>` FontStrings (decision 1517).
|
||
///
|
||
/// This pins a fact three window files spent months asserting the opposite of. `SendMailFrame`
|
||
/// ships `hidden="true"` and is shown by the tab click; its title lives in its OWN Layers, not on
|
||
/// the window root. The "flat layout" those windows adopted was a workaround for a constraint that
|
||
/// never existed — and it produced no symptom precisely because it was always obeyed, which is why
|
||
/// the claim needed a test rather than a header comment.
|
||
#[test]
|
||
fn a_runtime_shown_pane_renders_its_own_layers() {
|
||
let s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.eval::<()>("MailFrame:Show() SendMailFrame:Show()")
|
||
.unwrap();
|
||
assert!(
|
||
s.eval::<bool>("return SendMailFrame:IsVisible()").unwrap(),
|
||
"the pane itself"
|
||
);
|
||
assert!(
|
||
s.eval::<bool>("return SendMailTitleText:IsVisible()")
|
||
.unwrap(),
|
||
"a FontString declared inside a runtime-shown child frame's own <Layers>"
|
||
);
|
||
}
|
||
|
||
/// The auction house's mail is a RECEIPT, not a letter (decision 1522). Before this the window
|
||
/// showed exactly what the server wrote — `From: Unknown / Subject: 5529:0:2` over a body of
|
||
/// `6C:10000:10000:25:500` — because nothing parsed it. The subject rewrite is the engine's
|
||
/// (`ui_mail::invoice`); this is the pane, and it comes in two shapes off one set of seven values.
|
||
///
|
||
/// (`From: Unknown` is NOT part of the bug and is not fixed here: an auction mail carries no
|
||
/// player sender guid, so the reference's own `if ( not sender ) then sender = UNKNOWN` is what
|
||
/// puts that word there — MailFrame.lua l.286-288.)
|
||
///
|
||
/// The body text assertion is the 1527 fold-back: `GetInboxText` returns **nil** for an invoice by
|
||
/// an explicit carve-out in the reference, so `SetText(nil)` leaves the page genuinely empty.
|
||
#[test]
|
||
fn an_auction_invoice_renders_as_a_receipt() {
|
||
/// The pane reads the player's own GlobalStrings, which a bare-XML harness has none of. Stand
|
||
/// in synthetic ones: what is under test is that each reaches the right region, never their text.
|
||
fn strings(s: &UiScript) {
|
||
s.run(concat!(
|
||
"ITEM_SOLD_COLON = 'SOLD:' PURCHASED_BY_COLON = 'BY:' AMOUNT_RECEIVED_COLON = 'GOT:' ",
|
||
"ITEM_PURCHASED_COLON = 'BOUGHT:' SOLD_BY_COLON = 'FROM:' AMOUNT_PAID_COLON = 'PAID:' ",
|
||
"BUYOUT = 'Buyout' HIGH_BIDDER = 'High Bidder'",
|
||
))
|
||
.unwrap();
|
||
}
|
||
fn open_with(invoice: MailInvoice) -> UiScript {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
strings(&s);
|
||
let mut inbox = small_inbox();
|
||
inbox.inbox[0].is_invoice = true;
|
||
inbox.inbox[0].invoice = Some(invoice);
|
||
s.set_mail(Some(inbox));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
s
|
||
}
|
||
let text = |s: &UiScript, region: &str| {
|
||
s.eval::<String>(&format!("return tostring({region}:GetText())"))
|
||
.unwrap()
|
||
};
|
||
let money = |s: &UiScript, frame: &str| {
|
||
s.eval::<String>(&format!("return tostring({frame}.staticMoney)"))
|
||
.unwrap()
|
||
};
|
||
|
||
// ── The seller's: the full sum. 1g sale + 25c deposit back − 5c the house takes. ──────────
|
||
let mut s = open_with(MailInvoice {
|
||
seller: true,
|
||
item_name: "Linen Cloth".into(),
|
||
player_name: "Twowarrior".into(),
|
||
bid: 10_000,
|
||
buyout: 10_000,
|
||
deposit: 25,
|
||
consignment: 500,
|
||
});
|
||
assert!(
|
||
s.eval::<bool>("return OpenMailInvoiceFrame:IsShown()")
|
||
.unwrap(),
|
||
"a sold-auction mail shows the receipt"
|
||
);
|
||
assert_eq!(text(&s, "OpenMailInvoiceItemLabel"), "SOLD: Linen Cloth");
|
||
assert_eq!(text(&s, "OpenMailInvoicePurchaser"), "BY: Twowarrior");
|
||
// bid == buyout, so it was bought outright rather than won on a bid.
|
||
assert_eq!(text(&s, "OpenMailInvoiceBuyMode"), "(Buyout)");
|
||
assert_eq!(money(&s, "OpenMailSalePriceMoneyFrame"), "10000");
|
||
assert_eq!(money(&s, "OpenMailDepositMoneyFrame"), "25");
|
||
assert_eq!(money(&s, "OpenMailHouseCutMoneyFrame"), "500");
|
||
assert_eq!(
|
||
money(&s, "OpenMailTransactionAmountMoneyFrame"),
|
||
"9525",
|
||
"sale + deposit - cut, which is the whole point of the four lines"
|
||
);
|
||
assert!(s
|
||
.eval::<bool>("return OpenMailInvoiceHouseCut:IsShown()")
|
||
.unwrap());
|
||
assert_eq!(
|
||
text(&s, "OpenMailBodyText"),
|
||
"nil",
|
||
"an invoice has no letter body at all — `GetInboxText` nils it (1527), so the receipt has \
|
||
nothing to sit on top of"
|
||
);
|
||
assert!(s.take_errors().is_empty());
|
||
|
||
// ── The buyer's: one line, and the seller-only rows gone. Won on a bid, not bought out. ───
|
||
let mut s = open_with(MailInvoice {
|
||
seller: false,
|
||
item_name: "Small Blue Pouch".into(),
|
||
player_name: "Onewarrior".into(),
|
||
bid: 9_000,
|
||
buyout: 10_000,
|
||
deposit: 0,
|
||
consignment: 0,
|
||
});
|
||
assert!(s
|
||
.eval::<bool>("return OpenMailInvoiceFrame:IsShown()")
|
||
.unwrap());
|
||
assert_eq!(
|
||
text(&s, "OpenMailInvoiceItemLabel"),
|
||
"BOUGHT: Small Blue Pouch (High Bidder)",
|
||
"the buy mode rides the item line here, not the purchaser line"
|
||
);
|
||
assert_eq!(text(&s, "OpenMailInvoicePurchaser"), "FROM: Onewarrior");
|
||
assert_eq!(text(&s, "OpenMailInvoiceBuyMode"), "");
|
||
assert_eq!(money(&s, "OpenMailTransactionAmountMoneyFrame"), "9000");
|
||
for gone in [
|
||
"OpenMailInvoiceSalePrice",
|
||
"OpenMailInvoiceDeposit",
|
||
"OpenMailInvoiceHouseCut",
|
||
"OpenMailSalePriceMoneyFrame",
|
||
"OpenMailDepositMoneyFrame",
|
||
"OpenMailHouseCutMoneyFrame",
|
||
] {
|
||
assert!(
|
||
!s.eval::<bool>(&format!("return {gone}:IsShown()")).unwrap(),
|
||
"{gone} is a seller-only line"
|
||
);
|
||
}
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
/// A mail that is NOT an auction invoice keeps its letter: the pane stays hidden and the body text
|
||
/// survives. The blanking above is aimed at one kind of mail and must not reach any other.
|
||
#[test]
|
||
fn a_plain_letter_keeps_its_body_and_shows_no_receipt() {
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_mail(Some(small_inbox()));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
assert!(!s
|
||
.eval::<bool>("return OpenMailInvoiceFrame:IsShown()")
|
||
.unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return OpenMailBodyText:GetText()")
|
||
.unwrap(),
|
||
"body"
|
||
);
|
||
assert!(s.take_errors().is_empty());
|
||
}
|
||
|
||
/// The open letter's ring holds an ordinary **item** icon (`INV_Misc_Note_01`), so it has to go
|
||
/// through the PORTRAIT verb and not a bare `SetTexture` — which is exactly what the reference
|
||
/// reaches for here, and nowhere else in the file (`MailFrame.lua` l.174).
|
||
///
|
||
/// Set raw, the icon's square dark border shows through the ring's transparent corners as four
|
||
/// little squares around the circle (director's report, 2026-08-22). The INBOX window's ring is the
|
||
/// control: it holds `Mail-Icon`, purpose-drawn art that needs no mask, and the reference leaves
|
||
/// that one at its `file=` (ref l.258) — so a blanket "mask every ring" would be wrong too.
|
||
#[test]
|
||
fn the_open_letters_ring_icon_is_masked_but_the_inboxs_is_not() {
|
||
use benilla_ui::script::QuadContent;
|
||
|
||
let mut s = UiScript::new().unwrap();
|
||
load_ui(&s);
|
||
s.set_screen_size(1024.0, 768.0);
|
||
s.set_mail(Some(small_inbox()));
|
||
s.fire_event("MAIL_SHOW", vec![]);
|
||
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
|
||
s.run("MailItem1Button:Click()").unwrap();
|
||
s.resolve();
|
||
|
||
let masked = |needle: &str| -> Vec<bool> {
|
||
s.extract()
|
||
.into_iter()
|
||
.filter_map(|q| match q.content {
|
||
QuadContent::Texture {
|
||
path: Some(p),
|
||
circular,
|
||
..
|
||
} if p.contains(needle) => Some(circular),
|
||
_ => None,
|
||
})
|
||
.collect()
|
||
};
|
||
|
||
let stationery = masked("INV_Misc_Note_01");
|
||
assert!(
|
||
!stationery.is_empty(),
|
||
"the stationery icon should be drawn somewhere"
|
||
);
|
||
assert!(
|
||
stationery.contains(&true),
|
||
"the open letter's ring icon draws masked to its inscribed circle; got {stationery:?}"
|
||
);
|
||
|
||
let mail_icon = masked("Mail-Icon");
|
||
assert!(
|
||
!mail_icon.contains(&true),
|
||
"the inbox window's own ring is purpose-drawn art and stays raw, as in the reference; \
|
||
got {mail_icon:?}"
|
||
);
|
||
}
|