using AngleSharp.Dom; using XFESystem.Browser; using Xunit; using Xunit.Abstractions; namespace XFESystem.Documents.Tests; public sealed class WebsiteWebBrowserTests(ITestOutputHelper output) { [Fact] public void ExactWebsiteScriptsHighlightCSharpAndBuildRealInteractiveCodeTools() { var browser = CreateBrowser(); browser.NavigateDocument(WebsiteAssets.ReadText("Fixtures/WebsiteParity.md"), "WebsiteParity.md"); for (int frame = 1; frame <= 8; frame++) browser.AdvanceTime(frame * 20); output.WriteLine(browser.RefreshDocument().Text); Assert.DoesNotContain("JavaScript error:", browser.CurrentPage.Text, StringComparison.Ordinal); var doc = Assert.IsAssignableFrom(browser.Document); Assert.True(doc.QuerySelectorAll(".language-csharp .hljs-keyword").Length > 0); Assert.True(doc.QuerySelectorAll(".language-csharp .hljs-string").Length > 0); Assert.True(doc.QuerySelectorAll(".language-csharp .hljs-comment").Length > 0); var pre = Assert.Single(doc.QuerySelectorAll("pre.code-block-enhanced")); Assert.Equal("true", pre.GetAttribute("data-code-tools-applied")); Assert.Equal(4, pre.QuerySelectorAll("[data-code-action]").Length); Assert.True(pre.QuerySelectorAll(".code-line-numbers > span").Length > 8); Assert.True(pre.ClassList.Contains("code-lines-visible")); browser.ExecuteJavaScript("document.querySelector('[data-code-action=lines]').click();"); Assert.False(pre.ClassList.Contains("code-lines-visible")); browser.ExecuteJavaScript("document.querySelector('[data-code-action=lines]').click();"); Assert.True(pre.ClassList.Contains("code-lines-visible")); Assert.Equal(3, doc.QuerySelectorAll("table tr").Length); Assert.NotEmpty(doc.QuerySelectorAll("blockquote")); Assert.NotEmpty(doc.QuerySelectorAll("ul li")); Assert.NotEmpty(doc.QuerySelectorAll("ol li")); Assert.NotEmpty(doc.QuerySelectorAll("a[href^='#']")); Assert.Contains(browser.LoadedResources, resource => resource.Address.EndsWith("/js/docs/csharp.js", StringComparison.Ordinal)); Assert.All(browser.LoadedResources, resource => Assert.StartsWith(PackagedBrowserResources.Origin + "/", resource.Address)); } [Fact] public void ExactWebsiteKatexProducesRealDomAndMermaidReportsMissingSvgThroughOriginalFallback() { var browser = CreateBrowser(); browser.NavigateDocument(WebsiteAssets.ReadText("Fixtures/WebsiteParity.md"), "WebsiteParity.md"); for (int frame = 1; frame <= 32; frame++) browser.AdvanceTime(frame * 20); output.WriteLine(browser.RefreshDocument().Text); var doc = Assert.IsAssignableFrom(browser.Document); Assert.Equal(2, doc.QuerySelectorAll(".katex").Length); Assert.NotNull(doc.QuerySelector(".katex .mfrac")); var wrapper = Assert.Single(doc.QuerySelectorAll(".mermaid-wrapper")); Assert.True(wrapper.ClassList.Contains("is-error")); Assert.False(wrapper.ClassList.Contains("is-rendered")); Assert.Empty(wrapper.QuerySelectorAll("svg")); Assert.Contains("图表渲染失败", wrapper.QuerySelector(".mermaid-error-title")!.TextContent, StringComparison.Ordinal); var originalSource = wrapper.QuerySelector("pre.mermaid-source code")!.TextContent; Assert.Contains("flowchart LR", originalSource, StringComparison.Ordinal); Assert.Contains("A[Markdown] --> B[Web renderer]", originalSource, StringComparison.Ordinal); Assert.Contains(browser.LoadedResources, resource => resource.Address.Contains("/js/docs/mermaid.min.js", StringComparison.Ordinal)); Assert.True(wrapper.QuerySelector("[data-mermaid-action=zoom-in]")!.HasAttribute("disabled")); output.WriteLine("MermaidNeedsSvg=true; original library and fallback executed; SVG getBBox and vector painting remain unsupported."); } [Fact] public void GeneratedLocalPreviewSanitizesMarkdownAndRefusesOffOriginAutomaticResources() { var transport = new RejectNetwork(); var browser = new BrowserService(transport); browser.NavigateDocument("# Safe\n", "Safe.md"); Assert.Equal("undefined", browser.ExecuteJavaScript("typeof MarkdownEvil")); Assert.DoesNotContain("onerror", browser.Document!.DocumentElement.OuterHtml, StringComparison.OrdinalIgnoreCase); browser.ExecuteJavaScript("var outside=document.createElement('script');outside.src='http://outside.invalid/Evil.js';document.head.appendChild(outside);"); Assert.Empty(transport.Requests); Assert.DoesNotContain(browser.LoadedResources, resource => resource.Address.Contains("outside.invalid", StringComparison.Ordinal)); Assert.Contains("bundled", browser.RefreshDocument().Text, StringComparison.OrdinalIgnoreCase); } private static BrowserService CreateBrowser() => new(transport: new RejectNetwork()); private sealed class RejectNetwork : IWebTransport { public List Requests { get; } = []; public WebResponse Get(Uri address, BrowserLimits limits) { Requests.Add(address); throw new InvalidOperationException("The fixture cannot access a network: " + address); } } }