Files
benilla-octo/crates/benilla-ui/src/script/tests/button.rs
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

397 lines
14 KiB
Rust

//! Button / CheckButton: method sets, state textures, click registration, additive highlight.
use super::common::script;
use crate::script::*;
#[test]
fn button_methods_exist_only_on_buttons() {
let s = script();
s.run(
r#"
f = CreateFrame("Frame", "PlainF")
b = CreateFrame("Button", "Btn")
cb = CreateFrame("CheckButton", "CBtn")
"#,
)
.unwrap();
// Duck-typing honesty across the class chain (RF-28 method sets).
assert!(s.eval::<bool>("return f.SetText == nil").unwrap());
assert!(s.eval::<bool>("return f.SetChecked == nil").unwrap());
assert!(s.eval::<bool>("return b.SetText ~= nil").unwrap());
assert!(s.eval::<bool>("return b.SetChecked == nil").unwrap());
assert!(s.eval::<bool>("return b.SetValue == nil").unwrap());
assert!(s.eval::<bool>("return cb.SetChecked ~= nil").unwrap());
assert!(s.eval::<bool>("return cb.SetNormalTexture ~= nil").unwrap());
// Buttons are mouse-enabled by construction (the client ctor enables input).
assert!(s.eval::<bool>("return b:IsMouseEnabled()").unwrap());
assert!(!s.eval::<bool>("return f:IsMouseEnabled()").unwrap());
}
#[test]
fn button_state_textures_switch_with_interaction() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local b = CreateFrame("Button", "StateBtn")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetNormalTexture("Interface\\N.blp")
b:SetPushedTexture("Interface\\P.blp")
b:SetDisabledTexture("Interface\\D.blp")
b:SetHighlightTexture("Interface\\H.blp")
b:SetText("Go")
"#,
)
.unwrap();
s.resolve();
let visible = |s: &UiScript| -> Vec<String> {
s.extract()
.iter()
.filter_map(|q| match &q.content {
QuadContent::Texture { path: Some(p), .. } => Some(p.clone()),
_ => None,
})
.collect()
};
// Idle: normal only (pushed/disabled suppressed; not hovered ⇒ no highlight).
assert_eq!(visible(&s), vec!["Interface\\N.blp".to_string()]);
// The ButtonText always draws.
assert!(s
.extract()
.iter()
.any(|q| matches!(&q.content, QuadContent::Text { text: Some(t), .. } if t == "Go")));
// Hover: normal + additive highlight.
s.mouse_move(50.0, 50.0);
let v = visible(&s);
assert!(
v.contains(&"Interface\\N.blp".to_string()) && v.contains(&"Interface\\H.blp".to_string())
);
// Held down over it: pushed replaces normal (highlight still hovering).
s.mouse_button(50.0, 50.0, "LeftButton", true);
let v = visible(&s);
assert!(
v.contains(&"Interface\\P.blp".to_string()) && !v.contains(&"Interface\\N.blp".to_string())
);
s.mouse_button(50.0, 50.0, "LeftButton", false);
// Disabled: disabled texture only, and no highlight even under the cursor.
s.run("StateBtn:Disable()").unwrap();
assert_eq!(visible(&s), vec!["Interface\\D.blp".to_string()]);
assert!(!s.eval::<bool>("return StateBtn:IsEnabled()").unwrap());
s.run("StateBtn:Enable()").unwrap();
assert_eq!(s.eval::<String>("return StateBtn:GetText()").unwrap(), "Go");
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// `SetButtonState`/`GetButtonState` (`0x780270`/`0x780180`) — the scripted press state the ref's
/// `ActionButtonDown/Up` keybind pair drives: PUSHED shows the pushed texture with no mouse
/// involved, NORMAL restores, unknown states error, and a disabled button answers DISABLED.
#[test]
fn set_button_state_drives_the_pushed_visual() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local b = CreateFrame("Button", "PushBtn")
b:SetPoint("BOTTOMLEFT", 0, 0); b:SetSize(100, 100)
b:SetNormalTexture("Interface\\N.blp")
b:SetPushedTexture("Interface\\P.blp")
"#,
)
.unwrap();
s.resolve();
let shows = |s: &UiScript, path: &str| {
s.extract()
.iter()
.any(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == path))
};
assert_eq!(
s.eval::<String>("return PushBtn:GetButtonState()").unwrap(),
"NORMAL"
);
s.run(r#"PushBtn:SetButtonState("PUSHED")"#).unwrap();
assert_eq!(
s.eval::<String>("return PushBtn:GetButtonState()").unwrap(),
"PUSHED"
);
assert!(shows(&s, "Interface\\P.blp") && !shows(&s, "Interface\\N.blp"));
s.run(r#"PushBtn:SetButtonState("NORMAL")"#).unwrap();
assert!(shows(&s, "Interface\\N.blp") && !shows(&s, "Interface\\P.blp"));
// Unknown state: a runtime error, not a silent no-op.
assert!(s.run(r#"PushBtn:SetButtonState("SIDEWAYS")"#).is_err());
// Disabled wins the read (Enable/Disable owns that state, not SetButtonState).
s.run("PushBtn:Disable()").unwrap();
assert_eq!(
s.eval::<String>("return PushBtn:GetButtonState()").unwrap(),
"DISABLED"
);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn disabled_button_swallows_clicks_checkbutton_toggles_before_onclick() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
clicks, seen_checked = 0, nil
local cb = CreateFrame("CheckButton", "Toggler")
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
cb:SetScript("OnClick", function(self, button, down)
clicks = clicks + 1
seen_checked = self:GetChecked()
end)
"#,
)
.unwrap();
s.resolve();
// Physical click: the checked state flips BEFORE OnClick (the documented contract).
s.mouse_button(50.0, 50.0, "LeftButton", true);
s.mouse_button(50.0, 50.0, "LeftButton", false);
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
assert!(s.eval::<bool>("return seen_checked == true").unwrap());
assert!(s.eval::<bool>("return Toggler:GetChecked()").unwrap());
// Programmatic Click() rides the same path: toggles back off.
s.run("Toggler:Click()").unwrap();
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 2);
assert!(!s.eval::<bool>("return Toggler:GetChecked()").unwrap());
// Disabled: the click is swallowed — no OnClick, no toggle.
s.run("Toggler:Disable()").unwrap();
s.mouse_button(50.0, 50.0, "LeftButton", true);
s.mouse_button(50.0, 50.0, "LeftButton", false);
s.run("Toggler:Click()").unwrap();
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 2);
assert!(!s.eval::<bool>("return Toggler:GetChecked()").unwrap());
// SetChecked takes 1/true/nil (1.12 and Era forms).
s.run("Toggler:SetChecked(1)").unwrap();
assert!(s.eval::<bool>("return Toggler:GetChecked()").unwrap());
s.run("Toggler:SetChecked(nil)").unwrap();
assert!(!s.eval::<bool>("return Toggler:GetChecked()").unwrap());
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn default_registration_is_left_click_only_right_click_reaches_nothing() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
clicks = 0
local btn = CreateFrame("Button", "Vendor")
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
btn:SetScript("OnClick", function(self, button, down) clicks = clicks + 1 end)
"#,
)
.unwrap();
s.resolve();
// A right-click (press+release, same frame) reaches OnMouseDown/OnMouseUp but never OnClick —
// the client's own default registered-click set is {"LeftButtonUp"} only.
s.mouse_button(50.0, 50.0, "RightButton", true);
s.mouse_button(50.0, 50.0, "RightButton", false);
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 0);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn register_for_clicks_grows_right_click_and_carries_the_button_name() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
clicks, click_btn, arg1_btn = 0, nil, nil
local btn = CreateFrame("Button", "Vendor")
btn:SetPoint("BOTTOMLEFT", 0, 0); btn:SetSize(100, 100)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:SetScript("OnClick", function(self, button, down)
clicks = clicks + 1
click_btn = button
arg1_btn = arg1 -- the 1.12 legacy-global convention, same value
end)
"#,
)
.unwrap();
s.resolve();
s.mouse_button(50.0, 50.0, "RightButton", true);
s.mouse_button(50.0, 50.0, "RightButton", false);
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
assert_eq!(s.eval::<String>("return click_btn").unwrap(), "RightButton");
assert_eq!(s.eval::<String>("return arg1_btn").unwrap(), "RightButton");
// RegisterForClicks replaces the set with its whole vararg list — LeftButtonUp still fires.
s.mouse_button(50.0, 50.0, "LeftButton", true);
s.mouse_button(50.0, 50.0, "LeftButton", false);
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 2);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn down_registration_fires_on_press_and_toggles_checked_once() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
clicks, seen_down = 0, nil
local cb = CreateFrame("CheckButton", "QuickSell")
cb:SetPoint("BOTTOMLEFT", 0, 0); cb:SetSize(100, 100)
cb:RegisterForClicks("LeftButtonDown")
cb:SetScript("OnClick", function(self, button, down)
clicks = clicks + 1
seen_down = down
end)
"#,
)
.unwrap();
s.resolve();
// The press alone fires OnClick (down=true) and toggles Checked exactly once.
s.mouse_button(50.0, 50.0, "LeftButton", true);
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
assert!(s.eval::<bool>("return seen_down == true").unwrap());
assert!(s.eval::<bool>("return QuickSell:GetChecked()").unwrap());
// The release does not fire again — "LeftButtonUp" is no longer in the registered set
// (RegisterForClicks replaced it, it did not add to it).
s.mouse_button(50.0, 50.0, "LeftButton", false);
assert_eq!(s.eval::<i64>("return clicks").unwrap(), 1);
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
#[test]
fn highlight_is_additive_and_sized_regions_center() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.run(
r#"
local b = CreateFrame("Button", "AddBtn")
b:SetPoint("BOTTOMLEFT", 100, 100); b:SetSize(36, 36)
b:SetNormalTexture("Interface\\Ring.blp")
b:GetNormalTexture():SetSize(64, 64)
b:SetHighlightTexture("Interface\\Hi.blp")
"#,
)
.unwrap();
s.resolve();
s.mouse_move(118.0, 118.0); // hover so the highlight draws
let quads = s.extract();
let find = |path: &str| {
quads
.iter()
.find(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == path))
.expect(path)
};
// The highlight carries the ADD contract (the client's SetHighlightTexture default).
assert!(matches!(
&find("Interface\\Hi.blp").content,
QuadContent::Texture { additive: true, .. }
));
// The sized ring centers on the 36px button: 100..136 → 64px box at 86..150.
let r = find("Interface\\Ring.blp").rect.unwrap();
assert_eq!(
(r.left, r.right, r.bottom, r.top),
(86.0, 150.0, 86.0, 150.0)
);
// SetBlendMode overrides back to straight alpha.
s.run(r#"AddBtn:GetHighlightTexture():SetBlendMode("BLEND")"#)
.unwrap();
let quads = s.extract();
let hi = quads
.iter()
.find(|q| matches!(&q.content, QuadContent::Texture { path: Some(p), .. } if p == "Interface\\Hi.blp"))
.unwrap();
assert!(matches!(
&hi.content,
QuadContent::Texture {
additive: false,
..
}
));
assert!(s.errors().is_empty(), "{:?}", s.errors());
}
/// The per-state label fonts (`SetTextFontObject`/`SetDisabledFontObject`, XML `<NormalFont>` etc.):
/// the ButtonText re-points to the CURRENT state's font object at extract — gold while enabled,
/// gray after Disable() — with no Lua repaint (the client's UIPanelButtonTemplate label behavior).
/// An explicit SetTextColor survives the re-point (the client's explicitly-set mask).
#[test]
fn button_label_repaints_by_state_font_object() {
let mut s = script();
s.set_screen_size(800.0, 600.0);
s.register_font_object(
"GoldFont",
FontObject {
color: Some([1.0, 0.82, 0.0, 1.0]),
height: Some(12.0),
..Default::default()
},
);
s.register_font_object(
"GrayFont",
FontObject {
color: Some([0.5, 0.5, 0.5, 1.0]),
height: Some(12.0),
..Default::default()
},
);
s.run(
r#"
b = CreateFrame("Button", "FontBtn")
b:SetPoint("CENTER", 0, 0); b:SetSize(100, 20)
b:SetText("Label")
b:SetTextFontObject("GoldFont")
b:SetDisabledFontObject("GrayFont")
"#,
)
.unwrap();
let label_color = |s: &mut crate::script::UiScript| {
s.resolve();
s.extract()
.into_iter()
.find_map(|q| match q.content {
QuadContent::Text {
text: Some(t),
color,
..
} if t == "Label" => Some(color),
_ => None,
})
.expect("label text quad")
};
assert_eq!(
label_color(&mut s),
Some([1.0, 0.82, 0.0, 1.0]),
"enabled: gold"
);
s.run("b:Disable()").unwrap();
assert_eq!(
label_color(&mut s),
Some([0.5, 0.5, 0.5, 1.0]),
"disabled: gray"
);
s.run("b:Enable()").unwrap();
assert_eq!(
label_color(&mut s),
Some([1.0, 0.82, 0.0, 1.0]),
"re-enabled: gold again"
);
// An explicit SetTextColor wins over the state font's color (explicitly-set mask).
s.run("b:GetFontString():SetTextColor(0.1, 0.2, 0.3)")
.unwrap();
let c = label_color(&mut s).expect("colored");
assert!((c[0] - 0.1).abs() < 1e-6 && (c[1] - 0.2).abs() < 1e-6);
}