using AngleSharp.Html.Parser;
namespace XFESystem.Browser.Tests;
public sealed class BrowserDatasetTests
{
[Fact]
public void ConsoleCallbacksAreCallableWithStandardFunctionBindAndCall()
{
Run("console.log.bind(console)('bound');console.warn.call(console,'called');");
}
[Fact]
public void BoundJavaScriptMethodsCanBeBoundAgain()
{
Run("class C { constructor(){this.value=42;this.method=this.method.bind(this);this.second=this.method.bind(this);}method(){return this.value;}} if(new C().second()!==42)throw Error('bind twice');");
}
[Fact]
public void DatasetReflectsExistingAttributesAndStaysTheSameObject()
{
Run("""
const element=document.querySelector('div'),data=element.dataset;
if(data!==element.dataset||!(data instanceof DOMStringMap)||Object.prototype.toString.call(data)!=='[object DOMStringMap]')throw Error('identity');
if(data.fooBar!=='one'||data.empty!==''||data.missing!==undefined)throw Error('values');
element.setAttribute('data-foo-bar','two');if(data.fooBar!=='two')throw Error('attribute reflection');
""");
}
[Fact]
public void DatasetWritesAndDeletesActualCamelCaseAttributes()
{
var doc = Run("""
const element=document.querySelector('div');element.dataset.codeToolsApplied=true;element.dataset.fooBar=null;
if(element.getAttribute('data-code-tools-applied')!=='true'||element.getAttribute('data-foo-bar')!=='null')throw Error('set');
delete element.dataset.fooBar;if(element.hasAttribute('data-foo-bar')||'fooBar' in element.dataset)throw Error('delete');
element.dataset['number-1']='ok';element.dataset['foo-Bar']='dash';
if(element.getAttribute('data-number-1')!=='ok'||element.getAttribute('data-foo--bar')!=='dash')throw Error('special');
""");
Assert.Equal("true", doc.QuerySelector("div")!.GetAttribute("data-code-tools-applied"));
}
[Fact]
public void DatasetOwnKeysDescriptorsAndJsonReflectTheLiveAttributeCollection()
{
Run("""
const element=document.querySelector('div'),data=element.dataset;
if(Object.keys(data).join(',')!=='fooBar,empty')throw Error('keys');
const descriptor=Object.getOwnPropertyDescriptor(data,'fooBar');
if(!descriptor.enumerable||!descriptor.configurable||!descriptor.writable||descriptor.value!=='one')throw Error('descriptor');
element.setAttribute('data-new-key','three');if(Object.entries(data).length!==3||JSON.parse(JSON.stringify(data)).newKey!=='three')throw Error('live keys');
delete data.empty;if(Object.keys(data).join(',')!=='fooBar,newKey')throw Error('removed keys');
""");
}
[Fact]
public void DatasetPrototypeNamedPropertiesCannotChangeTheActualPrototype()
{
Run("""
const data=document.querySelector('div').dataset,prototype=Object.getPrototypeOf(data);
data.__proto__='safe';if(data.__proto__!=='safe'||Object.getPrototypeOf(data)!==prototype)throw Error('prototype');
if(!Object.hasOwn(data,'__proto__'))throw Error('own');delete data.__proto__;
data.constructor='text';if(data.constructor!=='text')throw Error('constructor');delete data.constructor;
if(data.constructor!==DOMStringMap)throw Error('prototype restoration');
""");
}
[Fact]
public void DatasetRejectsAmbiguousHyphenLowercaseNames()
{
Run("""
const element=document.querySelector('div');let name='';try{element.dataset['foo-bar']='bad';}catch(error){name=error.name;}
if(name!=='SyntaxError'||element.getAttribute('data-foo-bar')!=='one')throw Error('invalid name');
Object.defineProperty(element.dataset,'definedKey',{value:'defined',configurable:true,enumerable:true,writable:true});
if(element.getAttribute('data-defined-key')!=='defined')throw Error('define');
""");
}
private static AngleSharp.Dom.IDocument Run(string code)
{
var runtime = new JavaScriptRuntime();
var doc = new HtmlParser().ParseDocument("<div data-foo-bar='one' data-empty=''></div>");
_ = new DomBridge(runtime, doc, new("http://xfesystem.local/"), new());
var result = runtime.Evaluate(code);
Assert.True(result.Success, result.Error);
return doc;
}
}
using AngleSharp.Html.Parser;
namespace XFESystem.Browser.Tests;
public sealed class BrowserDatasetTests
{
[Fact]
public void ConsoleCallbacksAreCallableWithStandardFunctionBindAndCall()
{
Run("console.log.bind(console)('bound');console.warn.call(console,'called');");
}
[Fact]
public void BoundJavaScriptMethodsCanBeBoundAgain()
{
Run("class C { constructor(){this.value=42;this.method=this.method.bind(this);this.second=this.method.bind(this);}method(){return this.value;}} if(new C().second()!==42)throw Error('bind twice');");
}
[Fact]
public void DatasetReflectsExistingAttributesAndStaysTheSameObject()
{
Run("""
const element=document.querySelector('div'),data=element.dataset;
if(data!==element.dataset||!(data instanceof DOMStringMap)||Object.prototype.toString.call(data)!=='[object DOMStringMap]')throw Error('identity');
if(data.fooBar!=='one'||data.empty!==''||data.missing!==undefined)throw Error('values');
element.setAttribute('data-foo-bar','two');if(data.fooBar!=='two')throw Error('attribute reflection');
""");
}
[Fact]
public void DatasetWritesAndDeletesActualCamelCaseAttributes()
{
var doc = Run("""
const element=document.querySelector('div');element.dataset.codeToolsApplied=true;element.dataset.fooBar=null;
if(element.getAttribute('data-code-tools-applied')!=='true'||element.getAttribute('data-foo-bar')!=='null')throw Error('set');
delete element.dataset.fooBar;if(element.hasAttribute('data-foo-bar')||'fooBar' in element.dataset)throw Error('delete');
element.dataset['number-1']='ok';element.dataset['foo-Bar']='dash';
if(element.getAttribute('data-number-1')!=='ok'||element.getAttribute('data-foo--bar')!=='dash')throw Error('special');
""");
Assert.Equal("true", doc.QuerySelector("div")!.GetAttribute("data-code-tools-applied"));
}
[Fact]
public void DatasetOwnKeysDescriptorsAndJsonReflectTheLiveAttributeCollection()
{
Run("""
const element=document.querySelector('div'),data=element.dataset;
if(Object.keys(data).join(',')!=='fooBar,empty')throw Error('keys');
const descriptor=Object.getOwnPropertyDescriptor(data,'fooBar');
if(!descriptor.enumerable||!descriptor.configurable||!descriptor.writable||descriptor.value!=='one')throw Error('descriptor');
element.setAttribute('data-new-key','three');if(Object.entries(data).length!==3||JSON.parse(JSON.stringify(data)).newKey!=='three')throw Error('live keys');
delete data.empty;if(Object.keys(data).join(',')!=='fooBar,newKey')throw Error('removed keys');
""");
}
[Fact]
public void DatasetPrototypeNamedPropertiesCannotChangeTheActualPrototype()
{
Run("""
const data=document.querySelector('div').dataset,prototype=Object.getPrototypeOf(data);
data.__proto__='safe';if(data.__proto__!=='safe'||Object.getPrototypeOf(data)!==prototype)throw Error('prototype');
if(!Object.hasOwn(data,'__proto__'))throw Error('own');delete data.__proto__;
data.constructor='text';if(data.constructor!=='text')throw Error('constructor');delete data.constructor;
if(data.constructor!==DOMStringMap)throw Error('prototype restoration');
""");
}
[Fact]
public void DatasetRejectsAmbiguousHyphenLowercaseNames()
{
Run("""
const element=document.querySelector('div');let name='';try{element.dataset['foo-bar']='bad';}catch(error){name=error.name;}
if(name!=='SyntaxError'||element.getAttribute('data-foo-bar')!=='one')throw Error('invalid name');
Object.defineProperty(element.dataset,'definedKey',{value:'defined',configurable:true,enumerable:true,writable:true});
if(element.getAttribute('data-defined-key')!=='defined')throw Error('define');
""");
}
private static AngleSharp.Dom.IDocument Run(string code)
{
var runtime = new JavaScriptRuntime();
var doc = new HtmlParser().ParseDocument("<div data-foo-bar='one' data-empty=''></div>");
_ = new DomBridge(runtime, doc, new("http://xfesystem.local/"), new());
var result = runtime.Evaluate(code);
Assert.True(result.Success, result.Error);
return doc;
}
}