using AngleSharp.Dom;
using AngleSharp.Html.Parser;
namespace XFESystem.Browser.Tests;
public sealed class BrowserAdjacentTests
{
[Theory]
[InlineData("beforebegin", "LmoveoriginalR", "original")]
[InlineData("afterbegin", "LmoveoriginalR", "moveoriginal")]
[InlineData("beforeend", "LoriginalmoveR", "originalmove")]
[InlineData("afterend", "LoriginalmoveR", "original")]
public void ElementAndTextSupportAllFourLocations(string position, string parentText, string targetText)
{
foreach (var method in new[] { "Element", "Text" })
{
var page = new Page();
page.Run("var target=document.getElementById('Target'),moving=document.createElement('span');moving.textContent='move';document.body.append(moving);");
page.Run(method == "Element" ?
$"if(target.insertAdjacentElement('{position}',moving)!==moving)throw Error('return');" :
$"if(target.insertAdjacentText('{position}','move')!==undefined)throw Error('return');");
Assert.Equal(parentText, page.Document.GetElementById("Parent")!.TextContent);
Assert.Equal(targetText, page.Document.GetElementById("Target")!.TextContent);
}
}
[Fact]
public void HtmlFourPositionsPreserveSiblingsWhitespaceAndWrappedIdentity()
{
var page = new Page();
page.Run("""
const target=document.getElementById('Target'), original=target.firstChild;
target.insertAdjacentHTML('BEFOREBEGIN',' <span id="Before">before</span> ');
target.insertAdjacentHTML('afterbegin','A<i id="First">first</i> ');
target.insertAdjacentHTML('beforeend',' <b id="Last">last</b>Z');
target.insertAdjacentHTML('afterend',' <span id="After">after</span> ');
if(target!==document.getElementById('Target')||original!==target.childNodes.item(3))throw Error('identity');
if(target.insertAdjacentHTML('beforeend','')!==undefined)throw Error('return');
""");
Assert.Equal("Afirst original lastZ", page.Document.GetElementById("Target")!.TextContent);
Assert.Equal("L before Afirst original lastZ after R", page.Document.GetElementById("Parent")!.TextContent);
Assert.Same(page.Document, page.Document.GetElementById("Before")!.Owner);
}
[Fact]
public void FragmentContextHandlesTableSelectAndSvg()
{
var page = new Page();
page.Run("""
var table=document.createElement('table'),select=document.createElement('select'),svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
document.body.append(table,select,svg);
table.insertAdjacentHTML('beforeend','<tr><td>cell</td></tr>');
select.insertAdjacentHTML('beforeend','<option value="x">choice</option>');
svg.insertAdjacentHTML('afterbegin','<circle id="Circle" cx="4"/>');
if(table.firstElementChild.tagName!=='TBODY'||table.querySelector('td').textContent!=='cell')throw Error('table');
if(select.firstElementChild.tagName!=='OPTION'||svg.firstElementChild.namespaceURI!=='http://www.w3.org/2000/svg')throw Error('context:'+select.firstElementChild.tagName+':'+svg.namespaceURI+':'+svg.firstElementChild.namespaceURI);
""");
Assert.Same(page.Document, page.Document.GetElementById("Circle")!.Owner);
}
[Fact]
public void ParsedScriptsStayInertAndOnlyNewLinksNotifyResourceOwner()
{
var resources = new List<IElement>(); var mutations = 0;
var page = new Page(host: new() { ResourceRequested = resources.Add, Mutated = () => ++mutations });
page.Run("""
var changes=[];new MutationObserver(records=>changes.push(...records.map(record=>record.type+':'+record.addedNodes.length))).observe(document.body,{subtree:true,childList:true});
document.getElementById('Target').insertAdjacentHTML('beforeend','<script src="Inert.js">globalThis.executed=true</script><link rel="stylesheet" href="New.css"><em>tail</em>');
""");
Assert.Equal(1, mutations); Assert.Single(resources); Assert.Equal("link", resources[0].LocalName);
Assert.Equal("childList:3", page.Value("changes.join(',')"));
Assert.Equal("undefined", page.Value("typeof executed"));
}
[Fact]
public void ElementMovesKeepIdentityAndReturnInsertedElementAndTextStaysLiteral()
{
var page = new Page();
page.Run("""
var target=document.getElementById('Target'),child=document.createElement('strong');child.textContent='move';document.body.append(child);
if(target.insertAdjacentElement('afterbegin',child)!==child||target.firstChild!==child)throw Error('identity');
if(target.insertAdjacentText('beforeend','<b>literal</b>')!==undefined)throw Error('text return');
target.insertAdjacentElement('afterend',child);
if(target.nextSibling!==child||child.ownerDocument!==document)throw Error('move');
""");
Assert.Equal("original<b>literal</b>", page.Document.GetElementById("Target")!.TextContent);
Assert.Null(page.Document.GetElementById("Target")!.QuerySelector("b"));
}
[Fact]
public void InvalidPositionsAndDetachedParentBehaviorAreDistinctAndCatchable()
{
var page = new Page();
Assert.Equal("SyntaxError:NoModificationAllowedError:NoModificationAllowedError:TypeError:true", page.Value("""
var names=[],detached=document.createElement('div');
try{detached.insertAdjacentHTML(' beforeend','x')}catch(error){names.push(error.name)}
try{detached.insertAdjacentHTML('beforebegin','x')}catch(error){names.push(error.name)}
try{document.documentElement.insertAdjacentHTML('afterend','x')}catch(error){names.push(error.name)}
try{detached.insertAdjacentElement('afterbegin',document.createTextNode('x'))}catch(error){names.push(error.name)}
names.push(detached.insertAdjacentElement('beforebegin',document.createElement('span'))===null&&detached.insertAdjacentText('afterend','x')===undefined&&detached.childNodes.length===0);
names.join(':')
"""));
}
[Fact]
public void CompleteFragmentNodeBudgetFailureDoesNotMutateOrQueueResources()
{
var mutations = 0; var resources = 0;
var page = new Page(new() { MaximumDomNodes = 18 }, new() { Mutated = () => ++mutations, ResourceRequested = _ => ++resources });
page.Run("var target=document.getElementById('Target'),original=target.firstChild;");
Assert.False(page.Runtime.Evaluate("target.insertAdjacentHTML('afterbegin','<link href=\"Blocked.css\">'+'<i>x</i>'.repeat(20))").Success);
Assert.Equal(0, mutations); Assert.Equal(0, resources);
Assert.Equal("true:original", page.Value("(original===target.firstChild)+':'+target.textContent"));
page.Run("target.insertAdjacentText('beforeend',' ok');");
Assert.Equal("original ok", page.Document.GetElementById("Target")!.TextContent);
}
[Fact]
public void CharacterAndActualInsertionDepthAreCheckedBeforeMutation()
{
var page = new Page(new() { MaximumHtmlCharacters = 100, MaximumDomDepth = 6 });
Assert.False(page.Runtime.Evaluate("document.getElementById('Target').insertAdjacentHTML('beforeend','x'.repeat(90))").Success);
Assert.False(page.Runtime.Evaluate("document.getElementById('Target').insertAdjacentHTML('afterbegin','<a><b><c>deep</c></b></a>')").Success);
Assert.Equal("original", page.Document.GetElementById("Target")!.TextContent);
page.Run("document.getElementById('Target').insertAdjacentHTML('beforeend','<i>ok</i>');");
Assert.Equal("originalok", page.Document.GetElementById("Target")!.TextContent);
}
[Fact]
public void ElementDepthBudgetFailureRestoresItsOldParentAndIdentity()
{
var page = new Page(new() { MaximumDomDepth = 6 });
page.Run("var old=document.createElement('aside'),child=document.createElement('b');child.textContent='old';old.append(child);document.body.append(old);var target=document.getElementById('Target');");
Assert.False(page.Runtime.Evaluate("target.insertAdjacentElement('beforeend',old)").Success);
Assert.Equal("true:true:old:original", page.Value("(old.parentNode===document.body)+':'+(child.parentNode===old)+':'+old.textContent+':'+target.textContent"));
}
private sealed class Page
{
public Page(BrowserLimits? limits = null, BrowserDomHost? host = null)
{
limits ??= new(); Runtime = new(limits);
Document = new HtmlParser().ParseDocument("<!doctype html><html><head></head><body><div id='Parent'>L<div id='Target'>original</div>R</div></body></html>");
_ = new DomBridge(Runtime, Document, new("https://example.test/Page"), limits, host);
}
public JavaScriptRuntime Runtime { get; }
public IDocument Document { get; }
public void Run(string source) { var result = Runtime.Evaluate(source); Assert.True(result.Success, result.Error); }
public string Value(string source) { var result = Runtime.Evaluate(source); Assert.True(result.Success, result.Error); return result.Value; }
}
}
using AngleSharp.Dom;
using AngleSharp.Html.Parser;
namespace XFESystem.Browser.Tests;
public sealed class BrowserAdjacentTests
{
[Theory]
[InlineData("beforebegin", "LmoveoriginalR", "original")]
[InlineData("afterbegin", "LmoveoriginalR", "moveoriginal")]
[InlineData("beforeend", "LoriginalmoveR", "originalmove")]
[InlineData("afterend", "LoriginalmoveR", "original")]
public void ElementAndTextSupportAllFourLocations(string position, string parentText, string targetText)
{
foreach (var method in new[] { "Element", "Text" })
{
var page = new Page();
page.Run("var target=document.getElementById('Target'),moving=document.createElement('span');moving.textContent='move';document.body.append(moving);");
page.Run(method == "Element" ?
$"if(target.insertAdjacentElement('{position}',moving)!==moving)throw Error('return');" :
$"if(target.insertAdjacentText('{position}','move')!==undefined)throw Error('return');");
Assert.Equal(parentText, page.Document.GetElementById("Parent")!.TextContent);
Assert.Equal(targetText, page.Document.GetElementById("Target")!.TextContent);
}
}
[Fact]
public void HtmlFourPositionsPreserveSiblingsWhitespaceAndWrappedIdentity()
{
var page = new Page();
page.Run("""
const target=document.getElementById('Target'), original=target.firstChild;
target.insertAdjacentHTML('BEFOREBEGIN',' <span id="Before">before</span> ');
target.insertAdjacentHTML('afterbegin','A<i id="First">first</i> ');
target.insertAdjacentHTML('beforeend',' <b id="Last">last</b>Z');
target.insertAdjacentHTML('afterend',' <span id="After">after</span> ');
if(target!==document.getElementById('Target')||original!==target.childNodes.item(3))throw Error('identity');
if(target.insertAdjacentHTML('beforeend','')!==undefined)throw Error('return');
""");
Assert.Equal("Afirst original lastZ", page.Document.GetElementById("Target")!.TextContent);
Assert.Equal("L before Afirst original lastZ after R", page.Document.GetElementById("Parent")!.TextContent);
Assert.Same(page.Document, page.Document.GetElementById("Before")!.Owner);
}
[Fact]
public void FragmentContextHandlesTableSelectAndSvg()
{
var page = new Page();
page.Run("""
var table=document.createElement('table'),select=document.createElement('select'),svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
document.body.append(table,select,svg);
table.insertAdjacentHTML('beforeend','<tr><td>cell</td></tr>');
select.insertAdjacentHTML('beforeend','<option value="x">choice</option>');
svg.insertAdjacentHTML('afterbegin','<circle id="Circle" cx="4"/>');
if(table.firstElementChild.tagName!=='TBODY'||table.querySelector('td').textContent!=='cell')throw Error('table');
if(select.firstElementChild.tagName!=='OPTION'||svg.firstElementChild.namespaceURI!=='http://www.w3.org/2000/svg')throw Error('context:'+select.firstElementChild.tagName+':'+svg.namespaceURI+':'+svg.firstElementChild.namespaceURI);
""");
Assert.Same(page.Document, page.Document.GetElementById("Circle")!.Owner);
}
[Fact]
public void ParsedScriptsStayInertAndOnlyNewLinksNotifyResourceOwner()
{
var resources = new List<IElement>(); var mutations = 0;
var page = new Page(host: new() { ResourceRequested = resources.Add, Mutated = () => ++mutations });
page.Run("""
var changes=[];new MutationObserver(records=>changes.push(...records.map(record=>record.type+':'+record.addedNodes.length))).observe(document.body,{subtree:true,childList:true});
document.getElementById('Target').insertAdjacentHTML('beforeend','<script src="Inert.js">globalThis.executed=true</script><link rel="stylesheet" href="New.css"><em>tail</em>');
""");
Assert.Equal(1, mutations); Assert.Single(resources); Assert.Equal("link", resources[0].LocalName);
Assert.Equal("childList:3", page.Value("changes.join(',')"));
Assert.Equal("undefined", page.Value("typeof executed"));
}
[Fact]
public void ElementMovesKeepIdentityAndReturnInsertedElementAndTextStaysLiteral()
{
var page = new Page();
page.Run("""
var target=document.getElementById('Target'),child=document.createElement('strong');child.textContent='move';document.body.append(child);
if(target.insertAdjacentElement('afterbegin',child)!==child||target.firstChild!==child)throw Error('identity');
if(target.insertAdjacentText('beforeend','<b>literal</b>')!==undefined)throw Error('text return');
target.insertAdjacentElement('afterend',child);
if(target.nextSibling!==child||child.ownerDocument!==document)throw Error('move');
""");
Assert.Equal("original<b>literal</b>", page.Document.GetElementById("Target")!.TextContent);
Assert.Null(page.Document.GetElementById("Target")!.QuerySelector("b"));
}
[Fact]
public void InvalidPositionsAndDetachedParentBehaviorAreDistinctAndCatchable()
{
var page = new Page();
Assert.Equal("SyntaxError:NoModificationAllowedError:NoModificationAllowedError:TypeError:true", page.Value("""
var names=[],detached=document.createElement('div');
try{detached.insertAdjacentHTML(' beforeend','x')}catch(error){names.push(error.name)}
try{detached.insertAdjacentHTML('beforebegin','x')}catch(error){names.push(error.name)}
try{document.documentElement.insertAdjacentHTML('afterend','x')}catch(error){names.push(error.name)}
try{detached.insertAdjacentElement('afterbegin',document.createTextNode('x'))}catch(error){names.push(error.name)}
names.push(detached.insertAdjacentElement('beforebegin',document.createElement('span'))===null&&detached.insertAdjacentText('afterend','x')===undefined&&detached.childNodes.length===0);
names.join(':')
"""));
}
[Fact]
public void CompleteFragmentNodeBudgetFailureDoesNotMutateOrQueueResources()
{
var mutations = 0; var resources = 0;
var page = new Page(new() { MaximumDomNodes = 18 }, new() { Mutated = () => ++mutations, ResourceRequested = _ => ++resources });
page.Run("var target=document.getElementById('Target'),original=target.firstChild;");
Assert.False(page.Runtime.Evaluate("target.insertAdjacentHTML('afterbegin','<link href=\"Blocked.css\">'+'<i>x</i>'.repeat(20))").Success);
Assert.Equal(0, mutations); Assert.Equal(0, resources);
Assert.Equal("true:original", page.Value("(original===target.firstChild)+':'+target.textContent"));
page.Run("target.insertAdjacentText('beforeend',' ok');");
Assert.Equal("original ok", page.Document.GetElementById("Target")!.TextContent);
}
[Fact]
public void CharacterAndActualInsertionDepthAreCheckedBeforeMutation()
{
var page = new Page(new() { MaximumHtmlCharacters = 100, MaximumDomDepth = 6 });
Assert.False(page.Runtime.Evaluate("document.getElementById('Target').insertAdjacentHTML('beforeend','x'.repeat(90))").Success);
Assert.False(page.Runtime.Evaluate("document.getElementById('Target').insertAdjacentHTML('afterbegin','<a><b><c>deep</c></b></a>')").Success);
Assert.Equal("original", page.Document.GetElementById("Target")!.TextContent);
page.Run("document.getElementById('Target').insertAdjacentHTML('beforeend','<i>ok</i>');");
Assert.Equal("originalok", page.Document.GetElementById("Target")!.TextContent);
}
[Fact]
public void ElementDepthBudgetFailureRestoresItsOldParentAndIdentity()
{
var page = new Page(new() { MaximumDomDepth = 6 });
page.Run("var old=document.createElement('aside'),child=document.createElement('b');child.textContent='old';old.append(child);document.body.append(old);var target=document.getElementById('Target');");
Assert.False(page.Runtime.Evaluate("target.insertAdjacentElement('beforeend',old)").Success);
Assert.Equal("true:true:old:original", page.Value("(old.parentNode===document.body)+':'+(child.parentNode===old)+':'+old.textContent+':'+target.textContent"));
}
private sealed class Page
{
public Page(BrowserLimits? limits = null, BrowserDomHost? host = null)
{
limits ??= new(); Runtime = new(limits);
Document = new HtmlParser().ParseDocument("<!doctype html><html><head></head><body><div id='Parent'>L<div id='Target'>original</div>R</div></body></html>");
_ = new DomBridge(Runtime, Document, new("https://example.test/Page"), limits, host);
}
public JavaScriptRuntime Runtime { get; }
public IDocument Document { get; }
public void Run(string source) { var result = Runtime.Evaluate(source); Assert.True(result.Success, result.Error); }
public string Value(string source) { var result = Runtime.Evaluate(source); Assert.True(result.Success, result.Error); return result.Value; }
}
}