using System.Text.Json;
namespace XFESystem.Browser.Tests;
public sealed class RemotePageProofTests
{
private const string Hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
[Fact]
public void PublishedProofBindsActualLayoutSourceAndSnapshotWithoutInvalidatingPaint()
{
var page = new BrowserService(new Fixture()); page.Navigate("https://example.test/");
page.Layout(new(800, 500)); long passes = page.LayoutPassCount, revision = page.Revision;
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(12)); var value = proof.RootElement;
Assert.Equal(Hash, value.GetProperty("BodySha256").GetString());
Assert.Equal(12, value.GetProperty("SnapshotSequence").GetInt64());
Assert.Equal("Remote fixture", value.GetProperty("Title").GetString());
Assert.Contains(value.GetProperty("TextRuns").EnumerateArray(), item => item.GetProperty("Text").GetString()!.Contains("Actual"));
Assert.Contains(value.GetProperty("SearchInputs").EnumerateArray(), item => item.GetProperty("Name").GetString() == "q" && item.GetProperty("Bounds").GetProperty("Width").GetDouble() > 0);
Assert.Single(value.GetProperty("LoadedStyles").EnumerateArray());
Assert.Single(value.GetProperty("DeclaredStyleLinks").EnumerateArray());
Assert.Equal(passes, page.LayoutPassCount); Assert.Equal(revision, page.Revision);
}
[Fact]
public void OfflineNavigationNeverReusesPreviousRemoteEvidence()
{
var page = new BrowserService(new Fixture()); page.Navigate("https://example.test/"); page.Layout(new(800, 500));
Assert.NotEqual("{}", page.ReadRemotePageProof());
page.Navigate("about:Start"); page.Layout(new(800, 500)); Assert.Equal("{}", page.ReadRemotePageProof());
}
[Fact]
public void MissingCertificateOrBodyEvidenceCannotBecomeRemoteProof()
{
var page = new BrowserService(new Fixture(verified: false));
Assert.Equal("Navigation failed", page.Navigate("https://example.test/").Title);
Assert.Equal("{}", page.ReadRemotePageProof());
page = new(new Fixture(hash: null)); page.Navigate("https://example.test/"); page.Layout(new(800, 500));
Assert.Equal("{}", page.ReadRemotePageProof());
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ActualStylePaintAndProofRetainRequestedAndFinalUrls(bool redirectStyle)
{
var page = new BrowserService(new Fixture(redirectStyle: redirectStyle));
page.Navigate("https://example.test/");
var layout = page.Layout(new(800, 500));
Assert.Contains(layout.Commands, command => command.Kind == BrowserDrawKind.Text &&
command.Text.Contains("Actual", StringComparison.Ordinal) && command.Color == 0x123456);
var receipt = Assert.Single(page.LoadedResources, item => item.Kind == "style");
string final = redirectStyle ? "https://cdn.example.test/FinalStyle.css" : "https://example.test/Style.css";
Assert.Equal("https://example.test/Style.css", receipt.RequestedAddress);
Assert.Equal(final, receipt.Address);
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(14));
var style = Assert.Single(proof.RootElement.GetProperty("LoadedStyles").EnumerateArray());
Assert.Equal("https://example.test/Style.css", style.GetProperty("RequestedAddress").GetString());
Assert.Equal(final, style.GetProperty("Address").GetString());
Assert.Equal(final, style.GetProperty("FinalAddress").GetString());
Assert.Equal(style.GetProperty("RequestedAddress").GetString(),
Assert.Single(proof.RootElement.GetProperty("DeclaredStyleLinks").EnumerateArray()).GetString());
}
[Fact]
public void RedirectedStylesheetWithInvalidTlsDoesNotBecomeLoadedProof()
{
var page = new BrowserService(new Fixture(redirectStyle: true, styleVerified: false));
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
Assert.DoesNotContain(page.LoadedResources, item => item.Kind == "style");
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(15));
Assert.Empty(proof.RootElement.GetProperty("LoadedStyles").EnumerateArray());
Assert.Single(proof.RootElement.GetProperty("DeclaredStyleLinks").EnumerateArray());
}
[Fact]
public void VisibleMissingImageCannotBeHiddenByClippedOrOffscreenTelemetry()
{
var page = new BrowserService(new GeometryFixture("<img width=24 height=24 src='/missing.png'><div style='height:0;overflow:hidden'><img width=24 height=24 src='/clipped.png'></div><img style='position:absolute;top:900px' width=24 height=24 src='/offscreen.png'>"));
try
{
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(20));
var missing = Assert.Single(proof.RootElement.GetProperty("MissingVisibleImages").EnumerateArray());
Assert.EndsWith("/missing.png", missing.GetProperty("Source").GetString(), StringComparison.Ordinal);
Assert.True(proof.RootElement.GetProperty("DocumentHeight").GetDouble() >= 900);
}
finally { page.Release(); }
}
[Fact]
public void CitationIdentityAndAffineSamplesComeFromTheActualLayout()
{
var page = new BrowserService(new GeometryFixture("<ul><li><cite>Actual citation</cite><h2>Actual heading</h2></li></ul><div class='arrow' style='position:absolute;left:120px;top:150px;width:40px;height:40px;transform:scale(.25) rotate(45deg)'></div>"));
try
{
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
long passes = page.LayoutPassCount, revision = page.Revision;
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(21)); var value = proof.RootElement;
var cite = Assert.Single(value.GetProperty("TextRuns").EnumerateArray(), item => item.GetProperty("Text").GetString() == "citation");
var heading = Assert.Single(value.GetProperty("TextRuns").EnumerateArray(), item => item.GetProperty("Text").GetString() == "heading");
Assert.True(cite.GetProperty("ListItemId").GetInt32() > 0);
Assert.Equal(cite.GetProperty("ListItemId").GetInt32(), heading.GetProperty("ListItemId").GetInt32());
Assert.Contains("cite|li", cite.GetProperty("Ancestors").GetString(), StringComparison.Ordinal);
var arrow = Assert.Single(value.GetProperty("LayoutSamples").EnumerateArray(), item => item.GetProperty("Class").GetString() == "arrow");
Assert.InRange(arrow.GetProperty("Bounds").GetProperty("Width").GetDouble(), 14.14, 14.15);
Assert.InRange(arrow.GetProperty("Transform")[0].GetDouble(), .176, .177);
Assert.Equal(passes, page.LayoutPassCount); Assert.Equal(revision, page.Revision);
}
finally { page.Release(); }
}
[Fact]
public void NavigationReadbackUsesPaintCommandsAndRetainsClippedLinksAsUnpainted()
{
var page = new BrowserService(new GeometryFixture("<nav style='height:25px;overflow:hidden'><a href='/one' style='display:block;height:20px;line-height:20px'>One</a><a href='/two' style='display:block;height:20px;line-height:20px'>Two</a><a href='/three' style='display:block;height:20px;line-height:20px'>Three</a></nav>"));
try
{
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(22));
var links = proof.RootElement.GetProperty("NavigationLinks").EnumerateArray().ToArray();
Assert.Equal(3, links.Length); Assert.True(links[0].GetProperty("Painted").GetBoolean());
Assert.False(links[2].GetProperty("Painted").GetBoolean());
Assert.True(links[0].GetProperty("MenuId").GetInt32() > 0);
}
finally { page.Release(); }
}
private sealed class GeometryFixture(string body) : IWebTransport
{
public WebResponse Get(Uri address, BrowserLimits limits) => address.AbsolutePath == "/"
? new(address, 200, "text/html", "<html><head><title>Geometry</title></head><body>" + body + "</body></html>", true, BodySha256: Hash)
: new(address, 404, "text/plain", "Missing", true);
}
private sealed class Fixture(bool verified = true, string? hash = Hash, bool redirectStyle = false, bool styleVerified = true) : IWebTransport
{
public WebResponse Get(Uri address, BrowserLimits limits) => address.AbsolutePath == "/Style.css"
? new(redirectStyle ? new Uri("https://cdn.example.test/FinalStyle.css") : address, 200, "text/css", "p{color:#123456} input{width:240px;height:32px}", verified && styleVerified)
: new(address, 200, "text/html", "<html><head><title>Remote fixture</title><link rel='stylesheet' href='/Style.css'></head><body><p>Actual remote text</p><input name='q' value='Query'></body></html>", verified, BodySha256: hash);
}
}
using System.Text.Json;
namespace XFESystem.Browser.Tests;
public sealed class RemotePageProofTests
{
private const string Hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
[Fact]
public void PublishedProofBindsActualLayoutSourceAndSnapshotWithoutInvalidatingPaint()
{
var page = new BrowserService(new Fixture()); page.Navigate("https://example.test/");
page.Layout(new(800, 500)); long passes = page.LayoutPassCount, revision = page.Revision;
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(12)); var value = proof.RootElement;
Assert.Equal(Hash, value.GetProperty("BodySha256").GetString());
Assert.Equal(12, value.GetProperty("SnapshotSequence").GetInt64());
Assert.Equal("Remote fixture", value.GetProperty("Title").GetString());
Assert.Contains(value.GetProperty("TextRuns").EnumerateArray(), item => item.GetProperty("Text").GetString()!.Contains("Actual"));
Assert.Contains(value.GetProperty("SearchInputs").EnumerateArray(), item => item.GetProperty("Name").GetString() == "q" && item.GetProperty("Bounds").GetProperty("Width").GetDouble() > 0);
Assert.Single(value.GetProperty("LoadedStyles").EnumerateArray());
Assert.Single(value.GetProperty("DeclaredStyleLinks").EnumerateArray());
Assert.Equal(passes, page.LayoutPassCount); Assert.Equal(revision, page.Revision);
}
[Fact]
public void OfflineNavigationNeverReusesPreviousRemoteEvidence()
{
var page = new BrowserService(new Fixture()); page.Navigate("https://example.test/"); page.Layout(new(800, 500));
Assert.NotEqual("{}", page.ReadRemotePageProof());
page.Navigate("about:Start"); page.Layout(new(800, 500)); Assert.Equal("{}", page.ReadRemotePageProof());
}
[Fact]
public void MissingCertificateOrBodyEvidenceCannotBecomeRemoteProof()
{
var page = new BrowserService(new Fixture(verified: false));
Assert.Equal("Navigation failed", page.Navigate("https://example.test/").Title);
Assert.Equal("{}", page.ReadRemotePageProof());
page = new(new Fixture(hash: null)); page.Navigate("https://example.test/"); page.Layout(new(800, 500));
Assert.Equal("{}", page.ReadRemotePageProof());
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ActualStylePaintAndProofRetainRequestedAndFinalUrls(bool redirectStyle)
{
var page = new BrowserService(new Fixture(redirectStyle: redirectStyle));
page.Navigate("https://example.test/");
var layout = page.Layout(new(800, 500));
Assert.Contains(layout.Commands, command => command.Kind == BrowserDrawKind.Text &&
command.Text.Contains("Actual", StringComparison.Ordinal) && command.Color == 0x123456);
var receipt = Assert.Single(page.LoadedResources, item => item.Kind == "style");
string final = redirectStyle ? "https://cdn.example.test/FinalStyle.css" : "https://example.test/Style.css";
Assert.Equal("https://example.test/Style.css", receipt.RequestedAddress);
Assert.Equal(final, receipt.Address);
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(14));
var style = Assert.Single(proof.RootElement.GetProperty("LoadedStyles").EnumerateArray());
Assert.Equal("https://example.test/Style.css", style.GetProperty("RequestedAddress").GetString());
Assert.Equal(final, style.GetProperty("Address").GetString());
Assert.Equal(final, style.GetProperty("FinalAddress").GetString());
Assert.Equal(style.GetProperty("RequestedAddress").GetString(),
Assert.Single(proof.RootElement.GetProperty("DeclaredStyleLinks").EnumerateArray()).GetString());
}
[Fact]
public void RedirectedStylesheetWithInvalidTlsDoesNotBecomeLoadedProof()
{
var page = new BrowserService(new Fixture(redirectStyle: true, styleVerified: false));
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
Assert.DoesNotContain(page.LoadedResources, item => item.Kind == "style");
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(15));
Assert.Empty(proof.RootElement.GetProperty("LoadedStyles").EnumerateArray());
Assert.Single(proof.RootElement.GetProperty("DeclaredStyleLinks").EnumerateArray());
}
[Fact]
public void VisibleMissingImageCannotBeHiddenByClippedOrOffscreenTelemetry()
{
var page = new BrowserService(new GeometryFixture("<img width=24 height=24 src='/missing.png'><div style='height:0;overflow:hidden'><img width=24 height=24 src='/clipped.png'></div><img style='position:absolute;top:900px' width=24 height=24 src='/offscreen.png'>"));
try
{
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(20));
var missing = Assert.Single(proof.RootElement.GetProperty("MissingVisibleImages").EnumerateArray());
Assert.EndsWith("/missing.png", missing.GetProperty("Source").GetString(), StringComparison.Ordinal);
Assert.True(proof.RootElement.GetProperty("DocumentHeight").GetDouble() >= 900);
}
finally { page.Release(); }
}
[Fact]
public void CitationIdentityAndAffineSamplesComeFromTheActualLayout()
{
var page = new BrowserService(new GeometryFixture("<ul><li><cite>Actual citation</cite><h2>Actual heading</h2></li></ul><div class='arrow' style='position:absolute;left:120px;top:150px;width:40px;height:40px;transform:scale(.25) rotate(45deg)'></div>"));
try
{
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
long passes = page.LayoutPassCount, revision = page.Revision;
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(21)); var value = proof.RootElement;
var cite = Assert.Single(value.GetProperty("TextRuns").EnumerateArray(), item => item.GetProperty("Text").GetString() == "citation");
var heading = Assert.Single(value.GetProperty("TextRuns").EnumerateArray(), item => item.GetProperty("Text").GetString() == "heading");
Assert.True(cite.GetProperty("ListItemId").GetInt32() > 0);
Assert.Equal(cite.GetProperty("ListItemId").GetInt32(), heading.GetProperty("ListItemId").GetInt32());
Assert.Contains("cite|li", cite.GetProperty("Ancestors").GetString(), StringComparison.Ordinal);
var arrow = Assert.Single(value.GetProperty("LayoutSamples").EnumerateArray(), item => item.GetProperty("Class").GetString() == "arrow");
Assert.InRange(arrow.GetProperty("Bounds").GetProperty("Width").GetDouble(), 14.14, 14.15);
Assert.InRange(arrow.GetProperty("Transform")[0].GetDouble(), .176, .177);
Assert.Equal(passes, page.LayoutPassCount); Assert.Equal(revision, page.Revision);
}
finally { page.Release(); }
}
[Fact]
public void NavigationReadbackUsesPaintCommandsAndRetainsClippedLinksAsUnpainted()
{
var page = new BrowserService(new GeometryFixture("<nav style='height:25px;overflow:hidden'><a href='/one' style='display:block;height:20px;line-height:20px'>One</a><a href='/two' style='display:block;height:20px;line-height:20px'>Two</a><a href='/three' style='display:block;height:20px;line-height:20px'>Three</a></nav>"));
try
{
page.Navigate("https://example.test/"); page.Layout(new(800, 500));
using var proof = JsonDocument.Parse(page.ReadRemotePageProof(22));
var links = proof.RootElement.GetProperty("NavigationLinks").EnumerateArray().ToArray();
Assert.Equal(3, links.Length); Assert.True(links[0].GetProperty("Painted").GetBoolean());
Assert.False(links[2].GetProperty("Painted").GetBoolean());
Assert.True(links[0].GetProperty("MenuId").GetInt32() > 0);
}
finally { page.Release(); }
}
private sealed class GeometryFixture(string body) : IWebTransport
{
public WebResponse Get(Uri address, BrowserLimits limits) => address.AbsolutePath == "/"
? new(address, 200, "text/html", "<html><head><title>Geometry</title></head><body>" + body + "</body></html>", true, BodySha256: Hash)
: new(address, 404, "text/plain", "Missing", true);
}
private sealed class Fixture(bool verified = true, string? hash = Hash, bool redirectStyle = false, bool styleVerified = true) : IWebTransport
{
public WebResponse Get(Uri address, BrowserLimits limits) => address.AbsolutePath == "/Style.css"
? new(redirectStyle ? new Uri("https://cdn.example.test/FinalStyle.css") : address, 200, "text/css", "p{color:#123456} input{width:240px;height:32px}", verified && styleVerified)
: new(address, 200, "text/html", "<html><head><title>Remote fixture</title><link rel='stylesheet' href='/Style.css'></head><body><p>Actual remote text</p><input name='q' value='Query'></body></html>", verified, BodySha256: hash);
}
}