- the chat window is the reference's own `ChatFrame.xml` and `FloatingChatFrame.xml`, run off the player's chain; our 2216-line transcription is deleted, the feed fires `CHAT_MSG_*` and the reference's `ChatFrame_OnEvent` prints — no app-side composition into the windows, no routing table, no whisper chime or flash of ours, and the transcribed edit-box machine (sticky type, tell ring, Tab cycle) retires with it; the verbs the two files call are built to the bytes — the whole `0x98` chat-window record and its eight verbs, the 94-entry chat-type colour registry, the language walk, the channel verbs as a command queue, the slash handlers' emote/roll/assist/uninvite/console/logging verbs; `chat-cache.txt` is written and read in the reference's whole grammar, the 68-entry message-group table in flag order and the no-file boot included, and the chat bindings and macro lines run through the reference's own arms (1948) - the spellbook is the reference's own `SpellBookFrame.xml`; our 965-line transcription is deleted, and two quirks it reproduced by hand (the pet page ignores its page number, its arrows write the spell book's counter) now come from the reference's lines; one adapter remains because the reference's autocast shine is a `<Model>` that draws no pixels in this engine — the script puts the token-texture shine beside each button's model and carries it with the model's shown state; the nine spell-slot bindings (`GetSpellName`, `CastSpell`, `PickupSpell` and their kin) now parse their arguments the way the reference's shared marshaller `0x4b3ec0` does — a numeric arg 1, a mandatory arg 2, the index gated to [0, 0x400) with "Invalid spell slot in <Verb>" on a miss, "pet" matched without case — where ours raised mlua's own message on a negative id, answered nil for 0 and above 1024, and read the wrong book for "PET" (1952) - a long-hidden world model part parks its `Mesh3d`: the entity path keeps the batches the retained pass declines resident — 5.3k hidden submeshes at any instant at the Stormwind auction house — and Bevy's sweeps walk every `Mesh3d` whether it draws or not, so a part parks after 30 consecutive hidden frames and unparks the frame its verdict turns visible; every world model part takes part, units' bodies included, while a booth's mirror parts stay out; interleaved pairs read an alone Stormwind 0.1–0.4 ms lighter, and `WOW_NO_MESH_PARK=1` is the A/B; the audio thread's realtime class was the standing suspect for the raid's `cpu_p99` spikes with sound on, and `WOW_AUDIO_NO_RT=1` across two interleaved Blackrock pairs read no difference, so the lever stays as an instrument (1949); and a no-merge prop batch now says which rule declined it — animated (1551 at the auction house) or transparent, not order-free (340) (1951) - the probe reads CPU per thread across its window (user and system split, because a user-mode sampler never sees kernel time) and the main thread's CPU inside the app's frame beside its whole-frame CPU; together they place the vsync tax every player pays by default — cpu_ms 10.7 uncapped against 12.5 under vsync, alone in Stormwind, with no system growing and no thread spinning: it lands on the main thread, in user time, inside the schedules, the same systems each running slower, and a low-priority spinner holding one core busy erases it to the last decimal (vsync + spinner 10.18 against uncapped 10.17), which is frequency scaling — under vsync the cores idle deeper between frames and the next burst starts at a lower clock; nothing in the client is the cause and nothing in the client changes for it (1950) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1752 lines
79 KiB
Rust
1752 lines
79 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()),
|
||
}],
|
||
reward_spell: None,
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// 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 _data = benilla_formats::wow_data_or_skip!();
|
||
let s = UiScript::new().unwrap();
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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 QuestLogTitle1NormalText: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:"
|
||
);
|
||
// …hidden, not blanked: stock QuestFrameItems_Update Show()s/Hide()s the choose text
|
||
// (QuestFrame.lua:454-473) and leaves its string alone.
|
||
assert!(
|
||
!s.eval::<bool>("return QuestLogItemChooseText:IsShown()")
|
||
.unwrap(),
|
||
"no choices: the choose text is hidden"
|
||
);
|
||
// 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");
|
||
let before = s
|
||
.eval::<String>("return QuestLogTitle1NormalText:GetText()")
|
||
.unwrap();
|
||
s.mouse_wheel(wx, wy, -1.0);
|
||
assert!(s.errors().is_empty(), "wheel errors: {:?}", s.errors());
|
||
// **Not "by one row" any more.** That was our own `ScrollFrameTemplate_OnMouseWheel`, which
|
||
// stepped one `valueStep`. The reference's moves the bar by **half its own height** in value
|
||
// units (`UIPanelTemplates.lua`), which over a 16px row and a 237px bar is roughly seven rows.
|
||
// The migration reverts our step; how far a notch should travel is a look call, recorded in
|
||
// 1846 rather than re-authored back in.
|
||
assert_ne!(
|
||
s.eval::<String>("return QuestLogTitle1NormalText:GetText()")
|
||
.unwrap(),
|
||
before,
|
||
"wheel-down scrolled the list"
|
||
);
|
||
// 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("QuestLogFrameAbandonButton:Click()").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("QuestLogFrameAbandonButton:Click()").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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
// Stock seats the watch check at update time off `questNormalText:GetWidth()` (QuestLogFrame.lua:205-215),
|
||
// synchronously — the app's measure seam is synchronous too (measure.rs); its stand-in here.
|
||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
|
||
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(eight_entries());
|
||
// The app fires QUEST_LOG_UPDATE when the log lands; stock QuestLog_OnEvent's arm runs the
|
||
// first QuestWatch_Update, which is what hides an empty tracker.
|
||
s.fire_event("QUEST_LOG_UPDATE", vec![]);
|
||
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: stock's shift-click inserts it with QUEST_WATCH_NO_EXPIRE
|
||
// (QuestLogFrame.lua:496), the timer that AutoQuestWatch_OnUpdate never counts down.
|
||
assert!(s
|
||
.eval::<bool>(
|
||
"return QUEST_WATCH_LIST[1] ~= nil and QUEST_WATCH_LIST[1].timer == QUEST_WATCH_NO_EXPIRE"
|
||
)
|
||
.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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
|
||
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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(),
|
||
1,
|
||
"a shift-click never selects — stock QuestLogTitleButton_OnClick's shift branch returns \
|
||
before the plain-click arm's QuestLog_SetSelection (QuestLogFrame.lua:472-500)"
|
||
);
|
||
|
||
// 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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
// AUTO_QUEST_WATCH is the options window's (UIOptionsFrame_Init in the reference; 1944), so the
|
||
// auto-watch needs that window loaded, as the manifest has it long before the log.
|
||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||
load_xml(&s, "KeyBindingsPage.xml");
|
||
load_xml(&s, "OptionsFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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(
|
||
"QUEST_WATCH_UPDATE",
|
||
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 QUEST_WATCH_LIST[1] ~= nil and QUEST_WATCH_LIST[1].index == 1 \
|
||
and QUEST_WATCH_LIST[1].timer == MAX_QUEST_WATCH_TIMER"
|
||
)
|
||
.unwrap(),
|
||
"the reference's QUEST_WATCH_LIST carries the armed timer (QuestLogFrame.lua:752-769)"
|
||
);
|
||
|
||
// 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(
|
||
"QUEST_WATCH_UPDATE",
|
||
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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
// The flag is declared by the options window's init since 1944, as the reference declares it
|
||
// in UIOptionsFrame_Init — so the window that owns it loads first, as in the manifest.
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "KeyBindingsPage.xml");
|
||
load_xml(&s, "OptionsFrame.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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(
|
||
"QUEST_WATCH_UPDATE",
|
||
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(
|
||
"QUEST_WATCH_UPDATE",
|
||
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`.
|
||
/// The quest TAG on the list row (`GetQuestLogTitle`'s third return): it paints on its own
|
||
/// right-flush `$parentTag` string, wrapped in the ref's parentheses, and a finished quest's
|
||
/// Failed/Complete word OVERWRITES it — the ref's own precedence (`QuestLogFrame.lua:187-195`),
|
||
/// which is why one row never shows both. Headers carry no tag. The title keeps the bare quest
|
||
/// name: the state word is NOT appended to it any more.
|
||
#[test]
|
||
fn the_row_tag_is_its_own_right_flush_string_and_the_state_word_wins() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
// The state word comes from the reference's OWN globals (`COMPLETE`/`FAILED`) as of this row's
|
||
// ref-verbatim tag handling — the app loads GlobalStrings.lua as `benilla.toc`'s second entry,
|
||
// so this harness must too or the override silently reads nil.
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
load_xml(&s, "ScrollTemplates.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
||
|
||
let mut state = eight_entries();
|
||
// Row 1: a plain elite quest. Row 2: an elite quest that is COMPLETE — the state word wins.
|
||
// Row 3: no tag at all, in progress. Row 4: a header, which can carry neither.
|
||
state.entries[0].tag = Some("Elite".into());
|
||
state.entries[1].tag = Some("Raid".into());
|
||
state.entries[1].complete = 1;
|
||
state.entries[2].complete = -1;
|
||
state.entries[3].is_header = true;
|
||
state.entries[3].title = "Elwynn Forest".into();
|
||
state.entries[3].tag = None;
|
||
s.set_quest_log(state);
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
|
||
let tag = |s: &mut UiScript, i: u32| {
|
||
s.eval::<String>(&format!("return QuestLogTitle{i}Tag:GetText()"))
|
||
.unwrap()
|
||
};
|
||
assert_eq!(
|
||
tag(&mut s, 1),
|
||
"(Elite)",
|
||
"the engine's tag, in parentheses"
|
||
);
|
||
assert_eq!(
|
||
tag(&mut s, 2),
|
||
"(Complete)",
|
||
"COMPLETE overwrites the quest's own tag — ref l.190-192"
|
||
);
|
||
assert_eq!(tag(&mut s, 3), "(Failed)", "FAILED, same override");
|
||
assert_eq!(tag(&mut s, 4), "", "a header carries no tag");
|
||
|
||
// The title is the bare (indented) name — the state word lives on the tag string now, so
|
||
// appending it here too would double it.
|
||
let title1 = s
|
||
.eval::<String>("return QuestLogTitle1NormalText:GetText()")
|
||
.unwrap();
|
||
assert_eq!(title1, " Quest 1");
|
||
let title2 = s
|
||
.eval::<String>("return QuestLogTitle2NormalText:GetText()")
|
||
.unwrap();
|
||
assert_eq!(title2, " Quest 2", "no \"(Complete)\" on the title");
|
||
|
||
// Right-flush at the row's right edge less 2 — the ref's own anchor (its
|
||
// QuestLogFrame.xml:10-21).
|
||
let row_right = s.eval::<f32>("return QuestLogTitle1:GetRight()").unwrap();
|
||
let tag_right = s
|
||
.eval::<f32>("return QuestLogTitle1Tag:GetRight()")
|
||
.unwrap();
|
||
assert!(
|
||
(row_right - tag_right - 2.0).abs() < 0.5,
|
||
"tag right-flush at row right -2 (row {row_right}, tag {tag_right})"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn empty_quest_log_hides_rows_and_disables_abandon() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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.
|
||
// The empty log says the reference's QUESTLOG_NO_QUESTS_TEXT (GlobalStrings.lua:3225) through
|
||
// stock QuestLogNoQuestsText; "Your quest log is empty." was ours (1944).
|
||
assert_eq!(
|
||
s.eval::<String>("return QuestLogNoQuestsText:GetText()")
|
||
.unwrap(),
|
||
"No Active Quests"
|
||
);
|
||
assert!(!s.eval::<bool>("return QuestLogTitle1:IsVisible()").unwrap());
|
||
assert!(!s
|
||
.eval::<bool>("return QuestLogFrameAbandonButton:IsEnabled() ~= 0")
|
||
.unwrap());
|
||
// With no selection the reference hides the whole detail pane (stock
|
||
// `QuestLog_UpdateQuestDetails`: `QuestLogDetailScrollFrame:Hide()`), header included — it
|
||
// keeps its static `text="QUEST_DESCRIPTION"`; ours used to blank the header instead (1944).
|
||
assert!(
|
||
!s.eval::<bool>("return QuestLogDetailScrollFrame:IsVisible()")
|
||
.unwrap(),
|
||
"no selection: the detail pane is hidden, header and all"
|
||
);
|
||
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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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()
|
||
}],
|
||
reward_spell: None,
|
||
});
|
||
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 will be able to 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).
|
||
/// The measure round-trip the app's resolve pass drives with its font engine: answer every
|
||
/// FontString the layout asked about with the 6px-per-glyph, 12px-per-line stand-in the
|
||
/// synchronous `FixedWidthFont(6.0)` uses, so the two agree.
|
||
fn answer_measures(s: &mut UiScript) {
|
||
let answers: Vec<(u32, f32, f32, u64)> = s
|
||
.fontstrings_needing_measure()
|
||
.into_iter()
|
||
.map(|r| {
|
||
let ink = r.text.chars().count() as f32 * 6.0;
|
||
match r.wrap_width {
|
||
Some(w) if w > 0.0 && ink > w => (r.id, w, (ink / w).ceil() * 12.0, r.key),
|
||
_ => (r.id, ink, 12.0, r.key),
|
||
}
|
||
})
|
||
.collect();
|
||
s.set_measured_text_unwrapped(&answers);
|
||
}
|
||
|
||
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()
|
||
}],
|
||
reward_spell: None,
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// 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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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), and it is the only frame in this
|
||
/// window's bucket covering the detail rect, so the spin lands on it outright.
|
||
#[test]
|
||
fn wheel_over_the_detail_pane_changes_vertical_scroll() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(overflowing_entry());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
// The detail pane's scroll range is the union of its measured content (0128): the layout's
|
||
// measure round-trip has to be answered, as the app's resolve pass answers it every frame.
|
||
s.resolve();
|
||
answer_measures(&mut s);
|
||
s.resolve(); // the measured span reaches the scroll range, and the bar, on this pass
|
||
|
||
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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
s.set_text_measurer(Box::new(super::FixedWidthFont(6.0)));
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
||
|
||
s.set_quest_log(overflowing_entry());
|
||
s.run("ToggleQuestLog()").unwrap();
|
||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||
// The detail pane's scroll range is the union of its measured content (0128): the layout's
|
||
// measure round-trip has to be answered, as the app's resolve pass answers it every frame.
|
||
s.resolve();
|
||
answer_measures(&mut s);
|
||
s.resolve(); // the measured span reaches the scroll range, and the bar, on this pass
|
||
s.resolve(); // GetVerticalScrollRange (SetVerticalScroll's clamp) reads resolved rects.
|
||
|
||
// Scroll THROUGH the bar, as the wheel and the thumb do (ScrollFrameTemplate_OnMouseWheel sets
|
||
// the bar; its OnValueChanged moves the frame): a reselect's `ScrollBar:SetValue(0)` is what
|
||
// snaps the pane back, and a bar already at 0 would fire no change over a frame-only scroll.
|
||
s.run("QuestLogDetailScrollFrameScrollBar:SetValue(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("QuestLogTitle1:Click()").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() {
|
||
let _data = benilla_formats::wow_data_or_skip!();
|
||
use benilla_ui::script::ItemTemplateView;
|
||
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
|
||
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
|
||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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, "Interface\\FrameXML\\ChatFrame.xml"); // ChatFrameEditBox lives here
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||
|
||
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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
// A shift-click reaches stock QuestLogTitleButton_OnClick's unguarded `ChatFrameEditBox:IsVisible()`
|
||
// (QuestLogFrame.lua:478) — the chat window is on the manifest long before the log (1944).
|
||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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, "Interface\\FrameXML\\ChatFrame.xml"); // ChatFrameEditBox lives here
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||
|
||
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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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("QuestLog_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("QuestLog_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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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 _data = benilla_formats::wow_data_or_skip!();
|
||
let mut s = UiScript::new().unwrap();
|
||
s.set_screen_size(1024.0, 768.0);
|
||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||
load_xml(&s, "MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, "UiPanels.xml");
|
||
load_xml(&s, "GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, "MicroMenu.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\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("QuestLog_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());
|
||
}
|