Files
scadaproj/ZB.MOM.WW.Theme/tests/ZB.MOM.WW.Theme.Tests/TechButtonExtensionsTests.cs
T
Joseph Doherty a2e2f4b463 fix(theme): TechButton honours a caller-supplied disabled; bump 0.5.1 (scadaproj#44)
The trailing disabled="@Busy" overrode the splatted attribute, so a caller's
disabled was dropped whenever Busy was false and the button stayed clickable.
The effective state is now Busy || caller disabled (Blazor splat semantics).

- tests: caller disabled (true/"disabled"/""), false, and re-render toggle
- Directory.Build.props 0.5.0 -> 0.5.1 (NOT packed or published)
- code-reviews: Theme-007 Resolved; README regenerated

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 14:44:14 -05:00

103 lines
3.8 KiB
C#

namespace ZB.MOM.WW.Theme.Tests;
public sealed class TechButtonExtensionsTests : BunitContext
{
[Theory]
[InlineData(ButtonSize.Default, null)]
[InlineData(ButtonSize.Small, "btn-sm")]
[InlineData(ButtonSize.Large, "btn-lg")]
public void Size_preserves_variant_and_composes_both_class_inputs(ButtonSize size, string? expected)
{
var cut = Render<TechButton>(p => p
.Add(x => x.Variant, ButtonVariant.Secondary)
.Add(x => x.Size, size)
.Add(x => x.Class, "explicit-action")
.Add(x => x.AdditionalAttributes, new Dictionary<string, object>
{
["class"] = "caller-action", ["aria-label"] = "Save changes"
})
.AddChildContent("Save"));
var button = cut.Find("button");
foreach (var cls in new[] { "btn", "btn-outline-secondary", "explicit-action", "caller-action" })
Assert.Contains(cls, button.ClassList);
Assert.Equal("Save changes", button.GetAttribute("aria-label"));
if (expected is not null) Assert.Contains(expected, button.ClassList);
else
{
Assert.DoesNotContain("btn-sm", button.ClassList);
Assert.DoesNotContain("btn-lg", button.ClassList);
}
}
[Fact]
public void Class_composition_keeps_busy_type_and_click_behavior()
{
var clicks = 0;
var attributes = new Dictionary<string, object>
{
["class"] = "caller", ["disabled"] = false,
["onclick"] = EventCallback.Factory.Create(this, () => clicks++)
};
var cut = Render<TechButton>(p => p
.Add(x => x.Class, "extra")
.Add(x => x.Type, "submit")
.Add(x => x.Busy, true)
.Add(x => x.AdditionalAttributes, attributes)
.AddChildContent("Save"));
Assert.True(cut.Find("button").HasAttribute("disabled"));
Assert.Equal("submit", cut.Find("button").GetAttribute("type"));
Assert.NotNull(cut.Find(".spinner-border"));
Assert.Equal("caller", attributes["class"]);
cut.Render(p => p.Add(x => x.Busy, false));
cut.Find("button").Click();
Assert.Equal(1, clicks);
Assert.DoesNotContain(cut.FindAll("span"), x => x.ClassList.Contains("spinner-border"));
}
[Theory]
[InlineData(true)]
[InlineData("disabled")]
[InlineData("")]
public void Caller_disabled_is_honoured_when_not_busy(object disabled)
{
// Theme-007 (scadaproj#44): the trailing disabled="@Busy" overwrote the splatted value, so
// a caller's disabled was silently dropped whenever Busy was false.
var cut = Render<TechButton>(p => p
.AddUnmatched("disabled", disabled)
.AddChildContent("Delete"));
var button = cut.Find("button");
Assert.True(button.HasAttribute("disabled"));
Assert.Empty(cut.FindAll(".spinner-border"));
}
[Fact]
public void Caller_disabled_false_leaves_the_button_enabled()
{
var cut = Render<TechButton>(p => p.AddUnmatched("disabled", false).AddChildContent("Go"));
Assert.False(cut.Find("button").HasAttribute("disabled"));
}
[Fact]
public void Caller_disabled_toggles_off_on_rerender()
{
var cut = Render<TechButton>(p => p.AddUnmatched("disabled", true).AddChildContent("Go"));
Assert.True(cut.Find("button").HasAttribute("disabled"));
cut.Render(p => p.AddUnmatched("disabled", false));
Assert.False(cut.Find("button").HasAttribute("disabled"));
}
[Fact]
public void Lowercase_class_attribute_preserves_required_button_classes()
{
var cut = Render<TechButton>(p => p.AddUnmatched("class", "caller").AddChildContent("Go"));
foreach (var cls in new[] { "btn", "btn-primary", "caller" })
Assert.Contains(cls, cut.Find("button").ClassList);
}
}