This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Bunit;
|
||||
using SufiChain.SufiBlazor.Components.Overlays;
|
||||
using SufiChain.SufiBlazor.Localization;
|
||||
using Xunit;
|
||||
|
||||
namespace SufiChain.SufiBlazor.Tests.Components.Overlays;
|
||||
|
||||
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 SbConfirmDialogTests : BunitContext
|
||||
{
|
||||
public SbConfirmDialogTests()
|
||||
{
|
||||
Services.AddSingleton<IStringLocalizer<SufiBlazorResource>>(new StubStringLocalizer());
|
||||
}
|
||||
|
||||
private IRenderedComponent<SbConfirmDialog> RenderConfirmDialog(
|
||||
Action<ComponentParameterCollectionBuilder<SbConfirmDialog>>? configure = null)
|
||||
{
|
||||
return Render<SbConfirmDialog>(p => configure?.Invoke(p));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotRenderWhenClosed()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.Message, "Are you sure?"));
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-confirm-dialog-overlay"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RendersWhenShowCalled()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.Message, "Proceed?"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var overlay = cut.Find(".sb-confirm-dialog-overlay");
|
||||
Assert.NotNull(overlay);
|
||||
var dialog = cut.Find(".sb-confirm-dialog");
|
||||
Assert.NotNull(dialog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RendersTitleAndMessage()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Delete Item")
|
||||
.Add(x => x.Message, "This cannot be undone."));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Delete Item", cut.Markup);
|
||||
Assert.Contains("This cannot be undone.", cut.Markup);
|
||||
var title = cut.Find(".sb-confirm-dialog__title");
|
||||
Assert.NotNull(title);
|
||||
var message = cut.Find(".sb-confirm-dialog__message");
|
||||
Assert.NotNull(message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RendersChildContent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.AddChildContent(b => b.AddMarkupContent(0, "<p>Custom content</p>")));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Custom content", cut.Markup);
|
||||
var body = cut.Find(".sb-confirm-dialog__body");
|
||||
Assert.NotNull(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DoesNotRenderMessageWhenNull()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Title only"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-confirm-dialog__message"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppliesVariantClass()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.Variant, SbConfirmDialogVariant.Danger));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find(".sb-confirm-dialog");
|
||||
Assert.Contains("sb-confirm-dialog--danger", dialog.ClassList);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SbConfirmDialogVariant.Default, "default")]
|
||||
[InlineData(SbConfirmDialogVariant.Danger, "danger")]
|
||||
[InlineData(SbConfirmDialogVariant.Warning, "warning")]
|
||||
[InlineData(SbConfirmDialogVariant.Info, "info")]
|
||||
public async Task AppliesCorrectVariantClass(SbConfirmDialogVariant variant, string expectedClass)
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.Variant, variant));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find(".sb-confirm-dialog");
|
||||
Assert.Contains($"sb-confirm-dialog--{expectedClass}", dialog.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowsCancelButtonByDefault()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p.Add(x => x.Title, "Confirm"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert - two buttons: Cancel and Confirm
|
||||
var buttons = cut.FindAll("button");
|
||||
Assert.True(buttons.Count >= 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HidesCancelButtonWhenShowCancelButtonFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.ShowCancelButton, false));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert - only Confirm button (SbButton with Ghost variant is Cancel)
|
||||
var ghostButtons = cut.FindAll("button.sb-button--ghost");
|
||||
Assert.Empty(ghostButtons);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppliesCustomButtonText()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.ConfirmText, "Yes, delete")
|
||||
.Add(x => x.CancelText, "Keep"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Yes, delete", cut.Markup);
|
||||
Assert.Contains("Keep", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AppliesClassParameter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.Class, "my-dialog"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find(".sb-confirm-dialog");
|
||||
Assert.Contains("my-dialog", dialog.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasAlertDialogRole()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p.Add(x => x.Title, "Confirm"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find(".sb-confirm-dialog");
|
||||
Assert.Equal("alertdialog", dialog.GetAttribute("role"));
|
||||
Assert.Equal("true", dialog.GetAttribute("aria-modal"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfirmButtonInvokesOnConfirm()
|
||||
{
|
||||
// Arrange
|
||||
var confirmed = false;
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.OnConfirm, EventCallback.Factory.Create(this, () => confirmed = true)));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Act - click Confirm (Solid button, not Ghost)
|
||||
var confirmBtn = cut.FindAll("button.sb-button--solid").Last();
|
||||
await cut.InvokeAsync(() => confirmBtn.Click());
|
||||
|
||||
// Assert
|
||||
Assert.True(confirmed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelButtonInvokesOnCancel()
|
||||
{
|
||||
// Arrange
|
||||
var cancelled = false;
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.OnCancel, EventCallback.Factory.Create(this, () => cancelled = true)));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Act - click Cancel (Ghost button)
|
||||
var cancelBtn = cut.Find("button.sb-button--ghost");
|
||||
await cut.InvokeAsync(() => cancelBtn!.Click());
|
||||
|
||||
// Assert
|
||||
Assert.True(cancelled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowAsyncReturnsTrueWhenConfirmed()
|
||||
{
|
||||
// Arrange
|
||||
var cut = RenderConfirmDialog(p => p
|
||||
.Add(x => x.Title, "Confirm")
|
||||
.Add(x => x.ShowCancelButton, false));
|
||||
Task<bool> showTask = null!;
|
||||
await cut.InvokeAsync(() => { showTask = cut.Instance.ShowAsync(); });
|
||||
cut.Render();
|
||||
|
||||
// Act
|
||||
var confirmBtn = cut.Find("button.sb-button--solid");
|
||||
await cut.InvokeAsync(() => confirmBtn!.Click());
|
||||
|
||||
// Assert
|
||||
var result = await showTask;
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowAsyncReturnsFalseWhenCancelled()
|
||||
{
|
||||
// Arrange
|
||||
var cut = RenderConfirmDialog(p => p.Add(x => x.Title, "Confirm"));
|
||||
Task<bool> showTask = null!;
|
||||
await cut.InvokeAsync(() => { showTask = cut.Instance.ShowAsync(); });
|
||||
cut.Render();
|
||||
|
||||
// Act - click Cancel
|
||||
var cancelBtn = cut.Find("button.sb-button--ghost");
|
||||
await cut.InvokeAsync(() => cancelBtn!.Click());
|
||||
|
||||
// Assert
|
||||
var result = await showTask;
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseHidesDialog()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p.Add(x => x.Title, "Confirm"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
Assert.NotNull(cut.Find(".sb-confirm-dialog-overlay"));
|
||||
|
||||
await cut.InvokeAsync(() => cut.Instance.Close());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-confirm-dialog-overlay"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RendersIconSection()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderConfirmDialog(p => p.Add(x => x.Title, "Confirm"));
|
||||
await cut.InvokeAsync(() => cut.Instance.Show());
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var icon = cut.Find(".sb-confirm-dialog__icon");
|
||||
Assert.NotNull(icon);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
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<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 SbDialogTests : BunitContext
|
||||
{
|
||||
public SbDialogTests()
|
||||
{
|
||||
Services.AddSingleton<IStringLocalizer<SufiBlazorResource>>(new StubStringLocalizer());
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private IRenderedComponent<SbDialog> RenderDialog(
|
||||
Action<ComponentParameterCollectionBuilder<SbDialog>>? configure = null)
|
||||
{
|
||||
return Render<SbDialog>(p => configure?.Invoke(p));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersDialogStructure()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDialog(p => p.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "<span>Body</span>"))));
|
||||
|
||||
// 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, "<p>Custom body content</p>"))));
|
||||
|
||||
// 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, "<button>Save</button>"))));
|
||||
|
||||
// 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, "<h1>Custom Header</h1>")))
|
||||
.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<SbDialogCloseReason>(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<bool>(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<SbDialogCloseReason>(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<SbDialogCloseReason>(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<SbDialogCloseReason>(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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
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<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 SbDrawerTests : BunitContext
|
||||
{
|
||||
public SbDrawerTests()
|
||||
{
|
||||
Services.AddSingleton<IStringLocalizer<SufiBlazorResource>>(new StubStringLocalizer());
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private IRenderedComponent<SbDrawer> RenderDrawer(
|
||||
Action<ComponentParameterCollectionBuilder<SbDrawer>>? configure = null)
|
||||
{
|
||||
return Render<SbDrawer>(p => configure?.Invoke(p));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersDrawerStructure()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "<span>Body</span>"))));
|
||||
|
||||
// Assert - dialog (drawer) is always in DOM
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.NotNull(dialog);
|
||||
Assert.NotNull(cut.Find(".sb-drawer__container"));
|
||||
Assert.NotNull(cut.Find(".sb-drawer__body"));
|
||||
Assert.Contains("Body", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersTitleWhenProvided()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Title, "My Drawer")
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "Content"))));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("My Drawer", cut.Markup);
|
||||
var title = cut.Find(".sb-drawer__title");
|
||||
Assert.NotNull(title);
|
||||
Assert.Equal("My Drawer", title.TextContent.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersChildContent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "<p>Custom body content</p>"))));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Custom body content", cut.Markup);
|
||||
var body = cut.Find(".sb-drawer__body");
|
||||
Assert.NotNull(body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersFooterWhenProvided()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Title, "Drawer")
|
||||
.Add(x => x.Footer, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Save</button>"))));
|
||||
|
||||
// Assert
|
||||
var footer = cut.Find(".sb-drawer__footer");
|
||||
Assert.NotNull(footer);
|
||||
Assert.Contains("Save", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersCustomHeaderWhenProvided()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Header, (RenderFragment)(b => b.AddMarkupContent(0, "<h1>Custom Header</h1>")))
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "Body"))));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Custom Header", cut.Markup);
|
||||
var header = cut.Find(".sb-drawer__header");
|
||||
Assert.NotNull(header);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowsCloseButtonWhenTitleProvided()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p.Add(x => x.Title, "Drawer"));
|
||||
|
||||
// Assert
|
||||
var closeBtn = cut.Find(".sb-drawer__close-btn");
|
||||
Assert.NotNull(closeBtn);
|
||||
Assert.Equal("Close", closeBtn.GetAttribute("aria-label"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SbDrawerPlacement.Start, "start")]
|
||||
[InlineData(SbDrawerPlacement.End, "end")]
|
||||
[InlineData(SbDrawerPlacement.Top, "top")]
|
||||
[InlineData(SbDrawerPlacement.Bottom, "bottom")]
|
||||
public void AppliesPlacementClass(SbDrawerPlacement placement, string expectedClass)
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Placement, placement)
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.Contains($"sb-drawer--{expectedClass}", dialog.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesModalClassWhenModalTrue()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Modal, true)
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.Contains("sb-drawer--modal", dialog.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotApplyModalClassWhenModalFalse()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Modal, false)
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.DoesNotContain("sb-drawer--modal", dialog.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesClassParameter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x")))
|
||||
.Add(x => x.Class, "my-drawer"));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.Contains("my-drawer", dialog.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesWidthVariableForStartPlacement()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Placement, SbDrawerPlacement.Start)
|
||||
.Add(x => x.Width, "400px")
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
var style = dialog.GetAttribute("style") ?? "";
|
||||
Assert.Contains("--sb-drawer-width: 400px", style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesHeightVariableForTopPlacement()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Placement, SbDrawerPlacement.Top)
|
||||
.Add(x => x.Height, "300px")
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
var style = dialog.GetAttribute("style") ?? "";
|
||||
Assert.Contains("--sb-drawer-height: 300px", style);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasAriaModalTrueWhenModal()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Modal, true)
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.Equal("true", dialog.GetAttribute("aria-modal"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasAriaModalNullWhenNotModal()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Modal, false)
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.Null(dialog.GetAttribute("aria-modal"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenTrueTriggersShowModal()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
Assert.NotNull(dialog);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CloseButtonClickInvokesOpenChangedToFalse()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.Title, "Drawer")
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
|
||||
// Act
|
||||
var closeBtn = cut.Find(".sb-drawer__close-btn");
|
||||
await cut.InvokeAsync(() => closeBtn.Click());
|
||||
|
||||
// Assert
|
||||
Assert.False(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EscapeKeyClosesWhenCloseOnEscapeTrue()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.Title, "Drawer")
|
||||
.Add(x => x.CloseOnEscape, true)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
|
||||
// Act
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
await cut.InvokeAsync(() => dialog.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Escape" }));
|
||||
|
||||
// Assert
|
||||
Assert.False(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EscapeKeyDoesNotCloseWhenCloseOnEscapeFalse()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.Title, "Drawer")
|
||||
.Add(x => x.CloseOnEscape, false)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
|
||||
// Act
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
await cut.InvokeAsync(() => dialog.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Escape" }));
|
||||
|
||||
// Assert
|
||||
Assert.True(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BackdropClickClosesWhenCloseOnBackdropClickTrue()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.Title, "Drawer")
|
||||
.Add(x => x.CloseOnBackdropClick, true)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
|
||||
// Act - trigger onclick on dialog (simulates backdrop click)
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
await cut.InvokeAsync(() => dialog.TriggerEventAsync("onclick", new MouseEventArgs()));
|
||||
|
||||
// Assert
|
||||
Assert.False(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BackdropClickDoesNotCloseWhenCloseOnBackdropClickFalse()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.Title, "Drawer")
|
||||
.Add(x => x.CloseOnBackdropClick, false)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
|
||||
// Act
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
await cut.InvokeAsync(() => dialog.TriggerEventAsync("onclick", new MouseEventArgs()));
|
||||
|
||||
// Assert
|
||||
Assert.True(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotShowHeaderWhenNoTitleOrHeader()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-drawer__header"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesStyleParameter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderDrawer(p => p
|
||||
.Add(x => x.Placement, SbDrawerPlacement.Start)
|
||||
.Add(x => x.Style, "custom: value")
|
||||
.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "x"))));
|
||||
|
||||
// Assert
|
||||
var dialog = cut.Find("dialog.sb-drawer");
|
||||
var style = dialog.GetAttribute("style") ?? "";
|
||||
Assert.Contains("custom: value", style);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Bunit;
|
||||
using Bunit.JSInterop;
|
||||
using SufiChain.SufiBlazor.Components.Overlays;
|
||||
using Xunit;
|
||||
|
||||
namespace SufiChain.SufiBlazor.Tests.Components.Overlays;
|
||||
|
||||
public class SbMenuTests : BunitContext
|
||||
{
|
||||
public SbMenuTests()
|
||||
{
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private static RenderFragment DefaultMenuItems => (b) =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Item 1");
|
||||
b.CloseComponent();
|
||||
b.OpenComponent<SbMenuItem>(2);
|
||||
b.AddAttribute(3, "Text", "Item 2");
|
||||
b.CloseComponent();
|
||||
};
|
||||
|
||||
private IRenderedComponent<SbMenu> RenderMenu(
|
||||
Action<ComponentParameterCollectionBuilder<SbMenu>>? configure = null)
|
||||
{
|
||||
return Render<SbMenu>(p =>
|
||||
{
|
||||
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button class=\"anchor-btn\">Open</button>")));
|
||||
p.AddChildContent(DefaultMenuItems);
|
||||
configure?.Invoke(p);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersAnchorContent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu();
|
||||
|
||||
// Assert
|
||||
var anchor = cut.Find(".sb-menu-anchor");
|
||||
Assert.NotNull(anchor);
|
||||
Assert.Contains("Open", cut.Markup);
|
||||
Assert.NotNull(cut.Find(".anchor-btn"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotRenderMenuWhenClosed()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-menu"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersMenuWhenOpen()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p.Add(x => x.Open, true));
|
||||
|
||||
// Assert
|
||||
var menu = cut.Find(".sb-menu");
|
||||
Assert.NotNull(menu);
|
||||
Assert.Equal("menu", menu.GetAttribute("role"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersMenuItemsWhenOpen()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p.Add(x => x.Open, true));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Item 1", cut.Markup);
|
||||
Assert.Contains("Item 2", cut.Markup);
|
||||
var items = cut.FindAll(".sb-menu-item");
|
||||
Assert.Equal(2, items.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MenuItemsHaveMenuitemRole()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p.Add(x => x.Open, true));
|
||||
|
||||
// Assert
|
||||
var items = cut.FindAll("[role=\"menuitem\"]");
|
||||
Assert.Equal(2, items.Count);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SbPlacement.BottomStart, "bottom-start")]
|
||||
[InlineData(SbPlacement.Top, "top")]
|
||||
[InlineData(SbPlacement.End, "end")]
|
||||
[InlineData(SbPlacement.Start, "start")]
|
||||
public void AppliesPlacementClass(SbPlacement placement, string expectedClass)
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.Placement, placement));
|
||||
|
||||
// Assert
|
||||
var menu = cut.Find(".sb-menu");
|
||||
Assert.Contains($"sb-menu--{expectedClass}", menu.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesClassParameter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.Class, "my-menu"));
|
||||
|
||||
// Assert
|
||||
var menu = cut.Find(".sb-menu");
|
||||
Assert.Contains("my-menu", menu.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MenuItemRendersText()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p
|
||||
.AddChildContent(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Save File");
|
||||
b.CloseComponent();
|
||||
})
|
||||
.Add(x => x.Open, true));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Save File", cut.Markup);
|
||||
var textSpan = cut.Find(".sb-menu-item__text");
|
||||
Assert.NotNull(textSpan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MenuItemRendersChildContentOverridesText()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p
|
||||
.AddChildContent(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Ignore");
|
||||
b.AddAttribute(2, "ChildContent", (RenderFragment)(c => c.AddMarkupContent(0, "<span>Custom</span>")));
|
||||
b.CloseComponent();
|
||||
})
|
||||
.Add(x => x.Open, true));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Custom", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MenuItemRendersShortcut()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p
|
||||
.AddChildContent(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "New");
|
||||
b.AddAttribute(2, "Shortcut", "Ctrl+N");
|
||||
b.CloseComponent();
|
||||
})
|
||||
.Add(x => x.Open, true));
|
||||
|
||||
// Assert
|
||||
var shortcut = cut.Find(".sb-menu-item__shortcut");
|
||||
Assert.NotNull(shortcut);
|
||||
Assert.Equal("Ctrl+N", shortcut.TextContent.Trim());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisabledMenuItemHasDisabledClassAndAttribute()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = Render<SbMenu>(p =>
|
||||
{
|
||||
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
|
||||
p.Add(x => x.Open, true);
|
||||
p.Add(x => x.ChildContent, (RenderFragment)(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Disabled");
|
||||
b.AddAttribute(2, "Disabled", true);
|
||||
b.CloseComponent();
|
||||
}));
|
||||
});
|
||||
|
||||
// Assert
|
||||
var item = cut.Find("button.sb-menu-item");
|
||||
Assert.Contains("sb-menu-item--disabled", item.GetAttribute("class") ?? "");
|
||||
Assert.NotNull(item.GetAttribute("disabled"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MenuItemAppliesClassParameter()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = Render<SbMenu>(p =>
|
||||
{
|
||||
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
|
||||
p.Add(x => x.Open, true);
|
||||
p.Add(x => x.ChildContent, (RenderFragment)(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Item");
|
||||
b.AddAttribute(2, "Class", "my-item");
|
||||
b.CloseComponent();
|
||||
}));
|
||||
});
|
||||
|
||||
// Assert
|
||||
var item = cut.Find("button.sb-menu-item");
|
||||
Assert.Contains("my-item", item.GetAttribute("class") ?? "");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MenuItemClickInvokesOnClick()
|
||||
{
|
||||
// Arrange - use default items with OnClick on first item via markup in ChildContent
|
||||
var clicked = false;
|
||||
var cut = Render<SbMenu>(p =>
|
||||
{
|
||||
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
|
||||
p.Add(x => x.Open, true);
|
||||
p.Add(x => x.CloseOnItemClick, false);
|
||||
p.Add(x => x.ChildContent, (RenderFragment)(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Click Me");
|
||||
b.AddAttribute(2, "OnClick", EventCallback.Factory.Create(this, () => clicked = true));
|
||||
b.CloseComponent();
|
||||
}));
|
||||
});
|
||||
|
||||
// Act
|
||||
var item = cut.Find("button.sb-menu-item");
|
||||
await cut.InvokeAsync(() => item.Click());
|
||||
|
||||
// Assert
|
||||
Assert.True(clicked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MenuItemClickClosesMenuWhenCloseOnItemClickTrue()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderMenu(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.CloseOnItemClick, true)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
|
||||
// Act
|
||||
var item = cut.Find("button.sb-menu-item");
|
||||
await cut.InvokeAsync(() => item.Click());
|
||||
|
||||
// Assert - OpenChanged invoked with false
|
||||
Assert.False(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MenuItemClickDoesNotCloseMenuWhenCloseOnItemClickFalse()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderMenu(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.CloseOnItemClick, false)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
|
||||
// Act
|
||||
var item = cut.Find("button.sb-menu-item");
|
||||
await cut.InvokeAsync(() => item.Click());
|
||||
|
||||
// Assert
|
||||
Assert.True(openChangedValue);
|
||||
Assert.NotNull(cut.Find(".sb-menu"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisabledMenuItemClickDoesNotInvokeOnClick()
|
||||
{
|
||||
// Arrange - disabled item should not fire OnClick
|
||||
var clicked = false;
|
||||
var cut = Render<SbMenu>(p =>
|
||||
{
|
||||
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button>Open</button>")));
|
||||
p.Add(x => x.Open, true);
|
||||
p.Add(x => x.ChildContent, (RenderFragment)(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Disabled");
|
||||
b.AddAttribute(2, "Disabled", true);
|
||||
b.AddAttribute(3, "OnClick", EventCallback.Factory.Create(this, () => clicked = true));
|
||||
b.CloseComponent();
|
||||
}));
|
||||
});
|
||||
|
||||
// Act
|
||||
var item = cut.Find("button.sb-menu-item");
|
||||
await cut.InvokeAsync(() => item.Click());
|
||||
|
||||
// Assert - disabled items don't respond to click
|
||||
Assert.False(clicked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnchorKeyDownEnterOpensMenu()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = false;
|
||||
var cut = RenderMenu(p => p.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
var anchorDiv = cut.Find(".sb-menu-anchor > div");
|
||||
|
||||
// Act
|
||||
await cut.InvokeAsync(() => anchorDiv.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Enter" }));
|
||||
|
||||
// Assert
|
||||
Assert.True(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnchorKeyDownSpaceOpensMenu()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = false;
|
||||
var cut = RenderMenu(p => p.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
var anchorDiv = cut.Find(".sb-menu-anchor > div");
|
||||
|
||||
// Act
|
||||
await cut.InvokeAsync(() => anchorDiv.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = " " }));
|
||||
|
||||
// Assert
|
||||
Assert.True(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AnchorKeyDownArrowDownOpensMenu()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = false;
|
||||
var cut = RenderMenu(p => p.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
var anchorDiv = cut.Find(".sb-menu-anchor > div");
|
||||
|
||||
// Act
|
||||
await cut.InvokeAsync(() => anchorDiv.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "ArrowDown" }));
|
||||
|
||||
// Assert
|
||||
Assert.True(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MenuKeyDownEscapeClosesMenu()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderMenu(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
var menu = cut.Find(".sb-menu");
|
||||
|
||||
// Act
|
||||
await cut.InvokeAsync(() => menu.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Escape" }));
|
||||
|
||||
// Assert
|
||||
Assert.False(openChangedValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MenuKeyDownTabClosesMenu()
|
||||
{
|
||||
// Arrange
|
||||
var openChangedValue = true;
|
||||
var cut = RenderMenu(p => p
|
||||
.Add(x => x.Open, true)
|
||||
.Add(x => x.OpenChanged, EventCallback.Factory.Create<bool>(this, v => openChangedValue = v)));
|
||||
var menu = cut.Find(".sb-menu");
|
||||
|
||||
// Act
|
||||
await cut.InvokeAsync(() => menu.TriggerEventAsync("onkeydown", new KeyboardEventArgs { Key = "Tab" }));
|
||||
|
||||
// Assert
|
||||
Assert.False(openChangedValue);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void MenuItemRendersIcon()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderMenu(p => p
|
||||
.AddChildContent(b =>
|
||||
{
|
||||
b.OpenComponent<SbMenuItem>(0);
|
||||
b.AddAttribute(1, "Text", "Edit");
|
||||
b.AddAttribute(2, "Icon", (RenderFragment)(c => c.AddMarkupContent(0, "<span class=\"icon\">✎</span>")));
|
||||
b.CloseComponent();
|
||||
})
|
||||
.Add(x => x.Open, true));
|
||||
|
||||
// Assert
|
||||
var icon = cut.Find(".sb-menu-item__icon");
|
||||
Assert.NotNull(icon);
|
||||
Assert.Contains("✎", cut.Markup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Bunit;
|
||||
using Bunit.JSInterop;
|
||||
using SufiChain.SufiBlazor.Components.Overlays;
|
||||
using Xunit;
|
||||
|
||||
namespace SufiChain.SufiBlazor.Tests.Components.Overlays;
|
||||
|
||||
public class SbPopoverTests : BunitContext
|
||||
{
|
||||
public SbPopoverTests()
|
||||
{
|
||||
JSInterop.Mode = JSRuntimeMode.Loose;
|
||||
}
|
||||
|
||||
private IRenderedComponent<SbPopover> RenderPopover(
|
||||
Action<ComponentParameterCollectionBuilder<SbPopover>>? configure = null)
|
||||
{
|
||||
return Render<SbPopover>(p =>
|
||||
{
|
||||
p.Add(x => x.AnchorContent, (RenderFragment)(b => b.AddMarkupContent(0, "<button class=\"anchor-btn\">Open</button>")));
|
||||
p.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "<span>Popover content</span>")));
|
||||
configure?.Invoke(p);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersAnchorContent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderPopover();
|
||||
|
||||
// Assert
|
||||
var anchor = cut.Find(".sb-popover-anchor");
|
||||
Assert.NotNull(anchor);
|
||||
Assert.Contains("Open", cut.Markup);
|
||||
Assert.NotNull(cut.Find(".anchor-btn"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotRenderPopoverWhenClosed()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderPopover();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-popover"));
|
||||
}
|
||||
|
||||
// Note: SbPopover with Open=true triggers OnAfterRenderAsync which calls StateHasChanged(),
|
||||
// causing a render loop in bunit. We test structure and closed state only.
|
||||
// Open-state tests (placement class, style, etc.) would require the component to avoid
|
||||
// unconditional StateHasChanged() when Open, or a different test setup.
|
||||
|
||||
[Fact]
|
||||
public void AcceptsPlacementParameterWhenClosed()
|
||||
{
|
||||
// Component accepts Placement without error
|
||||
var cut = RenderPopover(p => p.Add(x => x.Placement, SbPlacement.Top));
|
||||
Assert.NotNull(cut.Find(".sb-popover-anchor"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsCloseOnClickAwayAndCloseOnEscapeParameters()
|
||||
{
|
||||
var cut = RenderPopover(p => p
|
||||
.Add(x => x.CloseOnClickAway, false)
|
||||
.Add(x => x.CloseOnEscape, false));
|
||||
Assert.NotNull(cut.Find(".sb-popover-anchor"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Bunit;
|
||||
using SufiChain.SufiBlazor.Components.Overlays;
|
||||
using Xunit;
|
||||
|
||||
namespace SufiChain.SufiBlazor.Tests.Components.Overlays;
|
||||
|
||||
public class SbTooltipTests : BunitContext
|
||||
{
|
||||
private IRenderedComponent<SbTooltip> RenderTooltip(
|
||||
Action<ComponentParameterCollectionBuilder<SbTooltip>>? configure = null)
|
||||
{
|
||||
return Render<SbTooltip>(p =>
|
||||
{
|
||||
p.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "<span class=\"trigger\">Hover me</span>")));
|
||||
p.Add(x => x.Text, "Tooltip text");
|
||||
configure?.Invoke(p);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersWrapperAndChildContent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderTooltip();
|
||||
|
||||
// Assert
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
Assert.NotNull(wrapper);
|
||||
Assert.Contains("Hover me", cut.Markup);
|
||||
Assert.NotNull(cut.Find(".trigger"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotRenderTooltipWhenNotTriggered()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderTooltip();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-tooltip"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RendersTextParameter()
|
||||
{
|
||||
// Arrange & Act - with Delay=0, mouseenter shows immediately
|
||||
var cut = RenderTooltip(p => p.Add(x => x.Delay, 0));
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onmouseenter", EventArgs.Empty));
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var tooltip = cut.Find(".sb-tooltip");
|
||||
Assert.NotNull(tooltip);
|
||||
Assert.Contains("Tooltip text", cut.Markup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RendersContentWhenProvided()
|
||||
{
|
||||
// Arrange & Act - use Content instead of Text
|
||||
var cut = Render<SbTooltip>(p =>
|
||||
{
|
||||
p.Add(x => x.ChildContent, (RenderFragment)(b => b.AddMarkupContent(0, "<span class=\"trigger\">Hover</span>")));
|
||||
p.Add(x => x.Delay, 0);
|
||||
p.Add(x => x.Content, (RenderFragment)(b => b.AddMarkupContent(0, "<strong>Rich content</strong>")));
|
||||
});
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onmouseenter", EventArgs.Empty));
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Rich content", cut.Markup);
|
||||
var tooltip = cut.Find(".sb-tooltip");
|
||||
Assert.Contains("sb-tooltip--rich", tooltip.ClassList);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SbPlacement.Top, "top")]
|
||||
[InlineData(SbPlacement.Bottom, "bottom")]
|
||||
[InlineData(SbPlacement.End, "end")]
|
||||
[InlineData(SbPlacement.Start, "start")]
|
||||
public async Task AppliesPlacementClass(SbPlacement placement, string expectedClass)
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderTooltip(p => p
|
||||
.Add(x => x.Delay, 0)
|
||||
.Add(x => x.Placement, placement));
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onmouseenter", EventArgs.Empty));
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var tooltip = cut.Find(".sb-tooltip");
|
||||
Assert.Contains($"sb-tooltip--{expectedClass}", tooltip.ClassList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TooltipHasRoleAndId()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderTooltip(p => p.Add(x => x.Delay, 0));
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onmouseenter", EventArgs.Empty));
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
var tooltip = cut.Find(".sb-tooltip");
|
||||
Assert.Equal("tooltip", tooltip.GetAttribute("role"));
|
||||
Assert.StartsWith("sb-tooltip-", tooltip.GetAttribute("id") ?? "");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MouseLeaveHidesTooltipWhenHideDelayZero()
|
||||
{
|
||||
// Arrange
|
||||
var cut = RenderTooltip(p => p.Add(x => x.Delay, 0));
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onmouseenter", EventArgs.Empty));
|
||||
cut.Render();
|
||||
Assert.NotNull(cut.Find(".sb-tooltip"));
|
||||
|
||||
// Act
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onmouseleave", EventArgs.Empty));
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-tooltip"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FocusShowsTooltipWhenDelayZero()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderTooltip(p => p.Add(x => x.Delay, 0));
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onfocus", EventArgs.Empty));
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(cut.Find(".sb-tooltip"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BlurHidesTooltipWhenHideDelayZero()
|
||||
{
|
||||
// Arrange
|
||||
var cut = RenderTooltip(p => p.Add(x => x.Delay, 0));
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onfocus", EventArgs.Empty));
|
||||
cut.Render();
|
||||
Assert.NotNull(cut.Find(".sb-tooltip"));
|
||||
|
||||
// Act
|
||||
await cut.InvokeAsync(() => wrapper.TriggerEventAsync("onblur", EventArgs.Empty));
|
||||
cut.Render();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(cut.FindAll(".sb-tooltip"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PassesAdditionalAttributesToWrapper()
|
||||
{
|
||||
// Arrange & Act
|
||||
var cut = RenderTooltip(p => p
|
||||
.Add(x => x.AdditionalAttributes, new Dictionary<string, object> { ["data-test"] = "tooltip-wrapper" }));
|
||||
|
||||
// Assert
|
||||
var wrapper = cut.Find(".sb-tooltip-wrapper");
|
||||
Assert.Equal("tooltip-wrapper", wrapper.GetAttribute("data-test"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user