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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;
using AngleSharp.Html.Parser;

namespace XFESystem.Browser.Tests;

public sealed class BrowserAdjacentTests
{
    [Theory]
    [InlineData("beforebegin", "LmoveoriginalR", "original")]
    [InlineData("afterbegin", "LmoveoriginalR", "moveoriginal")]
    [InlineData("beforeend", "LoriginalmoveR", "originalmove")]
    [InlineData("afterend", "LoriginalmoveR", "original")]
    public void ElementAndTextSupportAllFourLocations(string position, string parentText, string targetText)
    {
        foreach (var method in new[] { "Element", "Text" })
        {
            var page = new Page();
            page.Run("var target=document.getElementById('Target'),moving=document.createElement('span');moving.textContent='move';document.body.append(moving);");
            page.Run(method == "Element" ?
                $"if(target.insertAdjacentElement('{position}',moving)!==moving)throw Error('return');" :
                $"if(target.insertAdjacentText('{position}','move')!==undefined)throw Error('return');");
            Assert.Equal(parentText, page.Document.GetElementById("Parent")!.TextContent);
            Assert.Equal(targetText, page.Document.GetElementById("Target")!.TextContent);
        }
    }

    [Fact]
    public void HtmlFourPositionsPreserveSiblingsWhitespaceAndWrappedIdentity()
    {
        var page = new Page();
        page.Run("""
            const target=document.getElementById('Target'), original=target.firstChild;
            target.insertAdjacentHTML('BEFOREBEGIN',' <span id="Before">before</span> ');
            target.insertAdjacentHTML('afterbegin','A<i id="First">first</i> ');
            target.insertAdjacentHTML('beforeend',' <b id="Last">last</b>Z');
            target.insertAdjacentHTML('afterend',' <span id="After">after</span> ');
            if(target!==document.getElementById('Target')||original!==target.childNodes.item(3))throw Error('identity');
            if(target.insertAdjacentHTML('beforeend','')!==undefined)throw Error('return');
            """);
        Assert.Equal("Afirst original lastZ", page.Document.GetElementById("Target")!.TextContent);
        Assert.Equal("L before Afirst original lastZ after R", page.Document.GetElementById("Parent")!.TextContent);
        Assert.Same(page.Document, page.Document.GetElementById("Before")!.Owner);
    }

    [Fact]
    public void FragmentContextHandlesTableSelectAndSvg()
    {
        var page = new Page();
        page.Run("""
            var table=document.createElement('table'),select=document.createElement('select'),svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
            document.body.append(table,select,svg);
            table.insertAdjacentHTML('beforeend','<tr><td>cell</td></tr>');
            select.insertAdjacentHTML('beforeend','<option value="x">choice</option>');
            svg.insertAdjacentHTML('afterbegin','<circle id="Circle" cx="4"/>');
            if(table.firstElementChild.tagName!=='TBODY'||table.querySelector('td').textContent!=='cell')throw Error('table');
            if(select.firstElementChild.tagName!=='OPTION'||svg.firstElementChild.namespaceURI!=='http://www.w3.org/2000/svg')throw Error('context:'+select.firstElementChild.tagName+':'+svg.namespaceURI+':'+svg.firstElementChild.namespaceURI);
            """);
        Assert.Same(page.Document, page.Document.GetElementById("Circle")!.Owner);
    }

    [Fact]
    public void ParsedScriptsStayInertAndOnlyNewLinksNotifyResourceOwner()
    {
        var resources = new List<IElement>(); var mutations = 0;
        var page = new Page(host: new() { ResourceRequested = resources.Add, Mutated = () => ++mutations });
        page.Run("""
            var changes=[];new MutationObserver(records=>changes.push(...records.map(record=>record.type+':'+record.addedNodes.length))).observe(document.body,{subtree:true,childList:true});
            document.getElementById('Target').insertAdjacentHTML('beforeend','<script src="Inert.js">globalThis.executed=true</script><link rel="stylesheet" href="New.css"><em>tail</em>');
            """);
        Assert.Equal(1, mutations); Assert.Single(resources); Assert.Equal("link", resources[0].LocalName);
        Assert.Equal("childList:3", page.Value("changes.join(',')"));
        Assert.Equal("undefined", page.Value("typeof executed"));
    }

    [Fact]
    public void ElementMovesKeepIdentityAndReturnInsertedElementAndTextStaysLiteral()
    {
        var page = new Page();
        page.Run("""
            var target=document.getElementById('Target'),child=document.createElement('strong');child.textContent='move';document.body.append(child);
            if(target.insertAdjacentElement('afterbegin',child)!==child||target.firstChild!==child)throw Error('identity');
            if(target.insertAdjacentText('beforeend','<b>literal</b>')!==undefined)throw Error('text return');
            target.insertAdjacentElement('afterend',child);
            if(target.nextSibling!==child||child.ownerDocument!==document)throw Error('move');
            """);
        Assert.Equal("original<b>literal</b>", page.Document.GetElementById("Target")!.TextContent);
        Assert.Null(page.Document.GetElementById("Target")!.QuerySelector("b"));
    }

    [Fact]
    public void InvalidPositionsAndDetachedParentBehaviorAreDistinctAndCatchable()
    {
        var page = new Page();
        Assert.Equal("SyntaxError:NoModificationAllowedError:NoModificationAllowedError:TypeError:true", page.Value("""
            var names=[],detached=document.createElement('div');
            try{detached.insertAdjacentHTML(' beforeend','x')}catch(error){names.push(error.name)}
            try{detached.insertAdjacentHTML('beforebegin','x')}catch(error){names.push(error.name)}
            try{document.documentElement.insertAdjacentHTML('afterend','x')}catch(error){names.push(error.name)}
            try{detached.insertAdjacentElement('afterbegin',document.createTextNode('x'))}catch(error){names.push(error.name)}
            names.push(detached.insertAdjacentElement('beforebegin',document.createElement('span'))===null&&detached.insertAdjacentText('afterend','x')===undefined&&detached.childNodes.length===0);
            names.join(':')
            """));
    }

    [Fact]
    public void CompleteFragmentNodeBudgetFailureDoesNotMutateOrQueueResources()
    {
        var mutations = 0; var resources = 0;
        var page = new Page(new() { MaximumDomNodes = 18 }, new() { Mutated = () => ++mutations, ResourceRequested = _ => ++resources });
        page.Run("var target=document.getElementById('Target'),original=target.firstChild;");
        Assert.False(page.Runtime.Evaluate("target.insertAdjacentHTML('afterbegin','<link href=\"Blocked.css\">'+'<i>x</i>'.repeat(20))").Success);
        Assert.Equal(0, mutations); Assert.Equal(0, resources);
        Assert.Equal("true:original", page.Value("(original===target.firstChild)+':'+target.textContent"));
        page.Run("target.insertAdjacentText('beforeend',' ok');");
        Assert.Equal("original ok", page.Document.GetElementById("Target")!.TextContent);
    }

    [Fact]
    public void CharacterAndActualInsertionDepthAreCheckedBeforeMutation()
    {
        var page = new Page(new() { MaximumHtmlCharacters = 100, MaximumDomDepth = 6 });
        Assert.False(page.Runtime.Evaluate("document.getElementById('Target').insertAdjacentHTML('beforeend','x'.repeat(90))").Success);
        Assert.False(page.Runtime.Evaluate("document.getElementById('Target').insertAdjacentHTML('afterbegin','<a><b><c>deep</c></b></a>')").Success);
        Assert.Equal("original", page.Document.GetElementById("Target")!.TextContent);
        page.Run("document.getElementById('Target').insertAdjacentHTML('beforeend','<i>ok</i>');");
        Assert.Equal("originalok", page.Document.GetElementById("Target")!.TextContent);
    }

    [Fact]
    public void ElementDepthBudgetFailureRestoresItsOldParentAndIdentity()
    {
        var page = new Page(new() { MaximumDomDepth = 6 });
        page.Run("var old=document.createElement('aside'),child=document.createElement('b');child.textContent='old';old.append(child);document.body.append(old);var target=document.getElementById('Target');");
        Assert.False(page.Runtime.Evaluate("target.insertAdjacentElement('beforeend',old)").Success);
        Assert.Equal("true:true:old:original", page.Value("(old.parentNode===document.body)+':'+(child.parentNode===old)+':'+old.textContent+':'+target.textContent"));
    }

    private sealed class Page
    {
        public Page(BrowserLimits? limits = null, BrowserDomHost? host = null)
        {
            limits ??= new(); Runtime = new(limits);
            Document = new HtmlParser().ParseDocument("<!doctype html><html><head></head><body><div id='Parent'>L<div id='Target'>original</div>R</div></body></html>");
            _ = new DomBridge(Runtime, Document, new("https://example.test/Page"), limits, host);
        }
        public JavaScriptRuntime Runtime { get; }
        public IDocument Document { get; }
        public void Run(string source) { var result = Runtime.Evaluate(source); Assert.True(result.Success, result.Error); }
        public string Value(string source) { var result = Runtime.Evaluate(source); Assert.True(result.Success, result.Error); return result.Value; }
    }
}