using ScadaLink.CentralUI.Components.Shared; namespace ScadaLink.CentralUI.Tests.Shared; /// /// Characterization tests for CentralUI-015 (re-triaged Won't Fix — see /// findings.md). The finding claimed ContinueWith(..., TaskScheduler.Default) /// made callers resume off the render thread; that premise is incorrect — an /// await always resumes on the awaiter's own captured /// regardless of where the awaited task /// completes. ConfirmAsync_AwaiterResumesOnItsCapturedSyncContext pins /// that correct behaviour (it passes against both the old ContinueWith /// form and the current inline-projection form). The remaining tests pin the /// dialog result-resolution contract. /// public class DialogServiceThreadingTests { /// /// A single-threaded sync context that records every posted callback — /// stands in for the Blazor renderer's dispatcher. /// private sealed class TrackingSyncContext : SynchronizationContext { private readonly Thread _thread; private readonly System.Collections.Concurrent.BlockingCollection<(SendOrPostCallback, object?)> _queue = new(); public int PostedCount; public TrackingSyncContext() { _thread = new Thread(() => { SetSynchronizationContext(this); foreach (var (cb, st) in _queue.GetConsumingEnumerable()) { cb(st); } }) { IsBackground = true }; _thread.Start(); } public override void Post(SendOrPostCallback d, object? state) { Interlocked.Increment(ref PostedCount); _queue.Add((d, state)); } public void Complete() => _queue.CompleteAdding(); } [Fact] public async Task ConfirmAsync_AwaiterResumesOnItsCapturedSyncContext() { var service = new DialogService(); var ctx = new TrackingSyncContext(); // Run the awaiting "component" code on the tracking context. var done = new TaskCompletionSource(); ctx.Post(async void (_) => { try { var task = service.ConfirmAsync("t", "m"); // Resolve from another thread, mimicking the host dispatching. _ = Task.Run(() => service.Resolve(true)); await task; // The continuation after the await must be back on the tracking // context's single thread. done.SetResult(Environment.CurrentManagedThreadId); } catch (Exception ex) { done.SetException(ex); } }, null); var resumeThreadId = await done.Task; ctx.Complete(); // The continuation was posted to (and ran on) the captured context. Assert.True(ctx.PostedCount >= 1, "ConfirmAsync continuation must post back to the caller's SynchronizationContext."); Assert.NotEqual(Environment.CurrentManagedThreadId, resumeThreadId); } [Fact] public async Task ConfirmAsync_ResolvesWithExpectedValue() { var service = new DialogService(); var task = service.ConfirmAsync("t", "m"); service.Resolve(true); Assert.True(await task); } [Fact] public async Task PromptAsync_ResolvesWithExpectedValue() { var service = new DialogService(); var task = service.PromptAsync("t", "label"); service.Resolve("typed value"); Assert.Equal("typed value", await task); } [Fact] public async Task PromptAsync_CancelledResolvesToNull() { var service = new DialogService(); var task = service.PromptAsync("t", "label"); service.Resolve(null); Assert.Null(await task); } }