using AngleSharp.Dom;
using XFESystem.Desktop;
namespace XFESystem.Browser.Tests;
public sealed class BrowserPipelineTests
{
[Fact]
public void OrdinaryScriptErrorDoesNotSuppressFollowingIndependentScripts()
{
var source = new Resources();source.Add("/","text/html","
before
");
var page = new BrowserService(source).Navigate("http://test/");
Assert.Contains("after",page.Text,StringComparison.Ordinal);Assert.Contains("JavaScript error:",page.Text,StringComparison.Ordinal);
}
[Fact]
public void BudgetFailureStopsAutomaticDeferredScriptsUntilNextNavigation()
{
var source = new Resources();source.Add("/","text/html","
before
");
source.Add("/After.js","application/javascript","document.getElementById('P').textContent='after'");
var page = new BrowserService(source,new BrowserLimits{MaximumStatements=100}).Navigate("http://test/");
Assert.Contains("before",page.Text,StringComparison.Ordinal);Assert.DoesNotContain("after",page.Text,StringComparison.Ordinal);Assert.Single(source.Requests);
}
[Fact]
public void StylesExternalClassicDeferDynamicScriptsAndLifecycleUseOneRealDocument()
{
var source = new Resources();
source.Add("/Index", "text/html", """
Pipeline
before
""");
source.Add("/First.js", "application/javascript", "order.push('first');document.querySelector('#Box').textContent='external worked';const s=document.createElement('script');s.src='Dynamic.js';s.onload=()=>order.push('dynamic-load');document.head.appendChild(s);");
source.Add("/Dynamic.js", "text/javascript", "order.push('dynamic');");
source.Add("/Defer.js", "application/javascript", "order.push('defer');");
source.Add("/Theme.css", "text/css", "#Box{width:200px;height:50px;color:#123456;background:#aabbcc;margin:0}");
var browser = new BrowserService(source);
var page = browser.Navigate("http://test/Index");
Assert.Contains("external worked", page.Text, StringComparison.Ordinal);
Assert.Equal("first,dynamic,dynamic-load,inline,defer,dom,load", browser.ExecuteJavaScript("order.join(',')"));
var layout = browser.Layout(new(600, 400));
var box = layout.Boxes.Single(b => b.Element?.Id == "Box" && b.Node is IElement);
Assert.Equal(200, box.ContentBox.Width); Assert.Equal(50, box.ContentBox.Height);
Assert.Contains(layout.Commands, c => c.Kind == BrowserDrawKind.Fill && c.Color == 0xAABBCC);
Assert.Equal(4, browser.LoadedResources.Count);
}
[Fact]
public void PointerBubblesToParentAndKeyboardEditsFocusedTextareaWithInputEvents()
{
var resources = new Resources(); resources.Add("/", "text/html", """
""".Replace("Parent.addEventListener", "document.getElementById('Parent').addEventListener").Replace("Entry.addEventListener", "document.getElementById('Entry').addEventListener").Replace("Output.textContent=Entry.value", "document.getElementById('Output').textContent=document.getElementById('Entry').value"));
var browser = new BrowserService(resources); browser.Navigate("http://test/"); browser.Layout(new(600,400));
browser.Pointer(new("mousedown", 10, 10, Buttons:1)); browser.Pointer(new("mouseup",10,10));
browser.Key(new("keydown", "a", "KeyA", "a")); browser.Key(new("keyup", "a", "KeyA"));
Assert.Equal("a", browser.ExecuteJavaScript("document.getElementById('Entry').value"));
Assert.Equal("parent", browser.ExecuteJavaScript("events.join(',')"));
Assert.Contains("a", browser.RefreshDocument().Text, StringComparison.Ordinal);
browser.Key(new("keydown","Backspace"));
Assert.Equal("", browser.ExecuteJavaScript("document.getElementById('Entry').value"));
}
[Fact]
public void PreventDefaultStopsTypingAndWheelDefaultAction()
{
var resources = new Resources(); resources.Add("/", "text/html", "");
var browser = new BrowserService(resources); browser.Navigate("http://test/");
browser.Key(new("keydown", "a", "KeyA", "a"));
Assert.Equal("",browser.ExecuteJavaScript("document.getElementById('T').value"));
Assert.True(browser.Pointer(new("wheel",10,10,DeltaY:60)));
}
[Fact]
public void TimersAndRafRequireExplicitMonotonicPump()
{
var resources = new Resources(); resources.Add("/", "text/html", "
before
");
var browser = new BrowserService(resources); browser.Navigate("http://test/");
Assert.DoesNotContain("timer", browser.CurrentPage.Text, StringComparison.Ordinal);
browser.AdvanceTime(20);
Assert.Contains("timer", browser.CurrentPage.Text, StringComparison.Ordinal);
Assert.StartsWith("frame ", browser.CurrentPage.Title, StringComparison.Ordinal);
}
[Theory]
[InlineData("http://test/Unsafe.js",true,"HTTP subresource")]
[InlineData("https://test/Unsafe.js",false,"certificate validation")]
public void HttpsSubresourcesCannotBypassMixedContentOrCertificateChecks(string script, bool validated,string error)
{
var source = new Resources(); source.Add("/", "text/html", ""); source.Add("/Unsafe.js", "application/javascript", "document.title='unsafe'",validated);
var browser = new BrowserService(source); var page = browser.Navigate("https://test/");
Assert.Contains(error, page.Text,StringComparison.Ordinal); Assert.NotEqual("unsafe",page.Title);
}
[Fact]
public void DeclaredCspBlocksExternalStyleAndScriptRequestsAndInlineStyles()
{
var source = new Resources(); source.Add("/", "text/html", "
safe
");
var browser = new BrowserService(source); browser.Navigate("http://test/");
var list = browser.Layout(new(600,400));
Assert.Single(source.Requests); Assert.Empty(browser.LoadedResources);
Assert.DoesNotContain(list.Commands,c=>c.Color==0xFF0000);
}
[Fact]
public void PageSessionsDoNotShareGlobalsOrDom()
{
var browser = new BrowserService(); var first=(BrowserService)browser.CreatePageSession();var second=(BrowserService)browser.CreatePageSession();
first.Navigate("about:Start");second.Navigate("about:Start");first.ExecuteJavaScript("globalThis.privateValue=123;document.title='first'");
Assert.Equal("undefined",second.ExecuteJavaScript("typeof privateValue"));Assert.NotEqual(first.RefreshDocument().Title,second.RefreshDocument().Title);
}
[Fact]
public void PackagedMonacoCoreIsTheVerifiedTransformationWithoutHostPathAccess()
{
var resources = new PackagedBrowserResources();
Assert.True(resources.TryGet(new(PackagedBrowserResources.MonacoCoreRoot+"EditorCore.js"),out var response));
var bytes = System.Text.Encoding.UTF8.GetBytes(response.Body);
Assert.Equal(2_293_601, bytes.Length);
Assert.Equal("828c22be163ab40c177106ef4d4a0ff86ae3611bafc9dd82ccef678e08971952",
Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(bytes)));
Assert.Equal("application/javascript",response.ContentType);
Assert.False(resources.TryGet(new(PackagedBrowserResources.Origin+"/Browser/Monaco/0.52.2/min/vs/editor/editor.main.js"),out _));
Assert.False(resources.TryGet(new("file:///C:/Secret"),out _));
}
private sealed class Resources : IWebTransport
{
private readonly Dictionary values=[];
public List Requests {get;}=[];
public void Add(string path,string mime,string body,bool validated=true)=>values[path]=(mime,body,validated);
public WebResponse Get(Uri address,BrowserLimits limits)
{ Requests.Add(address.AbsoluteUri);var value=values[address.AbsolutePath];return new(address,200,value.Mime,value.Body,value.Validated); }
}
}