using XFESystem.Desktop;
namespace XFESystem.Browser.Tests;
public sealed class BrowserPaintIdentityTests
{
[Fact]
public void ActualPageAndBackgroundSnapshotsUseDistinctIdentitiesAndRestoreOnlyTheirOwnFrames()
{
var canvas = new FrameCanvas();
var scheduler = new Scheduler();
var browser = new BrowserService();
browser.ResizeViewport(canvas.Width, canvas.Height);
browser.Navigate("about:Start");
var viewport = new DesktopBounds(0, 0, canvas.Width, canvas.Height);
browser.Paint(canvas, viewport, 0);
Assert.NotEmpty(canvas.Stores);
long regularIdentity = canvas.Stores[^1].PageId;
var regularPixels = (int[])canvas.Pixels.Clone();
var preparation = new BrowserEditorPreparationService(new BrowserService(), scheduler, new CanvasFactory());
var operation = preparation.Begin(new(7, 1, "const mixedIdentity = 42;", "Mixed.js", canvas.Width, canvas.Height));
scheduler.RunAll();
Assert.True(operation.TryTakeResult(out var prepared));
Assert.Null(prepared.Error);
var actor = Assert.IsType<BackgroundBrowserEditorSession>(prepared.Page);
try
{
actor.Paint(canvas, viewport, 0);
long actorIdentity = canvas.Requests[^1].PageId;
Assert.NotEqual(regularIdentity, actorIdentity);
var actorPixels = (int[])canvas.Pixels.Clone();
Assert.False(regularPixels.AsSpan().SequenceEqual(actorPixels));
Assert.Equal("const mixedIdentity = 42;", actor.ReadEditorText());
// Repainting an unchanged page must still hit the real bounded
// cache. Painting either owner after the other restores exactly
// that owner's pixels, with no extra capture or raster calls.
int stores = canvas.Stores.Count, fills = canvas.FillCalls;
browser.Paint(canvas, viewport, 0);
Assert.Equal(regularPixels, canvas.Pixels);
Assert.Equal(1, browser.CachedPaintCount);
actor.Paint(canvas, viewport, 0);
Assert.Equal(actorPixels, canvas.Pixels);
Assert.Equal(1, actor.CachedPaintCount);
Assert.Equal(stores, canvas.Stores.Count);
Assert.Equal(fills, canvas.FillCalls);
var another = new BrowserService();
try
{
another.Navigate("about:Start");
another.Paint(canvas, viewport, 0);
long otherIdentity = canvas.Stores[^1].PageId;
Assert.NotEqual(regularIdentity, otherIdentity);
Assert.NotEqual(actorIdentity, otherIdentity);
Assert.Equal(0, another.CachedPaintCount);
}
finally { another.Release(); }
}
finally
{
actor.Cancel(); scheduler.RunAll();
browser.Release();
}
}
private sealed class Scheduler : IEditorBackgroundWorkScheduler
{
private readonly Queue<Action> pending = [];
public bool TryQueue(Action work) { pending.Enqueue(work); return true; }
public void RunAll()
{
int count = 0;
while (pending.TryDequeue(out var work)) { Assert.True(++count < 20); work(); }
}
}
private sealed class CanvasFactory : IEditorPreparationCanvasFactory
{
public IDesktopCanvas CreateEditorPreparationCanvas() => new FrameCanvas();
}
private sealed class FrameCanvas : IDesktopWebCanvas, IDesktopWebFrameCache
{
private readonly DesktopWebFrameCache frames = new();
public int Width => 796;
public int Height => 404;
public int[] Pixels { get; } = new int[796 * 404];
public List<DesktopWebFrameKey> Requests { get; } = [];
public List<DesktopWebFrameKey> Stores { get; } = [];
public int FillCalls { get; private set; }
public DesktopBounds WebPaintClip => new(0, 0, Width, Height);
public bool TryRestoreWebFrame(long page, long revision, long layout, DesktopBounds bounds, double scrollX, double scrollY)
{
var key = new DesktopWebFrameKey(page, revision, layout, bounds.Width, bounds.Height, 100, scrollX, scrollY);
Requests.Add(key);
return frames.TryRestore(key, Pixels, Width, Height, bounds.X, bounds.Y, WebPaintClip);
}
public void StoreWebFrame(long page, long revision, long layout, DesktopBounds bounds, double scrollX, double scrollY)
{
var key = new DesktopWebFrameKey(page, revision, layout, bounds.Width, bounds.Height, 100, scrollX, scrollY);
Assert.True(frames.Store(key, Pixels, Width, Height, bounds.X, bounds.Y, WebPaintClip));
Stores.Add(key);
}
public void Fill(uint rgb, int x, int y, int width, int height)
{
FillCalls++;
int left = Math.Clamp(x, 0, Width), top = Math.Clamp(y, 0, Height);
int right = Math.Clamp(x + width, left, Width), bottom = Math.Clamp(y + height, top, Height);
for (int row = top; row < bottom; row++) Array.Fill(Pixels, (int)rgb, row * Width + left, right - left);
}
// Deterministic host-only text marks make cached frame mixups visible;
// this is a cache ownership test, never native glyph/font evidence.
public void Text(string text, uint rgb, int x, int y)
{
uint hash = 2166136261;
foreach (char character in text) hash = unchecked((hash ^ character) * 16777619);
if ((uint)x < Width && (uint)y < Height) Pixels[y * Width + x] = unchecked((int)(hash ^ rgb));
}
public void Line(uint rgb, int x1, int y1, int x2, int y2) { }
public void Present() { }
public void PushClip(DesktopBounds bounds) { }
public void PopClip() { }
public double MeasureWebText(string text, double size, string family, int weight) =>
new BrowserFallbackTextMetrics().Measure(text, new(size, family, weight));
public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double size, string family, int weight) => (0, 0, []);
public void WebText(string text, uint rgb, double x, double y, double size, string family, int weight, double opacity = 1) =>
Text(text, rgb, (int)Math.Round(x), (int)Math.Round(y));
}
}
using XFESystem.Desktop;
namespace XFESystem.Browser.Tests;
public sealed class BrowserPaintIdentityTests
{
[Fact]
public void ActualPageAndBackgroundSnapshotsUseDistinctIdentitiesAndRestoreOnlyTheirOwnFrames()
{
var canvas = new FrameCanvas();
var scheduler = new Scheduler();
var browser = new BrowserService();
browser.ResizeViewport(canvas.Width, canvas.Height);
browser.Navigate("about:Start");
var viewport = new DesktopBounds(0, 0, canvas.Width, canvas.Height);
browser.Paint(canvas, viewport, 0);
Assert.NotEmpty(canvas.Stores);
long regularIdentity = canvas.Stores[^1].PageId;
var regularPixels = (int[])canvas.Pixels.Clone();
var preparation = new BrowserEditorPreparationService(new BrowserService(), scheduler, new CanvasFactory());
var operation = preparation.Begin(new(7, 1, "const mixedIdentity = 42;", "Mixed.js", canvas.Width, canvas.Height));
scheduler.RunAll();
Assert.True(operation.TryTakeResult(out var prepared));
Assert.Null(prepared.Error);
var actor = Assert.IsType<BackgroundBrowserEditorSession>(prepared.Page);
try
{
actor.Paint(canvas, viewport, 0);
long actorIdentity = canvas.Requests[^1].PageId;
Assert.NotEqual(regularIdentity, actorIdentity);
var actorPixels = (int[])canvas.Pixels.Clone();
Assert.False(regularPixels.AsSpan().SequenceEqual(actorPixels));
Assert.Equal("const mixedIdentity = 42;", actor.ReadEditorText());
// Repainting an unchanged page must still hit the real bounded
// cache. Painting either owner after the other restores exactly
// that owner's pixels, with no extra capture or raster calls.
int stores = canvas.Stores.Count, fills = canvas.FillCalls;
browser.Paint(canvas, viewport, 0);
Assert.Equal(regularPixels, canvas.Pixels);
Assert.Equal(1, browser.CachedPaintCount);
actor.Paint(canvas, viewport, 0);
Assert.Equal(actorPixels, canvas.Pixels);
Assert.Equal(1, actor.CachedPaintCount);
Assert.Equal(stores, canvas.Stores.Count);
Assert.Equal(fills, canvas.FillCalls);
var another = new BrowserService();
try
{
another.Navigate("about:Start");
another.Paint(canvas, viewport, 0);
long otherIdentity = canvas.Stores[^1].PageId;
Assert.NotEqual(regularIdentity, otherIdentity);
Assert.NotEqual(actorIdentity, otherIdentity);
Assert.Equal(0, another.CachedPaintCount);
}
finally { another.Release(); }
}
finally
{
actor.Cancel(); scheduler.RunAll();
browser.Release();
}
}
private sealed class Scheduler : IEditorBackgroundWorkScheduler
{
private readonly Queue<Action> pending = [];
public bool TryQueue(Action work) { pending.Enqueue(work); return true; }
public void RunAll()
{
int count = 0;
while (pending.TryDequeue(out var work)) { Assert.True(++count < 20); work(); }
}
}
private sealed class CanvasFactory : IEditorPreparationCanvasFactory
{
public IDesktopCanvas CreateEditorPreparationCanvas() => new FrameCanvas();
}
private sealed class FrameCanvas : IDesktopWebCanvas, IDesktopWebFrameCache
{
private readonly DesktopWebFrameCache frames = new();
public int Width => 796;
public int Height => 404;
public int[] Pixels { get; } = new int[796 * 404];
public List<DesktopWebFrameKey> Requests { get; } = [];
public List<DesktopWebFrameKey> Stores { get; } = [];
public int FillCalls { get; private set; }
public DesktopBounds WebPaintClip => new(0, 0, Width, Height);
public bool TryRestoreWebFrame(long page, long revision, long layout, DesktopBounds bounds, double scrollX, double scrollY)
{
var key = new DesktopWebFrameKey(page, revision, layout, bounds.Width, bounds.Height, 100, scrollX, scrollY);
Requests.Add(key);
return frames.TryRestore(key, Pixels, Width, Height, bounds.X, bounds.Y, WebPaintClip);
}
public void StoreWebFrame(long page, long revision, long layout, DesktopBounds bounds, double scrollX, double scrollY)
{
var key = new DesktopWebFrameKey(page, revision, layout, bounds.Width, bounds.Height, 100, scrollX, scrollY);
Assert.True(frames.Store(key, Pixels, Width, Height, bounds.X, bounds.Y, WebPaintClip));
Stores.Add(key);
}
public void Fill(uint rgb, int x, int y, int width, int height)
{
FillCalls++;
int left = Math.Clamp(x, 0, Width), top = Math.Clamp(y, 0, Height);
int right = Math.Clamp(x + width, left, Width), bottom = Math.Clamp(y + height, top, Height);
for (int row = top; row < bottom; row++) Array.Fill(Pixels, (int)rgb, row * Width + left, right - left);
}
// Deterministic host-only text marks make cached frame mixups visible;
// this is a cache ownership test, never native glyph/font evidence.
public void Text(string text, uint rgb, int x, int y)
{
uint hash = 2166136261;
foreach (char character in text) hash = unchecked((hash ^ character) * 16777619);
if ((uint)x < Width && (uint)y < Height) Pixels[y * Width + x] = unchecked((int)(hash ^ rgb));
}
public void Line(uint rgb, int x1, int y1, int x2, int y2) { }
public void Present() { }
public void PushClip(DesktopBounds bounds) { }
public void PopClip() { }
public double MeasureWebText(string text, double size, string family, int weight) =>
new BrowserFallbackTextMetrics().Measure(text, new(size, family, weight));
public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double size, string family, int weight) => (0, 0, []);
public void WebText(string text, uint rgb, double x, double y, double size, string family, int weight, double opacity = 1) =>
Text(text, rgb, (int)Math.Round(x), (int)Math.Round(y));
}
}