362 lines
12 KiB
C#
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
|
|
}
|
|
}
|