using System.Globalization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Bunit; using Bunit.JSInterop; using SufiChain.SufiBlazor.Components.Overlays; using SufiChain.SufiBlazor.Localization; using Xunit; namespace SufiChain.SufiBlazor.Tests.Components.Overlays; 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 SbDialogTests : BunitContext { public SbDialogTests() { Services.AddSingleton>(new StubStringLocalizer()); JSInterop.Mode = JSRuntimeMode.Loose; } private IRenderedComponent RenderDialog( Action>? configure = null) { return Render(p => configure?.Invoke(p)); } [Fact] public void RendersDialogStructure() { // Arrange & Act var cut = RenderDialog(p => p.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "Body")))); // Assert - dialog is always in DOM var dialog = cut.Find("dialog.sb-dialog"); Assert.NotNull(dialog); Assert.NotNull(cut.Find(".sb-dialog__container")); Assert.NotNull(cut.Find(".sb-dialog__body")); Assert.Contains("Body", cut.Markup); } [Fact] public void RendersTitleWhenProvided() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.Title, "My Dialog") .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "Content")))); // Assert Assert.Contains("My Dialog", cut.Markup); var title = cut.Find(".sb-dialog__title"); Assert.NotNull(title); Assert.Equal("My Dialog", title.TextContent.Trim()); } [Fact] public void RendersChildContent() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "

Custom body content

")))); // Assert Assert.Contains("Custom body content", cut.Markup); var body = cut.Find(".sb-dialog__body"); Assert.NotNull(body); } [Fact] public void RendersFooterWhenProvided() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.Title, "Dialog") .Add(x => x.Footer, (RenderFragment)(b => b.AddMarkupContent(0, "")))); // Assert var footer = cut.Find(".sb-dialog__footer"); Assert.NotNull(footer); Assert.Contains("Save", cut.Markup); } [Fact] public void RendersCustomHeaderWhenProvided() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.Header, (RenderFragment)(b => b.AddMarkupContent(0, "

Custom Header

"))) .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "Body")))); // Assert - Header replaces Title Assert.Contains("Custom Header", cut.Markup); var header = cut.Find(".sb-dialog__header"); Assert.NotNull(header); } [Fact] public void ShowsCloseButtonByDefault() { // Arrange & Act var cut = RenderDialog(p => p.Add(x => x.Title, "Dialog")); // Assert var closeBtn = cut.Find(".sb-dialog__close-btn"); Assert.NotNull(closeBtn); Assert.Equal("Close", closeBtn.GetAttribute("aria-label")); } [Fact] public void HidesCloseButtonWhenShowCloseButtonFalse() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.Title, "Dialog") .Add(x => x.ShowCloseButton, false)); // Assert Assert.Empty(cut.FindAll(".sb-dialog__close-btn")); } [Theory] [InlineData(SbDialogSize.Sm, "sm")] [InlineData(SbDialogSize.Md, "md")] [InlineData(SbDialogSize.Lg, "lg")] [InlineData(SbDialogSize.Xl, "xl")] [InlineData(SbDialogSize.FullScreen, "fullscreen")] public void AppliesSizeClass(SbDialogSize size, string expectedClass) { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.Size, size) .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x")))); // Assert var dialog = cut.Find("dialog.sb-dialog"); Assert.Contains($"sb-dialog--{expectedClass}", dialog.ClassList); } [Fact] public void AppliesClassParameter() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))) .Add(x => x.Class, "my-dialog")); // Assert var dialog = cut.Find("dialog.sb-dialog"); Assert.Contains("my-dialog", dialog.ClassList); } [Fact] public void AppliesAllowDropdownOverflowClass() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))) .Add(x => x.AllowDropdownOverflow, true)); // Assert var dialog = cut.Find("dialog.sb-dialog"); Assert.Contains("sb-dialog--allow-dropdown-overflow", dialog.ClassList); } [Fact] public void AppliesWidthAndMaxWidthViaStyle() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.Width, "800px") .Add(x => x.MaxWidth, "90%") .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x")))); // Assert var dialog = cut.Find("dialog.sb-dialog"); var style = dialog.GetAttribute("style") ?? ""; Assert.Contains("width: 800px", style); Assert.Contains("max-width: 90%", style); } [Fact] public void HasAriaModalAndAriaLabelledByWhenTitleSet() { // Arrange & Act var cut = RenderDialog(p => p .Add(x => x.Title, "Dialog Title") .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x")))); // Assert var dialog = cut.Find("dialog.sb-dialog"); Assert.Equal("true", dialog.GetAttribute("aria-modal")); var labelId = dialog.GetAttribute("aria-labelledby"); Assert.NotNull(labelId); var titleEl = cut.Find($"#{labelId}"); Assert.NotNull(titleEl); Assert.Equal("Dialog Title", titleEl.TextContent.Trim()); } [Fact] public async Task OpenTrueTriggersShowModal() { // Arrange & Act - render with Open=true so OnParametersSetAsync runs ShowAsync var cut = RenderDialog(p => p .Add(x => x.Open, true) .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x")))); // Assert - JS interop called (Loose mode); dialog structure present var dialog = cut.Find("dialog.sb-dialog"); Assert.NotNull(dialog); await Task.CompletedTask; } [Fact] public async Task CloseButtonClickInvokesOnCloseWithCloseButtonReason() { // Arrange SbDialogCloseReason? closeReason = null; var cut = RenderDialog(p => p .Add(x => x.Open, true) .Add(x => x.Title, "Dialog") .Add(x => x.OnClose, EventCallback.Factory.Create(this, r => closeReason = r))); // Act var closeBtn = cut.Find(".sb-dialog__close-btn"); await cut.InvokeAsync(() => closeBtn.Click()); // Assert Assert.Equal(SbDialogCloseReason.CloseButton, closeReason); } [Fact] public async Task CloseButtonClickInvokesOpenChangedToFalse() { // Arrange var openChangedValue = true; var cut = RenderDialog(p => p .Add(x => x.Open, true) .Add(x => x.Title, "Dialog") .Add(x => x.OpenChanged, EventCallback.Factory.Create(this, v => openChangedValue = v))); // Act var closeBtn = cut.Find(".sb-dialog__close-btn"); await cut.InvokeAsync(() => closeBtn.Click()); // Assert Assert.False(openChangedValue); } [Fact] public async Task OnCloseButtonClickTakesPrecedenceWhenSet() { // Arrange - when OnCloseButtonClick is set, it runs instead of default close var closeBtnClicked = false; SbDialogCloseReason? closeReason = null; var cut = RenderDialog(p => p .Add(x => x.Open, true) .Add(x => x.Title, "Dialog") .Add(x => x.OnCloseButtonClick, EventCallback.Factory.Create(this, () => closeBtnClicked = true)) .Add(x => x.OnClose, EventCallback.Factory.Create(this, r => closeReason = r))); // Act var closeBtn = cut.Find(".sb-dialog__close-btn"); await cut.InvokeAsync(() => closeBtn.Click()); // Assert - OnCloseButtonClick was invoked; parent typically handles close, so OnClose may not fire Assert.True(closeBtnClicked); // With OnCloseButtonClick delegate, CloseAsync is NOT called (see HandleCloseButtonClick logic) // so OnClose and OpenChanged are NOT invoked by default Assert.Null(closeReason); } [Fact] public async Task EscapeKeyClosesWhenCloseOnEscapeTrue() { // Arrange SbDialogCloseReason? closeReason = null; var cut = RenderDialog(p => p .Add(x => x.Open, true) .Add(x => x.Title, "Dialog") .Add(x => x.CloseOnEscape, true) .Add(x => x.OnClose, EventCallback.Factory.Create(this, r => closeReason = r))); // Act - trigger keydown on the dialog var dialog = cut.Find("dialog.sb-dialog"); await cut.InvokeAsync(() => dialog.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Escape" })); // Assert Assert.Equal(SbDialogCloseReason.Escape, closeReason); } [Fact] public async Task EscapeKeyDoesNotCloseWhenCloseOnEscapeFalse() { // Arrange SbDialogCloseReason? closeReason = null; var cut = RenderDialog(p => p .Add(x => x.Open, true) .Add(x => x.Title, "Dialog") .Add(x => x.CloseOnEscape, false) .Add(x => x.OnClose, EventCallback.Factory.Create(this, r => closeReason = r))); // Act var dialog = cut.Find("dialog.sb-dialog"); await cut.InvokeAsync(() => dialog.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Escape" })); // Assert Assert.Null(closeReason); } [Fact] public void DoesNotShowHeaderWhenNoTitleHeaderOrCloseButton() { // Arrange & Act - ShowHeader = Header != null || Title != null || ShowCloseButton var cut = RenderDialog(p => p .Add(x => x.ShowCloseButton, false) .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x")))); // Assert - no header when Title=null, Header=null, ShowCloseButton=false Assert.Empty(cut.FindAll(".sb-dialog__header")); } [Fact] public void ShowsHeaderWhenOnlyCloseButton() { // Arrange & Act - ShowCloseButton true but no Title/Header var cut = RenderDialog(p => p .Add(x => x.Title, (string?)null) .Add(x => x.Header, (RenderFragment?)null) .Add(x => x.ShowCloseButton, true) .Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x")))); // Assert var header = cut.Find(".sb-dialog__header"); Assert.NotNull(header); Assert.NotNull(cut.Find(".sb-dialog__close-btn")); } }