the interface labels announce their writes, and the idle tick goes quiet
- a text write names its own label, so the per-frame measure sweep drains a short ledger instead of hashing every font string twice — ~0.3 ms a frame at the Stormwind gates, gone - the idle interface tick stops re-writing settled state: buff durations and the weapon-enchant frame write only on change, and the fade and unit-popup drivers park until woken - the interface extract skips a hidden region before cloning its row, not after - WOW_UPLOAD_BUDGET=<MB> brackets the arrival burst's upload pacing — a lever for measuring world entry, not a fix yet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -750,6 +750,186 @@ fn the_temporary_enchant_row_shows_a_weapon_enchant_and_moves_the_top_row_aside(
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **The idle enchant row writes nothing** (decision 1396's class, the audit's item on
|
||||
/// `TemporaryEnchantFrame:OnUpdate` — 1396 measured it at ~14 µs/frame as the post-fix lead). With
|
||||
/// neither hand enchanted, `BuffFrame_Enchant_OnUpdate` used to clear both slots and re-park the
|
||||
/// bar every frame of every session; now the no-enchant branch latches on `benillaCleared` after
|
||||
/// its first pass.
|
||||
///
|
||||
/// The probes are sentinels the branch would overwrite if it still ran: the bar re-anchored 41px
|
||||
/// left of its park, and slot 1 shown. Ten ticks later both survive. Then the row is DRIVEN — both
|
||||
/// hands at once — proving the latch releases, the straight-line hand packer keeps the reference's
|
||||
/// off-hand-first order, and the `filled`-gated width/anchor writes fire on the 0→2 edge; the drop
|
||||
/// back to empty re-parks the bar (the first pass after a transition always writes).
|
||||
#[test]
|
||||
fn an_idle_enchant_row_stops_rewriting_the_bar() {
|
||||
let mut s = harness();
|
||||
push(&mut s, mixed_bar());
|
||||
frame(&mut s, 0.1); // first pass: the clear + park write once and latch
|
||||
frame(&mut s, 0.1);
|
||||
s.resolve();
|
||||
let resting = s.eval::<f64>("return BuffFrame:GetRight()").unwrap();
|
||||
|
||||
// Sentinels the no-enchant branch can never leave standing if it still writes.
|
||||
s.run(
|
||||
r#"BuffFrame:SetPoint("TOPRIGHT", "TemporaryEnchantFrame", "TOPRIGHT", -41, 0)
|
||||
TempEnchant1:Show()"#,
|
||||
)
|
||||
.unwrap();
|
||||
for _ in 0..10 {
|
||||
frame(&mut s, 0.1);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
assert!(
|
||||
shown(&s, "TempEnchant1"),
|
||||
"a latched idle row must not re-hide the slot every frame"
|
||||
);
|
||||
let displaced = s.eval::<f64>("return BuffFrame:GetRight()").unwrap();
|
||||
assert!(
|
||||
(displaced - (resting - 41.0)).abs() < 1e-3,
|
||||
"a latched idle row must not re-park the bar every frame — got {displaced}, resting {resting}"
|
||||
);
|
||||
|
||||
// The control: both hands enchant. Off hand packs slot 1 (the reference's order — main hand
|
||||
// sits to its LEFT in slot 2), the row is 64 wide, the bar starts one 5px gutter left of it.
|
||||
s.set_weapon_enchants(
|
||||
Some(benilla_ui::script::WeaponEnchant {
|
||||
remaining_ms: Some(480_000),
|
||||
charges: 0,
|
||||
}),
|
||||
Some(benilla_ui::script::WeaponEnchant {
|
||||
remaining_ms: Some(120_000),
|
||||
charges: 0,
|
||||
}),
|
||||
);
|
||||
frame(&mut s, 0.1);
|
||||
assert!(shown(&s, "TempEnchant1") && shown(&s, "TempEnchant2"));
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return TempEnchant1:GetID()").unwrap(),
|
||||
17,
|
||||
"off hand first: slot 1 is the OffHandSlot id"
|
||||
);
|
||||
assert_eq!(s.eval::<i64>("return TempEnchant2:GetID()").unwrap(), 16);
|
||||
assert_eq!(text(&s, "TempEnchant1Duration"), "2 m");
|
||||
assert_eq!(text(&s, "TempEnchant2Duration"), "8 m");
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return TemporaryEnchantFrame:GetWidth()")
|
||||
.unwrap(),
|
||||
64.0,
|
||||
"two 32px columns"
|
||||
);
|
||||
let both = s.eval::<f64>("return BuffFrame:GetRight()").unwrap();
|
||||
assert!(
|
||||
(resting - both - 69.0).abs() < 1e-3,
|
||||
"the bar clears both columns + the gutter: {resting} -> {both}"
|
||||
);
|
||||
|
||||
// And back to empty: the transition still writes (the latch re-arms, not re-fires).
|
||||
s.set_weapon_enchants(None, None);
|
||||
frame(&mut s, 0.1);
|
||||
assert!(!shown(&s, "TempEnchant1") && !shown(&s, "TempEnchant2"));
|
||||
s.resolve();
|
||||
let parked = s.eval::<f64>("return BuffFrame:GetRight()").unwrap();
|
||||
assert!(
|
||||
(parked - resting).abs() < 1e-3,
|
||||
"the drop-to-empty pass re-parks the bar: {parked} vs {resting}"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// **A settled buff button writes nothing per frame** (the same 1396 class, one row down): the
|
||||
/// poll (`GetPlayerBuffTimeLeft` every frame — load-bearing, decision 0846) stays, but the alpha
|
||||
/// and the duration-region writes are gated on what was last written. The probes are sentinels the
|
||||
/// handler can never produce: alpha 0.42 (it writes only 1.0 or the warning ramp, and at 5 min out
|
||||
/// the ramp is not running) and the text "X" (it writes only `SecondsToTimeAbbrev` strings — and
|
||||
/// the compare is against the computed STRING, never a measured extent, which is 1396's fixture
|
||||
/// trap). The controls: the minute rollover still rewrites the text, the warning band still turns
|
||||
/// the number white, and inside the last 31s the pulse still writes a fresh alpha per tick.
|
||||
#[test]
|
||||
fn a_settled_buff_button_stops_rewriting_alpha_and_duration() {
|
||||
let mut s = harness();
|
||||
push(
|
||||
&mut s,
|
||||
vec![aura(
|
||||
1126,
|
||||
"Mark of the Wild",
|
||||
"Interface\\Icons\\Spell_Nature_Regeneration",
|
||||
true,
|
||||
1,
|
||||
None,
|
||||
300.0,
|
||||
true,
|
||||
)],
|
||||
);
|
||||
frame(&mut s, 0.1); // the event repaint + the poll's first write ("5 m", alpha 1.0)
|
||||
frame(&mut s, 0.1);
|
||||
assert_eq!(text(&s, "BuffButton0Duration"), "5 m");
|
||||
|
||||
s.run(r#"BuffButton0:SetAlpha(0.42); BuffButton0Duration:SetText("X")"#)
|
||||
.unwrap();
|
||||
for _ in 0..10 {
|
||||
frame(&mut s, 0.016);
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
assert!(
|
||||
(alpha(&s, "BuffButton0") - 0.42).abs() < 1e-6,
|
||||
"a settled button must not rewrite its alpha every frame — got {}",
|
||||
alpha(&s, "BuffButton0")
|
||||
);
|
||||
assert_eq!(
|
||||
text(&s, "BuffButton0Duration"),
|
||||
"X",
|
||||
"a settled button must not rewrite its unchanged duration text every frame"
|
||||
);
|
||||
|
||||
// Control 1: a REAL text change flows — the abbreviation drops to "4 m" past the boundary.
|
||||
s.tick(60.0);
|
||||
assert_eq!(
|
||||
text(&s, "BuffButton0Duration"),
|
||||
"4 m",
|
||||
"the minute rollover overwrites the sentinel: the gate passes real changes"
|
||||
);
|
||||
|
||||
// Control 2: inside the last 31s the pulse writes a fresh ramp value every tick, and the
|
||||
// number wears the sub-minute HIGHLIGHT white (the band is part of the write tuple).
|
||||
s.tick(210.0); // ~29.5s left
|
||||
s.resolve();
|
||||
let a1 = alpha(&s, "BuffButton0");
|
||||
s.tick(0.2);
|
||||
let a2 = alpha(&s, "BuffButton0");
|
||||
s.tick(0.3);
|
||||
let a3 = alpha(&s, "BuffButton0");
|
||||
assert!(
|
||||
a1 != a2 && a2 != a3,
|
||||
"the warning pulse flows through the gate per tick: {a1} {a2} {a3}"
|
||||
);
|
||||
s.resolve();
|
||||
let seconds_text = text(&s, "BuffButton0Duration");
|
||||
assert!(
|
||||
seconds_text.ends_with(" s"),
|
||||
"inside the last minute the timer counts seconds: {seconds_text}"
|
||||
);
|
||||
let white = s.extract().iter().any(|q| match &q.content {
|
||||
QuadContent::Text {
|
||||
text: Some(t),
|
||||
color: Some(c),
|
||||
..
|
||||
} => {
|
||||
*t == seconds_text
|
||||
&& (c[0] - 1.0).abs() < 1e-3
|
||||
&& (c[1] - 1.0).abs() < 1e-3
|
||||
&& (c[2] - 1.0).abs() < 1e-3
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
assert!(
|
||||
white,
|
||||
"the sub-minute band rewrote the vertex color to HIGHLIGHT white"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// The duration line against the **real shipped strings**, end to end — the leg the engine's own
|
||||
/// synthetic-template tests cannot reach.
|
||||
///
|
||||
|
||||
@@ -716,3 +716,69 @@ fn hovering_the_dock_still_reveals_the_tabs() {
|
||||
"a stationary hover reveals the selected tab at full alpha — got {alpha}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **A view at the bottom writes nothing** — `ChatFrame_OnUpdate`'s at-bottom branch used to call
|
||||
/// `flash:Hide()` and zero `flashTimer` every frame; the 1.12 reference gates both behind
|
||||
/// `flash:IsVisible()` (1396's residual item 3, now restored — our `flashTimer = 0` rides inside
|
||||
/// the reference's gate, so a return-to-bottom during a hidden blink phase keeps the residual
|
||||
/// phase, a divergence that costs nothing at rest).
|
||||
///
|
||||
/// The probe is a sentinel the gated branch can only zero: `flashTimer = 0.42` with the flash
|
||||
/// hidden survives ten ticks. The controls: scrolled up, the bottom-button blink still runs its
|
||||
/// 0.5s toggle; and scrolling back while the flash is LIT still hides it and zeroes the timer.
|
||||
#[test]
|
||||
fn a_chat_view_at_the_bottom_stops_rewriting_the_flash() {
|
||||
let mut s = chat_frame();
|
||||
for t in ["L0", "L1", "L2"] {
|
||||
s.add_chat_message("ChatFrame1", t, 1.0, 1.0, 1.0);
|
||||
}
|
||||
for _ in 0..3 {
|
||||
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);
|
||||
s.resolve();
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
let timer: f64 = s.eval("return ChatFrame1.flashTimer").unwrap();
|
||||
assert!(
|
||||
(timer - 0.42).abs() < 1e-6,
|
||||
"a settled view must not rewrite flashTimer every frame — got {timer}"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return ChatFrame1BottomButtonFlash:IsVisible()")
|
||||
.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
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<bool>("return ChatFrame1BottomButtonFlash:IsVisible()")
|
||||
.unwrap(),
|
||||
"scrolled up, the bottom-button blink still 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.tick(0.016);
|
||||
s.resolve();
|
||||
assert!(
|
||||
!s.eval::<bool>("return ChatFrame1BottomButtonFlash:IsVisible()")
|
||||
.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)"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -979,3 +979,99 @@ fn an_addons_own_frame_registered_in_uipanelwindows_takes_the_left_slot() {
|
||||
s.run("HideUIPanel(AddonPanel)").unwrap();
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
// ── The fade kit's driver (UIFrameFade / BenillaFadeDriver) ──────────────────────────────────
|
||||
|
||||
/// **The idle fade driver parks itself** (decision 1396's class, the audit's driver-hide item):
|
||||
/// with `FADEFRAMES` empty there is nothing to walk, so the driver's OnUpdate hides its own frame
|
||||
/// — and the engine dispatches OnUpdate only to visible frames (`tick.rs`), which stops the
|
||||
/// per-frame walk cold. The probe wraps `UIFrameFadeUpdate` in a counter BEFORE the first tick:
|
||||
/// ten idle ticks later it has never run, and the driver is hidden.
|
||||
#[test]
|
||||
fn an_idle_fade_driver_parks_itself_off_the_tick() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
s.resolve();
|
||||
s.run(
|
||||
"BENILLA_TEST_FADE_TICKS = 0\n\
|
||||
local real = UIFrameFadeUpdate\n\
|
||||
function UIFrameFadeUpdate(elapsed)\n\
|
||||
BENILLA_TEST_FADE_TICKS = BENILLA_TEST_FADE_TICKS + 1\n\
|
||||
real(elapsed)\n\
|
||||
end",
|
||||
)
|
||||
.unwrap();
|
||||
for _ in 0..10 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaFadeDriver:IsShown()")
|
||||
.unwrap(),
|
||||
"an empty FADEFRAMES parks the driver"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BENILLA_TEST_FADE_TICKS").unwrap(),
|
||||
0,
|
||||
"a parked driver never runs the fade walk"
|
||||
);
|
||||
}
|
||||
|
||||
/// The control for the guard above: the gate must not cost the fades it gates. `UIFrameFade` (the
|
||||
/// kit's single insertion point — `UIFrameFadeIn`/`Out` both land there) wakes the driver beside
|
||||
/// its `table.insert`; the fade then ramps on real ticks, completes, and the driver re-parks on
|
||||
/// the first empty tick after.
|
||||
#[test]
|
||||
fn a_started_fade_still_ramps_and_the_driver_reparks_after() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
s.resolve();
|
||||
s.run(r#"CreateFrame("Frame", "BenillaFadeProbe")"#)
|
||||
.unwrap();
|
||||
for _ in 0..2 {
|
||||
s.tick(0.016); // settle: the driver parks
|
||||
s.resolve();
|
||||
}
|
||||
assert!(!s
|
||||
.eval::<bool>("return BenillaFadeDriver:IsShown()")
|
||||
.unwrap());
|
||||
|
||||
s.run("UIFrameFadeIn(BenillaFadeProbe, 1.0)").unwrap();
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaFadeDriver:IsShown()")
|
||||
.unwrap(),
|
||||
"starting a fade wakes the driver"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return BenillaFadeProbe:GetAlpha()").unwrap(),
|
||||
0.0,
|
||||
"the fade armed at its IN startAlpha"
|
||||
);
|
||||
|
||||
s.tick(0.5);
|
||||
s.resolve();
|
||||
let mid = s.eval::<f64>("return BenillaFadeProbe:GetAlpha()").unwrap();
|
||||
assert!(
|
||||
(mid - 0.5).abs() < 1e-3,
|
||||
"half the timeToFade in, half the ramp: {mid}"
|
||||
);
|
||||
|
||||
s.tick(0.6); // past timeToFade: the fade completes and leaves the list
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.eval::<f64>("return BenillaFadeProbe:GetAlpha()").unwrap(),
|
||||
1.0,
|
||||
"the fade completes at its endAlpha"
|
||||
);
|
||||
s.tick(0.016); // the first empty tick re-parks the driver
|
||||
s.resolve();
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaFadeDriver:IsShown()")
|
||||
.unwrap(),
|
||||
"the driver re-parks once the list is empty"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
@@ -727,6 +727,146 @@ fn the_pet_details_row_opens_the_pet_paper_doll() {
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
// ── The enable-pass driver (BenillaUnitPopupDriver) ─────────────────────────────────────────────
|
||||
|
||||
/// **The idle popup driver parks itself** (decision 1396's class, the audit's driver-hide item):
|
||||
/// with no unit menu of ours open, `UnitPopup_OnUpdate`'s early-out hides the driver's own frame —
|
||||
/// and the engine dispatches OnUpdate only to visible frames (`tick.rs`), so the per-frame scan
|
||||
/// stops entirely. `UnitPopup_ShowMenu` — the one opener every unit menu goes through (grep:
|
||||
/// UnitFrames/PartyFrame/FriendsFrame/ItemRef all call it) — wakes it again.
|
||||
///
|
||||
/// The probe wraps `UnitPopup_OnUpdate` in a counter: after the first tick parks the shipped-shown
|
||||
/// driver, ten idle ticks never run the pass. The control: an open target menu still runs the
|
||||
/// per-frame enable pass — a disconnected target's Whisper row greys, and greys back live when the
|
||||
/// unit reconnects mid-open — and closing the menu re-parks the driver.
|
||||
#[test]
|
||||
fn an_idle_unit_popup_driver_parks_itself_off_the_tick() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
bake_strings(&s);
|
||||
load_popup_frames(&s);
|
||||
|
||||
s.set_unit(
|
||||
"player",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
name: Some("Me".into()),
|
||||
is_player: true,
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
// A friendly player target, DISCONNECTED (is_connected defaults to false): the Whisper row's
|
||||
// grey predicate is live from the first enable pass.
|
||||
s.set_unit(
|
||||
"target",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
name: Some("Ally".into()),
|
||||
health: 40,
|
||||
max_health: 40,
|
||||
is_player: true,
|
||||
reaction: 5,
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "load errors: {:?}", s.errors());
|
||||
|
||||
s.run(
|
||||
"BENILLA_TEST_POPUP_TICKS = 0\n\
|
||||
local real = UnitPopup_OnUpdate\n\
|
||||
function UnitPopup_OnUpdate(elapsed)\n\
|
||||
BENILLA_TEST_POPUP_TICKS = BENILLA_TEST_POPUP_TICKS + 1\n\
|
||||
real(elapsed)\n\
|
||||
end",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The driver ships shown; the first tick's early-out parks it, and then nothing runs.
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaUnitPopupDriver:IsShown()")
|
||||
.unwrap(),
|
||||
"no menu open: the first tick parks the driver"
|
||||
);
|
||||
s.run("BENILLA_TEST_POPUP_TICKS = 0").unwrap();
|
||||
for _ in 0..10 {
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return BENILLA_TEST_POPUP_TICKS").unwrap(),
|
||||
0,
|
||||
"a parked driver never runs the enable pass"
|
||||
);
|
||||
|
||||
// Open the PLAYER menu through the real hit path: the driver wakes...
|
||||
let (cx, cy) = s
|
||||
.eval::<(f64, f64)>("return TargetFrame:GetCenter()")
|
||||
.unwrap();
|
||||
s.mouse_button(cx as f32, cy as f32, "RightButton", true);
|
||||
s.mouse_button(cx as f32, cy as f32, "RightButton", false);
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
|
||||
"the menu opened"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return BenillaUnitPopupDriver:IsShown()")
|
||||
.unwrap(),
|
||||
"UnitPopup_ShowMenu wakes the driver"
|
||||
);
|
||||
|
||||
// ...and the pass runs per frame: the disconnected target's Whisper row greys...
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<i64>("return BENILLA_TEST_POPUP_TICKS").unwrap() > 0,
|
||||
"an open menu ticks the enable pass"
|
||||
);
|
||||
assert!(
|
||||
!s.eval::<bool>("return DropDownList1Button2:IsEnabled()")
|
||||
.unwrap(),
|
||||
"Whisper greys against a disconnected target"
|
||||
);
|
||||
|
||||
// ...and un-greys LIVE when the unit reconnects while the menu is up — the whole reason the
|
||||
// pass is per-frame rather than baked at open.
|
||||
s.set_unit(
|
||||
"target",
|
||||
Some(UnitState {
|
||||
exists: true,
|
||||
name: Some("Ally".into()),
|
||||
health: 40,
|
||||
max_health: 40,
|
||||
is_player: true,
|
||||
reaction: 5,
|
||||
is_connected: true,
|
||||
..UnitState::default()
|
||||
}),
|
||||
);
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList1Button2:IsEnabled()")
|
||||
.unwrap(),
|
||||
"Whisper enables the frame the target reconnects"
|
||||
);
|
||||
|
||||
// Closing the menu re-parks the driver on its next tick.
|
||||
s.run("CloseDropDownMenus()").unwrap();
|
||||
s.tick(0.016);
|
||||
s.resolve();
|
||||
assert!(
|
||||
!s.eval::<bool>("return BenillaUnitPopupDriver:IsShown()")
|
||||
.unwrap(),
|
||||
"a closed menu parks the driver again"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
|
||||
/// Click an open dropdown row through the real hit path.
|
||||
fn click_row(s: &mut UiScript, row: u32) {
|
||||
click_frame(s, &format!("DropDownList1Button{row}"));
|
||||
|
||||
Reference in New Issue
Block a user