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 { 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 GetAllStrings(bool includeParentCultures) => Array.Empty(); } public class SbTimePickerTests : BunitContext { public SbTimePickerTests() { Services.AddSingleton>(new StubStringLocalizer()); JSInterop.Mode = JSRuntimeMode.Loose; } private IRenderedComponent RenderTimePicker( Action>? configure = null) { return Render(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(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(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(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(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 } }