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(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 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 Requests { get; } = []; public List 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)); } }