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<T>(BrowserService browser,string field)where T:class =>
Assert.IsType<T>(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<string>();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<JavaScriptRuntime>(browser,"runtime");
var dom=Owner<DomBridge>(browser,"dom");
var layout=Owner<BrowserLayoutEngine>(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<ObjectDisposedException>(()=>new OwnedWeakTable<object,object>(scope));
var recovered=browser.Navigate("about:Start");
Assert.Contains("JavaScript ready: 12",recovered.Text,StringComparison.Ordinal);
Assert.NotSame(runtime,Owner<JavaScriptRuntime>(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);
}
}
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<T>(BrowserService browser,string field)where T:class =>
Assert.IsType<T>(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<string>();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<JavaScriptRuntime>(browser,"runtime");
var dom=Owner<DomBridge>(browser,"dom");
var layout=Owner<BrowserLayoutEngine>(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<ObjectDisposedException>(()=>new OwnedWeakTable<object,object>(scope));
var recovered=browser.Navigate("about:Start");
Assert.Contains("JavaScript ready: 12",recovered.Text,StringComparison.Ordinal);
Assert.NotSame(runtime,Owner<JavaScriptRuntime>(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);
}
}