mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 08:07:38 +00:00
fix(dom): convert MutationObserver options through WebIDL
This commit is contained in:
@@ -18,7 +18,7 @@ use moli_webapi_declare::WebApiFunctionTemplate;
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
#[webapi(interface = web_api_interfaces::MutationObserver, enumerable, receiver)]
|
||||
struct MutationObserverTemplateMethodsDeclaration {
|
||||
#[webapi(method, length = 2, callback = observer_runtime::mutation_observer_observe_callback)]
|
||||
#[webapi(method, length = 1, callback = observer_runtime::mutation_observer_observe_callback)]
|
||||
observe: (),
|
||||
|
||||
#[webapi(
|
||||
|
||||
@@ -312,6 +312,54 @@ pub(super) struct MutationObserverOptions {
|
||||
attribute_filter: Option<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl TryFrom<MutationObserverInit> for MutationObserverOptions {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(init: MutationObserverInit) -> Result<Self, Self::Error> {
|
||||
let attributes = init
|
||||
.attributes
|
||||
.unwrap_or(init.attribute_old_value.is_some() || init.attribute_filter.is_some());
|
||||
let character_data = init
|
||||
.character_data
|
||||
.unwrap_or(init.character_data_old_value.is_some());
|
||||
let attribute_old_value = init.attribute_old_value.unwrap_or(false);
|
||||
let character_data_old_value = init.character_data_old_value.unwrap_or(false);
|
||||
|
||||
if !init.child_list && !attributes && !character_data {
|
||||
return Err(
|
||||
"Failed to execute 'observe' on 'MutationObserver': at least one of childList, attributes, or characterData must be true.",
|
||||
);
|
||||
}
|
||||
if !attributes && (attribute_old_value || init.attribute_filter.is_some()) {
|
||||
return Err(
|
||||
"Failed to execute 'observe' on 'MutationObserver': attributeOldValue and attributeFilter require attributes to be true.",
|
||||
);
|
||||
}
|
||||
if !character_data && character_data_old_value {
|
||||
return Err(
|
||||
"Failed to execute 'observe' on 'MutationObserver': characterDataOldValue requires characterData to be true.",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
child_list: init.child_list,
|
||||
attributes,
|
||||
character_data,
|
||||
subtree: init.subtree,
|
||||
attribute_old_value,
|
||||
character_data_old_value,
|
||||
// Native mutation local names are UTF-8. Ill-formed UTF-16 entries
|
||||
// cannot match them; retain the filter even if no entries can match.
|
||||
attribute_filter: init.attribute_filter.map(|names| {
|
||||
names
|
||||
.into_iter()
|
||||
.filter_map(|name| String::from_utf16(&name.0).ok())
|
||||
.collect()
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct QueuedMutationRecord {
|
||||
target: NativeNodeId,
|
||||
@@ -400,6 +448,30 @@ struct MutationObserverConstructorArgs {
|
||||
callback: webidl::WebIdlCallbackFunction,
|
||||
}
|
||||
|
||||
#[derive(Default, webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "MutationObserverInit")]
|
||||
struct MutationObserverInit {
|
||||
#[webidl(sequence, converter = "raw")]
|
||||
attribute_filter: Option<Vec<webidl::DomString16>>,
|
||||
attribute_old_value: Option<bool>,
|
||||
attributes: Option<bool>,
|
||||
character_data: Option<bool>,
|
||||
character_data_old_value: Option<bool>,
|
||||
#[webidl(default = false)]
|
||||
child_list: bool,
|
||||
#[webidl(default = false)]
|
||||
subtree: bool,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlArgs)]
|
||||
#[webidl(prefix = "MutationObserver.observe")]
|
||||
struct MutationObserverObserveArgs<'s> {
|
||||
#[webidl(required, interface = web_api_interfaces::Node)]
|
||||
target: v8::Local<'s, v8::Object>,
|
||||
#[webidl(dictionary, default = MutationObserverInit::default())]
|
||||
options: MutationObserverInit,
|
||||
}
|
||||
|
||||
#[derive(webidl::WebIdlDictionary)]
|
||||
#[webidl(prefix = "IntersectionObserverInit")]
|
||||
struct IntersectionObserverInitMembers {
|
||||
@@ -987,8 +1059,7 @@ impl MutationObserverRegistration {
|
||||
return false;
|
||||
}
|
||||
self.options.attribute_filter.as_ref().is_none_or(|filter| {
|
||||
filter.contains(mutation.local_name())
|
||||
|| filter.contains(&mutation.local_name().to_ascii_lowercase())
|
||||
mutation.namespace().is_none() && filter.contains(mutation.local_name())
|
||||
})
|
||||
}
|
||||
DomMutationRecordKind::CharacterData { .. } => self.options.character_data,
|
||||
@@ -1058,21 +1129,25 @@ pub(super) fn mutation_observer_observe_callback<'s>(
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let raw_target = args.get(0);
|
||||
let Some(parsed) = webidl::parse_args::<MutationObserverObserveArgs>(scope, &args) else {
|
||||
return;
|
||||
};
|
||||
let options = match MutationObserverOptions::try_from(parsed.options) {
|
||||
Ok(options) => options,
|
||||
Err(message) => {
|
||||
throw_type_error(scope, message);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let raw_target = moli_webapi_declare::web_api_object_target(scope, parsed.target)
|
||||
.unwrap_or(parsed.target)
|
||||
.into();
|
||||
let Some(target) = callback_value_dom_handle(scope, raw_target)
|
||||
.or_else(|| detached_native_target_handle(scope, raw_target))
|
||||
else {
|
||||
if is_detached_node_value(scope, raw_target) {
|
||||
match parse_mutation_observer_options(scope, args.get(1)) {
|
||||
Ok(_) => {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
Err(message) => {
|
||||
throw_type_error(scope, message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
throw_type_error(
|
||||
scope,
|
||||
@@ -1080,13 +1155,6 @@ pub(super) fn mutation_observer_observe_callback<'s>(
|
||||
);
|
||||
return;
|
||||
};
|
||||
let options = match parse_mutation_observer_options(scope, args.get(1)) {
|
||||
Ok(options) => options,
|
||||
Err(message) => {
|
||||
throw_type_error(scope, message);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
rv.set_undefined();
|
||||
return;
|
||||
@@ -1498,67 +1566,6 @@ fn wrap_node_handle<'s>(
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
}
|
||||
|
||||
fn parse_mutation_observer_options(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
value: v8::Local<'_, v8::Value>,
|
||||
) -> Result<MutationObserverOptions, &'static str> {
|
||||
let object = v8::Local::<v8::Object>::try_from(value).map_err(
|
||||
|_| "Failed to execute 'observe' on 'MutationObserver': parameter 2 is not an object.",
|
||||
)?;
|
||||
let attributes_specified = has_property(scope, object, "attributes");
|
||||
let attributes_enabled = bool_option(scope, object, "attributes");
|
||||
let character_data_specified = has_property(scope, object, "characterData");
|
||||
let character_data_enabled = bool_option(scope, object, "characterData");
|
||||
let mut options = MutationObserverOptions {
|
||||
child_list: bool_option(scope, object, "childList"),
|
||||
attributes: attributes_enabled,
|
||||
character_data: character_data_enabled,
|
||||
subtree: bool_option(scope, object, "subtree"),
|
||||
attribute_old_value: bool_option(scope, object, "attributeOldValue"),
|
||||
character_data_old_value: bool_option(scope, object, "characterDataOldValue"),
|
||||
attribute_filter: string_set_option(scope, object, "attributeFilter"),
|
||||
};
|
||||
|
||||
if attributes_specified
|
||||
&& !attributes_enabled
|
||||
&& (options.attribute_old_value || options.attribute_filter.is_some())
|
||||
{
|
||||
return Err(
|
||||
"Failed to execute 'observe' on 'MutationObserver': attributeOldValue and attributeFilter require attributes to be true.",
|
||||
);
|
||||
}
|
||||
if character_data_specified && !character_data_enabled && options.character_data_old_value {
|
||||
return Err(
|
||||
"Failed to execute 'observe' on 'MutationObserver': characterDataOldValue requires characterData to be true.",
|
||||
);
|
||||
}
|
||||
|
||||
if !attributes_specified && (options.attribute_old_value || options.attribute_filter.is_some())
|
||||
{
|
||||
options.attributes = true;
|
||||
}
|
||||
if !character_data_specified && options.character_data_old_value {
|
||||
options.character_data = true;
|
||||
}
|
||||
if !options.child_list && !options.attributes && !options.character_data {
|
||||
return Err(
|
||||
"Failed to execute 'observe' on 'MutationObserver': at least one of childList, attributes, or characterData must be true.",
|
||||
);
|
||||
}
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
fn has_property(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
object: v8::Local<'_, v8::Object>,
|
||||
key: &str,
|
||||
) -> bool {
|
||||
let Some(key) = v8_string(scope, key) else {
|
||||
return false;
|
||||
};
|
||||
object.has(scope, key.into()).unwrap_or(false)
|
||||
}
|
||||
|
||||
enum IntersectionObserverOptionsError {
|
||||
Syntax(&'static str),
|
||||
WebIdl(webidl::WebIdlError),
|
||||
@@ -1642,19 +1649,6 @@ fn parse_intersection_observer_options<'s>(
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
fn bool_option(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
object: v8::Local<'_, v8::Object>,
|
||||
key: &str,
|
||||
) -> bool {
|
||||
let Some(key) = v8_string(scope, key) else {
|
||||
return false;
|
||||
};
|
||||
object
|
||||
.get(scope, key.into())
|
||||
.is_some_and(|value| value.boolean_value(scope))
|
||||
}
|
||||
|
||||
fn intersection_observer_threshold_member<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
object: v8::Local<'s, v8::Object>,
|
||||
@@ -1698,23 +1692,6 @@ fn normalize_intersection_thresholds(
|
||||
Ok(thresholds)
|
||||
}
|
||||
|
||||
fn string_set_option(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
object: v8::Local<'_, v8::Object>,
|
||||
key: &str,
|
||||
) -> Option<HashSet<String>> {
|
||||
let key = v8_string(scope, key)?;
|
||||
let value = object.get(scope, key.into())?;
|
||||
let array = v8::Local::<v8::Array>::try_from(value).ok()?;
|
||||
let mut result = HashSet::new();
|
||||
for index in 0..array.length() {
|
||||
let entry = array.get_index(scope, index)?;
|
||||
let text = entry.to_string(scope)?.to_rust_string_lossy(scope);
|
||||
result.insert(text);
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn define_intersection_observer_slots(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
@@ -2420,6 +2397,8 @@ fn mutation_observer_id_from_object(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
object: v8::Local<'_, v8::Object>,
|
||||
) -> Option<u32> {
|
||||
let object = v8::Local::new(scope, object);
|
||||
let object = moli_webapi_declare::web_api_object_target(scope, object)?;
|
||||
observer_id_from_private_slot(scope, object, MUTATION_OBSERVER_ID_SLOT)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,105 @@
|
||||
use super::*;
|
||||
|
||||
fn run_mutation_observer_init_checks(group: &str, total: usize) {
|
||||
let mut vm = new_storage_html_test_vm("https://mutation-observer-init.test/");
|
||||
vm.eval(&format!(
|
||||
"document.body.innerHTML='<iframe id=child></iframe>'; \
|
||||
globalThis.__mutationObserverGroup={group:?};"
|
||||
))
|
||||
.unwrap();
|
||||
vm.eval(include_str!(
|
||||
"../../../tests/fixtures/mutation-observer-init.js"
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval("__uiEventResults.complete").unwrap(), "true");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__uiEventResults.checks.filter(row=>!row.passed))")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("__uiEventResults.total").unwrap(),
|
||||
total.to_string()
|
||||
);
|
||||
let names: Vec<String> = serde_json::from_str(
|
||||
&vm.eval("JSON.stringify(__uiEventResults.checks.map(row=>row.name))")
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(names.len(), total);
|
||||
assert_eq!(
|
||||
names
|
||||
.into_iter()
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.len(),
|
||||
total
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_observer_init_converts_iterables_and_preserves_dictionary_order_and_registration() {
|
||||
run_mutation_observer_init_checks("conversion", 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_observer_filters_match_exact_names_namespaces_and_utf16_identity() {
|
||||
run_mutation_observer_init_checks("filters", 52);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_observer_observe_accepts_registered_native_proxy_targets_and_receivers() {
|
||||
fn register_proxy<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
mut rv: v8::ReturnValue<'s>,
|
||||
) {
|
||||
let target = v8::Local::<v8::Object>::try_from(args.get(0)).unwrap();
|
||||
let handler = crate::util::new_null_prototype_object(scope);
|
||||
let proxy = v8::Proxy::new(scope, target, handler).unwrap();
|
||||
moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap();
|
||||
rv.set(proxy.into());
|
||||
}
|
||||
let mut vm = new_storage_html_test_vm("https://mutation-observer-native-proxy.test/");
|
||||
let context_ptr = &vm.page_default_runtime.context as *const _;
|
||||
vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| {
|
||||
let global = scope.get_current_context().global(scope);
|
||||
let function = v8::Function::new(scope, register_proxy).unwrap();
|
||||
let key = crate::util::v8str(scope, "registerNativeProxy");
|
||||
assert_eq!(
|
||||
global.create_data_property(scope, key.into(), function.into()),
|
||||
Some(true)
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval(r#"(() => {
|
||||
const observer = new MutationObserver(() => {});
|
||||
const target = document.createElement('div');
|
||||
const observerProxy = registerNativeProxy(observer);
|
||||
const targetProxy = registerNativeProxy(target);
|
||||
MutationObserver.prototype.observe.call(observerProxy, targetProxy, {
|
||||
attributeFilter: new Set(['keep'])
|
||||
});
|
||||
target.setAttribute('drop', 'x'); target.setAttribute('keep', 'y');
|
||||
const records = MutationObserver.prototype.takeRecords.call(observerProxy);
|
||||
if (records.length !== 1 || records[0].target !== target || records[0].attributeName !== 'keep')
|
||||
throw Error('registered native identity');
|
||||
let gets = 0, traps = 0;
|
||||
for (const [receiver, node] of [
|
||||
[new Proxy(observerProxy, {get(){traps++;}}), targetProxy],
|
||||
[observerProxy, new Proxy(targetProxy, {get(){traps++;}})]
|
||||
]) {
|
||||
let error;
|
||||
try {MutationObserver.prototype.observe.call(receiver, node, {
|
||||
get childList(){gets++;return true;}
|
||||
});} catch(caught) {error = caught;}
|
||||
if (!(error instanceof TypeError)) throw Error('author Proxy was accepted');
|
||||
}
|
||||
MutationObserver.prototype.disconnect.call(observerProxy);
|
||||
return gets === 0 && traps === 0;
|
||||
})()"#).unwrap(), "true");
|
||||
}
|
||||
|
||||
fn observer_callback_test_vm(url: &str) -> StandaloneScriptVmHarness {
|
||||
let mut vm = new_storage_test_vm(url);
|
||||
vm.eval(
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
(() => {
|
||||
const realms = [globalThis, document.getElementById('child').contentWindow];
|
||||
const checks = [];
|
||||
const group = globalThis.__mutationObserverGroup || 'all';
|
||||
const facts = globalThis.__uiEventResults = {complete: false, group, checks};
|
||||
const assert = (ok, message) => {if (!ok) throw Error(message);};
|
||||
const equal = (a, b, message) => assert(JSON.stringify(a) === JSON.stringify(b),
|
||||
message + ': ' + JSON.stringify(a) + ' != ' + JSON.stringify(b));
|
||||
function test(name, body) {
|
||||
globalThis.__mutationObserverCurrentCase = name;
|
||||
try {body(); checks.push({name, passed: true});}
|
||||
catch (error) {checks.push({name, passed: false, detail: String(error)});}
|
||||
}
|
||||
for (const [ownerIndex, owner] of realms.entries()) {
|
||||
for (const [calleeIndex, callee] of realms.entries()) {
|
||||
const prefix = ownerIndex + ':' + calleeIndex + ':';
|
||||
const method = callee.MutationObserver.prototype.observe;
|
||||
function withObserver(body, target) {
|
||||
const observer = new owner.MutationObserver(() => {});
|
||||
target = target || owner.document.createElement('div');
|
||||
const observe = options => Reflect.apply(method, observer, [target, options]);
|
||||
try {body(observer, target, observe);} finally {observer.disconnect();}
|
||||
}
|
||||
const names = observer => observer.takeRecords().map(record => record.attributeName);
|
||||
const typeError = body => {
|
||||
let error;
|
||||
try {body();} catch (caught) {error = caught;}
|
||||
assert(error instanceof callee.TypeError, 'callee TypeError');
|
||||
};
|
||||
if (group !== 'filters') {
|
||||
test(prefix + 'observe.length', () => assert(method.length === 1, 'one required argument'));
|
||||
for (const [label, make] of [
|
||||
['Set', () => new Set(['keep'])],
|
||||
['generator', () => (function*() {yield 'keep';})()],
|
||||
['array custom iterator', () => Object.assign(['drop'], {[Symbol.iterator]: function*() {yield 'keep';}})],
|
||||
]) {
|
||||
test(prefix + 'iterable ' + label, () => withObserver((observer, target, observe) => {
|
||||
observe({attributes: true, attributeFilter: make()});
|
||||
target.setAttribute('drop', 'x'); target.setAttribute('keep', 'y');
|
||||
equal(names(observer), ['keep'], 'converted filter');
|
||||
}));
|
||||
}
|
||||
test(prefix + 'empty iterable remains an empty filter', () => withObserver((observer, target, observe) => {
|
||||
observe({attributeFilter: new Set()}); target.setAttribute('drop', 'x');
|
||||
equal(names(observer), [], 'nothing matches an empty filter');
|
||||
}));
|
||||
for (const [label, value] of [['null', null], ['string', 'keep'], ['number', 1], ['boolean', false],
|
||||
['plain object', {}], ['array-like', {0: 'keep', length: 1}], ['non-callable iterator', {[Symbol.iterator]: 1}]]) {
|
||||
test(prefix + 'reject non-sequence ' + label, () => withObserver((observer, target, observe) => {
|
||||
typeError(() => observe({attributes: true, attributeFilter: value}));
|
||||
target.setAttribute('keep', 'x'); equal(names(observer), [], 'failure does not register');
|
||||
}));
|
||||
}
|
||||
for (const oldValue of [false, null]) {
|
||||
test(prefix + 'attributeOldValue present ' + oldValue, () => withObserver((observer, target, observe) => {
|
||||
observe({attributeOldValue: oldValue}); target.setAttribute('keep', 'x');
|
||||
const records = observer.takeRecords();
|
||||
assert(records.length === 1 && records[0].oldValue === null, 'presence enables attributes');
|
||||
}));
|
||||
test(prefix + 'characterDataOldValue present ' + oldValue, () => {
|
||||
const target = owner.document.createTextNode('before');
|
||||
withObserver((observer, node, observe) => {
|
||||
observe({characterDataOldValue: oldValue}); node.data = 'after';
|
||||
const records = observer.takeRecords();
|
||||
assert(records.length === 1 && records[0].type === 'characterData' && records[0].oldValue === null,
|
||||
'presence enables characterData');
|
||||
}, target);
|
||||
});
|
||||
}
|
||||
test(prefix + 'undefined flags are absent', () => withObserver((observer, target, observe) => {
|
||||
target.setAttribute('keep', 'before');
|
||||
observe({attributes: undefined, attributeOldValue: true});
|
||||
target.setAttribute('keep', 'after');
|
||||
const records = observer.takeRecords();
|
||||
assert(records.length === 1 && records[0].oldValue === 'before', 'undefined allows implicit attributes');
|
||||
}));
|
||||
const keys = ['attributeFilter', 'attributeOldValue', 'attributes', 'characterData',
|
||||
'characterDataOldValue', 'childList', 'subtree'];
|
||||
test(prefix + 'dictionary Get order without HasProperty', () => withObserver((observer, target, observe) => {
|
||||
const log = [], options = new Proxy({attributeFilter: ['keep'], attributes: true}, {
|
||||
get(object, key, receiver) {log.push(key); return Reflect.get(object, key, receiver);},
|
||||
has() {throw Error('unexpected HasProperty');},
|
||||
});
|
||||
observe(options); equal(log, keys, 'lexical dictionary order');
|
||||
}));
|
||||
test(prefix + 'inherited members are read once in lexical order', () => withObserver((observer, target, observe) => {
|
||||
const log = [], prototype = {};
|
||||
for (const key of keys) Object.defineProperty(prototype, key, {get() {
|
||||
log.push(key); return key === 'attributes' ? true : undefined;
|
||||
}});
|
||||
observe(Object.create(prototype)); equal(log, keys, 'inherited getter order');
|
||||
}));
|
||||
for (const failKey of keys) {
|
||||
test(prefix + 'getter abrupt completion at ' + failKey, () => withObserver((observer, target, observe) => {
|
||||
const sentinel = {}, log = [], options = {};
|
||||
for (const key of keys) Object.defineProperty(options, key, {get() {
|
||||
log.push(key); if (key === failKey) throw sentinel;
|
||||
return key === 'attributeFilter' ? ['keep'] : key === 'attributes' ? true : undefined;
|
||||
}});
|
||||
let error; try {observe(options);} catch (caught) {error = caught;}
|
||||
assert(error === sentinel, 'original thrown value');
|
||||
equal(log, keys.slice(0, keys.indexOf(failKey) + 1), 'stop at failed member');
|
||||
target.setAttribute('keep', 'after'); equal(names(observer), [], 'no partial registration');
|
||||
}));
|
||||
}
|
||||
for (const failure of ['iterator', 'item']) {
|
||||
test(prefix + 'failed reconfiguration preserves registration: ' + failure, () => withObserver((observer, target, observe) => {
|
||||
target.setAttribute('keep', 'before');
|
||||
observe({attributes: true, attributeOldValue: true, attributeFilter: ['keep']});
|
||||
const sentinel = {}, filter = failure === 'iterator' ? {
|
||||
*[Symbol.iterator]() {yield 'changed'; throw sentinel;}
|
||||
} : [{toString() {throw sentinel;}}];
|
||||
let error, laterGets = 0;
|
||||
try {observe({attributeFilter: filter, get attributes() {laterGets++; return true;}});}
|
||||
catch (caught) {error = caught;}
|
||||
assert(error === sentinel && laterGets === 0, 'conversion fails before later members');
|
||||
target.setAttribute('changed', 'x'); target.setAttribute('keep', 'after');
|
||||
const records = observer.takeRecords();
|
||||
assert(records.length === 1 && records[0].attributeName === 'keep' && records[0].oldValue === 'before',
|
||||
'old registration is intact');
|
||||
}));
|
||||
}
|
||||
test(prefix + 'iteration and DOMString conversion happen at member position', () => withObserver((observer, target, observe) => {
|
||||
const log = [], iterable = {
|
||||
get [Symbol.iterator]() {log.push('iterator'); return new Proxy(function*() {
|
||||
log.push('iterate'); yield {toString() {log.push('string'); return 'keep';}};
|
||||
}, {});}
|
||||
};
|
||||
observe({get attributeFilter() {log.push('filter'); return iterable;},
|
||||
get attributeOldValue() {log.push('oldValue'); return false;},
|
||||
get attributes() {log.push('attributes'); return true;}});
|
||||
equal(log, ['filter', 'iterator', 'iterate', 'string', 'oldValue', 'attributes'], 'conversion position');
|
||||
target.setAttribute('drop', 'x'); target.setAttribute('keep', 'after');
|
||||
equal(names(observer), ['keep'], 'resulting filter');
|
||||
}));
|
||||
test(prefix + 'invalid target precedes options conversion', () => withObserver((observer, target) => {
|
||||
const revoked = Proxy.revocable(target, {}); revoked.revoke();
|
||||
for (const invalid of [{}, Object.create(target), new Proxy(target, {}), revoked.proxy, null]) {
|
||||
let gets = 0;
|
||||
typeError(() => Reflect.apply(method, observer, [invalid, {get childList() {gets++; return true;}}]));
|
||||
assert(gets === 0, 'invalid target stops dictionary getters');
|
||||
}
|
||||
}));
|
||||
test(prefix + 'invalid receiver precedes argument conversion', () => withObserver((observer, target) => {
|
||||
for (const invalid of [{}, Object.create(observer), new Proxy(observer, {})]) {
|
||||
let gets = 0;
|
||||
typeError(() => Reflect.apply(method, invalid, [target, {get childList() {gets++; return true;}}]));
|
||||
assert(gets === 0, 'receiver validation comes first');
|
||||
}
|
||||
}));
|
||||
test(prefix + 'explicit false stays false', () => withObserver((observer, target, observe) => {
|
||||
observe({childList: true, attributes: false, attributeOldValue: false});
|
||||
target.setAttribute('keep', 'x'); target.appendChild(owner.document.createTextNode('text'));
|
||||
const records = observer.takeRecords();
|
||||
assert(records.length === 1 && records[0].type === 'childList', 'no implicit attributes');
|
||||
typeError(() => observe({childList: true, attributes: false, attributeFilter: []}));
|
||||
typeError(() => observe({childList: true, characterData: false, characterDataOldValue: true}));
|
||||
}));
|
||||
}
|
||||
if (group !== 'conversion') {
|
||||
for (const documentKind of ['live', 'html', 'xml']) {
|
||||
const doc = documentKind === 'live' ? owner.document : documentKind === 'html' ?
|
||||
owner.document.implementation.createHTMLDocument('') :
|
||||
owner.document.implementation.createDocument('urn:fixture', 'root');
|
||||
test(prefix + documentKind + ':filter excludes non-null namespaces', () => withObserver((observer, target, observe) => {
|
||||
observe({attributes: true, attributeFilter: ['keep']});
|
||||
target.setAttributeNS('urn:fixture', 'p:keep', 'namespaced');
|
||||
target.setAttribute('keep', 'plain');
|
||||
const records = observer.takeRecords();
|
||||
assert(records.length === 1 && records[0].attributeName === 'keep' && records[0].attributeNamespace === null,
|
||||
'only unnamespaced local name matches');
|
||||
}, doc.createElement('item')));
|
||||
test(prefix + documentKind + ':no filter retains namespaced mutations', () => withObserver((observer, target, observe) => {
|
||||
observe({attributes: true}); target.setAttributeNS('urn:fixture', 'p:keep', 'value');
|
||||
const records = observer.takeRecords();
|
||||
assert(records.length === 1 && records[0].attributeNamespace === 'urn:fixture', 'unfiltered namespace');
|
||||
}, doc.createElement('item')));
|
||||
test(prefix + documentKind + ':filter preserves lone-surrogate identity', () => withObserver((observer, target, observe) => {
|
||||
observe({attributes: true, attributeFilter: [String.fromCharCode(0xd800)]});
|
||||
target.setAttribute('\ufffd', 'value'); equal(names(observer), [], 'no replacement-character alias');
|
||||
}, doc.createElement('item')));
|
||||
test(prefix + documentKind + ':filter preserves well-formed Unicode', () => withObserver((observer, target, observe) => {
|
||||
const name = String.fromCodePoint(0x10000);
|
||||
observe({attributeFilter: [name]}); target.setAttribute(name, 'value');
|
||||
equal(names(observer), [name], 'non-BMP local name');
|
||||
}, doc.createElement('item')));
|
||||
}
|
||||
test(prefix + 'XML attribute filters match case exactly', () => {
|
||||
const doc = owner.document.implementation.createDocument('urn:fixture', 'root');
|
||||
withObserver((observer, target, observe) => {
|
||||
observe({attributeFilter: ['lower']});
|
||||
target.setAttribute('LOWER', 'upper'); target.setAttribute('lower', 'lower');
|
||||
equal(names(observer), ['lower'], 'case-sensitive local names');
|
||||
}, doc.documentElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
facts.total = checks.length;
|
||||
facts.passed = checks.filter(check => check.passed).length;
|
||||
facts.complete = true;
|
||||
return facts.passed === facts.total;
|
||||
})()
|
||||
@@ -12,7 +12,7 @@ callback MutationCallback = undefined (sequence<MutationRecord> mutations, Mutat
|
||||
|
||||
interface MutationObserver {
|
||||
constructor(MutationCallback callback);
|
||||
undefined observe(Node target, MutationObserverInit options);
|
||||
undefined observe(Node target, optional MutationObserverInit options = {});
|
||||
undefined disconnect();
|
||||
sequence<MutationRecord> takeRecords();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user