Guard dialog completion by presentation identity

This commit is contained in:
Joseph Doherty
2026-11-13 21:25:42 -05:00
parent d370dbd37c
commit 6489c9fc0f
9 changed files with 217 additions and 19 deletions
@@ -0,0 +1,29 @@
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
/// <summary>Custom completions belong to the dialog whose typed context was issued.</summary>
public sealed class DialogContextIdentityTests
{
/// <summary>A canceled custom context cannot confirm or cancel a subsequently opened dialog.</summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task StaleCustomContext_DoesNotResolveNewDialog(bool staleCancel)
{
var service = new DialogService();
DialogContext<bool> first = null!;
var pendingFirst = service.ShowAsync<bool>("First", context => { first = context; return _ => { }; });
first.Cancel();
Assert.False(await pendingFirst);
DialogContext<bool> second = null!;
var pendingSecond = service.ShowAsync<bool>("Second", context => { second = context; return _ => { }; });
if (staleCancel) first.Cancel(); else first.Close(true);
Assert.False(pendingSecond.IsCompleted);
Assert.Equal("Second", service.Current?.Title);
second.Close(true);
Assert.True(await pendingSecond);
Assert.Null(service.Current);
}
}
@@ -0,0 +1,33 @@
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared;
/// <summary>A late native activation result belongs to its original presentation.</summary>
public sealed class DialogHostPresentationIdentityTests : BunitContext
{
/// <summary>Rejecting a closed confirm cannot cancel a prompt that has since opened.</summary>
[Fact]
public async Task LateActivationRejection_DoesNotCancelReplacementPrompt()
{
var service = new DialogService();
Services.AddSingleton<IDialogService>(service);
JSInterop.Mode = JSRuntimeMode.Loose;
var firstActivation = JSInterop.Setup<bool?>("sbDialog.activate", call => call.Arguments[1] is null);
JSInterop.Setup<bool?>("sbDialog.activate", call => call.Arguments[1] is not null).SetResult(true);
var cut = Render<DialogHost>();
Task<bool> first = null!;
await cut.InvokeAsync(() => { first = service.ConfirmAsync("First", "Synthetic confirm"); });
cut.WaitForAssertion(() => Assert.Single(firstActivation.Invocations));
await cut.InvokeAsync(() => service.Resolve(false));
Assert.False(await first);
Task<string?> second = null!;
await cut.InvokeAsync(() => { second = service.PromptAsync("Second", "Synthetic prompt", "retained"); });
cut.WaitForAssertion(() => Assert.Equal("retained", cut.Find("input").GetAttribute("value")));
firstActivation.SetResult(false);
cut.WaitForAssertion(() => Assert.Equal("Second", service.Current?.Title));
await cut.InvokeAsync(() => cut.Find("button.btn-close").Click());
Assert.Null(await second);
}
}