using System.Reflection; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Browsing; using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw; using ZB.MOM.WW.OtOpcUa.Commons.Browsing; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw; // BL0006: the test renderer needs RenderTree types (same suppression as RawBrowseModalStaleOpenTests). #pragma warning disable BL0006 /// /// G2-F3: after a rebirth request the browse modal re-reads the tree itself (the session's /// RequestRebirthAsync now waits for the births), instead of telling the operator to re-expand it. /// [Trait("Category", "Unit")] public sealed class RawBrowseRebirthRefreshTests : IDisposable { private const BindingFlags Private = BindingFlags.NonPublic | BindingFlags.Instance; private readonly List _renderers = new(); public void Dispose() { foreach (var r in _renderers) r.Dispose(); } [Fact] public async Task A_completed_rebirth_rekeys_the_tree_and_keeps_the_outcome() { var svc = new RebirthBrowserService { Result = Task.FromResult(2) }; var modal = NewOpenModal(svc); var before = Get(modal, "_treeGeneration"); await ConfirmAsync(modal); Get(modal, "_treeGeneration").ShouldBe(before + 1); Get(modal, "_rebirthOutcome")!.ShouldContain("The tree was refreshed"); Get(modal, "_rebirthTarget").ShouldBeNull(); svc.Scopes.ShouldBe(["Grp/EdgeA"]); } [Fact] public async Task A_modal_closed_during_the_wait_is_not_refreshed() { var gate = new TaskCompletionSource(); var svc = new RebirthBrowserService { Result = gate.Task }; var modal = NewOpenModal(svc); var before = Get(modal, "_treeGeneration"); var confirm = ConfirmAsync(modal); typeof(RawBrowseModal).GetProperty("Visible")!.SetValue(modal, false); gate.SetResult(1); await confirm; Get(modal, "_treeGeneration").ShouldBe(before); Get(modal, "_rebirthOutcome")!.ShouldNotContain("refreshed"); // Positive control: the publish itself was still made and reported. svc.Scopes.ShouldBe(["Grp/EdgeA"]); } [Fact] public async Task An_open_superseded_by_a_close_and_reopen_closes_its_own_session() { // Review r0 M5: Visible is true again after a reopen, so only the generation identifies the stale open. var gate = new TaskCompletionSource(); var svc = new RebirthBrowserService { Result = Task.FromResult(0), Open = () => gate.Task }; var modal = NewOpenModal(svc); Set(modal, "_token", Guid.Empty); var open = (Task)typeof(RawBrowseModal).GetMethod("OpenBrowseAsync", Private)!.Invoke(modal, null)!; Set(modal, "_openGeneration", Get(modal, "_openGeneration") + 1); // closed + reopened meanwhile var stale = Guid.NewGuid(); gate.SetResult(new BrowseOpenResult(true, null, stale)); await open; await svc.Closed.Task.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); svc.ClosedTokens.ShouldBe([stale]); Get(modal, "_token").ShouldBe(Guid.Empty); } [Fact] public async Task A_current_open_adopts_its_session() { // Positive control for the test above. var token = Guid.NewGuid(); var svc = new RebirthBrowserService { Result = Task.FromResult(0), Open = () => Task.FromResult(new BrowseOpenResult(true, null, token)) }; var modal = NewOpenModal(svc); Set(modal, "_token", Guid.Empty); await (Task)typeof(RawBrowseModal).GetMethod("OpenBrowseAsync", Private)!.Invoke(modal, null)!; Get(modal, "_token").ShouldBe(token); svc.ClosedTokens.ShouldBeEmpty(); } private RawBrowseModal NewOpenModal(IBrowserSessionService svc) { var modal = new RawBrowseModal(); typeof(RawBrowseModal).GetProperty("BrowserService", Private)!.SetValue(modal, svc); typeof(RawBrowseModal).GetProperty("Visible")!.SetValue(modal, true); Set(modal, "_token", Guid.NewGuid()); Set(modal, "_canRebirth", true); var targetType = typeof(RawBrowseModal).GetNestedType("RebirthTarget", BindingFlags.NonPublic)!; Set(modal, "_rebirthTarget", Activator.CreateInstance(targetType, "Grp/EdgeA", "edge node EdgeA", "detail", false)); Set(modal, "_rebirthConfirming", true); var renderer = new InertRenderer(); _renderers.Add(renderer); renderer.Attach(modal); return modal; } private static Task ConfirmAsync(RawBrowseModal modal) => (Task)typeof(RawBrowseModal).GetMethod("ConfirmRebirthAsync", Private)!.Invoke(modal, null)!; private static void Set(RawBrowseModal modal, string field, object? value) { var f = typeof(RawBrowseModal).GetField(field, Private); f.ShouldNotBeNull($"{field} was renamed"); f!.SetValue(modal, value); } private static T Get(RawBrowseModal modal, string field) => (T)typeof(RawBrowseModal).GetField(field, Private)!.GetValue(modal)!; private sealed class RebirthBrowserService : IBrowserSessionService { public required Task Result { get; init; } public Func> Open { get; init; } = () => throw new NotImplementedException(); public List Scopes { get; } = new(); public List ClosedTokens { get; } = new(); public TaskCompletionSource Closed { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); public bool CanBrowse(string driverType, string configJson) => true; public Task OpenAsync(string driverType, string configJson, CancellationToken ct) => Open(); public Task> RootAsync(Guid token, CancellationToken ct) => throw new NotImplementedException(); public Task> ExpandAsync(Guid token, string nodeId, CancellationToken ct) => throw new NotImplementedException(); public Task> AttributesAsync(Guid token, string nodeId, CancellationToken ct) => throw new NotImplementedException(); public bool CanRequestRebirth(Guid token) => true; public Task CloseAsync(Guid token) { lock (ClosedTokens) ClosedTokens.Add(token); Closed.TrySetResult(); return Task.CompletedTask; } public Task RequestRebirthAsync(Guid token, string scope, CancellationToken ct) { Scopes.Add(scope); return Result; } } private sealed class InertRenderer() : Renderer(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance) { public override Dispatcher Dispatcher { get; } = new InlineDispatcher(); public void Attach(IComponent component) => AssignRootComponentId(component); protected override void HandleException(Exception exception) => ExceptionDispatchInfo.Capture(exception).Throw(); protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) => Task.CompletedTask; } private sealed class InlineDispatcher : Dispatcher { public override bool CheckAccess() => true; public override Task InvokeAsync(Action workItem) { workItem(); return Task.CompletedTask; } public override Task InvokeAsync(Func workItem) => workItem(); public override Task InvokeAsync(Func workItem) => Task.FromResult(workItem()); public override Task InvokeAsync(Func> workItem) => workItem(); } }