namespace XFESystem.Browser.Tests; public sealed class BrowserCustomElementTests { [Fact] public void ParsedAutonomousElementUpgradesInPlaceAndReceivesAttributesAndConnectionReactions() { var browser = new BrowserService(new Fixture("existing child")); browser.Navigate("https://page.test/"); Assert.Equal("true", browser.ExecuteJavaScript("const original=document.getElementById('Icon');let calls=[];class Icon extends HTMLElement{static observedAttributes=['tone'];constructor(){super();this.constructed=true;}attributeChangedCallback(name,old,value){calls.push('attribute:'+name+':'+old+':'+value)}connectedCallback(){calls.push('connected');this.setAttribute('ready','yes')}disconnectedCallback(){calls.push('disconnected')}}customElements.define('sample-icon',Icon);original===document.getElementById('Icon')&&original instanceof Icon&&original.constructed&&original.ownerDocument===document")); Assert.Equal("attribute:tone:null:blue|connected", browser.ExecuteJavaScript("calls.join('|')")); Assert.Equal("yes", browser.Document!.GetElementById("Icon")!.GetAttribute("ready")); browser.ExecuteJavaScript("original.setAttribute('tone','red');original.remove();document.body.appendChild(original)"); Assert.Equal("attribute:tone:null:blue|connected|attribute:tone:blue:red|disconnected|connected", browser.ExecuteJavaScript("calls.join('|')")); Assert.Contains("existing child", originalText(browser)); static string originalText(BrowserService page) => page.Document!.GetElementById("Icon")!.TextContent; } [Fact] public void NewAndCreateElementProduceRealNodesWithOneConstructorCallEach() { var browser = new BrowserService(new Fixture("")); browser.Navigate("https://page.test/"); Assert.Equal("2:true:true", browser.ExecuteJavaScript("let constructed=0;class Widget extends HTMLElement{constructor(){super();constructed++;this.textContent='actual widget'}}customElements.define('sample-widget',Widget);const a=new Widget(),b=document.createElement('sample-widget');document.body.append(a,b);constructed+':'+(a instanceof Widget)+':'+(b instanceof Widget)")); Assert.Equal(2, browser.Document!.QuerySelectorAll("sample-widget").Length); Assert.Contains(browser.Layout(new(400, 200)).Commands, command => command.Text.Contains("actual")); } [Fact] public void DetachedUpgradeAndWhenDefinedUseTheRegisteredConstructor() { var browser = new BrowserService(new Fixture("")); browser.Navigate("https://page.test/"); browser.ExecuteJavaScript("let resolved=false;const detached=document.createElement('late-widget');customElements.whenDefined('late-widget').then(value=>resolved=value===Late);class Late extends HTMLElement{constructor(){super();this.setAttribute('upgraded','yes')}}customElements.define('late-widget',Late);customElements.upgrade(detached)"); Assert.Equal("true:true:yes", browser.ExecuteJavaScript("resolved+':'+(customElements.get('late-widget')===Late)+':'+detached.getAttribute('upgraded')")); } [Fact] public void UnsupportedAndDuplicateDefinitionsFailWithoutPublishingFakeDefinitions() { var browser = new BrowserService(new Fixture("")); browser.Navigate("https://page.test/"); Assert.Equal("SyntaxError:NotSupportedError:NotSupportedError:false", browser.ExecuteJavaScript("let errors=[];for(const [name,options] of [['badname',{}],['some-button',{extends:'button'}]]){try{customElements.define(name,class extends HTMLElement{},options)}catch(e){errors.push(e.name)}}class One extends HTMLElement{}customElements.define('one-widget',One);try{customElements.define('one-widget',class extends HTMLElement{})}catch(e){errors.push(e.name)}errors.join(':')+':'+(customElements.get('some-button')!==undefined)")); } [Fact] public void AbortSignalRemovesActualDomListenersAndPreservesTheFirstReason() { var browser = new BrowserService(new Fixture("")); browser.Navigate("https://page.test/"); Assert.Equal("1:true:stop:true", browser.ExecuteJavaScript("const controller=new AbortController(),button=document.getElementById('Button');let clicks=0,aborts=0;controller.signal.addEventListener('abort',()=>aborts++);button.addEventListener('click',()=>clicks++,{signal:controller.signal});button.click();controller.abort('stop');controller.abort('other');button.click();let thrown=false;try{controller.signal.throwIfAborted()}catch(value){thrown=value==='stop'}clicks+':'+controller.signal.aborted+':'+controller.signal.reason+':'+thrown")); Assert.Equal("1", browser.ExecuteJavaScript("aborts")); } [Fact] public void AbortedQueuedFetchAndXmlHttpRequestDoNotPerformAHostRequest() { var transport = new Fixture(""); var browser = new BrowserService(transport); browser.Navigate("https://page.test/"); browser.ExecuteJavaScript("let cancelled='';const controller=new AbortController();fetch('/cancelled',{signal:controller.signal}).catch(reason=>cancelled=reason);controller.abort('cancelled');const request=new XMLHttpRequest();request.open('GET','/also-cancelled');request.send();request.abort()"); browser.AdvanceTime(10); Assert.Equal("cancelled", browser.ExecuteJavaScript("cancelled")); Assert.Single(transport.Requests); } [Fact] public void AbortTimeoutAndAnyUseRealTimersAndAbortEvents() { var browser = new BrowserService(new Fixture("")); browser.Navigate("https://page.test/"); browser.ExecuteJavaScript("const timed=AbortSignal.timeout(10),combined=AbortSignal.any([timed,new AbortController().signal]);let timedOut=false;combined.addEventListener('abort',()=>timedOut=combined.reason.name==='TimeoutError')"); browser.AdvanceTime(20); Assert.Equal("true:true", browser.ExecuteJavaScript("timed.aborted+':'+timedOut")); } private sealed class Fixture(string html) : IWebTransport { internal readonly List Requests = []; public WebResponse Get(Uri address, BrowserLimits limits) { Requests.Add(address); return new(address, 200, "text/html", html, true); } } }