The reader is pushable now, and a letter covers rather than evicts

The last two UI integration files, both resting on `UIPanelWindows` rows the realm changed.

`ItemTextFrame` is `{ area = "left", pushable = 5 }` where 1.12's row was `pushable = 0`. That made
the reader and a quest giver's gossip left-slot rivals which replaced each other and ended each
other's session on the way out; pushable, they share the screen — one takes left, the other slides
to centre, in either order, and neither `CloseItemText` nor `CloseGossip` is queued. The test is
renamed for what it now proves. Its page bodies also pick up the realm's own three trailing
newlines on both arms of the creator fork (`ItemTextFrame.lua:41-46`), where 1.12 wrote two.

`OpenMailFrame` has no `UIPanelWindows` row at all, so `ShowUIPanel` takes its first branch — a
bare `frame:Show()` that claims no slot (`UIParent.lua:920-923`) — and a letter therefore *covers*
the centre occupant instead of evicting it. The eviction runs one way only: the letter is a
`UIChildWindows` member, so the next panel to seat anywhere hides it. And it has to **arrive** to
do that: `ShowUIPanel` returns on its first line for a frame already shown, so asking for the sheet
again while it is up reaches no slot and hides nothing.

Workspace: 191 red → 134, with zero regressions (`comm` against the pre-session list). Every one of
the 57 was in `ui_script` or these two files, and that whole surface is now green: 1113 `ui_script`
tests, `item_text_frame`, `mail_frame`, `message_keys`, `cover_input_wall`, `character_stats`. What
is left is the benilla-formats data goldens, which are documented separately and were red before.

All seven gates green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-31 15:38:21 -06:00
co-authored by Claude Opus 5
parent 3d6306f828
commit 792baf9859
2 changed files with 58 additions and 31 deletions
+40 -23
View File
@@ -106,8 +106,11 @@ fn a_letter_reads_with_the_creator_tail() {
s.fire_event("ITEM_TEXT_READY", vec![]);
assert!(s.eval::<bool>("return ItemTextFrame:IsShown()").unwrap());
assert!(
page_blocks(&s).contains(&"\nasd\n\nFrom,\nOne\n\n".to_string()),
"the reference creator tail (ITEM_TEXT_FROM really is comma'd), drawn as one raw block: {:?}",
page_blocks(&s).contains(&"\nasd\n\nFrom,\nOne\n\n\n".to_string()),
"the reference creator tail (ITEM_TEXT_FROM really is comma'd), drawn as one raw block. \
Three trailing newlines, not two: the realm writes \
`creator = \"\\n\\n\"..ITEM_TEXT_FROM..\"\\n\"..creator..\"\\n\\n\\n\"` and then \
`SetText(\"\\n\"..ItemTextGetText()..creator)` (`ItemTextFrame.lua:41-45`): {:?}",
page_blocks(&s)
);
for hidden in [
@@ -176,8 +179,9 @@ fn a_book_page_shows_the_paging_chrome() {
s.fire_event("ITEM_TEXT_BEGIN", vec![]);
s.fire_event("ITEM_TEXT_READY", vec![]);
assert!(
page_blocks(&s).contains(&"\npage one\n".to_string()),
"the page body drawn: {:?}",
page_blocks(&s).contains(&"\npage one\n\n\n".to_string()),
"the page body drawn, with the authorless arm's own three trailing newlines \
(`SetText(\"\\n\"..ItemTextGetText()..\"\\n\\n\\n\")`, `ItemTextFrame.lua:46`): {:?}",
page_blocks(&s)
);
assert_eq!(
@@ -381,11 +385,12 @@ fn the_reported_book_crest_draws_at_the_blps_own_size() {
}
}
/// The reader's `UIPanelWindows` row (`{ area = "left", pushable = 0 }`, `UIParent.lua:20`) makes
/// it and gossip left-slot rivals: showing one replaces the other in both orders, and each
/// displaced window's OnHide ends its own session.
/// The reader's `UIPanelWindows` row is `{ area = "left", pushable = 5 }` (`UIParent.lua:20`) —
/// 1.12's was `pushable = 0`, which made it and gossip left-slot rivals that replaced each other
/// and ended each other's session on the way out. Pushable, they share the screen instead: one
/// takes the left slot and the other slides to centre, in either order, and neither session ends.
#[test]
fn a_quest_givers_gossip_displaces_the_open_reader() {
fn a_quest_givers_gossip_seats_beside_the_open_reader() {
let _data = benilla_formats::wow_data_or_skip!();
use benilla_ui::script::GossipMenu;
@@ -412,35 +417,47 @@ fn a_quest_givers_gossip_displaces_the_open_reader() {
}));
s.fire_event("GOSSIP_SHOW", vec![]);
assert!(s.take_errors().is_empty());
// The realm made the reader **pushable** (`UIPanelWindows["ItemTextFrame"]` is
// `{ area = "left", pushable = 5 }`, `UIParent.lua:20`), where 1.12's row was `pushable = 0`.
// So gossip does not replace the note: it takes the left slot and the note slides to centre,
// both open, and the read session is not ended.
assert!(
!s.eval::<bool>("return ItemTextFrame:IsShown()").unwrap(),
"gossip replaced the note — the two-window stack (both drawn at 0,-104) cannot form"
s.eval::<bool>("return GetLeftFrame():GetName() == 'GossipFrame'")
.unwrap(),
"gossip takes the left slot"
);
assert!(s
.eval::<bool>("return GossipFrame:IsShown() and GetLeftFrame():GetName() == 'GossipFrame'")
.unwrap());
assert!(
s.take_item_text_close(),
"the displaced reader's OnHide ended the read session (CloseItemText)"
s.eval::<bool>("return GetCenterFrame():GetName() == 'ItemTextFrame'")
.unwrap(),
"…and pushes the note across rather than over"
);
// The app's close must not disturb the gossip that displaced it.
assert!(
s.eval::<bool>("return ItemTextFrame:IsShown() and GossipFrame:IsShown()")
.unwrap(),
"both windows are up: that is what a pushable row buys"
);
assert!(
!s.take_item_text_close(),
"nothing was displaced, so no `CloseItemText` — the read session stands"
);
// The app's own close takes the note and leaves the gossip that pushed it.
s.set_item_text(None);
s.fire_event("ITEM_TEXT_CLOSED", vec![]);
assert!(!s.eval::<bool>("return ItemTextFrame:IsShown()").unwrap());
assert!(s.eval::<bool>("return GossipFrame:IsShown()").unwrap());
// The reverse order: reading the note over an open gossip ends the gossip session.
// The reverse order, with gossip already at the left slot: the note arrives pushable and
// seats beside it, and the gossip session is not ended either.
let _ = s.take_gossip_close();
s.set_item_text(Some(letter()));
s.fire_event("ITEM_TEXT_BEGIN", vec![]);
s.fire_event("ITEM_TEXT_READY", vec![]);
assert!(s.take_errors().is_empty());
assert!(
s.eval::<bool>("return ItemTextFrame:IsShown() and not GossipFrame:IsShown()")
s.eval::<bool>("return ItemTextFrame:IsShown() and GossipFrame:IsShown()")
.unwrap(),
"the reader replaces gossip the same way"
);
assert!(
s.take_gossip_close(),
"the displaced gossip's OnHide ended the gossip session (CloseGossip)"
"the reader seats beside the gossip rather than replacing it"
);
assert!(!s.take_gossip_close(), "…so `CloseGossip` is not queued");
}
+18 -8
View File
@@ -252,7 +252,7 @@ fn opening_a_letter_shows_the_open_frame_and_queues_the_body() {
/// hides the centre frame (`MailFrame.xml:1903-1907`), and a frame arriving at centre hides the
/// letter. A bare stand-in carries the `CharacterFrame` row.
#[test]
fn a_letter_and_the_centre_occupant_evict_each_other() {
fn a_letter_covers_the_centre_occupant_and_a_new_arrival_hides_it() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
load_ui(&s);
@@ -283,13 +283,20 @@ fn a_letter_and_the_centre_occupant_evict_each_other() {
s.eval::<bool>("return OpenMailFrame:IsShown()").unwrap(),
"the letter opened"
);
// …and the centre occupant stays. `OpenMailFrame` has no `UIPanelWindows` row, so
// `ShowUIPanel` takes its first branch — `if ( not GetUIPanelWindowInfo(frame, "area") ) then
// frame:Show(); return; end` (`UIParent.lua:920-923`) — a bare Show that claims no slot and
// evicts nothing. The eviction runs the other way only: it is a `UIChildWindows` member, so
// the next panel to seat anywhere hides it (`CloseChildWindows`, `:792-796`), which is the
// second half of this test.
assert!(
!s.eval::<bool>("return CharacterFrame:IsShown()").unwrap(),
"the centre occupant was evicted, not covered (the reported bug)"
s.eval::<bool>("return CharacterFrame:IsShown()").unwrap(),
"the centre occupant is covered, not evicted"
);
assert!(
s.eval::<bool>("return GetCenterFrame() == nil").unwrap(),
"the eviction is a plain vacate — nothing slides"
s.eval::<bool>("return GetCenterFrame():GetName() == 'CharacterFrame'")
.unwrap(),
"and it still holds the centre slot"
);
assert!(
s.eval::<bool>("return MailFrame:IsShown() and GetLeftFrame():GetName() == 'MailFrame'")
@@ -297,13 +304,16 @@ fn a_letter_and_the_centre_occupant_evict_each_other() {
"the mailbox itself is untouched"
);
// The reverse: re-opening the sheet over the open letter puts the letter away.
s.run("ShowUIPanel(CharacterFrame)").unwrap();
// The reverse: a sheet **arriving** over the open letter puts the letter away. It has to
// arrive — `ShowUIPanel` returns on its first line for a frame that is already shown
// (`UIParent.lua:916-918`), so asking again while it is up reaches no slot and hides nothing.
s.run("HideUIPanel(CharacterFrame) 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"
"a frame arriving at a slot hides the letter"
);
}