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',' before '); target.insertAdjacentHTML('afterbegin','Afirst '); target.insertAdjacentHTML('beforeend',' lastZ'); target.insertAdjacentHTML('afterend',' after '); 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','cell'); select.insertAdjacentHTML('beforeend',''); svg.insertAdjacentHTML('afterbegin',''); 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(); 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','tail'); """); 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','literal')!==undefined)throw Error('text return'); target.insertAdjacentElement('afterend',child); if(target.nextSibling!==child||child.ownerDocument!==document)throw Error('move'); """); Assert.Equal("originalliteral", 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',''+'x'.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','deep')").Success); Assert.Equal("original", page.Document.GetElementById("Target")!.TextContent); page.Run("document.getElementById('Target').insertAdjacentHTML('beforeend','ok');"); 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("
L
original
R
"); _ = 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; } } }