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,7 @@
# Dialog presentation identity followup
A custom body retained the service-wide resolver after cancellation, and a delayed native activation rejection canceled whichever presentation was current. Both defects reproduced before the change: two stale custom-context cases and one delayed rejection case failed.
DialogService now checks the captured completion source. DialogHost binds rendered handlers and activation rejection to their captured DialogState; a stale handler cannot change a replacement prompt draft or result. Existing public APIs, typed cancellation outcomes and the single-active contract are preserved. Disposal still cancels the current presentation.
Verification: all three regressions pass; the full CentralUI suite passes 1600/1600, zero skips/failures. Logs retain actual red and green runs. This is a local managed lifecycle regression check, not a new native browser matrix or production operation.
@@ -0,0 +1,43 @@
ZB.MOM.WW.ScadaBridge.Commons -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Commons/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Commons.dll
ZB.MOM.WW.ScadaBridge.ScriptAnalysis -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.dll
ZB.MOM.WW.ScadaBridge.KpiHistory -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.KpiHistory/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.KpiHistory.dll
ZB.MOM.WW.ScadaBridge.Security -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Security/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Security.dll
ZB.MOM.WW.ScadaBridge.SiteEventLogging -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.SiteEventLogging.dll
ZB.MOM.WW.ScadaBridge.StoreAndForward -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.StoreAndForward.dll
ZB.MOM.WW.ScadaBridge.Communication -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Communication/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Communication.dll
ZB.MOM.WW.ScadaBridge.ConfigurationDatabase -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.dll
ZB.MOM.WW.ScadaBridge.TemplateEngine -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.TemplateEngine.dll
ZB.MOM.WW.ScadaBridge.AuditLog -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.AuditLog/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.AuditLog.dll
ZB.MOM.WW.ScadaBridge.DeploymentManager -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.DeploymentManager.dll
ZB.MOM.WW.ScadaBridge.InboundAPI -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.InboundAPI/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.InboundAPI.dll
ZB.MOM.WW.ScadaBridge.Transport -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Transport/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Transport.dll
ZB.MOM.WW.ScadaBridge.HealthMonitoring -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.HealthMonitoring.dll
ZB.MOM.WW.ScadaBridge.NotificationService -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.NotificationService/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.NotificationService.dll
ZB.MOM.WW.ScadaBridge.ExternalSystemGateway -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.dll
ZB.MOM.WW.ScadaBridge.NotificationOutbox -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.NotificationOutbox.dll
ZB.MOM.WW.ScadaBridge.ManagementService -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ManagementService/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ManagementService.dll
ZB.MOM.WW.ScadaBridge.CentralUI -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.dll
ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker.dll
ZB.MOM.WW.ScadaBridge.CentralUI.Tests -> /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll
Test run for /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (.NETCoreApp,Version=v10.0)
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.19] ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogContextIdentityTests.StaleCustomContext_DoesNotResolveNewDialog(staleCancel: True) [FAIL]
[xUnit.net 00:00:00.19] ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogContextIdentityTests.StaleCustomContext_DoesNotResolveNewDialog(staleCancel: False) [FAIL]
Failed ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogContextIdentityTests.StaleCustomContext_DoesNotResolveNewDialog(staleCancel: True) [6 ms]
Error Message:
Assert.Equal() Failure: Strings differ
Expected: "Second"
Actual: null
Stack Trace:
at ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogContextIdentityTests.StaleCustomContext_DoesNotResolveNewDialog(Boolean staleCancel) in /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/DialogContextIdentityTests.cs:line 24
--- End of stack trace from previous location ---
Failed ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogContextIdentityTests.StaleCustomContext_DoesNotResolveNewDialog(staleCancel: False) [< 1 ms]
Error Message:
Assert.Equal() Failure: Strings differ
Expected: "Second"
Actual: null
Stack Trace:
at ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogContextIdentityTests.StaleCustomContext_DoesNotResolveNewDialog(Boolean staleCancel) in /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/DialogContextIdentityTests.cs:line 24
--- End of stack trace from previous location ---
Failed! - Failed: 2, Passed: 0, Skipped: 0, Total: 2, Duration: 41 ms - ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (net10.0)
@@ -0,0 +1,25 @@
ZB.MOM.WW.ScadaBridge.Commons -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Commons/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Commons.dll
ZB.MOM.WW.ScadaBridge.KpiHistory -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.KpiHistory/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.KpiHistory.dll
ZB.MOM.WW.ScadaBridge.ConfigurationDatabase -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.dll
ZB.MOM.WW.ScadaBridge.Communication -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Communication/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Communication.dll
ZB.MOM.WW.ScadaBridge.Security -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Security/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Security.dll
ZB.MOM.WW.ScadaBridge.ScriptAnalysis -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.dll
ZB.MOM.WW.ScadaBridge.SiteEventLogging -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.SiteEventLogging.dll
ZB.MOM.WW.ScadaBridge.StoreAndForward -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.StoreAndForward.dll
ZB.MOM.WW.ScadaBridge.TemplateEngine -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.TemplateEngine.dll
ZB.MOM.WW.ScadaBridge.AuditLog -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.AuditLog/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.AuditLog.dll
ZB.MOM.WW.ScadaBridge.Transport -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Transport/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Transport.dll
ZB.MOM.WW.ScadaBridge.NotificationService -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.NotificationService/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.NotificationService.dll
ZB.MOM.WW.ScadaBridge.HealthMonitoring -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.HealthMonitoring.dll
ZB.MOM.WW.ScadaBridge.DeploymentManager -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.DeploymentManager.dll
ZB.MOM.WW.ScadaBridge.ExternalSystemGateway -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.dll
ZB.MOM.WW.ScadaBridge.InboundAPI -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.InboundAPI/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.InboundAPI.dll
ZB.MOM.WW.ScadaBridge.NotificationOutbox -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.NotificationOutbox.dll
ZB.MOM.WW.ScadaBridge.ManagementService -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ManagementService/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ManagementService.dll
ZB.MOM.WW.ScadaBridge.CentralUI -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.dll
ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker.dll
ZB.MOM.WW.ScadaBridge.CentralUI.Tests -> /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll
Test run for /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (.NETCoreApp,Version=v10.0)
A total of 1 test files matched the specified pattern.
Passed! - Failed: 0, Passed: 3, Skipped: 0, Total: 3, Duration: 166 ms - ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (net10.0)
@@ -0,0 +1,4 @@
Test run for /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (.NETCoreApp,Version=v10.0)
A total of 1 test files matched the specified pattern.
Passed! - Failed: 0, Passed: 1600, Skipped: 0, Total: 1600, Duration: 32 s - ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (net10.0)
@@ -0,0 +1,55 @@
ZB.MOM.WW.ScadaBridge.Commons -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Commons/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Commons.dll
ZB.MOM.WW.ScadaBridge.KpiHistory -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.KpiHistory/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.KpiHistory.dll
ZB.MOM.WW.ScadaBridge.ConfigurationDatabase -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.dll
ZB.MOM.WW.ScadaBridge.Communication -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Communication/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Communication.dll
ZB.MOM.WW.ScadaBridge.Security -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Security/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Security.dll
ZB.MOM.WW.ScadaBridge.ScriptAnalysis -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.dll
ZB.MOM.WW.ScadaBridge.SiteEventLogging -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.SiteEventLogging/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.SiteEventLogging.dll
ZB.MOM.WW.ScadaBridge.StoreAndForward -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.StoreAndForward/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.StoreAndForward.dll
ZB.MOM.WW.ScadaBridge.TemplateEngine -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.TemplateEngine.dll
ZB.MOM.WW.ScadaBridge.AuditLog -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.AuditLog/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.AuditLog.dll
ZB.MOM.WW.ScadaBridge.Transport -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.Transport/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.Transport.dll
ZB.MOM.WW.ScadaBridge.DeploymentManager -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.DeploymentManager/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.DeploymentManager.dll
ZB.MOM.WW.ScadaBridge.HealthMonitoring -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.HealthMonitoring.dll
ZB.MOM.WW.ScadaBridge.NotificationService -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.NotificationService/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.NotificationService.dll
ZB.MOM.WW.ScadaBridge.ExternalSystemGateway -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ExternalSystemGateway.dll
ZB.MOM.WW.ScadaBridge.InboundAPI -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.InboundAPI/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.InboundAPI.dll
ZB.MOM.WW.ScadaBridge.NotificationOutbox -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.NotificationOutbox/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.NotificationOutbox.dll
ZB.MOM.WW.ScadaBridge.ManagementService -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ManagementService/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.ManagementService.dll
ZB.MOM.WW.ScadaBridge.CentralUI -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.dll
ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker -> /Users/dohertj2/Desktop/ScadaBridge/src/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.SandboxWorker.dll
ZB.MOM.WW.ScadaBridge.CentralUI.Tests -> /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll
Test run for /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/bin/Debug/net10.0/ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (.NETCoreApp,Version=v10.0)
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:01.32] ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogHostPresentationIdentityTests.LateActivationRejection_DoesNotCancelReplacementPrompt [FAIL]
Failed ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogHostPresentationIdentityTests.LateActivationRejection_DoesNotCancelReplacementPrompt [1 s]
Error Message:
Bunit.Extensions.WaitForHelpers.WaitForFailedException : The assertion did not pass within the timeout period.
If this test does not fail consistently, the reason may be that
the wait timeout is too short, and the runtime did not have enough
time to complete the necessary number of renders of the component under test.
This can happen on highly utilized or slower hardware, for example.
To determine if this is the cause, compare the check and render count(s) below
and see if they match what is expected. If they do not,
consider increasing the timeout, either at the individual
method call level, e.g. WaitForElement("div", TimeSpan.FromSeconds(15)),
or via the static TestContext.DefaultWaitTimeout property.
Check count: 1.
Component render count: 5.
Total render count across all components: 0.
---- Assert.Equal() Failure: Strings differ
Expected: "Second"
Actual: null
Stack Trace:
at Bunit.RenderedComponentWaitForHelperExtensions.WaitForAssertion[TComponent](IRenderedComponent`1 renderedComponent, Action assertion, Nullable`1 timeout) in /_/src/bunit/Extensions/WaitForHelpers/RenderedComponentWaitForHelperExtensions.WaitForState.cs:line 77
at ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogHostPresentationIdentityTests.LateActivationRejection_DoesNotCancelReplacementPrompt() in /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/DialogHostPresentationIdentityTests.cs:line 29
--- End of stack trace from previous location ---
----- Inner Stack Trace -----
at ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Shared.DialogHostPresentationIdentityTests.<>c__DisplayClass0_0.<LateActivationRejection_DoesNotCancelReplacementPrompt>b__7() in /Users/dohertj2/Desktop/ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/DialogHostPresentationIdentityTests.cs:line 29
at Bunit.Extensions.WaitForHelpers.WaitForAssertionHelper`1.<>c__DisplayClass6_0.<.ctor>b__0() in /_/src/bunit/Extensions/WaitForHelpers/WaitForAssertionHelper.cs:line 35
at Bunit.Extensions.WaitForHelpers.WaitForHelper`2.OnAfterRender(Object sender, EventArgs args) in /_/src/bunit/Extensions/WaitForHelpers/WaitForHelper.cs:line 173
Failed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1, Duration: 1 s - ZB.MOM.WW.ScadaBridge.CentralUI.Tests.dll (net10.0)
@@ -18,12 +18,12 @@
aria-modal="true" aria-modal="true"
aria-labelledby="@TitleId" aria-labelledby="@TitleId"
aria-describedby="@(state.Kind == DialogKind.Confirm ? BodyId : null)" aria-describedby="@(state.Kind == DialogKind.Confirm ? BodyId : null)"
@onkeydown="OnKeyDown"> @onkeydown="e => OnKeyDown(state, e)">
<div class="modal-dialog modal-dialog-centered@(state.Size is null ? "" : $" {state.Size}")" role="document"> <div class="modal-dialog modal-dialog-centered@(state.Size is null ? "" : $" {state.Size}")" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 id="@TitleId" class="modal-title">@state.Title</h5> <h5 id="@TitleId" class="modal-title">@state.Title</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="Cancel"></button> <button type="button" class="btn-close" aria-label="Close" @onclick="() => Cancel(state)"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@if (state.Kind == DialogKind.Confirm) @if (state.Kind == DialogKind.Confirm)
@@ -41,7 +41,7 @@
class="form-control form-control-sm" class="form-control form-control-sm"
placeholder="@state.Placeholder" placeholder="@state.Placeholder"
value="@_promptValue" value="@_promptValue"
@oninput="OnPromptInput" /> @oninput="e => OnPromptInput(state, e)" />
} }
</div> </div>
@* Custom dialogs supply their own action buttons inside the body, @* Custom dialogs supply their own action buttons inside the body,
@@ -50,10 +50,10 @@
{ {
<div class="modal-footer"> <div class="modal-footer">
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-secondary" @onclick="Cancel">Cancel</button> <button type="button" class="btn btn-outline-secondary" @onclick="() => Cancel(state)">Cancel</button>
<button type="button" <button type="button"
class="btn @ConfirmButtonClass(state)" class="btn @ConfirmButtonClass(state)"
@onclick="Confirm"> @onclick="() => Confirm(state)">
@ConfirmLabel(state) @ConfirmLabel(state)
</button> </button>
</div> </div>
@@ -86,7 +86,7 @@
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (Service is DialogService svc) svc.OnChange -= OnServiceChanged; if (Service is DialogService svc) svc.OnChange -= OnServiceChanged;
Cancel(); // A disposed host must not leave its local dialog result pending. if ((Service as DialogService)?.Current is { } current) Cancel(current); // Complete the disposed presentation.
try { await JS.InvokeVoidAsync("sbDialog.deactivate", _overlayId); } try { await JS.InvokeVoidAsync("sbDialog.deactivate", _overlayId); }
catch (JSDisconnectedException) { } catch (JSDisconnectedException) { }
catch (InvalidOperationException) { /* prerender */ } catch (InvalidOperationException) { /* prerender */ }
@@ -114,7 +114,7 @@
{ {
var activated = await JS.InvokeAsync<bool?>("sbDialog.activate", _overlayId, var activated = await JS.InvokeAsync<bool?>("sbDialog.activate", _overlayId,
current.Kind == DialogKind.Prompt ? "#" + PromptId : null); current.Kind == DialogKind.Prompt ? "#" + PromptId : null);
if (activated == false) Cancel(); // Single-active-overlay contract. if (activated == false) Cancel(current); // Single-active-overlay contract.
} }
catch (JSDisconnectedException) { } catch (JSDisconnectedException) { }
catch (InvalidOperationException) { /* prerender */ } catch (InvalidOperationException) { /* prerender */ }
@@ -128,21 +128,22 @@
} }
} }
private void OnKeyDown(KeyboardEventArgs e) private void OnKeyDown(DialogState state, KeyboardEventArgs e)
{ {
// Native DOM keydown owns synchronous Tab containment; Escape retains // Native DOM keydown owns synchronous Tab containment; Escape retains
// the service's existing typed cancellation path. // the service's existing typed cancellation path.
if (e.Key == "Escape") Cancel(); if (e.Key == "Escape") Cancel(state);
} }
private void OnPromptInput(ChangeEventArgs e) private void OnPromptInput(DialogState state, ChangeEventArgs e)
{ {
if (!ReferenceEquals((Service as DialogService)?.Current, state)) return;
_promptValue = e.Value?.ToString() ?? string.Empty; _promptValue = e.Value?.ToString() ?? string.Empty;
} }
private void Cancel() private void Cancel(DialogState state)
{ {
if (Service is not DialogService svc || svc.Current is null) return; if (Service is not DialogService svc || !ReferenceEquals(svc.Current, state)) return;
var resolveValue = svc.Current.Kind == DialogKind.Confirm var resolveValue = svc.Current.Kind == DialogKind.Confirm
? (object)false ? (object)false
: (object?)null; : (object?)null;
@@ -150,9 +151,9 @@
svc.Resolve(resolveValue); svc.Resolve(resolveValue);
} }
private void Confirm() private void Confirm(DialogState state)
{ {
if (Service is not DialogService svc || svc.Current is null) return; if (Service is not DialogService svc || !ReferenceEquals(svc.Current, state)) return;
var resolveValue = svc.Current.Kind == DialogKind.Confirm var resolveValue = svc.Current.Kind == DialogKind.Confirm
? (object)true ? (object)true
: (object?)_promptValue; : (object?)_promptValue;
@@ -78,11 +78,12 @@ public class DialogService : IDialogService
var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
_tcs = tcs; _tcs = tcs;
// The context routes Close/Cancel back through the same boxed Resolve() // A custom body can finish after its presentation has closed. Only its
// path the host uses for confirm/prompt — Close boxes the typed value, // captured completion source may resolve the active dialog.
// Cancel passes null. Capturing Resolve (not a service reference) keeps var context = new DialogContext<TResult>(result =>
// the body's surface narrow and the resolution path uniform. {
var context = new DialogContext<TResult>(Resolve); if (ReferenceEquals(_tcs, tcs)) Resolve(result);
});
// Materialise the caller's RenderFragment<DialogContext<TResult>> into a // Materialise the caller's RenderFragment<DialogContext<TResult>> into a
// plain RenderFragment bound to this context, so DialogState stays // plain RenderFragment bound to this context, so DialogState stays
@@ -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);
}
}