using Bunit; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared; /// /// bUnit tests for the third DialogHost dialog kind (T33a): /// renders arbitrary custom /// body content, omits the standard Cancel/Confirm footer (the body supplies /// its own buttons), and resolves a typed result through the /// the body is handed. The tests render a /// real behind a live DialogHost so the /// interface contract is exercised end to end. /// /// bUnit cannot run real JS interop, so the host's sbDialog.* focus-trap /// / focus-restoration calls are exercised in /// mode where they become no-ops. /// public class DialogHostShowAsyncTests : BunitContext { private readonly DialogService _service = new(); public DialogHostShowAsyncTests() { // Register the concrete service as the interface so DialogHost (which // injects IDialogService and casts to DialogService) resolves the same // instance the test drives. Services.AddSingleton(_service); // Focus trap / restoration / body-lock interop are no-ops under test. JSInterop.Mode = JSRuntimeMode.Loose; } [Fact] public async Task ShowAsync_RendersCustomBody_AndResolvesTypedResult() { var cut = Render(); // Open a custom dialog whose body renders a single button that closes // the dialog with a typed string result via the supplied context. Task pending = null!; await cut.InvokeAsync(() => { pending = _service.ShowAsync("Title", ctx => builder => { builder.OpenElement(0, "button"); builder.AddAttribute(1, "data-test", "ok"); builder.AddAttribute(2, "onclick", EventCallback.Factory.Create(this, () => ctx.Close("done"))); builder.CloseElement(); }); }); cut.Render(); // The custom body renders, and the standard footer is NOT present. Assert.NotNull(cut.Find("button[data-test='ok']")); Assert.DoesNotContain(cut.FindAll("button"), b => b.TextContent.Trim() is "Cancel" or "Confirm"); Assert.False(pending.IsCompleted, "Dialog must stay open until the body acts."); // Clicking the custom button resolves the awaited task with the value. cut.Find("button[data-test='ok']").Click(); var completed = await Task.WhenAny(pending, Task.Delay(TimeSpan.FromSeconds(2))); Assert.Same(pending, completed); Assert.Equal("done", await pending); } [Fact] public async Task ShowAsync_CancelResolvesNull() { var cut = Render(); Task pending = null!; await cut.InvokeAsync(() => { pending = _service.ShowAsync("Title", ctx => builder => { builder.OpenElement(0, "button"); builder.AddAttribute(1, "data-test", "cancel"); builder.AddAttribute(2, "onclick", EventCallback.Factory.Create(this, () => ctx.Cancel())); builder.CloseElement(); }); }); cut.Render(); cut.Find("button[data-test='cancel']").Click(); var completed = await Task.WhenAny(pending, Task.Delay(TimeSpan.FromSeconds(2))); Assert.Same(pending, completed); Assert.Null(await pending); } [Fact] public async Task ShowAsync_EscapeResolvesNull() { var cut = Render(); Task pending = null!; await cut.InvokeAsync(() => { pending = _service.ShowAsync("Title", _ => builder => { builder.OpenElement(0, "span"); builder.AddContent(1, "no buttons"); builder.CloseElement(); }); }); cut.Render(); // The host's Escape handler cancels custom dialogs the same way. cut.Find("div.modal").KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Key = "Escape" }); var completed = await Task.WhenAny(pending, Task.Delay(TimeSpan.FromSeconds(2))); Assert.Same(pending, completed); Assert.Null(await pending); } [Fact] public async Task ShowAsync_AppliesSizeClassToModalDialog() { var cut = Render(); await cut.InvokeAsync(() => { _ = _service.ShowAsync("Title", _ => builder => { builder.AddContent(0, "body"); }, size: "modal-lg"); }); cut.Render(); var dialog = cut.Find("div.modal-dialog"); Assert.Contains("modal-lg", dialog.ClassList); } [Fact] public async Task Backdrop_HasTokenHookClass() { var cut = Render(); await cut.InvokeAsync(() => { _ = _service.ShowAsync("Title", _ => builder => { builder.AddContent(0, "body"); }); }); cut.Render(); // The backdrop carries the tokenized hook class so a later task can // attach the actual var-driven background rule without touching markup. var backdrop = cut.Find("div.modal-backdrop"); Assert.Contains("sb-modal-backdrop", backdrop.ClassList); } [Fact] public async Task Confirm_StillResolvesTrue_NoRegression() { var cut = Render(); Task pending = null!; await cut.InvokeAsync(() => { pending = _service.ConfirmAsync("Title", "Sure?"); }); cut.Render(); // The standard footer's Confirm button is present for Confirm-kind dialogs. cut.FindAll("button").First(b => b.TextContent.Trim() == "Confirm").Click(); var completed = await Task.WhenAny(pending, Task.Delay(TimeSpan.FromSeconds(2))); Assert.Same(pending, completed); Assert.True(await pending); } }