- a size-less `<Model>` pane's rect is its file's box in layout units: the reference's geometry overrides (`0x76d080`/`0x76d0d0`) answer the bbox extent whenever no size is authored, `bboxExtent · 768·√(a²+1) · layoutScale` FrameXML units (`1280 · extent` at 4:3), written into the pane's layout input when the file's facts land, re-derived on an aspect change, and ended for good by any authored size; `SetPosition` is the same unit, which is how `0x4a7b20` centres the arrow at `½·GetWidth, ½·GetHeight` through the override — so the map arrow's sprite arm and its hand-sized 33.6 px footprint go (its rect is `0.0262 × 0.0263` layout units, 33.5 × 33.7 FrameXML units at 4:3, 41.0 × 41.2 at 16:9), and the stock `ItemAnim` pane plays the item push unadapted (`ReplaceIconTexture` on the type-14 batch, the 1000 ms clamp, the stock `OnAnimFinished`), `ContainerFrameAdapters.xml` losing 0887's Lua card and its hand-measured 42.41 × 124.72 rect — the implicit one at 16:9 — and keeping the keyring fork and the three bag verbs (2015); and the cooldown is the reference's own model: the texture-transform law `uv' = R_q((uv + t − p) ⊙ s) + p`, `p = (½, ½)`, the raw quaternion component-lerped and never normalised, is one function (`uv_transform`) with the shader's UV fold as its twin — rotation and scale tracks bake per file sequence slot beside the translation, the mat-anim table gains an affine row `[cos − 1, sin, sx − 1, sy − 1]` on `anim_slots.z` whose identity is the zero row so every static batch reads exactly what it did, and a UI tile clones the twin material per animated batch and writes both rows every frame off the pane's play head, never the world clock; the stock `Cooldown.xml` runs off the chain on 2007's two handlers, the file's four quadrant quads turned by their own rotation tracks (`0 → −90°` over each quadrant's 250 ms window, clockwise from 12 o'clock) and its alpha tracks revealing the star for the flash; retired: the native `Cooldown` widget and `SetCooldown` (an Era API 1.12 never had — `CreateFrame("Cooldown")` is the reference's unknown-type refusal now), the loader's `Model → Cooldown` substitution, the app's hand-built pie and star, and our `Cooldown.xml`; `assets/ui` is eight files (2019)
- `UpdateMapHighlight` gets its zone-level exits (B360): at zone zoom ours returned the nil tail for every cursor position, so hovering Deadwind Pass from Duskwood showed no label where 1.12 shows "Deadwind Pass"; the reference's `0x4a7fa0` runs two lookups at zone level and answers a NAME only — first the current zone's exploration-gated overlay list (the builder at `0x4a6ad9` gating on the explored byte), each row's HitRect scaled by 1/1002 and 1/668 against the normalized cursor, edges inclusive, the first hit naming its Area1, then the continent grid re-windowed through the displayed zone's own rect, naming the neighbouring zone or city and nothing for the displayed zone itself — and only the name-only tail is reachable from there, fileName nil and the six coords zero, so the stock frame hides the highlight quad, as the director's own 1.12 shot shows; and `SetVerticalScroll` stores the offset as given (B370, B363): the reference's `0x786db0` compares the new offset against the old and writes it to `[+0x328]` verbatim, `0x787100` re-anchors the scroll child off it, and the range at `[+0x320]` is never read on that path — every clamp the reference exhibits is FrameXML's, through the scroll bar's `[min, max]`; ours clamped into `[0, GetVerticalScrollRange()]`, the child's overflow past the FRAME, which is 40 px short of where `FauxScrollFrame_Update` sends the bar on the 220-tall skills list (three rows lost) and 15 px short on the 287-tall who list (one); the setter fires `OnVerticalScroll` only on a change, as the bytes do, `BenillaScroll_Step` bounds its wheel in Lua where the reference's handler does, and the ScrollFrame kind's prose stops calling its offset unpinned (2017)
- overdraw's cheap half, the sky first: since 0588 every sky element wrote `frag_depth = 0.0`, which costs a pipeline its early-Z, so on an outdoor frame every full-screen dome fragment under the ground and the buildings was shaded in full (the gradient's `asin`, `atan2` and eight table lookups at dawn and dusk) and thrown away; one shared `sky_vertex.wgsl` — bevy's mesh vertex verbatim plus `out.position.z = 0.0` — is now the vertex stage of the gradient dome, the cloud dome, the star patches and the celestial discs and glares, and the model lane's `WOW_SKY_DEPTH` branch moves from its fragment to its vertex, so no sky fragment declares `frag_depth` and the rasterizer rejects the covered screen before the shader runs; every sky `specialize` zeroes the rasterizer bias constant the `sky_order` rung had been leaking into `DepthBiasState::constant`, harmless only while the fragment overwrote its depth, and the prose beside the pin follows it; then the retained `static_gx` pass draws BEFORE bevy's opaque pass (`StartMainPass → static_gx → MainOpaquePass`) — buildings and doodads are the frame's best occluders and a terrain fragment its dearest, six samples a pixel — so walls reject the terrain behind them instead of the reverse, the pass taking the first-use clear with it and the dev depth probe ordered after it; measured on 16 capture scenarios: 14 bit-identical, `northshire-dusk` 1 px and `water-night` 11 px, attributed to exact depth ties on static/terrain silhouettes and last-bit interpolation noise; a depth prepass and near-to-far bins are declined until a journal says where the time goes (2016)
- the grass cut goes per pixel because of a quarter-mip: `0x6813d0` sets `D3DSAMP_MIPMAPLODBIAS = +0.25` on stage 0 (`0x6813f4`) once per frame before the detail-doodad pass — capture-confirmed on all 29 doodad batches, and outside the `[0x6b1000, 0x6b3200)` scan 2004 had called complete; it decides the whole look because the `clutter_cut` probe finds 6 of 31 shipped detail atlases carry a BINARY alpha pyramid (mip 0 has DXT3's 16 levels, every level below exactly two, both starting zones among them), so `a × ramp` degenerates to a global on/off and every texel of a leaf crosses at the same depth — Elwynn's survivor coverage is flat at 47.6 % across the whole 52.5–61.1 yd band, then 0.0 %, while the probe's box-filtered control erodes 47.7 → 20.9 over the same band, so the art has the gradient the shipped pyramid threw away, and `clutter_size` puts the median tuft at 0.92 yd tall, 1.03 yd deep, a full yard for the boundary plane to sweep; the escape is bilinear: full alpha needs a 2×2 all-opaque texel neighbourhood, and those run out between mip 2 and 3 (179/56/10/0/0/0 on the dominant Elwynn cell), so the reference's λ ≈ 2.9–3.3 sits past the cliff and the tuft erodes continuously — 2.2 % of a tuft's pixels leave in the last half-yard with the bias, 11.6 % without, 24.2 % one mip sharper; wgpu has no sampler LOD bias, so the clutter lane re-samples base colour at `view.mip_bias + 0.25`, keeping the sampler every other model batch shares at the reference's 0; ruled out on the way: our BLP decoder (all nine uploaded levels byte-identical against the archive's 52 793 BLP2s), mip regeneration, the `has_mipmaps` byte's value 2 (a plain boolean, an authoring marker), anisotropy and geometry; what it does not fix is resolution-dependent, and so is the reference's own look — λ with the bias is 3.22 at the capture's 648p but 2.48 at 1080p and 1.48 at 2160p, so at a modern resolution the reference itself pops leaf by leaf, and scaling the bias with the viewport is one reversible line left for a look call (2021)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1792 lines
84 KiB
Rust
1792 lines
84 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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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\\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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\UIErrorsFrame.xml"); // the guards' red-line surface
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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\\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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.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, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.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, "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, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.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, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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 (the bar's [min, max]) 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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.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, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // the window tab template, ours (1004/1988)
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, "Interface\\FrameXML\\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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, "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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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, r"Interface\FrameXML\MoneyFrame.lua");
|
||
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
|
||
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
|
||
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
|
||
load_xml(&s, "ScrollTemplates.xml"); // our window tab template, before the window that inherits it (1988)
|
||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||
// 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, "Interface\\FrameXML\\BasicControls.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\ItemButtonTemplate.xml");
|
||
load_xml(&s, "Interface\\FrameXML\\QuestFrame.xml");
|
||
load_xml(&s, r"Interface\FrameXML\MainMenuBarMicroButtons.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());
|
||
}
|