namespace XFESystem.Browser.Tests;
public sealed class BrowserCustomElementTests
{
[Fact]
public void ParsedAutonomousElementUpgradesInPlaceAndReceivesAttributesAndConnectionReactions()
{
var browser = new BrowserService(new Fixture("<sample-icon id='Icon' tone='blue'><span>existing child</span></sample-icon>")); 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("<body></body>")); 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("<body></body>")); 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("<body></body>")); 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("<button id='Button'>button</button>")); 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("<body></body>"); 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("<body></body>")); 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<Uri> Requests = [];
public WebResponse Get(Uri address, BrowserLimits limits) { Requests.Add(address); return new(address, 200, "text/html", html, true); }
}
}
namespace XFESystem.Browser.Tests;
public sealed class BrowserCustomElementTests
{
[Fact]
public void ParsedAutonomousElementUpgradesInPlaceAndReceivesAttributesAndConnectionReactions()
{
var browser = new BrowserService(new Fixture("<sample-icon id='Icon' tone='blue'><span>existing child</span></sample-icon>")); 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("<body></body>")); 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("<body></body>")); 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("<body></body>")); 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("<button id='Button'>button</button>")); 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("<body></body>"); 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("<body></body>")); 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<Uri> Requests = [];
public WebResponse Get(Uri address, BrowserLimits limits) { Requests.Add(address); return new(address, 200, "text/html", html, true); }
}
}