the chat and spellbook go stock, and hidden props park their meshes
- the chat window is the reference's own `ChatFrame.xml` and `FloatingChatFrame.xml`, run off the player's chain; our 2216-line transcription is deleted, the feed fires `CHAT_MSG_*` and the reference's `ChatFrame_OnEvent` prints — no app-side composition into the windows, no routing table, no whisper chime or flash of ours, and the transcribed edit-box machine (sticky type, tell ring, Tab cycle) retires with it; the verbs the two files call are built to the bytes — the whole `0x98` chat-window record and its eight verbs, the 94-entry chat-type colour registry, the language walk, the channel verbs as a command queue, the slash handlers' emote/roll/assist/uninvite/console/logging verbs; `chat-cache.txt` is written and read in the reference's whole grammar, the 68-entry message-group table in flag order and the no-file boot included, and the chat bindings and macro lines run through the reference's own arms (1948) - the spellbook is the reference's own `SpellBookFrame.xml`; our 965-line transcription is deleted, and two quirks it reproduced by hand (the pet page ignores its page number, its arrows write the spell book's counter) now come from the reference's lines; one adapter remains because the reference's autocast shine is a `<Model>` that draws no pixels in this engine — the script puts the token-texture shine beside each button's model and carries it with the model's shown state; the nine spell-slot bindings (`GetSpellName`, `CastSpell`, `PickupSpell` and their kin) now parse their arguments the way the reference's shared marshaller `0x4b3ec0` does — a numeric arg 1, a mandatory arg 2, the index gated to [0, 0x400) with "Invalid spell slot in <Verb>" on a miss, "pet" matched without case — where ours raised mlua's own message on a negative id, answered nil for 0 and above 1024, and read the wrong book for "PET" (1952) - a long-hidden world model part parks its `Mesh3d`: the entity path keeps the batches the retained pass declines resident — 5.3k hidden submeshes at any instant at the Stormwind auction house — and Bevy's sweeps walk every `Mesh3d` whether it draws or not, so a part parks after 30 consecutive hidden frames and unparks the frame its verdict turns visible; every world model part takes part, units' bodies included, while a booth's mirror parts stay out; interleaved pairs read an alone Stormwind 0.1–0.4 ms lighter, and `WOW_NO_MESH_PARK=1` is the A/B; the audio thread's realtime class was the standing suspect for the raid's `cpu_p99` spikes with sound on, and `WOW_AUDIO_NO_RT=1` across two interleaved Blackrock pairs read no difference, so the lever stays as an instrument (1949); and a no-merge prop batch now says which rule declined it — animated (1551 at the auction house) or transparent, not order-free (340) (1951) - the probe reads CPU per thread across its window (user and system split, because a user-mode sampler never sees kernel time) and the main thread's CPU inside the app's frame beside its whole-frame CPU; together they place the vsync tax every player pays by default — cpu_ms 10.7 uncapped against 12.5 under vsync, alone in Stormwind, with no system growing and no thread spinning: it lands on the main thread, in user time, inside the schedules, the same systems each running slower, and a low-priority spinner holding one core busy erases it to the last decimal (vsync + spinner 10.18 against uncapped 10.17), which is frequency scaling — under vsync the cores idle deeper between frames and the next burst starts at a lower clock; nothing in the client is the cause and nothing in the client changes for it (1950) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -389,7 +389,16 @@ fn managed_positions_track_the_bottom_bar_stack() {
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\CastingBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "GameTooltip.xml"); // the dropdown kit's MenuBackdrop reads TOOLTIP_DEFAULT_COLOR
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||||
|
||||
// The loader's post-load bootstrap, replayed with no bars in existence: the bare bases.
|
||||
s.run("UIParent_ManageFramePositions()").unwrap();
|
||||
|
||||
@@ -50,17 +50,29 @@ fn chat_with_menu() -> UiScript {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\ColorPickerFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
bake_strings(&s);
|
||||
super::fire_chat_login(&mut s);
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
s.resolve();
|
||||
s
|
||||
}
|
||||
|
||||
/// Right-click a frame's centre through the real hit path.
|
||||
/// The tabs ship hidden and the dock reveals them on a stationary hover (`FCF_OnUpdate`); a
|
||||
/// click on a tab needs that reveal first.
|
||||
fn reveal_then(s: &mut UiScript) {
|
||||
reveal_dock(s);
|
||||
}
|
||||
|
||||
fn right_click(s: &mut UiScript, frame: &str) {
|
||||
let (x, y) = s
|
||||
.eval::<(f64, f64)>(&format!("return {frame}:GetCenter()"))
|
||||
@@ -89,7 +101,7 @@ fn reveal_dock(s: &mut UiScript) {
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(x, y);
|
||||
for _ in 0..25 {
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
@@ -119,41 +131,29 @@ fn right_clicking_a_chat_tab_opens_its_options_menu() {
|
||||
.unwrap(),
|
||||
"no menu before any click"
|
||||
);
|
||||
|
||||
reveal_then(&mut s);
|
||||
right_click(&mut s, "ChatFrame1Tab");
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
|
||||
"the tab's right-click opens the options menu"
|
||||
);
|
||||
// The window verb, then the DISPLAY block, in the reference's own order
|
||||
// (FloatingChatFrame.lua l.236-307).
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return DropDownList1.numButtons").unwrap(),
|
||||
4,
|
||||
"Lock Window + Display title + Font Size + Background"
|
||||
);
|
||||
// **Unlock**, not Lock: both dock windows ship LOCKED (the stock chat-cache `LOCKED 1`), so
|
||||
// the row offers the way out of it. `chat_resize_tests` drives the toggle itself.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button1:GetText()")
|
||||
.unwrap(),
|
||||
"Unlock Window"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button2:GetText()")
|
||||
.unwrap(),
|
||||
"Display"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button3:GetText()")
|
||||
.unwrap(),
|
||||
"Font Size"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button4:GetText()")
|
||||
.unwrap(),
|
||||
"Background"
|
||||
);
|
||||
// The reference's first six rows for the default window (FloatingChatFrame.lua
|
||||
// `FCFOptionsDropDown_Initialize`, level 1): the lock verb, Rename, New Window, the Display
|
||||
// title, Font Size and Background — a docked non-default window gets Close before Display.
|
||||
for (n, key) in [
|
||||
(1, "UNLOCK_WINDOW"),
|
||||
(2, "RENAME_CHAT_WINDOW"),
|
||||
(3, "NEW_CHAT_WINDOW"),
|
||||
(4, "DISPLAY"),
|
||||
(5, "FONT_SIZE"),
|
||||
(6, "BACKGROUND"),
|
||||
] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!("return DropDownList1Button{n}:GetText() == {key}"))
|
||||
.unwrap(),
|
||||
"row {n} is {key}"
|
||||
);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -163,9 +163,10 @@ fn right_clicking_a_chat_tab_opens_its_options_menu() {
|
||||
fn a_left_click_still_selects_the_tab_and_opens_no_menu() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_with_menu();
|
||||
reveal_then(&mut s);
|
||||
left_click(&mut s, "ChatFrame2Tab");
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BenillaFCF.selected").unwrap(),
|
||||
s.eval::<i64>("return SELECTED_DOCK_FRAME:GetID()").unwrap(),
|
||||
2,
|
||||
"left-click selects"
|
||||
);
|
||||
@@ -174,7 +175,6 @@ fn a_left_click_still_selects_the_tab_and_opens_no_menu() {
|
||||
.unwrap(),
|
||||
"a left click opens no menu"
|
||||
);
|
||||
// …and it closes one that is open (ref FCF_Tab_OnClick's `CloseDropDownMenus()`).
|
||||
right_click(&mut s, "ChatFrame1Tab");
|
||||
assert!(s.eval::<bool>("return DropDownList1:IsVisible()").unwrap());
|
||||
left_click(&mut s, "ChatFrame1Tab");
|
||||
@@ -197,28 +197,32 @@ fn a_left_click_still_selects_the_tab_and_opens_no_menu() {
|
||||
fn the_background_row_opens_the_picker_and_its_opacity_slider_drives_the_window() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_with_menu();
|
||||
reveal_then(&mut s);
|
||||
right_click(&mut s, "ChatFrame1Tab");
|
||||
|
||||
// The shipped window is fully transparent at rest — the classic "text over the world".
|
||||
assert_eq!(
|
||||
s.eval::<f64>("local _,_,_,_,_,a = GetChatWindowInfo(1) return a")
|
||||
.unwrap(),
|
||||
0.0
|
||||
);
|
||||
// …so the reversed slider is seeded at its far end.
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return DropDownList1Button4.opacity")
|
||||
s.eval::<f64>("return DropDownList1Button6.opacity")
|
||||
.unwrap(),
|
||||
1.0,
|
||||
"info.opacity is 1 - a (the ref's own 'the slider is reversed')"
|
||||
);
|
||||
|
||||
left_click(&mut s, "DropDownList1Button4ColorSwatch");
|
||||
left_click(&mut s, "DropDownList1Button6ColorSwatch");
|
||||
assert!(
|
||||
s.eval::<bool>("return ColorPickerFrame:IsVisible()")
|
||||
.unwrap(),
|
||||
"the Background swatch opens the colour picker"
|
||||
);
|
||||
// The player's hand is on the picker now, off the chat frame: the hover's fade-out runs to
|
||||
// its end before the slider is touched, exactly as it does in real time.
|
||||
s.mouse_move(1500.0, 850.0);
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert!(
|
||||
s.eval::<bool>("return OpacitySliderFrame:IsVisible()")
|
||||
.unwrap(),
|
||||
@@ -230,11 +234,7 @@ fn the_background_row_opens_the_picker_and_its_opacity_slider_drives_the_window(
|
||||
1.0,
|
||||
"the slider opens seeded from the window"
|
||||
);
|
||||
|
||||
// Drag the thumb to 80% opaque. The slider's OnValueChanged is the live-preview hop.
|
||||
s.run("OpacitySliderFrame:SetValue(1 - 0.8)").unwrap();
|
||||
// **203, not 204** — and that is the reference, not a rounding bug of ours. Its setter is
|
||||
// `__ftol(x · 255.0)`, which TRUNCATES, and `0.8 × 255` is `203.99999999999997` in binary.
|
||||
let stored: f64 = s
|
||||
.eval("local _,_,_,_,_,a = GetChatWindowInfo(1) return a")
|
||||
.unwrap();
|
||||
@@ -242,15 +242,11 @@ fn the_background_row_opens_the_picker_and_its_opacity_slider_drives_the_window(
|
||||
(stored - 203.0 / 255.0).abs() < 1e-9,
|
||||
"the drag reached the engine store, quantized to its byte: {stored}"
|
||||
);
|
||||
|
||||
// And the pixels follow: one FCF_OnUpdate tick with the cursor nowhere near the dock, and the
|
||||
// background wears the new base rather than fading back to nothing.
|
||||
//
|
||||
// **The paint takes the FLOAT the slider produced, not the byte the store kept** — that split
|
||||
// is the reference's, not ours: its `FCF_SetWindowAlpha` calls `SetAlpha(alpha)` with the raw
|
||||
// value and `SetChatWindowAlpha(id, alpha)` with the same one, and only the second quantizes.
|
||||
// The two reconcile at the next `FloatingChatFrame_Update`, i.e. the next login.
|
||||
s.run("FCF_OnUpdate(1.0)").unwrap();
|
||||
s.mouse_move(1500.0, 850.0);
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
let painted: f64 = s.eval("return ChatFrame1Background:GetAlpha()").unwrap();
|
||||
assert!(
|
||||
(painted - 0.8).abs() < 1e-6,
|
||||
@@ -278,11 +274,10 @@ fn the_shipped_window_still_fades_zero_to_a_quarter() {
|
||||
"at rest: invisible"
|
||||
);
|
||||
reveal_dock(&mut s);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return ChatFrame1Background:GetAlpha()")
|
||||
.unwrap(),
|
||||
0.25,
|
||||
"hovered: DEFAULT_CHATFRAME_ALPHA, exactly as before 1589"
|
||||
let alpha: f64 = s.eval("return ChatFrame1Background:GetAlpha()").unwrap();
|
||||
assert!(
|
||||
(alpha - 0.25).abs() < 1e-6,
|
||||
"hovered: DEFAULT_CHATFRAME_ALPHA — got {alpha}"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
@@ -294,29 +289,40 @@ fn the_shipped_window_still_fades_zero_to_a_quarter() {
|
||||
fn the_font_size_submenu_resizes_the_window_and_stores_the_pick() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_with_menu();
|
||||
reveal_then(&mut s);
|
||||
right_click(&mut s, "ChatFrame1Tab");
|
||||
|
||||
// Open the submenu the way a player does — hover the `hasArrow` row (UIDropDownMenu.xml's
|
||||
// row OnEnter → `UIDropDownMenuRow_OpenSubmenu`).
|
||||
hover(&mut s, "DropDownList1Button3");
|
||||
hover(&mut s, "DropDownList1Button5");
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return DropDownList2.numButtons").unwrap(),
|
||||
4,
|
||||
"CHAT_FONT_HEIGHTS is 12, 14, 16, 18"
|
||||
);
|
||||
// The rows come from `for index, value in CHAT_FONT_HEIGHTS` — a `next` walk over a table
|
||||
// built with explicit `[n] =` keys, whose order is the VM's hash order and not necessarily
|
||||
// ascending; the test reads the rows by their values rather than assuming one.
|
||||
let row_with = |s: &UiScript, pt: i64| -> String {
|
||||
s.eval::<String>(&format!(
|
||||
"for i = 1, DropDownList2.numButtons do \
|
||||
local b = getglobal('DropDownList2Button'..i) \
|
||||
if b.value == {pt} then return b:GetName() end \
|
||||
end return ''"
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList2Button1:GetText()")
|
||||
s.eval::<String>(&format!("return {}:GetText()", row_with(&s, 12)))
|
||||
.unwrap(),
|
||||
"12 pt"
|
||||
"12 pt",
|
||||
"FONT_SIZE_TEMPLATE over the value"
|
||||
);
|
||||
// ChatFontNormal is 14 px, so the second row is the one already ticked.
|
||||
let ticked = row_with(&s, 14);
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList2Button2Check:IsVisible()")
|
||||
s.eval::<bool>(&format!("return {ticked}Check:IsVisible()"))
|
||||
.unwrap(),
|
||||
"the tick follows the font the frame is actually wearing"
|
||||
"the tick follows the font the frame is actually wearing (ChatFontNormal, 14)"
|
||||
);
|
||||
|
||||
left_click(&mut s, "DropDownList2Button3"); // 16 pt
|
||||
let sixteen = row_with(&s, 16);
|
||||
left_click(&mut s, &sixteen);
|
||||
let (_, height): (String, f64) = s
|
||||
.eval("local f, h = ChatFrame1:GetFont() return f, h")
|
||||
.unwrap();
|
||||
@@ -337,12 +343,17 @@ fn the_font_size_submenu_resizes_the_window_and_stores_the_pick() {
|
||||
fn each_tabs_menu_writes_its_own_window() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_with_menu();
|
||||
reveal_then(&mut s);
|
||||
right_click(&mut s, "ChatFrame2Tab");
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return FCF_GetCurrentChatFrameID()").unwrap(),
|
||||
2
|
||||
);
|
||||
left_click(&mut s, "DropDownList1Button4ColorSwatch");
|
||||
// A docked non-default window's menu carries Close before Display, so Background is row 7.
|
||||
assert!(s
|
||||
.eval::<bool>("return DropDownList1Button7:GetText() == BACKGROUND")
|
||||
.unwrap());
|
||||
left_click(&mut s, "DropDownList1Button7ColorSwatch");
|
||||
s.run("OpacitySliderFrame:SetValue(0)").unwrap(); // reversed: 0 = fully opaque
|
||||
assert_eq!(
|
||||
s.eval::<f64>("local _,_,_,_,_,a = GetChatWindowInfo(2) return a")
|
||||
@@ -365,15 +376,14 @@ fn each_tabs_menu_writes_its_own_window() {
|
||||
fn cancelling_the_picker_puts_the_window_back() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_with_menu();
|
||||
// Start from a non-default look so "restored" is distinguishable from "reset".
|
||||
s.run("FCF_SetWindowAlpha(ChatFrame1, 0.4) FCF_SetWindowColor(ChatFrame1, 0.2, 0.4, 0.6)")
|
||||
.unwrap();
|
||||
let before: f64 = s
|
||||
.eval("local _,_,_,_,_,a = GetChatWindowInfo(1) return a")
|
||||
.unwrap();
|
||||
|
||||
reveal_then(&mut s);
|
||||
right_click(&mut s, "ChatFrame1Tab");
|
||||
left_click(&mut s, "DropDownList1Button4ColorSwatch");
|
||||
left_click(&mut s, "DropDownList1Button6ColorSwatch");
|
||||
s.run("OpacitySliderFrame:SetValue(0)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("local _,_,_,_,_,a = GetChatWindowInfo(1) return a")
|
||||
@@ -381,7 +391,6 @@ fn cancelling_the_picker_puts_the_window_back() {
|
||||
1.0,
|
||||
"the drag previewed live"
|
||||
);
|
||||
|
||||
left_click(&mut s, "ColorPickerCancelButton");
|
||||
let after: f64 = s
|
||||
.eval("local _,_,_,_,_,a = GetChatWindowInfo(1) return a")
|
||||
@@ -411,10 +420,20 @@ fn the_restore_event_repaints_the_window_from_the_store() {
|
||||
font_size: 18,
|
||||
locked: true,
|
||||
docked: Some(1),
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
// The reference paints colour, alpha and lock from the record on the FIRST
|
||||
// `UPDATE_CHAT_WINDOWS` a frame sees (`FloatingChatFrame_Update`'s `not isInitialized`
|
||||
// gate) — a login, which is when the app restores the file. The helper already fired that
|
||||
// one, so this is the frame meeting the event fresh, as at the next login.
|
||||
s.run("ChatFrame1.isInitialized = nil").unwrap();
|
||||
s.fire_event("UPDATE_CHAT_WINDOWS", vec![]);
|
||||
s.run("BenillaFCF.hover = false FCF_OnUpdate(1.0)").unwrap();
|
||||
s.mouse_move(1500.0, 850.0);
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return ChatFrame1Background:GetAlpha()")
|
||||
.unwrap(),
|
||||
@@ -425,14 +444,9 @@ fn the_restore_event_repaints_the_window_from_the_store() {
|
||||
.eval("return ChatFrame1Background:GetVertexColor()")
|
||||
.unwrap();
|
||||
assert_eq!((r, g, b), (1.0, 0.0, 0.0), "and the restored tint");
|
||||
let (_, height): (String, f64) = s
|
||||
.eval("local f, h = ChatFrame1:GetFont() return f, h")
|
||||
.unwrap();
|
||||
assert_eq!(height, 18.0, "and the restored font size");
|
||||
// Restoring is not a player edit: nothing was queued back at the host.
|
||||
assert!(
|
||||
s.take_chat_window_changes().is_empty(),
|
||||
"the restore path does not dirty the file it came from"
|
||||
);
|
||||
// The restore paints through the `doNotSave` arms, but the reference's dock pass that follows
|
||||
// (`FCF_DockFrame` → `FCF_SaveDock` → `SetChatWindowDocked`) is a real write — the DOCKED 1 /
|
||||
// DOCKED 2 every stock file carries are FrameXML's, not the loader's.
|
||||
assert_eq!(s.take_chat_window_changes(), vec![0, 1]);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -28,10 +28,23 @@ fn chat_ui() -> UiScript {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\ColorPickerFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
// `FCF_ValidateChatFramePosition` (a tab-drag stop) reads `MainMenuBar:GetHeight()`; the
|
||||
// bar's own load-time chain precedes it, as in the action-bar harness.
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
super::fire_chat_login(&mut s);
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
s.resolve();
|
||||
s
|
||||
@@ -78,6 +91,29 @@ fn left_click(s: &mut UiScript, frame: &str) {
|
||||
}
|
||||
|
||||
/// Press a grip and walk the cursor to `(x, y)` — the real gesture, without releasing.
|
||||
/// The tabs ship hidden; a stationary hover over the dock reveals them (`FCF_OnUpdate`), and
|
||||
/// the reference moves a window by dragging its TAB (ChatTabTemplate's OnDragStart →
|
||||
/// `StartMoving`), so every drag here starts with the reveal.
|
||||
fn reveal(s: &mut UiScript) {
|
||||
let (cx, cy) = centre(s, "ChatFrame1");
|
||||
s.mouse_move(cx, cy);
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drag `ChatFrame1Tab` by `dx, dy` — the reference's move gesture — and release.
|
||||
fn drag_tab(s: &mut UiScript, dx: f32, dy: f32) {
|
||||
reveal(s);
|
||||
let (tx, ty) = centre(s, "ChatFrame1Tab");
|
||||
s.mouse_button(tx, ty, "LeftButton", true);
|
||||
s.mouse_move(tx + 5.0, ty); // past the 4px threshold: OnDragStart -> StartMoving
|
||||
s.mouse_move(tx + dx, ty + dy);
|
||||
s.mouse_button(tx + dx, ty + dy, "LeftButton", false);
|
||||
s.resolve();
|
||||
}
|
||||
|
||||
fn grab_grip(s: &mut UiScript, grip: &str) -> (f32, f32) {
|
||||
let (x, y) = centre(s, grip);
|
||||
s.mouse_button(x, y, "LeftButton", true);
|
||||
@@ -98,17 +134,14 @@ fn both_dock_windows_ship_locked() {
|
||||
))
|
||||
.unwrap(),
|
||||
1,
|
||||
"window {i} ships locked (the stock chat-cache row)"
|
||||
"window {i} ships locked (the boot init's LOCKED 1)"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>(&format!("return ChatFrame{i}.isLocked ~= nil"))
|
||||
.unwrap(),
|
||||
"…and the frame was seated from it"
|
||||
"…and the frame was seated from it by FloatingChatFrame_Update"
|
||||
);
|
||||
}
|
||||
// The blanket switch ships OFF — the reference's own initialiser, not its checkbox table's
|
||||
// disagreeing `default = "1"`.
|
||||
assert_eq!(s.eval::<String>("return CHAT_LOCKED").unwrap(), "0");
|
||||
assert!(s
|
||||
.eval::<Option<i64>>("return FCF_Get_ChatLocked()")
|
||||
.unwrap()
|
||||
@@ -123,20 +156,17 @@ fn both_dock_windows_ship_locked() {
|
||||
fn the_tab_menus_lock_row_toggles_the_window_and_flips_its_label() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
reveal(&mut s);
|
||||
right_click(&mut s, "ChatFrame1Tab");
|
||||
// Shipped locked, so the row offers the way OUT of it.
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button1:GetText()")
|
||||
.unwrap(),
|
||||
"Unlock Window"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>("return DropDownList1Button1:GetText() == UNLOCK_WINDOW")
|
||||
.unwrap());
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return DropDownList1Button1.notCheckable")
|
||||
.unwrap(),
|
||||
1,
|
||||
"a verb, not a tick"
|
||||
);
|
||||
|
||||
left_click(&mut s, "DropDownList1Button1");
|
||||
assert!(
|
||||
s.eval::<Option<i64>>("return ChatFrame1.isLocked")
|
||||
@@ -150,14 +180,10 @@ fn the_tab_menus_lock_row_toggles_the_window_and_flips_its_label() {
|
||||
.is_none(),
|
||||
"…and wrote it through SetChatWindowLocked, so it can be persisted"
|
||||
);
|
||||
|
||||
// Re-open: the same row is now the other verb.
|
||||
right_click(&mut s, "ChatFrame1Tab");
|
||||
assert_eq!(
|
||||
s.eval::<String>("return DropDownList1Button1:GetText()")
|
||||
.unwrap(),
|
||||
"Lock Window"
|
||||
);
|
||||
assert!(s
|
||||
.eval::<bool>("return DropDownList1Button1:GetText() == LOCK_WINDOW")
|
||||
.unwrap());
|
||||
left_click(&mut s, "DropDownList1Button1");
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ChatFrame1.isLocked").unwrap(),
|
||||
@@ -176,39 +202,21 @@ fn a_locked_window_refuses_a_grip_drag() {
|
||||
let mut s = chat_ui();
|
||||
let before = width(&mut s);
|
||||
assert_eq!(before, 430.0, "the authored width");
|
||||
|
||||
// The grip is where it should be — this is not a test that passes because nothing is there.
|
||||
// The reference's grips take the mouse whatever the lock says — the refusal is
|
||||
// `FCF_Resize`'s `isLocked` gate, not a disabled button.
|
||||
assert!(s
|
||||
.eval::<bool>("return ChatFrame1ResizeBottomRight ~= nil")
|
||||
.eval::<bool>("return ChatFrame1ResizeBottomRight:IsMouseEnabled()")
|
||||
.unwrap());
|
||||
assert!(
|
||||
!s.eval::<bool>("return ChatFrame1ResizeBottomRight:IsMouseEnabled()")
|
||||
.unwrap(),
|
||||
"a locked window's grips are click-through"
|
||||
);
|
||||
// The press falls past the disabled button — to whatever is behind it, which at this corner
|
||||
// is the world (the grip straddles the window's edge and its centre sits just outside).
|
||||
let (x, y) = centre(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
assert_ne!(
|
||||
s.hit_test_name(x, y).as_deref(),
|
||||
Some("ChatFrame1ResizeBottomRight"),
|
||||
"a disabled grip does not eat the click"
|
||||
);
|
||||
|
||||
grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
let (x, y) = grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
s.mouse_move(x + 60.0, y - 40.0);
|
||||
assert_eq!(width(&mut s), before, "a locked window does not resize");
|
||||
|
||||
// …and the direct call refuses too, which is the reference's own gate rather than ours.
|
||||
s.run("ChatFrame1ResizeBottomRight:EnableMouse(true)")
|
||||
.unwrap();
|
||||
grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
s.mouse_move(x + 60.0, y - 40.0);
|
||||
assert_eq!(
|
||||
width(&mut s),
|
||||
before,
|
||||
"FCF_Resize's isLocked gate refuses even a live grip"
|
||||
assert!(
|
||||
s.eval::<Option<i64>>("return ChatFrame1.resizing")
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"FCF_Resize returned before StartSizing"
|
||||
);
|
||||
s.mouse_button(x + 60.0, y - 40.0, "LeftButton", false);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -299,35 +307,29 @@ fn the_release_ends_the_resize_from_anywhere_on_screen() {
|
||||
/// gesture has to cross the drag threshold before anything moves, which is what keeps 0843's
|
||||
/// held-spell dismissal working on the same button.
|
||||
#[test]
|
||||
fn the_body_drag_moves_an_unlocked_window_and_a_locked_one_stays_put() {
|
||||
fn the_tab_drag_moves_an_unlocked_window_and_a_locked_one_stays_put() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
let (cx, cy) = centre(&mut s, "ChatFrame1");
|
||||
let start = left(&mut s);
|
||||
|
||||
// Locked: not drag-registered at all, so the gesture never arms.
|
||||
s.mouse_button(cx, cy, "LeftButton", true);
|
||||
s.mouse_move(cx + 80.0, cy);
|
||||
s.mouse_button(cx + 80.0, cy, "LeftButton", false);
|
||||
assert_eq!(left(&mut s), start, "a locked window does not drag");
|
||||
|
||||
drag_tab(&mut s, 80.0, 0.0);
|
||||
assert_eq!(
|
||||
left(&mut s),
|
||||
start,
|
||||
"a locked window does not drag — ChatTabTemplate's OnDragStart returns on isLocked"
|
||||
);
|
||||
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
|
||||
let (cx, cy) = centre(&mut s, "ChatFrame1");
|
||||
s.mouse_button(cx, cy, "LeftButton", true);
|
||||
s.mouse_move(cx + 5.0, cy); // past the 4px threshold: OnDragStart -> StartMoving
|
||||
s.mouse_move(cx + 80.0, cy);
|
||||
// **75, not 80.** The gesture's first 5px are spent crossing the drag threshold, and
|
||||
// `StartMoving` samples the cursor where it is when `OnDragStart` fires — so the window follows
|
||||
// from *there*, not from the press. That is the engine's own shape (`cursor::maybe_start_drag`
|
||||
// runs after the move pump in the same `mouse_move`), and it is what every drag-to-move frame
|
||||
// in the client does.
|
||||
reveal(&mut s);
|
||||
let (tx, ty) = centre(&mut s, "ChatFrame1Tab");
|
||||
s.mouse_button(tx, ty, "LeftButton", true);
|
||||
s.mouse_move(tx + 5.0, ty);
|
||||
s.mouse_move(tx + 80.0, ty);
|
||||
assert_eq!(
|
||||
left(&mut s),
|
||||
start + 75.0,
|
||||
"the window followed the cursor from where the drag started"
|
||||
);
|
||||
s.mouse_button(cx + 80.0, cy, "LeftButton", false);
|
||||
s.mouse_move(cx + 400.0, cy);
|
||||
s.mouse_button(tx + 80.0, ty, "LeftButton", false);
|
||||
s.mouse_move(tx + 400.0, ty);
|
||||
assert_eq!(
|
||||
left(&mut s),
|
||||
start + 75.0,
|
||||
@@ -343,20 +345,14 @@ fn the_body_drag_moves_an_unlocked_window_and_a_locked_one_stays_put() {
|
||||
fn a_moved_window_is_user_placed_and_the_managed_pass_skips_it() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
// The managed pass owns the seat to begin with — the control for the assertion below.
|
||||
s.run("UIParent_ManageFramePositions()").unwrap();
|
||||
let seated = left(&mut s);
|
||||
assert!(
|
||||
!s.eval::<bool>("return ChatFrame1:IsUserPlaced()").unwrap(),
|
||||
"nothing has placed it yet"
|
||||
);
|
||||
|
||||
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
|
||||
let (cx, cy) = centre(&mut s, "ChatFrame1");
|
||||
s.mouse_button(cx, cy, "LeftButton", true);
|
||||
s.mouse_move(cx + 5.0, cy);
|
||||
s.mouse_move(cx + 120.0, cy);
|
||||
s.mouse_button(cx + 120.0, cy, "LeftButton", false);
|
||||
drag_tab(&mut s, 120.0, 0.0);
|
||||
let moved = left(&mut s);
|
||||
assert_eq!(
|
||||
moved,
|
||||
@@ -367,7 +363,6 @@ fn a_moved_window_is_user_placed_and_the_managed_pass_skips_it() {
|
||||
s.eval::<bool>("return ChatFrame1:IsUserPlaced()").unwrap(),
|
||||
"the drag set the userPlaced bit"
|
||||
);
|
||||
|
||||
s.run("UIParent_ManageFramePositions()").unwrap();
|
||||
assert_eq!(
|
||||
left(&mut s),
|
||||
@@ -390,12 +385,7 @@ fn the_geometry_round_trips_through_the_save_file() {
|
||||
let (gx, gy) = grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
s.mouse_move(gx + 70.0, gy - 40.0);
|
||||
s.mouse_button(gx + 70.0, gy - 40.0, "LeftButton", false);
|
||||
let (cx, cy) = centre(&mut s, "ChatFrame1");
|
||||
s.mouse_button(cx, cy, "LeftButton", true);
|
||||
s.mouse_move(cx + 5.0, cy + 5.0);
|
||||
s.mouse_move(cx + 90.0, cy + 60.0);
|
||||
s.mouse_button(cx + 90.0, cy + 60.0, "LeftButton", false);
|
||||
s.resolve();
|
||||
drag_tab(&mut s, 90.0, 60.0);
|
||||
|
||||
let want = (
|
||||
width(&mut s),
|
||||
@@ -452,8 +442,11 @@ fn the_geometry_round_trips_through_the_save_file() {
|
||||
fn the_grip_art_follows_the_windows_reveal_and_tint() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
// Off-hover, at the shipped alpha-0 base: everything in the set is invisible.
|
||||
s.run("BenillaFCF.hover = false FCF_OnUpdate(1.0)").unwrap();
|
||||
s.mouse_move(1500.0, 850.0);
|
||||
for _ in 0..4 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
for piece in ["Background", "ResizeBottomRightTexture", "ResizeTopTexture"] {
|
||||
assert_eq!(
|
||||
s.eval::<f64>(&format!("return ChatFrame1{piece}:GetAlpha()"))
|
||||
@@ -462,24 +455,16 @@ fn the_grip_art_follows_the_windows_reveal_and_tint() {
|
||||
"{piece} is invisible at rest"
|
||||
);
|
||||
}
|
||||
|
||||
// Park the cursor in the window and let the stationary delay and the ramp run out.
|
||||
let (cx, cy) = centre(&mut s, "ChatFrame1");
|
||||
s.mouse_move(cx, cy);
|
||||
for _ in 0..25 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
reveal(&mut s);
|
||||
for piece in ["Background", "ResizeBottomRightTexture", "ResizeTopTexture"] {
|
||||
assert_eq!(
|
||||
s.eval::<f64>(&format!("return ChatFrame1{piece}:GetAlpha()"))
|
||||
.unwrap(),
|
||||
0.25,
|
||||
"{piece} reveals with the box (DEFAULT_CHATFRAME_ALPHA)"
|
||||
let alpha: f64 = s
|
||||
.eval(&format!("return ChatFrame1{piece}:GetAlpha()"))
|
||||
.unwrap();
|
||||
assert!(
|
||||
(alpha - 0.25).abs() < 1e-6,
|
||||
"{piece} reveals with the box (DEFAULT_CHATFRAME_ALPHA): {alpha}"
|
||||
);
|
||||
}
|
||||
|
||||
// And the tab menu's tint reaches them too — one list, one colour.
|
||||
s.run("FCF_SetWindowColor(ChatFrame1, 1, 0, 0)").unwrap();
|
||||
let (r, g, b): (f64, f64, f64) = s
|
||||
.eval("return ChatFrame1ResizeTopTexture:GetVertexColor()")
|
||||
@@ -496,24 +481,38 @@ fn a_drag_in_flight_holds_the_chrome_visible() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
|
||||
reveal(&mut s);
|
||||
grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
// Drag the cursor right out of the window's hover rect. Everything the fade normally keys on
|
||||
// now says "conceal": the cursor is nowhere near the dock, so only `chatFrame.resizing` can
|
||||
// hold the grips on screen.
|
||||
s.mouse_move(1500.0, 820.0);
|
||||
s.run("FCF_OnUpdate(1.0)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return BenillaFCF.reveal").unwrap(),
|
||||
1.0,
|
||||
"the resize holds the reveal open with the cursor off the window"
|
||||
s.eval::<i64>("return ChatFrame1.resizing").unwrap(),
|
||||
1,
|
||||
"FCF_Resize marked the frame"
|
||||
);
|
||||
s.mouse_move(1500.0, 820.0);
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
let alpha: f64 = s.eval("return ChatFrame1Background:GetAlpha()").unwrap();
|
||||
assert!(
|
||||
(alpha - 0.25).abs() < 1e-6,
|
||||
"the resize holds the reveal open with the cursor off the window (`or chatFrame.resizing`): {alpha}"
|
||||
);
|
||||
// The falsifier is the release: with the drag over, the same cursor position conceals.
|
||||
s.mouse_button(1500.0, 820.0, "LeftButton", false);
|
||||
s.run("FCF_OnUpdate(1.0)").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return BenillaFCF.reveal").unwrap(),
|
||||
0.0,
|
||||
"and lets go when the drag ends"
|
||||
assert!(
|
||||
s.eval::<Option<i64>>("return ChatFrame1.resizing")
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"FCF_StopResize cleared it"
|
||||
);
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
let alpha: f64 = s.eval("return ChatFrame1Background:GetAlpha()").unwrap();
|
||||
assert!(
|
||||
alpha < 1e-6,
|
||||
"and the box fades once the drag ends: {alpha}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -522,30 +521,24 @@ fn a_drag_in_flight_holds_the_chrome_visible() {
|
||||
#[test]
|
||||
fn the_chat_locked_global_overrides_the_per_window_lock() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let s = chat_ui();
|
||||
let mut s = chat_ui();
|
||||
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return FCF_CanMove(ChatFrame1) == 1")
|
||||
.unwrap());
|
||||
|
||||
let before = width(&mut s);
|
||||
s.run("FCF_Set_ChatLocked(1)").unwrap();
|
||||
assert_eq!(s.eval::<String>("return CHAT_LOCKED").unwrap(), "1");
|
||||
assert!(
|
||||
s.eval::<Option<i64>>("return FCF_CanMove(ChatFrame1)")
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"the blanket switch wins over an unlocked window"
|
||||
let (x, y) = grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
s.mouse_move(x + 50.0, y - 30.0);
|
||||
assert_eq!(
|
||||
width(&mut s),
|
||||
before,
|
||||
"the blanket switch wins over an unlocked window — FCF_Resize's first gate"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return ChatFrame1ResizeBottomRight:IsMouseEnabled()")
|
||||
.unwrap(),
|
||||
"…and takes the edges back"
|
||||
);
|
||||
|
||||
s.mouse_button(x + 50.0, y - 30.0, "LeftButton", false);
|
||||
s.run("FCF_Set_ChatLocked(nil)").unwrap();
|
||||
assert!(s
|
||||
.eval::<bool>("return FCF_CanMove(ChatFrame1) == 1")
|
||||
.unwrap());
|
||||
let (x, y) = grab_grip(&mut s, "ChatFrame1ResizeBottomRight");
|
||||
s.mouse_move(x + 50.0, y - 30.0);
|
||||
assert_eq!(width(&mut s), before + 50.0, "and lets go when cleared");
|
||||
s.mouse_button(x + 50.0, y - 30.0, "LeftButton", false);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -556,24 +549,22 @@ fn the_chat_locked_global_overrides_the_per_window_lock() {
|
||||
fn the_docked_combat_log_cannot_be_moved_or_resized_on_its_own() {
|
||||
let _data = benilla_formats::wow_data_or_skip!();
|
||||
let mut s = chat_ui();
|
||||
s.run("FCF_SetLocked(ChatFrame2, nil)").unwrap();
|
||||
assert!(
|
||||
s.eval::<Option<i64>>("return FCF_CanMove(ChatFrame2)")
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"a docked window that is not the default one owns no geometry"
|
||||
);
|
||||
assert!(!s
|
||||
.eval::<bool>("return ChatFrame2ResizeBottomRight:IsMouseEnabled()")
|
||||
.unwrap());
|
||||
// …and it still follows window 1 when window 1 does move.
|
||||
s.run("FCF_SetLocked(ChatFrame1, nil)").unwrap();
|
||||
let (cx, cy) = centre(&mut s, "ChatFrame1");
|
||||
s.mouse_button(cx, cy, "LeftButton", true);
|
||||
s.mouse_move(cx + 5.0, cy);
|
||||
s.mouse_move(cx + 60.0, cy);
|
||||
s.mouse_button(cx + 60.0, cy, "LeftButton", false);
|
||||
s.run("FCF_SelectDockFrame(ChatFrame2) FCF_SetLocked(ChatFrame2, nil)")
|
||||
.unwrap();
|
||||
s.resolve();
|
||||
let before: f64 = s.eval("return ChatFrame2:GetWidth()").unwrap();
|
||||
let (x, y) = grab_grip(&mut s, "ChatFrame2ResizeBottomRight");
|
||||
s.mouse_move(x + 50.0, y - 30.0);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return ChatFrame2:GetWidth()").unwrap(),
|
||||
before,
|
||||
"a docked window that is not the default one owns no geometry — FCF_Resize's third gate"
|
||||
);
|
||||
s.mouse_button(x + 50.0, y - 30.0, "LeftButton", false);
|
||||
s.run("FCF_SelectDockFrame(ChatFrame1) FCF_SetLocked(ChatFrame1, nil)")
|
||||
.unwrap();
|
||||
drag_tab(&mut s, 60.0, 0.0);
|
||||
let one: f64 = s.eval("return ChatFrame1:GetLeft()").unwrap();
|
||||
let two: f64 = s.eval("return ChatFrame2:GetLeft()").unwrap();
|
||||
assert_eq!(one, two, "the dock moved as one");
|
||||
|
||||
@@ -23,7 +23,25 @@ fn chat_frame() -> UiScript {
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||||
// The UIParent slice (RaiseFrameLevel, MouseIsOver, the fade and flash kits) ahead of the
|
||||
// bar and the chat files; `FCF_OnUpdate` — the dock's hover, fade and drag driver — runs
|
||||
// from UIParent's OnUpdate, as in the reference.
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
// `FCF_ValidateChatFramePosition` (a tab-drag stop) reads `MainMenuBar:GetHeight()`; the bar's
|
||||
// own load-time chain precedes it, as in the action-bar harness.
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.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");
|
||||
super::fire_chat_login(&mut s);
|
||||
s.set_screen_size(1600.0, 900.0);
|
||||
s.resolve();
|
||||
s
|
||||
@@ -110,24 +128,19 @@ fn newest_line_sits_at_the_bottom() {
|
||||
#[test]
|
||||
fn wheel_scroll_re_arms_the_fade_then_freezes_it() {
|
||||
let mut s = chat_frame();
|
||||
// Short fade so a tick visibly ramps: straight into phase 2 (timeVisible 0), 4s ramp.
|
||||
s.run("ChatFrame1:SetTimeVisible(0); ChatFrame1:SetFadeDuration(4)")
|
||||
.unwrap();
|
||||
for t in ["L0", "L1", "L2"] {
|
||||
s.add_chat_message("ChatFrame1", t, 1.0, 1.0, 1.0);
|
||||
}
|
||||
s.resolve();
|
||||
|
||||
// One tick at the bottom ramps every line's alpha down.
|
||||
s.tick(1.0);
|
||||
s.resolve();
|
||||
let a1 = text_color(&s.extract(), "L1").expect("L1 visible")[3];
|
||||
assert!(a1 < 1.0 && a1 > 0.0, "the line faded partway: {a1}");
|
||||
|
||||
// Scroll up via the wheel (a point inside the docked frame: BOTTOMLEFT (32,85), 430x160). The
|
||||
// Lua OnMouseWheel handler calls ScrollUp, which re-arms every displayed line to full alpha —
|
||||
// and, no longer AtBottom, the tick then holds it there.
|
||||
s.mouse_wheel(100.0, 150.0, 1.0);
|
||||
// The scroll verb is what the reference's scroll buttons call; the 1.12 chat frame takes
|
||||
// no wheel of its own (`enableMouse="false"`, no OnMouseWheel).
|
||||
s.run("ChatFrame1:ScrollUp()").unwrap();
|
||||
s.resolve();
|
||||
let a2 = text_color(&s.extract(), "L1").expect("L1 still visible")[3];
|
||||
assert!(close(a2, 1.0), "the scroll brought the line back: {a2}");
|
||||
@@ -135,9 +148,7 @@ fn wheel_scroll_re_arms_the_fade_then_freezes_it() {
|
||||
s.resolve();
|
||||
let a3 = text_color(&s.extract(), "L1").expect("L1 still visible")[3];
|
||||
assert!(close(a3, 1.0), "frozen while scrolled up: {a3}");
|
||||
|
||||
// Wheel back down to the bottom — that scroll re-arms too, so the ramp restarts from full.
|
||||
s.mouse_wheel(100.0, 150.0, -1.0);
|
||||
s.run("ChatFrame1:ScrollToBottom()").unwrap();
|
||||
s.tick(1.0);
|
||||
s.resolve();
|
||||
let a4 = text_color(&s.extract(), "L1").expect("L1 visible")[3];
|
||||
@@ -148,18 +159,25 @@ fn wheel_scroll_re_arms_the_fade_then_freezes_it() {
|
||||
#[test]
|
||||
fn input_editbox_enter_drains_the_typed_line() {
|
||||
let mut s = chat_frame();
|
||||
// ENTER opens (the app calls focus_editbox); typing goes to the focused box.
|
||||
assert!(s.focus_editbox("ChatFrameEditBox"), "the edit box focuses");
|
||||
assert!(s.has_keyboard_focus(), "focus gates the world's keys");
|
||||
s.char_input("/yell hi");
|
||||
// ENTER → OnEnterPressed → SubmitChatInput(GetText()) + SetText('') + ClearFocus + Hide.
|
||||
assert!(s.key_input("ENTER"), "the box consumes ENTER");
|
||||
assert_eq!(s.take_chat_input(), vec!["/yell hi".to_string()]);
|
||||
// The reference's own path: `ChatEdit_OnEnterPressed` → `ChatEdit_SendText` →
|
||||
// `ChatEdit_ParseText` → `SendChatMessage("hi", "YELL", …)`.
|
||||
let sends = s.take_chat_sends();
|
||||
assert_eq!(
|
||||
sends
|
||||
.iter()
|
||||
.map(|c| (c.text.as_str(), c.chat_type.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("hi", "YELL")]
|
||||
);
|
||||
assert!(
|
||||
!s.has_keyboard_focus(),
|
||||
"submit closes the box (ClearFocus + Hide)"
|
||||
"submit closes the box (ChatEdit_OnEscapePressed: ClearFocus + Hide)"
|
||||
);
|
||||
assert!(s.take_chat_input().is_empty(), "drained");
|
||||
assert!(s.take_chat_sends().is_empty(), "drained");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -182,12 +200,13 @@ fn chat_box_arrows_edit_and_history_recalls() {
|
||||
let mut s = chat_frame();
|
||||
assert!(s.focus_editbox("ChatFrameEditBox"));
|
||||
s.char_input("ab");
|
||||
// Plain LEFT must MOVE the caret (not be swallowed): a backspace after it deletes 'a', not 'b'.
|
||||
s.editbox_action(EditAction::Move {
|
||||
unit: EditUnit::Char,
|
||||
back: true,
|
||||
extend: false,
|
||||
});
|
||||
// The reference's ChatFrameEditBoxTemplate is `ignoreArrows="true"` — the engine's
|
||||
// AltArrowKeyMode, which the app's key routing reads to hand a plain arrow to the bindings
|
||||
// (the character turns while you type) rather than to the box. The box itself still edits.
|
||||
assert!(
|
||||
s.editbox_alt_arrow_mode(),
|
||||
"the stock template's ignoreArrows landed as AltArrowKeyMode on the focused box"
|
||||
);
|
||||
s.editbox_action(EditAction::Delete {
|
||||
unit: EditUnit::Char,
|
||||
back: true,
|
||||
@@ -195,30 +214,38 @@ fn chat_box_arrows_edit_and_history_recalls() {
|
||||
assert_eq!(
|
||||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||||
.unwrap(),
|
||||
"b",
|
||||
"plain arrows move the caret in the shipped chat box"
|
||||
"a"
|
||||
);
|
||||
|
||||
// Submit, then push the canonical line the router would (ui_chat::input::history_line) — the
|
||||
// asynchronous ChatEdit_AddHistory slot.
|
||||
s.run("ChatFrameEditBox:SetText('')").unwrap();
|
||||
s.char_input("/yell hi");
|
||||
assert!(s.key_input("ENTER"));
|
||||
assert_eq!(s.take_chat_input(), vec!["/yell hi".to_string()]);
|
||||
assert!(
|
||||
s.editbox_add_history("ChatFrameEditBox", "/yell hi"),
|
||||
"the seam finds the shipped box by name after it closed"
|
||||
// `ChatEdit_SendText` → `ChatEdit_ParseText` → the YELL type's send, and the reference's
|
||||
// `AddHistoryLine` on the way.
|
||||
let sends = s.take_chat_sends();
|
||||
assert_eq!(
|
||||
sends
|
||||
.iter()
|
||||
.map(|c| (c.text.as_str(), c.chat_type.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("hi", "YELL")]
|
||||
);
|
||||
|
||||
// Reopen (the app refocuses on the next Enter): Up recalls, Down restores the draft.
|
||||
assert!(s.focus_editbox("ChatFrameEditBox"));
|
||||
s.char_input("dra");
|
||||
s.editbox_action(EditAction::HistoryPrev);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||||
// `ChatEdit_AddHistory` filed `SLASH_YELL1 .. " " .. text` = "/y hi" (ChatFrame.lua
|
||||
// l.1913-1937); the recall lands it in the box, and the box's own `OnTextChanged` live parse
|
||||
// (`ChatEdit_ParseText(this, 0)`) turns it straight into the YELL type with "hi" — which is
|
||||
// what the player sees: a "Yell:" header and the text.
|
||||
assert!(
|
||||
s.eval::<bool>(
|
||||
"return ChatFrameEditBox.chatType == 'YELL' and ChatFrameEditBox:GetText() == 'hi'"
|
||||
)
|
||||
.unwrap(),
|
||||
"Up recalls the filed line, live-parsed: {:?} {:?}",
|
||||
s.eval::<String>("return ChatFrameEditBox.chatType")
|
||||
.unwrap(),
|
||||
"/yell hi",
|
||||
"Up recalls the submitted line"
|
||||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||||
.unwrap()
|
||||
);
|
||||
s.editbox_action(EditAction::HistoryNext);
|
||||
assert_eq!(
|
||||
@@ -239,19 +266,13 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
|
||||
use benilla_ui::script::{
|
||||
ContainerSlot, ContainerState, SpellBookState, SpellSlotView, SpellTabView,
|
||||
};
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The reference's spellbook wants the action-bar chain beneath it (1952); the chat window
|
||||
// loads after it, as in the manifest.
|
||||
let mut s = super::spellbook_tests::spellbook_ui(1024.0, 768.0);
|
||||
for f in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"SpellBookFrame.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
@@ -742,11 +763,9 @@ fn hovering_the_dock_still_reveals_the_tabs() {
|
||||
s.resolve();
|
||||
}
|
||||
assert!(
|
||||
s.eval::<f64>("return ChatFrame1Tab:GetAlpha()").unwrap() < 1e-6,
|
||||
"the dock starts concealed"
|
||||
!s.eval::<bool>("return ChatFrame1Tab:IsVisible()").unwrap(),
|
||||
"the dock starts concealed — the tab template ships hidden"
|
||||
);
|
||||
|
||||
// The dock's own centre, asked of the layout rather than hardcoded.
|
||||
let (x, y): (f32, f32) = s
|
||||
.eval(
|
||||
"return (ChatFrame1:GetLeft() + ChatFrame1:GetRight()) / 2, \
|
||||
@@ -754,17 +773,15 @@ fn hovering_the_dock_still_reveals_the_tabs() {
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(x, y);
|
||||
// 0.2 s stationary to arm the reveal, then the 0.15 s ramp — 25 frames covers both with room.
|
||||
for _ in 0..25 {
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
|
||||
let alpha: f64 = s.eval("return ChatFrame1Tab:GetAlpha()").unwrap();
|
||||
assert!(
|
||||
(alpha - 1.0).abs() < 1e-6,
|
||||
"a stationary hover reveals the selected tab at full alpha — got {alpha}"
|
||||
s.eval::<bool>("return ChatFrame1Tab:IsVisible()").unwrap() && (alpha - 1.0).abs() < 1e-6,
|
||||
"a stationary hover past CHAT_TAB_SHOW_DELAY reveals the selected tab at full alpha — got {alpha}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -787,8 +804,6 @@ fn a_chat_view_at_the_bottom_stops_rewriting_the_flash() {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
|
||||
// At the bottom, flash hidden: the sentinel must survive.
|
||||
s.run("ChatFrame1.flashTimer = 0.42").unwrap();
|
||||
for _ in 0..10 {
|
||||
s.tick(0.016);
|
||||
@@ -805,20 +820,17 @@ fn a_chat_view_at_the_bottom_stops_rewriting_the_flash() {
|
||||
.unwrap(),
|
||||
"the flash stays hidden at rest"
|
||||
);
|
||||
|
||||
// Control 1: scrolled up, the blink still runs — the timer accumulates past 0.5 and shows
|
||||
// the flash (a point inside the docked frame, as the wheel test uses).
|
||||
s.mouse_wheel(100.0, 150.0, 1.0);
|
||||
s.tick(0.3); // 0.42 + 0.3 = 0.72 >= 0.5 -> toggle on
|
||||
// The reference's ChatFrameTemplate takes no mouse and has no wheel script (1.12 scrolls its
|
||||
// chat by the buttons), so the scroll is the frame's own verb.
|
||||
s.run("ChatFrame1:ScrollUp()").unwrap();
|
||||
s.tick(0.3); // 0.42 + 0.3 = 0.72 >= CHAT_BUTTON_FLASH_TIME -> toggle on
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<bool>("return ChatFrame1BottomButtonFlash:IsVisible()")
|
||||
.unwrap(),
|
||||
"scrolled up, the bottom-button blink still lights"
|
||||
"scrolled up, the bottom-button blink lights"
|
||||
);
|
||||
|
||||
// Control 2: back to the bottom while the flash is LIT — the transition still writes.
|
||||
s.mouse_wheel(100.0, 150.0, -1.0);
|
||||
s.run("ChatFrame1:ScrollToBottom()").unwrap();
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
assert!(
|
||||
@@ -826,10 +838,12 @@ fn a_chat_view_at_the_bottom_stops_rewriting_the_flash() {
|
||||
.unwrap(),
|
||||
"returning to the bottom hides a lit flash"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return ChatFrame1.flashTimer").unwrap(),
|
||||
0.0,
|
||||
"and zeroes the timer with it (our reset rides inside the reference's gate)"
|
||||
// …and leaves the residual phase where it was: the reference's at-bottom arm hides the flash
|
||||
// and returns, it never zeroes the timer (ChatFrame.lua `ChatFrame_OnUpdate`).
|
||||
let timer: f64 = s.eval("return ChatFrame1.flashTimer").unwrap();
|
||||
assert!(
|
||||
(timer - 0.22).abs() < 1e-6,
|
||||
"the residual phase stands: {timer}"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
@@ -851,7 +865,6 @@ fn a_chat_view_at_the_bottom_stops_rewriting_the_flash() {
|
||||
#[test]
|
||||
fn selecting_the_combat_log_keeps_the_dock_driver_running() {
|
||||
let mut s = chat_frame();
|
||||
// Hover the dock and let the reveal settle: General at full, Combat Log at half.
|
||||
let (x, y): (f32, f32) = s
|
||||
.eval(
|
||||
"return (ChatFrame1:GetLeft() + ChatFrame1:GetRight()) / 2, \
|
||||
@@ -859,7 +872,7 @@ fn selecting_the_combat_log_keeps_the_dock_driver_running() {
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(x, y);
|
||||
for _ in 0..25 {
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
@@ -873,19 +886,16 @@ fn selecting_the_combat_log_keeps_the_dock_driver_running() {
|
||||
),
|
||||
"the hovered dock starts with General selected — this test must not pass vacuously"
|
||||
);
|
||||
|
||||
// Click the Combat Log tab, then advance the clock WITHOUT touching FCF_OnUpdate.
|
||||
s.run("BenillaFCF_TabClick(2)").unwrap();
|
||||
s.run("FCF_SelectDockFrame(ChatFrame2)").unwrap();
|
||||
for _ in 0..10 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
|
||||
assert!(
|
||||
s.eval::<bool>("return ChatFrame2:IsShown() and not ChatFrame1:IsShown()")
|
||||
.unwrap(),
|
||||
"the click swapped the windows"
|
||||
"the selection swapped the windows"
|
||||
);
|
||||
let (a1, a2): (f32, f32) = s
|
||||
.eval("return ChatFrame1Tab:GetAlpha(), ChatFrame2Tab:GetAlpha()")
|
||||
@@ -908,11 +918,11 @@ fn the_dock_still_fades_out_with_the_combat_log_selected() {
|
||||
)
|
||||
.unwrap();
|
||||
s.mouse_move(x, y);
|
||||
for _ in 0..25 {
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
s.run("BenillaFCF_TabClick(2)").unwrap();
|
||||
s.run("FCF_SelectDockFrame(ChatFrame2)").unwrap();
|
||||
for _ in 0..10 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
@@ -924,17 +934,16 @@ fn the_dock_still_fades_out_with_the_combat_log_selected() {
|
||||
),
|
||||
"revealed before the cursor leaves"
|
||||
);
|
||||
|
||||
s.mouse_move(1500.0, 850.0); // away from the dock
|
||||
for _ in 0..20 {
|
||||
for _ in 0..45 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
let a2: f32 = s.eval("return ChatFrame2Tab:GetAlpha()").unwrap();
|
||||
// The reference fades the tab out and `FCF_ChatTabFadeFinished` hides it.
|
||||
assert!(
|
||||
a2 < 1e-6,
|
||||
"the dock conceals itself again on leave — got Combat Log tab alpha {a2}"
|
||||
!s.eval::<bool>("return ChatFrame2Tab:IsVisible()").unwrap(),
|
||||
"the dock conceals itself again on leave"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -945,7 +954,7 @@ fn the_dock_still_fades_out_with_the_combat_log_selected() {
|
||||
#[test]
|
||||
fn the_combat_log_window_runs_its_own_bottom_button_blink() {
|
||||
let mut s = chat_frame();
|
||||
s.run("BenillaFCF_TabClick(2)").unwrap();
|
||||
s.run("FCF_SelectDockFrame(ChatFrame2)").unwrap();
|
||||
for i in 0..40 {
|
||||
s.add_chat_message("ChatFrame2", &format!("line {i}"), 1.0, 1.0, 1.0);
|
||||
}
|
||||
@@ -987,7 +996,14 @@ fn the_chat_menu_builds_its_rows_on_the_references_kit() {
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"GameTooltip.xml",
|
||||
r"Interface\FrameXML\UIMenu.xml",
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
@@ -1023,10 +1039,13 @@ fn the_chat_menu_builds_its_rows_on_the_references_kit() {
|
||||
.unwrap(),
|
||||
"the reference's row click hides the menu"
|
||||
);
|
||||
// `ChatMenu_Say` → `ChatMenu_SetChatType(chatFrame, "SAY")`: the box opens and its type is
|
||||
// set directly — no slash is typed into it (ChatFrame.lua l.2245-2255).
|
||||
assert!(
|
||||
s.eval::<String>("return ChatFrameEditBox:GetText()")
|
||||
.unwrap()
|
||||
.starts_with("/s"),
|
||||
"and it seeded the edit box with the row's slash"
|
||||
s.eval::<bool>(
|
||||
"return ChatFrameEditBox:IsVisible() and ChatFrameEditBox.chatType == 'SAY'"
|
||||
)
|
||||
.unwrap(),
|
||||
"and it opened the edit box as SAY"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,13 @@ const ROOM_UI: &[&str] = &[
|
||||
"Interface\\FrameXML\\StackSplitFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
];
|
||||
|
||||
/// The shipped files the dressing room's click sites need, in manifest order — **with no bag
|
||||
@@ -99,7 +105,18 @@ fn shown_paper_doll() -> UiScript {
|
||||
for file in CHARACTER_UI {
|
||||
super::test_ui::load_ui_strict(&s, file);
|
||||
}
|
||||
for file in ["DressUpFrame.xml", "ChatFrame.xml"] {
|
||||
for file in [
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
super::test_ui::load_ui_strict(&s, file);
|
||||
}
|
||||
s.set_unit(
|
||||
|
||||
@@ -148,7 +148,14 @@ fn escape_is_consumed_by_a_focused_editbox_and_leaves_windows_open() {
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||||
s.set_money(0);
|
||||
s.set_container(0, Some(one_item_backpack()));
|
||||
|
||||
@@ -237,7 +244,14 @@ fn escape_closes_an_open_stack_split_frame() {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
load_xml(&s, "Interface\\FrameXML\\MerchantFrame.xml"); // ContainerFrameItemButton_OnClick reads MerchantFrame
|
||||
load_xml(&s, "ChatFrame.xml"); // …and ChatFrameEditBox, the shift fork's first test
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml"); // …and ChatFrameEditBox, the shift fork's first test
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\StackSplitFrame.xml");
|
||||
s.set_money(0);
|
||||
|
||||
|
||||
@@ -180,97 +180,6 @@ const KNOWN: &[Known] = &[
|
||||
// because OUR `ScrollFrame` ctor took the mouse and the reference's does not. Correcting the
|
||||
// ctor list to the client's made both sides agree, and an accepted divergence that has been
|
||||
// fixed is documentation claiming a defect we do not have.
|
||||
Known {
|
||||
frame: "ChatFrame1",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame2",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame3",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame4",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame5",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame6",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
Known {
|
||||
frame: "ChatFrame7",
|
||||
flag: Flag::Mouse,
|
||||
why: "our chat window takes the mouse and the reference's does not. It was believed a \
|
||||
`<ScrollingMessageFrame>` takes it BY CONSTRUCTION; wow-re's follow-up round \
|
||||
(`68987021`) proved that ctor leaves the bit clear, and that a 1.12 chat link is \
|
||||
clickable through one synthesised `CSimpleHyperlinkButton` child per link span. Our \
|
||||
hit test now carries that law as a span disjunct, so the LINKS no longer need the \
|
||||
flag — but ChatFrame.xml still sets it, and a documented chain of choices hangs off \
|
||||
the old premise (no `SetFrameLevel-1` OnLoad because it would sink the resize grips \
|
||||
under a mouse-enabled parent; `OnClick` dismisses a held spell, 0843). Unwinding \
|
||||
that is the chat window's own piece of work, not this flag's.",
|
||||
},
|
||||
// The two `TargetofTarget*Bar` entries RETIRED here (the unit-frame migration), for the same
|
||||
// reason their twin the pet page's XP bar went: they said our bars were plain StatusBars where
|
||||
// the reference inherits `TextStatusBar` — true of our transcription, and no longer true of
|
||||
@@ -694,7 +603,10 @@ fn the_shipped_frames_carry_the_references_flags() {
|
||||
}
|
||||
|
||||
assert!(
|
||||
compared > 400,
|
||||
// 1948 retired our ChatFrame.xml and 1952 our SpellBookFrame.xml (their frames are the
|
||||
// reference's own now), which took the paired count from the low 400s to the low 360s;
|
||||
// the floor guards the pairing, not the census.
|
||||
compared > 350,
|
||||
"only {compared} frames compared — the pairing broke, and the sweep guards nothing"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -611,7 +611,14 @@ fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink, the shared shift-insert helper
|
||||
load_xml(&s, "DressUpFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||||
s.set_loot_rolls(rolls());
|
||||
|
||||
// Roll 8 (BoE Worn Shortsword) claims frame 1 — a non-BoP roll, so the dice below can land a
|
||||
|
||||
@@ -613,7 +613,14 @@ fn ctrl_and_shift_on_a_loot_row_preview_and_post_without_looting() {
|
||||
"Interface\\FrameXML\\LootFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
|
||||
@@ -1140,7 +1140,14 @@ fn ctrl_and_shift_on_a_vendor_row_preview_and_post_without_buying() {
|
||||
"Interface\\FrameXML\\MerchantFrame.xml",
|
||||
"DressUpFrame.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
|
||||
@@ -771,6 +771,33 @@ impl benilla_ui::script::TextMeasure for FixedWidthFont {
|
||||
#[cfg(test)]
|
||||
mod test_ui;
|
||||
|
||||
/// The chat loader's two login events (wow-re chat-cache-grammar.md §8; `ui_chat::settings`):
|
||||
/// `UPDATE_CHAT_WINDOWS` once, then `UPDATE_CHAT_COLOR` for every registry entry. The reference's
|
||||
/// `FloatingChatFrame_Update` docks, hides and colours the windows on the first and
|
||||
/// `ChatFrame_OnEvent` fills `ChatTypeInfo` from the second, so a test VM that loads the
|
||||
/// reference's chat files needs both before the windows look or route like a client's.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn fire_chat_login(s: &mut benilla_ui::script::UiScript) {
|
||||
// `FCF_OnUpdate` reads `UIOptionsFrame:IsShown()` every frame — the reference's options
|
||||
// window, which OptionsFrame.xml aliases and a chat test VM does not load. A hidden stand-in
|
||||
// is what a closed options window answers.
|
||||
s.run("if not UIOptionsFrame then UIOptionsFrame = CreateFrame('Frame') UIOptionsFrame:Hide() end")
|
||||
.expect("the options stand-in");
|
||||
s.fire_event("UPDATE_CHAT_WINDOWS", vec![]);
|
||||
let renorm = |b: u8| f64::from(b as f32 * (1.0f32 / 255.0f32));
|
||||
for entry in s.chat_colors() {
|
||||
s.fire_event(
|
||||
"UPDATE_CHAT_COLOR",
|
||||
vec![
|
||||
benilla_ui::script::ScriptValue::Str(entry.name),
|
||||
benilla_ui::script::ScriptValue::Number(renorm(entry.rgb[0])),
|
||||
benilla_ui::script::ScriptValue::Number(renorm(entry.rgb[1])),
|
||||
benilla_ui::script::ScriptValue::Number(renorm(entry.rgb[2])),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [`test_ui::load_ui`] for a test module OUTSIDE `ui_script` — `ui_action::feed_tests` drives the
|
||||
/// real `UIErrorsFrame` end to end and needs the same both-stores reader everything else uses.
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -796,7 +796,12 @@ fn chat_harness() -> UiScript {
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
],
|
||||
);
|
||||
harness_on(s)
|
||||
|
||||
@@ -112,7 +112,10 @@ fn setup() -> UiScript {
|
||||
load_xml(&s, "GameTooltip.xml"); // TOOLTIP_DEFAULT_COLOR, read by the dropdown backdrops
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml"); // ChatFrame's seven dropdowns inherit its template
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
|
||||
load_xml(&s, "ChatFrame.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\\GuildRegistrarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\PetitionFrame.xml");
|
||||
s
|
||||
|
||||
@@ -801,7 +801,11 @@ fn reward_rows_preview_and_post_without_selecting_the_choice() {
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\ChatFrame.xml"); // ChatFrameEditBox lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIPanelTemplates.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
|
||||
|
||||
const SWORD: &str = "|cffffffff|Hitem:2299:0:0:0|h[Worn Sword]|h|r";
|
||||
const MACE: &str = "|cffffffff|Hitem:2300:0:0:0|h[Worn Mace]|h|r";
|
||||
|
||||
@@ -343,7 +343,10 @@ fn shift_click_toggles_the_watch_checkbox_and_the_tracker_hud() {
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml");
|
||||
load_xml(&s, "ChatFrame.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());
|
||||
@@ -485,7 +488,10 @@ fn watch_guards_no_op_without_erroring() {
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIDropDownMenu.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml");
|
||||
load_xml(&s, "ChatFrame.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());
|
||||
@@ -1416,7 +1422,10 @@ fn reward_rows_preview_and_post_and_a_plain_click_stays_inert() {
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
load_xml(&s, "DressUpFrame.xml"); // DressUpItemLink lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||||
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();
|
||||
@@ -1507,7 +1516,10 @@ fn shift_click_on_a_title_posts_the_quest_name_with_chat_open_and_watches_with_i
|
||||
load_xml(&s, "Interface\\FrameXML\\QuestLogFrame.xml");
|
||||
load_xml(&s, "UIParent.xml"); // BenillaChatEdit_InsertLink lives here
|
||||
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit its menus build from
|
||||
load_xml(&s, "ChatFrame.xml"); // ChatFrameEditBox lives here
|
||||
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();
|
||||
|
||||
@@ -268,6 +268,47 @@ mod tests {
|
||||
/// block), Lua `--[[ … ]]` blocks, and XML `<!-- … -->` blocks. It does not track string
|
||||
/// literals, so a `"--"` inside a string truncates that line — which can only ever cause an
|
||||
/// UNDER-report, the safe direction for every reader of it.
|
||||
/// Blank the contents of every Lua string literal (`"…"`, `'…'`, `[[…]]`), keeping the quotes,
|
||||
/// so a pattern like `"/([^%s]+)%s(.*)"` cannot read as a call to `s`.
|
||||
fn strip_strings(text: &str) -> String {
|
||||
let b: Vec<char> = text.chars().collect();
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
let c = b[i];
|
||||
if c == '"' || c == '\'' {
|
||||
out.push(c);
|
||||
i += 1;
|
||||
while i < b.len() && b[i] != c && b[i] != '\n' {
|
||||
if b[i] == '\\' {
|
||||
i += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if i < b.len() && b[i] == c {
|
||||
out.push(c);
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if c == '[' && i + 1 < b.len() && b[i + 1] == '[' {
|
||||
out.push_str("[[");
|
||||
i += 2;
|
||||
while i + 1 < b.len() && !(b[i] == ']' && b[i + 1] == ']') {
|
||||
i += 1;
|
||||
}
|
||||
if i + 1 < b.len() {
|
||||
out.push_str("]]");
|
||||
i += 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push(c);
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn strip_comments(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
let mut in_xml = false;
|
||||
@@ -1455,6 +1496,13 @@ mod tests {
|
||||
update; our `inventory_alerts` snapshot is recomputed on every inventory push \
|
||||
instead, so the recompute exists and only the Lua verb that forces one does not.",
|
||||
),
|
||||
(
|
||||
"ChatFrame.xml",
|
||||
"value",
|
||||
"not a global: `for index, value in SlashCmdList do … value(msg)` \
|
||||
(ChatFrame.lua l.2168-2171) calls the addon's handler through the table's loop \
|
||||
variable. The scan has no scopes, so the name reads as a call.",
|
||||
),
|
||||
];
|
||||
|
||||
let toc = &super::super::addons::Addon::builtin().toc.files;
|
||||
@@ -1481,7 +1529,7 @@ mod tests {
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
let text = strip_comments(&text);
|
||||
let text = strip_strings(&strip_comments(&text));
|
||||
let defines: std::collections::HashSet<String> = text
|
||||
.lines()
|
||||
.filter_map(|l| l.trim_start().strip_prefix("function "))
|
||||
@@ -2022,7 +2070,6 @@ mod tests {
|
||||
// built; none is 1819-shaped, because no PAIR is split (a half-fired pair is the tell).
|
||||
const UNPRODUCED: &[(&str, &str)] = &[
|
||||
("BAG_OPEN", "ContainerFrame.lua"),
|
||||
("CHAT_MSG_RAID_WARNING", "RaidWarning.lua"),
|
||||
(
|
||||
"CVAR_UPDATE",
|
||||
"TextStatusBar.lua — a CVar change does not repaint the bar text",
|
||||
@@ -2036,6 +2083,12 @@ mod tests {
|
||||
flow works without it",
|
||||
),
|
||||
("ITEM_TEXT_TRANSLATION", "ItemTextFrame.lua"),
|
||||
(
|
||||
"LEARNED_SPELL_IN_TAB",
|
||||
"SpellBookFrame.lua — the tab flash when a new spell lands in it; the reference \
|
||||
fires it with the tab index on spell add, which benilla's spellbook feed does not \
|
||||
derive yet (1952)",
|
||||
),
|
||||
("PET_UI_CLOSE", "PetPaperDollFrame.lua"),
|
||||
("PET_UI_UPDATE", "PetPaperDollFrame.lua"),
|
||||
("PLAYER_DAMAGE_DONE_MODS", "PaperDollFrame.lua"),
|
||||
@@ -2058,6 +2111,11 @@ mod tests {
|
||||
"four files — the paperdoll model refresh",
|
||||
),
|
||||
("UNIT_PORTRAIT_UPDATE", "three files — the portrait refresh"),
|
||||
(
|
||||
"ZONE_UNDER_ATTACK",
|
||||
"ChatFrame.lua — the reference's `SMSG_ZONE_UNDER_ATTACK` line (\"%s is under \
|
||||
attack!\"); the wire handler is not built (1948)",
|
||||
),
|
||||
];
|
||||
|
||||
let mut fired: std::collections::HashSet<String> =
|
||||
@@ -2067,6 +2125,50 @@ mod tests {
|
||||
.and_then(|p| p.parent())
|
||||
.expect("workspace root")
|
||||
.join("crates");
|
||||
let caps = |lit: &str| {
|
||||
!lit.is_empty()
|
||||
&& lit
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit())
|
||||
};
|
||||
// The two literal shapes an indirect fire's file can carry: `"EVENT", vec!` and a match
|
||||
// arm `=> "EVENT",` — the event-name table a `fire_event(name_of(kind), …)` reads.
|
||||
let scan_arms = |text: &str, fired: &mut std::collections::HashSet<String>| {
|
||||
const SHAPE: &str = "\", vec!";
|
||||
for (i, _) in text.match_indices(SHAPE) {
|
||||
let before = &text[..i];
|
||||
let Some(q) = before.rfind('"') else { continue };
|
||||
let lit = &before[q + 1..];
|
||||
if caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
}
|
||||
}
|
||||
let lines: Vec<&str> = text.lines().map(str::trim).collect();
|
||||
for (i, t) in lines.iter().enumerate() {
|
||||
let (t, arm_value) = match t.find("=> \"") {
|
||||
Some(k) => (t[k + 3..].trim_end_matches(','), true),
|
||||
None => (*t, false),
|
||||
};
|
||||
let Some(lit) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"')) else {
|
||||
continue;
|
||||
};
|
||||
if arm_value && caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
continue;
|
||||
}
|
||||
let arm = i > 0
|
||||
&& lines[i - 1].ends_with('{')
|
||||
&& lines.get(i + 1).is_some_and(|n| n.starts_with('}'));
|
||||
if arm && caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
// An indirect fire — `fire_event(EXECUTE_CHAT_LINE, …)`, `fire_event(event_name(kind), …)`
|
||||
// — names a const or a function; the literal lives where THAT is defined, which may be
|
||||
// another file (1948: `ui_chat::event::event_name` answers for `frames::route`'s fire).
|
||||
let mut indirect: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut texts: Vec<String> = Vec::new();
|
||||
let mut stack = vec![root];
|
||||
while let Some(dir) = stack.pop() {
|
||||
for e in std::fs::read_dir(&dir).into_iter().flatten().flatten() {
|
||||
@@ -2075,11 +2177,6 @@ mod tests {
|
||||
stack.push(p);
|
||||
} else if p.extension().is_some_and(|x| x == "rs") {
|
||||
let text = std::fs::read_to_string(&p).unwrap_or_default();
|
||||
// Split so this file cannot match its own walker (see the sibling gate).
|
||||
// Two producer shapes: `UiScript::fire_event(` and its `&Lua` twin
|
||||
// `tick::fire_event_into(` (1924), which an engine verb fires from inside a
|
||||
// binding — `UpdateSpells` → SPELLS_CHANGED, `ChangeActionBarPage` →
|
||||
// ACTIONBAR_PAGE_CHANGED (1938).
|
||||
const CALLS: [&str; 2] =
|
||||
[concat!("fire_event", "("), concat!("fire_event_into", "(")];
|
||||
let mut fires_indirectly = false;
|
||||
@@ -2090,59 +2187,40 @@ mod tests {
|
||||
from = at;
|
||||
let rest = text[at..].trim_start();
|
||||
let rest = rest.strip_prefix("lua,").map_or(rest, str::trim_start);
|
||||
if !rest.starts_with('"') {
|
||||
fires_indirectly = true;
|
||||
}
|
||||
if let Some(body) = rest.strip_prefix('"') {
|
||||
if let Some(end) = body.find('"') {
|
||||
fired.insert(body[..end].to_string());
|
||||
}
|
||||
} else {
|
||||
fires_indirectly = true;
|
||||
let ident: String = rest
|
||||
.chars()
|
||||
.take_while(|c| c.is_alphanumeric() || *c == '_')
|
||||
.collect();
|
||||
if !ident.is_empty() {
|
||||
indirect.insert(ident);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if fires_indirectly {
|
||||
// An indirect fire names its event either as `"NAME", vec![…]` or as one
|
||||
// arm of an `if … { "A" } else { "B" }` chosen before the call — a literal
|
||||
// standing alone on its line (`ui_action/state.rs`'s START/STOP_AUTOREPEAT).
|
||||
const SHAPE: &str = "\", vec!";
|
||||
let caps = |lit: &str| {
|
||||
!lit.is_empty()
|
||||
&& lit.chars().all(|c| {
|
||||
c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit()
|
||||
})
|
||||
};
|
||||
for (i, _) in text.match_indices(SHAPE) {
|
||||
let before = &text[..i];
|
||||
let Some(q) = before.rfind('"') else { continue };
|
||||
let lit = &before[q + 1..];
|
||||
if caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
}
|
||||
}
|
||||
let lines: Vec<&str> = text.lines().map(str::trim).collect();
|
||||
for (i, t) in lines.iter().enumerate() {
|
||||
// …or the value of a `match` arm (`QuestPanel::Progress =>
|
||||
// "QUEST_PROGRESS",` — `ui_quest.rs`'s `panel_event`).
|
||||
let (t, arm_value) = match t.find("=> \"") {
|
||||
Some(k) => (t[k + 3..].trim_end_matches(','), true),
|
||||
None => (*t, false),
|
||||
};
|
||||
let Some(lit) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"'))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if arm_value && caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
continue;
|
||||
}
|
||||
let arm = i > 0
|
||||
&& lines[i - 1].ends_with('{')
|
||||
&& lines.get(i + 1).is_some_and(|n| n.starts_with('}'));
|
||||
if arm && caps(lit) {
|
||||
fired.insert(lit.to_string());
|
||||
}
|
||||
}
|
||||
scan_arms(&text, &mut fired);
|
||||
}
|
||||
texts.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
for text in &texts {
|
||||
for ident in &indirect {
|
||||
let decl = format!("const {ident}: &str = \"");
|
||||
if let Some(k) = text.find(&decl) {
|
||||
let body = &text[k + decl.len()..];
|
||||
if let Some(end) = body.find('"') {
|
||||
fired.insert(body[..end].to_string());
|
||||
}
|
||||
}
|
||||
if text.contains(&format!("fn {ident}(")) {
|
||||
scan_arms(text, &mut fired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,13 @@ fn the_autocast_brackets_reach_each_buttons_corners() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The spell book's overlay is the reference's own template resized by SpellBookAdapters.xml's
|
||||
// script (1952), so its number is measured off a VM, not read off an XML.
|
||||
let s = super::spellbook_tests::spellbook_ui(1024.0, 768.0);
|
||||
let (overlay, button): (f32, f32) = s
|
||||
.eval("return SpellButton1AutoCastable:GetWidth(), SpellButton1:GetWidth()")
|
||||
.unwrap();
|
||||
found.push(("SpellButton1AutoCastable".into(), overlay * ART, button));
|
||||
assert_eq!(found.len(), 2, "expected two autocast overlays: {found:?}");
|
||||
for (name, brackets, button) in &found {
|
||||
assert!(
|
||||
@@ -213,8 +220,10 @@ fn the_autocast_brackets_reach_each_buttons_corners() {
|
||||
/// The spell book is **one deliberate deviation** (decision 1392). The reference writes
|
||||
/// `scale="1.22"` into a 36-unit viewport — a 0.87x rim that floats clear of the edge, is never
|
||||
/// clipped, and washes the icon; the real 1.12 client looks the same way (director-checked), so
|
||||
/// this is taste, not fidelity. We write 1.44 to borrow the pet button's ratio. This test is what
|
||||
/// stops that drifting, or being "corrected" back to 1.22 by someone who only read the ref.
|
||||
/// this is taste, not fidelity. We write 1.48 on a 37-unit rim (1393's numbers, set by
|
||||
/// SpellBookAdapters.xml over the reference's own template since 1952) to borrow the pet button's
|
||||
/// ratio. This test is what stops that drifting, or being "corrected" back to 1.22 by someone
|
||||
/// who only read the ref.
|
||||
#[test]
|
||||
fn the_shine_tokens_ask_for_the_rims_we_meant() {
|
||||
use benilla_ui::framexml::{Element, TopLevel};
|
||||
@@ -272,6 +281,41 @@ fn the_shine_tokens_ask_for_the_rims_we_meant() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The spell book's shine is created by SpellBookAdapters.xml's script beside the reference's
|
||||
// own Model (1952), so its token is read off a VM's extract — where the renderer reads it —
|
||||
// not an XML. (Not `GetTexture`, which answers the path up to its last dot, the reference's
|
||||
// own shape, and so would cut the token's scale in half.)
|
||||
let mut s = super::spellbook_tests::spellbook_ui(1024.0, 768.0);
|
||||
s.run("ToggleSpellBook(BOOKTYPE_SPELL)").unwrap();
|
||||
s.tick(0.05);
|
||||
s.resolve();
|
||||
s.run("SpellButton1Shine:Show()").unwrap();
|
||||
s.resolve();
|
||||
let quads = s.extract();
|
||||
let token = quads
|
||||
.iter()
|
||||
.find_map(|q| match &q.content {
|
||||
benilla_ui::script::QuadContent::Texture { path: Some(p), .. }
|
||||
if p.starts_with(crate::autocast_shine::SHINE_TOKEN) =>
|
||||
{
|
||||
Some(p.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let owned: Vec<_> = quads
|
||||
.iter()
|
||||
.filter(|q| {
|
||||
s.quad_owner_name(q.target)
|
||||
.is_some_and(|n| n.starts_with("SpellButton1"))
|
||||
})
|
||||
.map(|q| (s.quad_owner_name(q.target), format!("{:?}", q.content)))
|
||||
.collect();
|
||||
panic!("no shine token among SpellButton1's quads: {owned:?}")
|
||||
});
|
||||
let scale = crate::autocast_shine::token_model_scale(&token).expect("a shine token");
|
||||
let view: f32 = s.eval("return SpellButton1Shine:GetWidth()").unwrap();
|
||||
found.push(("SpellButton1Shine".into(), 0.02 * 1280.0 * scale, view));
|
||||
assert_eq!(
|
||||
found.len(),
|
||||
2,
|
||||
|
||||
@@ -80,9 +80,49 @@ fn click(s: &mut UiScript, name: &str, button: &str) {
|
||||
s.mouse_button(x, y, button, false);
|
||||
}
|
||||
|
||||
/// The reference's spellbook needs the action-bar chain beneath it: `SpellBookFrame_OnShow` and
|
||||
/// `_OnHide` call `MultiActionBar_ShowAllGrids`/`HideAllGrids` (MultiActionBars.lua, whose file
|
||||
/// wants the options window's uvars), `OnShow` calls `UpdateMicroButtons`, and the window docks
|
||||
/// in UiPanels' left slot. One chain, in the manifest's order, for every test that opens it.
|
||||
pub(super) fn spellbook_ui(w: f32, h: f32) -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(w, h);
|
||||
for f in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"Cooldown.xml",
|
||||
"Interface\\FrameXML\\ActionButtonTemplate.xml",
|
||||
"Interface\\FrameXML\\TextStatusBar.lua",
|
||||
"Interface\\FrameXML\\TextStatusBar.xml",
|
||||
"Interface\\FrameXML\\GlobalStrings.lua",
|
||||
"Interface\\FrameXML\\BasicControls.xml",
|
||||
"Interface\\FrameXML\\MainMenuBar.xml",
|
||||
"MoneyFrame.xml",
|
||||
"GameTooltip.xml",
|
||||
"Interface\\FrameXML\\ActionBarFrame.xml",
|
||||
"Interface\\FrameXML\\BonusActionBarFrame.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"ScrollTemplates.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"Interface\\FrameXML\\MultiActionBars.xml",
|
||||
"MicroMenu.xml",
|
||||
"Interface\\FrameXML\\SpellBookFrame.xml",
|
||||
"SpellBookAdapters.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// The loader itself: every file the window depends on parses and materializes with no errors —
|
||||
/// the window + close + prev/next page buttons + 12 spell buttons (each with a Cooldown child)
|
||||
/// + 8 skill-line tabs + the 3 Spell/Pet toggle tabs.
|
||||
/// the window + close + prev/next page buttons + 12 spell buttons (each with a Cooldown and an
|
||||
/// AutoCast Model child) + 8 skill-line tabs + the 3 Spell/Pet toggle tabs + the tab flash frame.
|
||||
#[test]
|
||||
fn shipped_spellbook_loads_clean() {
|
||||
let s = UiScript::new().unwrap();
|
||||
@@ -92,22 +132,28 @@ fn shipped_spellbook_loads_clean() {
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/SpellBookFrame.xml"),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
// The reference's own file, off the chain, with the one adapter it needs from this engine.
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\BasicControls.xml");
|
||||
let frames = load_xml(&s, "Interface\\FrameXML\\SpellBookFrame.xml");
|
||||
load_xml(&s, "SpellBookAdapters.xml");
|
||||
assert!(s.errors().is_empty(), "loader errors: {:?}", s.errors());
|
||||
assert_eq!(
|
||||
report.frames, 39,
|
||||
"window + close + prev/next + 12 spell buttons (each with a Cooldown child) + 8 \
|
||||
skill-line tabs + the 3 Spell/Pet toggle tabs (decision 1032)"
|
||||
frames, 52,
|
||||
"window + close + prev/next + 12 spell buttons (each with its Cooldown and AutoCast \
|
||||
Model children) + 8 skill-line tabs + the 3 Spell/Pet toggle tabs + the tab flash frame"
|
||||
);
|
||||
for name in [
|
||||
"SpellBookFrame",
|
||||
"SpellButton12",
|
||||
"SpellButton1AutoCast",
|
||||
"SpellButton1Shine",
|
||||
] {
|
||||
assert!(
|
||||
s.eval::<bool>(&format!("return {name} ~= nil")).unwrap(),
|
||||
"{name} exists"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole contract in one end-to-end drive: `ToggleSpellBook` (the 'P' binding's entry point)
|
||||
@@ -117,32 +163,7 @@ fn shipped_spellbook_loads_clean() {
|
||||
/// bar-to-bar drag uses — packing kind 0x00 (SPELL) with the spell id.
|
||||
#[test]
|
||||
fn shipped_spellbook_drives_end_to_end() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\GlobalStrings.lua");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionButtonTemplate.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.lua");
|
||||
load_xml(&s, "Interface\\FrameXML\\TextStatusBar.xml");
|
||||
load_xml(&s, "UIParent.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
|
||||
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
|
||||
// The reference declares the reputation WATCH BAR in `ReputationFrame.xml`, and
|
||||
// `ExhaustionTick_Update` reads `ReputationWatchBar:IsShown()` twice — the reference's own
|
||||
// coupling of MainMenuBar to that pane. So an action-bar harness loads it, and with it the
|
||||
// two template files its check boxes inherit through (1875).
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\OptionsFrameTemplates.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\ReputationFrame.xml");
|
||||
load_xml(&s, "SpellBookFrame.xml");
|
||||
let mut s = spellbook_ui(1024.0, 768.0);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
|
||||
s.set_spellbook(book());
|
||||
@@ -234,20 +255,7 @@ fn shipped_spellbook_drives_end_to_end() {
|
||||
fn shipped_spellbook_shows_the_cooldown_pie() {
|
||||
use benilla_ui::script::QuadContent;
|
||||
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for f in [
|
||||
"Interface\\FrameXML\\Fonts.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
r"Interface\FrameXML\UIPanelTemplates.lua",
|
||||
r"Interface\FrameXML\UIPanelTemplates.xml",
|
||||
"GameTooltip.xml",
|
||||
"Cooldown.xml",
|
||||
"SpellBookFrame.xml",
|
||||
] {
|
||||
load_xml(&s, f);
|
||||
}
|
||||
let mut s = spellbook_ui(1024.0, 768.0);
|
||||
s.set_spellbook(book());
|
||||
s.run("ToggleSpellBook(BOOKTYPE_SPELL)").unwrap();
|
||||
s.tick(10.0); // GetTime = 10
|
||||
@@ -314,16 +322,15 @@ fn shipped_spellbook_shows_the_cooldown_pie() {
|
||||
/// putting a gold ring on all 12 slots — the director's "spellbook looks very wrong".
|
||||
#[test]
|
||||
fn shipped_spellbook_empty_slot_draws_only_the_background() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(640.0, 700.0);
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "SpellBookFrame.xml");
|
||||
// An EMPTY book: every slot takes the `id > offset + numSpells` disable path.
|
||||
let mut s = spellbook_ui(640.0, 700.0);
|
||||
// A one-spell book: slot 5 takes the reference's `id > offset + numSpells` disable path. (A
|
||||
// book with NO spells is a state no character is ever in — the reference's own
|
||||
// `ToggleSpellBook` clamps the page to 0 for it and every button id goes negative.)
|
||||
let mut b = book();
|
||||
b.tabs.truncate(1);
|
||||
b.tabs[0].num_spells = 1;
|
||||
b.slots.truncate(1);
|
||||
s.set_spellbook(b);
|
||||
s.run("ToggleSpellBook(BOOKTYPE_SPELL)").unwrap();
|
||||
s.tick(0.05);
|
||||
s.resolve();
|
||||
@@ -352,9 +359,7 @@ fn shipped_spellbook_empty_slot_draws_only_the_background() {
|
||||
/// l.132/134/268/296-303/336): 1/"true"/true check; 0/"false"/nil/non-numeric strings uncheck.
|
||||
#[test]
|
||||
fn set_checked_uses_blizzard_bool_coercion() {
|
||||
let s = UiScript::new().unwrap();
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "SpellBookFrame.xml");
|
||||
let s = spellbook_ui(1024.0, 768.0);
|
||||
for (arg, want) in [
|
||||
("1", true),
|
||||
("0", false),
|
||||
@@ -421,16 +426,7 @@ fn pet_book() -> benilla_ui::script::PetBookState {
|
||||
/// the class token's label, and a right-click flips autocast instead of casting.
|
||||
#[test]
|
||||
fn the_pet_tab_switches_books_and_renders_the_pets_spells() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
|
||||
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
|
||||
load_xml(&s, "GameTooltip.xml");
|
||||
load_xml(&s, "Cooldown.xml");
|
||||
load_xml(&s, "SpellBookFrame.xml");
|
||||
let mut s = spellbook_ui(1024.0, 768.0);
|
||||
s.fire_event("PLAYER_ENTERING_WORLD", vec![]);
|
||||
s.set_spellbook(book());
|
||||
|
||||
@@ -713,7 +709,14 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() {
|
||||
r"Interface\FrameXML\OptionsFrameTemplates.xml",
|
||||
r"Interface\FrameXML\ReputationFrame.xml",
|
||||
r"Interface\AddOns\Blizzard_MacroUI\Blizzard_MacroUI.xml",
|
||||
"SpellBookFrame.xml",
|
||||
// The reference's spellbook shows and hides the multibar grids (MultiActionBars.lua),
|
||||
// whose file wants the options window's uvars — the same tail `spellbook_ui` carries.
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"KeyBindingsPage.xml",
|
||||
"OptionsFrame.xml",
|
||||
"Interface\\FrameXML\\MultiActionBars.xml",
|
||||
"Interface\\FrameXML\\SpellBookFrame.xml",
|
||||
"SpellBookAdapters.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
|
||||
@@ -353,7 +353,11 @@ pub(super) const BAG_UI: &[&str] = &[
|
||||
// `if ( ChatFrameEditBox:IsShown() )` (ContainerFrame.lua l.569) to decide between posting the
|
||||
// item's link and splitting the stack, so a VM without it raises before either.
|
||||
"Interface\\FrameXML\\UIMenu.xml", // the kit ChatMenu/EmoteMenu/VoiceMacroMenu build from
|
||||
"ChatFrame.xml",
|
||||
"Interface\\FrameXML\\ChatFrame.xml",
|
||||
"Interface\\FrameXML\\UIDropDownMenu.xml",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.lua",
|
||||
"Interface\\FrameXML\\UIPanelTemplates.xml",
|
||||
"Interface\\FrameXML\\FloatingChatFrame.xml",
|
||||
// Our adapters over the reference's container files — the keyring tooltip wrapper, the three
|
||||
// bag verbs 0561 shadows (`OpenBackpack`/`CloseBackpack`/`CloseAllBags`), and the item-push
|
||||
// card the reference draws with a `<Model>` this engine does not render (0887). It has to be
|
||||
|
||||
Reference in New Issue
Block a user