diff --git a/moli-renderer-v8/src/native_bridge/document/queries/xpath/result.rs b/moli-renderer-v8/src/native_bridge/document/queries/xpath/result.rs index 6d96fefe297..13ae0c317b7 100644 --- a/moli-renderer-v8/src/native_bridge/document/queries/xpath/result.rs +++ b/moli-renderer-v8/src/native_bridge/document/queries/xpath/result.rs @@ -49,7 +49,7 @@ struct XPathResultBaseDeclaration<'scope> { } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::XPathResult, enumerable)] +#[webapi(interface = web_api_interfaces::XPathResult, enumerable, receiver)] struct XPathResultPrototypeDeclaration { #[webapi(accessor_property, getter = xpath_result_result_type_getter_callback)] result_type: (), @@ -67,7 +67,7 @@ struct XPathResultPrototypeDeclaration { snapshot_length: (), #[webapi(method, length = 0, callback = xpath_result_iterate_next_callback)] iterate_next: (), - #[webapi(method, length = 0, callback = xpath_result_snapshot_item_callback)] + #[webapi(method, length = 1, callback = xpath_result_snapshot_item_callback)] snapshot_item: (), } @@ -133,12 +133,24 @@ pub(super) fn install_xpath_result_template_bindings<'s>( XPathResultPrototypeDeclaration::initialize_prototype_template(scope, prototype); } -fn set_xpath_result_slot( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, +fn xpath_result_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, name: &'static str, - value: v8::Local<'_, v8::Value>, +) -> Option> { + let object = moli_webapi_declare::web_api_object_target(scope, object)?; + get_private_value(scope, object, name) +} + +fn set_xpath_result_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + name: &'static str, + value: v8::Local<'s, v8::Value>, ) { + let Some(object) = moli_webapi_declare::web_api_object_target(scope, object) else { + return; + }; set_private_value(scope, object, name, value); } @@ -234,7 +246,7 @@ fn xpath_result_type<'s>( scope: &mut v8::PinScope<'s, '_>, result: v8::Local<'s, v8::Object>, ) -> u32 { - get_private_value(scope, result, XPATH_RESULT_TYPE_SLOT) + xpath_result_slot(scope, result, XPATH_RESULT_TYPE_SLOT) .and_then(|value| value.uint32_value(scope)) .unwrap_or(XPATH_ANY_TYPE) } @@ -268,7 +280,7 @@ fn xpath_result_baseline_query_version<'s>( scope: &mut v8::PinScope<'s, '_>, result: v8::Local<'s, v8::Object>, ) -> Option { - let value = get_private_value(scope, result, XPATH_RESULT_QUERY_VERSION_SLOT)?; + let value = xpath_result_slot(scope, result, XPATH_RESULT_QUERY_VERSION_SLOT)?; let big = v8::Local::::try_from(value).ok()?; let (version, _lossless) = big.u64_value(); Some(version) @@ -278,7 +290,7 @@ fn xpath_result_runtime_ptr<'s>( scope: &mut v8::PinScope<'s, '_>, result: v8::Local<'s, v8::Object>, ) -> Option<*mut JsContextHost> { - let value = get_private_value(scope, result, XPATH_RESULT_RUNTIME_SLOT)?; + let value = xpath_result_slot(scope, result, XPATH_RESULT_RUNTIME_SLOT)?; let external = v8::Local::::try_from(value).ok()?; let runtime_ptr = external.value() as *mut JsContextHost; (!runtime_ptr.is_null()).then_some(runtime_ptr) @@ -288,7 +300,7 @@ fn xpath_result_object_tree_root<'s>( scope: &mut v8::PinScope<'s, '_>, result: v8::Local<'s, v8::Object>, ) -> Option> { - get_private_value(scope, result, XPATH_RESULT_OBJECT_TREE_ROOT_SLOT) + xpath_result_slot(scope, result, XPATH_RESULT_OBJECT_TREE_ROOT_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) } @@ -340,7 +352,7 @@ fn xpath_result_number_value_getter_callback<'s>( xpath_result_throw_type_error(scope, "The result type is not a number."); return; } - if let Some(value) = get_private_value(scope, result, XPATH_RESULT_NUMBER_VALUE_SLOT) { + if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_NUMBER_VALUE_SLOT) { rv.set(value); } } @@ -355,7 +367,7 @@ fn xpath_result_string_value_getter_callback<'s>( xpath_result_throw_type_error(scope, "The result type is not a string."); return; } - if let Some(value) = get_private_value(scope, result, XPATH_RESULT_STRING_VALUE_SLOT) { + if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_STRING_VALUE_SLOT) { rv.set(value); } } @@ -370,7 +382,7 @@ fn xpath_result_boolean_value_getter_callback<'s>( xpath_result_throw_type_error(scope, "The result type is not a boolean."); return; } - if let Some(value) = get_private_value(scope, result, XPATH_RESULT_BOOLEAN_VALUE_SLOT) { + if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_BOOLEAN_VALUE_SLOT) { rv.set(value); } } @@ -385,7 +397,7 @@ fn xpath_result_single_node_value_getter_callback<'s>( xpath_result_throw_type_error(scope, "The result type is not a single node."); return; } - if let Some(value) = get_private_value(scope, result, XPATH_RESULT_SINGLE_NODE_VALUE_SLOT) { + if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_SINGLE_NODE_VALUE_SLOT) { rv.set(value); } } @@ -400,7 +412,7 @@ fn xpath_result_snapshot_length_getter_callback<'s>( xpath_result_throw_type_error(scope, "The result type is not a snapshot."); return; } - if let Some(value) = get_private_value(scope, result, XPATH_RESULT_SNAPSHOT_LENGTH_SLOT) { + if let Some(value) = xpath_result_slot(scope, result, XPATH_RESULT_SNAPSHOT_LENGTH_SLOT) { rv.set(value); } } @@ -516,13 +528,13 @@ fn xpath_result_iterate_next_callback<'s>( ); return; } - let Some(nodes) = get_private_value(scope, result, XPATH_RESULT_NODES_SLOT) + let Some(nodes) = xpath_result_slot(scope, result, XPATH_RESULT_NODES_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) else { rv.set_null(); return; }; - let index = get_private_value(scope, result, XPATH_RESULT_INDEX_SLOT) + let index = xpath_result_slot(scope, result, XPATH_RESULT_INDEX_SLOT) .and_then(|value| value.uint32_value(scope)) .unwrap_or(0); if index >= nodes.length() { @@ -547,19 +559,19 @@ fn xpath_result_snapshot_item_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, ) { let result = args.this(); + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; if !xpath_result_type_is_snapshot(xpath_result_type(scope, result)) { xpath_result_throw_type_error(scope, "The result type is not a snapshot."); return; } - let Some(nodes) = get_private_value(scope, result, XPATH_RESULT_NODES_SLOT) + let Some(nodes) = xpath_result_slot(scope, result, XPATH_RESULT_NODES_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) else { rv.set_null(); return; }; - let Some(parsed) = webidl::parse_args::(scope, &args) else { - return; - }; if parsed.index >= nodes.length() { rv.set_null(); return; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index e3898e136a7..6ee9eae6fbc 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -85,3 +85,4 @@ mod video_codec_support; mod event_prototype_attributes; mod named_node_map_receivers; +mod xpath_result_bindings; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/xpath_result_bindings.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/xpath_result_bindings.rs new file mode 100644 index 00000000000..06d0ae3a989 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/xpath_result_bindings.rs @@ -0,0 +1,150 @@ +use super::*; + +#[test] +fn xpath_result_members_validate_receivers_and_convert_indices_before_algorithms() { + let mut vm = new_parsed_test_vm( + "https://xpath-result-bindings.test/", + "", + ); + let result = vm + .eval(include_str!( + "../../../../tests/fixtures/xpath-result-native-bindings.js" + )) + .expect("XPathResult binding matrix should complete"); + assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "3240"); + assert_eq!( + result, + "true", + "{}", + vm.eval("JSON.stringify(__uiEventResults.rows.filter(row => !row.passed))") + .unwrap() + ); +} + +#[test] +fn xpath_result_registered_native_aliases_share_values_cursor_and_mutation_state() { + let mut vm = new_parsed_test_vm( + "https://xpath-result-native-aliases.test/", + "", + ); + vm.eval(r#" + globalThis.xpathProxyRecords = []; + globalThis.xpathProxyTraps = 0; + const other = document.querySelector('#child').contentWindow; + for (const owner of [globalThis, other]) for (const flavor of ['live', 'html', 'xml']) { + const doc = flavor === 'live' ? owner.document : flavor === 'html' ? + owner.document.implementation.createHTMLDocument('xpath') : owner.document.implementation.createDocument(null, 'r'); + for (let type=1;type<=9;type++) { + const root = doc.createElement('root'); + const a = root.appendChild(doc.createElement('a')); + const b = root.appendChild(doc.createElement('b')); + (doc.body || doc.documentElement).appendChild(root); + xpathProxyRecords.push({owner, doc, type, root, a, b, + callee: owner === globalThis ? other : globalThis, + handler: {get() {xpathProxyTraps++; throw Error('get');}, + getPrototypeOf() {xpathProxyTraps++; throw Error('getPrototypeOf');}}}); + } + } + // Materialize the trees before taking any iterator mutation baseline. + for (const record of xpathProxyRecords) { + const {doc,root,type} = record; + record.target = doc.evaluate(type===1?'7':type===2?"'value'":type===3?'true()':'child::*',root,null,type,null); + } + true; + "#).unwrap(); + vm.with_default_context_scope_and_checkpoint_for_test(|scope, _host_ptr| { + let global = scope.get_current_context().global(scope); + let key = v8::String::new(scope, "xpathProxyRecords").unwrap(); + let value = global.get(scope, key.into()).unwrap(); + let records = v8::Local::::try_from(value).unwrap(); + assert_eq!(records.length(), 54); + for index in 0..records.length() { + let value = records.get_index(scope, index).unwrap(); + let record = v8::Local::::try_from(value).unwrap(); + let key = v8::String::new(scope, "target").unwrap(); + let value = record.get(scope, key.into()).unwrap(); + let target = v8::Local::::try_from(value).unwrap(); + let target = moli_webapi_declare::web_api_object_target(scope, target).unwrap(); + let context = target.get_creation_context(scope).unwrap(); + let key = v8::String::new(scope, "handler").unwrap(); + let value = record.get(scope, key.into()).unwrap(); + let handler = v8::Local::::try_from(value).unwrap(); + for name in ["firstAlias", "secondAlias"] { + let proxy = { + let scope = &mut v8::ContextScope::new(scope, context); + let private_handler = crate::util::new_null_prototype_object(scope); + for trap in ["get", "getPrototypeOf"] { + let key = v8::String::new(scope, trap).unwrap(); + let value = handler.get(scope, key.into()).unwrap(); + assert!( + private_handler + .create_data_property(scope, key.into(), value) + .unwrap() + ); + } + let proxy = v8::Proxy::new(scope, target, private_handler).unwrap(); + moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap(); + proxy + }; + assert!(crate::web_api_interfaces::XPathResult::is_instance( + scope, + proxy.into() + )); + let key = v8::String::new(scope, name).unwrap(); + assert!(record.set(scope, key.into(), proxy.into()).unwrap()); + } + } + Ok(()) + }) + .unwrap(); + assert_eq!(vm.eval(r#"(() => { + for (const [index, record] of xpathProxyRecords.entries()) { + const {callee,target,firstAlias,secondAlias,type,a,b} = record; + const methods = callee.XPathResult.prototype; + const get = (name,receiver) => Object.getOwnPropertyDescriptor(methods,name).get.call(receiver); + const check = (value,name) => {if (!value) throw Error(`${index}/${type}: ${name}`);}; + const typeError = body => {try {body();return false;} catch(error) {return Object.getPrototypeOf(error) === callee.TypeError.prototype;}}; + for (const receiver of [target,firstAlias,secondAlias]) { + check(get('resultType',receiver) === type && !get('invalidIteratorState',receiver), 'initial state'); + if (type===1) check(get('numberValue',receiver) === 7, 'number'); + if (type===2) check(get('stringValue',receiver) === 'value', 'string'); + if (type===3) check(get('booleanValue',receiver) === true, 'boolean'); + if (type===8 || type===9) check(get('singleNodeValue',receiver) === a, 'single node'); + if (type===6 || type===7) check(get('snapshotLength',receiver) === 2 && methods.snapshotItem.call(receiver,0) === a && methods.snapshotItem.call(receiver,1) === b, 'snapshot'); + } + if (type===4 || type===5) check(methods.iterateNext.call(firstAlias) === a && + methods.iterateNext.call(target) === b && methods.iterateNext.call(secondAlias) === null && + methods.iterateNext.call(firstAlias) === null, 'shared iterator cursor'); + let converted = 0;const sentinel = {}; + try {methods.snapshotItem.call(firstAlias,{valueOf(){converted++;throw sentinel;}});throw Error('missing exception');} + catch(error) {check(error === sentinel && converted === 1,'index conversion before result kind');} + const author = new Proxy(firstAlias,{}); + for (const member of ['resultType','invalidIteratorState','numberValue','stringValue','booleanValue','singleNodeValue','snapshotLength']) + check(typeError(() => get(member,author)), 'author alias ' + member); + converted = 0; + check(typeError(() => methods.snapshotItem.call(author,{valueOf(){converted++;return 0;}})) && converted === 0, 'author alias before conversion'); + check(xpathProxyTraps === 0, 'no author traps'); + } + // Mutations invalidate a whole Document. Test all cursors before mutating + // any tree so another record's legitimate invalidation cannot mask a bug. + for (const [index, record] of xpathProxyRecords.entries()) { + const {callee,target,firstAlias,secondAlias,type,root,a,b} = record; + if (type<4 || type>7) continue; + root.setAttribute('mutation','changed'); + const methods = callee.XPathResult.prototype; + const get = (name,receiver) => Object.getOwnPropertyDescriptor(methods,name).get.call(receiver); + for (const receiver of [target,firstAlias,secondAlias]) { + if (type===4 || type===5) { + if (!get('invalidIteratorState',receiver)) throw Error(`${index}: missing invalidation`); + let error;try {methods.iterateNext.call(receiver);} catch(caught) {error=caught;} + if (error?.name !== 'InvalidStateError' || error.code !== 11 || Object.getPrototypeOf(error) !== callee.DOMException.prototype) + throw Error(`${index}: invalid iterator error`); + } else if (get('invalidIteratorState',receiver) || get('snapshotLength',receiver) !== 2 || + methods.snapshotItem.call(receiver,0) !== a || methods.snapshotItem.call(receiver,1) !== b) + throw Error(`${index}: mutated snapshot changed`); + } + } + for (const record of xpathProxyRecords) record.root.remove(); + return true; + })()"#).unwrap(), "true"); +} diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/xpath_and_selectors.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/xpath_and_selectors.rs index 73527fbc2ec..6e21a36886e 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/xpath_and_selectors.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/xpath_and_selectors.rs @@ -637,7 +637,7 @@ fn document_xpath_result_uses_browser_like_object_shape() { assert_eq!( result, - r#"{"constructorType":"function","constructorName":"XPathResult","constructorLength":0,"illegalConstructor":"throw:TypeError","tag":"[object XPathResult]","instanceofXPathResult":true,"prototypeMatch":true,"constructorOnPrototype":true,"ownKeys":[],"ownIterateNext":false,"ownResultType":false,"resultTypeValue":5,"invalidIteratorStateValue":false,"prototypeIterateNext":["function","iterateNext",0,true,true,true],"prototypeSnapshotItem":["function","snapshotItem",0,true,true,true],"prototypeToStringTag":["XPathResult",false,true,false],"prototypeResultTypePresent":true,"constructorConstantsEnumerable":true,"prototypeConstantsEnumerable":true,"prototypeMethodsEnumerable":[true,true],"constructorConstant":5,"prototypeConstant":5,"windowEnumerable":false}"# + r#"{"constructorType":"function","constructorName":"XPathResult","constructorLength":0,"illegalConstructor":"throw:TypeError","tag":"[object XPathResult]","instanceofXPathResult":true,"prototypeMatch":true,"constructorOnPrototype":true,"ownKeys":[],"ownIterateNext":false,"ownResultType":false,"resultTypeValue":5,"invalidIteratorStateValue":false,"prototypeIterateNext":["function","iterateNext",0,true,true,true],"prototypeSnapshotItem":["function","snapshotItem",1,true,true,true],"prototypeToStringTag":["XPathResult",false,true,false],"prototypeResultTypePresent":true,"constructorConstantsEnumerable":true,"prototypeConstantsEnumerable":true,"prototypeMethodsEnumerable":[true,true],"constructorConstant":5,"prototypeConstant":5,"windowEnumerable":false}"# ); } @@ -717,7 +717,7 @@ fn document_xpath_result_slots_ignore_reflection_and_spoofing() { snapshotLength: snapshot.snapshotLength, snapshotFirst: snapshot.snapshotItem(0).id, numberValue: number.numberValue, - fakeResultType: Object.getOwnPropertyDescriptor(proto, "resultType").get.call(fake), + fakeResultType: probe(() => Object.getOwnPropertyDescriptor(proto, "resultType").get.call(fake)), fakeStringValue: probe(() => Object.getOwnPropertyDescriptor(proto, "stringValue").get.call(fake)), fakeSnapshotItem: probe(() => proto.snapshotItem.call(fake, 0)) }); @@ -728,7 +728,7 @@ fn document_xpath_result_slots_ignore_reflection_and_spoofing() { assert_eq!( result, - r#"{"internalNamesBefore":{"iterator":[],"snapshot":[],"number":[]},"iteratorType":5,"iteratorInvalid":false,"iteratorNext":"first","snapshotLength":2,"snapshotFirst":"first","numberValue":2,"fakeResultType":0,"fakeStringValue":"throw:TypeError","fakeSnapshotItem":"throw:TypeError"}"# + r#"{"internalNamesBefore":{"iterator":[],"snapshot":[],"number":[]},"iteratorType":5,"iteratorInvalid":false,"iteratorNext":"first","snapshotLength":2,"snapshotFirst":"first","numberValue":2,"fakeResultType":"throw:TypeError","fakeStringValue":"throw:TypeError","fakeSnapshotItem":"throw:TypeError"}"# ); } diff --git a/moli-renderer-v8/tests/fixtures/xpath-result-native-bindings.js b/moli-renderer-v8/tests/fixtures/xpath-result-native-bindings.js new file mode 100644 index 00000000000..0e3ed27a4df --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/xpath-result-native-bindings.js @@ -0,0 +1,94 @@ +(() => { + const rows = []; + let traps = 0; + const check = (name, body) => { + traps = 0; + try {if (!body()) throw Error('false'); rows.push({name,passed:true});} + catch(error) {rows.push({name,passed:false,error:String(error)});} + }; + const other = document.querySelector('#child').contentWindow; + const attributes = ['resultType','invalidIteratorState','numberValue','stringValue','booleanValue','singleNodeValue','snapshotLength']; + for (const [ci,callee] of [globalThis,other].entries()) for (const [oi,owner] of [globalThis,other].entries()) + for (const flavor of ['live','html','xml']) { + const doc = flavor === 'live' ? owner.document : flavor === 'html' ? + owner.document.implementation.createHTMLDocument('xpath') : owner.document.implementation.createDocument(null,'r'); + const root = doc.createElement('root'), a = doc.createElement('a'), b = doc.createElement('b'); + root.appendChild(a); root.appendChild(b); (doc.body || doc.documentElement).appendChild(root); + const prefix = `${ci}/${oi}/${flavor}`, proto = callee.XPathResult.prototype; + const descriptor = name => Object.getOwnPropertyDescriptor(proto,name); + const typeError = body => {try {body();return false;} catch(error) {return Object.getPrototypeOf(error) === callee.TypeError.prototype;}}; + const invalidState = body => {try {body();return false;} catch(error) {return error.name === 'InvalidStateError' && error.code === 11 && Object.getPrototypeOf(error) === callee.DOMException.prototype;}}; + const evaluate = (expression,type,context=root) => doc.evaluate(expression,context,null,type,null); + const make = type => evaluate(type===1?'7':type===2?"'value'":type===3?'true()':'child::*',type); + const genuine = make(7); + const handler = {get() {traps++;throw Error('get trap');},getPrototypeOf() {traps++;throw Error('prototype trap');}}; + const revoked = Proxy.revocable(genuine,handler); revoked.revoke(); + const invalid = [null,undefined,0,true,'',Symbol(),{},Object.create(proto),Object.create(genuine),new Proxy(genuine,handler),revoked.proxy,root,new callee.Event('test')]; + for (const name of attributes.concat(['iterateNext','snapshotItem'])) { + for (const [ri,receiver] of invalid.entries()) check(`${prefix}/${name}/receiver/${ri}`,() => { + let converted = 0; + const index = {valueOf() {converted++;throw Error('index conversion');}}; + const body = attributes.includes(name) ? () => descriptor(name).get.call(receiver) : () => proto[name].call(receiver,index); + return typeError(body) && converted === 0 && traps === 0; + }); + check(`${prefix}/${name}/descriptor`,() => { + const d = descriptor(name); + if (!d.enumerable || !d.configurable) return false; + return attributes.includes(name) ? typeof d.get === 'function' && d.get.length === 0 && d.set === undefined : + d.writable && typeof d.value === 'function' && d.value.length === (name === 'snapshotItem' ? 1 : 0); + }); + } + for (let type=1;type<=9;type++) { + const result = make(type), name = `${prefix}/type/${type}`; + check(name+'/state',() => descriptor('resultType').get.call(result) === type && descriptor('invalidIteratorState').get.call(result) === false); + const supported = {numberValue:type===1,stringValue:type===2,booleanValue:type===3,singleNodeValue:type===8||type===9,snapshotLength:type===6||type===7}; + const expected = {numberValue:7,stringValue:'value',booleanValue:true,singleNodeValue:a,snapshotLength:2}; + for (const member of Object.keys(supported)) check(name+'/'+member,() => + supported[member] ? descriptor(member).get.call(result) === expected[member] : typeError(() => descriptor(member).get.call(result))); + if (type===4 || type===5) check(name+'/iteration',() => proto.iterateNext.call(result) === a && proto.iterateNext.call(result) === b && proto.iterateNext.call(result) === null && proto.iterateNext.call(result) === null); + else check(name+'/wrong-iterator',() => typeError(() => proto.iterateNext.call(result))); + if (type===6 || type===7) { + for (const [ii,index] of [0,1,2,-1,4294967296,4294967297,NaN,Infinity,undefined,null,-0,1.9].entries()) + check(name+`/index/${ii}`,() => proto.snapshotItem.call(result,index) === ([a,b,null,null,a,b,a,a,a,a,a,b][ii])); + } else check(name+'/wrong-snapshot',() => typeError(() => proto.snapshotItem.call(result,0))); + check(name+'/missing-index',() => typeError(() => proto.snapshotItem.call(result))); + check(name+'/throwing-index',() => { + const sentinel = {}; let count = 0; + try {proto.snapshotItem.call(result,{valueOf() {count++;throw sentinel;}}); return false;} + catch(error) {return error === sentinel && count === 1;} + }); + check(name+'/invalid-index',() => { + let count = 0; + return typeError(() => proto.snapshotItem.call(result,{valueOf() {count++;return Symbol();}})) && count === 1; + }); + check(name+'/reentrant-index',() => { + let count = 0; const inserted = doc.createElement('conversion'); + let value, error; + try {value = proto.snapshotItem.call(result,{valueOf() {count++;root.appendChild(inserted);return 0;}});} catch(caught) {error = caught;} + const ok = count === 1 && inserted.parentNode === root && + (type===6 || type===7 ? error === undefined && value === a : error && Object.getPrototypeOf(error) === callee.TypeError.prototype); + inserted.remove(); return ok; + }); + check(name+'/private-state',() => { + Object.defineProperty(result,'resultType',{configurable:true,get() {throw Error('public resultType');}}); + Object.defineProperty(result,'invalidIteratorState',{configurable:true,get() {throw Error('public invalidIteratorState');}}); + const ok = descriptor('resultType').get.call(result) === type; + delete result.resultType; delete result.invalidIteratorState; return ok; + }); + } + for (const type of [4,5,6,7]) check(`${prefix}/mutation/${type}`,() => { + const result = make(type), was = descriptor('invalidIteratorState').get.call(result); + const initial = type===4 || type===5 ? proto.iterateNext.call(result) : proto.snapshotItem.call(result,0); + a.setAttribute('changed',String(type)); + if (type===4 || type===5) return !was && initial === a && descriptor('invalidIteratorState').get.call(result) && invalidState(() => proto.iterateNext.call(result)); + return !was && initial === a && !descriptor('invalidIteratorState').get.call(result) && descriptor('snapshotLength').get.call(result) === 2 && proto.snapshotItem.call(result,0) === a; + }); + check(`${prefix}/snapshot/removal`,() => { + const snapshot = make(7); a.remove(); + return descriptor('snapshotLength').get.call(snapshot) === 2 && proto.snapshotItem.call(snapshot,0) === a && proto.snapshotItem.call(snapshot,1) === b && !descriptor('invalidIteratorState').get.call(snapshot); + }); + root.remove(); + } + globalThis.__uiEventResults = {complete:true,total:rows.length,passed:rows.filter(row=>row.passed).length,rows}; + return rows.every(row=>row.passed); +})()