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 SufiChain.SufiBlazor.Utilities.DateUtils;
using Xunit;
namespace SufiChain.SufiBlazor.Tests.Components.Forms;
///
/// Stub localizer for SbDatePicker tests.
///
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 SbDatePickerTests : BunitContext
{
public SbDatePickerTests()
{
Services.AddSingleton>(new StubStringLocalizer());
JSInterop.Mode = JSRuntimeMode.Loose;
}
private IRenderedComponent RenderDatePicker(
Action>? configure = null)
{
return Render(p =>
{
p.Add(x => x.CalendarSystem, SbCalendarSystem.Gregorian); // Predictable calendar for tests
configure?.Invoke(p);
});
}
[Fact]
public void RendersDatePickerStructure()
{
// Arrange & Act
var cut = RenderDatePicker();
// Assert
var wrapper = cut.Find(".sb-datepicker");
Assert.NotNull(wrapper);
Assert.NotNull(cut.Find(".sb-datepicker__trigger"));
Assert.NotNull(cut.Find(".sb-datepicker__placeholder"));
Assert.NotNull(cut.Find(".sb-datepicker__icon"));
}
[Fact]
public void RendersLabelWhenProvided()
{
// Arrange & Act
var cut = RenderDatePicker(p => p.Add(x => x.Label, "Start date"));
// Assert
var label = cut.Find(".sb-datepicker__label");
Assert.NotNull(label);
Assert.Contains("Start date", label.TextContent);
}
[Fact]
public void DoesNotRenderLabelWhenEmpty()
{
// Arrange & Act
var cut = RenderDatePicker();
// Assert
Assert.Empty(cut.FindAll(".sb-datepicker__label"));
}
[Fact]
public void DisplaysValueWhenValueSet()
{
// Arrange - use a fixed date for predictable output
var date = new DateOnly(2025, 3, 15);
// Act
var cut = RenderDatePicker(p => p.Add(x => x.Value, date));
// Assert - value span shows formatted date
var valueSpan = cut.Find(".sb-datepicker__value");
Assert.NotNull(valueSpan);
Assert.Contains("15", valueSpan.TextContent);
Assert.Contains("2025", valueSpan.TextContent);
}
[Fact]
public void UsesLocalizedPlaceholderWhenPlaceholderNull()
{
// Arrange & Act
var cut = RenderDatePicker();
// Assert - StubStringLocalizer returns key as value
var placeholderSpan = cut.Find(".sb-datepicker__placeholder");
Assert.NotNull(placeholderSpan);
Assert.Equal("SelectDate_Placeholder", placeholderSpan.TextContent);
}
[Fact]
public void RendersPlaceholderWhenProvided()
{
// Arrange & Act
var cut = RenderDatePicker(p => p.Add(x => x.Placeholder, "Pick a date..."));
// Assert
var placeholderSpan = cut.Find(".sb-datepicker__placeholder");
Assert.NotNull(placeholderSpan);
Assert.Equal("Pick a date...", placeholderSpan.TextContent);
}
[Fact]
public void TriggerHasDisabledClassWhenDisabledTrue()
{
// Arrange & Act
var cut = RenderDatePicker(p => p.Add(x => x.Disabled, true));
// Assert
var trigger = cut.Find(".sb-datepicker__trigger");
Assert.Contains("sb-datepicker__trigger--disabled", trigger.ClassList);
}
[Fact]
public void DoesNotRenderClearButtonWhenValueEmpty()
{
// Arrange & Act
var cut = RenderDatePicker();
// Assert
Assert.Empty(cut.FindAll(".sb-datepicker__clear"));
}
[Fact]
public void RendersClearButtonWhenValueSetAndClearable()
{
// Arrange & Act
var cut = RenderDatePicker(p => p
.Add(x => x.Value, new DateOnly(2025, 1, 10))
.Add(x => x.Clearable, true));
// Assert
var clearBtn = cut.Find(".sb-datepicker__clear");
Assert.NotNull(clearBtn);
Assert.Equal("ClearDate", clearBtn.GetAttribute("aria-label"));
}
[Fact]
public void DoesNotRenderClearButtonWhenClearableFalse()
{
// Arrange & Act
var cut = RenderDatePicker(p => p
.Add(x => x.Value, new DateOnly(2025, 1, 10))
.Add(x => x.Clearable, false));
// Assert
Assert.Empty(cut.FindAll(".sb-datepicker__clear"));
}
[Fact]
public async Task InvokesValueChangedWhenClearClicked()
{
// Arrange
DateOnly? received = null;
var cut = RenderDatePicker(p => p
.Add(x => x.Value, new DateOnly(2025, 1, 10))
.Add(x => x.Clearable, true)
.Add(x => x.ValueChanged, EventCallback.Factory.Create(this, v => received = v)));
// Act
var clearBtn = cut.Find(".sb-datepicker__clear");
await cut.InvokeAsync(() => clearBtn!.Click());
// Assert
Assert.Null(received);
}
[Fact]
public async Task OpensDropdownWhenTriggerClicked()
{
// Arrange & Act
var cut = RenderDatePicker();
var trigger = cut.Find(".sb-datepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Assert
Assert.NotNull(cut.Find(".sb-datepicker__dropdown"));
Assert.NotNull(cut.Find(".sb-datepicker__header"));
Assert.NotNull(cut.Find(".sb-datepicker__day-names"));
Assert.NotNull(cut.Find(".sb-datepicker__days"));
Assert.NotNull(cut.Find(".sb-datepicker__footer"));
}
[Fact]
public async Task DoesNotOpenWhenDisabled()
{
// Arrange & Act
var cut = RenderDatePicker(p => p.Add(x => x.Disabled, true));
var trigger = cut.Find(".sb-datepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Assert
Assert.Empty(cut.FindAll(".sb-datepicker__dropdown"));
}
[Fact]
public async Task InvokesValueChangedWhenTodayClicked()
{
// Arrange
DateOnly? received = null;
var cut = RenderDatePicker(p => p
.Add(x => x.ValueChanged, EventCallback.Factory.Create(this, v => received = v)));
// Act - open dropdown, click Today button
var trigger = cut.Find(".sb-datepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
var todayBtn = cut.Find(".sb-datepicker__today-btn");
Assert.NotNull(todayBtn);
await cut.InvokeAsync(() => todayBtn!.Click());
// Assert
Assert.NotNull(received);
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), received!.Value);
}
[Fact]
public async Task InvokesValueChangedWhenDaySelected()
{
// Arrange - use a fixed month so we can find a specific day
DateOnly? received = null;
var cut = RenderDatePicker(p => p
.Add(x => x.Value, new DateOnly(2025, 3, 1)) // March 2025 - display will show this month
.Add(x => x.ValueChanged, EventCallback.Factory.Create(this, v => received = v)));
// Act - open dropdown, click day 15 (guaranteed to exist in March)
var trigger = cut.Find(".sb-datepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
var day15 = cut.FindAll(".sb-datepicker__day")
.FirstOrDefault(b => b.TextContent.Trim() == "15"
&& !b.ClassList.Contains("sb-datepicker__day--disabled")
&& !b.ClassList.Contains("sb-datepicker__day--other-month"));
Assert.NotNull(day15);
await cut.InvokeAsync(() => day15!.Click());
// Assert
Assert.NotNull(received);
Assert.Equal(new DateOnly(2025, 3, 15), received!.Value);
}
[Fact]
public async Task NavigatesToPreviousMonth()
{
// Arrange
var cut = RenderDatePicker(p => p.Add(x => x.Value, new DateOnly(2025, 3, 15)));
var trigger = cut.Find(".sb-datepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
var title = cut.Find(".sb-datepicker__month-year");
var initialTitle = title.TextContent;
// Act - click previous month
var prevBtn = cut.FindAll(".sb-datepicker__nav-btn").First();
await cut.InvokeAsync(() => prevBtn!.Click());
// Assert - title should have changed (e.g. Feb 2025)
var newTitle = cut.Find(".sb-datepicker__month-year").TextContent;
Assert.NotEqual(initialTitle, newTitle);
}
[Fact]
public async Task SwitchesToMonthViewWhenMonthYearClicked()
{
// Arrange
var cut = RenderDatePicker();
var trigger = cut.Find(".sb-datepicker__trigger");
await cut.InvokeAsync(() => trigger!.Click());
// Act - click month/year header to switch to months view
var monthYearBtn = cut.Find(".sb-datepicker__month-year");
await cut.InvokeAsync(() => monthYearBtn!.Click());
// Assert - months grid should be visible
Assert.NotNull(cut.Find(".sb-datepicker__months"));
}
[Fact]
public void AppliesCustomClass()
{
// Arrange & Act
var cut = RenderDatePicker(p => p.Add(x => x.Class, "my-datepicker"));
// Assert
var wrapper = cut.Find(".sb-datepicker");
Assert.Contains("my-datepicker", wrapper.ClassList);
}
[Fact]
public void AppliesInlineStyle()
{
// Arrange & Act
var cut = RenderDatePicker(p => p.Add(x => x.Style, "max-width: 220px;"));
// Assert
var wrapper = cut.Find(".sb-datepicker");
Assert.Contains("max-width: 220px", wrapper.GetAttribute("style"));
}
[Fact]
public void UsesCustomFormatWhenProvided()
{
// Arrange & Act
var cut = RenderDatePicker(p => p
.Add(x => x.Value, new DateOnly(2025, 12, 25))
.Add(x => x.Format, "yyyy-MM-dd"));
// Assert
var valueSpan = cut.Find(".sb-datepicker__value");
Assert.NotNull(valueSpan);
Assert.Contains("2025-12-25", valueSpan.TextContent);
}
}