seven more windows are the reference's own files
- seven more windows run off the player's own patch chain: the durability shield (`DurabilityFrame.xml`, with a Rust-side repair for the reference's own stale side-glyph inset the director caught on screen), `Localization.xml` + `GameTime.xml` (so `LocalizeFrames` finally runs from `VARIABLES_LOADED`, and the clock's 24-hour flag comes from where 1.12 sets it), the chat-link tooltip (`ItemRef.xml` — draggable now, and its shift-click arm fills the friend/ignore/who boxes as the reference's does), the mirror timers (`MirrorTimer.xml`, which exposed that our `STATICPOPUP_NUMDIALOGS` was 2 against the reference's 4, so a third breath/fatigue/feign bar could never be claimed), the zone splash with the fading kit, the item-text reader and the combo-point dots — each under the reference's published frame names, which is what an addon reaches for - `AddMessage` swallows a text it cannot use, silently, as the reference does: `0x795590` is a `lua_isstring` gate whose failure edge jumps straight to the epilogue — no line, no error, no record — and stock FrameXML takes that path: a full keyring calls `AddMessage(NO_EMPTY_KEYRING_SLOTS, …)` and no such GlobalString exists in 1.12, so it is a dead click on a real client where benilla put a script-error dialog on it; byte-verified by a four-worker cross-check in wow-re, and NOT generalised — a bad r/g/b/a still adds the line opaque white, only a bad text shows nothing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1792,19 +1792,15 @@ fn a_key_dropped_on_the_button_files_itself() {
|
||||
"the reference's own dangling string name prints nothing — un-quirking it is a decision, \
|
||||
not a default"
|
||||
);
|
||||
// The nil ALSO raises, out of `MessageFrame:AddMessage`. That is deliberately not asserted as
|
||||
// correct: our binding takes its text as an mlua `String`, which raises on nil by default —
|
||||
// not from a carved gate (`messageframe/plain.rs` reads `0x795590`'s rgb/alpha parse off the
|
||||
// bytes and says nothing about the text argument). Whether 1.12 raises here, no-ops, or prints
|
||||
// a blank line is out with wow-re. So this pins the keyring's own behaviour — the refusal, and
|
||||
// the silence — and lets exactly that one error through by name, which still fails the test on
|
||||
// any OTHER error the click produces.
|
||||
let errs = s.errors();
|
||||
assert!(
|
||||
errs.iter()
|
||||
.all(|e| e.contains("AddMessage") && e.contains("PutKeyInKeyRing")),
|
||||
"the only error may be the reference's own nil message: {errs:?}"
|
||||
);
|
||||
// And it does not RAISE either, which is the half this test could not assert until the bytes
|
||||
// came back. `AddMessage`'s text is fetched through `lua_isstring 0x6f3510` whose failure edge
|
||||
// is a silent jump to the function's own epilogue — no line, no error (wow-5875-re
|
||||
// `addmessage-text-gate-silent-skip.md`, 4-worker cross-check). Ours took an mlua `String` and
|
||||
// raised, which put a script-error dialog on an ordinary player gesture; the fix is in
|
||||
// `messageframe/mod.rs::message_text` and its own test carries the full law.
|
||||
//
|
||||
// So a full keyring is a DEAD CLICK on a real 1.12 client, and now here.
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The item-push drop animation (decision 0887): `ITEM_PUSH(container, icon)` runs the pushed item's
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! The combo-point dots (decisions 0869, 0875) — our `ComboFrame` implementation driven
|
||||
//! through the real loader: the show/hide edges, the per-point highlight/shine fade chain, the
|
||||
//! The combo-point dots (decisions 0869, 0875) — the reference's own `ComboFrame.xml`/`.lua`,
|
||||
//! executed off the player's chain since 1751's tenth window, driven through the real loader: the show/hide edges, the per-point highlight/shine fade chain, the
|
||||
//! "only newly-earned points flare" rule that `COMBO_FRAME_LAST_NUM_POINTS` exists to enforce, and
|
||||
//! the two gates that live in `GetComboPoints` rather than in this Lua — rogue-or-druid only, and
|
||||
//! the points must be banked on the CURRENT target.
|
||||
@@ -10,22 +10,7 @@ use benilla_ui::script::{PlayerReqState, UiScript, UnitState};
|
||||
const MOB_A: u64 = 0xF130_0000_0000_0001;
|
||||
const MOB_B: u64 = 0xF130_0000_0000_0002;
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` (the panel tests' loader), panicking on any loader error.
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The manifest order this file actually ships in: `UiPanels.xml` for `UIFrameFade`,
|
||||
/// `UnitFrames.xml` for the `TargetFrame` the dots anchor to.
|
||||
@@ -40,7 +25,7 @@ fn load_combo_frame() -> UiScript {
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "UnitFrames.xml");
|
||||
load_xml(&s, "ComboFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ComboFrame.xml");
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
s
|
||||
@@ -83,6 +68,7 @@ fn highlight_alpha(s: &mut UiScript, i: u32) -> f64 {
|
||||
/// completion first, since the highlight arrives via `UIFrameFade`, not a straight `SetAlpha`.
|
||||
#[test]
|
||||
fn combo_frame_follows_the_point_count() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_combo_frame();
|
||||
play_as(&mut s, 4, MOB_A); // rogue, mob A selected
|
||||
|
||||
@@ -135,6 +121,7 @@ fn combo_frame_follows_the_point_count() {
|
||||
/// combo event, so re-targeting mid-window fires exactly this repaint.
|
||||
#[test]
|
||||
fn a_repaint_at_the_same_count_does_not_re_flare() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_combo_frame();
|
||||
play_as(&mut s, 4, MOB_A);
|
||||
s.set_combo_points(2, MOB_A);
|
||||
@@ -164,6 +151,7 @@ fn a_repaint_at_the_same_count_does_not_re_flare() {
|
||||
/// the rogue ladder's first rung, so only the class differs.
|
||||
#[test]
|
||||
fn a_warriors_overpower_point_lights_no_dot() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_combo_frame();
|
||||
play_as(&mut s, 1, MOB_A); // warrior
|
||||
s.set_combo_points(1, MOB_A);
|
||||
@@ -184,6 +172,7 @@ fn a_warriors_overpower_point_lights_no_dot() {
|
||||
/// target entirely (guid 0) empties them too.
|
||||
#[test]
|
||||
fn re_targeting_empties_the_dots_without_the_count_moving() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = load_combo_frame();
|
||||
play_as(&mut s, 4, MOB_A);
|
||||
s.set_combo_points(3, MOB_A);
|
||||
|
||||
@@ -30,7 +30,7 @@ const JERKY_LINK: &str = "|cffffffff|Hitem:117|h[Tough Jerky]|h|r";
|
||||
const ROOM_UI: &[&str] = &[
|
||||
"UIDropDownMenu.xml",
|
||||
"UnitPopup.xml",
|
||||
"ItemRef.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"StackSplit.xml",
|
||||
"DressUpFrame.xml",
|
||||
|
||||
@@ -1,30 +1,16 @@
|
||||
//! The durability alert ("armor guy", `assets/ui/DurabilityFrame.xml`) against the shipped XML:
|
||||
//! The durability alert ("armor guy") against the reference's own `DurabilityFrame.xml`/`.lua`,
|
||||
//! executed off the player's chain since 1751's fourth window:
|
||||
//! the engine's `GetInventoryAlertStatus` statuses (recomputed on every inventory push, a change
|
||||
//! firing `UPDATE_INVENTORY_ALERTS`) drive the ref's own SetAlerts law — body pieces show
|
||||
//! together when any body region alerts, showSeparate pieces (Weapon/Shield/Ranged) each show
|
||||
//! only themselves, the shield glyph swaps for the off-weapon glyph when the off hand holds a
|
||||
//! WEAPON, and the whole frame hides at zero alerts.
|
||||
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
use benilla_ui::script::{
|
||||
InvSlotView, InventorySlots, ItemTemplateView, QuadContent, ScriptValue, UiScript,
|
||||
};
|
||||
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
|
||||
fn harness() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
@@ -35,10 +21,17 @@ fn harness() -> UiScript {
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"MinimapCluster.xml",
|
||||
"DurabilityFrame.xml",
|
||||
// The reference's own file. This module carried a private disk-only `load_xml` until the
|
||||
// swap, which structurally could not name a chain entry — [`super::test_ui::load_ui`] is
|
||||
// the one reader that speaks both stores, and it is why that helper exists.
|
||||
"Interface\\FrameXML\\DurabilityFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
// The app installs this at the end of every real load; a test VM has to say so itself. It is
|
||||
// benilla's stated repair of a reference bug — the seat going stale when a side glyph appears
|
||||
// while the frame is already shown — and its whole reasoning is at the constant.
|
||||
super::manifest::install_durability_reseat(&s).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
@@ -88,6 +81,7 @@ const YELLOW: [f32; 4] = [1.0, 0.82, 0.18, 1.0];
|
||||
/// white), then repaired (hidden again).
|
||||
#[test]
|
||||
fn armor_guy_shows_red_broken_yellow_damaged_and_hides_clean() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
// The ref's initial settle: the frame is authored shown; PLAYER_ENTERING_WORLD's SetAlerts
|
||||
// hides it while everything is clean.
|
||||
@@ -166,6 +160,7 @@ fn armor_guy_shows_red_broken_yellow_damaged_and_hides_clean() {
|
||||
/// 1.12 FrameXML never reads it.
|
||||
#[test]
|
||||
fn flag_bits_and_the_low_ammo_region() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
|
||||
|
||||
@@ -216,6 +211,7 @@ fn flag_bits_and_the_low_ammo_region() {
|
||||
/// broken off-hand WEAPON (template class 2) swaps it for the off-weapon glyph.
|
||||
#[test]
|
||||
fn off_hand_glyph_follows_what_the_hand_holds() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.set_item_template(
|
||||
2362,
|
||||
@@ -269,6 +265,7 @@ fn off_hand_glyph_follows_what_the_hand_holds() {
|
||||
/// the pass on every alert transition.
|
||||
#[test]
|
||||
fn manage_pass_seats_the_frame_inside_the_cluster_edge() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![ScriptValue::Str("".into())]);
|
||||
|
||||
@@ -313,6 +310,7 @@ fn manage_pass_seats_the_frame_inside_the_cluster_edge() {
|
||||
/// (director-caught). The OnEvent re-manage keeps it fresh on every alert recompute.
|
||||
#[test]
|
||||
fn a_late_side_glyph_refreshes_the_seat_while_the_frame_stays_shown() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.set_item_template(
|
||||
25,
|
||||
@@ -383,6 +381,7 @@ fn a_late_side_glyph_refreshes_the_seat_while_the_frame_stays_shown() {
|
||||
/// colliding there (director-caught, over "The Captain's Chest").
|
||||
#[test]
|
||||
fn the_quest_tracker_stacks_below_the_durability_guy() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in [
|
||||
@@ -393,11 +392,12 @@ fn the_quest_tracker_stacks_below_the_durability_guy() {
|
||||
"GameTooltip.xml",
|
||||
"MinimapCluster.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"DurabilityFrame.xml",
|
||||
"Interface\\FrameXML\\DurabilityFrame.xml",
|
||||
"QuestLogFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
super::manifest::install_durability_reseat(&s).unwrap();
|
||||
|
||||
// A right-side glyph shows the 65-tall durability frame under the minimap.
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
|
||||
@@ -364,16 +364,6 @@ const KNOWN: &[Known] = &[
|
||||
// question this flag exists to ask (decision 1757). A frame whose seat crosses the boundary
|
||||
// between UIParent's tree and the top level is a defect, because `SetFullScreenFrame` hides
|
||||
// `UIParent` and everything below it; a frame seated one rung along inside that tree is not.
|
||||
Known {
|
||||
frame: "FriendsDropDown",
|
||||
flag: Flag::Parent,
|
||||
why: "the reference nests this menu HOST in FriendsFrame; ours sits on UIParent in \
|
||||
ItemRef.xml, beside the SetItemRef branch that is its only caller — and it has to, \
|
||||
because FriendsFrame.xml loads 262 lines further down the manifest and a forward \
|
||||
parent reference resolves to nothing. Both seats are inside UIParent's tree, and a \
|
||||
host frame is never shown: ToggleDropDownMenu seats the visible DropDownList1, \
|
||||
which is top-level on both sides.",
|
||||
},
|
||||
Known {
|
||||
frame: "SendMailBodyEditBox",
|
||||
flag: Flag::Parent,
|
||||
|
||||
@@ -658,8 +658,7 @@ fn right_clicking_a_who_row_opens_the_friend_menu() {
|
||||
"the menu opens"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BenillaFriendsDropDown.name")
|
||||
.unwrap(),
|
||||
s.eval::<String>("return FriendsDropDown.name").unwrap(),
|
||||
"Tigole",
|
||||
"addressed by name, not by a unit token"
|
||||
);
|
||||
|
||||
@@ -1108,8 +1108,7 @@ fn right_clicking_a_roster_row_offers_the_guild_rows() {
|
||||
"the menu opens"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return BenillaFriendsDropDown.name")
|
||||
.unwrap(),
|
||||
s.eval::<String>("return FriendsDropDown.name").unwrap(),
|
||||
"Furor",
|
||||
"addressed by name — a roster row has no unit token behind it"
|
||||
);
|
||||
@@ -1166,8 +1165,7 @@ fn the_guild_rows_stay_off_a_who_row_menu() {
|
||||
// NOT our own name: the WHISPER row hides on yourself, and with INVITE gated the same way the
|
||||
// menu would have nothing to show and never open — which would make the assertion below pass
|
||||
// for the wrong reason.
|
||||
s.run("BenillaFriendsFrame_ShowDropdown(\"Thrall\", 1)")
|
||||
.unwrap();
|
||||
s.run("FriendsFrame_ShowDropdown(\"Thrall\", 1)").unwrap();
|
||||
assert!(s.eval::<bool>("return DropDownList1:IsVisible()").unwrap());
|
||||
let present = r#"
|
||||
for i = 1, UIDROPDOWNMENU_MAXBUTTONS do
|
||||
|
||||
@@ -66,9 +66,49 @@ fn bootstrap_positions(script: &UiScript) -> Vec<String> {
|
||||
error!("ui_script: managed-positions bootstrap: {e}");
|
||||
return vec![format!("managed-positions bootstrap: {e}")];
|
||||
}
|
||||
if let Err(e) = install_durability_reseat(script) {
|
||||
error!("ui_script: durability re-seat hook: {e}");
|
||||
return vec![format!("durability re-seat hook: {e}")];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// **A stated divergence from the reference, installed rather than edited in** (1751 window 4).
|
||||
///
|
||||
/// The reference seats `DurabilityFrame` 20 further in when one of its three side glyphs is up
|
||||
/// (`UIParent.lua` l.1759-1768), and recomputes that only when something calls
|
||||
/// `UIParent_ManageFramePositions`. Stock `DurabilityFrame.xml` calls it from `OnShow`/`OnHide`
|
||||
/// only — so the case where the frame is **already shown** and a side glyph *then* appears leaves
|
||||
/// the seat stale, and the shield glyph's ~17-unit overhang hangs off the screen edge until some
|
||||
/// unrelated frame happens to trigger the next pass. That is the reference's own bug and the
|
||||
/// director caught it on their screen; it is not ours to reproduce faithfully.
|
||||
///
|
||||
/// So the fix rides on the frame's `OnEvent` — which is exactly the recompute the glyphs change on
|
||||
/// — and it is installed **here** rather than written into anything of Blizzard's. Two reasons for
|
||||
/// that home rather than a FrameXML one: `assets/ui` does not grow (1779), and this is the wrong
|
||||
/// side of the line for a `ContainerFrameAdapters`-class shim anyway — that clause is for a genuine
|
||||
/// engine difference (1751 §2), and this is a deliberate repair of a reference defect. Rust is also
|
||||
/// the durable home: our `UIParent.xml` is itself a transcription awaiting its own window, and a
|
||||
/// hook parked there would be homeless again the day it migrates.
|
||||
///
|
||||
/// Idempotent by its own latch, so a `ReloadUI` cannot stack wrappers. The reference's handler
|
||||
/// still runs first and unchanged: `this`, `event` and `arg1` are globals the engine has already
|
||||
/// set for the dispatch, so calling it through the captured reference is the same call.
|
||||
pub(super) fn install_durability_reseat(script: &UiScript) -> Result<(), String> {
|
||||
script.run(DURABILITY_RESEAT).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
const DURABILITY_RESEAT: &str = r#"
|
||||
if DurabilityFrame and not DurabilityFrame.benillaReseat then
|
||||
DurabilityFrame.benillaReseat = 1
|
||||
local refOnEvent = DurabilityFrame:GetScript("OnEvent")
|
||||
DurabilityFrame:SetScript("OnEvent", function()
|
||||
if refOnEvent then refOnEvent() end
|
||||
UIParent_ManageFramePositions()
|
||||
end)
|
||||
end
|
||||
"#;
|
||||
|
||||
/// Load benilla's own default UI — every file [`MANIFEST`] names — through the engine-free loader.
|
||||
/// This is our content (MIT/Apache), committed and **compiled into the binary**
|
||||
/// ([`super::content`], decision 1175); a dev build still prefers the copy on disk, so editing a
|
||||
|
||||
@@ -5,23 +5,7 @@
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the other tests'
|
||||
/// loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn enabled(s: &UiScript, button: &str) -> bool {
|
||||
s.eval::<bool>(&format!("return {button}:IsEnabled() ~= 0"))
|
||||
@@ -30,6 +14,7 @@ fn enabled(s: &UiScript, button: &str) -> bool {
|
||||
|
||||
#[test]
|
||||
fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// Host globals the cluster's OnLoad/clicks lean on that a bare engine doesn't install.
|
||||
@@ -93,6 +78,7 @@ fn minimap_zoom_buttons_resync_when_switching_inside_and_outside() {
|
||||
/// tracking state pushed, hide when it clears.
|
||||
#[test]
|
||||
fn tracking_frame_follows_get_tracking_texture_across_player_auras_changed() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
use benilla_ui::script::TrackingState;
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
@@ -134,18 +120,24 @@ fn game_time_session(hour: u32, minute: u32) -> UiScript {
|
||||
s.run("function GetMinimapZoneText() return '' end")
|
||||
.unwrap();
|
||||
s.run("function PlaySound() end").unwrap();
|
||||
// Byte-exact 1.12 GlobalStrings (l.4251-4253) — the engine-only session loads no
|
||||
// GlobalStrings off a chain.
|
||||
s.run("TIME_TWELVEHOURAM = \"%d:%02d AM\"; TIME_TWELVEHOURPM = \"%d:%02d PM\"; TIME_TWENTYFOURHOURS = \"%d:%02d\"")
|
||||
.unwrap();
|
||||
s.run(&format!(
|
||||
"__benilla_game_hour = {hour}; __benilla_game_minute = {minute}"
|
||||
))
|
||||
.unwrap();
|
||||
// The player's own strings and locale rather than a hand-typed copy of three of them: the
|
||||
// window is the reference's file since 1751 window 5, and `TIME_TWENTYFOURHOURS` and
|
||||
// `TwentyFourHourTime` are exactly what it reads. `Localization.xml` only DEFINES
|
||||
// `LocalizeFrames`; the reference calls it from `UIParent_OnEvent`'s VARIABLES_LOADED arm, and
|
||||
// this session has no UIParent, so it calls it directly.
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\Localization.xml");
|
||||
s.run("LocalizeFrames()").unwrap();
|
||||
load_xml(&s, "Fonts.xml");
|
||||
// `TEXT()`, which the reference's own tooltip and label formatting passes every string through.
|
||||
load_xml(&s, "BasicControls.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "MinimapCluster.xml");
|
||||
load_xml(&s, "GameTime.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GameTime.xml");
|
||||
s
|
||||
}
|
||||
|
||||
@@ -161,6 +153,7 @@ fn tod_window(s: &UiScript) -> (f64, f64, f64, f64) {
|
||||
/// the ref compares (`< DAWN or >= DUSK`).
|
||||
#[test]
|
||||
fn game_time_frame_slides_the_sun_moon_window_on_the_game_clock() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = game_time_session(10, 30);
|
||||
// OnLoad seeded `timeOfDay = 0` and 10:30 ≠ 0, so the very first update already seated the
|
||||
// window — no OnUpdate tick needed for the initial state.
|
||||
@@ -194,6 +187,7 @@ fn game_time_frame_slides_the_sun_moon_window_on_the_game_clock() {
|
||||
/// hit-test never captures) and the `<HitRectInsets>` hull.
|
||||
#[test]
|
||||
fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = game_time_session(21, 7);
|
||||
s.resolve();
|
||||
|
||||
@@ -239,6 +233,7 @@ fn hovering_the_indicator_shows_and_live_updates_the_game_time_tooltip() {
|
||||
/// Calling `Minimap_OnClick()` from `s.run` would prove none of it (1234 §2).
|
||||
#[test]
|
||||
fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run("function GetMinimapZoneText() return '' end")
|
||||
@@ -282,6 +277,7 @@ fn a_click_on_the_minimap_parks_a_centre_relative_ping_request() {
|
||||
/// under its own feet.
|
||||
#[test]
|
||||
fn get_ping_position_is_nil_until_there_is_a_ping() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run("function GetMinimapZoneText() return '' end")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The breath / fatigue bars against the transcribed reference behavior (decision 0874): the
|
||||
//! extracted 1.12 `MirrorTimer.lua` is the spec — a bar per running timer, the timer's own colour
|
||||
//! and caption, the value integrating `scale * elapsed` between packets, and the frame released
|
||||
//! back to the pool on STOP.
|
||||
//! The breath / fatigue bars against the reference's own `MirrorTimer.xml`/`.lua`, executed off
|
||||
//! the player's chain since 1751's seventh window (decision 0874 is the arc): a bar per running
|
||||
//! timer, the timer's own colour and caption, the value integrating `scale * elapsed` between
|
||||
//! packets, and the frame released back to the pool on STOP.
|
||||
//!
|
||||
//! Two halves, and the second is the load-bearing one (the cast bar's lesson, `cast_tests`): the
|
||||
//! state tests read the Lua back, which is blind to what actually *paints*. The
|
||||
@@ -10,28 +10,22 @@
|
||||
|
||||
use benilla_ui::script::{ExtractedQuad, QuadContent, ScriptValue, UiScript};
|
||||
|
||||
/// Load one shipped `assets/ui/<file>`, panicking on any loader error.
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn harness() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "MirrorTimer.xml");
|
||||
// The bars' START driver: `UIParent_OnEvent`'s MIRROR_TIMER_START arm is what calls
|
||||
// `MirrorTimer_Show`, which is where the reference keeps it (UIParent.lua l.97 + l.374-377)
|
||||
// and where window 7 moved ours back to. Without this file the event reaches nothing.
|
||||
load_xml(&s, "UIParent.xml");
|
||||
// `STATICPOPUP_NUMDIALOGS`, which the reference's own `MirrorTimer_Show` bounds its free-bar
|
||||
// search by (MirrorTimer.lua l.32 — a copy-paste from StaticPopup.lua, and its own bug). A
|
||||
// session without it searches `1, nil` and finds no free bar at all, so this is not scenery.
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MirrorTimer.xml");
|
||||
s
|
||||
}
|
||||
|
||||
@@ -105,6 +99,7 @@ fn tex_quad<'a>(quads: &'a [ExtractedQuad], leaf: &str) -> Option<&'a ExtractedQ
|
||||
/// Every bar starts hidden — no timer, no chrome on screen.
|
||||
#[test]
|
||||
fn the_three_timers_load_hidden() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
for i in 1..=3 {
|
||||
assert!(!shown(&s, &format!("MirrorTimer{i}")), "MirrorTimer{i}");
|
||||
@@ -119,6 +114,7 @@ fn the_three_timers_load_hidden() {
|
||||
/// seated at the value the server named.
|
||||
#[test]
|
||||
fn breath_takes_the_first_bar_in_the_reference_blue() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
|
||||
@@ -140,6 +136,7 @@ fn breath_takes_the_first_bar_in_the_reference_blue() {
|
||||
/// client's key for it is `EXHAUSTION`, not the server's `FATIGUE`.
|
||||
#[test]
|
||||
fn fatigue_is_the_reference_yellow_under_its_own_caption() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
|
||||
|
||||
@@ -156,6 +153,7 @@ fn fatigue_is_the_reference_yellow_under_its_own_caption() {
|
||||
/// reference's draining rate that is one bar-second per real second.
|
||||
#[test]
|
||||
fn the_bar_drains_at_the_servers_signed_rate() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
|
||||
@@ -184,6 +182,7 @@ fn the_bar_drains_at_the_servers_signed_rate() {
|
||||
/// A paused timer holds its value — the frozen state the server sends when it stops the clock.
|
||||
#[test]
|
||||
fn a_paused_timer_holds_its_value() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.fire_event(
|
||||
"MIRROR_TIMER_START",
|
||||
@@ -205,8 +204,18 @@ fn a_paused_timer_holds_its_value() {
|
||||
"frozen: no integration at all"
|
||||
);
|
||||
|
||||
// MIRROR_TIMER_PAUSE(name, 0) releases it (our arg2 reading — the reference's own branch is
|
||||
// bugged and the packet is never sent by vmangos; see MirrorTimer.xml departure 3).
|
||||
// **And `MIRROR_TIMER_PAUSE` cannot release it, because the reference's own branch is
|
||||
// bugged.** `MirrorTimerFrame_OnEvent` reads `arg1` as the timer NAME (`arg1 ~= this.timer`)
|
||||
// and then, two lines later, as a number (`arg1 > 0`) — comparing a string to a number, which
|
||||
// raises in Lua 5.0. So the handler dies before it can clear `paused` and the bar stays
|
||||
// frozen.
|
||||
//
|
||||
// Ours read the flag from `arg2` until 1751's seventh window, which is what both branches
|
||||
// plainly intend; the swap made the reference's body the live one and this pins what it
|
||||
// actually does. It is unreachable in play: vmangos refuses to send that packet and
|
||||
// substitutes a full START, saying so in `Player::SendMirrorTimers` — which is presumably why
|
||||
// the bug survived 1.12 at all. Repairing it is a decision someone can make in the adapters;
|
||||
// it is not the default, and it is not silent.
|
||||
s.fire_event(
|
||||
"MIRROR_TIMER_PAUSE",
|
||||
vec![ScriptValue::Str("BREATH".into()), ScriptValue::Int(0)],
|
||||
@@ -214,9 +223,15 @@ fn a_paused_timer_holds_its_value() {
|
||||
for _ in 0..20 {
|
||||
frame(&mut s, 0.05);
|
||||
}
|
||||
assert_eq!(
|
||||
bar_value(&s, "MirrorTimer1"),
|
||||
45.0,
|
||||
"the reference's own bugged branch cannot unfreeze a bar"
|
||||
);
|
||||
let errs = s.errors();
|
||||
assert!(
|
||||
bar_value(&s, "MirrorTimer1") < 44.5,
|
||||
"unfrozen: draining again"
|
||||
errs.iter().any(|e| e.contains("compare")),
|
||||
"…and says so, loudly, rather than failing quietly: {errs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,6 +240,7 @@ fn a_paused_timer_holds_its_value() {
|
||||
/// consuming a third.
|
||||
#[test]
|
||||
fn two_timers_stack_and_restate_in_place() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
@@ -244,6 +260,7 @@ fn two_timers_stack_and_restate_in_place() {
|
||||
/// STOP hides only the named timer and hands its frame back to the pool.
|
||||
#[test]
|
||||
fn stop_hides_that_timer_and_frees_its_frame() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
@@ -269,6 +286,7 @@ fn stop_hides_that_timer_and_frees_its_frame() {
|
||||
/// (the reference's `PLAYER_ENTERING_WORLD` arm).
|
||||
#[test]
|
||||
fn entering_the_world_clears_every_bar() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "EXHAUSTION", 60_000, 60_000, -1, "Fatigue");
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
@@ -283,6 +301,7 @@ fn entering_the_world_clears_every_bar() {
|
||||
/// out from under a different timer.
|
||||
#[test]
|
||||
fn a_stop_for_an_idle_timer_touches_nothing() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
s.fire_event(
|
||||
@@ -297,6 +316,7 @@ fn a_stop_for_an_idle_timer_touches_nothing() {
|
||||
/// state tests above all pass on a bar that draws nothing.
|
||||
#[test]
|
||||
fn a_running_timer_paints_its_chrome_and_fill() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
let quads = frame(&mut s, 0.016);
|
||||
@@ -326,6 +346,7 @@ fn a_running_timer_paints_its_chrome_and_fill() {
|
||||
/// …and a stopped timer takes all of it off screen again.
|
||||
#[test]
|
||||
fn a_stopped_timer_paints_nothing() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
frame(&mut s, 0.016);
|
||||
@@ -346,6 +367,7 @@ fn a_stopped_timer_paints_nothing() {
|
||||
/// fill in the painter's order — i.e. strictly greater `z`.
|
||||
#[test]
|
||||
fn the_border_and_caption_draw_over_the_fill() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
start(&mut s, "BREATH", 45_000, 60_000, -1, "Breath");
|
||||
let quads = frame(&mut s, 0.016);
|
||||
|
||||
@@ -31,7 +31,7 @@ fn harness() -> UiScript {
|
||||
"UiPanels.xml",
|
||||
"UIParent.xml",
|
||||
"GameTooltip.xml",
|
||||
"ItemRef.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"Cooldown.xml",
|
||||
"CharacterFrame.xml",
|
||||
@@ -56,7 +56,11 @@ fn harness_with_bags() -> UiScript {
|
||||
for f in BAG_UI {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
for f in ["ItemRef.xml", "MerchantFrame.xml", "CharacterFrame.xml"] {
|
||||
for f in [
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"MerchantFrame.xml",
|
||||
"CharacterFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
s.set_money(0);
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
//! The unit right-click popups reached the two ways a solo player invites someone (director
|
||||
//! report, 2026-07-17): right-clicking a **target** unit frame, and right-clicking a **chat
|
||||
//! name**. Both drive the shared UnitPopup engine (`UnitPopup.xml`); the chat path adds the
|
||||
//! `SetItemRef` player branch + its minimal `FriendsFrame` stand-in (`ItemRef.xml`). Proven
|
||||
//! serverless through the real hit/route paths.
|
||||
//! `SetItemRef` player branch — the reference's own since 1751 window 6 — and the FRIEND dropdown
|
||||
//! it opens, which lives in `FriendsFrame.xml` where 1.12 keeps it. Proven serverless through the
|
||||
//! real hit/route paths.
|
||||
|
||||
use benilla_ui::script::{FollowRequest, PartyRequest, UiScript, UnitState};
|
||||
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
/// The production prefix the popups need (ui_script/mod.rs order): the dropdown kit, the unit
|
||||
/// popups, the chat-link router (`ItemRef.xml`, after UnitPopup — its `FriendsDropDown` inherits
|
||||
/// `UIDropDownMenuTemplate` and opens a UnitPopup FRIEND menu), then the unit frames.
|
||||
/// The production prefix the popups need, in `benilla.toc`'s own order: the dropdown kit, the
|
||||
/// unit popups, the reference's chat-link router, the unit frames — and `FriendsFrame.xml`, which
|
||||
/// is where `FriendsFrame_ShowDropdown` and the `FriendsDropDown` host live in 1.12 and where they
|
||||
/// live here since window 6 (our ItemRef.xml used to carry a private second copy of both).
|
||||
fn load_popup_frames(s: &UiScript) {
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"UIParent.xml",
|
||||
// `SmallMoneyFrame_OnLoad`, which UiPanels' own StaticPopup money rows call at load.
|
||||
"MoneyFrame.xml",
|
||||
// `StaticPopupDialogs` and the `PanelTemplates_*` family, both of which FriendsFrame.xml
|
||||
// reaches at LOAD (its tab row and its confirm dialogs).
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
"UIDropDownMenu.xml",
|
||||
"UnitPopup.xml",
|
||||
"ItemRef.xml",
|
||||
"Interface\\FrameXML\\ItemRef.xml",
|
||||
"UnitFrames.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
"FriendsFrame.xml",
|
||||
] {
|
||||
load_xml(s, file);
|
||||
}
|
||||
@@ -69,6 +65,7 @@ fn bake_strings(s: &UiScript) {
|
||||
/// hit — the player branch of `SetItemRef` used to be a no-op stub.
|
||||
#[test]
|
||||
fn chat_name_right_click_opens_the_invite_menu() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_strings(&s);
|
||||
@@ -137,6 +134,7 @@ fn chat_name_right_click_opens_the_invite_menu() {
|
||||
/// off for players — only NPC/self targets, whose every row needs a party, open nothing.
|
||||
#[test]
|
||||
fn solo_target_right_click_invites_a_player() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_strings(&s);
|
||||
@@ -236,6 +234,7 @@ fn solo_target_right_click_invites_a_player() {
|
||||
/// so a break between `this.value == "TRADE"` and the queued initiate token shows up here.
|
||||
#[test]
|
||||
fn solo_target_trade_click_queues_an_initiate() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_strings(&s);
|
||||
@@ -303,6 +302,7 @@ fn solo_target_trade_click_queues_an_initiate() {
|
||||
/// exactly the kind of break a row-label assertion cannot see.
|
||||
#[test]
|
||||
fn solo_target_follow_click_queues_an_exact_by_name_follow() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_strings(&s);
|
||||
@@ -369,6 +369,7 @@ fn solo_target_follow_click_queues_an_exact_by_name_follow() {
|
||||
/// popup's dispatch, and `inspect_tests.rs` owns what the window then does.
|
||||
#[test]
|
||||
fn solo_target_inspect_click_reaches_inspect_unit() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_strings(&s);
|
||||
@@ -536,6 +537,7 @@ fn right_click_the_pet_frame(s: &mut UiScript) {
|
||||
/// offered a row the reference never shows.
|
||||
#[test]
|
||||
fn the_pet_menu_forks_between_abandon_and_dismiss() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_pet_strings(&s);
|
||||
@@ -605,6 +607,7 @@ fn the_pet_menu_forks_between_abandon_and_dismiss() {
|
||||
/// giving up a tamed pet is a server-side delete.
|
||||
#[test]
|
||||
fn dismiss_sends_immediately_and_abandon_waits_for_the_confirm() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_pet_strings(&s);
|
||||
@@ -661,6 +664,7 @@ fn dismiss_sends_immediately_and_abandon_waits_for_the_confirm() {
|
||||
/// opens while the name dialog is still up, so the second one lands in instance 2.
|
||||
#[test]
|
||||
fn renaming_a_pet_reads_the_name_back_before_sending_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_pet_strings(&s);
|
||||
@@ -712,6 +716,7 @@ fn renaming_a_pet_reads_the_name_back_before_sending_it() {
|
||||
/// was blocked on, and the only row here that opens a window rather than a wire verb.
|
||||
#[test]
|
||||
fn the_pet_details_row_opens_the_pet_paper_doll() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_pet_strings(&s);
|
||||
@@ -742,6 +747,7 @@ fn the_pet_details_row_opens_the_pet_paper_doll() {
|
||||
/// unit reconnects mid-open — and closing the menu re-parks the driver.
|
||||
#[test]
|
||||
fn an_idle_unit_popup_driver_parks_itself_off_the_tick() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_strings(&s);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! The shipped `ZoneText.xml` driven engine-only (decision 0287): the zone splash shows on the
|
||||
//! The reference's own `ZoneText.xml` — executed off the player's chain since 1751's eighth
|
||||
//! window — driven engine-only (decision 0287): the zone splash shows on the
|
||||
//! right events with the right strings/colors, fades on the reference 0.5/1.0/2.0 timeline, and
|
||||
//! honors the subtle law — a plain `ZONE_CHANGED` re-caches the zone name *silently*, so a later
|
||||
//! `ZONE_CHANGED_NEW_AREA` that lands on the already-cached name never re-splashes.
|
||||
@@ -8,23 +9,7 @@
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error (the sibling
|
||||
/// tests' loader, duplicated so this file is self-contained).
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
use super::test_ui::load_ui as load_xml;
|
||||
|
||||
fn visible(s: &UiScript, frame: &str) -> bool {
|
||||
s.eval::<bool>(&format!("return {frame}:IsVisible() and true or false"))
|
||||
@@ -50,16 +35,26 @@ fn set_area(s: &UiScript, zone: &str, sub: &str, pvp: &str, faction: &str) {
|
||||
fn harness() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The player's own strings: the splash formats CONTESTED_TERRITORY,
|
||||
// FACTION_CONTROLLED_TERRITORY, FREE_FOR_ALL_TERRITORY and the AUTOFOLLOW pair straight out of
|
||||
// GlobalStrings. Our deleted copy carried `X = X or "…"` fallbacks for a harness with no
|
||||
// chain; the reference's file has none, and it should not.
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Fonts.xml");
|
||||
// `TEXT()`, which AutoFollowStatus_OnEvent puts its message through.
|
||||
load_xml(&s, "BasicControls.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "ZoneText.xml");
|
||||
// The fading kit, its own manifest entry since window 8 — as it is in the reference's TOC.
|
||||
load_xml(&s, "Interface\\FrameXML\\FadingFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ZoneText.xml");
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_area_splashes_zone_pvp_and_subzone_then_fades_out() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
|
||||
// OnLoad hides both fading frames.
|
||||
@@ -89,6 +84,7 @@ fn new_area_splashes_zone_pvp_and_subzone_then_fades_out() {
|
||||
|
||||
#[test]
|
||||
fn subzone_hop_shows_only_the_small_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(&s, "Elwynn Forest", "", "friendly", "Alliance");
|
||||
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
|
||||
@@ -108,6 +104,7 @@ fn subzone_hop_shows_only_the_small_line() {
|
||||
|
||||
#[test]
|
||||
fn plain_zone_changed_recaches_silently_so_new_area_wont_resplash() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(&s, "Elwynn Forest", "", "friendly", "Alliance");
|
||||
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
|
||||
@@ -143,6 +140,7 @@ fn plain_zone_changed_recaches_silently_so_new_area_wont_resplash() {
|
||||
/// SubZoneTextFrame's handler calls SetZoneText(1), which SETS the (hidden) PVP string.
|
||||
#[test]
|
||||
fn abbey_grounds_subzone_hop_shows_no_territory_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(
|
||||
&s,
|
||||
@@ -178,6 +176,7 @@ fn abbey_grounds_subzone_hop_shows_no_territory_line() {
|
||||
/// truth, produced by the engine feed rather than by handler-order luck.
|
||||
#[test]
|
||||
fn abbey_interior_shows_the_room_in_the_small_line_alone() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(
|
||||
&s,
|
||||
@@ -210,6 +209,7 @@ fn abbey_interior_shows_the_room_in_the_small_line_alone() {
|
||||
/// shows under it, exactly as on a NEW_AREA splash.
|
||||
#[test]
|
||||
fn inn_entry_splashes_the_inn_name_with_territory_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(&s, "Elwynn Forest", "Goldshire", "friendly", "Alliance");
|
||||
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
|
||||
@@ -231,6 +231,7 @@ fn inn_entry_splashes_the_inn_name_with_territory_line() {
|
||||
/// exactly like the entry hop.
|
||||
#[test]
|
||||
fn indoor_exit_returns_the_subzone_line_alone() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(
|
||||
&s,
|
||||
@@ -265,6 +266,7 @@ fn indoor_exit_returns_the_subzone_line_alone() {
|
||||
/// the big frame never shows, so no territory line.
|
||||
#[test]
|
||||
fn room_to_room_hop_splashes_the_room_name_alone() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(&s, "Elwynn Forest", "Main Hall", "friendly", "Alliance");
|
||||
s.fire_event("ZONE_CHANGED_INDOORS", vec![]);
|
||||
@@ -284,6 +286,7 @@ fn room_to_room_hop_splashes_the_room_name_alone() {
|
||||
/// the "PvP Area" arena string under it, red per the quote's PVPArenaTextString color.
|
||||
#[test]
|
||||
fn arena_pit_shows_the_ffa_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(&s, "Stranglethorn Vale", "", "contested", "");
|
||||
s.fire_event("ZONE_CHANGED_NEW_AREA", vec![]);
|
||||
@@ -304,6 +307,7 @@ fn arena_pit_shows_the_ffa_line() {
|
||||
/// anchors to PVPInfoTextString's BOTTOM — the three-line stack the director's screenshot shows.
|
||||
#[test]
|
||||
fn subzone_seat_hangs_under_the_territory_line_on_new_area() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
set_area(
|
||||
&s,
|
||||
@@ -336,6 +340,7 @@ fn subzone_seat_hangs_under_the_territory_line_on_new_area() {
|
||||
/// name at start rather than re-reading it — by the time a follow ends, the followee is often gone.
|
||||
#[test]
|
||||
fn the_autofollow_status_line_names_the_followee_and_fades_on_end() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
assert!(
|
||||
!visible(&s, "AutoFollowStatus"),
|
||||
@@ -382,6 +387,7 @@ fn the_autofollow_status_line_names_the_followee_and_fades_on_end() {
|
||||
/// than inheriting the dying one's alpha.
|
||||
#[test]
|
||||
fn a_begin_during_the_end_fade_resets_the_line() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = harness();
|
||||
s.fire_event(
|
||||
"AUTOFOLLOW_BEGIN",
|
||||
|
||||
Reference in New Issue
Block a user