Files
benilla-octo/crates/benilla-app/src/ui_script/dropdown_tests.rs
T
SamandClaude Fable 5.1 e8db9ef14c the world map's arrow shows on zone sheets, and UIParent goes stock
- the world map's arrow shows on every zone sheet, and alt-clicking a town jumps there: the stock file creates the player arrow as a `Model` child of `WorldMapFrame` and lays a zone sheet's explored-area overlays as ARTWORK textures on `WorldMapDetailFrame` — one `(strata, level)` bucket, where the draw layer outranks the frame — and benilla drew a model's scene at the frame's own layer-0 slot, so every overlay painted over it (the continent sheet has no overlays, which is why it was the only sheet with an arrow); wow-re's carve: a layer batch carries three sub-arrays — quads, the string batch, a render-callback list — that `0x76fb00` drains in that order, and `0x76d160` registers a model's callback only for layer 2 (`0x76d17f cmp ebx,2`), so a model's scene is ARTWORK content, neither a separate pass nor a layer-0 slot; a `ModelPane` quad now keys at the ARTWORK content rank, which re-orders the pet page's happiness icon (a sibling's BACKGROUND quad) under the pane's scene — the reference's own order, harmless because the bake is transparent where the model is not; a model against a LATER-linked ARTWORK font string in the same bucket is still ordered by link stamp, named not built; and the alt-click jump, which required the topmost frame under the cursor to be `WorldMapButton` itself while the stock file parents every POI icon and blip to that button, now walks up from whatever was hit to the button and takes its rect — the map's own chrome still answers nothing (1992)
- `UIParent` is the reference's own: `UIParent.xml` loads off the chain at its toc's slot, right after `WorldFrame.xml`, sourcing `UIParent.lua` and `LocaleProperties.lua` itself, and our `UIParent.xml` + `UiPanels.xml` — 3086 lines of transcribed `UIParent.lua` — are deleted; the one unbound verb the stock file ticks, `CheckReadyCheckTime`, was already 1989's; back from the reference: all sixty-six registered events and their arms (`PLAYER_CONTROL_LOST`/`_GAINED`, `RAID_ROSTER_UPDATE`, the trade-request pair, `PLAYER_SKINNED`, `GOSSIP_ENTER_CODE`), the ESC ladder as written — StaticPopup, the options window's Cancel, the game menu, `CloseMenus`, `SpellStopCasting`, `SpellStopTargeting`, `CloseAllWindows`, `ClearTarget`, else open — so ESC no longer clears a held cursor item or cancels the colour picker, two arms ours had grown that the reference does not have; the fade/flash/pulse kits walked from `UIParent`'s own OnUpdate in place of three 1×1 driver frames of ours, so a cinematic's `UIParent:Hide()` stops the walks as it does there; the managed bottom-stack positions without our `UIParent_RegisterManagedPositionListener` extension, which no shipped file registered any more; `LowHealthFrame`/`OutOfControlFrame`; and the `firstTimeLoaded` close on re-show; what stayed ours moved out — the window tab template and its OnUpdate settle (1004's async-measure adapter) and `BENILLA_FALLBACK_ICON` to `ScrollTemplates.xml`, 0561's bag verbs to `ContainerFrameAdapters`, the script log's slot row to its own file, `OptionsFrameCancel = OptionsFrameCloseButton` beside the two options aliases — and the Auction/Inspect/Macro/Talent panel rows, verbatim copies of the LoadOnDemand addons' own registrations, go; the reference also decides that the area spirit healer's dialog accepts on show with one CANCEL button, that losing control closes every window bar the reference's exceptions, and that the bottom-bar clearance reads the saved globals `SHOW_MULTI_ACTIONBAR_1`/`_2` rather than the bars' shown state; three integration binaries that kept private copies of the manifest loader use the shared one now (1988)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:30:54 +07:00

173 lines
7.3 KiB
Rust

//! The two shared dropdown lists' own scripts (`UIDropDownMenu.xml`'s `DropDownList1`/`2`).
//!
//! These exist because the instance `<Scripts>` block was absent entirely and nothing said so: the
//! lists were one-line self-closing elements, the loader has no opinion about a frame with no
//! scripts, and every dropdown still opened and closed. The reference hangs three separate jobs off
//! those two elements and we had none of them.
//!
//! Each test is named after the claim it defends (1212): the submenu catch-all, the open-menu
//! registry's clear, the fact that the default text height is DERIVED rather than a literal, and —
//! found by the third while it was failing — that a button's label answers for the font object its
//! button gave it, which it did not.
use benilla_ui::script::UiScript;
use super::test_ui::load_ui as load_xml;
/// The production prefix the lists need (ui_script/mod.rs order): the fonts (this OnLoad reads a
/// real font height off Button1's NormalText), UIParent (the lists declare `parent="UIParent"`),
/// and GameTooltip — the list's own `$parentMenuBackdrop` OnLoad indexes `TOOLTIP_DEFAULT_COLOR`,
/// which GameTooltip.xml declares.
fn load_dropdown_kit(s: &UiScript) {
for file in [
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\UIParent.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
"Interface\\FrameXML\\GameTooltip.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
] {
load_xml(s, file);
}
}
/// **Hiding level 1 closes level 2.** The bug this replaces was reachable by hand: open a menu with
/// a `hasArrow` row, hover the submenu open, then toggle level 1 shut. `ToggleDropDownMenu`'s close
/// arm is a bare `listFrame:Hide()`, so DropDownList2 stayed on screen with its parent gone.
///
/// The test hides level 1 *directly* rather than through the toggle, which is the stronger claim
/// and the reason the reference puts this on `OnHide` instead of in the toggle: it must catch every
/// path that hides level 1, including a parent Hide, the ESC ladder, and an addon calling
/// `DropDownList1:Hide()` on its own.
#[test]
fn hiding_the_parent_list_closes_an_open_submenu() {
let s = UiScript::new().unwrap();
load_dropdown_kit(&s);
s.run("DropDownList1:Show(); DropDownList2:Show();")
.unwrap();
assert!(
s.eval::<bool>("return DropDownList2:IsVisible()").unwrap(),
"fixture: the submenu must be open before level 1 hides"
);
s.run("DropDownList1:Hide();").unwrap();
assert!(
!s.eval::<bool>("return DropDownList2:IsVisible()").unwrap(),
"level 2 was left on screen after its parent list hid — the orphaned submenu"
);
}
/// **`OPEN_DROPDOWNMENUS` clears when its list hides.** `UnitPopup.xml` writes the level when a
/// unit menu opens and its every-frame driver walks the table; nothing ever removed the entry, so
/// it outlived its menu forever.
///
/// No player-visible symptom is reachable from the stale entry today — `UnitPopup_OnUpdate` returns
/// early unless `DropDownList1` is visible and a `UnitPopupFrame` owns the open menu, and
/// `UIDropDownMenu_AddButton` re-enables every button unconditionally. That is stated here rather
/// than left implied, because both of those guards belong to *other* code and either could move.
/// The divergence from the reference is in this file, so it is fixed and falsified in this file.
#[test]
fn hiding_a_list_clears_its_open_menu_registry_entry() {
let s = UiScript::new().unwrap();
load_dropdown_kit(&s);
s.run(
r#"
DropDownList1:Show();
DropDownList2:Show();
OPEN_DROPDOWNMENUS[1] = { which = "SELF", unit = "player" };
OPEN_DROPDOWNMENUS[2] = { which = "SELF", unit = "player" };
"#,
)
.unwrap();
s.run("DropDownList2:Hide();").unwrap();
assert!(
s.eval::<bool>("return OPEN_DROPDOWNMENUS[2] == nil")
.unwrap(),
"level 2's registry entry survived its list hiding"
);
s.run("DropDownList1:Hide();").unwrap();
assert!(
s.eval::<bool>("return OPEN_DROPDOWNMENUS[1] == nil")
.unwrap(),
"level 1's registry entry survived its list hiding"
);
}
/// **The default text height is read off Button1's font, never written as a literal.**
///
/// This is the trap the whole change turned on. `UIDropDownMenu.lua:16` forward-declares
/// `UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT = nil`, and reading only that line says "the reference
/// leaves it nil, so defining it would be a divergence". The assignment is in the *XML*, in this
/// OnLoad, and it derives the value from the button's own NormalText — so the honest answer is that
/// the name is a real gap whose only correct value is whatever our font object reports.
///
/// The assertion is therefore that it is non-nil and EQUALS Button1's reported height, not that it
/// equals some number: a literal would pass a number check and still be wrong.
#[test]
fn the_default_text_height_is_derived_from_button1_not_a_literal() {
let s = UiScript::new().unwrap();
load_dropdown_kit(&s);
let (height, from_button): (Option<f64>, Option<f64>) = s
.eval(
r#"
local _, buttonHeight = DropDownList1Button1NormalText:GetFont();
return UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT, buttonHeight
"#,
)
.unwrap();
let from_button = from_button.expect("fixture: Button1's NormalText must report a font height");
assert_eq!(
height,
Some(from_button),
"the constant must be the height Button1's own NormalText reports, not a literal"
);
}
/// **A button label answers for the font object its button gave it.** The gap the test above
/// uncovered: `extract` overlays the per-state font object onto a *clone* of the region's data
/// every frame, so a `<NormalFont inherits="GameFontHighlightSmall"/>` label painted correctly and
/// still answered `nil` to both `GetFont` and `GetFontObject`. Nothing errored — a FontString with
/// no font of its own is a legal state — so it took the reference's own OnLoad, which reads exactly
/// that, to surface it.
///
/// Asserted on the dropdown row because that is the site that found it, and against
/// `GameFontHighlightSmall` by name because that is what `UIDropDownMenuButtonTemplate` declares.
#[test]
fn a_button_label_reports_the_font_object_its_button_set() {
let s = UiScript::new().unwrap();
load_dropdown_kit(&s);
let (object_height, label_height, label_face): (Option<f64>, Option<f64>, Option<String>) = s
.eval(
r#"
local _, objectHeight = GameFontHighlightSmall:GetFont();
local face, height = DropDownList1Button1NormalText:GetFont();
return objectHeight, height, face
"#,
)
.unwrap();
assert_eq!(
label_height, object_height,
"the row label must report GameFontHighlightSmall's height, not nil"
);
assert!(
label_face.is_some_and(|f| f.ends_with(".TTF")),
"the row label must report a real font face through its button's font object"
);
assert!(
s.eval::<bool>(
"return DropDownList1Button1NormalText:GetFontObject() == GameFontHighlightSmall"
)
.unwrap(),
"the label must hand back the font OBJECT, which is what the corpus indexes immediately"
);
}