mirror of
https://github.com/lexmount/moli.git
synced 2026-10-31 00:06:19 +00:00
fix(dom): preserve registry boundaries while importing shadow trees
This commit is contained in:
@@ -21,24 +21,17 @@ pub(super) fn registry_association_for_import_clone_source(
|
||||
host: &JsContextHost,
|
||||
source: DomHandle,
|
||||
target_document: DomHandle,
|
||||
fallback_registry: Option<CustomElementRegistryAssociation>,
|
||||
preserve_null_shadow_registry: bool,
|
||||
) -> Option<CustomElementRegistryAssociation> {
|
||||
if preserve_null_shadow_registry {
|
||||
return None;
|
||||
}
|
||||
|
||||
fallback_registry: CustomElementRegistryAssociation,
|
||||
) -> CustomElementRegistryAssociation {
|
||||
let target_default =
|
||||
host.default_custom_element_registry_association_for_document(target_document);
|
||||
match host.effective_custom_element_registry_association(source) {
|
||||
CustomElementRegistryAssociation::Registry(CustomElementRegistryKey::Scoped(scoped_id)) => {
|
||||
Some(CustomElementRegistryAssociation::Registry(
|
||||
CustomElementRegistryKey::Scoped(scoped_id),
|
||||
))
|
||||
CustomElementRegistryAssociation::Registry(CustomElementRegistryKey::Scoped(scoped_id))
|
||||
}
|
||||
CustomElementRegistryAssociation::Null => Some(fallback_registry.unwrap_or(target_default)),
|
||||
CustomElementRegistryAssociation::Null => fallback_registry,
|
||||
CustomElementRegistryAssociation::Registry(
|
||||
CustomElementRegistryKey::Global | CustomElementRegistryKey::Child(_),
|
||||
) => Some(target_default),
|
||||
) => target_default,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,17 @@ pub(crate) fn registry_association_retargets_for_import_clone(
|
||||
target_document: DomHandle,
|
||||
fallback_registry: Option<CustomElementRegistryAssociation>,
|
||||
) -> Vec<RegistryAssociationRetarget> {
|
||||
let host = unsafe { &*host_ptr };
|
||||
let fallback_registry = fallback_registry.unwrap_or_else(|| {
|
||||
host.default_custom_element_registry_association_for_document(target_document)
|
||||
});
|
||||
let mut retargets = Vec::new();
|
||||
collect_registry_association_retargets_for_import_clone(
|
||||
host_ptr,
|
||||
host,
|
||||
source_root,
|
||||
clone_root,
|
||||
target_document,
|
||||
fallback_registry,
|
||||
false,
|
||||
&mut retargets,
|
||||
);
|
||||
retargets
|
||||
@@ -60,26 +63,28 @@ pub(crate) fn registry_association_retargets_for_clone(
|
||||
}
|
||||
|
||||
fn collect_registry_association_retargets_for_import_clone(
|
||||
host_ptr: *mut JsContextHost,
|
||||
host: &JsContextHost,
|
||||
source: DomHandle,
|
||||
clone: DomHandle,
|
||||
target_document: DomHandle,
|
||||
fallback_registry: Option<CustomElementRegistryAssociation>,
|
||||
preserve_null_shadow_registry: bool,
|
||||
fallback_registry: CustomElementRegistryAssociation,
|
||||
retargets: &mut Vec<RegistryAssociationRetarget>,
|
||||
) {
|
||||
let host = unsafe { &*host_ptr };
|
||||
let mut stack = vec![(source, clone, preserve_null_shadow_registry)];
|
||||
while let Some((source, clone, preserve_null_shadow_registry)) = stack.pop() {
|
||||
if should_record_import_clone_registry_association(host.dom_host(), clone)
|
||||
&& let Some(association) = registry_association_for_import_clone_source(
|
||||
host,
|
||||
source,
|
||||
target_document,
|
||||
fallback_registry,
|
||||
preserve_null_shadow_registry,
|
||||
)
|
||||
{
|
||||
let mut stack = vec![(source, clone, fallback_registry)];
|
||||
while let Some((source, clone, fallback_registry)) = stack.pop() {
|
||||
if should_record_import_clone_registry_association(host.dom_host(), clone) {
|
||||
// ShadowRoot's registry is copied independently of the fallback
|
||||
// used to clone the host and its light children.
|
||||
let association = if host.dom_host().is_shadow_root(source) {
|
||||
registry_association_for_clone_source(host, source, target_document)
|
||||
} else {
|
||||
registry_association_for_import_clone_source(
|
||||
host,
|
||||
source,
|
||||
target_document,
|
||||
fallback_registry,
|
||||
)
|
||||
};
|
||||
retargets.push(RegistryAssociationRetarget {
|
||||
handle: clone,
|
||||
association,
|
||||
@@ -89,18 +94,20 @@ fn collect_registry_association_retargets_for_import_clone(
|
||||
let source_children = host.dom_host().child_handles(source).collect::<Vec<_>>();
|
||||
let clone_children = host.dom_host().child_handles(clone).collect::<Vec<_>>();
|
||||
for (source_child, clone_child) in source_children.into_iter().zip(clone_children).rev() {
|
||||
stack.push((source_child, clone_child, preserve_null_shadow_registry));
|
||||
stack.push((source_child, clone_child, fallback_registry));
|
||||
}
|
||||
if let (Some(source_shadow), Some(clone_shadow)) = (
|
||||
host.dom_host().shadow_root_handle(source),
|
||||
host.dom_host().shadow_root_handle(clone),
|
||||
) {
|
||||
let shadow_preserves_null_registry = preserve_null_shadow_registry
|
||||
|| host
|
||||
.dom_host()
|
||||
.shadow_root_uses_null_custom_element_registry(source_shadow)
|
||||
.unwrap_or(false);
|
||||
stack.push((source_shadow, clone_shadow, shadow_preserves_null_registry));
|
||||
// The DOM cloning algorithm intentionally omits fallbackRegistry
|
||||
// when recursively cloning shadow children. Explicit registries
|
||||
// on those children still retain their own identity.
|
||||
stack.push((
|
||||
source_shadow,
|
||||
clone_shadow,
|
||||
CustomElementRegistryAssociation::Null,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
(() => {
|
||||
const checks = [], resources = [];
|
||||
const check = (name, actual, expected) => checks.push({
|
||||
name, actual, expected, passed: JSON.stringify(actual) === JSON.stringify(expected),
|
||||
});
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame); resources.push(frame);
|
||||
let serial = 0;
|
||||
try {
|
||||
const realms = [['main', window], ['iframe', frame.contentWindow]];
|
||||
for (const [sourceName, sourceWindow] of realms) {
|
||||
for (const [targetName, targetWindow] of realms) {
|
||||
const sourceDocument = sourceWindow.document, targetDocument = targetWindow.document;
|
||||
const registryGetter = Object.getOwnPropertyDescriptor(sourceWindow.Element.prototype, 'customElementRegistry').get;
|
||||
for (const shadowKind of ['implicit', 'null', 'global', 'scoped']) {
|
||||
for (const childKind of ['null', 'global', 'scoped']) {
|
||||
for (const selfOnly of [false, true]) {
|
||||
const tag = `x-clone-registry-${serial++}`;
|
||||
const scoped = new sourceWindow.CustomElementRegistry();
|
||||
const fallback = new targetWindow.CustomElementRegistry();
|
||||
class SourceGlobal extends sourceWindow.HTMLElement { constructor() { super(); this.marker = 'source-global'; } }
|
||||
class SourceScoped extends sourceWindow.HTMLElement { constructor() { super(); this.marker = 'source-scoped'; } }
|
||||
class TargetGlobal extends targetWindow.HTMLElement { constructor() { super(); this.marker = 'target-global'; } }
|
||||
class Fallback extends targetWindow.HTMLElement { constructor() { super(); this.marker = 'fallback'; } }
|
||||
sourceWindow.customElements.define(tag, SourceGlobal);
|
||||
scoped.define(tag, SourceScoped); fallback.define(tag, Fallback);
|
||||
if (sourceWindow !== targetWindow) targetWindow.customElements.define(tag, TargetGlobal);
|
||||
const source = sourceDocument.createElement('div', {customElementRegistry: null});
|
||||
const options = {mode: 'open', clonable: true, serializable: true};
|
||||
if (shadowKind !== 'implicit') options.customElementRegistry = shadowKind === 'null' ? null : shadowKind === 'global' ? sourceWindow.customElements : scoped;
|
||||
const shadow = source.attachShadow(options);
|
||||
const make = kind => {
|
||||
const registry = kind === 'null' ? null : kind === 'global' ? sourceWindow.customElements : scoped;
|
||||
const child = sourceDocument.createElement(tag, {customElementRegistry: registry});
|
||||
child.textContent = '\ud800 value\udfff\r';
|
||||
child.setAttribute('data-value', '\udfff attribute\ud800');
|
||||
return child;
|
||||
};
|
||||
const light = make(childKind), shadowChild = make(childKind);
|
||||
source.appendChild(light); shadow.appendChild(shadowChild);
|
||||
const nestedHost = sourceDocument.createElement('div', {customElementRegistry: null});
|
||||
shadow.appendChild(nestedHost);
|
||||
const nestedRoot = nestedHost.attachShadow({mode: 'open', clonable: true, customElementRegistry: sourceWindow.customElements});
|
||||
const nestedNull = make('null'), nestedScoped = make('scoped');
|
||||
nestedRoot.append(nestedNull, nestedScoped);
|
||||
const sourceRegistry = registryGetter.call(shadowChild);
|
||||
const sourceLightRegistry = registryGetter.call(light);
|
||||
const sourceRootRegistry = shadow.customElementRegistry;
|
||||
const key = registry => registry === null ? 'null' : registry === scoped ? 'scoped' : registry === fallback ? 'fallback' : registry === sourceWindow.customElements ? 'source-global' : registry === targetWindow.customElements ? 'target-global' : String(registry);
|
||||
const rebaseGlobal = registry => registry === sourceWindow.customElements ? targetWindow.customElements : registry;
|
||||
const constructorFor = registry => registry === null ? null : registry.get(tag);
|
||||
const poison = childKind === 'scoped' && selfOnly;
|
||||
if (poison) {
|
||||
for (const object of [source, shadow, shadowChild, nestedHost]) {
|
||||
for (const property of ['customElementRegistry', 'ownerDocument', 'childNodes', 'firstChild', 'shadowRoot']) {
|
||||
Object.defineProperty(object, property, {configurable: true, get() { throw Error(`author ${property} getter`); }});
|
||||
}
|
||||
}
|
||||
}
|
||||
const operations = [
|
||||
['import-default', {selfOnly}, targetWindow.customElements],
|
||||
['import-fallback', {selfOnly, customElementRegistry: fallback}, fallback],
|
||||
];
|
||||
if (sourceWindow === targetWindow) operations.push(['clone', !selfOnly, null]);
|
||||
for (const [method, argument, lightFallback] of operations) {
|
||||
const prefix = `${sourceName}->${targetName}/${shadowKind}/${childKind}/${selfOnly ? 'self' : 'deep'}/${method}`;
|
||||
let clone;
|
||||
try {
|
||||
clone = method === 'clone' ? sourceWindow.Node.prototype.cloneNode.call(source, argument)
|
||||
: targetWindow.Document.prototype.importNode.call(targetDocument, source, argument);
|
||||
} catch (error) {
|
||||
check(`${prefix}/operation succeeds`, String(error), 'success');
|
||||
continue;
|
||||
}
|
||||
check(`${prefix}/root`, [clone !== source, clone.ownerDocument === targetDocument, clone.parentNode, key(clone.customElementRegistry)], [true, true, null, key(lightFallback)]);
|
||||
check(`${prefix}/light children`, clone.childNodes.length, selfOnly ? 0 : 1);
|
||||
const cloneRoot = clone.shadowRoot;
|
||||
check(`${prefix}/shadow metadata`, [cloneRoot !== shadow, cloneRoot.host === clone, cloneRoot.ownerDocument === targetDocument, cloneRoot.clonable, cloneRoot.serializable, key(cloneRoot.customElementRegistry)], [true, true, true, true, true, key(rebaseGlobal(sourceRootRegistry))]);
|
||||
const element = (label, node, registry) => {
|
||||
const Constructor = constructorFor(registry);
|
||||
check(`${prefix}/${label} registry`, key(node.customElementRegistry), key(registry));
|
||||
check(`${prefix}/${label} constructor`, [node.marker ?? null, Object.getPrototypeOf(node) === (Constructor ? Constructor.prototype : targetWindow.HTMLElement.prototype)], [Constructor === SourceScoped ? 'source-scoped' : Constructor === Fallback ? 'fallback' : Constructor === SourceGlobal ? 'source-global' : Constructor === TargetGlobal ? 'target-global' : null, true]);
|
||||
check(`${prefix}/${label} native data`, [node.ownerDocument === targetDocument, node.textContent, node.getAttribute('data-value')], [true, '\ud800 value\udfff\r', '\udfff attribute\ud800']);
|
||||
};
|
||||
if (!selfOnly) element('light child', clone.firstChild, sourceLightRegistry === null ? lightFallback : rebaseGlobal(sourceLightRegistry));
|
||||
element('shadow child', cloneRoot.firstChild, rebaseGlobal(sourceRegistry));
|
||||
const nested = cloneRoot.lastChild.shadowRoot;
|
||||
check(`${prefix}/nested shadow registry`, key(nested.customElementRegistry), key(targetWindow.customElements));
|
||||
element('nested null child', nested.firstChild, null);
|
||||
element('nested scoped child', nested.lastChild, scoped);
|
||||
check(`${prefix}/source registry unchanged`, key(registryGetter.call(shadowChild)), key(sourceRegistry));
|
||||
check(`${prefix}/source native data unchanged`, [light.ownerDocument === sourceDocument, nestedScoped.ownerDocument === sourceDocument], [true, true]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
globalThis.__documentCloneRegistryResults = {complete: true, total: checks.length, passed: checks.filter(row => row.passed).length, checks};
|
||||
globalThis.__uiEventResults = globalThis.__documentCloneRegistryResults;
|
||||
return true;
|
||||
} finally { for (const resource of resources.reverse()) resource.remove(); }
|
||||
})()
|
||||
@@ -1,5 +1,22 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn document_import_preserves_registry_scopes_when_cloning_shadow_trees() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://document-clone-registry.test/");
|
||||
vm.eval(include_str!("document_clone_registry_scopes_v2.js"))
|
||||
.expect("Document clone registry matrix should evaluate");
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(__documentCloneRegistryResults.checks.filter(row => !row.passed))")
|
||||
.unwrap(),
|
||||
"[]"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("__documentCloneRegistryResults.complete && __documentCloneRegistryResults.total === 3960 && __documentCloneRegistryResults.passed === 3960")
|
||||
.unwrap(),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_import_and_adopt_use_webidl_node_and_dictionary_conversion() {
|
||||
let mut vm = new_storage_page_task_executor_test_vm("https://document-import-adopt.test/");
|
||||
|
||||
Reference in New Issue
Block a user