the main action bar pages, and <Include> runs Lua like the client
- the main bar's page arrows page for real, with the reference's own rule that a page a shown multibar displays is not also reachable by paging - `<Include file="X.lua">` executes the file the way the client does — dispatch on the extension — so an addon pulling its libraries through XML stops losing them - UIParentLoadAddOn lands, the on-demand loader behind every "open the panel" path - nine constants take the reference's names back (NUM_GROUP_LOOT_FRAMES and friends — an addon reads the real name, not our prefix), and UnitCreatureType answers through the client's own three-stage resolver Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1068,10 +1068,18 @@ fn the_reference_action_bar_constants_are_defined() {
|
||||
"zBar.lua:40's numeric for must have a limit"
|
||||
);
|
||||
|
||||
assert!(
|
||||
s.eval::<bool>("return CURRENT_ACTIONBAR_PAGE == nil").unwrap(),
|
||||
"CURRENT_ACTIONBAR_PAGE is mutable page state we do not keep — nil fails loudly, a frozen 1 lies"
|
||||
// `CURRENT_ACTIONBAR_PAGE` was asserted ABSENT here, on the grounds that a frozen 1 lies where
|
||||
// nil fails loudly. That objection is discharged, not overruled: the bar pages now, so the
|
||||
// global is live state the paged-id formula reads rather than a frozen number. It is therefore
|
||||
// asserted as state — present, and MOVING — instead of as one of the constants above.
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 1);
|
||||
s.run("ActionBar_PageUp()").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(),
|
||||
2,
|
||||
"a frozen 1 would still be a lie — this one has to move"
|
||||
);
|
||||
s.run("ActionBar_PageDown()").unwrap();
|
||||
}
|
||||
|
||||
/// **The reference's two-level action-button split, both halves inheritable by name.**
|
||||
@@ -1155,3 +1163,102 @@ fn both_reference_action_button_templates_are_inheritable() {
|
||||
"BenillaActionButtonTemplate's 48 inherits= sites must be untouched by the split"
|
||||
);
|
||||
}
|
||||
|
||||
/// Main-bar paging — `CURRENT_ACTIONBAR_PAGE` and the three verbs around it.
|
||||
///
|
||||
/// The data was always there (the app owns all 120 action slots); only the selector was missing,
|
||||
/// and its absence was visible on screen as page arrows with no `OnClick`. `Bartender2.lua:686`
|
||||
/// died on the nil `ChangeActionBarPage` at session start.
|
||||
///
|
||||
/// Two things are asserted that a reconstruction would get wrong. **A bonus page outranks the
|
||||
/// paged one** — the reference's own `ActionButton_GetPagedID` takes the bonus branch first, so
|
||||
/// paging must be the `else` arm and not an addition. And **page-up wraps to the literal page 1**
|
||||
/// while page-down rescans for the last viewable page: that asymmetry is the reference's, and it
|
||||
/// is observable the moment a page is blanked from `VIEWABLE_ACTION_BAR_PAGES`.
|
||||
#[test]
|
||||
fn the_main_bar_pages_and_a_bonus_page_still_outranks_it() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in ["Cooldown.xml", "ActionBar.xml"] {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(&s, &doc, &|_| None);
|
||||
assert!(report.errors.is_empty(), "{file}: {:?}", report.errors);
|
||||
}
|
||||
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 1);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
1,
|
||||
"page 1 button 1 is action 1"
|
||||
);
|
||||
|
||||
// Page up walks to 2, so button 1 shows action 13.
|
||||
s.run("ActionBar_PageUp()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 2);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
13
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton12)")
|
||||
.unwrap(),
|
||||
24
|
||||
);
|
||||
|
||||
// Down again, and below page 1 it rescans to the LAST viewable page.
|
||||
s.run("ActionBar_PageDown()").unwrap();
|
||||
assert_eq!(s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(), 1);
|
||||
s.run("ActionBar_PageDown()").unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(),
|
||||
4,
|
||||
"page-down off the bottom rescans for the last VIEWABLE page — 4, not 6, because the \
|
||||
always-on bottom multibars own pages 5 and 6"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
37
|
||||
);
|
||||
// The pages the bottom bars already display are unreachable from the main bar, which is what
|
||||
// MultiActionBar_Update does on the reference the moment those bars are shown. Without this,
|
||||
// paging up lands on a duplicate of the twelve actions already on screen below.
|
||||
assert!(s
|
||||
.eval::<bool>(
|
||||
"return VIEWABLE_ACTION_BAR_PAGES[5] == nil and VIEWABLE_ACTION_BAR_PAGES[6] == nil"
|
||||
)
|
||||
.unwrap());
|
||||
s.run("CURRENT_ACTIONBAR_PAGE = 4 ActionBar_PageUp()")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return CURRENT_ACTIONBAR_PAGE").unwrap(),
|
||||
1,
|
||||
"walking up from the last viewable page skips 5 and 6 and wraps to the LITERAL 1 — the \
|
||||
reference's own asymmetry with page-down, which rescans instead"
|
||||
);
|
||||
|
||||
// A bonus page outranks the paged one entirely: with an offset up, the page is ignored.
|
||||
s.run("CURRENT_ACTIONBAR_PAGE = 3").unwrap();
|
||||
s.set_bonus_bar_offset(1);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
73,
|
||||
"bonus offset 1 is action 73, whatever page the main bar is on"
|
||||
);
|
||||
s.set_bonus_bar_offset(0);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return ActionButton_GetPagedID(ActionButton1)")
|
||||
.unwrap(),
|
||||
25,
|
||||
"and the page comes back when the form drops"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -415,3 +415,66 @@ fn the_list_reports_an_out_of_date_interface_like_the_char_select_twin() {
|
||||
"no `## Interface` at all is silent, not out of date"
|
||||
);
|
||||
}
|
||||
|
||||
/// `UIParentLoadAddOn(name)` — the on-demand loader 5 corpus addons name (FuBar_MCPFu,
|
||||
/// Fubar_RareTrackerFu, MCP, MikScrollingBattleText, SpecialTalent), and which raised as a nil
|
||||
/// global until every piece it needs was joined up in `BasicControls.xml`.
|
||||
///
|
||||
/// Two things are asserted, and the second is the one a reconstruction from the name would miss:
|
||||
/// it answers `LoadAddOn`'s own falsey result so a caller can branch, and `FailedAddOnLoad` shows
|
||||
/// the failure **once per addon name**, not once per attempt (ref `UIParent.lua` l.641-643).
|
||||
#[test]
|
||||
fn ui_parent_load_addon_reports_once_and_returns_the_load_result() {
|
||||
let s = UiScript::new().unwrap();
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui/BasicControls.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 two GlobalStrings this leans on. The real client has them from FrameXML's first file and
|
||||
// the corpus survey runs the real `GlobalStrings.lua`; a bare VM does not, so they are seated
|
||||
// here rather than left to make `format` raise on a nil.
|
||||
s.run(
|
||||
r#"
|
||||
ADDON_LOAD_FAILED = "Couldn't load %s: %s"
|
||||
ADDON_MISSING = "Missing"
|
||||
shown = {}
|
||||
message = function(t) table.insert(shown, t) end
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// A name no registry has: LoadAddOn answers (nil, "MISSING"), so this answers falsey.
|
||||
assert!(
|
||||
s.eval::<bool>(r#"return UIParentLoadAddOn("NoSuchAddonAnywhere") == nil"#)
|
||||
.unwrap(),
|
||||
"must hand back LoadAddOn's own falsey result for a caller to branch on"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return shown[1]").unwrap(),
|
||||
"Couldn't load NoSuchAddonAnywhere: Missing",
|
||||
"the reason word resolves through ADDON_..reason"
|
||||
);
|
||||
|
||||
// The latch: a retrying addon gets one modal, not one per attempt.
|
||||
for _ in 0..3 {
|
||||
s.run(r#"UIParentLoadAddOn("NoSuchAddonAnywhere")"#)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return table.getn(shown)").unwrap(),
|
||||
1,
|
||||
"FailedAddOnLoad must suppress every repeat for the same name"
|
||||
);
|
||||
|
||||
// A DIFFERENT name is a different latch entry, so it still reports.
|
||||
s.run(r#"UIParentLoadAddOn("AlsoNotThere")"#).unwrap();
|
||||
assert_eq!(s.eval::<i64>("return table.getn(shown)").unwrap(), 2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user