using System.Globalization;
using System.Text.RegularExpressions;
using AngleSharp.Dom;
using XFESystem.Browser;
using XFESystem.Desktop;
using Xunit;
namespace XFESystem.Documents.Tests;
/// <summary>The exact website reader script drives the generated document in
/// the real renderer; only its desktop outer-card presentation is overridden.</summary>
public sealed class DesktopDocumentReaderTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void DesktopArticleIsFlatWhileImportedStylesAndReaderScriptStayOriginal(bool dark)
{
var browser = new BrowserService();
try
{
browser.ResizeViewport(800, 400); browser.NavigateDocument("# Flat document\n\nA paragraph.", "Flat.md", dark);
var list = browser.Layout(new(800, 400));
var article = browser.Document!.QuerySelector(".docs-article")!;
var style = list.GetComputedStyle(article)!;
Assert.Equal("0px", style.GetPropertyValue("border-top-width"));
Assert.Equal("0px", style.GetPropertyValue("border-right-width"));
Assert.Equal("0", style.GetPropertyValue("border-radius"));
Assert.Equal("none", style.GetPropertyValue("box-shadow"));
Assert.Equal("visible", style.OverflowY);
Assert.DoesNotContain("radial-gradient", style.GetPropertyValue("background-image"), StringComparison.Ordinal);
Assert.Equal(0, list.GetBoundingClientRect(article).X, 3);
Assert.Equal(800, list.GetBoundingClientRect(article).Width, 3);
Assert.Contains(browser.LoadedResources, item => item.Address.EndsWith("/css/docs/xfesystem-desktop.css", StringComparison.Ordinal));
Assert.Contains(browser.LoadedResources, item => item.Address.EndsWith("/js/docs/docs-reader.js", StringComparison.Ordinal));
Assert.Equal(dark ? "dark" : "light", browser.Document.DocumentElement!.GetAttribute("data-bs-theme"));
Assert.NotNull(browser.Document.QuerySelector(".docs-reading-progress > [data-doc-progress]"));
}
finally { browser.Release(); }
}
[Theory]
[InlineData(396, 404)] // Right pane in the current split editor.
[InlineData(796, 404)]
[InlineData(1280, 720)]
public void ExactWebsiteProgressMovesWithRealViewportScrollAndRemainsPinnedToItsPane(int width, int height)
{
var browser = new BrowserService();
try
{
browser.ResizeViewport(width, height); browser.NavigateDocument(LongMarkdown(), "Long.md");
Pump(browser, 0);
double top = Progress(browser);
double maximum = browser.DocumentHeight - height;
Assert.True(maximum > height * 2);
browser.Layout(new(width, height, 0, maximum / 2)); Pump(browser, 1000);
double middle = Progress(browser);
Assert.True(middle > top + 20, $"Progress did not follow scrolling: {top} -> {middle}");
Assert.InRange(middle, 10, 90);
browser.Layout(new(width, height, 0, maximum)); Pump(browser, 2000);
double bottom = Progress(browser);
Assert.InRange(bottom, 90, 100);
Assert.True(bottom > middle);
var progress = browser.Document!.QuerySelector("[data-doc-progress]")!;
var list = browser.Layout(new(width, height, 0, maximum));
var track = list.GetBoundingClientRect(progress.ParentElement!);
var fill = list.GetBoundingClientRect(progress);
Assert.Equal(0, track.X, 3); Assert.Equal(0, track.Y, 3);
Assert.Equal(width, track.Width, 3); Assert.Equal(3, track.Height, 3);
Assert.InRange(fill.Width, 0, width);
Assert.Equal(width * bottom / 100, fill.Width, 2);
Assert.DoesNotContain("JavaScript error:", browser.CurrentPage.Text, StringComparison.Ordinal);
}
finally { browser.Release(); }
}
[Theory]
[InlineData("")]
[InlineData("# Short\n\nDone.")]
public void EmptyAndShortDocumentsHaveFiniteCompletedProgress(string markdown)
{
var browser = new BrowserService();
try
{
browser.ResizeViewport(800, 600); browser.NavigateDocument(markdown, "Short.md"); Pump(browser, 0);
Assert.Equal(100, Progress(browser));
Assert.DoesNotContain("NaN", browser.Document!.QuerySelector("[data-doc-progress]")!.GetAttribute("style"), StringComparison.Ordinal);
}
finally { browser.Release(); }
}
[Fact]
public void IndependentPanesDoNotShareProgressAndResizeRecomputesTheActualReaderState()
{
var left = new BrowserService(); var right = new BrowserService();
try
{
left.ResizeViewport(396, 404); right.ResizeViewport(796, 404);
left.NavigateDocument(LongMarkdown(), "Left.md"); right.NavigateDocument(LongMarkdown(), "Right.md");
Pump(left, 0); Pump(right, 0);
double originalRight = Progress(right);
double scroll = (left.DocumentHeight - 404) / 2;
left.Layout(new(396, 404, 0, scroll)); Pump(left, 1000);
double beforeResize = Progress(left);
Assert.True(beforeResize > originalRight + 20);
Assert.Equal(originalRight, Progress(right));
left.ResizeViewport(1000, 720); Pump(left, 2000);
Assert.InRange(Progress(left), 0, 100);
Assert.NotEqual(beforeResize, Progress(left));
var bounds = left.Layout(new(1000, 720, 0, scroll)).GetBoundingClientRect(left.Document!.QuerySelector(".docs-reading-progress")!);
Assert.Equal(1000, bounds.Width, 3); Assert.Equal(0, bounds.Y, 3);
Assert.Equal(originalRight, Progress(right));
}
finally { left.Release(); right.Release(); }
}
[Fact]
public void OriginalWebsitePurpleToCyanProgressActuallyPaintsAcrossTheFlatPaneTop()
{
var browser = new BrowserService(); var canvas = new PixelCanvas(600, 400);
try
{
browser.ResizeViewport(600, 400); browser.NavigateDocument("# Color progress\n\nShort document.", "Gradient.md"); Pump(browser, 0);
Assert.Equal(100, Progress(browser));
browser.Paint(canvas, new(0, 0, 600, 400), 0);
uint start = canvas.Pixel(10, 1), end = canvas.Pixel(589, 1);
Assert.True((start >> 16 & 255) > (end >> 16 & 255) + 40, $"Gradient has no purple-to-cyan red change: {start:x6} {end:x6}");
Assert.True((end >> 8 & 255) > (start >> 8 & 255) + 20, $"Gradient has no cyan green change: {start:x6} {end:x6}");
Assert.True((start & 255) > 160 && (end & 255) > 160);
}
finally { browser.Release(); }
}
private static string LongMarkdown() => "# Reading progress\n\n" + string.Join("\n\n", Enumerable.Repeat(
"A real document paragraph with enough text to wrap in a narrow split pane and retain its original website appearance.", 100));
private static void Pump(BrowserService browser, int begin) { for (int frame = 1; frame <= 8; frame++) browser.AdvanceTime(begin + frame * 20); }
private static double Progress(BrowserService browser)
{
string style = browser.Document!.QuerySelector("[data-doc-progress]")!.GetAttribute("style") ?? "";
var match = Regex.Match(style, @"(?:^|;)\s*width\s*:\s*([0-9.]+)%", RegexOptions.IgnoreCase);
Assert.True(match.Success, "The real reader did not set finite progress: " + style);
return double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
}
private sealed class PixelCanvas(int width, int height) : IDesktopCoverageCanvas
{
private readonly uint[] pixels = new uint[width * height];
public int Width => width; public int Height => height;
public uint Pixel(int x, int y) => pixels[y * width + x] & 0xFFFFFF;
public void Fill(uint rgb, int x, int y, int w, int h)
{ for (int yy = Math.Max(0, y); yy < Math.Min(height, y + h); yy++) for (int xx = Math.Max(0, x); xx < Math.Min(width, x + w); xx++) pixels[yy * width + xx] = rgb; }
public void BlendCoverage(uint rgb, int x, int y, int w, int h, ReadOnlySpan<byte> coverage)
{
for (int yy = Math.Max(0, y); yy < Math.Min(height, y + h); yy++)
for (int xx = Math.Max(0, x); xx < Math.Min(width, x + w); xx++)
{
uint alpha = coverage[(yy - y) * w + xx - x], old = pixels[yy * width + xx];
uint r = ((rgb >> 16 & 255) * alpha + (old >> 16 & 255) * (255 - alpha) + 127) / 255;
uint g = ((rgb >> 8 & 255) * alpha + (old >> 8 & 255) * (255 - alpha) + 127) / 255;
uint b = ((rgb & 255) * alpha + (old & 255) * (255 - alpha) + 127) / 255;
pixels[yy * width + xx] = r << 16 | g << 8 | b;
}
}
public void Text(string text, uint rgb, int x, int y) { }
public void Line(uint rgb, int x1, int y1, int x2, int y2) { }
public void Present() { }
}
}
using System.Globalization;
using System.Text.RegularExpressions;
using AngleSharp.Dom;
using XFESystem.Browser;
using XFESystem.Desktop;
using Xunit;
namespace XFESystem.Documents.Tests;
/// <summary>The exact website reader script drives the generated document in
/// the real renderer; only its desktop outer-card presentation is overridden.</summary>
public sealed class DesktopDocumentReaderTests
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void DesktopArticleIsFlatWhileImportedStylesAndReaderScriptStayOriginal(bool dark)
{
var browser = new BrowserService();
try
{
browser.ResizeViewport(800, 400); browser.NavigateDocument("# Flat document\n\nA paragraph.", "Flat.md", dark);
var list = browser.Layout(new(800, 400));
var article = browser.Document!.QuerySelector(".docs-article")!;
var style = list.GetComputedStyle(article)!;
Assert.Equal("0px", style.GetPropertyValue("border-top-width"));
Assert.Equal("0px", style.GetPropertyValue("border-right-width"));
Assert.Equal("0", style.GetPropertyValue("border-radius"));
Assert.Equal("none", style.GetPropertyValue("box-shadow"));
Assert.Equal("visible", style.OverflowY);
Assert.DoesNotContain("radial-gradient", style.GetPropertyValue("background-image"), StringComparison.Ordinal);
Assert.Equal(0, list.GetBoundingClientRect(article).X, 3);
Assert.Equal(800, list.GetBoundingClientRect(article).Width, 3);
Assert.Contains(browser.LoadedResources, item => item.Address.EndsWith("/css/docs/xfesystem-desktop.css", StringComparison.Ordinal));
Assert.Contains(browser.LoadedResources, item => item.Address.EndsWith("/js/docs/docs-reader.js", StringComparison.Ordinal));
Assert.Equal(dark ? "dark" : "light", browser.Document.DocumentElement!.GetAttribute("data-bs-theme"));
Assert.NotNull(browser.Document.QuerySelector(".docs-reading-progress > [data-doc-progress]"));
}
finally { browser.Release(); }
}
[Theory]
[InlineData(396, 404)] // Right pane in the current split editor.
[InlineData(796, 404)]
[InlineData(1280, 720)]
public void ExactWebsiteProgressMovesWithRealViewportScrollAndRemainsPinnedToItsPane(int width, int height)
{
var browser = new BrowserService();
try
{
browser.ResizeViewport(width, height); browser.NavigateDocument(LongMarkdown(), "Long.md");
Pump(browser, 0);
double top = Progress(browser);
double maximum = browser.DocumentHeight - height;
Assert.True(maximum > height * 2);
browser.Layout(new(width, height, 0, maximum / 2)); Pump(browser, 1000);
double middle = Progress(browser);
Assert.True(middle > top + 20, $"Progress did not follow scrolling: {top} -> {middle}");
Assert.InRange(middle, 10, 90);
browser.Layout(new(width, height, 0, maximum)); Pump(browser, 2000);
double bottom = Progress(browser);
Assert.InRange(bottom, 90, 100);
Assert.True(bottom > middle);
var progress = browser.Document!.QuerySelector("[data-doc-progress]")!;
var list = browser.Layout(new(width, height, 0, maximum));
var track = list.GetBoundingClientRect(progress.ParentElement!);
var fill = list.GetBoundingClientRect(progress);
Assert.Equal(0, track.X, 3); Assert.Equal(0, track.Y, 3);
Assert.Equal(width, track.Width, 3); Assert.Equal(3, track.Height, 3);
Assert.InRange(fill.Width, 0, width);
Assert.Equal(width * bottom / 100, fill.Width, 2);
Assert.DoesNotContain("JavaScript error:", browser.CurrentPage.Text, StringComparison.Ordinal);
}
finally { browser.Release(); }
}
[Theory]
[InlineData("")]
[InlineData("# Short\n\nDone.")]
public void EmptyAndShortDocumentsHaveFiniteCompletedProgress(string markdown)
{
var browser = new BrowserService();
try
{
browser.ResizeViewport(800, 600); browser.NavigateDocument(markdown, "Short.md"); Pump(browser, 0);
Assert.Equal(100, Progress(browser));
Assert.DoesNotContain("NaN", browser.Document!.QuerySelector("[data-doc-progress]")!.GetAttribute("style"), StringComparison.Ordinal);
}
finally { browser.Release(); }
}
[Fact]
public void IndependentPanesDoNotShareProgressAndResizeRecomputesTheActualReaderState()
{
var left = new BrowserService(); var right = new BrowserService();
try
{
left.ResizeViewport(396, 404); right.ResizeViewport(796, 404);
left.NavigateDocument(LongMarkdown(), "Left.md"); right.NavigateDocument(LongMarkdown(), "Right.md");
Pump(left, 0); Pump(right, 0);
double originalRight = Progress(right);
double scroll = (left.DocumentHeight - 404) / 2;
left.Layout(new(396, 404, 0, scroll)); Pump(left, 1000);
double beforeResize = Progress(left);
Assert.True(beforeResize > originalRight + 20);
Assert.Equal(originalRight, Progress(right));
left.ResizeViewport(1000, 720); Pump(left, 2000);
Assert.InRange(Progress(left), 0, 100);
Assert.NotEqual(beforeResize, Progress(left));
var bounds = left.Layout(new(1000, 720, 0, scroll)).GetBoundingClientRect(left.Document!.QuerySelector(".docs-reading-progress")!);
Assert.Equal(1000, bounds.Width, 3); Assert.Equal(0, bounds.Y, 3);
Assert.Equal(originalRight, Progress(right));
}
finally { left.Release(); right.Release(); }
}
[Fact]
public void OriginalWebsitePurpleToCyanProgressActuallyPaintsAcrossTheFlatPaneTop()
{
var browser = new BrowserService(); var canvas = new PixelCanvas(600, 400);
try
{
browser.ResizeViewport(600, 400); browser.NavigateDocument("# Color progress\n\nShort document.", "Gradient.md"); Pump(browser, 0);
Assert.Equal(100, Progress(browser));
browser.Paint(canvas, new(0, 0, 600, 400), 0);
uint start = canvas.Pixel(10, 1), end = canvas.Pixel(589, 1);
Assert.True((start >> 16 & 255) > (end >> 16 & 255) + 40, $"Gradient has no purple-to-cyan red change: {start:x6} {end:x6}");
Assert.True((end >> 8 & 255) > (start >> 8 & 255) + 20, $"Gradient has no cyan green change: {start:x6} {end:x6}");
Assert.True((start & 255) > 160 && (end & 255) > 160);
}
finally { browser.Release(); }
}
private static string LongMarkdown() => "# Reading progress\n\n" + string.Join("\n\n", Enumerable.Repeat(
"A real document paragraph with enough text to wrap in a narrow split pane and retain its original website appearance.", 100));
private static void Pump(BrowserService browser, int begin) { for (int frame = 1; frame <= 8; frame++) browser.AdvanceTime(begin + frame * 20); }
private static double Progress(BrowserService browser)
{
string style = browser.Document!.QuerySelector("[data-doc-progress]")!.GetAttribute("style") ?? "";
var match = Regex.Match(style, @"(?:^|;)\s*width\s*:\s*([0-9.]+)%", RegexOptions.IgnoreCase);
Assert.True(match.Success, "The real reader did not set finite progress: " + style);
return double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
}
private sealed class PixelCanvas(int width, int height) : IDesktopCoverageCanvas
{
private readonly uint[] pixels = new uint[width * height];
public int Width => width; public int Height => height;
public uint Pixel(int x, int y) => pixels[y * width + x] & 0xFFFFFF;
public void Fill(uint rgb, int x, int y, int w, int h)
{ for (int yy = Math.Max(0, y); yy < Math.Min(height, y + h); yy++) for (int xx = Math.Max(0, x); xx < Math.Min(width, x + w); xx++) pixels[yy * width + xx] = rgb; }
public void BlendCoverage(uint rgb, int x, int y, int w, int h, ReadOnlySpan<byte> coverage)
{
for (int yy = Math.Max(0, y); yy < Math.Min(height, y + h); yy++)
for (int xx = Math.Max(0, x); xx < Math.Min(width, x + w); xx++)
{
uint alpha = coverage[(yy - y) * w + xx - x], old = pixels[yy * width + xx];
uint r = ((rgb >> 16 & 255) * alpha + (old >> 16 & 255) * (255 - alpha) + 127) / 255;
uint g = ((rgb >> 8 & 255) * alpha + (old >> 8 & 255) * (255 - alpha) + 127) / 255;
uint b = ((rgb & 255) * alpha + (old & 255) * (255 - alpha) + 127) / 255;
pixels[yy * width + xx] = r << 16 | g << 8 | b;
}
}
public void Text(string text, uint rgb, int x, int y) { }
public void Line(uint rgb, int x1, int y1, int x2, int y2) { }
public void Present() { }
}
}