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

XFESystem

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text;
using AngleSharp.Dom;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace XFESystem.Browser.Tests;

public sealed class BrowserRemoteContentTests
{
    private const string Policy = "script-src https: 'strict-dynamic' 'nonce-YWJj'; base-uri 'self'";
    [Theory]
    [InlineData("location.assign('/next?q=value')")]
    [InlineData("location.replace('/next?q=value')")]
    [InlineData("location.href='/next?q=value'")]
    [InlineData("window.location='/next?q=value'")]
    [InlineData("document.location='/next?q=value'")]
    public void LocationNavigationFetchesTheActualDestinationAfterTheScriptReturns(string script)
    {
        var transport = new Fixture("<p>actual response</p>"); var browser = new BrowserService(transport); browser.Navigate("https://page.test/");
        browser.ExecuteJavaScript(script); browser.AdvanceTime(10);
        Assert.Equal("https://page.test/next?q=value", browser.CurrentPage.Address);
        Assert.Equal(new Uri(browser.CurrentPage.Address), transport.Requests.Last());
    }
    [Fact]
    public void DomCollectionsHaveTheirActualInterfacesAndStaticVersusLiveMembership()
    {
        var browser = new BrowserService(new Fixture("<p id='A' name='Named'>one</p><p id='B'>two</p>")); browser.Navigate("https://page.test/");
        Assert.Equal("true:true:true:true:false:true", browser.ExecuteJavaScript("const snapshot=document.querySelectorAll('p'),live=document.getElementsByTagName('p');[snapshot instanceof NodeList,live instanceof HTMLCollection,document.body.childNodes instanceof NodeList,document.body.children instanceof HTMLCollection,Array.isArray(snapshot),Array.isArray(navigator.languages)].join(':')"));
        Assert.Equal("B:A:A:true", browser.ExecuteJavaScript("[snapshot[1].id,live.A.id,live.namedItem('Named').id,snapshot.item(-1)===null].join(':')"));
        browser.ExecuteJavaScript("document.body.appendChild(document.createElement('p'))");
        string actual = browser.ExecuteJavaScript("snapshot.length+':'+live.length+':'+Array.from(snapshot.values()).map(e=>e.id).join(',')+':'+Object.prototype.toString.call(snapshot)");
        Assert.False(actual.StartsWith("JavaScript error:"), actual); Assert.Equal("2:3:A,B:[object NodeList]", actual);
        Assert.Equal("0,1", browser.ExecuteJavaScript("Object.keys(snapshot).join(',')"));
        Assert.Equal("A,B", browser.ExecuteJavaScript("Array.prototype.slice.call(snapshot).map(node=>node.id).join(',')"));
        Assert.Equal("true", browser.ExecuteJavaScript("(()=>{'use strict';try{snapshot[0]=null;return false}catch(e){return e instanceof TypeError}})()"));
    }
    [Fact]
    public void StaticNodeListLengthDoesNotProjectEveryMatchingNodeIntoJavaScript()
    {
        var html = "<body>" + string.Concat(Enumerable.Range(0, 500).Select(index => "<p id='P" + index + "'>item</p>")) + "</body>";
        var browser = new BrowserService(new Fixture(html), new BrowserLimits { MaximumScriptMemoryBytes = 8_000_000 });
        browser.Navigate("https://page.test/");
        Assert.Equal("500", browser.ExecuteJavaScript("globalThis.snapshot=document.querySelectorAll('p');snapshot.length"));
        browser.Document!.GetElementById("P499")!.Remove();
        Assert.Equal("500:P499:false", browser.ExecuteJavaScript("snapshot.length+':'+snapshot.item(499).id+':'+snapshot.item(499).isConnected"));
    }
    [Fact]
    public void IteratingALiveDescendantCollectionStaysWithinTheScriptAllocationBudget()
    {
        var html = "<body>" + string.Concat(Enumerable.Range(0, 500).Select(index => "<section><span id='P" + index + "'>item</span></section>")) + "</body>";
        var browser = new BrowserService(new Fixture(html), new BrowserLimits { MaximumScriptMemoryBytes = 128_000_000 });
        browser.Navigate("https://page.test/");
        Assert.Equal("500:P499", browser.ExecuteJavaScript("const elements=document.getElementsByTagName('span');let count=0,last='';for(let i=0;i<elements.length;i++){last=elements[i].id;count++;}count+':'+last"));
        Assert.Equal("501:added", browser.ExecuteJavaScript("const added=document.createElement('span');added.id='added';document.querySelector('section').appendChild(added);elements.length+':'+elements[1].id"));
    }
    [Fact]
    public void ScriptCanWrapAFormSubmitOperationAndStillSubmitTheActualForm()
    {
        var transport = new Fixture("<form id='F' action='/search'><input name='q' value='wrapped query'></form>");
        var browser = new BrowserService(transport); browser.Navigate("https://page.test/");
        Assert.Equal("1", browser.ExecuteJavaScript("'use strict';const form=document.getElementById('F'),nativeSubmit=form.submit;let calls=0;form.submit=function(){calls++;nativeSubmit.call(this)};form.submit();calls"));
        browser.AdvanceTime(10); Assert.Contains("q=wrapped+query", transport.Requests.Last().Query);
    }
    [Fact]
    public void HyperlinkUrlComponentsResolveAndReflectChangesWithoutUndefinedFields()
    {
        var browser = new BrowserService(new Fixture("<a id='A' href='../path?q=one#first'>link</a>")); browser.Navigate("https://page.test/folder/index.html");
        Assert.Equal("https:|page.test|page.test||/path|?q=one|#first|https://page.test", browser.ExecuteJavaScript("const a=document.getElementById('A');[a.protocol,a.host,a.hostname,a.port,a.pathname,a.search,a.hash,a.origin].join('|')"));
        Assert.Equal("http://other.test/changed?q=two#last", browser.ExecuteJavaScript("a.protocol='http:';a.host='other.test:8080';a.pathname='/changed';a.search='?q=two';a.hash='#last';a.port='';a.toString()"));
        Assert.Equal("https://example.test|u|p", browser.ExecuteJavaScript("a.href='https://u:p@example.test/x';[a.origin,a.username,a.password].join('|')"));
    }
    [Theory]
    [InlineData("screen and (min-width:600px) and (max-width:900px)", true)]
    [InlineData("(min-width:900px), (orientation:landscape)", true)]
    [InlineData("only screen and (min-height:700px)", false)]
    [InlineData("not screen and (min-width:900px)", true)]
    [InlineData("print and (min-width:1px)", false)]
    [InlineData("screen and (unknown:yes)", false)]
    [InlineData("(min-resolution:96dpi)", true)]
    [InlineData("(prefers-reduced-motion:reduce)", true)]
    [InlineData("(prefers-reduced-motion:no-preference)", false)]
    [InlineData("(prefers-color-scheme:light)", true)]
    [InlineData("(width:800px) and (height:600px)", true)]
    [InlineData("(min-aspect-ratio:4/3)", true)]
    [InlineData("(min-width:600px) (max-width:900px)", false)]
    public void MediaQueriesEvaluateConjunctionListsAndScreenPreferences(string query, bool expected)
        => Assert.Equal(expected, BrowserMediaQuery.Matches(query, new(800, 600)));
    [Fact]
    public void CssAndMediaQueryListsRespondToRealViewportResizeAndNotifyChanges()
    {
        var browser = new BrowserService(new Fixture("<style>#P{color:blue}@media screen and (min-width:600px) and (max-height:700px){#P{color:red}}</style><p id='P'>text</p>"));
        browser.ResizeViewport(800, 600); browser.Navigate("https://page.test/");
        Assert.Equal("true", browser.ExecuteJavaScript("const m=matchMedia('screen and (min-width:600px) and (max-height:700px)');let changes=0;m.addListener(e=>{if(e.target===m&&!e.matches)changes++});m.matches"));
        Assert.Equal(0xff0000u, browser.Layout(new(800, 600)).GetComputedStyle(browser.Document!.GetElementById("P")!)!.Color.Rgb);
        browser.ResizeViewport(400, 600);
        Assert.Equal("false:1", browser.ExecuteJavaScript("m.matches+':'+changes"));
        Assert.Equal(0x0000ffu, browser.Layout(new(400, 600)).GetComputedStyle(browser.Document.GetElementById("P")!)!.Color.Rgb);
        browser.ResizeViewport(350, 600); Assert.Equal("1", browser.ExecuteJavaScript("changes"));
    }
    [Fact]
    public void ReassigningTheSameCssDeclarationPreservesCssTextAndCachedGeometry()
    {
        var browser = new BrowserService(new Fixture("<p id='P' style='color:red;width:100px'>text</p>")); browser.Navigate("https://page.test/");
        browser.Layout(new(300, 200)); long layouts = browser.LayoutPassCount;
        Assert.Equal("0:color:red;width:100px", browser.ExecuteJavaScript("let records=0;const p=document.getElementById('P');new MutationObserver(r=>records+=r.length).observe(p,{attributes:true});for(let i=0;i<100;i++)p.style.color='red';records+':'+p.style.cssText"));
        browser.Layout(new(300, 200)); Assert.Equal(layouts, browser.LayoutPassCount);
        browser.ExecuteJavaScript("p.style.setProperty('color','red','important')"); browser.Layout(new(300, 200));
        Assert.True(browser.LayoutPassCount > layouts); Assert.Contains("!important", browser.Document!.GetElementById("P")!.GetAttribute("style"));
    }
    [Fact]
    public void RightFloatShrinksToItsContentAndStaysBesideAnExistingInlineForm()
    {
        var document = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<header id='Header' style='width:400px;padding-top:22px'><form id='Form' style='display:inline-block;width:200px;height:48px'></form><div id='Login' style='float:right;position:relative;right:32px;line-height:50px'><span>abc</span></div><nav id='Nav' style='height:39px;margin-top:11px'></nav></header>");
        var layout = new BrowserLayoutEngine().Layout(document, new(800, 500));
        var header = layout.GetBoundingClientRect(document.GetElementById("Header")!);
        var form = layout.GetBoundingClientRect(document.GetElementById("Form")!);
        var login = layout.GetBoundingClientRect(document.GetElementById("Login")!);
        Assert.Equal(form.Y, login.Y); Assert.InRange(login.Width, 20, 40);
        Assert.Equal(header.Right - 32, login.Right); Assert.Equal(120, header.Height);
    }
    [Fact]
    public void LeftFloatExcludesInlineTextAndClearMovesFollowingBlockBelowIt()
    {
        var document = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<div id='Parent' style='width:300px;display:flow-root'><div id='Float' style='float:left;width:80px;height:40px'></div><span id='Text'>text</span><div id='Clear' style='clear:both;height:10px'>after</div></div>");
        var layout = new BrowserLayoutEngine().Layout(document, new(800, 500));
        var floating = layout.GetBoundingClientRect(document.GetElementById("Float")!);
        Assert.Equal(floating.Right, layout.GetBoundingClientRect(document.GetElementById("Text")!).X);
        Assert.True(layout.GetBoundingClientRect(document.GetElementById("Clear")!).Y >= floating.Bottom);
    }
    [Fact]
    public void PositionedRectClipAppliesToPaintingAndHitTestingIncludingDeferredDescendants()
    {
        var document = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<div id='Invisible' style='position:absolute;width:100px;height:40px;clip:rect(1px,1px,1px,1px)'>hidden<a style='position:absolute;left:0;top:0'>link</a></div><div id='Partial' style='position:absolute;top:60px;width:100px;height:40px;clip:rect(0px 40px 20px 0px)'><a id='Inside' style='position:absolute;left:0;top:0;width:30px;height:10px'>in</a><a id='Outside' style='position:absolute;left:60px;top:0;width:30px;height:10px'>out</a></div>");
        var layout = new BrowserLayoutEngine().Layout(document, new(400, 200));
        Assert.Contains(layout.Commands, command => command.Kind == BrowserDrawKind.PushClip && command.Bounds.Width == 0 && command.Bounds.Height == 0);
        Assert.NotEqual("Invisible", layout.HitTest(10, 10)?.Element?.Id);
        Assert.Equal("Inside", layout.HitTest(10, 65)?.Element?.Id);
        Assert.NotEqual("Outside", layout.HitTest(70, 65)?.Element?.Id);
    }
    [Fact]
    public void InnerTextUpdatesActualHtmlNodeAndItsPaintedText()
    {
        var browser = new BrowserService(new Fixture("<span id='Text'></span><script>document.getElementById('Text').innerText='actual hint'</script>"));
        browser.Navigate("https://page.test/");
        Assert.Equal("actual hint", browser.Document!.GetElementById("Text")!.TextContent);
        Assert.Contains(browser.Layout(new(400, 200)).Commands, command => command.Text.Contains("actual"));
        Assert.Equal("false:first\\nsecond", browser.ExecuteJavaScript("const text=document.getElementById('Text'),old=text.firstChild;text.innerText='first\\nsecond';old.isConnected+':'+text.innerText").Replace("\n", "\\n"));
        browser.ExecuteJavaScript("text.innerText='\\u00a0'.repeat(256)");
        Assert.True(browser.Layout(new(400, 200)).GetBoundingClientRect(browser.Document.GetElementById("Text")!).Width > 100);
    }
    [Fact]
    public void PercentageSvgInsideShrinkToFitControlUsesItsIntrinsicIconSize()
    {
        var document = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<div style='position:relative;width:500px'><a id='Control' style='position:absolute;right:0;display:flex;align-items:center;padding:4px 6px'><svg id='Icon' style='width:100%;height:100%' viewBox='0 0 16 16'></svg><span>abc</span></a></div>");
        var layout = new BrowserLayoutEngine().Layout(document, new(800, 500));
        Assert.InRange(layout.GetBoundingClientRect(document.GetElementById("Control")!).Width, 40, 80);
        var icon = layout.GetBoundingClientRect(document.GetElementById("Icon")!);
        Assert.Equal(16, icon.Width); Assert.Equal(16, icon.Height);
    }
    [Fact]
    public void FlexRowsWrapBeforeShrinkingAndGeneratedAttributeTextStillInvalidates()
    {
        var doc = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<div id='Rows' style='display:flex;flex-wrap:wrap;width:300px;row-gap:5px'>" + string.Concat(Enumerable.Range(0, 6).Select(i => "<div id='I" + i + "' style='width:100px;height:20px'>item</div>")) + "</div>");
        var layout = new BrowserLayoutEngine().Layout(doc, new(500, 300));
        var first = layout.GetBoundingClientRect(doc.GetElementById("I0")!); var fourth = layout.GetBoundingClientRect(doc.GetElementById("I3")!);
        Assert.Equal(100, first.Width); Assert.Equal(first.X, fourth.X); Assert.Equal(first.Bottom + 5, fourth.Y);
        Assert.Equal(45, layout.GetBoundingClientRect(doc.GetElementById("Rows")!).Height);
        var browser = new BrowserService(new Fixture("<style>#P:before{content:attr(data-label)}</style><p id='P' data-label='before'></p>")); browser.Navigate("https://page.test/");
        browser.Layout(new(300, 300)); browser.ExecuteJavaScript("document.getElementById('P').setAttribute('data-label','after')");
        Assert.Contains(browser.Layout(new(300, 300)).Commands, command => command.Text == "after");
    }
    [Fact]
    public void AttributeBudgetUsesDeltasAndStillRejectsCombinedGrowthAndReflectedFormValues()
    {
        var limits = new BrowserLimits { MaximumHtmlCharacters = 200 };
        using var runtime = new JavaScriptRuntime(limits);
        var document = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<p id='P'>text</p><input id='Q'>");
        var dom = new DomBridge(runtime, document, new("https://page.test/"), limits);
        Assert.True(runtime.Evaluate("const p=document.getElementById('P');for(let i=0;i<100;i++)p.setAttribute('data-track',String(i))").Success);
        Assert.InRange(dom.BudgetValidationCount, 1, 2);
        Assert.True(runtime.Evaluate("p.removeAttribute('data-track');const q=document.getElementById('Q');q.setAttribute('value','x'.repeat(90))").Success);
        var result = runtime.Evaluate("q.setAttribute('value','y'.repeat(120))");
        Assert.False(result.Success); Assert.True(result.BudgetExceeded);
        Assert.Equal("90:90", runtime.Evaluate("[q.value.length,q.getAttribute('value').length].join(':')").Value);
        dom.ReleaseRetainedState();
    }
    [Fact]
    public void TrackingAttributesKeepGeometryCacheButSelectorAttributesAndObserversStillUpdate()
    {
        var browser = new BrowserService(new Fixture("<style>[data-state=red]{color:red}</style><p id='P'>value</p>")); browser.Navigate("https://page.test/");
        browser.Layout(new(300, 300)); long before = browser.LayoutPassCount;
        browser.ExecuteJavaScript("globalThis.Records=0;const p=document.getElementById('P');new MutationObserver(r=>Records+=r.length).observe(p,{attributes:true});p.setAttribute('data-track','one');p.setAttribute('data-track','one')");
        browser.Layout(new(300, 300)); Assert.Equal(before, browser.LayoutPassCount); Assert.Equal("2", browser.ExecuteJavaScript("Records"));
        browser.ExecuteJavaScript("p.setAttribute('data-state','red')");
        var layout = browser.Layout(new(300, 300)); Assert.True(browser.LayoutPassCount > before);
        Assert.Equal(0xff0000u, layout.GetComputedStyle(browser.Document!.GetElementById("P")!)!.Color.Rgb);
    }
    [Fact]
    public void GeometryReadDuringTimerDoesNotDownloadAndDecodeDisplayImagesInsideAuthorCallback()
    {
        var transport = new Fixture("<div id='P'>geometry</div><img src='/Actual.svg'><script>setTimeout(()=>document.getElementById('P').getBoundingClientRect(),0)</script>");
        transport.Images["/Actual.svg"] = Encoding.UTF8.GetBytes("<svg width='4' height='4'><rect width='4' height='4' fill='red'/></svg>");
        var browser = new BrowserService(transport); browser.Navigate("https://page.test/"); browser.AdvanceTime(0);
        Assert.Single(transport.Requests);
        browser.Layout(new(300, 300)); Assert.Equal(2, transport.Requests.Count);
        var image = browser.Document!.QuerySelector("img")!;
        var rect = browser.Layout(new(300, 300)).GetBoundingClientRect(image);
        Assert.Equal(4, rect.Width); Assert.Equal(4, rect.Height);
    }
    [Fact]
    public void SingularScaleTransformsDoNotPaintOrReceivePointerInput()
    {
        var doc = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<div id='Hidden' style='position:absolute;transform:scale(0);width:100px;height:100px'>hidden content</div><div id='Shown' style='position:absolute;width:100px;height:100px'>visible</div>");
        var layout = new BrowserLayoutEngine().Layout(doc, new(300, 300));
        Assert.DoesNotContain(layout.Commands, command => command.Text.Contains("hidden"));
        Assert.Equal("Shown", layout.HitTest(20, 20)?.Element?.Id);
    }
    [Fact]
    public void ScriptFormSubmitBypassesCanceledSubmitHandlerAndUsesCurrentValue()
    {
        var transport = new Fixture("<form id='F' action='/search'><input id='Q' name='q' value='real query'></form>");
        var browser = new BrowserService(transport); browser.Navigate("https://page.test/");
        browser.ExecuteJavaScript("document.getElementById('F').addEventListener('submit',e=>e.preventDefault());document.getElementById('F').requestSubmit()");
        browser.AdvanceTime(0); Assert.Single(transport.Requests);
        browser.ExecuteJavaScript("document.getElementById('F').submit()"); browser.AdvanceTime(10);
        Assert.Equal("/search", transport.Requests.Last().AbsolutePath); Assert.Contains("q=real+query", transport.Requests.Last().Query);
    }
    [Fact]
    public void PositionedDescendantsShareTheNearestStackingContextAndRetainAncestorClips()
    {
        var parser = new AngleSharp.Html.Parser.HtmlParser();
        string high = "<div id='High' style='position:absolute;z-index:99;left:0;top:0;width:50px;height:50px'></div>";
        string low = "<div id='Low' style='position:absolute;z-index:2;left:0;top:0;width:50px;height:50px'></div>";
        var layout = new BrowserLayoutEngine().Layout(parser.ParseDocument("<div style='position:relative'>" + high + "</div>" + low), new(200, 200));
        Assert.Equal("High", layout.HitTest(20, 20)?.Element?.Id);
        layout = new BrowserLayoutEngine().Layout(parser.ParseDocument("<div style='position:relative;z-index:1'>" + high + "</div>" + low), new(200, 200));
        Assert.Equal("Low", layout.HitTest(20, 20)?.Element?.Id);
        layout = new BrowserLayoutEngine().Layout(parser.ParseDocument("<div style='position:relative;overflow:hidden;width:10px;height:10px'>" + high + "</div>" + low), new(200, 200));
        Assert.Equal("High", layout.HitTest(12, 12)?.Element?.Id); Assert.Equal("Low", layout.HitTest(20, 20)?.Element?.Id);
    }
    [Fact]
    public void AttributeNodesAndDocumentSelectionExposeActualDocumentValues()
    {
        var browser = new BrowserService(new Fixture("<p id='P' dir='rtl'>actual text</p>")); browser.Navigate("https://page.test/");
        Assert.Equal("rtl:actual text:true:1", browser.ExecuteJavaScript("const p=document.getElementById('P');getSelection().selectAllChildren(p);[p.getAttributeNode('dir').value,getSelection().toString(),document.getSelection()===getSelection(),getSelection().rangeCount].join(':')"));
        Assert.Equal("ltr:0", browser.ExecuteJavaScript("p.getAttributeNode('dir').value='ltr';getSelection().removeAllRanges();p.dir+':'+getSelection().rangeCount"));
    }
    [Fact]
    public void LiveNodeCollectionSupportsHasPropertyUsedByArraySliceAndHydration()
    {
        var browser = new BrowserService(new Fixture("<div id='P'><span id='A'></span><span id='B'></span></div>")); browser.Navigate("https://page.test/");
        Assert.Equal("A,B:true:false:A", browser.ExecuteJavaScript("const p=document.getElementById('P'),nodes=p.childNodes;[Array.prototype.slice.call(nodes).map(n=>n.id).join(','),0 in nodes,2 in nodes,Object.getOwnPropertyDescriptor(nodes,'0').value.id].join(':')"));
        Assert.Equal("B:1:false", browser.ExecuteJavaScript("p.removeChild(p.firstChild);[Array.prototype.slice.call(nodes).map(n=>n.id).join(','),nodes.length,1 in nodes].join(':')"));
    }
    [Fact]
    public void Base64UsesByteStringsAndForgivingAsciiWhitespaceWithoutAcceptingInvalidCharacters()
    {
        var browser = new BrowserService(new Fixture("<p>base64</p>")); browser.Navigate("https://page.test/");
        Assert.Equal("AP8=:255:Man:Ma:null", browser.ExecuteJavaScript("[btoa('\\x00\\xff'),atob('AP8=').charCodeAt(1),atob(' T W\\nF u\\t'),atob('TWE'),atob(btoa(null))].join(':')"));
        Assert.Equal("InvalidCharacterError:InvalidCharacterError:InvalidCharacterError:TypeError", browser.ExecuteJavaScript("[()=>btoa('中文'),()=>atob('A'),()=>atob('TW=E'),()=>atob()].map(f=>{try{f();return 'wrong'}catch(e){return e.name}}).join(':')"));
    }
    [Fact]
    public void ImageDecodeResolvesActualPixelsAndRejectsBrokenOrEmptyImages()
    {
        var browser = new BrowserService(new Fixture("""
          <script>globalThis.Result=[];const good=new Image();good.src='data:image/svg+xml,%3Csvg width="2" height="3"%3E%3Crect width="2" height="3" fill="red"/%3E%3C/svg%3E';
          good.decode().then(()=>Result.push(good.complete+':'+good.naturalWidth+':'+good.naturalHeight));
          const bad=new Image();bad.src='data:image/png;base64,AAAA';bad.decode().catch(e=>Result.push(e.name));
          new Image().decode().catch(e=>Result.push(e.name));
          </script>
          """));
        browser.Navigate("https://page.test/");
        string result = browser.ExecuteJavaScript("Result.sort().join(',')");
        Assert.Equal("EncodingError,EncodingError,true:2:3", result);
    }
    [Fact]
    public void NonceProfileRunsTrustedDynamicScriptsButRejectsParserAndFragmentInjection()
    {
        var transport = new Fixture("""
            <p id="Value">before</p><script>globalThis.Forbidden=true</script>
            <script nonce="YWJj">
              const code=document.createElement('script');code.text='globalThis.DynamicInline=42';document.head.appendChild(code);
              const remote=document.createElement('script');remote.src='/Code.js';document.head.appendChild(remote);
              const holder=document.createElement('div');holder.innerHTML='<script>globalThis.FragmentInjection=true<\/script>';document.body.appendChild(holder);
              document.getElementById('Value').textContent='allowed';
            </script><script nonce="wrong">globalThis.WrongNonce=true</script>
            """, Policy);
        transport.Resources["/Code.js"] = ("application/x-javascript", "globalThis.DynamicExternal=84");
        var browser = new BrowserService(transport); browser.Navigate("https://page.test/");
        Assert.Equal("allowed", browser.Document!.GetElementById("Value")!.TextContent);
        Assert.Equal("42:84:undefined:undefined:undefined", browser.ExecuteJavaScript("[DynamicInline,DynamicExternal,typeof Forbidden,typeof WrongNonce,typeof FragmentInjection].join(':')"));
    }
    [Fact]
    public void AllPoliciesIntersectAndNonceDoesNotAuthorizeEvalOrEventAttributes()
    {
        var browser = new BrowserService(new Fixture("<script nonce='YWJj'>globalThis.ShouldNotRun=1</script>", Policy + ", script-src 'none'"));
        browser.Navigate("https://page.test/"); Assert.Equal("undefined", browser.ExecuteJavaScript("typeof ShouldNotRun"));
        browser = new(new Fixture("<button id='B' onclick='globalThis.AttributeRan=true'>button</button><script nonce='YWJj'>globalThis.EvalError=false;try{eval('globalThis.EvalRan=true')}catch(e){EvalError=true}</script>", Policy));
        browser.Navigate("https://page.test/");
        Assert.Equal("true:undefined:undefined", browser.ExecuteJavaScript("document.getElementById('B').click();[EvalError,typeof EvalRan,typeof AttributeRan].join(':')"));
    }
    [Fact]
    public void SameOriginXhrCompletesOutsideAuthorEvaluationAndPrefetchIsReusedByScript()
    {
        var transport = new Fixture("""
            <script>globalThis.States=[];const request=new XMLHttpRequest();request.open('GET','/Code.js');request.onreadystatechange=()=>States.push(request.readyState);
            request.onload=()=>{const script=document.createElement('script');script.src='/Code.js';document.head.appendChild(script);};request.send();</script>
            """);
        transport.Resources["/Code.js"] = ("text/javascript", "globalThis.Fetched=42");
        var browser = new BrowserService(transport); browser.Navigate("https://page.test/");
        Assert.Equal("2,3,4:42", browser.ExecuteJavaScript("States.join(',')+':'+Fetched"));
        Assert.Equal(1, transport.Requests.Count(uri => uri.AbsolutePath == "/Code.js"));
    }
    [Fact]
    public void ScriptFetchCannotReadCrossOriginOrRedirectedContent()
    {
        var transport = new Fixture("<script>globalThis.Result='pending';fetch('https://other.test/secret').then(r=>r.text()).then(t=>Result=t).catch(e=>Result='blocked');</script>");
        var browser = new BrowserService(transport); browser.Navigate("https://page.test/");
        Assert.Equal("blocked", browser.ExecuteJavaScript("Result"));
        Assert.Single(transport.Requests);
    }
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void BitmapDecodingUsesActualPngOrWebpPixelsAndChecksDimensions(bool webp)
    {
        using var image = new Image<Rgba32>(2, 1); image[0, 0] = new(255, 0, 0); image[1, 0] = new(0, 255, 0);
        using var stream = new MemoryStream();
        if (webp) image.SaveAsWebp(stream, new SixLabors.ImageSharp.Formats.Webp.WebpEncoder { FileFormat = SixLabors.ImageSharp.Formats.Webp.WebpFileFormatType.Lossless }); else image.SaveAsPng(stream);
        var decoded = BrowserImageDecoder.Decode(stream.ToArray(), 2);
        Assert.Equal(0xffff0000u, decoded.Pixel(0, 0)); Assert.Equal(0xff00ff00u, decoded.Pixel(1, 0));
        Assert.Throws<BrowserLimitException>(() => BrowserImageDecoder.Decode(stream.ToArray(), 1));
    }
    [Fact]
    public void SvgPathsTransformsAndTransparentBackgroundAreRasterized()
    {
        var bytes = Encoding.UTF8.GetBytes("<svg width='20' height='20' viewBox='0 0 20 20'><g transform='translate(2 2)'><path fill='#ff0000' d='M0 0H8V8H0Z'/><circle cx='12' cy='12' r='3' fill='#00ff00'/></g></svg>");
        var image = BrowserImageDecoder.Decode(bytes, 400);
        Assert.Equal(0xffff0000u, image.Pixel(4, 4)); Assert.Equal(0xff00ff00u, image.Pixel(14, 14)); Assert.Equal(0u, image.Pixel(19, 0));
        Assert.Throws<BrowserLimitException>(() => BrowserImageDecoder.Decode(bytes, 399));
    }
    [Fact]
    public void SvgCompactArcFlagsAreSeparateTokensAndInvalidFlagsAreRejected()
    {
        var compact = BrowserImageDecoder.Decode(Encoding.UTF8.GetBytes("<svg width='12' height='12'><path d='M1 6a5 5 0 0110 0a5 5 0 01-10 0z' fill='red'/></svg>"), 144);
        var spaced = BrowserImageDecoder.Decode(Encoding.UTF8.GetBytes("<svg width='12' height='12'><path d='M1 6a5 5 0 0 1 10 0a5 5 0 0 1 -10 0z' fill='red'/></svg>"), 144);
        for (int y = 0; y < 12; y++) for (int x = 0; x < 12; x++) Assert.Equal(spaced.Pixel(x, y), compact.Pixel(x, y));
        Assert.Equal(0xffff0000u, compact.Pixel(6, 6));
        Assert.Throws<IOException>(() => BrowserImageDecoder.Decode(Encoding.UTF8.GetBytes("<svg width='12' height='12'><path d='M1 6a5 5 0 2110 0z'/></svg>"), 144));
    }
    [Fact]
    public void SessionCookieJarPreservesDomainsAndHttpOnlyAndSecureVisibility()
    {
        var cookies = new BrowserCookieStore(); var page = new Uri("https://page.test/");
        cookies.Receive(page, "sid=secret; Path=/; HttpOnly; Secure"); cookies.Receive(page, "theme=dark; Path=/; Secure");
        Assert.Contains("sid=secret", cookies.RequestHeader(page)); Assert.DoesNotContain("sid", cookies.ReadDocumentCookies(page));
        Assert.Equal("theme=dark", cookies.ReadDocumentCookies(page)); Assert.Empty(cookies.RequestHeader(new("https://other.test/")));
        Assert.Empty(cookies.RequestHeader(new("http://page.test/"))); cookies.WriteDocumentCookie(page, "sid=changed");
        Assert.Contains("sid=secret", cookies.RequestHeader(page)); Assert.DoesNotContain("changed", cookies.RequestHeader(page));
        cookies.WriteDocumentCookie(page, "local=42; Path=/"); Assert.Contains("local=42", cookies.ReadDocumentCookies(page));
    }
    [Fact]
    public void GetFormSubmissionUsesRealEditedValueAndSuccessfulControls()
    {
        var transport = new Fixture("<form action='/search'><input id='Q' name='q'><input type='hidden' name='form' value='Test'><input name='disabled' disabled><button name='go' value='yes'>Search</button></form>");
        var browser = new BrowserService(transport); browser.Navigate("https://page.test/");
        var input = browser.Document!.GetElementById("Q")!; var rect = browser.Layout(new(800, 600)).GetBoundingClientRect(input);
        browser.Pointer(new("mousedown", rect.X + 10, rect.Y + 5, 0, 1)); browser.Pointer(new("mouseup", rect.X + 10, rect.Y + 5, 0, 0));
        browser.Key(new("keydown", "a", "KeyA", "a b&中文")); browser.Key(new("keydown", "Enter", "Enter"));
        var query = transport.Requests.Last(); Assert.Equal("/search", query.AbsolutePath);
        Assert.Contains("q=a+b%26", query.Query); Assert.Contains("form=Test", query.Query); Assert.Contains("go=yes", query.Query); Assert.DoesNotContain("disabled", query.Query);
    }
    [Fact]
    public void ConstrainedParentHeightIsUsedByPercentageChildren()
    {
        var doc = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<div style='height:100vh;max-height:52px'><div id='Child' style='height:100%'>text</div></div>");
        Assert.Equal(52, new BrowserLayoutEngine().Layout(doc, new(800, 600)).GetBoundingClientRect(doc.GetElementById("Child")!).Height);
    }
    [Fact]
    public void BlockDescendantOfInlineContainerStartsAndEndsItsOwnLine()
    {
        var doc = new AngleSharp.Html.Parser.HtmlParser().ParseDocument("<div style='width:400px'><main style='display:inline'><div id='A' style='height:30px'></div><div id='B' style='display:inline-block;width:200px;height:40px'></div></main></div>");
        var layout = new BrowserLayoutEngine().Layout(doc, new(800, 600));
        var a = layout.GetBoundingClientRect(doc.GetElementById("A")!); var b = layout.GetBoundingClientRect(doc.GetElementById("B")!);
        Assert.Equal(400, a.Width); Assert.Equal(a.X, b.X); Assert.Equal(a.Bottom, b.Y);
    }
    private sealed class Fixture(string html, string? policy = null) : IWebTransport
    {
        internal readonly Dictionary<string, (string Mime, string Body)> Resources = [];
        internal readonly Dictionary<string, byte[]> Images = [];
        internal readonly List<Uri> Requests = [];
        public WebResponse Get(Uri address, BrowserLimits limits)
        {
            Requests.Add(address);
            if (Images.TryGetValue(address.AbsolutePath, out var image)) return new(address, 200, "image/svg+xml", "", true, BinaryBody: image);
            return Resources.TryGetValue(address.AbsolutePath, out var value) ? new(address, 200, value.Mime, value.Body, true) : new(address, 200, "text/html", html, true, policy);
        }
    }
}