- the vendor window is the reference's own `MerchantFrame.xml`, which took three things in order: the vendor cursor, the coin kit's move, and `GameTooltip:SetMerchantCompareItem` — where `offset` is the ordinal among CANDIDATE slots (empty ones skipped without decrementing) and a candidate matches by item CLASS and never by entry (two of four RE workers read it as an entry match, which would have fired the compare tooltip only when you already wear the identical item, i.e. never, with the window looking finished); two visible changes ride with the reference's rows — the coin columns take `SmallMoneyFrameTemplate`'s own geometry, and only the ICON takes the mouse, so hovering a row's name shows nothing, which is the client's behaviour - the gossip window is the reference's own, and its deferral was wrong: the "2px rows" were a harness gap (a bare test VM with no text measurer, while the app always installs one), not an engine one; four things the reference does that ours did not are pinned — a hidden SPACER row between the quest groups, coded options are NOT greyed (a coded row is clickable, and the send path's missing code is named as its own gap), the row label is the Button's own text, and the scroll child is not grown - the CAMP/QUIT dialogs and the PARTY_INVITE dialog move to `StaticPopup`'s home — the fourth such rehome of a dialog that had lived in whichever window first needed it — and moving them found a real bug: `button1 = CANCEL` captured a nil at load because `CANCEL` was declared below the insertion point, so the popup engine fell back and put "Yes" on the camp dialog's cancel button - three instruments: the gap report stops counting commented-out calls (`ShowInventorySellCursor` blocked the character sheet on three commented lines — a whole RE round spent on an over-report), it runs the load itself and answers in ONE table (29 "free" became 24 actually unblocked), and a shadow report predicts which swaps will fail by intersecting the names our files define with the names the chain files in our own manifest define — 38 collisions across 5 files, ours winning every one Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1412 lines
60 KiB
Rust
1412 lines
60 KiB
Rust
//! The shipped **quest log window** driven end-to-end, engine-only (no Bevy): the real
|
||
//! `assets/ui/QuestLogFrame.xml` loaded behind `UiPanels.xml`/`MerchantFrame.xml` (the money helpers
|
||
//! its reward rows reuse) and fed a synthetic 8-entry log + a resolved detail — mirroring
|
||
//! `quest_tests.rs`/`bag_tests.rs`'s engine-only harness for the quest-log slice (decision 0088 arc).
|
||
|
||
use benilla_ui::script::{
|
||
ExtractedQuad, PartyMemberInfo, PartyState, QuadContent, QuestItemView, QuestLogDetail,
|
||
QuestLogEntryView, QuestLogObjectiveView, QuestLogState, SoundRequest, UiScript,
|
||
};
|
||
|
||
use super::test_ui::load_ui as load_xml;
|
||
|
||
/// Find a bare frame's own rect via its `QuadContent::Frame` entry (`panel_tests.rs`'s helper,
|
||
/// duplicated) — used to locate the window so the wheel test's coordinates land inside it.
|
||
fn frame_rect(quads: &[ExtractedQuad], w: f32, h: f32) -> benilla_ui::layout::Rect {
|
||
quads
|
||
.iter()
|
||
.find_map(|q| match &q.content {
|
||
QuadContent::Frame => q
|
||
.rect
|
||
.filter(|r| (r.width() - w).abs() < 0.5 && (r.height() - h).abs() < 0.5),
|
||
_ => None,
|
||
})
|
||
.unwrap_or_else(|| panic!("no bare-frame quad sized {w}x{h}"))
|
||
}
|
||
|
||
/// 8 flat quest entries (exercises the 6-row faux-scroll), the first carrying one objective line
|
||
/// (the auto-picked first selection reads it), + a resolved detail for that selection (no choices,
|
||
/// 1 fixed reward + money) — the fixture every test below shares. Each entry gets a distinct
|
||
/// `quest_id` (the watch set's stable key — `benilla-ui`'s `quest_log.rs` module doc) so the
|
||
/// watch/tracker tests below can tell entries apart across a scroll/reselect.
|
||
fn eight_entries() -> QuestLogState {
|
||
let entries = (1..=8)
|
||
.map(|i| QuestLogEntryView {
|
||
quest_id: i,
|
||
title: format!("Quest {i}"),
|
||
level: 5,
|
||
complete: 0,
|
||
objectives: if i == 1 {
|
||
vec![QuestLogObjectiveView {
|
||
text: "Kobold Vermin slain: 3/10".into(),
|
||
kind: "monster".into(),
|
||
finished: false,
|
||
cur: 3,
|
||
req: 10,
|
||
}]
|
||
} else {
|
||
vec![]
|
||
},
|
||
..Default::default()
|
||
})
|
||
.collect();
|
||
QuestLogState {
|
||
num_quests: 8,
|
||
entries,
|
||
detail: Some(QuestLogDetail {
|
||
description: "Speak with Marshal McBride.".into(),
|
||
objectives_text: "Report to Marshal McBride.".into(),
|
||
required_money: 0,
|
||
reward_money: 40,
|
||
choices: vec![],
|
||
rewards: vec![QuestItemView {
|
||
item_id: 2024,
|
||
name: Some("Militia Hammer".into()),
|
||
texture: Some("Interface\\Icons\\INV_Hammer_15".into()),
|
||
count: 1,
|
||
quality: 1,
|
||
usable: true,
|
||
// The reward row's ctrl/shift payload (`GetQuestLogItemLink`, decisions 1059/1060)
|
||
// — `ui_quest_log.rs` builds it through `ui_items::item_link` once the template
|
||
// lands, so the fixture carries that exact shape.
|
||
link: Some(HAMMER_LINK.into()),
|
||
}],
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// The fixture reward's full escaped link — a real 1.12 item, quality white.
|
||
const HAMMER_LINK: &str = "|cffffffff|Hitem:2024:0:0:0|h[Militia Hammer]|h|r";
|
||
|
||
/// The loader itself: every file the window depends on parses and materializes with no errors.
|
||
#[test]
|
||
fn shipped_questlog_frame_loads_clean() {
|
||
let s = UiScript::new().unwrap();
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
}
|
||
|
||
/// The whole contract in one end-to-end drive: open plays the kit and renders row 1 + the count +
|
||
/// the auto-picked first selection + the pushed detail text; the wheel scrolls the list; the abandon
|
||
/// two-step (mark on click, confirm Yes/No) drains (or doesn't) the right intent; close plays the
|
||
/// close kit.
|
||
#[test]
|
||
fn shipped_questlog_frame_drives_end_to_end() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(eight_entries());
|
||
|
||
// Hidden at load: no sound queued (never transitions on startup).
|
||
assert!(
|
||
s.take_sounds().is_empty(),
|
||
"no sound at load (never transitions)"
|
||
);
|
||
assert!(!s.eval::<bool>("return QuestLogFrame:IsVisible()").unwrap());
|
||
|
||
// ToggleQuestLog() (the 'L' binding's entry point) opens it through ShowUIPanel.
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
assert!(s.eval::<bool>("return QuestLogFrame:IsVisible()").unwrap());
|
||
assert_eq!(
|
||
s.take_sounds(),
|
||
vec![SoundRequest::KitName("igQuestLogOpen".into())],
|
||
"opening the quest log plays igQuestLogOpen"
|
||
);
|
||
|
||
// Row 1 carries entry 1's title (indented), the count line reads "Quests: |cffffffff8/20|r", and nothing having been
|
||
// selected, the first non-header entry auto-selects (pin §2's SetFirstValidSelection).
|
||
assert!(s
|
||
.eval::<String>("return QuestLogTitle1Text:GetText()")
|
||
.unwrap()
|
||
.contains("Quest 1"));
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogQuestCount:GetText()")
|
||
.unwrap(),
|
||
"Quests: |cffffffff8/20|r"
|
||
);
|
||
assert_eq!(s.eval::<i64>("return GetQuestLogSelection()").unwrap(), 1);
|
||
|
||
// The detail pane carries the pushed title + objective text.
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogQuestTitle:GetText()")
|
||
.unwrap(),
|
||
"Quest 1"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogObjective1:GetText()")
|
||
.unwrap(),
|
||
"Kobold Vermin slain: 3/10"
|
||
);
|
||
// The reward row (QuestFrame.xml's item-row pattern, reused) + its money.
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItem1Name:GetText()")
|
||
.unwrap(),
|
||
"Militia Hammer"
|
||
);
|
||
assert_ne!(
|
||
s.eval::<String>("return QuestLogRewardTitleText:GetText()")
|
||
.unwrap(),
|
||
"",
|
||
"the Rewards header shows once there's a reward to show"
|
||
);
|
||
// Lua-managed Description header (no longer a static `text=` — QuestLogFrame.xml's header
|
||
// comment on why it used to float over the empty-log parchment).
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogDescriptionTitle:GetText()")
|
||
.unwrap(),
|
||
"Description"
|
||
);
|
||
// No choices in this fixture: "You will receive:" (REWARD_ITEMS_ONLY), not "...also...", and
|
||
// the choose-text/choice rows stay blank/hidden (QuestFrame.lua:454-473).
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItemReceiveText:GetText()")
|
||
.unwrap(),
|
||
"You will receive:"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItemChooseText:GetText()")
|
||
.unwrap(),
|
||
""
|
||
);
|
||
// No choices, so the pool's first slot IS the fixed reward and the SECOND is the first
|
||
// unused one. (One pool now — the ref's shape; there is no separate "choice row 1".)
|
||
assert!(!s.eval::<bool>("return QuestLogItem2:IsShown()").unwrap());
|
||
|
||
// Wheel DOWN over an actual list ROW (delta -1) advances the faux-scroll offset by one: row 1 now
|
||
// shows entry 2's title. Aiming over a row (not the empty margin) is the case that matters — a spin
|
||
// over a row bubbles up the parent chain to the window's OnMouseWheel; when that handler lived only
|
||
// on the sibling catcher, wheeling over a row did nothing (the trainer window hit the same bug).
|
||
// Row 3 ("Quest 3") is unambiguously a list row — the detail pane shows the selected Quest 1.
|
||
s.resolve();
|
||
let quads = s.extract();
|
||
let (wx, wy) = quads
|
||
.iter()
|
||
.find_map(|q| match &q.content {
|
||
QuadContent::Text { text: Some(t), .. } if t.contains("Quest 3") => q
|
||
.rect
|
||
.map(|r| ((r.left + r.right) * 0.5, (r.bottom + r.top) * 0.5)),
|
||
_ => None,
|
||
})
|
||
.expect("a 'Quest 3' row text quad");
|
||
s.mouse_wheel(wx, wy, -1.0);
|
||
assert!(s.errors().is_empty(), "wheel errors: {:?}", s.errors());
|
||
assert!(
|
||
s.eval::<String>("return QuestLogTitle1Text:GetText()")
|
||
.unwrap()
|
||
.contains("Quest 2"),
|
||
"wheel-down scrolled the list by one row"
|
||
);
|
||
// Selection itself is untouched by scrolling.
|
||
assert_eq!(s.eval::<i64>("return GetQuestLogSelection()").unwrap(), 1);
|
||
|
||
// Abandon, No path: marks the selection, shows the registry's ABANDON_QUEST entry on the
|
||
// shared StaticPopup engine (decision 0308 §3), but No drains nothing and hides it.
|
||
s.run("BenillaQuestLogAbandonButton_OnClick()").unwrap();
|
||
assert!(s.eval::<bool>("return StaticPopup1:IsShown()").unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return GetAbandonQuestName()").unwrap(),
|
||
"Quest 1"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return StaticPopup1Text:GetText()")
|
||
.unwrap(),
|
||
"Abandon \"Quest 1\"?"
|
||
);
|
||
s.run("StaticPopup_OnClick(StaticPopup1, 2)").unwrap();
|
||
assert!(!s.eval::<bool>("return StaticPopup1:IsShown()").unwrap());
|
||
assert!(
|
||
s.take_quest_log_abandons().is_empty(),
|
||
"No queues no abandon intent"
|
||
);
|
||
// No plays the dialog's own open/close pair only (ref StaticPopup_OnShow/OnHide,
|
||
// StaticPopup.lua:1832/1841) — never the abandon kit.
|
||
assert_eq!(
|
||
s.take_sounds(),
|
||
vec![
|
||
SoundRequest::KitName("igMainMenuOpen".into()),
|
||
SoundRequest::KitName("igMainMenuClose".into()),
|
||
],
|
||
"No: dialog open/close kits only, no abandon kit"
|
||
);
|
||
|
||
// Abandon, Yes path: the pinned index (1, the selection at click time) drains, plus the kit.
|
||
s.run("BenillaQuestLogAbandonButton_OnClick()").unwrap();
|
||
s.run("StaticPopup_OnClick(StaticPopup1, 1)").unwrap();
|
||
assert_eq!(s.take_quest_log_abandons(), vec![1]);
|
||
// Show → igMainMenuOpen; the ref OnClick runs OnAccept FIRST (the abandon kit), THEN hides
|
||
// (igMainMenuClose) — StaticPopup.lua:1850-1868's order, which the old shim had reversed.
|
||
assert_eq!(
|
||
s.take_sounds(),
|
||
vec![
|
||
SoundRequest::KitName("igMainMenuOpen".into()),
|
||
SoundRequest::KitName("igQuestLogAbandonQuest".into()),
|
||
SoundRequest::KitName("igMainMenuClose".into()),
|
||
]
|
||
);
|
||
assert!(!s.eval::<bool>("return StaticPopup1:IsShown()").unwrap());
|
||
|
||
// ToggleQuestLog() again closes it through HideUIPanel, playing the close kit.
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
assert!(!s.eval::<bool>("return QuestLogFrame:IsVisible()").unwrap());
|
||
assert_eq!(
|
||
s.take_sounds(),
|
||
vec![SoundRequest::KitName("igQuestLogClose".into())],
|
||
"closing the quest log plays igQuestLogClose"
|
||
);
|
||
}
|
||
|
||
/// The quest-watch tracker slice: SHIFT+left-click toggles the watch (the ref's own fork,
|
||
/// QuestLogFrame.lua:469-505 — driven through the `set_modifiers` mirror the cursor arc landed),
|
||
/// toggling both the row's watch checkbox (a `SetTexture` toggle — bare Texture regions have no
|
||
/// Show/Hide, same limit as the FontString regions elsewhere in this window) and the
|
||
/// always-on-screen tracker HUD (`QuestWatchFrame`), which the log window itself doesn't
|
||
/// own or gate on visibility.
|
||
#[test]
|
||
fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(eight_entries());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
// Nothing watched yet — the tracker HUD is hidden and row 1 shows no checkbox texture.
|
||
assert!(!s
|
||
.eval::<bool>("return QuestWatchFrame:IsVisible()")
|
||
.unwrap());
|
||
let checkbox_shown = |s: &mut UiScript| {
|
||
s.resolve();
|
||
s.extract().iter().any(|q| {
|
||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||
if p.contains("UI-CheckBox-Check"))
|
||
})
|
||
};
|
||
assert!(!checkbox_shown(&mut s), "no checkbox before any watch");
|
||
|
||
// Shift-click row 1 ("Quest 1", the entry `eight_entries` gives one objective — a zero-objective
|
||
// quest can't be watched, ref QUEST_WATCH_NO_OBJECTIVES). Row 1 sits at the list's TOPLEFT+(19,
|
||
// -75), 300×16 (`QuestLogFrame.xml`'s own row-chain comment) — its center is inside the button
|
||
// but outside the checkbox's outside-the-row gutter, so this can't be mistaken for a checkbox hit.
|
||
s.resolve();
|
||
let win = frame_rect(&s.extract(), 384.0, 512.0);
|
||
let (rx, ry) = (win.left + 19.0 + 150.0, win.top - 75.0 - 8.0);
|
||
s.set_modifiers(true, false, false);
|
||
s.mouse_button(rx, ry, "LeftButton", true);
|
||
s.mouse_button(rx, ry, "LeftButton", false);
|
||
s.set_modifiers(false, false, false);
|
||
assert!(
|
||
s.errors().is_empty(),
|
||
"shift-click errors: {:?}",
|
||
s.errors()
|
||
);
|
||
|
||
assert!(s.eval::<bool>("return IsQuestWatched(1)").unwrap());
|
||
assert_eq!(s.eval::<i64>("return GetNumQuestWatches()").unwrap(), 1);
|
||
assert!(checkbox_shown(&mut s), "watched row shows its checkbox");
|
||
// The check SEATS just past the title's ink (ref QuestLog.lua:224 — dummy width + 24 from the
|
||
// row's left, netting ink end + 4 behind the text's own +20 inset), NOT the first build's
|
||
// outside-the-row gutter at row_left − 14. The engine-only harness has no font atlas, so
|
||
// answer the measure round-trip with a deterministic 6 px/char fake (the app-side extract
|
||
// answers from the real atlas) — the assertion pins the ANCHOR GRAPH seating the check at
|
||
// measured ink + 4, in a loose band that bakes no glyph metrics into the test.
|
||
let reqs = s.fontstrings_needing_measure();
|
||
let answers: Vec<(u32, f32, f32, u64)> = reqs
|
||
.iter()
|
||
.map(|r| (r.id, r.text.chars().count() as f32 * 6.0, 12.0, r.key))
|
||
.collect();
|
||
s.set_measured_text_unwrapped(&answers);
|
||
s.resolve();
|
||
let check = s
|
||
.extract()
|
||
.into_iter()
|
||
.find(|q| {
|
||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
|
||
if p.contains("UI-CheckBox-Check"))
|
||
})
|
||
.expect("check quad");
|
||
let check_left = check.rect.expect("check rect resolved").left;
|
||
let row_left = win.left + 19.0;
|
||
assert!(
|
||
check_left > row_left + 40.0 && check_left < row_left + 150.0,
|
||
"check seats after the title ink, got left = {check_left} (row_left = {row_left})"
|
||
);
|
||
|
||
// The tracker HUD is now visible: the flat line pool (ref MAX_QUESTWATCH_LINES) carries the
|
||
// watched quest's title on line 1 and its objective on line 2, ref-colored (title dark-gold —
|
||
// objectives incomplete; objective 0.8-gray — unfinished).
|
||
assert!(s
|
||
.eval::<bool>("return QuestWatchFrame:IsVisible()")
|
||
.unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestWatchLine1:GetText()")
|
||
.unwrap(),
|
||
"Quest 1"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestWatchLine2:GetText()")
|
||
.unwrap(),
|
||
" - Kobold Vermin slain: 3/10"
|
||
);
|
||
// A manual watch is permanent: no auto-watch timer entry rides it.
|
||
assert!(s
|
||
.eval::<bool>("return BENILLA_QUEST_WATCH_TIMERS[\"Quest 1\"] == nil")
|
||
.unwrap());
|
||
|
||
// Shift-click again unwatches: the checkbox clears and — nothing left watched — the whole HUD
|
||
// hides (the ref hides the FRAME and returns before touching the lines; stale text stays,
|
||
// drawn by nothing).
|
||
s.set_modifiers(true, false, false);
|
||
s.mouse_button(rx, ry, "LeftButton", true);
|
||
s.mouse_button(rx, ry, "LeftButton", false);
|
||
s.set_modifiers(false, false, false);
|
||
assert!(!s.eval::<bool>("return IsQuestWatched(1)").unwrap());
|
||
assert!(!checkbox_shown(&mut s), "unwatching clears the checkbox");
|
||
assert!(!s
|
||
.eval::<bool>("return QuestWatchFrame:IsVisible()")
|
||
.unwrap());
|
||
}
|
||
|
||
/// Shift-clicking a quest with zero objectives (ref `QUEST_WATCH_NO_OBJECTIVES`) and shift-clicking
|
||
/// past `GetNumQuestWatches() >= 5` (ref `QUEST_WATCH_TOO_MANY`) both refuse the watch and put the
|
||
/// ref's red line on the errors frame through its own `UIErrorsFrame:AddMessage`; the click still
|
||
/// selects the row exactly like any other click (pin §5).
|
||
#[test]
|
||
fn watch_guards_no_op_without_erroring() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // the guards' red-line surface
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(eight_entries());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
|
||
// Row 1's TOPLEFT is the window's (19,-75); each of the 6 visible rows is 300×16 chained with a
|
||
// 1px OVERLAP, not a gap (`QuestLogFrame.xml`'s row-chain comment) — pitch 16-1=15px. Row n's
|
||
// center, screen space.
|
||
s.resolve();
|
||
let win = frame_rect(&s.extract(), 384.0, 512.0);
|
||
let row_center = |n: u32| win.top - 75.0 - 15.0 * (n - 1) as f32 - 8.0;
|
||
let x = win.left + 19.0 + 150.0;
|
||
|
||
// Row 2 ("Quest 2") carries no objectives in this fixture — shift-clicking it can't watch.
|
||
s.set_modifiers(true, false, false);
|
||
s.mouse_button(x, row_center(2), "LeftButton", true);
|
||
s.mouse_button(x, row_center(2), "LeftButton", false);
|
||
s.set_modifiers(false, false, false);
|
||
assert!(
|
||
s.errors().is_empty(),
|
||
"no-objectives guard errors: {:?}",
|
||
s.errors()
|
||
);
|
||
assert!(!s.eval::<bool>("return IsQuestWatched(2)").unwrap());
|
||
// Read the toast off the drawn quads: `UIErrorsFrame` is a real `<MessageFrame>` and its
|
||
// lines are the widget's own bands, not named FontStrings.
|
||
s.resolve();
|
||
assert!(
|
||
s.extract().iter().any(|q| matches!(
|
||
&q.content,
|
||
QuadContent::Text { text: Some(t), .. } if t == "This quest has no objectives to track"
|
||
)),
|
||
"the ref's QUEST_WATCH_NO_OBJECTIVES red line surfaces"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<i64>("return GetQuestLogSelection()").unwrap(),
|
||
2,
|
||
"the click still selects"
|
||
);
|
||
|
||
// Fill the watch list to the cap (5) with quests 2-6 via the Lua API directly — the engine-level
|
||
// AddQuestWatch (`quest_log.rs`) has no objectives guard of its own (that guard is this file's,
|
||
// in the click handler only — mirroring the ref's own split between the native call and
|
||
// `QuestLogTitleButton_OnClick`'s Lua-side checks), so a zero-objective quest CAN be watched this
|
||
// way. This deliberately leaves quest 1 (the fixture's only objective-bearing entry) unwatched.
|
||
for i in 2..=6 {
|
||
s.run(&format!("AddQuestWatch({i})")).unwrap();
|
||
}
|
||
assert_eq!(s.eval::<i64>("return GetNumQuestWatches()").unwrap(), 5);
|
||
|
||
// Row 1 ("Quest 1", unwatched, HAS an objective) — shift-clicking it now clears the
|
||
// no-objectives guard but hits the watch-list-full guard instead: still a no-op.
|
||
s.set_modifiers(true, false, false);
|
||
s.mouse_button(x, row_center(1), "LeftButton", true);
|
||
s.mouse_button(x, row_center(1), "LeftButton", false);
|
||
s.set_modifiers(false, false, false);
|
||
assert!(
|
||
s.errors().is_empty(),
|
||
"too-many guard errors: {:?}",
|
||
s.errors()
|
||
);
|
||
assert!(!s.eval::<bool>("return IsQuestWatched(1)").unwrap());
|
||
assert_eq!(s.eval::<i64>("return GetNumQuestWatches()").unwrap(), 5);
|
||
s.resolve();
|
||
assert!(
|
||
s.extract().iter().any(|q| matches!(
|
||
&q.content,
|
||
QuadContent::Text { text: Some(t), .. } if t == "You may only watch 5 quests at a time"
|
||
)),
|
||
"the ref's QUEST_WATCH_TOO_MANY red line surfaces"
|
||
);
|
||
}
|
||
|
||
/// The auto quest watch (ref `AUTO_QUEST_WATCH` default-on, QuestLogFrame.lua:702-786 —
|
||
/// "Quests are automatically watched for 5 minutes when you achieve a quest objective."): the
|
||
/// engine's per-quest `BENILLA_QUEST_PROGRESS(logIndex)` arms a 300 s timed watch (the native
|
||
/// QUEST_WATCH_UPDATE carries the WATCH-LIST index per the §5-verified byte law — repaint only,
|
||
/// the shipped 1.12 auto-watch chain being broken at that seam), fresh progress
|
||
/// re-arms it, and expiry (the watch frame's OnUpdate) unwatches and hides the empty HUD.
|
||
#[test]
|
||
fn progress_auto_watches_for_five_minutes() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
s.set_quest_log(eight_entries());
|
||
|
||
// A kill credit lands: the engine fires the event with the quest's 1-based log index.
|
||
s.fire_event(
|
||
"BENILLA_QUEST_PROGRESS",
|
||
vec![benilla_ui::script::ScriptValue::Int(1)],
|
||
);
|
||
assert!(s.errors().is_empty(), "auto-watch errors: {:?}", s.errors());
|
||
assert!(s.eval::<bool>("return IsQuestWatched(1)").unwrap());
|
||
assert!(s
|
||
.eval::<bool>("return QuestWatchFrame:IsVisible()")
|
||
.unwrap());
|
||
assert!(s
|
||
.eval::<bool>("return BENILLA_QUEST_WATCH_TIMERS[\"Quest 1\"] ~= nil")
|
||
.unwrap());
|
||
|
||
// 299 s in it still holds; fresh progress re-arms the timer; expiry unwatches and hides.
|
||
s.tick(299.0);
|
||
assert!(s.eval::<bool>("return IsQuestWatched(1)").unwrap());
|
||
s.fire_event(
|
||
"BENILLA_QUEST_PROGRESS",
|
||
vec![benilla_ui::script::ScriptValue::Int(1)],
|
||
);
|
||
s.tick(299.0);
|
||
assert!(
|
||
s.eval::<bool>("return IsQuestWatched(1)").unwrap(),
|
||
"re-armed by fresh progress"
|
||
);
|
||
s.tick(2.0);
|
||
assert!(
|
||
!s.eval::<bool>("return IsQuestWatched(1)").unwrap(),
|
||
"expired after 300 s without progress"
|
||
);
|
||
assert!(!s
|
||
.eval::<bool>("return QuestWatchFrame:IsVisible()")
|
||
.unwrap());
|
||
assert!(s.errors().is_empty(), "expiry errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The flag that gates the above is the **reference's** uvar under the reference's name and value
|
||
/// shape (decision 1136 — it was `BENILLA_AUTO_QUEST_WATCH`, a boolean, while nothing could change
|
||
/// it): `AUTO_QUEST_WATCH`, a `"1"`/`"0"` string, which is what the Interface page's row writes.
|
||
/// Off, progress no longer watches anything; back on, it does again.
|
||
///
|
||
/// The default asserted first is the one 1.12 *advertises* and does not deliver: it assigns the
|
||
/// NUMBER `1` (UIOptionsFrame.lua:122) while every consumer compares `== "1"`, so the shipped
|
||
/// client's auto-watch is dead until the player toggles the box. Ours ships the string.
|
||
#[test]
|
||
fn the_auto_watch_flag_is_the_references_uvar_and_gates_the_watch() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
s.set_quest_log(eight_entries());
|
||
assert_eq!(s.eval::<String>("return AUTO_QUEST_WATCH").unwrap(), "1");
|
||
|
||
s.run(r#"AUTO_QUEST_WATCH = "0""#).unwrap();
|
||
s.fire_event(
|
||
"BENILLA_QUEST_PROGRESS",
|
||
vec![benilla_ui::script::ScriptValue::Int(1)],
|
||
);
|
||
assert!(
|
||
!s.eval::<bool>("return IsQuestWatched(1)").unwrap(),
|
||
"the flag off means progress watches nothing"
|
||
);
|
||
|
||
s.run(r#"AUTO_QUEST_WATCH = "1""#).unwrap();
|
||
s.fire_event(
|
||
"BENILLA_QUEST_PROGRESS",
|
||
vec![benilla_ui::script::ScriptValue::Int(1)],
|
||
);
|
||
assert!(s.eval::<bool>("return IsQuestWatched(1)").unwrap());
|
||
assert!(s.errors().is_empty(), "auto-watch errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The empty-log v1 simplification (this file's XML header comment): zero entries hides every row,
|
||
/// disables Abandon, and shows the centered empty-state message instead of `EmptyQuestLogFrame`.
|
||
#[test]
|
||
fn empty_quest_log_hides_rows_and_disables_abandon() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(QuestLogState::default());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
// A bare FontString region has no Show/Hide/IsVisible in this engine (only Frames/Buttons do —
|
||
// `region.rs`'s method table) — the empty-state message is a toggled SetText, read back here.
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogNoQuestsText:GetText()")
|
||
.unwrap(),
|
||
"Your quest log is empty."
|
||
);
|
||
assert!(!s.eval::<bool>("return QuestLogTitle1:IsVisible()").unwrap());
|
||
assert!(!s
|
||
.eval::<bool>("return QuestLogFrameAbandonButton:IsEnabled() ~= 0")
|
||
.unwrap());
|
||
// The Description header is now Lua-managed (BenillaQuestLogDetail_Clear blanks it) rather than
|
||
// a static `text=` — it used to float "Description" over the empty-log parchment with no
|
||
// selection (this task's fix; QuestLogFrame.xml's header comment on the FontString).
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogDescriptionTitle:GetText()")
|
||
.unwrap(),
|
||
""
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogQuestCount:GetText()")
|
||
.unwrap(),
|
||
"Quests: |cffffffff0/20|r"
|
||
);
|
||
}
|
||
|
||
/// `BenillaQuestLogRewards_Update`'s ported `QuestFrameItems_Update` layout (QuestFrame.lua:311-522)
|
||
/// — 2 choices + 1 fixed reward + money, the same shape the capture fixture (`ui-questlog`) drives:
|
||
/// choice rows 2-per-row, "You will also receive:" (choices present) anchored under the LEFT column
|
||
/// of the last choice row, the fixed reward below that, unused rows/choices past the count hidden.
|
||
#[test]
|
||
fn reward_rows_follow_the_refs_two_per_row_layout() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
let mut state = eight_entries();
|
||
state.detail = Some(QuestLogDetail {
|
||
description: "Speak with Marshal McBride.".into(),
|
||
objectives_text: "Report to Marshal McBride.".into(),
|
||
required_money: 0,
|
||
reward_money: 150, // 1s50c, the fixture's own amount
|
||
choices: vec![
|
||
QuestItemView {
|
||
item_id: 0,
|
||
name: Some("Worn Sword".into()),
|
||
texture: None,
|
||
count: 1,
|
||
quality: 1,
|
||
usable: true,
|
||
..Default::default()
|
||
},
|
||
QuestItemView {
|
||
item_id: 0,
|
||
name: Some("Worn Mace".into()),
|
||
texture: None,
|
||
count: 1,
|
||
quality: 1,
|
||
usable: true,
|
||
..Default::default()
|
||
},
|
||
],
|
||
rewards: vec![QuestItemView {
|
||
item_id: 0,
|
||
name: Some("Militia Hammer".into()),
|
||
texture: None,
|
||
count: 1,
|
||
quality: 1,
|
||
usable: true,
|
||
..Default::default()
|
||
}],
|
||
});
|
||
s.set_quest_log(state);
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
// Both choice rows show, the rest of the 6-row cap stays hidden.
|
||
// 2 choices then 1 fixed reward, in ONE pool: Item1/Item2 are the choices, Item3 the reward,
|
||
// Item4 the first unused slot.
|
||
assert!(s.eval::<bool>("return QuestLogItem1:IsShown()").unwrap());
|
||
assert!(s.eval::<bool>("return QuestLogItem2:IsShown()").unwrap());
|
||
assert!(!s.eval::<bool>("return QuestLogItem4:IsShown()").unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItem1Name:GetText()")
|
||
.unwrap(),
|
||
"Worn Sword"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItem2Name:GetText()")
|
||
.unwrap(),
|
||
"Worn Mace"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItemChooseText:GetText()")
|
||
.unwrap(),
|
||
"You may choose one of these rewards:"
|
||
);
|
||
|
||
// With choices present, the receive text reads "...also..." (QuestFrame.lua:461-462) — it
|
||
// anchors under choice row 1 (the left/odd column: index=2 is even -> anchorIndex-1 -> 1, this
|
||
// function's own logic). GetPoint() isn't wired for FontString regions in this engine (only
|
||
// Frame/Button — `region.rs` has no GetPoint), so the anchor CHAIN itself is verified via the
|
||
// mandatory capture-loop screenshot (this task's report), not introspected here.
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItemReceiveText:GetText()")
|
||
.unwrap(),
|
||
"You will also receive:"
|
||
);
|
||
|
||
// One fixed reward shows, chained under the receive text (its own SetPoint target).
|
||
assert!(s.eval::<bool>("return QuestLogItem3:IsShown()").unwrap());
|
||
assert!(!s.eval::<bool>("return QuestLogItem4:IsShown()").unwrap());
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogItem3Name:GetText()")
|
||
.unwrap(),
|
||
"Militia Hammer"
|
||
);
|
||
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogRewardTitleText:GetText()")
|
||
.unwrap(),
|
||
"Rewards"
|
||
);
|
||
}
|
||
|
||
/// One entry whose selected detail overflows the 261px-tall `QuestLogDetailScrollFrame`: 10
|
||
/// objective lines (each a FIXED 12px slot, `QuestLogObjective1`'s own XML comment) plus a
|
||
/// long description and a reward row push the child's summed height (`BenillaQuestLogDetail_
|
||
/// ResizeChild`) well past the pane — load-bearing for the scroll/clip tests below even
|
||
/// engine-only (no font atlas, so every auto-height FontString measures 0 here — this fixture
|
||
/// overflows on the FIXED-height portions alone: 10×12px objective rows + the reward row).
|
||
fn overflowing_entry() -> QuestLogState {
|
||
let objectives = (1..=10)
|
||
.map(|i| QuestLogObjectiveView {
|
||
text: format!("Objective {i} of 10 slain: 0/5"),
|
||
kind: "monster".into(),
|
||
finished: false,
|
||
cur: 0,
|
||
req: 5,
|
||
})
|
||
.collect();
|
||
QuestLogState {
|
||
num_quests: 1,
|
||
entries: vec![QuestLogEntryView {
|
||
quest_id: 1,
|
||
title: "A Very Long Quest".into(),
|
||
level: 5,
|
||
complete: 0,
|
||
objectives,
|
||
..Default::default()
|
||
}],
|
||
detail: Some(QuestLogDetail {
|
||
description: "A very long description. ".repeat(20),
|
||
objectives_text: "Report back once every objective below is complete.".into(),
|
||
required_money: 0,
|
||
reward_money: 0,
|
||
choices: vec![],
|
||
rewards: vec![QuestItemView {
|
||
item_id: 0,
|
||
name: Some("Militia Hammer".into()),
|
||
texture: None,
|
||
count: 1,
|
||
quality: 1,
|
||
usable: true,
|
||
..Default::default()
|
||
}],
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// The ScrollFrame clip (decision 0112 §4): every quad in the moved detail chain's subtree —
|
||
/// `QuestLogDetailScrollChildFrame` and its descendants (here, a reward row's icon-slot texture) —
|
||
/// carries `QuestLogDetailScrollFrame`'s own resolved rect as its clip, so content that overflows
|
||
/// the pane never draws past its bottom edge; a sibling entirely outside the scroll child (the
|
||
/// window's own book-icon chrome) stays unclipped, mirroring `scrollframe.rs`'s own unit test for
|
||
/// the mechanism, just through the shipped window instead of a synthetic fixture.
|
||
#[test]
|
||
fn overflowing_detail_content_clips_to_the_scrollframe_rect() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(overflowing_entry());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
s.resolve();
|
||
let quads = s.extract();
|
||
let scroll_rect = frame_rect(&quads, 300.0, 261.0);
|
||
|
||
let reward_plate_clips: Vec<Option<benilla_ui::layout::Rect>> = quads
|
||
.iter()
|
||
.filter_map(|q| match &q.content {
|
||
QuadContent::Texture { path: Some(p), .. } if p.contains("UI-QuestItemNameFrame") => {
|
||
Some(q.clip)
|
||
}
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
assert!(
|
||
!reward_plate_clips.is_empty(),
|
||
"expected at least one reward-row name-plate quad in the fixture"
|
||
);
|
||
for clip in reward_plate_clips {
|
||
assert_eq!(
|
||
clip,
|
||
Some(scroll_rect),
|
||
"a reward row (inside the scroll child's subtree) clips to the ScrollFrame's own rect"
|
||
);
|
||
}
|
||
|
||
let book_icon_clip = quads.iter().find_map(|q| match &q.content {
|
||
QuadContent::Texture { path: Some(p), .. } if p.contains("UI-QuestLog-BookIcon") => {
|
||
Some(q.clip)
|
||
}
|
||
_ => None,
|
||
});
|
||
assert_eq!(
|
||
book_icon_clip,
|
||
Some(None),
|
||
"the window's own chrome sits outside the scroll child and is unclipped"
|
||
);
|
||
}
|
||
|
||
/// Wheeling over the detail pane changes `GetVerticalScroll()` — the ScrollFrame is
|
||
/// mouse-wheel-interactive by construction (decision 0112, no wheel-catcher needed), and is
|
||
/// declared after `QuestLogWheelCatcher` in this window's `<Frames>` so it out-ranks the
|
||
/// catcher within its own rect (this window's own XML comment on hit-test z-order).
|
||
#[test]
|
||
fn wheel_over_the_detail_pane_changes_vertical_scroll() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(overflowing_entry());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
s.resolve();
|
||
let quads = s.extract();
|
||
let scroll_rect = frame_rect(&quads, 300.0, 261.0);
|
||
// Near the pane's own top-left, clear of the reward row (which sits further down the chain).
|
||
let (x, y) = (scroll_rect.left + 10.0, scroll_rect.top - 10.0);
|
||
|
||
assert_eq!(
|
||
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
|
||
.unwrap(),
|
||
0.0
|
||
);
|
||
s.mouse_wheel(x, y, -1.0); // WoW convention: -1 = down
|
||
assert!(s.errors().is_empty(), "wheel errors: {:?}", s.errors());
|
||
let after = s
|
||
.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
|
||
.unwrap();
|
||
assert!(
|
||
after > 0.0,
|
||
"wheel-down over the detail pane increased its scroll offset, got {after}"
|
||
);
|
||
}
|
||
|
||
/// Pin §3/§5's doNotScroll asymmetry (QUESTLOG-PIN.md, `QuestLogFrame.lua:348/455-457`): a manual
|
||
/// reselect always snaps the detail pane's scroll back to the top, but the `QUEST_LOG_UPDATE`
|
||
/// data-refresh path must NOT — a quest updating while already being read shouldn't yank the
|
||
/// reader's scroll position out from under them.
|
||
#[test]
|
||
fn selection_change_resets_detail_scroll_but_a_quest_log_update_refresh_does_not() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(overflowing_entry());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
s.resolve(); // GetVerticalScrollRange (SetVerticalScroll's clamp) reads resolved rects.
|
||
|
||
s.run("QuestLogDetailScrollFrame:SetVerticalScroll(10)")
|
||
.unwrap();
|
||
assert_eq!(
|
||
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
|
||
.unwrap(),
|
||
10.0
|
||
);
|
||
|
||
// A data refresh (fired the same way the app's own quest-log feed does) must not reset it.
|
||
s.fire_event("QUEST_LOG_UPDATE", vec![]);
|
||
assert!(s.errors().is_empty(), "refresh errors: {:?}", s.errors());
|
||
assert_eq!(
|
||
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
|
||
.unwrap(),
|
||
10.0,
|
||
"QUEST_LOG_UPDATE's data-refresh path must not yank the scroll position (pin §5)"
|
||
);
|
||
|
||
// A manual reselect (the row-click path — no doNotScroll) DOES reset it.
|
||
s.run(r#"BenillaQuestLogTitle_OnClick(QuestLogTitle1, "LeftButton")"#)
|
||
.unwrap();
|
||
assert!(s.errors().is_empty(), "click errors: {:?}", s.errors());
|
||
assert_eq!(
|
||
s.eval::<f32>("return QuestLogDetailScrollFrame:GetVerticalScroll()")
|
||
.unwrap(),
|
||
0.0,
|
||
"a manual reselect snaps the detail pane's scroll back to the top"
|
||
);
|
||
}
|
||
|
||
/// The SHARED item tooltip on a quest reward row (the director's "same tooltip as vendor items"):
|
||
/// hovering the reward fires the row's OnEnter → `SetItemById` → the ask-once store. First hover
|
||
/// (store cold) shows the fallback name line AND records the ask; after the app's push, a re-hover
|
||
/// renders the full stat head — the identical lines a vendor row/bag slot gets for this item.
|
||
#[test]
|
||
fn reward_row_hover_serves_the_shared_item_tooltip() {
|
||
use benilla_ui::script::ItemTemplateView;
|
||
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
// This used to load LootFrame.xml for `BENILLA_LOOT_QUALITY_COLORS`, an alias our own copy of
|
||
// that window set to `ITEM_QUALITY_COLORS`. 1751 retired the window and the alias with it, so
|
||
// Fonts.xml alone is the source now. Formerly: it
|
||
// needs the dropdown kit its GroupLootDropDown initializes against (benilla.toc l.64 vs 383).
|
||
load_xml(&s, "UIDropDownMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(eight_entries());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
s.resolve();
|
||
|
||
let icon_rect = s
|
||
.extract()
|
||
.iter()
|
||
.find(|q| {
|
||
matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p.contains("INV_Hammer_15"))
|
||
})
|
||
.and_then(|q| q.rect)
|
||
.expect("the reward row icon");
|
||
// Extracted rects and mouse_move share the same y-up UI space (the merchant hover test's
|
||
// own convention).
|
||
let (cx, cy) = (
|
||
(icon_rect.left + icon_rect.right) * 0.5,
|
||
(icon_rect.bottom + icon_rect.top) * 0.5,
|
||
);
|
||
let has_text = |s: &mut UiScript, t: &str| {
|
||
s.resolve();
|
||
s.extract()
|
||
.iter()
|
||
.any(|q| matches!(&q.content, QuadContent::Text { text: Some(x), .. } if x == t))
|
||
};
|
||
|
||
// Cold store: the hover shows the tooltip (fallback name only) and records the ask.
|
||
s.mouse_move(cx, cy);
|
||
assert!(s.errors().is_empty(), "hover errors: {:?}", s.errors());
|
||
assert!(
|
||
s.eval::<bool>("return GameTooltip:IsVisible()").unwrap(),
|
||
"tooltip shows on hover"
|
||
);
|
||
assert_eq!(
|
||
s.take_item_stat_asks(),
|
||
vec![2024],
|
||
"the miss recorded the ask"
|
||
);
|
||
|
||
// The app answers; a re-hover renders the full stat head.
|
||
s.set_item_template(
|
||
2024,
|
||
ItemTemplateView {
|
||
name: "Militia Hammer".into(),
|
||
quality: 1,
|
||
inventory_type: 21, // Main Hand
|
||
class: 2,
|
||
subclass: 4, // mace
|
||
damages: vec![(4.0, 9.0, 0)],
|
||
delay_ms: 2200,
|
||
..Default::default()
|
||
},
|
||
);
|
||
s.mouse_move(0.0, 0.0);
|
||
s.mouse_move(cx, cy);
|
||
assert!(s.errors().is_empty(), "re-hover errors: {:?}", s.errors());
|
||
assert!(
|
||
has_text(&mut s, "Main Hand"),
|
||
"slot line from the shared store"
|
||
);
|
||
assert!(
|
||
has_text(&mut s, "4 - 9 Damage"),
|
||
"damage line from the shared store"
|
||
);
|
||
}
|
||
|
||
/// A DIALOG-strata popup's CHILDREN inherit the stratum (and parent level + 1) at creation —
|
||
/// the client's CreateFrame law. Regression: the popup's translucent backdrop used to draw OVER
|
||
/// its own Yes/No buttons (children stuck in MEDIUM while the popup sat in DIALOG — the abandon
|
||
/// dialog's dark buttons, pixel-diffed against the raw UI-DialogBox-Button-Up art).
|
||
#[test]
|
||
fn popup_children_inherit_the_dialog_stratum() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
s.run(
|
||
"StaticPopupDialogs[\"TEST_STRATUM\"] = { text = \"Abandon?\", button1 = \"Yes\", \
|
||
button2 = \"No\", timeout = 0 }\n\
|
||
StaticPopup_Show(\"TEST_STRATUM\")",
|
||
)
|
||
.unwrap();
|
||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||
s.resolve();
|
||
let quads = s.extract();
|
||
let backdrop_z = quads
|
||
.iter()
|
||
.find_map(|q| match &q.content {
|
||
QuadContent::Backdrop { path, .. } if path.contains("UI-DialogBox-Background") => {
|
||
Some(q.z)
|
||
}
|
||
_ => None,
|
||
})
|
||
.expect("the popup backdrop");
|
||
let button_z = quads
|
||
.iter()
|
||
.find_map(|q| match &q.content {
|
||
QuadContent::Texture { path: Some(p), .. } if p.contains("UI-DialogBox-Button-Up") => {
|
||
Some(q.z)
|
||
}
|
||
_ => None,
|
||
})
|
||
.expect("a popup button face");
|
||
assert!(
|
||
button_z > backdrop_z,
|
||
"the buttons draw ABOVE their dialog's backdrop (button z {button_z:x} vs backdrop z {backdrop_z:x})"
|
||
);
|
||
}
|
||
|
||
/// The quest log's reward rows carry the same modifier fork as the questgiver's (decisions
|
||
/// 1059/1060) — ref `QuestLogRewardItem_OnClick` (QuestLogFrame.lua:542-552), the OnClick the ref's
|
||
/// own `QuestLogRewardItemTemplate` adds (ref QuestLogFrame.xml:118-120). There is no third arm
|
||
/// here: a quest-log reward row is never selectable, so a plain click stays inert — which is the
|
||
/// regression this pins, since the row had NO OnClick at all before this arc.
|
||
///
|
||
/// Ordering note: the dressing room docks the same left UIPanel slot as the log window, so the ctrl
|
||
/// arm runs last — opening the room closes the log, exactly as it does in play.
|
||
#[test]
|
||
fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
|
||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||
|
||
s.set_quest_log(eight_entries());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
assert!(
|
||
s.eval::<bool>("return QuestLogItem1:IsShown()").unwrap(),
|
||
"the fixture's one fixed reward row is laid out"
|
||
);
|
||
let _ = s.take_dressup_intents();
|
||
|
||
// A PLAIN click is inert: no room, no chat text, no error (the row has no select arm).
|
||
s.run("QuestLogItem1:Click()").unwrap();
|
||
assert!(
|
||
s.take_dressup_intents().is_empty(),
|
||
"a plain click never opens the dressing room"
|
||
);
|
||
assert_eq!(
|
||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||
.unwrap(),
|
||
"",
|
||
"a plain click posts nothing"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
// SHIFT + chat open → the reward's full escaped link (ref l.547-550).
|
||
assert!(s.focus_editbox("ChatFrameEditBox"));
|
||
s.set_modifiers(true, false, false);
|
||
s.run("QuestLogItem1:Click()").unwrap();
|
||
s.set_modifiers(false, false, false);
|
||
assert_eq!(
|
||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||
.unwrap(),
|
||
HAMMER_LINK,
|
||
"shift-click posted the reward's link"
|
||
);
|
||
assert!(
|
||
s.take_dressup_intents().is_empty(),
|
||
"the shift arm never opens the room"
|
||
);
|
||
|
||
// CTRL → the room wearing the reward (ref l.543-546): re-dress first, then try it on.
|
||
s.set_modifiers(false, true, false);
|
||
s.run("QuestLogItem1:Click()").unwrap();
|
||
s.set_modifiers(false, false, false);
|
||
assert_eq!(
|
||
s.take_dressup_intents(),
|
||
vec![
|
||
benilla_ui::script::DressUpIntent::Dress,
|
||
benilla_ui::script::DressUpIntent::TryOn(2024)
|
||
],
|
||
"ctrl-click opened the room wearing the reward"
|
||
);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// A quest log TITLE row is a quest NAME, not an item — ref `QuestLogTitleButton_OnClick`
|
||
/// (QuestLogFrame.lua:469-505): with the chat edit box up, shift-click inserts the row's own text
|
||
/// with its leading indent trimmed (l.478-480, plain text — 1.12 has no `|Hquest:` link); the
|
||
/// quest-watch toggle is that branch's `else` (l.481-500). Both halves in one test because it is the
|
||
/// *fork* that matters: watching is the behaviour that already shipped, and the insert must not have
|
||
/// eaten it. The select-and-repaint tail (l.503-504) runs either way.
|
||
#[test]
|
||
fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_it_closed() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||
|
||
s.set_quest_log(eight_entries());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
// Row 1's centre — the same seat `shift_click_toggles_the_watch_checkbox_and_the_tracker_hud`
|
||
// computes (list TOPLEFT+(19,-75), 300×16).
|
||
s.resolve();
|
||
let win = frame_rect(&s.extract(), 384.0, 512.0);
|
||
let (rx, ry) = (win.left + 19.0 + 150.0, win.top - 75.0 - 8.0);
|
||
let shift_click = |s: &mut UiScript| {
|
||
s.set_modifiers(true, false, false);
|
||
s.mouse_button(rx, ry, "LeftButton", true);
|
||
s.mouse_button(rx, ry, "LeftButton", false);
|
||
s.set_modifiers(false, false, false);
|
||
};
|
||
|
||
// Chat CLOSED → the watch toggle, exactly as before.
|
||
shift_click(&mut s);
|
||
assert!(
|
||
s.eval::<bool>("return IsQuestWatched(1)").unwrap(),
|
||
"with chat closed, shift-click still toggles the quest watch"
|
||
);
|
||
shift_click(&mut s); // and back off, so the assertion below is unambiguous
|
||
assert!(!s.eval::<bool>("return IsQuestWatched(1)").unwrap());
|
||
|
||
// Chat OPEN → the bare quest name, and NO watch.
|
||
assert!(s.focus_editbox("ChatFrameEditBox"));
|
||
shift_click(&mut s);
|
||
assert_eq!(
|
||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||
.unwrap(),
|
||
"Quest 1",
|
||
"the trimmed quest name landed in the chat box (ref's gsub result, no row indent)"
|
||
);
|
||
assert!(
|
||
!s.eval::<bool>("return IsQuestWatched(1)").unwrap(),
|
||
"with chat open the watch must NOT toggle — the ref makes watching the else"
|
||
);
|
||
// The tail runs either way (ref l.503-504): the click still selects the row.
|
||
assert_eq!(s.eval::<i64>("return GetQuestLogSelection()").unwrap(), 1);
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
// ── The Share Quest button (decision 1733) ───────────────────────────────────────────────────────
|
||
|
||
/// A party of `n` others; `GetNumPartyMembers` is that list's length.
|
||
fn party(n: usize) -> PartyState {
|
||
PartyState {
|
||
members: (0..n)
|
||
.map(|i| PartyMemberInfo {
|
||
name: format!("Mate{i}"),
|
||
guid: 0x300 + i as u64,
|
||
})
|
||
.collect(),
|
||
..PartyState::default()
|
||
}
|
||
}
|
||
|
||
/// The 8-entry fixture with `pushable` set on the entries named by `ids` — the app sets it from
|
||
/// the cached template's `QUEST_FLAGS_SHARABLE` bit, so a fixture is how the button's predicate is
|
||
/// exercised without a wire.
|
||
fn entries_sharable(ids: &[u32]) -> QuestLogState {
|
||
let mut state = eight_entries();
|
||
for e in &mut state.entries {
|
||
e.pushable = ids.contains(&e.quest_id);
|
||
}
|
||
state
|
||
}
|
||
|
||
/// The button's predicate is a CONJUNCTION, and each half is tested by moving only itself: a
|
||
/// sharable quest with no party is dark, a party with an unsharable selection is dark, and only
|
||
/// both together light it (ref `QuestLogFrame.lua:299-305`).
|
||
#[test]
|
||
fn share_quest_needs_both_a_sharable_selection_and_a_party() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
// Solo, quest 1 sharable and selected by the auto-selection.
|
||
s.set_quest_log(entries_sharable(&[1]));
|
||
s.set_party(PartyState::default());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
assert_eq!(s.eval::<i64>("return GetQuestLogSelection()").unwrap(), 1);
|
||
// `1`/nil, never true/false (decision 1738) — compared the way the reference's own predicate
|
||
// does, which is why the button below reads the same either way.
|
||
assert!(
|
||
s.eval::<bool>("return GetQuestLogPushable() ~= nil")
|
||
.unwrap(),
|
||
"quest 1 carries the sharable bit"
|
||
);
|
||
assert!(
|
||
!s.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
|
||
.unwrap(),
|
||
"solo: sharable is not enough"
|
||
);
|
||
|
||
// Join a party — the event alone must light it, with no quest-log change at all.
|
||
s.set_party(party(2));
|
||
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
|
||
assert!(
|
||
s.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
|
||
.unwrap(),
|
||
"PARTY_MEMBERS_CHANGED alone re-tests the button"
|
||
);
|
||
|
||
// Select an unsharable quest: party unchanged, button goes dark.
|
||
s.run("SelectQuestLogEntry(2)").unwrap();
|
||
s.run("BenillaQuestLogFrame_Update()").unwrap();
|
||
assert!(
|
||
s.eval::<bool>("return GetQuestLogPushable() == nil")
|
||
.unwrap(),
|
||
"quest 2 has no sharable bit"
|
||
);
|
||
assert!(
|
||
!s.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
|
||
.unwrap(),
|
||
"in a party: an unsharable selection is still dark"
|
||
);
|
||
|
||
// Leaving the party darkens it again from the other side.
|
||
s.run("SelectQuestLogEntry(1)").unwrap();
|
||
s.run("BenillaQuestLogFrame_Update()").unwrap();
|
||
assert!(s
|
||
.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
|
||
.unwrap());
|
||
s.set_party(PartyState::default());
|
||
s.fire_event("PARTY_MEMBERS_CHANGED", vec![]);
|
||
assert!(!s
|
||
.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
|
||
.unwrap());
|
||
}
|
||
|
||
/// An EMPTY log disables the button outright, ahead of both other conjuncts — the ref's own first
|
||
/// guard, and the one that matters because `GetQuestLogPushable()` on no selection is false anyway
|
||
/// but `GetNumPartyMembers() > 0` is not.
|
||
#[test]
|
||
fn share_quest_is_dark_on_an_empty_log_even_in_a_party() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(QuestLogState::default());
|
||
s.set_party(party(4));
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(!s
|
||
.eval::<bool>("return QuestFramePushQuestButton:IsEnabled() ~= 0")
|
||
.unwrap());
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|
||
|
||
/// The click queues the SELECTION's quest id — not its list index, and not the row under the
|
||
/// mouse. The fixture's ids are deliberately not equal to their 1-based positions after a
|
||
/// selection move, so an index/id confusion shows up here.
|
||
#[test]
|
||
fn share_quest_click_queues_the_selected_quests_id() {
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// ScrollTemplates.xml + UIPanelTemplates.xml are NOT optional: the detail pane inherits
|
||
// UIPanelScrollFrameTemplate, and a MISSING template is a loader *warning*, not an error —
|
||
// an under-loaded list passes load_xml and then loses the wheel, the arrows and the bar
|
||
// silently.
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "UIPanelTemplates.xml");
|
||
load_xml(&s, "QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(entries_sharable(&[1, 5]));
|
||
s.set_party(party(1));
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
|
||
s.run("SelectQuestLogEntry(5)").unwrap();
|
||
s.run("BenillaQuestLogFrame_Update()").unwrap();
|
||
s.run("QuestFramePushQuestButton:Click()").unwrap();
|
||
assert_eq!(s.take_quest_log_pushes(), vec![5]);
|
||
assert!(s.take_quest_log_pushes().is_empty(), "drained");
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
}
|