XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using XFESystem.Desktop;

namespace XFESystem.Browser.Tests;

public sealed class BackgroundPageTests
{
    [Fact]
    public void EnhancementContinuationDoesNotNeedAnotherUiTimerAndYieldsToQueuedWork()
    {
        var worker = new Worker(); var factory = new Factory(worker); long now = 100;
        var service = new BrowserEditorPreparationService(factory, worker, new Canvases(worker),
            monotonicMilliseconds: () => now);
        var operation = service.Begin(new(80, 1, "# Loading", "Loading.md", 500, 300, now, EditorPreparationKind.Document));
        worker.Run(); Assert.True(operation.TryTakeResult(out var result));
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        Assert.True(factory.Page!.DocumentEnhancementsPending); Assert.Equal(0, worker.Count);
        actor.Paint(new Scanout(), new(0, 0, 500, 300), 0); actor.FramePresented();
        bool interleaved = false;
        worker.TryQueue(() => { Assert.True(factory.Page.DocumentEnhancementsPending); interleaved = true; });
        worker.Run();
        Assert.True(interleaved); Assert.False(factory.Page.DocumentEnhancementsPending);
        Assert.Equal(100, now); Assert.Equal(0, worker.Count);
        actor.Cancel(); worker.Run(); Assert.True(actor.IsRetired);
    }

    [Fact]
    public void DocumentParsingGlyphPreparationAndEnhancementStayOnWorker()
    {
        var worker = new Worker(); var factory = new Factory(worker); var canvases = new Canvases(worker);
        long now = 100;
        var service = new BrowserEditorPreparationService(factory, worker, canvases,
            threadIdentity: () => worker.Running ? 2 : 1, monotonicMilliseconds: () => now);
        var operation = service.Begin(new(1, 1, "# Background heading\n\nA **real** Markdown document.", "Guide.md",
            796, 404, now, EditorPreparationKind.Document));
        Assert.Equal(0, factory.Creates); Assert.Equal(0, canvases.Creates); Assert.False(operation.TryTakeResult(out _));
        worker.Run();
        Assert.True(operation.TryTakeResult(out var result)); Assert.Null(result.Error);
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        Assert.Null(actor.ReadEditorText()); Assert.Contains("Background heading", actor.CurrentPage.Text);
        Assert.NotNull(factory.Page!.Document); Assert.True(factory.Page.DocumentEnhancementsPending);
        Assert.False(factory.Page.DocumentFirstFramePresented);
        Assert.True(canvases.Prepared > 0); Assert.Null(result.PreparedCanvas);
        var scanout = new Scanout();
        actor.Paint(scanout, new(4, 8, 796, 404), 0);
        Assert.True(scanout.Blends > 0); Assert.Equal(0, worker.Count);
        actor.FramePresented();
        Assert.False(factory.Page.DocumentFirstFramePresented); Assert.Equal(1, worker.Count);
        now += 100; worker.Run();
        Assert.True(factory.Page.DocumentFirstFramePresented);
        actor.Cancel(); worker.Run(); Assert.True(actor.IsRetired);
        Assert.Null(factory.Page.Document);
    }

    [Fact]
    public void NetworkWaitUsesIndependentQueueAndFailurePublishesVisibleSnapshot()
    {
        var documents = new Worker(); var network = new Worker();
        var factory = new Factory(network, failNetwork: true); var canvas = new Canvases(network);
        var service = new BrowserEditorPreparationService(factory, documents, canvas, navigationScheduler: network);
        var operation = service.Begin(new(2, 1, "", "https://www.bing.com/", 500, 300, Kind: EditorPreparationKind.Navigation));
        Assert.Equal(0, documents.Count); Assert.Equal(1, network.Count); Assert.Equal(0, factory.NetworkCalls);
        network.Run();
        Assert.True(operation.TryTakeResult(out var result)); Assert.Null(result.Error);
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        Assert.Equal("https://www.bing.com/", actor.CurrentPage.Address);
        Assert.Contains("controlled offline failure", actor.CurrentPage.Text);
        Assert.Null(actor.ReadEditorText()); Assert.Null(factory.Page!.Document);
        var scanout = new Scanout(); actor.Paint(scanout, new(0, 0, 500, 300), 0);
        Assert.True(scanout.Blends > 0);
        actor.Navigate("https://example.test/second");
        Assert.Equal(1, factory.NetworkCalls); network.Run();
        Assert.Equal(2, factory.NetworkCalls); Assert.Contains("/second", actor.CurrentPage.Address);
        actor.Release(); network.Run(); Assert.True(actor.IsRetired);
    }

    [Fact]
    public void PreparedGlyphPaintingQueriesNoUiFontsAndHonorsRoundedClipAndScale()
    {
        var worker = new Worker(); var canvases = new Canvases(worker);
        var page = new BrowserService();
        worker.TryQueue(() => { page.SetCanvas(canvases.CreateEditorPreparationCanvas()); page.Navigate("about:Start"); });
        worker.Run();
        BrowserPaintSnapshot? snapshot = null;
        worker.TryQueue(() => { page.ResizeViewport(796, 404); snapshot = page.CaptureEditorPaintSnapshot(); });
        worker.Run();
        Assert.NotNull(snapshot); Assert.True(snapshot.PreparedTextCount > 0);
        var scanout = new Scanout();
        snapshot.Paint(scanout, new(0, 0, 796, 404));
        Assert.True(scanout.Blends > 0);
        worker.TryQueue(page.Release); worker.Run();
    }

    [Fact]
    public void PreviewWheelDefaultRunsOnWorkerAndSurvivesTheNextSnapshotAndResize()
    {
        var worker = new Worker(); var factory = new Factory(worker);
        var service = new BrowserEditorPreparationService(factory, worker, new Canvases(worker));
        string source = string.Join("\n\n", Enumerable.Range(1, 60).Select(index => "Paragraph number " + index + "."));
        var operation = service.Begin(new(3, 1, source, "Long.md", 500, 300, Kind: EditorPreparationKind.Document));
        worker.Run(); Assert.True(operation.TryTakeResult(out var result)); Assert.Null(result.Error);
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        actor.Pointer(new("wheel", 40, 40, DeltaY: 120));
        Assert.Equal(0, actor.ScrollOffsetY); worker.Run(); Assert.Equal(120, actor.ScrollOffsetY);
        actor.Pointer(new("mousemove", 40, 40)); worker.Run(); Assert.Equal(120, actor.ScrollOffsetY);
        actor.ResizeViewport(600, 320); worker.Run(); Assert.Equal(120, actor.ScrollOffsetY);
        actor.Pointer(new("wheel", 40, 40, DeltaY: -10000)); worker.Run(); Assert.Equal(0, actor.ScrollOffsetY);
        actor.Cancel(); worker.Run(); Assert.True(actor.IsRetired);
    }

    [Fact]
    public void OldSnapshotAcknowledgementCannotPresentAReplacementDocument()
    {
        var worker = new Worker(); var factory = new Factory(worker);
        var service = new BrowserEditorPreparationService(factory, worker, new Canvases(worker));
        var operation = service.Begin(new(4, 1, "# Old", "Old.md", 500, 300, Kind: EditorPreparationKind.Document));
        worker.Run(); Assert.True(operation.TryTakeResult(out var result));
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        actor.Paint(new Scanout(), new(0, 0, 500, 300), 0);
        // The actual owner's navigation gets ahead of the UI's old-frame ack.
        worker.TryQueue(() => factory.Page!.BeginDocumentPreview("# Replacement", "New.md"));
        actor.FramePresented(); worker.Run();
        Assert.False(factory.Page!.DocumentFirstFramePresented);
        Assert.True(factory.Page.DocumentEnhancementsPending);
        Assert.Contains("Replacement", actor.CurrentPage.Text);
        actor.Paint(new Scanout(), new(0, 0, 500, 300), 0); actor.FramePresented(); worker.Run();
        Assert.True(factory.Page.DocumentFirstFramePresented);
        actor.Cancel(); worker.Run();
    }

    [Fact]
    public void StaticPagePresentationDoesNotStartASnapshotFeedbackLoop()
    {
        var worker = new Worker(); var service = new BrowserEditorPreparationService(new Factory(worker), worker, new Canvases(worker));
        var operation = service.Begin(new(5, 1, "", "about:Start", 500, 300, Kind: EditorPreparationKind.Navigation));
        worker.Run(); Assert.True(operation.TryTakeResult(out var result));
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        long sequence = actor.Revision;
        for (int index = 0; index < 20; index++)
        {
            actor.Paint(new Scanout(), new(0, 0, 500, 300), 0); actor.FramePresented();
            Assert.Equal(0, worker.Count); Assert.Equal(sequence, actor.Revision);
        }
        actor.Cancel(); worker.Run();
    }

    [Fact]
    public void UnchangedMouseMotionAndReadbackPreservePaintAndGlyphSnapshots()
    {
        var worker = new Worker(); var canvas = new Canvases(worker);
        var factory = new Factory(worker, html: "<body style='margin:0'><div style='width:450px;height:280px;cursor:text'>stationary page</div></body>");
        var service = new BrowserEditorPreparationService(factory, worker, canvas);
        var operation = service.Begin(new(6, 1, "", "http://pointer.test/", 500, 300, Kind: EditorPreparationKind.Navigation));
        worker.Run(); Assert.True(operation.TryTakeResult(out var result));
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page);
        actor.Pointer(new("mousemove", 20, 20)); worker.Run(); actor.AdvanceTime(0);
        Assert.True(actor.TextCursor);
        long paint = actor.Revision, layout = actor.Diagnostics.LayoutPassCount; int glyphs = canvas.Prepared;
        for (int index = 0; index < 1000; index++)
        { actor.Pointer(new("mousemove", 30 + index % 20, 20)); worker.Run(); Assert.False(actor.AdvanceTime(0)); }
        Assert.Equal(paint, actor.Revision); Assert.Equal(layout, actor.Diagnostics.LayoutPassCount); Assert.Equal(glyphs, canvas.Prepared);
        bool readback = false;
        Assert.True(actor.RequestEditorReadback((source, error) => { Assert.Null(source); Assert.Null(error); readback = true; }));
        worker.Run(); actor.AdvanceTime(0); Assert.True(readback); Assert.Equal(paint, actor.Revision);
        actor.Cancel(); worker.Run();
    }

    [Theory]
    [InlineData("<style>div:hover{background-color:#FF0000}</style><div style='width:400px;height:200px'>hover</div>")]
    [InlineData("<canvas id='c' width='400' height='200'></canvas><script>const c=document.getElementById('c');c.addEventListener('mousemove',()=>{c.getContext('2d').fillRect(0,0,10,10)});</script>")]
    public void ActualHoverAndScriptCanvasChangesStillPublishNewPaint(string html)
    {
        var worker = new Worker(); var factory = new Factory(worker, html: html);
        var service = new BrowserEditorPreparationService(factory, worker, new Canvases(worker));
        var operation = service.Begin(new(7, 1, "", "http://pointer.test/", 500, 300, Kind: EditorPreparationKind.Navigation));
        worker.Run(); Assert.True(operation.TryTakeResult(out var result)); Assert.Null(result.Error);
        var actor = Assert.IsType<BackgroundBrowserEditorSession>(result.Page); long before = actor.Revision;
        actor.Pointer(new("mousemove", 30, 30)); worker.Run();
        Assert.True(actor.Revision > before);
        actor.Cancel(); worker.Run();
    }

    private sealed class Worker : IEditorBackgroundWorkScheduler
    {
        private readonly Queue<Action> work = [];
        public bool Running; public int Count => work.Count;
        // The platform FIFO accepts a continuation from its current worker.
        public bool TryQueue(Action action) { work.Enqueue(action); return true; }
        public void Run()
        {
            int count = 0;
            while (work.TryDequeue(out var action))
            { Assert.True(++count < 20); Running = true; try { action(); } finally { Running = false; } }
        }
    }
    private sealed class Factory(Worker worker, bool failNetwork = false, string? html = null) : IInteractiveBrowserFactory, IWebTransport
    {
        public int Creates, NetworkCalls; public BrowserService? Page;
        public IInteractiveBrowserSession CreatePageSession()
        { Assert.True(worker.Running); Creates++; return Page = new(failNetwork || html is not null ? this : null); }
        public WebResponse Get(Uri address, BrowserLimits limits)
        { Assert.True(worker.Running); NetworkCalls++; if (html is not null) return new(address, 200, "text/html", html); throw new IOException("controlled offline failure"); }
    }
    private sealed class Canvases(Worker worker) : IEditorPreparationCanvasFactory
    {
        public int Creates, Prepared;
        public IDesktopCanvas CreateEditorPreparationCanvas()
        { Assert.True(worker.Running); Creates++; return new Metrics(this, worker); }
        private sealed class Metrics(Canvases owner, Worker worker) : IDesktopWebCanvas, IDesktopWebTextPreparationCanvas
        {
            public int Width => 796; public int Height => 404;
            public void Fill(uint rgb, int x, int y, int w, int h) => throw new InvalidOperationException();
            public void Line(uint rgb, int x1, int y1, int x2, int y2) => throw new InvalidOperationException();
            public void Text(string text, uint rgb, int x, int y) => throw new InvalidOperationException();
            public void Present() => throw new InvalidOperationException();
            public void PushClip(DesktopBounds bounds) => throw new InvalidOperationException();
            public void PopClip() => throw new InvalidOperationException();
            public double MeasureWebText(string text, double size, string family, int weight)
            { Assert.True(worker.Running); return new BrowserFallbackTextMetrics().Measure(text, new(size, family, weight)); }
            public double MeasureWebLineHeight(double size, string family, int weight)
            { Assert.True(worker.Running); return size * 1.2; }
            public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double size, string family, int weight) => throw new InvalidOperationException();
            public void WebText(string text, uint color, double x, double y, double size, string family, int weight, double opacity = 1) => throw new InvalidOperationException();
            public IDesktopPreparedWebText PrepareWebText(string text, double size, string family, int weight, double opacity)
            { Assert.True(worker.Running); owner.Prepared++; return new TextCoverage(size * 1.2); }
        }
    }
    private sealed class TextCoverage(double lineHeight) : IDesktopPreparedWebText
    {
        private readonly byte[] alpha = [255, 128, 64, 0];
        public int ScalePercent => 100; public double LineHeight => lineHeight;
        public void Paint(IDesktopCoverageCanvas target, uint rgb, double x, double y)
            => target.BlendCoverage(rgb, (int)Math.Round(x), (int)Math.Round(y), 2, 2, alpha);
    }
    private sealed class Scanout : IDesktopWebCanvas, IDesktopCoverageCanvas
    {
        public int Blends; public int Width => 1000; public int Height => 700;
        public void Fill(uint rgb, int x, int y, int w, int h) { }
        public void Line(uint rgb, int x1, int y1, int x2, int y2) { }
        public void Text(string text, uint rgb, int x, int y) => throw new InvalidOperationException("UI text fallback");
        public void Present() { } public void PushClip(DesktopBounds bounds) { } public void PopClip() { }
        public void BlendCoverage(uint rgb, int x, int y, int width, int height, ReadOnlySpan<byte> alpha) { Blends++; }
        public double MeasureWebText(string text, double size, string family, int weight) => throw new InvalidOperationException("UI measurement");
        public double MeasureWebLineHeight(double size, string family, int weight) => throw new InvalidOperationException("UI line metrics");
        public (int Width, int Height, byte[] Alpha) RasterizeWebText(string text, double size, string family, int weight) => throw new InvalidOperationException("UI rasterization");
        public void WebText(string text, uint rgb, double x, double y, double size, string family, int weight, double opacity = 1) => throw new InvalidOperationException("UI font painting");
    }
}