The target-of-target rows only write their globals, as 1.12's options do (#2150)

1.12's options declare Show Target of Target as a bare uvar
(UIOptionsFrame.lua:65) and the mode is read on save
(UIOptionsFrame.lua:365); neither calls TargetofTarget_Update. The frame
follows from TargetFrame_OnUpdate and TargetofTarget_OnUpdate on the next
frame (TargetFrame.lua:492-501). Our rows called it as their applyFunc,
with the row as `this`; the function refreshes `this`'s debuffs
(RefreshBuffs(this, 0, "targettarget"), TargetFrame.lua:538), so with a
target's target up the switch raised "BuffFrame.lua:281: attempt to index
local 'debuffBorder' (a nil value)" (seen on a Linux client).

Verified: the_target_of_target_switch_leaves_the_frame_to_its_own_updates
(with a target's target up: no error, the frame shows after one tick on
and hides after one tick off) fails with the applyFunc put back, with
exactly that error, and passes without it; the options and tot_frame
tests pass on a deDE install; cargo test -p benilla-app --lib 5145,
clippy -D warnings, fmt. Built on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4d6f9ecc8a0d99e6d3ecf7fa4a8da229ebad92c2)
This commit is contained in:
andi79h-claude
2026-10-25 23:26:20 -06:00
committed by grom
parent 2b1e285101
commit 73f4bf808d
2 changed files with 59 additions and 10 deletions
@@ -2412,7 +2412,12 @@
</OnLoad></Scripts>
</Frame>
<!-- Saved globals in 1.12; the mode greys with its switch
(UIOptionsFrame.lua:697-701). -->
(UIOptionsFrame.lua:697-701). Neither row applies anything:
1.12's options only write the uvars (UIOptionsFrame.lua:65),
and the target frames' OnUpdates show or hide the frame on
the next frame (TargetFrame.lua:492-501). Called from a row,
TargetofTarget_Update would refresh the row's debuffs
(RefreshBuffs(this, ...), TargetFrame.lua:538). -->
<Frame name="$parentRowTargetOfTarget" inherits="BenillaOptionsCheckboxRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowStatusText" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
@@ -2421,7 +2426,6 @@
<Scripts><OnLoad>
BenillaOptionsGlobalRow_OnLoad(this, "SHOW_TARGET_OF_TARGET",
"Show Target of Target", "OPTION_TOOLTIP_SHOW_TARGET_OF_TARGET")
this.applyFunc = "TargetofTarget_Update"
</OnLoad></Scripts>
</Frame>
<!-- The label is ours, since 1.12 never sets this
@@ -2441,7 +2445,6 @@
{ text = "Party or Raid", value = "4", tip = "OPTION_TOOLTIP_TARGETOFTARGET4" },
{ text = "Always", value = "5", tip = "OPTION_TOOLTIP_TARGETOFTARGET5" },
})
this.applyFunc = "TargetofTarget_Update"
</OnLoad></Scripts>
</Frame>
<!-- PLAYER_FLAGS bits, API rows
@@ -3751,8 +3751,9 @@ fn the_interface_page_writes_the_three_stock_globals() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// The picker is dead while the switch is off (`UIOptionsFrame.lua:697-701`), both rows run
/// `TargetofTarget_Update` on a write, and the five entries are the reference's, in its order.
/// The picker is dead while the switch is off (`UIOptionsFrame.lua:697-701`), both rows only write
/// their globals, as 1.12's options do (`UIOptionsFrame.lua:65`), and the five entries are the
/// reference's, in its order.
#[test]
fn the_target_of_target_rows_gate_each_other_and_write_their_globals() {
benilla_formats::wow_data_or_skip!();
@@ -3807,13 +3808,11 @@ fn the_target_of_target_rows_gate_each_other_and_write_their_globals() {
);
assert!(
s.eval::<bool>(
"return BenillaOptionsFrameContainerBodyInterfaceRowTargetOfTarget.applyFunc \
== \"TargetofTarget_Update\" \
and BenillaOptionsFrameContainerBodyInterfaceRowTargetOfTargetMode.applyFunc \
== \"TargetofTarget_Update\""
"return BenillaOptionsFrameContainerBodyInterfaceRowTargetOfTarget.applyFunc == nil \
and BenillaOptionsFrameContainerBodyInterfaceRowTargetOfTargetMode.applyFunc == nil"
)
.unwrap(),
"both rows re-decide the frame when they are written"
"the frames' own OnUpdates re-decide the frame, as in 1.12"
);
s.run("BenillaOptionsFrameContainerBodyInterfaceRowTargetOfTargetModeDropdownButton:Click()")
@@ -3848,6 +3847,53 @@ fn the_target_of_target_rows_gate_each_other_and_write_their_globals() {
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// With a target's target up, the switch raises nothing and the frame follows on the next frame,
/// from `TargetFrame_OnUpdate` and `TargetofTarget_OnUpdate` (`TargetFrame.lua:492-501`). Run from
/// the row, `TargetofTarget_Update` refreshed the row's own debuffs (`RefreshBuffs(this, …)`,
/// `TargetFrame.lua:538`) and raised `BuffFrame.lua:281: attempt to index local 'debuffBorder'`.
#[test]
fn the_target_of_target_switch_leaves_the_frame_to_its_own_updates() {
use benilla_ui::script::UnitState;
benilla_formats::wow_data_or_skip!();
let mut s = interface_harness();
let unit = |name: &str, guid: u64| UnitState {
exists: true,
is_connected: true,
name: Some(name.into()),
guid,
health: 80,
max_health: 100,
level: 60,
..UnitState::default()
};
s.set_unit("player", Some(unit("Tri", 0x100)));
s.set_unit("target", Some(unit("Kobold Miner", 0x200)));
s.set_unit("targettarget", Some(unit("Tri", 0x100)));
s.fire_event("PLAYER_TARGET_CHANGED", vec![]);
s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap();
s.run("BenillaOptionsFrameCategoryListRowInterface:Click()")
.unwrap();
let shown = |s: &mut UiScript| {
s.eval::<bool>("return TargetofTargetFrame:IsShown() and true or false")
.unwrap()
};
s.run("BenillaOptionsFrameContainerBodyInterfaceRowTargetOfTargetCheck:Click()")
.unwrap();
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s.tick(0.016);
assert!(
shown(&mut s),
"on: the target frame's OnUpdate brings it up"
);
s.run("BenillaOptionsFrameContainerBodyInterfaceRowTargetOfTargetCheck:Click()")
.unwrap();
s.tick(0.016);
assert!(!shown(&mut s), "off: its own OnUpdate takes it down");
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
}
/// Show Cloak and Show Helm are bits of the character's `PLAYER_FLAGS`, read and written through
/// 1.12's `func`/`setFunc` pair (`UIOptionsFrame.lua:18-19`). The getter follows the click before
/// the server answers: the wire verb is a blind flip, so a second click must see the new state.