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 BrowserCssCacheTests
{
    private static IDocument Page(string css, string content = "<div id=A>Code</div>") =>
        new HtmlParser().ParseDocument("<style id=Css>" + css + "</style>" + content);
    private static BrowserDisplayList Layout(BrowserLayoutEngine engine, IDocument page, double width = 800, double height = 600) => engine.Layout(page, new(width, height));
    private static IElement A(IDocument page) => page.GetElementById("A")!;

    [Fact]
    public void RepeatedLayoutReusesAuthorRulesWhileCurrentDomStylesStillChange()
    {
        var page = Page("#A{width:100px;height:20px}"); var engine = new BrowserLayoutEngine();
        Layout(engine, page); A(page).SetAttribute("style", "width:180px"); var next = Layout(engine, page);
        Assert.Equal(180, next.GetBoundingClientRect(A(page)).Width); Assert.Equal(1, engine.CssParseGeneration); Assert.Equal(1, engine.CssReuseCount);
    }

    [Fact]
    public void TextDataMutationInvalidatesCachedCascadeWithoutElementTextContentConcatenation()
    {
        var page = Page("#A{width:100px}"); var engine = new BrowserLayoutEngine(); Layout(engine, page);
        ((IText)page.GetElementById("Css")!.FirstChild!).Data = "#A{width:177px}";
        Assert.Equal(177, Layout(engine, page).GetBoundingClientRect(A(page)).Width); Assert.Equal(2, engine.CssParseGeneration);
    }

    [Fact]
    public void MultiNodeStyleTextPreservesConcatenationAndInvalidatesChangesAndOrder()
    {
        var page = Page(""); var css = page.GetElementById("Css")!;
        css.AppendChild(page.CreateTextNode("#A{width:")); var value = page.CreateTextNode("100px"); css.AppendChild(value); css.AppendChild(page.CreateComment("ignored")); css.AppendChild(page.CreateTextNode("}"));
        var engine = new BrowserLayoutEngine(); Assert.Equal(100, Layout(engine, page).GetBoundingClientRect(A(page)).Width);
        Assert.Equal(100, Layout(engine, page).GetBoundingClientRect(A(page)).Width); Assert.Equal(1, engine.CssReuseCount);
        value.Data = "188px"; Assert.Equal(188, Layout(engine, page).GetBoundingClientRect(A(page)).Width); Assert.Equal(2, engine.CssParseGeneration);
    }

    [Fact]
    public void LinkedOwnerRemovalAndAuthorOrderChangeRebuildActualCascade()
    {
        var page = new HtmlParser().ParseDocument("<style>#A{width:100px}</style><link id=Link rel=stylesheet><style id=Last>#A{width:300px}</style><div id=A></div>");
        var link = page.GetElementById("Link")!; var last = page.GetElementById("Last")!; var sheets = new[] { new BrowserStyleSheet("#A{width:200px}", link) };
        var engine = new BrowserLayoutEngine(); Assert.Equal(300, engine.Layout(page, new(800, 600), sheets).GetBoundingClientRect(A(page)).Width);
        page.Head!.InsertBefore(last, link); Assert.Equal(200, engine.Layout(page, new(800, 600), sheets).GetBoundingClientRect(A(page)).Width);
        link.Remove(); Assert.Equal(300, engine.Layout(page, new(800, 600), sheets).GetBoundingClientRect(A(page)).Width); Assert.Equal(3, engine.CssParseGeneration);
    }

    [Fact]
    public void WidthAndHeightMediaConditionsAndViewportUnitsUseCurrentViewport()
    {
        var page = Page("#A{width:100px;height:10vh}@media(min-width:600px){#A{width:200px}}@media(min-height:500px){#A{width:300px}}");
        var engine = new BrowserLayoutEngine(); Assert.Equal(300, Layout(engine, page).GetBoundingClientRect(A(page)).Width);
        var shortPage = Layout(engine, page, 800, 400); Assert.Equal(200, shortPage.GetBoundingClientRect(A(page)).Width); Assert.Equal(40, shortPage.GetBoundingClientRect(A(page)).Height);
        Assert.Equal(100, Layout(engine, page, 400, 400).GetBoundingClientRect(A(page)).Width); Assert.Equal(3, engine.CssParseGeneration);
    }

    [Fact]
    public void CachedSelectorsReadLiveHoverActiveAndFocusStateWithoutReparsing()
    {
        var page = Page("#A{width:100px}#A:hover{width:150px}#A:active{width:180px}#A:focus{height:70px}"); var engine = new BrowserLayoutEngine();
        Layout(engine, page); engine.SetInteractionState(A(page), null, null); Assert.Equal(150, Layout(engine, page).GetBoundingClientRect(A(page)).Width);
        engine.SetInteractionState(A(page), A(page), A(page)); var active = Layout(engine, page); Assert.Equal(180, active.GetBoundingClientRect(A(page)).Width); Assert.Equal(70, active.GetBoundingClientRect(A(page)).Height);
        engine.SetInteractionState(null, null, null); Assert.Equal(100, Layout(engine, page).GetBoundingClientRect(A(page)).Width);
        Assert.Equal(1, engine.CssParseGeneration); Assert.Equal(3, engine.CssReuseCount);
    }

    [Fact]
    public void NavigationClearsPreviousDocumentOwnersAndInteractionState()
    {
        var page = Page("#A:hover{width:100px}"); var engine = new BrowserLayoutEngine(); engine.SetInteractionState(A(page), null, null); Layout(engine, page);
        var second = Page("#A{width:180px}"); Assert.Equal(180, Layout(engine, second).GetBoundingClientRect(A(second)).Width); Assert.Equal(2, engine.CssParseGeneration);
        A(second).SetAttribute("style", "height:30px"); Assert.Equal(30, Layout(engine, second).GetBoundingClientRect(A(second)).Height); Assert.Equal(1, engine.CssReuseCount);
    }

    [Fact]
    public void CspToggleCannotReuseAuthorRulesAcrossDisabledStyles()
    {
        var page = Page("#A{width:100px}"); var engine = new BrowserLayoutEngine(); Layout(engine, page);
        var blocked = engine.Layout(page, new(800, 600), blockAuthorStyles: true); Assert.Equal(784, blocked.GetBoundingClientRect(A(page)).Width);
        Assert.Contains(blocked.Diagnostics, message => message.Contains("CSP")); Assert.Equal(100, Layout(engine, page).GetBoundingClientRect(A(page)).Width);
    }

    [Fact]
    public void CacheResetsInlineDeclarationAndSelectorBudgetsPerLayoutButStillRejectsExcess()
    {
        var page = Page("#A{width:100px}", "<div id=A style='height:20px'>X</div>");
        var engine = new BrowserLayoutEngine(new() { MaximumDeclarations = 2, MaximumSelectorEvaluations = 5 });
        for (int count = 0; count < 10; count++) Assert.Equal(20, Layout(engine, page).GetBoundingClientRect(A(page)).Height);
        Assert.Equal(1, engine.CssParseGeneration);
        A(page).SetAttribute("style", "height:20px;color:red"); Assert.Throws<BrowserLimitException>(() => Layout(engine, page));
        // The subject index correctly skips #A for other elements. Use a
        // universal rule to exercise the actual matcher-work budget.
        page.QuerySelector("style")!.TextContent = "*{width:100px}";
        A(page).SetAttribute("style", "height:20px"); A(page).AppendChild(page.CreateElement("span")); Assert.Throws<BrowserLimitException>(() => Layout(engine, page));
    }

    [Fact]
    public void ChangedCssStillChecksSourceCharacterLimitAndParseDiagnosticsReplay()
    {
        var page = Page("#A{filter:none;width:100px}"); var engine = new BrowserLayoutEngine(new() { MaximumCssCharacters = 60 });
        var first = Layout(engine, page); var cached = Layout(engine, page);
        Assert.Contains(first.Diagnostics, message => message.Contains("filter")); Assert.Contains(cached.Diagnostics, message => message.Contains("filter"));
        ((IText)page.GetElementById("Css")!.FirstChild!).Data = "/*" + new string('x', 100) + "*/";
        Assert.Throws<BrowserLimitException>(() => Layout(engine, page));
    }

    [Fact]
    public void CachedLayoutDoesNotReallocateLargeUnchangedStylesheetCopies()
    {
        // Keep substantial real parse work after comment scanning no longer
        // copies the entire source: cache reuse must avoid compiling these rules.
        string rules=string.Concat(Enumerable.Range(0,512).Select(index=>$".Unused{index}{{width:100px;height:20px}}"));
        var page = Page("/*" + new string('x', 150000) + "*/"+rules+"#A{width:100px}"); var engine = new BrowserLayoutEngine();
        Layout(engine, page); Layout(engine, page);
        long before = GC.GetAllocatedBytesForCurrentThread(); Layout(engine, page); long cached = GC.GetAllocatedBytesForCurrentThread() - before;
        before = GC.GetAllocatedBytesForCurrentThread(); Layout(new BrowserLayoutEngine(), page); long fresh = GC.GetAllocatedBytesForCurrentThread() - before;
        Assert.True(cached < fresh / 4, $"Unchanged cache allocated {cached}; fresh parse allocated {fresh}."); Assert.Equal(1, engine.CssParseGeneration);
    }

    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void ReplacedNodesAreNotRetainedByPersistentEngineAndEarlierGeometryStillWorks(bool suppliedIdentity)
    {
        var page = Page("#A{width:100px;height:20px}"); var engine = new BrowserLayoutEngine();
        int next = 100;
        Func<INode, int>? identity = suppliedIdentity ? _ => ++next : null;
        var first = engine.Layout(page, new(800, 600), nodeIdentity: identity);
        var previous = A(page); int expected = engine.RetainedNodeIdentityCount;
        int bodyId = Assert.Single(first.Boxes, box => ReferenceEquals(box.Node, page.Body)).NodeId;
        for (int iteration = 0; iteration < 100; iteration++)
        {
            var replacement = page.CreateElement("div"); replacement.Id = "A"; replacement.TextContent = "Replacement";
            A(page).Remove(); page.Body!.AppendChild(replacement);
            var current = engine.Layout(page, new(800, 600), nodeIdentity: identity);
            Assert.Equal(expected, engine.RetainedNodeIdentityCount);
            Assert.Equal(bodyId, Assert.Single(current.Boxes, box => ReferenceEquals(box.Node, page.Body)).NodeId);
            Assert.Equal(default, current.GetBoundingClientRect(previous));
        }
        Assert.Equal(100, first.GetBoundingClientRect(previous).Width);
        Assert.Equal(1, engine.CssParseGeneration);
    }

    [Fact]
    public void FailedDomBudgetAlsoPrunesReplacedNodeIdentityReferences()
    {
        var page = Page("#A{width:100px}"); var engine = new BrowserLayoutEngine(new() { MaximumNodes = 8 });
        Layout(engine, page); var previous = A(page);
        page.Body!.InnerHtml = string.Concat(Enumerable.Repeat("<div>New</div>", 20));
        Assert.Throws<BrowserLimitException>(() => Layout(engine, page));
        Assert.InRange(engine.RetainedNodeIdentityCount, 0, 8);
        page.Body.InnerHtml = "<div id=A>Recovered</div>";
        var current = Layout(engine, page);
        Assert.Equal(default, current.GetBoundingClientRect(previous)); Assert.Equal(100, current.GetBoundingClientRect(A(page)).Width);
        Assert.Equal(7, engine.RetainedNodeIdentityCount);
    }
}