using System.Reflection; using XFESystem.Collections; namespace XFESystem.Browser.Tests; public sealed class NavigationFailureLifetimeTests { private sealed class FailureTransport:IWebTransport { public WebResponse Get(Uri address,BrowserLimits limits) { if(address.AbsolutePath=="/Unsupported")return new(address,200,"application/json","{}"); throw new IOException("Controlled navigation transport failure."); } } private static T Owner(BrowserService browser,string field)where T:class => Assert.IsType(typeof(BrowserService).GetField(field,BindingFlags.Instance|BindingFlags.NonPublic)!.GetValue(browser)); [Theory] [InlineData("about:Unsupported")] [InlineData("https://")] [InlineData("http://failure.test/Offline")] [InlineData("http://failure.test/Unsupported")] public void FailedNavigationReleasesEveryOldOwnerBeforeDroppingItAndCanRecover(string address) { var trace=new List();var browser=new BrowserService(new FailureTransport()){Trace=trace.Add}; browser.Navigate("about:Start"); Assert.Equal("ready",browser.ExecuteJavaScript("globalThis.kept=document.createElement('button');document.body.append(kept);kept.addEventListener('click',()=>{});globalThis.map=new WeakMap();map.set(kept,{same:kept});setInterval(()=>map.has(kept),1000);'ready'")); browser.Layout(new(796,404)); // Inspection holds the old owners alive deliberately: passing cannot // depend on host GC/finalizers collecting an otherwise leaked owner. var runtime=Owner(browser,"runtime"); var dom=Owner(browser,"dom"); var layout=Owner(browser,"layoutEngine"); var scope=((IWeakCollectionOwner)(object)runtime.Engine).WeakCollections; Assert.True(dom.Lifetime.OutstandingHandles>0);Assert.True(scope.Lifetime.Created>0); Assert.True(runtime.PendingTaskCount>0);Assert.True(layout.RetainedNodeIdentityCount>0); var failed=browser.Navigate(address); Assert.Equal("Navigation failed",failed.Title);Assert.Equal(address,failed.Address); Assert.StartsWith("Navigation failed: ",failed.Text,StringComparison.Ordinal); Assert.Null(browser.Document);Assert.Null(browser.ReadLifetimeDiagnostics());Assert.Null(browser.ReadWeakCollectionLifetime()); Assert.Equal(0,dom.Lifetime.OutstandingHandles);Assert.Equal(dom.Lifetime.HandlesCreated,dom.Lifetime.HandlesDisposed); Assert.Equal(0,runtime.PendingTaskCount);Assert.Equal(0,layout.RetainedNodeIdentityCount); var released=scope.Lifetime; Assert.Equal(0,released.Tables);Assert.Equal(0,released.Entries);Assert.Equal(released.Created,released.Disposed); Assert.Equal(released.TableHandlesCreated,released.TableHandlesDisposed); var release=Assert.Single(trace,line=>line.Contains("XFE_BROWSER_WEAK_RELEASE|",StringComparison.Ordinal)); Assert.Contains("|TablesAfter|0|EntriesAfter|0|Created|"+released.Created+"|Disposed|"+released.Disposed,release,StringComparison.Ordinal); Assert.Contains("|OwnerHandlesCreated|"+released.TableHandlesCreated+"|OwnerHandlesDisposed|"+released.TableHandlesDisposed,release,StringComparison.Ordinal); Assert.EndsWith("|Reason|NavigationFailed\n",release,StringComparison.Ordinal); Assert.Throws(()=>new OwnedWeakTable(scope)); var recovered=browser.Navigate("about:Start"); Assert.Contains("JavaScript ready: 12",recovered.Text,StringComparison.Ordinal); Assert.NotSame(runtime,Owner(browser,"runtime")); Assert.Equal("true",browser.ExecuteJavaScript("const key={};const m=new WeakMap();m.set(key,42);m.get(key)===42")); browser.Release();Assert.Equal(released,scope.Lifetime); } }