Files
benilla-octo/crates/benilla-app/src/ui_script/group_loot_tests.rs
T
Sam SungandClaude Opus 5.5 4f8350c9ed String literals and test names in benilla-app carry no record numbers (#368)
The strings pass for `benilla-app`, the last crate: string literals and test names lose the private record numbers and the workspace's words that a comment pass cannot touch.

Fixes #92. Fixes #36.

- **Messages.** About 230 assertion, panic, `expect` and log messages, printed tool labels, and comments inside embedded Lua fixtures drop their record numbers; the eight that named "the director" keep their fact in plain words. Stale claims in a few messages are corrected on the way (a cast-bar step, a font line, the every-shot recompute).
- **Names.** Seven test and helper names that carried a record number or "director" now say what they check. `git grep` found no other use of any of them.
- **Player files.** The key-bindings file's header and `config.toml`'s header each lose a record citation. Neither header is compared when the file is read back: the bindings reader skips every `#` line, and `config.toml`'s header is a TOML comment.
- **Fixtures.** Test fixtures use neutral character, account and frame names.
- **#36.** The quest-material test labels GameObject type 10 GOOBER (in 1.12, 3 is CHEST, 9 TEXT, 10 GOOBER); the code under test was already right, and the comments were fixed with the comment pass.
- **Verification.** With comments stripped and every literal's contents masked, each file is identical to main and keeps its literals' line counts, apart from the renamed functions and one rustfmt reflow. `cargo fmt --check` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 01:06:28 +07:00

645 lines
25 KiB
Rust

//! The stock group-loot roll popups (`GroupLootFrame1..4` in `LootFrame.xml`), driven by a pushed
//! [`LootRollsState`] and `START_LOOT_ROLL`/`CANCEL_LOOT_ROLL`.
use benilla_ui::script::{
DressUpIntent, ExtractedQuad, LootRollEntry, LootRollsState, QuadContent, ScriptValue, UiScript,
};
use super::test_ui::{load_ui as load_xml, load_ui_no_warnings as load_xml_no_warnings};
/// The centre of the first texture quad whose path contains `needle`, for clicking it.
fn quad_center(quads: &[ExtractedQuad], needle: &str) -> (f32, f32) {
let r = quads
.iter()
.find(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p.contains(needle))
})
.and_then(|q| q.rect)
.unwrap_or_else(|| panic!("no quad for {needle}"));
((r.left + r.right) * 0.5, (r.bottom + r.top) * 0.5)
}
/// The colour of the first text quad reading `t`.
fn text_color(quads: &[ExtractedQuad], t: &str) -> Option<[f32; 4]> {
quads.iter().find_map(|q| match &q.content {
QuadContent::Text {
text: Some(x),
color,
..
} if x == t => Some(*color),
_ => None,
})?
}
/// The four popups, declared in stock `LootFrame.xml`. The caller loads `UIParent.xml`, whose
/// `UIParent_OnEvent` routes `START_LOOT_ROLL` (`UIParent.lua:513-516`).
fn load_group_loot(s: &UiScript) {
// `GroupLootDropDown`'s OnLoad runs its initializer at once, which reads `MAX_PARTY_MEMBERS`
// (`LootFrame.lua:217`), a constant of `PartyMemberFrame.lua`.
load_xml(s, r"Interface\FrameXML\UIDropDownMenu.xml");
load_xml(s, r"Interface\FrameXML\PartyMemberFrame.lua");
load_xml(s, r"Interface\FrameXML\LootFrame.xml");
}
fn setup() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
// The loot window's labels (`ITEMS`, `PREV`, `NEXT`) are GlobalStrings keys; a missing one is
// a loader warning, which `load_ui_no_warnings` fails.
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\ItemButtonTemplate.xml");
// `UIPanelCloseButton`, which each popup's pass button inherits (`LootFrame.xml:364`).
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml"); // the vote and item hovers
load_xml(&s, "Interface\\FrameXML\\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\\GlobalStrings.lua");
load_xml(&s, "Interface\\FrameXML\\MainMenuBar.xml");
load_xml(&s, "Interface\\FrameXML\\ActionBarFrame.xml");
load_xml(&s, "Interface\\FrameXML\\BonusActionBarFrame.xml");
s
}
/// The resolved rolls' links as `ui_loot_roll` builds them: quality colour, four-field `|Hitem:`.
const STAFF_LINK: &str = "|cffa335ee|Hitem:17182:0:0:0|h[Staff of Jordan]|h|r";
const SWORD_LINK: &str = "|cffffffff|Hitem:25:0:0:0|h[Worn Shortsword]|h|r";
/// Roll 7 resolved Epic and bind-on-pickup, roll 8 resolved Common, roll 9 with no template yet.
fn rolls() -> LootRollsState {
LootRollsState {
rolls: vec![
LootRollEntry {
roll_id: 7,
name: Some("Staff of Jordan".into()),
texture: Some("Interface\\Icons\\INV_Staff_12".into()),
quantity: 1,
quality: Some(4), // Epic -> purple
bind_on_pickup: true,
time_left_ms: 42_000,
item_id: 17182,
link: Some(STAFF_LINK.into()),
random_property_id: 0,
},
LootRollEntry {
roll_id: 8,
name: Some("Worn Shortsword".into()),
texture: Some("Interface\\Icons\\INV_Sword_04".into()),
quantity: 1,
quality: Some(1), // Common -> white
bind_on_pickup: false,
time_left_ms: 60_000,
item_id: 25,
link: Some(SWORD_LINK.into()),
random_property_id: 0,
},
// No template yet: name, texture, quality and link (which embeds the name) are nil.
LootRollEntry {
roll_id: 9,
name: None,
texture: None,
quantity: 1,
quality: None,
bind_on_pickup: false,
time_left_ms: 55_000,
item_id: 4306,
link: None,
random_property_id: 0,
},
],
}
}
/// The popups load with no loader warning of any kind and start hidden.
#[test]
fn shipped_group_loot_frame_loads_clean_and_starts_hidden() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, r"Interface\FrameXML\UIDropDownMenu.xml");
load_xml(&s, r"Interface\FrameXML\PartyMemberFrame.lua");
// The popups come inside the whole loot window, so the frame count is only a floor.
assert!(
load_xml_no_warnings(&s, r"Interface\FrameXML\LootFrame.xml") > 4,
"the loot window brought its frames"
);
for i in 1..=4 {
let name = format!("GroupLootFrame{i}");
assert!(
!s.eval::<bool>(&format!("return {name}:IsVisible()"))
.unwrap(),
"{name} starts hidden"
);
}
}
/// `START_LOOT_ROLL` claims the first free popup (`LootFrame.lua:246-258`) and paints the roll:
/// name, quality colour, bind-on-pickup decoration and timer range; Need clicks through.
#[test]
fn start_loot_roll_claims_frames_in_order_and_paints_the_roll() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_group_loot(&s);
s.set_loot_rolls(rolls());
s.fire_event(
"START_LOOT_ROLL",
vec![ScriptValue::Int(7), ScriptValue::Int(42_000)],
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return GroupLootFrame1:IsVisible()")
.unwrap());
assert_eq!(s.eval::<i64>("return GroupLootFrame1.rollID").unwrap(), 7);
// The timer's range is set before `Show()` (`LootFrame.lua:253`).
assert_eq!(
s.eval::<(f64, f64)>("return GroupLootFrame1Timer:GetMinMaxValues()")
.unwrap(),
(0.0, 42_000.0)
);
// Bind-on-pickup shows the gold decoration (`LootFrame.lua:263-266`).
assert!(s
.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
.unwrap());
s.fire_event(
"START_LOOT_ROLL",
vec![ScriptValue::Int(8), ScriptValue::Int(60_000)],
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return GroupLootFrame1:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return GroupLootFrame2:IsVisible()")
.unwrap());
assert_eq!(s.eval::<i64>("return GroupLootFrame2.rollID").unwrap(), 8);
assert!(!s
.eval::<bool>("return GroupLootFrame2Decoration:IsShown()")
.unwrap());
s.resolve();
let quads = s.extract();
assert_eq!(
s.eval::<String>("return GroupLootFrame1Name:GetText()")
.unwrap(),
"Staff of Jordan"
);
let purple = text_color(&quads, "Staff of Jordan").expect("Staff of Jordan colour");
assert!(
(purple[0] - 0.64).abs() < 0.02 && (purple[1] - 0.21).abs() < 0.02,
"Epic item text is purple, got {purple:?}"
);
let white = text_color(&quads, "Worn Shortsword").expect("Worn Shortsword colour");
assert!(
(white[0] - 1.0).abs() < 0.02 && (white[1] - 1.0).abs() < 0.02,
"Common item text is white, got {white:?}"
);
// Found by name: the popups are `toplevel` (`LootFrame.xml:243`), so draw order is not
// declaration order. Roll 7 is bind-on-pickup, so the vote waits for the confirm.
let (nx, ny) = s
.eval::<(f64, f64)>(
"return (GroupLootFrame1RollButton:GetLeft() + GroupLootFrame1RollButton:GetRight()) \
/ 2, (GroupLootFrame1RollButton:GetBottom() + GroupLootFrame1RollButton:GetTop()) / 2",
)
.unwrap();
let (nx, ny) = (nx as f32, ny as f32);
assert_eq!(
s.hit_test_name(nx, ny).as_deref(),
Some("GroupLootFrame1RollButton"),
"the click has to land on frame 1's Need button for the rest of this to mean anything"
);
s.mouse_button(nx, ny, "LeftButton", true);
s.mouse_button(nx, ny, "LeftButton", false);
assert!(
s.take_loot_roll_votes().is_empty(),
"a BoP Need must not reach the wire before the confirm"
);
assert_eq!(s.take_loot_roll_confirms(), vec![(7, 1)], "Need on roll 7");
}
/// `CONFIRM_LOOT_ROLL` opens the dialog carrying `(rollID, rollType)` (`UIParent.lua:517-523`);
/// its OK calls `ConfirmLootRoll` (`StaticPopup.lua:1375-1377`), which sends the withheld vote.
#[test]
fn the_bop_confirm_popup_lands_the_withheld_vote() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_group_loot(&s);
s.set_loot_rolls(rolls());
// The app fires this after draining the confirm queue (`ui_loot_roll::drain_loot_rolls`).
s.fire_event(
"CONFIRM_LOOT_ROLL",
vec![ScriptValue::Int(7), ScriptValue::Int(1)],
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// `data2` is the rollType, so `OnAccept` can replay the click.
assert!(
s.eval::<bool>("return StaticPopup_FindVisible(\"CONFIRM_LOOT_ROLL\") ~= nil")
.unwrap(),
"the confirm popup should be visible"
);
let (data, data2) = s
.eval::<(i64, i64)>(
"local d = StaticPopup_FindVisible(\"CONFIRM_LOOT_ROLL\")\nreturn d.data, d.data2",
)
.unwrap();
assert_eq!((data, data2), (7, 1), "rollID and rollType ride the dialog");
// The 1.12 `LOOT_NO_DROP` and `OKAY` strings.
let dialog = s
.eval::<String>("return StaticPopup_FindVisible(\"CONFIRM_LOOT_ROLL\"):GetName()")
.unwrap();
assert_eq!(
s.eval::<String>(&format!("return {dialog}Text:GetText()"))
.unwrap(),
"Looting this item will bind it to you."
);
assert_eq!(
s.eval::<String>(&format!("return {dialog}Button1:GetText()"))
.unwrap(),
"Okay"
);
assert!(s.take_loot_roll_votes().is_empty());
s.eval::<bool>(
"local d = StaticPopup_FindVisible(\"CONFIRM_LOOT_ROLL\")\n\
StaticPopupDialogs[\"CONFIRM_LOOT_ROLL\"].OnAccept(d.data, d.data2)\nreturn true",
)
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.take_loot_roll_votes(),
vec![(7, 1)],
"OK on the confirm sends the Need the gate withheld"
);
}
/// `CANCEL_LOOT_ROLL` hides the popup holding that rollID and no other (`LootFrame.lua:279-285`).
#[test]
fn cancel_loot_roll_hides_only_that_frame() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_group_loot(&s);
s.set_loot_rolls(rolls());
s.fire_event(
"START_LOOT_ROLL",
vec![ScriptValue::Int(7), ScriptValue::Int(42_000)],
);
s.fire_event(
"START_LOOT_ROLL",
vec![ScriptValue::Int(8), ScriptValue::Int(60_000)],
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s.fire_event("CANCEL_LOOT_ROLL", vec![ScriptValue::Int(7)]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(!s
.eval::<bool>("return GroupLootFrame1:IsVisible()")
.unwrap());
assert!(s
.eval::<bool>("return GroupLootFrame2:IsVisible()")
.unwrap());
// A rollID no popup holds is a no-op.
s.fire_event("CANCEL_LOOT_ROLL", vec![ScriptValue::Int(7)]);
s.fire_event("CANCEL_LOOT_ROLL", vec![ScriptValue::Int(999)]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return GroupLootFrame2:IsVisible()")
.unwrap());
s.fire_event("CANCEL_LOOT_ROLL", vec![ScriptValue::Int(8)]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(!s
.eval::<bool>("return GroupLootFrame2:IsVisible()")
.unwrap());
}
/// A roll with its item template in flight opens without error, with no name and no icon.
#[test]
fn in_flight_roll_opens_without_a_name_or_icon() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_group_loot(&s);
s.set_loot_rolls(rolls());
s.fire_event(
"START_LOOT_ROLL",
vec![ScriptValue::Int(9), ScriptValue::Int(55_000)],
);
assert!(
s.errors().is_empty(),
"an in-flight roll (all-nil GetLootRollItemInfo) must not error: {:?}",
s.errors()
);
assert!(s
.eval::<bool>("return GroupLootFrame1:IsVisible()")
.unwrap());
assert_eq!(s.eval::<i64>("return GroupLootFrame1.rollID").unwrap(), 9);
// `SetText(nil)` (`LootFrame.lua:274`) leaves no text, not "nil".
assert_eq!(
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
None,
""
);
assert!(!s
.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
.unwrap());
// No placeholder icon: `GroupLootFrame_OnShow` passes the nil texture to `SetTexture`
// (`LootFrame.lua:273`), which leaves the icon empty.
s.resolve();
let has_fallback_icon = s.extract().iter().any(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.contains("INV_Misc_QuestionMark"))
});
assert!(
!has_fallback_icon,
"the reference paints no placeholder for an unresolved item"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The stock popup paints only in `OnShow`, so a `START_LOOT_ROLL` that beats its roll to the model
/// stays blank. On a cache miss the reference fires it from the item-template arrival callback
/// (`0x61b460`, armed by `0x61b310`), and `feed_loot_rolls` holds it the same way.
#[test]
fn nothing_repaints_a_frame_that_opened_before_its_snapshot() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_group_loot(&s);
// The order the app must never produce: the event first, against a model with no such roll.
s.fire_event(
"START_LOOT_ROLL",
vec![ScriptValue::Int(7), ScriptValue::Int(42_000)],
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert!(s
.eval::<bool>("return GroupLootFrame1:IsVisible()")
.unwrap());
assert_eq!(
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
None,
"nothing to paint yet — this is the reported state"
);
s.set_loot_rolls(rolls());
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
// No repaint: nothing re-enters `OnShow`, as in 1.12.
assert_eq!(
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
None,
"the name stays blank: the reference has no repaint path"
);
assert!(
!s.eval::<bool>("return GroupLootFrame1Decoration:IsShown()")
.unwrap(),
"and so does the BoP decoration"
);
s.resolve();
let quads = s.extract();
assert!(
!quads.iter().any(|q| {
matches!(&q.content, QuadContent::Texture { path: Some(p), .. }
if p.contains("INV_Staff_12"))
}),
"and no icon arrives either — same missing repaint, same one cause"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(
s.eval::<Option<String>>("return GroupLootFrame1Name:GetText()")
.unwrap(),
None
);
}
/// `UIPARENT_MANAGED_FRAME_POSITIONS["GroupLootFrame1"]` (baseY 60, bottomEither 42, pet 42;
/// `UIParent.lua:1577`) stacks the popups over the bottom bars; the pass finds the frame by name.
#[test]
fn managed_positions_engage_for_the_bare_frame_name() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
load_xml(&s, "Interface\\FrameXML\\Fonts.xml");
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.lua");
load_xml(&s, r"Interface\FrameXML\MoneyFrame.xml");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.lua");
load_xml(&s, r"Interface\FrameXML\UIPanelTemplates.xml");
load_xml(&s, r"Interface\FrameXML\GlobalStrings.lua");
load_xml(&s, r"Interface\FrameXML\BasicControls.xml");
load_xml(&s, r"Interface\FrameXML\LocaleProperties.lua");
// `GameTooltip.xml` brings `TOOLTIP_DEFAULT_COLOR`, which the dropdown lists read in their
// OnLoad (`UIDropDownMenuTemplates.xml:186`).
load_xml(&s, r"Interface\FrameXML\StaticPopup.xml");
load_xml(&s, "Interface\\FrameXML\\GameTooltip.xml");
load_group_loot(&s);
let bottom = |s: &UiScript| s.eval::<f64>("return GroupLootFrame1:GetBottom()").unwrap();
// No bars: the row's base, 60, which the XML anchor also gives.
s.run("UIParent_ManageFramePositions()").unwrap();
s.resolve();
assert_eq!(bottom(&s), 60.0, "baseY");
// The pass reads `SHOW_MULTI_ACTIONBAR_1`/`_2`, not the bars (`UIParent.lua:1599-1606`).
s.run("SHOW_MULTI_ACTIONBAR_1 = 1 SHOW_MULTI_ACTIONBAR_2 = 1 UIParent_ManageFramePositions()")
.unwrap();
s.resolve();
assert_eq!(bottom(&s), 102.0, "60 + bottomEither 42 — the row engaged");
// The stance bar shows too. The pass touches its three shelf textures by name, unguarded
// (`UIParent.lua:1705-1732`), so the stand-in bar needs stand-in textures.
s.run(
"local t = { Show = function() end, Hide = function() end } \
ShapeshiftBarLeft, ShapeshiftBarMiddle, ShapeshiftBarRight = t, t, t",
)
.unwrap();
s.run(
"ShapeshiftBarFrame = ShapeshiftBarFrame or CreateFrame(\"Frame\", \"ShapeshiftBarFrame\") \
ShapeshiftBarFrame:Show() UIParent_ManageFramePositions()",
)
.unwrap();
s.resolve();
assert_eq!(bottom(&s), 144.0, "60 + 42 + pet 42");
s.run("ShapeshiftBarFrame:Hide() UIParent_ManageFramePositions()")
.unwrap();
s.resolve();
assert_eq!(bottom(&s), 102.0, "back to the multibar-only stack");
}
/// The roll icon's OnClick (`LootFrame.xml:353-361`): ctrl dresses up the item and shift posts
/// its link to an open chat box, both through `GetLootRollItemLink`; the icon never votes.
#[test]
fn ctrl_and_shift_on_the_roll_icon_preview_and_post_its_link() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = setup();
load_group_loot(&s);
load_xml(&s, r"Interface\FrameXML\UIParent.xml");
load_xml(&s, "Interface\\FrameXML\\DressUpFrame.xml");
load_xml(&s, "Interface\\FrameXML\\UIMenu.xml"); // the kit the chat menus build from
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, r"Interface\FrameXML\LocaleProperties.lua");
load_xml(&s, "Interface\\FrameXML\\FloatingChatFrame.xml");
s.set_loot_rolls(rolls());
// Roll 8 is not bind-on-pickup, so the dice below vote without a confirm.
s.fire_event(
"START_LOOT_ROLL",
vec![ScriptValue::Int(8), ScriptValue::Int(60_000)],
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s.resolve();
let quads = s.extract();
let (x, y) = quad_center(&quads, "INV_Sword_04");
// A plain click does nothing: the handler has only the ctrl and shift arms.
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
assert!(
s.take_loot_roll_votes().is_empty() && s.take_loot_roll_confirms().is_empty(),
"an unmodified icon click votes nothing"
);
assert!(s.focus_editbox("ChatFrameEditBox"));
s.set_modifiers(true, false, false);
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
s.set_modifiers(false, false, false);
assert_eq!(
s.eval::<String>("return ChatFrameEditBox:GetText()")
.unwrap(),
SWORD_LINK,
"the rolled item's full escaped link landed in the chat box"
);
s.set_modifiers(false, true, false);
s.mouse_button(x, y, "LeftButton", true);
s.mouse_button(x, y, "LeftButton", false);
s.set_modifiers(false, false, false);
assert_eq!(
s.take_dressup_intents(),
vec![DressUpIntent::Dress, DressUpIntent::TryOn(25)],
"re-dress first, then try the rolled item on"
);
assert!(
s.take_loot_roll_votes().is_empty() && s.take_loot_roll_confirms().is_empty(),
"no modified icon click may vote"
);
let (nx, ny) = quad_center(&quads, "UI-GroupLoot-Dice-Up");
s.mouse_button(nx, ny, "LeftButton", true);
s.mouse_button(nx, ny, "LeftButton", false);
assert_eq!(
s.take_loot_roll_votes(),
vec![(8, 1)],
"the Need button still votes"
);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Addons inherit `GroupLootFrameTemplate` by its 1.12 name and reach for its children; the icon
/// is `IconFrameIcon` because it is nested in the `IconFrame` button (`LootFrame.xml:314`).
#[test]
fn the_roll_template_carries_the_reference_name_and_the_parts_addons_reach_for() {
let _data = benilla_formats::wow_data_or_skip!();
let s = setup();
load_group_loot(&s);
// `CreateFrame` raises on an unknown template, so this call is the check.
s.run(r#"Probe = CreateFrame("Frame", "ProbeRoll", nil, "GroupLootFrameTemplate")"#)
.expect("the reference-named template must resolve");
for part in [
"IconFrame",
"IconFrameIcon",
"Name",
"Corner",
"Decoration",
"Timer",
] {
assert!(
s.eval::<bool>(&format!(r#"return getglobal("ProbeRoll{part}") ~= nil"#))
.unwrap(),
"an addon inheriting the template must find ProbeRoll{part}"
);
}
// The timer is a StatusBar (`LootFrame.lua:253` sets its range).
s.run(r#"ProbeRollTimer:SetMinMaxValues(0, 60000)"#)
.expect("the timer must answer StatusBar verbs");
assert!(s
.eval::<bool>(r#"return getglobal("BenillaGroupLootFrameTemplate") == nil"#)
.unwrap());
}
/// The stock `GroupLootFrame_OnShow` reads `ITEM_QUALITY_COLORS[quality].r` with no fallback
/// (`LootFrame.lua:275-276`), so an unresolved or unknown roll must still answer a quality.
#[test]
fn the_stock_group_loot_frame_survives_an_in_flight_roll() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for f in super::test_ui::LOOT_UI {
load_xml(&s, f);
}
load_xml(&s, "Interface\\FrameXML\\LootFrame.xml");
s.set_loot_rolls(rolls());
// Roll 9 is in flight and roll 99 unknown: both answer the reference's miss tail, quality 1
// (`0x4c31a3`).
for roll in [9, 99] {
s.run(&format!("GroupLootFrame_OpenNewFrame({roll}, 55000)"))
.unwrap_or_else(|e| panic!("stock OpenNewFrame raised on roll {roll}: {e}"));
assert!(
s.errors().is_empty(),
"stock GroupLootFrame_OnShow raised on roll {roll}: {:?}",
s.errors()
);
}
assert!(s
.eval::<bool>("return GroupLootFrame1:IsVisible() and GroupLootFrame2:IsVisible()")
.unwrap());
// The name is painted Common, set at `LootFrame.lua:276`.
let painted: (f64, f64, f64) = s
.eval("local r, g, b = GroupLootFrame1Name:GetTextColor()\nreturn r, g, b")
.unwrap();
let common: (f64, f64, f64) = s
.eval("local r, g, b = GetItemQualityColor(1)\nreturn r, g, b")
.unwrap();
assert!(
(painted.0 - common.0).abs() < 1e-6
&& (painted.1 - common.1).abs() < 1e-6
&& (painted.2 - common.2).abs() < 1e-6,
"the stock roll popup paints the miss tail's Common, got {painted:?} want {common:?}"
);
}