Files
2026-05-18 15:53:59 +03:30

362 lines
12 KiB
C#

using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Bunit;
using Bunit.JSInterop;
using SufiChain.SufiBlazor.Components.Forms;
using SufiChain.SufiBlazor.Localization;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Forms;
file class StubStringLocalizer : IStringLocalizer<SufiBlazorResource>
{
public LocalizedString this[string name] => new(name, name);
public LocalizedString this[string name, params object[] arguments] => new(name, string.Format(CultureInfo.InvariantCulture, name, arguments));
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures) => Array.Empty<LocalizedString>();
}
public class SbTimePickerTests : BunitContext
{
public SbTimePickerTests()
{
Services.AddSingleton<IStringLocalizer<SufiBlazorResource>>(new StubStringLocalizer());
JSInterop.Mode = JSRuntimeMode.Loose;
}
private IRenderedComponent<SbTimePicker> RenderTimePicker(
Action<ComponentParameterCollectionBuilder<SbTimePicker>>? configure = null)
{
return Render<SbTimePicker>(p => configure?.Invoke(p));
}
[Fact]
public void RendersTimePickerStructure()
{
// Arrange & Act
var cut = RenderTimePicker();
// Assert
var wrapper = cut.Find(".sb-timepicker");
Assert.NotNull(wrapper);
Assert.NotNull(cut.Find(".sb-timepicker__trigger"));
Assert.NotNull(cut.Find(".sb-timepicker__placeholder"));
Assert.NotNull(cut.Find(".sb-timepicker__icon"));
}
[Fact]
public void RendersLabelWhenProvided()
{
// Arrange & Act
var cut = RenderTimePicker(p => p
.Add(x => x.Label, "Start time")
.Add(x => x.Id, "start"));
// Assert
var label = cut.Find(".sb-timepicker__label");
Assert.NotNull(label);
Assert.Contains("Start time", label.TextContent);
Assert.Equal("start", label.GetAttribute("for"));
}
[Fact]
public void DoesNotRenderLabelWhenEmpty()
{
// Arrange & Act
var cut = RenderTimePicker();
// Assert
Assert.Empty(cut.FindAll(".sb-timepicker__label"));
}
[Fact]
public void RendersRequiredIndicatorWhenRequiredTrue()
{
// Arrange & Act
var cut = RenderTimePicker(p => p
.Add(x => x.Label, "Time")
.Add(x => x.Required, true));
// Assert
var required = cut.Find(".sb-timepicker__required");
Assert.NotNull(required);
Assert.Equal("*", required.TextContent);
}
[Fact]
public void UsesLocalizedPlaceholderWhenPlaceholderNull()
{
// Arrange & Act
var cut = RenderTimePicker();
// Assert - StubStringLocalizer returns key as value
var placeholder = cut.Find(".sb-timepicker__placeholder");
Assert.NotNull(placeholder);
Assert.Equal("SelectTime_Placeholder", placeholder.TextContent);
}
[Fact]
public void RendersCustomPlaceholderWhenProvided()
{
// Arrange & Act
var cut = RenderTimePicker(p => p.Add(x => x.Placeholder, "Choose time..."));
// Assert
var placeholder = cut.Find(".sb-timepicker__placeholder");
Assert.NotNull(placeholder);
Assert.Equal("Choose time...", placeholder.TextContent);
}
[Fact]
public void DisplaysValueIn24HourFormat()
{
// Arrange & Act
var time = new TimeOnly(14, 30, 0);
var cut = RenderTimePicker(p => p.Add(x => x.Value, time));
// Assert
var valueSpan = cut.Find(".sb-timepicker__value");
Assert.NotNull(valueSpan);
Assert.Contains("14", valueSpan.TextContent);
Assert.Contains("30", valueSpan.TextContent);
}
[Fact]
public void DisplaysValueIn12HourFormatWhenUse24HourFalse()
{
// Arrange & Act
var time = new TimeOnly(14, 30, 0);
var cut = RenderTimePicker(p => p
.Add(x => x.Value, time)
.Add(x => x.Use24Hour, false));
// Assert - 14:30 in 12h = 2:30 PM
var valueSpan = cut.Find(".sb-timepicker__value");
Assert.NotNull(valueSpan);
Assert.Contains("PM", valueSpan.TextContent);
}
[Fact]
public void DisplaysSecondsWhenShowSecondsTrue()
{
// Arrange & Act
var time = new TimeOnly(9, 5, 45);
var cut = RenderTimePicker(p => p
.Add(x => x.Value, time)
.Add(x => x.ShowSeconds, true));
// Assert
var valueSpan = cut.Find(".sb-timepicker__value");
Assert.NotNull(valueSpan);
Assert.Contains("45", valueSpan.TextContent);
}
[Fact]
public void RendersClearButtonWhenClearableAndValueSet()
{
// Arrange & Act
var cut = RenderTimePicker(p => p
.Add(x => x.Value, new TimeOnly(10, 0, 0))
.Add(x => x.Clearable, true));
// Assert
var clearBtn = cut.Find(".sb-timepicker__clear");
Assert.NotNull(clearBtn);
Assert.Equal("ClearTime", clearBtn.GetAttribute("aria-label"));
}
[Fact]
public void DoesNotRenderClearButtonWhenValueEmpty()
{
// Arrange & Act
var cut = RenderTimePicker(p => p.Add(x => x.Clearable, true));
// Assert
Assert.Empty(cut.FindAll(".sb-timepicker__clear"));
}
[Fact]
public void TriggerHasDisabledClassWhenDisabledTrue()
{
// Arrange & Act
var cut = RenderTimePicker(p => p.Add(x => x.Disabled, true));
// Assert
var trigger = cut.Find(".sb-timepicker__trigger");
Assert.Contains("sb-timepicker__trigger--disabled", trigger.ClassList);
}
[Fact]
public void AppliesClassParameter()
{
// Arrange & Act
var cut = RenderTimePicker(p => p.Add(x => x.Class, "my-timepicker"));
// Assert
var wrapper = cut.Find(".sb-timepicker");
Assert.Contains("my-timepicker", wrapper.ClassList);
}
[Fact]
public async Task OpensDropdownWhenTriggerClicked()
{
// Arrange & Act
var cut = RenderTimePicker();
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Assert
Assert.NotNull(cut.Find(".sb-timepicker__dropdown"));
Assert.NotNull(cut.Find(".sb-timepicker__selectors"));
Assert.NotNull(cut.Find(".sb-timepicker__footer"));
}
[Fact]
public async Task DoesNotOpenWhenDisabled()
{
// Arrange & Act
var cut = RenderTimePicker(p => p.Add(x => x.Disabled, true));
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Assert
Assert.Empty(cut.FindAll(".sb-timepicker__dropdown"));
}
[Fact]
public async Task InvokesValueChangedWhenApplyClicked()
{
// Arrange - preset Value so dropdown opens with 14:30 selected; Apply confirms it
TimeOnly? received = null;
var cut = RenderTimePicker(p => p
.Add(x => x.Value, new TimeOnly(14, 30, 0))
.Add(x => x.ValueChanged, EventCallback.Factory.Create<TimeOnly?>(this, v => received = v)));
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
var applyBtn = cut.Find(".sb-timepicker__apply-btn");
await cut.InvokeAsync(() => applyBtn!.Click());
// Assert
Assert.NotNull(received);
Assert.Equal(14, received!.Value.Hour);
Assert.Equal(30, received.Value.Minute);
}
[Fact]
public async Task InvokesValueChangedWhenNowClicked()
{
// Arrange
TimeOnly? received = null;
var cut = RenderTimePicker(p => p
.Add(x => x.ValueChanged, EventCallback.Factory.Create<TimeOnly?>(this, v => received = v)));
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
var nowBtn = cut.Find(".sb-timepicker__now-btn");
Assert.NotNull(nowBtn);
await cut.InvokeAsync(() => nowBtn!.Click());
// Assert
Assert.NotNull(received);
var expected = TimeOnly.FromDateTime(DateTime.Now);
Assert.Equal(expected.Hour, received!.Value.Hour);
Assert.Equal(expected.Minute, received.Value.Minute);
}
[Fact]
public async Task InvokesValueChangedNullWhenClearClicked()
{
// Arrange
TimeOnly? received = new TimeOnly(10, 30, 0);
var cut = RenderTimePicker(p => p
.Add(x => x.Value, new TimeOnly(10, 30, 0))
.Add(x => x.Clearable, true)
.Add(x => x.ValueChanged, EventCallback.Factory.Create<TimeOnly?>(this, v => received = v)));
var clearBtn = cut.Find(".sb-timepicker__clear");
await cut.InvokeAsync(() => clearBtn!.Click());
// Assert
Assert.Null(received);
}
[Fact]
public async Task ShowsPeriodColumnWhenUse24HourFalse()
{
// Arrange & Act
var cut = RenderTimePicker(p => p.Add(x => x.Use24Hour, false));
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Assert - AM/PM options
var periodOptions = cut.FindAll(".sb-timepicker__option")
.Where(o => o.TextContent.Trim() is "AM" or "PM").ToList();
Assert.Equal(2, periodOptions.Count);
}
[Fact]
public async Task InvokesValueChangedWithCorrectTimeWhen12HourAndPMSelected()
{
// Arrange
TimeOnly? received = null;
var cut = RenderTimePicker(p => p
.Add(x => x.Use24Hour, false)
.Add(x => x.ValueChanged, EventCallback.Factory.Create<TimeOnly?>(this, v => received = v)));
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Select hour 2, minute 30, PM
var hour2 = cut.FindAll(".sb-timepicker__option").FirstOrDefault(b => b.TextContent.Trim() == "02");
Assert.NotNull(hour2);
await cut.InvokeAsync(() => hour2!.Click());
var minute30 = cut.FindAll(".sb-timepicker__option").FirstOrDefault(b => b.TextContent.Trim() == "30");
Assert.NotNull(minute30);
await cut.InvokeAsync(() => minute30!.Click());
var pmBtn = cut.FindAll(".sb-timepicker__option").FirstOrDefault(b => b.TextContent.Trim() == "PM");
Assert.NotNull(pmBtn);
await cut.InvokeAsync(() => pmBtn!.Click());
var applyBtn = cut.Find(".sb-timepicker__apply-btn");
await cut.InvokeAsync(() => applyBtn!.Click());
// Assert - 2:30 PM = 14:30
Assert.NotNull(received);
Assert.Equal(14, received!.Value.Hour);
Assert.Equal(30, received.Value.Minute);
}
[Fact]
public async Task ShowsSecondsColumnWhenShowSecondsTrue()
{
// Arrange & Act
var cut = RenderTimePicker(p => p.Add(x => x.ShowSeconds, true));
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Assert - column headers include Second
var headers = cut.FindAll(".sb-timepicker__column-header");
Assert.Contains(headers, h => h.TextContent.Contains("Second"));
}
[Fact]
public async Task RespectsMinuteStep()
{
// Arrange - with MinuteStep 15: 4 minute options (00, 15, 30, 45). 24 hours = 28 total options.
// With default step 1: 60 minute options. 24 + 60 = 84 total.
var cut = RenderTimePicker(p => p.Add(x => x.MinuteStep, 15));
var trigger = cut.Find(".sb-timepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Assert
var allOptions = cut.FindAll(".sb-timepicker__option");
Assert.Equal(28, allOptions.Count); // 24 hours + 4 minutes
}
}