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<IDocument>(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<IDocument>(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<script>globalThis.MarkdownEvil=true</script><img src=x onerror=\"globalThis.MarkdownEvil=true\">", "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<Uri> Requests { get; } = [];
public WebResponse Get(Uri address, BrowserLimits limits)
{ Requests.Add(address); throw new InvalidOperationException("The fixture cannot access a network: " + address); }
}
}
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<IDocument>(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<IDocument>(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<script>globalThis.MarkdownEvil=true</script><img src=x onerror=\"globalThis.MarkdownEvil=true\">", "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<Uri> Requests { get; } = [];
public WebResponse Get(Uri address, BrowserLimits limits)
{ Requests.Add(address); throw new InvalidOperationException("The fixture cannot access a network: " + address); }
}
}