Files
benilla-octo/crates/benilla/tests/item_text_frame.rs
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

187 lines
6.7 KiB
Rust

//! Drives the REAL `assets/ui/ItemTextFrame.xml` through the engine — the reader window for bag
//! letters (mail-made permanent copies) and, later, books. Loads the same file chain the app does
//! (cut to the reader's dependency prefix), pushes an `ItemTextState`, fires the reference event
//! flow `ITEM_TEXT_BEGIN` → `ITEM_TEXT_READY` → `ITEM_TEXT_CLOSED`, and asserts the transcribed
//! Lua actually paints (title, the "From," creator tail, page-button visibility, the close intent).
use benilla_ui::script::{ItemTextState, UiScript};
const UI_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ui");
const FILES: [&str; 3] = ["Fonts.xml", "UiPanels.xml", "ItemTextFrame.xml"];
fn load_ui(script: &UiScript) {
let dir = std::path::Path::new(UI_DIR);
let provider = |req: &str| -> Option<String> {
let norm = req.replace('\\', "/");
let base = norm.rsplit('/').next().unwrap_or(&norm);
std::fs::read_to_string(dir.join(&norm))
.or_else(|_| std::fs::read_to_string(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
);
}
}
fn letter() -> ItemTextState {
ItemTextState {
item: "Plain Letter".into(),
creator: Some("One".into()),
text: "asd".into(),
page: 1,
has_next: false,
material: None,
}
}
/// The letter flow: BEGIN paints the title, READY paints the body with the reference "From," tail
/// and shows the window; a single page shows no page number and no page-turn buttons.
#[test]
fn a_letter_reads_with_the_creator_tail() {
let mut s = UiScript::new().unwrap();
load_ui(&s);
assert!(!s
.eval::<bool>("return BenillaItemTextFrame:IsShown()")
.unwrap());
s.set_item_text(Some(letter()));
s.fire_event("ITEM_TEXT_BEGIN", vec![]);
assert_eq!(
s.eval::<String>("return BenillaItemTextTitleText:GetText()")
.unwrap(),
"Plain Letter"
);
s.fire_event("ITEM_TEXT_READY", vec![]);
assert!(s
.eval::<bool>("return BenillaItemTextFrame:IsShown()")
.unwrap());
assert_eq!(
s.eval::<String>("return BenillaItemTextPageText:GetText()")
.unwrap(),
"\nasd\n\nFrom,\nOne\n\n",
"the reference creator tail (ITEM_TEXT_FROM really is comma'd)"
);
for hidden in [
"BenillaItemTextCurrentPage",
"BenillaItemTextPrevPageButton",
"BenillaItemTextNextPageButton",
"BenillaItemTextStatusBar",
] {
assert!(
!s.eval::<bool>(&format!("return {hidden}:IsShown()"))
.unwrap(),
"{hidden} must stay hidden on a single-page letter"
);
}
assert!(s.take_errors().is_empty());
}
/// The scrollbar track (the ref's black `$parentMiddle` strip) belongs in the scrollbar column,
/// right of the page — regression for the black bar over the parchment: the ref declares the
/// ARTWORK layer before BACKGROUND because `Middle` anchors to `Top` by name and anchors resolve
/// at SetPoint time; a reordered transcription silently fell back to the parent. The warning
/// check pins the tripwire that now catches any such unresolved named anchor at load.
#[test]
fn the_scrollbar_track_sits_right_of_the_page() {
let mut s = UiScript::new().unwrap();
load_ui(&s);
let unresolved: Vec<String> = s
.warnings()
.into_iter()
.filter(|w| w.contains("does not resolve"))
.collect();
assert!(unresolved.is_empty(), "unresolved anchors: {unresolved:#?}");
s.set_screen_size(1024.0, 768.0);
s.set_item_text(Some(letter()));
s.fire_event("ITEM_TEXT_BEGIN", vec![]);
s.fire_event("ITEM_TEXT_READY", vec![]);
s.resolve();
let (track_left, page_right): (f32, f32) = s
.eval(
"return BenillaItemTextScrollFrameMiddle:GetLeft(), \
BenillaItemTextScrollFrame:GetRight()",
)
.unwrap();
assert!(
track_left >= page_right - 0.5,
"the track ({track_left}) must not overlap the page (right edge {page_right})"
);
assert!(s.take_errors().is_empty());
}
/// An authorless multi-page text (a book): no "From," tail, the page number and the page-turn
/// buttons show per the reference visibility rules (page 1 with a next → Next only).
#[test]
fn a_book_page_shows_the_paging_chrome() {
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_item_text(Some(ItemTextState {
item: "Lament of the Highborne".into(),
creator: None,
text: "page one".into(),
page: 1,
has_next: true,
material: None,
}));
s.fire_event("ITEM_TEXT_BEGIN", vec![]);
s.fire_event("ITEM_TEXT_READY", vec![]);
assert_eq!(
s.eval::<String>("return BenillaItemTextPageText:GetText()")
.unwrap(),
"\npage one\n"
);
assert_eq!(
s.eval::<String>("return BenillaItemTextCurrentPage:GetText()")
.unwrap(),
"1"
);
assert!(!s
.eval::<bool>("return BenillaItemTextPrevPageButton:IsShown()")
.unwrap());
assert!(s
.eval::<bool>("return BenillaItemTextNextPageButton:IsShown()")
.unwrap());
s.run("BenillaItemTextNextPageButton:Click()").unwrap();
assert_eq!(s.take_item_text_page_turns(), vec![1]);
assert!(s.take_errors().is_empty());
}
/// Closing: the X hides the panel, the OnHide queues the `CloseItemText` intent (the app then
/// clears the session and fires `ITEM_TEXT_CLOSED` — which hides again, harmlessly).
#[test]
fn closing_queues_the_close_intent() {
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_item_text(Some(letter()));
s.fire_event("ITEM_TEXT_BEGIN", vec![]);
s.fire_event("ITEM_TEXT_READY", vec![]);
let _ = s.take_item_text_close();
s.run("BenillaItemTextCloseButton:Click()").unwrap();
assert!(!s
.eval::<bool>("return BenillaItemTextFrame:IsShown()")
.unwrap());
assert!(s.take_item_text_close(), "OnHide queued the close intent");
// The app's answer: session cleared + ITEM_TEXT_CLOSED (stays hidden, no errors).
s.set_item_text(None);
s.fire_event("ITEM_TEXT_CLOSED", vec![]);
assert!(!s
.eval::<bool>("return BenillaItemTextFrame:IsShown()")
.unwrap());
assert!(s.take_errors().is_empty());
}