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

XFESystem

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

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

namespace XFESystem.Browser.Tests;

public sealed class BrowserPipelineTests
{
    [Fact]
    public void OrdinaryScriptErrorDoesNotSuppressFollowingIndependentScripts()
    {
        var source = new Resources();source.Add("/","text/html","<p id='P'>before</p><script>throw new Error('statement budget is only text');</script><script>document.getElementById('P').textContent='after'</script>");
        var page = new BrowserService(source).Navigate("http://test/");
        Assert.Contains("after",page.Text,StringComparison.Ordinal);Assert.Contains("JavaScript error:",page.Text,StringComparison.Ordinal);
    }

    [Fact]
    public void BudgetFailureStopsAutomaticDeferredScriptsUntilNextNavigation()
    {
        var source = new Resources();source.Add("/","text/html","<p id='P'>before</p><script>while(true){}</script><script defer src='/After.js'></script>");
        source.Add("/After.js","application/javascript","document.getElementById('P').textContent='after'");
        var page = new BrowserService(source,new BrowserLimits{MaximumStatements=100}).Navigate("http://test/");
        Assert.Contains("before",page.Text,StringComparison.Ordinal);Assert.DoesNotContain("after",page.Text,StringComparison.Ordinal);Assert.Single(source.Requests);
    }
    [Fact]
    public void StylesExternalClassicDeferDynamicScriptsAndLifecycleUseOneRealDocument()
    {
        var source = new Resources();
        source.Add("/Index", "text/html", """
            <title>Pipeline</title><link rel="stylesheet" href="Theme.css"><div id="Box">before</div>
            <script>globalThis.order=[];document.addEventListener('DOMContentLoaded',()=>order.push('dom'));window.addEventListener('load',()=>order.push('load'));</script>
            <script src="First.js"></script><script defer src="Defer.js"></script><script>order.push('inline');</script>
            """);
        source.Add("/First.js", "application/javascript", "order.push('first');document.querySelector('#Box').textContent='external worked';const s=document.createElement('script');s.src='Dynamic.js';s.onload=()=>order.push('dynamic-load');document.head.appendChild(s);");
        source.Add("/Dynamic.js", "text/javascript", "order.push('dynamic');");
        source.Add("/Defer.js", "application/javascript", "order.push('defer');");
        source.Add("/Theme.css", "text/css", "#Box{width:200px;height:50px;color:#123456;background:#aabbcc;margin:0}");
        var browser = new BrowserService(source);
        var page = browser.Navigate("http://test/Index");
        Assert.Contains("external worked", page.Text, StringComparison.Ordinal);
        Assert.Equal("first,dynamic,dynamic-load,inline,defer,dom,load", browser.ExecuteJavaScript("order.join(',')"));
        var layout = browser.Layout(new(600, 400));
        var box = layout.Boxes.Single(b => b.Element?.Id == "Box" && b.Node is IElement);
        Assert.Equal(200, box.ContentBox.Width); Assert.Equal(50, box.ContentBox.Height);
        Assert.Contains(layout.Commands, c => c.Kind == BrowserDrawKind.Fill && c.Color == 0xAABBCC);
        Assert.Equal(4, browser.LoadedResources.Count);
    }

    [Fact]
    public void PointerBubblesToParentAndKeyboardEditsFocusedTextareaWithInputEvents()
    {
        var resources = new Resources(); resources.Add("/", "text/html", """
            <style>body{margin:0}textarea{display:block;width:200px;height:40px;margin:0;padding:0;border:0}</style>
            <div id="Parent"><textarea id="Entry"></textarea></div><p id="Output"></p><script>
            globalThis.events=[];Parent.addEventListener('click',()=>events.push('parent'));
            Entry.addEventListener('input',()=>Output.textContent=Entry.value);
            </script>
            """.Replace("Parent.addEventListener", "document.getElementById('Parent').addEventListener").Replace("Entry.addEventListener", "document.getElementById('Entry').addEventListener").Replace("Output.textContent=Entry.value", "document.getElementById('Output').textContent=document.getElementById('Entry').value"));
        var browser = new BrowserService(resources); browser.Navigate("http://test/"); browser.Layout(new(600,400));
        browser.Pointer(new("mousedown", 10, 10, Buttons:1)); browser.Pointer(new("mouseup",10,10));
        browser.Key(new("keydown", "a", "KeyA", "a")); browser.Key(new("keyup", "a", "KeyA"));
        Assert.Equal("a", browser.ExecuteJavaScript("document.getElementById('Entry').value"));
        Assert.Equal("parent", browser.ExecuteJavaScript("events.join(',')"));
        Assert.Contains("a", browser.RefreshDocument().Text, StringComparison.Ordinal);
        browser.Key(new("keydown","Backspace"));
        Assert.Equal("", browser.ExecuteJavaScript("document.getElementById('Entry').value"));
    }

    [Fact]
    public void PreventDefaultStopsTypingAndWheelDefaultAction()
    {
        var resources = new Resources(); resources.Add("/", "text/html", "<textarea style='width:200px;height:50px' id='T'></textarea><script>document.getElementById('T').addEventListener('keydown',e=>e.preventDefault());window.addEventListener('wheel',e=>e.preventDefault());document.getElementById('T').focus()</script>");
        var browser = new BrowserService(resources); browser.Navigate("http://test/");
        browser.Key(new("keydown", "a", "KeyA", "a"));
        Assert.Equal("",browser.ExecuteJavaScript("document.getElementById('T').value"));
        Assert.True(browser.Pointer(new("wheel",10,10,DeltaY:60)));
    }

    [Fact]
    public void TimersAndRafRequireExplicitMonotonicPump()
    {
        var resources = new Resources(); resources.Add("/", "text/html", "<p id='P'>before</p><script>setTimeout(()=>document.getElementById('P').textContent='timer',20);requestAnimationFrame(t=>document.title='frame '+t);</script>");
        var browser = new BrowserService(resources); browser.Navigate("http://test/");
        Assert.DoesNotContain("timer", browser.CurrentPage.Text, StringComparison.Ordinal);
        browser.AdvanceTime(20);
        Assert.Contains("timer", browser.CurrentPage.Text, StringComparison.Ordinal);
        Assert.StartsWith("frame ", browser.CurrentPage.Title, StringComparison.Ordinal);
    }

    [Theory]
    [InlineData("http://test/Unsafe.js",true,"HTTP subresource")]
    [InlineData("https://test/Unsafe.js",false,"certificate validation")]
    public void HttpsSubresourcesCannotBypassMixedContentOrCertificateChecks(string script, bool validated,string error)
    {
        var source = new Resources(); source.Add("/", "text/html", "<script src='"+script+"'></script>"); source.Add("/Unsafe.js", "application/javascript", "document.title='unsafe'",validated);
        var browser = new BrowserService(source); var page = browser.Navigate("https://test/");
        Assert.Contains(error, page.Text,StringComparison.Ordinal); Assert.NotEqual("unsafe",page.Title);
    }

    [Fact]
    public void DeclaredCspBlocksExternalStyleAndScriptRequestsAndInlineStyles()
    {
        var source = new Resources(); source.Add("/", "text/html", "<meta http-equiv='Content-Security-Policy' content=\"default-src 'none'\"><link rel='stylesheet' href='/X.css'><div id='X' style='width:200px;background:#ff0000'>safe</div><script src='/X.js'></script>");
        var browser = new BrowserService(source); browser.Navigate("http://test/");
        var list = browser.Layout(new(600,400));
        Assert.Single(source.Requests); Assert.Empty(browser.LoadedResources);
        Assert.DoesNotContain(list.Commands,c=>c.Color==0xFF0000);
    }

    [Fact]
    public void PageSessionsDoNotShareGlobalsOrDom()
    {
        var browser = new BrowserService(); var first=(BrowserService)browser.CreatePageSession();var second=(BrowserService)browser.CreatePageSession();
        first.Navigate("about:Start");second.Navigate("about:Start");first.ExecuteJavaScript("globalThis.privateValue=123;document.title='first'");
        Assert.Equal("undefined",second.ExecuteJavaScript("typeof privateValue"));Assert.NotEqual(first.RefreshDocument().Title,second.RefreshDocument().Title);
    }

    [Fact]
    public void PackagedMonacoCoreIsTheVerifiedTransformationWithoutHostPathAccess()
    {
        var resources = new PackagedBrowserResources();
        Assert.True(resources.TryGet(new(PackagedBrowserResources.MonacoCoreRoot+"EditorCore.js"),out var response));
        var bytes = System.Text.Encoding.UTF8.GetBytes(response.Body);
        Assert.Equal(2_293_601, bytes.Length);
        Assert.Equal("828c22be163ab40c177106ef4d4a0ff86ae3611bafc9dd82ccef678e08971952",
            Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(bytes)));
        Assert.Equal("application/javascript",response.ContentType);
        Assert.False(resources.TryGet(new(PackagedBrowserResources.Origin+"/Browser/Monaco/0.52.2/min/vs/editor/editor.main.js"),out _));
        Assert.False(resources.TryGet(new("file:///C:/Secret"),out _));
    }

    private sealed class Resources : IWebTransport
    {
        private readonly Dictionary<string,(string Mime,string Body,bool Validated)> values=[];
        public List<string> Requests {get;}=[];
        public void Add(string path,string mime,string body,bool validated=true)=>values[path]=(mime,body,validated);
        public WebResponse Get(Uri address,BrowserLimits limits)
        { Requests.Add(address.AbsoluteUri);var value=values[address.AbsolutePath];return new(address,200,value.Mime,value.Body,value.Validated); }
    }
}