@remote-dom/polyfill 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/README.md +51 -3
- package/build/cjs/Attr.cjs +33 -4
- package/build/cjs/CharacterData.cjs +10 -0
- package/build/cjs/ChildNode.cjs +79 -13
- package/build/cjs/Comment.cjs +1 -1
- package/build/cjs/CustomElementRegistry.cjs +27 -2
- package/build/cjs/CustomEvent.cjs +2 -2
- package/build/cjs/Document.cjs +56 -32
- package/build/cjs/DocumentFragment.cjs +6 -2
- package/build/cjs/Element.cjs +68 -8
- package/build/cjs/Event.cjs +29 -27
- package/build/cjs/EventTarget.cjs +128 -58
- package/build/cjs/HTMLTemplateElement.cjs +1 -0
- package/build/cjs/MutationObserver.cjs +167 -1
- package/build/cjs/NamedNodeMap.cjs +82 -45
- package/build/cjs/Node.cjs +47 -11
- package/build/cjs/NodeList.cjs +9 -1
- package/build/cjs/ParentNode.cjs +319 -77
- package/build/cjs/SVGElement.cjs +1 -1
- package/build/cjs/Text.cjs +1 -1
- package/build/cjs/Window.cjs +131 -38
- package/build/cjs/attribute-reactions.cjs +20 -0
- package/build/cjs/constants.cjs +48 -24
- package/build/cjs/custom-element-reactions.cjs +52 -0
- package/build/cjs/dom-exception.cjs +15 -0
- package/build/cjs/hook-effects.cjs +51 -0
- package/build/cjs/names.cjs +61 -0
- package/build/cjs/selectors.cjs +213 -122
- package/build/cjs/serialization.cjs +168 -65
- package/build/cjs/shared.cjs +177 -28
- package/build/esm/Attr.mjs +34 -5
- package/build/esm/CharacterData.mjs +10 -0
- package/build/esm/ChildNode.mjs +76 -15
- package/build/esm/Comment.mjs +2 -2
- package/build/esm/CustomElementRegistry.mjs +27 -2
- package/build/esm/CustomEvent.mjs +2 -2
- package/build/esm/Document.mjs +60 -35
- package/build/esm/DocumentFragment.mjs +7 -3
- package/build/esm/Element.mjs +69 -9
- package/build/esm/Event.mjs +27 -27
- package/build/esm/EventTarget.mjs +130 -60
- package/build/esm/HTMLTemplateElement.mjs +2 -1
- package/build/esm/MutationObserver.mjs +166 -2
- package/build/esm/NamedNodeMap.mjs +83 -46
- package/build/esm/Node.mjs +48 -12
- package/build/esm/NodeList.mjs +9 -1
- package/build/esm/ParentNode.mjs +322 -81
- package/build/esm/SVGElement.mjs +2 -2
- package/build/esm/Text.mjs +2 -2
- package/build/esm/Window.mjs +132 -39
- package/build/esm/attribute-reactions.mjs +16 -0
- package/build/esm/constants.mjs +28 -23
- package/build/esm/custom-element-reactions.mjs +47 -0
- package/build/esm/dom-exception.mjs +11 -0
- package/build/esm/hook-effects.mjs +46 -0
- package/build/esm/names.mjs +54 -0
- package/build/esm/selectors.mjs +202 -124
- package/build/esm/serialization.mjs +169 -66
- package/build/esm/shared.mjs +173 -30
- package/build/esnext/Attr.esnext +45 -5
- package/build/esnext/CharacterData.esnext +10 -0
- package/build/esnext/ChildNode.esnext +84 -15
- package/build/esnext/Comment.esnext +2 -2
- package/build/esnext/CustomElementRegistry.esnext +42 -0
- package/build/esnext/CustomEvent.esnext +2 -2
- package/build/esnext/Document.esnext +69 -32
- package/build/esnext/DocumentFragment.esnext +6 -2
- package/build/esnext/Element.esnext +78 -9
- package/build/esnext/Event.esnext +36 -37
- package/build/esnext/EventTarget.esnext +133 -61
- package/build/esnext/HTMLTemplateElement.esnext +3 -1
- package/build/esnext/MutationObserver.esnext +163 -2
- package/build/esnext/NamedNodeMap.esnext +110 -42
- package/build/esnext/Node.esnext +50 -12
- package/build/esnext/NodeList.esnext +1 -1
- package/build/esnext/ParentNode.esnext +352 -79
- package/build/esnext/SVGElement.esnext +2 -2
- package/build/esnext/Text.esnext +2 -2
- package/build/esnext/Window.esnext +120 -16
- package/build/esnext/attribute-reactions.esnext +21 -0
- package/build/esnext/constants.esnext +34 -21
- package/build/esnext/custom-element-reactions.esnext +49 -0
- package/build/esnext/dom-exception.esnext +11 -0
- package/build/esnext/hook-effects.esnext +46 -0
- package/build/esnext/names.esnext +61 -0
- package/build/esnext/selectors.esnext +202 -100
- package/build/esnext/serialization.esnext +171 -48
- package/build/esnext/shared.esnext +181 -34
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/typescript/Attr.d.ts +6 -4
- package/build/typescript/Attr.d.ts.map +1 -1
- package/build/typescript/CharacterData.d.ts.map +1 -1
- package/build/typescript/ChildNode.d.ts +5 -0
- package/build/typescript/ChildNode.d.ts.map +1 -1
- package/build/typescript/ClipboardEvent.d.ts.map +1 -1
- package/build/typescript/Comment.d.ts +2 -2
- package/build/typescript/Comment.d.ts.map +1 -1
- package/build/typescript/CustomElementRegistry.d.ts +1 -0
- package/build/typescript/CustomElementRegistry.d.ts.map +1 -1
- package/build/typescript/CustomEvent.d.ts.map +1 -1
- package/build/typescript/Document.d.ts +10 -5
- package/build/typescript/Document.d.ts.map +1 -1
- package/build/typescript/DocumentFragment.d.ts +2 -1
- package/build/typescript/DocumentFragment.d.ts.map +1 -1
- package/build/typescript/Element.d.ts +16 -7
- package/build/typescript/Element.d.ts.map +1 -1
- package/build/typescript/ErrorEvent.d.ts.map +1 -1
- package/build/typescript/Event.d.ts +9 -10
- package/build/typescript/Event.d.ts.map +1 -1
- package/build/typescript/EventTarget.d.ts +20 -3
- package/build/typescript/EventTarget.d.ts.map +1 -1
- package/build/typescript/FocusEvent.d.ts.map +1 -1
- package/build/typescript/HTMLTemplateElement.d.ts.map +1 -1
- package/build/typescript/MutationObserver.d.ts +42 -0
- package/build/typescript/MutationObserver.d.ts.map +1 -1
- package/build/typescript/NamedNodeMap.d.ts +5 -3
- package/build/typescript/NamedNodeMap.d.ts.map +1 -1
- package/build/typescript/Node.d.ts +4 -3
- package/build/typescript/Node.d.ts.map +1 -1
- package/build/typescript/NodeList.d.ts +3 -2
- package/build/typescript/NodeList.d.ts.map +1 -1
- package/build/typescript/ParentNode.d.ts +29 -10
- package/build/typescript/ParentNode.d.ts.map +1 -1
- package/build/typescript/PromiseRejectionEvent.d.ts.map +1 -1
- package/build/typescript/SVGElement.d.ts +1 -1
- package/build/typescript/SVGElement.d.ts.map +1 -1
- package/build/typescript/Text.d.ts +1 -1
- package/build/typescript/Text.d.ts.map +1 -1
- package/build/typescript/ToggleEvent.d.ts.map +1 -1
- package/build/typescript/Window.d.ts +5 -1
- package/build/typescript/Window.d.ts.map +1 -1
- package/build/typescript/attribute-reactions.d.ts +4 -0
- package/build/typescript/attribute-reactions.d.ts.map +1 -0
- package/build/typescript/constants.d.ts +27 -18
- package/build/typescript/constants.d.ts.map +1 -1
- package/build/typescript/custom-element-reactions.d.ts +6 -0
- package/build/typescript/custom-element-reactions.d.ts.map +1 -0
- package/build/typescript/dom-exception.d.ts +2 -0
- package/build/typescript/dom-exception.d.ts.map +1 -0
- package/build/typescript/extensions.d.ts +4 -0
- package/build/typescript/extensions.d.ts.map +1 -0
- package/build/typescript/hook-effects.d.ts +6 -0
- package/build/typescript/hook-effects.d.ts.map +1 -0
- package/build/typescript/hooks.d.ts +3 -3
- package/build/typescript/hooks.d.ts.map +1 -1
- package/build/typescript/index.d.ts +1 -0
- package/build/typescript/index.d.ts.map +1 -1
- package/build/typescript/names.d.ts +11 -0
- package/build/typescript/names.d.ts.map +1 -0
- package/build/typescript/selectors.d.ts +21 -22
- package/build/typescript/selectors.d.ts.map +1 -1
- package/build/typescript/serialization.d.ts.map +1 -1
- package/build/typescript/shared.d.ts +10 -0
- package/build/typescript/shared.d.ts.map +1 -1
- package/build/typescript/tests/ChildNode.test.d.ts +2 -0
- package/build/typescript/tests/ChildNode.test.d.ts.map +1 -0
- package/build/typescript/tests/MutationObserver.test.d.ts +2 -0
- package/build/typescript/tests/MutationObserver.test.d.ts.map +1 -0
- package/build/typescript/tests/adopt-inserted-subtrees.test.d.ts +2 -0
- package/build/typescript/tests/adopt-inserted-subtrees.test.d.ts.map +1 -0
- package/build/typescript/tests/attr-mutation.test.d.ts +2 -0
- package/build/typescript/tests/attr-mutation.test.d.ts.map +1 -0
- package/build/typescript/tests/child-node-before-after.test.d.ts +2 -0
- package/build/typescript/tests/child-node-before-after.test.d.ts.map +1 -0
- package/build/typescript/tests/child-node-replace-with.test.d.ts +2 -0
- package/build/typescript/tests/child-node-replace-with.test.d.ts.map +1 -0
- package/build/typescript/tests/clone-import-namespaces.test.d.ts +2 -0
- package/build/typescript/tests/clone-import-namespaces.test.d.ts.map +1 -0
- package/build/typescript/tests/constants.test.d.ts +2 -0
- package/build/typescript/tests/constants.test.d.ts.map +1 -0
- package/build/typescript/tests/create-element-namespaces.test.d.ts +2 -0
- package/build/typescript/tests/create-element-namespaces.test.d.ts.map +1 -0
- package/build/typescript/tests/create-text-node.test.d.ts +2 -0
- package/build/typescript/tests/create-text-node.test.d.ts.map +1 -0
- package/build/typescript/tests/custom-element-reactions.test.d.ts +2 -0
- package/build/typescript/tests/custom-element-reactions.test.d.ts.map +1 -0
- package/build/typescript/tests/custom-element-registry.test.d.ts +2 -0
- package/build/typescript/tests/custom-element-registry.test.d.ts.map +1 -0
- package/build/typescript/tests/custom-event.test.d.ts +2 -0
- package/build/typescript/tests/custom-event.test.d.ts.map +1 -0
- package/build/typescript/tests/document-clone-import-contract.test.d.ts +2 -0
- package/build/typescript/tests/document-clone-import-contract.test.d.ts.map +1 -0
- package/build/typescript/tests/document.test.d.ts +2 -0
- package/build/typescript/tests/document.test.d.ts.map +1 -0
- package/build/typescript/tests/dom-mutation-selector-errors.test.d.ts +2 -0
- package/build/typescript/tests/dom-mutation-selector-errors.test.d.ts.map +1 -0
- package/build/typescript/tests/event-dispatch.test.d.ts +2 -0
- package/build/typescript/tests/event-dispatch.test.d.ts.map +1 -0
- package/build/typescript/tests/event-target.test.d.ts +2 -0
- package/build/typescript/tests/event-target.test.d.ts.map +1 -0
- package/build/typescript/tests/event.test.d.ts +2 -0
- package/build/typescript/tests/event.test.d.ts.map +1 -0
- package/build/typescript/tests/extensions.test.d.ts +2 -0
- package/build/typescript/tests/extensions.test.d.ts.map +1 -0
- package/build/typescript/tests/get-elements-by-tag-name.test.d.ts +2 -0
- package/build/typescript/tests/get-elements-by-tag-name.test.d.ts.map +1 -0
- package/build/typescript/tests/getElementsByClassName.test.d.ts +2 -0
- package/build/typescript/tests/getElementsByClassName.test.d.ts.map +1 -0
- package/build/typescript/tests/hooks.test.d.ts +2 -0
- package/build/typescript/tests/hooks.test.d.ts.map +1 -0
- package/build/typescript/tests/inner-html-parsing.test.d.ts +2 -0
- package/build/typescript/tests/inner-html-parsing.test.d.ts.map +1 -0
- package/build/typescript/tests/is-default-namespace.test.d.ts +2 -0
- package/build/typescript/tests/is-default-namespace.test.d.ts.map +1 -0
- package/build/typescript/tests/name-and-namespace-normalization.test.d.ts +2 -0
- package/build/typescript/tests/name-and-namespace-normalization.test.d.ts.map +1 -0
- package/build/typescript/tests/named-node-map.test.d.ts +2 -0
- package/build/typescript/tests/named-node-map.test.d.ts.map +1 -0
- package/build/typescript/tests/namespaced-attribute-reactions.test.d.ts +2 -0
- package/build/typescript/tests/namespaced-attribute-reactions.test.d.ts.map +1 -0
- package/build/typescript/tests/node.test.d.ts +2 -0
- package/build/typescript/tests/node.test.d.ts.map +1 -0
- package/build/typescript/tests/parent-node-mutation-return-values.test.d.ts +2 -0
- package/build/typescript/tests/parent-node-mutation-return-values.test.d.ts.map +1 -0
- package/build/typescript/tests/parent-node-mutation-validation.test.d.ts +2 -0
- package/build/typescript/tests/parent-node-mutation-validation.test.d.ts.map +1 -0
- package/build/typescript/tests/parent-node.test.d.ts +2 -0
- package/build/typescript/tests/parent-node.test.d.ts.map +1 -0
- package/build/typescript/tests/serialization-template.test.d.ts +2 -0
- package/build/typescript/tests/serialization-template.test.d.ts.map +1 -0
- package/build/typescript/tests/slot-attribute.test.d.ts +2 -0
- package/build/typescript/tests/slot-attribute.test.d.ts.map +1 -0
- package/build/typescript/tests/text-content.test.d.ts +2 -0
- package/build/typescript/tests/text-content.test.d.ts.map +1 -0
- package/build/typescript/tests/toggle-attribute.test.d.ts +2 -0
- package/build/typescript/tests/toggle-attribute.test.d.ts.map +1 -0
- package/build/typescript/tests/wide-tree-traversal.test.d.ts +2 -0
- package/build/typescript/tests/wide-tree-traversal.test.d.ts.map +1 -0
- package/build/typescript/tests/window-set-global.test.d.ts +2 -0
- package/build/typescript/tests/window-set-global.test.d.ts.map +1 -0
- package/package.json +1 -1
- package/source/Attr.ts +60 -8
- package/source/CharacterData.ts +13 -0
- package/source/ChildNode.ts +107 -14
- package/source/Comment.ts +2 -2
- package/source/CustomElementRegistry.ts +56 -0
- package/source/CustomEvent.ts +2 -2
- package/source/Document.ts +104 -38
- package/source/DocumentFragment.ts +12 -2
- package/source/Element.ts +129 -14
- package/source/Event.ts +34 -32
- package/source/EventTarget.ts +203 -79
- package/source/HTMLTemplateElement.ts +2 -1
- package/source/MutationObserver.ts +279 -1
- package/source/NamedNodeMap.ts +141 -60
- package/source/Node.ts +70 -14
- package/source/NodeList.ts +13 -3
- package/source/ParentNode.ts +500 -88
- package/source/SVGElement.ts +2 -2
- package/source/Text.ts +2 -2
- package/source/Window.ts +156 -17
- package/source/attribute-reactions.ts +32 -0
- package/source/constants.ts +40 -17
- package/source/custom-element-reactions.ts +64 -0
- package/source/dom-exception.ts +14 -0
- package/source/extensions.ts +4 -0
- package/source/hook-effects.ts +56 -0
- package/source/hooks.ts +11 -3
- package/source/index.ts +1 -0
- package/source/names.ts +94 -0
- package/source/selectors.ts +301 -138
- package/source/serialization.ts +224 -66
- package/source/shared.ts +269 -40
- package/source/tests/ChildNode.test.ts +178 -0
- package/source/tests/MutationObserver.test.ts +303 -0
- package/source/tests/adopt-inserted-subtrees.test.ts +584 -0
- package/source/tests/attr-mutation.test.ts +112 -0
- package/source/tests/child-node-before-after.test.ts +421 -0
- package/source/tests/child-node-replace-with.test.ts +251 -0
- package/source/tests/clone-import-namespaces.test.ts +365 -0
- package/source/tests/constants.test.ts +46 -0
- package/source/tests/create-element-namespaces.test.ts +142 -0
- package/source/tests/create-text-node.test.ts +38 -0
- package/source/tests/custom-element-reactions.test.ts +600 -0
- package/source/tests/custom-element-registry.test.ts +137 -0
- package/source/tests/custom-event.test.ts +26 -0
- package/source/tests/document-clone-import-contract.test.ts +77 -0
- package/source/tests/document.test.ts +122 -0
- package/source/tests/dom-mutation-selector-errors.test.ts +484 -0
- package/source/tests/event-dispatch.test.ts +270 -0
- package/source/tests/event-target.test.ts +208 -0
- package/source/tests/event.test.ts +98 -0
- package/source/tests/extensions.test.ts +159 -0
- package/source/tests/get-elements-by-tag-name.test.ts +79 -0
- package/source/tests/getElementsByClassName.test.ts +109 -0
- package/source/tests/hooks.test.ts +43 -0
- package/source/tests/inner-html-parsing.test.ts +461 -0
- package/source/tests/is-default-namespace.test.ts +111 -0
- package/source/tests/name-and-namespace-normalization.test.ts +319 -0
- package/source/tests/named-node-map.test.ts +122 -0
- package/source/tests/namespaced-attribute-reactions.test.ts +141 -0
- package/source/tests/node.test.ts +56 -0
- package/source/tests/parent-node-mutation-return-values.test.ts +63 -0
- package/source/tests/parent-node-mutation-validation.test.ts +362 -0
- package/source/tests/parent-node.test.ts +169 -0
- package/source/tests/selectors.test.ts +533 -22
- package/source/tests/serialization-template.test.ts +232 -0
- package/source/tests/slot-attribute.test.ts +133 -0
- package/source/tests/text-content.test.ts +440 -0
- package/source/tests/toggle-attribute.test.ts +113 -0
- package/source/tests/wide-tree-traversal.test.ts +699 -0
- package/source/tests/window-set-global.test.ts +321 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it} from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {CHILD} from '../constants.ts';
|
|
4
|
+
import {HOOKS, HTMLElement as PolyfillHTMLElement, Window} from '../index.ts';
|
|
5
|
+
|
|
6
|
+
const WIDE_TREE_SIZE = 10_000;
|
|
7
|
+
|
|
8
|
+
let polyfillWindow: Window;
|
|
9
|
+
let polyfillDocument: Window['document'];
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
polyfillWindow = new Window();
|
|
13
|
+
Window.setGlobalThis(polyfillWindow);
|
|
14
|
+
polyfillDocument = polyfillWindow.document;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('wide-tree traversal', () => {
|
|
18
|
+
it('collects text from wide sibling lists without overflowing the stack', () => {
|
|
19
|
+
const root = polyfillDocument.createElement('div');
|
|
20
|
+
|
|
21
|
+
for (let index = 0; index < WIDE_TREE_SIZE; index++) {
|
|
22
|
+
root.appendChild(polyfillDocument.createTextNode('x'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
expect(root.textContent).toBe('x'.repeat(WIDE_TREE_SIZE));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('queries wide sibling lists without overflowing the stack', () => {
|
|
29
|
+
const root = polyfillDocument.createElement('div');
|
|
30
|
+
let last;
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < WIDE_TREE_SIZE; index++) {
|
|
33
|
+
const child = polyfillDocument.createElement('span');
|
|
34
|
+
if (index === WIDE_TREE_SIZE - 1) child.id = 'last-wide-child';
|
|
35
|
+
root.appendChild(child);
|
|
36
|
+
last = child;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
expect(root.querySelectorAll('span')).toHaveLength(WIDE_TREE_SIZE);
|
|
40
|
+
expect(root.querySelector('#last-wide-child')).toBe(last);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('completes connectivity, hooks, and reactions for wide subtree insertion', () => {
|
|
44
|
+
let connected = 0;
|
|
45
|
+
let disconnected = 0;
|
|
46
|
+
|
|
47
|
+
class WideTreeElement extends PolyfillHTMLElement {
|
|
48
|
+
connectedCallback() {
|
|
49
|
+
connected++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
disconnectedCallback() {
|
|
53
|
+
disconnected++;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
polyfillWindow.customElements.define(
|
|
58
|
+
'wide-tree-element',
|
|
59
|
+
WideTreeElement as unknown as CustomElementConstructor,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const root = polyfillDocument.createElement('div');
|
|
63
|
+
for (let index = 0; index < WIDE_TREE_SIZE; index++) {
|
|
64
|
+
root.appendChild(polyfillDocument.createElement('wide-tree-element'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const first = root.firstChild!;
|
|
68
|
+
const last = root.lastChild!;
|
|
69
|
+
const hooks: string[] = [];
|
|
70
|
+
polyfillWindow[HOOKS].insertChild = () => hooks.push('insert');
|
|
71
|
+
polyfillWindow[HOOKS].removeChild = () => hooks.push('remove');
|
|
72
|
+
|
|
73
|
+
polyfillDocument.body.appendChild(root);
|
|
74
|
+
|
|
75
|
+
expect(hooks).toEqual(['insert']);
|
|
76
|
+
expect(connected).toBe(WIDE_TREE_SIZE);
|
|
77
|
+
expect(root.isConnected).toBe(true);
|
|
78
|
+
expect(first.isConnected).toBe(true);
|
|
79
|
+
expect(last.isConnected).toBe(true);
|
|
80
|
+
|
|
81
|
+
polyfillDocument.body.removeChild(root);
|
|
82
|
+
|
|
83
|
+
expect(hooks).toEqual(['insert', 'remove']);
|
|
84
|
+
expect(disconnected).toBe(WIDE_TREE_SIZE);
|
|
85
|
+
expect(root.isConnected).toBe(false);
|
|
86
|
+
expect(first.isConnected).toBe(false);
|
|
87
|
+
expect(last.isConnected).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('keeps custom-element reactions in document preorder', () => {
|
|
91
|
+
const reactions: string[] = [];
|
|
92
|
+
|
|
93
|
+
class OrderedElement extends PolyfillHTMLElement {
|
|
94
|
+
connectedCallback() {
|
|
95
|
+
reactions.push(this.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
polyfillWindow.customElements.define(
|
|
100
|
+
'ordered-element',
|
|
101
|
+
OrderedElement as unknown as CustomElementConstructor,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const root = polyfillDocument.createElement('ordered-element');
|
|
105
|
+
const left = polyfillDocument.createElement('ordered-element');
|
|
106
|
+
const grandchild = polyfillDocument.createElement('ordered-element');
|
|
107
|
+
const right = polyfillDocument.createElement('ordered-element');
|
|
108
|
+
root.id = 'root';
|
|
109
|
+
left.id = 'left';
|
|
110
|
+
grandchild.id = 'grandchild';
|
|
111
|
+
right.id = 'right';
|
|
112
|
+
left.appendChild(grandchild);
|
|
113
|
+
root.append(left, right);
|
|
114
|
+
|
|
115
|
+
polyfillDocument.body.appendChild(root);
|
|
116
|
+
|
|
117
|
+
expect(reactions).toEqual(['root', 'left', 'grandchild', 'right']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('leaves links, connectivity, and hooks unchanged when insertion traversal fails', () => {
|
|
121
|
+
const source = polyfillDocument.createElement('div');
|
|
122
|
+
const destination = polyfillDocument.createElement('div');
|
|
123
|
+
const subtree = polyfillDocument.createElement('section');
|
|
124
|
+
const descendant = polyfillDocument.createElement('span');
|
|
125
|
+
subtree.appendChild(descendant);
|
|
126
|
+
source.appendChild(subtree);
|
|
127
|
+
polyfillDocument.body.append(source, destination);
|
|
128
|
+
|
|
129
|
+
const traversalError = new Error('traversal failed');
|
|
130
|
+
Object.defineProperty(subtree, CHILD, {
|
|
131
|
+
get() {
|
|
132
|
+
throw traversalError;
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const hooks: string[] = [];
|
|
137
|
+
polyfillWindow[HOOKS].insertChild = () => hooks.push('insert');
|
|
138
|
+
polyfillWindow[HOOKS].removeChild = () => hooks.push('remove');
|
|
139
|
+
|
|
140
|
+
expect(() => destination.appendChild(subtree)).toThrow(traversalError);
|
|
141
|
+
|
|
142
|
+
expect(hooks).toEqual([]);
|
|
143
|
+
expect([...source.childNodes]).toEqual([subtree]);
|
|
144
|
+
expect(destination.childNodes).toHaveLength(0);
|
|
145
|
+
expect(subtree.parentNode).toBe(source);
|
|
146
|
+
expect(subtree.previousSibling).toBeNull();
|
|
147
|
+
expect(subtree.nextSibling).toBeNull();
|
|
148
|
+
expect(subtree.isConnected).toBe(true);
|
|
149
|
+
expect(descendant.parentNode).toBe(subtree);
|
|
150
|
+
expect(descendant.isConnected).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('keeps a connected source intact across repeated failed moves to a disconnected parent', () => {
|
|
154
|
+
const source = polyfillDocument.createElement('div');
|
|
155
|
+
const destination = polyfillDocument.createElement('div');
|
|
156
|
+
const subtree = polyfillDocument.createElement('section');
|
|
157
|
+
const descendant = polyfillDocument.createElement('span');
|
|
158
|
+
subtree.appendChild(descendant);
|
|
159
|
+
source.appendChild(subtree);
|
|
160
|
+
polyfillDocument.body.appendChild(source);
|
|
161
|
+
|
|
162
|
+
const traversalError = new Error('traversal failed');
|
|
163
|
+
Object.defineProperty(subtree, CHILD, {
|
|
164
|
+
get() {
|
|
165
|
+
throw traversalError;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const hooks: string[] = [];
|
|
170
|
+
polyfillWindow[HOOKS].insertChild = () => hooks.push('insert');
|
|
171
|
+
polyfillWindow[HOOKS].removeChild = () => hooks.push('remove');
|
|
172
|
+
|
|
173
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
174
|
+
expect(() => destination.appendChild(subtree)).toThrow(traversalError);
|
|
175
|
+
expect([...source.childNodes]).toEqual([subtree]);
|
|
176
|
+
expect(destination.childNodes).toHaveLength(0);
|
|
177
|
+
expect(subtree.parentNode).toBe(source);
|
|
178
|
+
expect(subtree.isConnected).toBe(true);
|
|
179
|
+
expect(descendant.isConnected).toBe(true);
|
|
180
|
+
}
|
|
181
|
+
expect(hooks).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('reuses one subtree snapshot for source and destination connectivity', () => {
|
|
185
|
+
const source = polyfillDocument.createElement('div');
|
|
186
|
+
const destination = polyfillDocument.createElement('div');
|
|
187
|
+
const subtree = polyfillDocument.createElement('section');
|
|
188
|
+
const descendant = polyfillDocument.createElement('span');
|
|
189
|
+
subtree.appendChild(descendant);
|
|
190
|
+
source.appendChild(subtree);
|
|
191
|
+
polyfillDocument.body.append(source, destination);
|
|
192
|
+
|
|
193
|
+
const traversalError = new Error('subtree traversed twice');
|
|
194
|
+
const child = subtree[CHILD];
|
|
195
|
+
let childReads = 0;
|
|
196
|
+
Object.defineProperty(subtree, CHILD, {
|
|
197
|
+
get() {
|
|
198
|
+
childReads++;
|
|
199
|
+
if (childReads > 1) throw traversalError;
|
|
200
|
+
return child;
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(() => destination.appendChild(subtree)).not.toThrow();
|
|
205
|
+
expect(childReads).toBe(1);
|
|
206
|
+
expect(source.childNodes).toHaveLength(0);
|
|
207
|
+
expect([...destination.childNodes]).toEqual([subtree]);
|
|
208
|
+
expect(subtree.isConnected).toBe(true);
|
|
209
|
+
expect(descendant.isConnected).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('preflights replacement traversal before removing the old child', () => {
|
|
213
|
+
const source = polyfillDocument.createElement('div');
|
|
214
|
+
const parent = polyfillDocument.createElement('div');
|
|
215
|
+
const replacement = polyfillDocument.createElement('section');
|
|
216
|
+
const replacementChild = polyfillDocument.createElement('span');
|
|
217
|
+
const oldChild = polyfillDocument.createElement('p');
|
|
218
|
+
replacement.appendChild(replacementChild);
|
|
219
|
+
source.appendChild(replacement);
|
|
220
|
+
parent.appendChild(oldChild);
|
|
221
|
+
polyfillDocument.body.append(source, parent);
|
|
222
|
+
|
|
223
|
+
const traversalError = new Error('replacement traversal failed');
|
|
224
|
+
Object.defineProperty(replacement, CHILD, {
|
|
225
|
+
get() {
|
|
226
|
+
throw traversalError;
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const hooks: string[] = [];
|
|
231
|
+
polyfillWindow[HOOKS].insertChild = () => hooks.push('insert');
|
|
232
|
+
polyfillWindow[HOOKS].removeChild = () => hooks.push('remove');
|
|
233
|
+
|
|
234
|
+
expect(() => parent.replaceChild(replacement, oldChild)).toThrow(
|
|
235
|
+
traversalError,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(hooks).toEqual([]);
|
|
239
|
+
expect([...source.childNodes]).toEqual([replacement]);
|
|
240
|
+
expect([...parent.childNodes]).toEqual([oldChild]);
|
|
241
|
+
expect(replacement.parentNode).toBe(source);
|
|
242
|
+
expect(replacement.isConnected).toBe(true);
|
|
243
|
+
expect(replacementChild.isConnected).toBe(true);
|
|
244
|
+
expect(oldChild.parentNode).toBe(parent);
|
|
245
|
+
expect(oldChild.isConnected).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('preflights every fragment child before consuming the fragment', () => {
|
|
249
|
+
const destination = polyfillDocument.createElement('div');
|
|
250
|
+
const existing = polyfillDocument.createElement('p');
|
|
251
|
+
const fragment = polyfillDocument.createDocumentFragment();
|
|
252
|
+
const first = polyfillDocument.createElement('section');
|
|
253
|
+
const second = polyfillDocument.createElement('section');
|
|
254
|
+
destination.appendChild(existing);
|
|
255
|
+
fragment.append(first, second);
|
|
256
|
+
polyfillDocument.body.appendChild(destination);
|
|
257
|
+
|
|
258
|
+
const traversalError = new Error('later fragment child traversal failed');
|
|
259
|
+
Object.defineProperty(second, CHILD, {
|
|
260
|
+
get() {
|
|
261
|
+
throw traversalError;
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const hooks: string[] = [];
|
|
266
|
+
polyfillWindow[HOOKS].insertChild = () => hooks.push('insert');
|
|
267
|
+
polyfillWindow[HOOKS].removeChild = () => hooks.push('remove');
|
|
268
|
+
|
|
269
|
+
expect(() => destination.appendChild(fragment)).toThrow(traversalError);
|
|
270
|
+
|
|
271
|
+
expect(hooks).toEqual([]);
|
|
272
|
+
expect([...fragment.childNodes]).toEqual([first, second]);
|
|
273
|
+
expect([...destination.childNodes]).toEqual([existing]);
|
|
274
|
+
expect(first.parentNode).toBe(fragment);
|
|
275
|
+
expect(second.parentNode).toBe(fragment);
|
|
276
|
+
expect(first.isConnected).toBe(false);
|
|
277
|
+
expect(second.isConnected).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('runs removal-hook mutations against the committed destination tree', () => {
|
|
281
|
+
const events: string[] = [];
|
|
282
|
+
|
|
283
|
+
class TransactionElement extends PolyfillHTMLElement {
|
|
284
|
+
connectedCallback() {
|
|
285
|
+
events.push(`${this.id}:connected:${this.isConnected}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
disconnectedCallback() {
|
|
289
|
+
events.push(`${this.id}:disconnected:${this.isConnected}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
polyfillWindow.customElements.define(
|
|
294
|
+
'transaction-element',
|
|
295
|
+
TransactionElement as unknown as CustomElementConstructor,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const source = polyfillDocument.createElement('div');
|
|
299
|
+
const destination = polyfillDocument.createElement('div');
|
|
300
|
+
const subtree = polyfillDocument.createElement('transaction-element');
|
|
301
|
+
const existing = polyfillDocument.createElement('transaction-element');
|
|
302
|
+
const added = polyfillDocument.createElement('transaction-element');
|
|
303
|
+
source.id = 'source';
|
|
304
|
+
destination.id = 'destination';
|
|
305
|
+
subtree.id = 'subtree';
|
|
306
|
+
existing.id = 'existing';
|
|
307
|
+
added.id = 'added';
|
|
308
|
+
subtree.appendChild(existing);
|
|
309
|
+
source.appendChild(subtree);
|
|
310
|
+
polyfillDocument.body.append(source, destination);
|
|
311
|
+
events.length = 0;
|
|
312
|
+
|
|
313
|
+
polyfillWindow[HOOKS].removeChild = (parent, child) => {
|
|
314
|
+
events.push(`remove:${parent.id}:${(child as Element).id}`);
|
|
315
|
+
if (parent === (source as any) && child === (subtree as any)) {
|
|
316
|
+
expect(subtree.parentNode).toBe(destination);
|
|
317
|
+
expect(subtree.isConnected).toBe(true);
|
|
318
|
+
expect(existing.isConnected).toBe(true);
|
|
319
|
+
subtree.appendChild(added);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
polyfillWindow[HOOKS].insertChild = (parent, child) => {
|
|
323
|
+
events.push(`insert:${parent.id}:${(child as Element).id}`);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
destination.appendChild(subtree);
|
|
327
|
+
|
|
328
|
+
expect(subtree.parentNode).toBe(destination);
|
|
329
|
+
expect(added.parentNode).toBe(subtree);
|
|
330
|
+
expect(added.isConnected).toBe(true);
|
|
331
|
+
expect(events).toEqual([
|
|
332
|
+
'remove:source:subtree',
|
|
333
|
+
'added:connected:true',
|
|
334
|
+
'insert:destination:subtree',
|
|
335
|
+
'subtree:disconnected:true',
|
|
336
|
+
'subtree:connected:true',
|
|
337
|
+
'existing:disconnected:true',
|
|
338
|
+
'existing:connected:true',
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('preserves lifecycle chronology for a reentrant destination removal', () => {
|
|
343
|
+
const events: string[] = [];
|
|
344
|
+
|
|
345
|
+
class ReentrantMoveElement extends PolyfillHTMLElement {
|
|
346
|
+
connectedCallback() {
|
|
347
|
+
events.push(`callback:connected:${this.isConnected}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
disconnectedCallback() {
|
|
351
|
+
events.push(`callback:disconnected:${this.isConnected}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
polyfillWindow.customElements.define(
|
|
356
|
+
'reentrant-move-element',
|
|
357
|
+
ReentrantMoveElement as unknown as CustomElementConstructor,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const source = polyfillDocument.createElement('div');
|
|
361
|
+
const destination = polyfillDocument.createElement('div');
|
|
362
|
+
const child = polyfillDocument.createElement('reentrant-move-element');
|
|
363
|
+
source.id = 'source';
|
|
364
|
+
destination.id = 'destination';
|
|
365
|
+
child.id = 'child';
|
|
366
|
+
source.appendChild(child);
|
|
367
|
+
polyfillDocument.body.append(source, destination);
|
|
368
|
+
events.length = 0;
|
|
369
|
+
|
|
370
|
+
polyfillWindow[HOOKS].removeChild = (parent, node) => {
|
|
371
|
+
events.push(`hook:remove:${parent.id}:${(node as Element).id}`);
|
|
372
|
+
if (parent === (source as any)) destination.removeChild(child);
|
|
373
|
+
};
|
|
374
|
+
polyfillWindow[HOOKS].insertChild = (parent, node) => {
|
|
375
|
+
events.push(`hook:insert:${parent.id}:${(node as Element).id}`);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
destination.appendChild(child);
|
|
379
|
+
|
|
380
|
+
expect(events).toEqual([
|
|
381
|
+
'hook:remove:source:child',
|
|
382
|
+
'callback:disconnected:false',
|
|
383
|
+
'callback:connected:false',
|
|
384
|
+
'callback:disconnected:false',
|
|
385
|
+
'hook:insert:destination:child',
|
|
386
|
+
'hook:remove:destination:child',
|
|
387
|
+
]);
|
|
388
|
+
expect(source.childNodes).toHaveLength(0);
|
|
389
|
+
expect(destination.childNodes).toHaveLength(0);
|
|
390
|
+
expect(child.parentNode).toBeNull();
|
|
391
|
+
expect(child.isConnected).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('folds an insertion into a pending subtree publication', () => {
|
|
395
|
+
const destination = polyfillDocument.createElement('div');
|
|
396
|
+
const fragment = polyfillDocument.createDocumentFragment();
|
|
397
|
+
const first = polyfillDocument.createElement('section');
|
|
398
|
+
const second = polyfillDocument.createElement('section');
|
|
399
|
+
const added = polyfillDocument.createElement('span');
|
|
400
|
+
destination.id = 'destination';
|
|
401
|
+
first.id = 'first';
|
|
402
|
+
second.id = 'second';
|
|
403
|
+
added.id = 'added';
|
|
404
|
+
fragment.append(first, second);
|
|
405
|
+
polyfillDocument.body.appendChild(destination);
|
|
406
|
+
|
|
407
|
+
const hooks: string[] = [];
|
|
408
|
+
polyfillWindow[HOOKS].insertChild = (parent, node) => {
|
|
409
|
+
hooks.push(`insert:${parent.id}:${(node as Element).id}`);
|
|
410
|
+
if (node === (first as any)) second.appendChild(added);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
destination.appendChild(fragment);
|
|
414
|
+
|
|
415
|
+
expect(hooks).toEqual([
|
|
416
|
+
'insert:destination:first',
|
|
417
|
+
'insert:destination:second',
|
|
418
|
+
]);
|
|
419
|
+
expect([...second.childNodes]).toEqual([added]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('folds a removal into a pending subtree publication', () => {
|
|
423
|
+
const destination = polyfillDocument.createElement('div');
|
|
424
|
+
const fragment = polyfillDocument.createDocumentFragment();
|
|
425
|
+
const first = polyfillDocument.createElement('section');
|
|
426
|
+
const second = polyfillDocument.createElement('section');
|
|
427
|
+
const removed = polyfillDocument.createElement('span');
|
|
428
|
+
const kept = polyfillDocument.createElement('span');
|
|
429
|
+
first.id = 'first';
|
|
430
|
+
second.id = 'second';
|
|
431
|
+
removed.id = 'removed';
|
|
432
|
+
kept.id = 'kept';
|
|
433
|
+
second.append(removed, kept);
|
|
434
|
+
fragment.append(first, second);
|
|
435
|
+
polyfillDocument.body.appendChild(destination);
|
|
436
|
+
|
|
437
|
+
const hooks: string[] = [];
|
|
438
|
+
polyfillWindow[HOOKS].insertChild = (_parent, node) => {
|
|
439
|
+
hooks.push(`insert:${(node as Element).id}`);
|
|
440
|
+
if (node === (first as any)) second.removeChild(removed);
|
|
441
|
+
};
|
|
442
|
+
polyfillWindow[HOOKS].removeChild = (_parent, node) => {
|
|
443
|
+
hooks.push(`remove:${(node as Element).id}`);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
destination.appendChild(fragment);
|
|
447
|
+
|
|
448
|
+
expect(hooks).toEqual(['insert:first', 'insert:second']);
|
|
449
|
+
expect([...second.childNodes]).toEqual([kept]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('emits every outer fragment insertion before a reentrant insertion', () => {
|
|
453
|
+
const destination = polyfillDocument.createElement('div');
|
|
454
|
+
const fragment = polyfillDocument.createDocumentFragment();
|
|
455
|
+
const first = polyfillDocument.createElement('span');
|
|
456
|
+
const second = polyfillDocument.createElement('span');
|
|
457
|
+
const added = polyfillDocument.createElement('span');
|
|
458
|
+
destination.id = 'destination';
|
|
459
|
+
first.id = 'first';
|
|
460
|
+
second.id = 'second';
|
|
461
|
+
added.id = 'added';
|
|
462
|
+
fragment.append(first, second);
|
|
463
|
+
polyfillDocument.body.appendChild(destination);
|
|
464
|
+
|
|
465
|
+
const hooks: string[] = [];
|
|
466
|
+
polyfillWindow[HOOKS].insertChild = (parent, node) => {
|
|
467
|
+
hooks.push(`insert:${parent.id}:${(node as Element).id}`);
|
|
468
|
+
if (node === (first as any)) destination.appendChild(added);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
destination.appendChild(fragment);
|
|
472
|
+
|
|
473
|
+
expect(hooks).toEqual([
|
|
474
|
+
'insert:destination:first',
|
|
475
|
+
'insert:destination:second',
|
|
476
|
+
'insert:destination:added',
|
|
477
|
+
]);
|
|
478
|
+
expect([...destination.childNodes]).toEqual([first, second, added]);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('queues replacement insertion before a reentrant removal', () => {
|
|
482
|
+
const parent = polyfillDocument.createElement('div');
|
|
483
|
+
const oldChild = polyfillDocument.createElement('span');
|
|
484
|
+
const replacement = polyfillDocument.createElement('span');
|
|
485
|
+
parent.id = 'parent';
|
|
486
|
+
oldChild.id = 'old';
|
|
487
|
+
replacement.id = 'replacement';
|
|
488
|
+
parent.appendChild(oldChild);
|
|
489
|
+
polyfillDocument.body.appendChild(parent);
|
|
490
|
+
|
|
491
|
+
const hooks: string[] = [];
|
|
492
|
+
polyfillWindow[HOOKS].removeChild = (hookParent, node) => {
|
|
493
|
+
hooks.push(`remove:${hookParent.id}:${(node as Element).id}`);
|
|
494
|
+
if (node === (oldChild as any)) parent.removeChild(replacement);
|
|
495
|
+
};
|
|
496
|
+
polyfillWindow[HOOKS].insertChild = (hookParent, node) => {
|
|
497
|
+
hooks.push(`insert:${hookParent.id}:${(node as Element).id}`);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
parent.replaceChild(replacement, oldChild);
|
|
501
|
+
|
|
502
|
+
expect(hooks).toEqual([
|
|
503
|
+
'remove:parent:old',
|
|
504
|
+
'insert:parent:replacement',
|
|
505
|
+
'remove:parent:replacement',
|
|
506
|
+
]);
|
|
507
|
+
expect(parent.childNodes).toHaveLength(0);
|
|
508
|
+
expect(oldChild.parentNode).toBeNull();
|
|
509
|
+
expect(replacement.parentNode).toBeNull();
|
|
510
|
+
expect(replacement.isConnected).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('finishes queued hook effects before rethrowing a nested hook error', () => {
|
|
514
|
+
const error = new Error('hook failed');
|
|
515
|
+
const destination = polyfillDocument.createElement('div');
|
|
516
|
+
const fragment = polyfillDocument.createDocumentFragment();
|
|
517
|
+
const first = polyfillDocument.createElement('span');
|
|
518
|
+
const second = polyfillDocument.createElement('span');
|
|
519
|
+
const added = polyfillDocument.createElement('span');
|
|
520
|
+
first.id = 'first';
|
|
521
|
+
second.id = 'second';
|
|
522
|
+
added.id = 'added';
|
|
523
|
+
fragment.append(first, second);
|
|
524
|
+
polyfillDocument.body.appendChild(destination);
|
|
525
|
+
|
|
526
|
+
const hooks: string[] = [];
|
|
527
|
+
polyfillWindow[HOOKS].insertChild = (_parent, node) => {
|
|
528
|
+
hooks.push((node as Element).id);
|
|
529
|
+
if (node === (first as any)) {
|
|
530
|
+
destination.appendChild(added);
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
expect(() => destination.appendChild(fragment)).toThrow(error);
|
|
536
|
+
|
|
537
|
+
expect(hooks).toEqual(['first', 'second', 'added']);
|
|
538
|
+
expect([...destination.childNodes]).toEqual([first, second, added]);
|
|
539
|
+
expect(first.isConnected).toBe(true);
|
|
540
|
+
expect(second.isConnected).toBe(true);
|
|
541
|
+
expect(added.isConnected).toBe(true);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('flushes earned insertion reactions while preserving the first hook error', () => {
|
|
545
|
+
const hookError = new Error('insertion hook failed');
|
|
546
|
+
const callbackError = new Error('connected callback failed');
|
|
547
|
+
const events: string[] = [];
|
|
548
|
+
|
|
549
|
+
class ThrowingInsertionElement extends PolyfillHTMLElement {
|
|
550
|
+
connectedCallback() {
|
|
551
|
+
events.push(`callback:${this.id}`);
|
|
552
|
+
if (this.id === 'first') throw callbackError;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
polyfillWindow.customElements.define(
|
|
557
|
+
'throwing-insertion-element',
|
|
558
|
+
ThrowingInsertionElement as unknown as CustomElementConstructor,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const destination = polyfillDocument.createElement('div');
|
|
562
|
+
const fragment = polyfillDocument.createDocumentFragment();
|
|
563
|
+
const first = polyfillDocument.createElement('throwing-insertion-element');
|
|
564
|
+
const second = polyfillDocument.createElement('throwing-insertion-element');
|
|
565
|
+
first.id = 'first';
|
|
566
|
+
second.id = 'second';
|
|
567
|
+
fragment.append(first, second);
|
|
568
|
+
polyfillDocument.body.appendChild(destination);
|
|
569
|
+
|
|
570
|
+
polyfillWindow[HOOKS].insertChild = (_parent, node) => {
|
|
571
|
+
const id = (node as Element).id;
|
|
572
|
+
events.push(`hook:${id}`);
|
|
573
|
+
if (id === 'first') throw hookError;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
let thrown: unknown;
|
|
577
|
+
try {
|
|
578
|
+
destination.appendChild(fragment);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
thrown = error;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
expect(thrown).toBe(hookError);
|
|
584
|
+
expect(events).toEqual([
|
|
585
|
+
'hook:first',
|
|
586
|
+
'hook:second',
|
|
587
|
+
'callback:first',
|
|
588
|
+
'callback:second',
|
|
589
|
+
]);
|
|
590
|
+
expect(first.isConnected).toBe(true);
|
|
591
|
+
expect(second.isConnected).toBe(true);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('flushes an earned disconnection reaction when its removal hook throws', () => {
|
|
595
|
+
const hookError = new Error('removal hook failed');
|
|
596
|
+
const events: string[] = [];
|
|
597
|
+
|
|
598
|
+
class ThrowingRemovalElement extends PolyfillHTMLElement {
|
|
599
|
+
disconnectedCallback() {
|
|
600
|
+
events.push(`callback:disconnected:${this.isConnected}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
polyfillWindow.customElements.define(
|
|
605
|
+
'throwing-removal-element',
|
|
606
|
+
ThrowingRemovalElement as unknown as CustomElementConstructor,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const child = polyfillDocument.createElement('throwing-removal-element');
|
|
610
|
+
polyfillDocument.body.appendChild(child);
|
|
611
|
+
polyfillWindow[HOOKS].removeChild = () => {
|
|
612
|
+
events.push('hook:remove');
|
|
613
|
+
throw hookError;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
expect(() => polyfillDocument.body.removeChild(child)).toThrow(hookError);
|
|
617
|
+
|
|
618
|
+
expect(events).toEqual(['hook:remove', 'callback:disconnected:false']);
|
|
619
|
+
expect(child.parentNode).toBeNull();
|
|
620
|
+
expect(child.isConnected).toBe(false);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('flushes replacement reactions after a throwing removal hook', () => {
|
|
624
|
+
const hookError = new Error('replacement removal hook failed');
|
|
625
|
+
const events: string[] = [];
|
|
626
|
+
|
|
627
|
+
class ThrowingReplacementElement extends PolyfillHTMLElement {
|
|
628
|
+
connectedCallback() {
|
|
629
|
+
events.push(`callback:connected:${this.id}:${this.isConnected}`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
disconnectedCallback() {
|
|
633
|
+
events.push(`callback:disconnected:${this.id}:${this.isConnected}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
polyfillWindow.customElements.define(
|
|
638
|
+
'throwing-replacement-element',
|
|
639
|
+
ThrowingReplacementElement as unknown as CustomElementConstructor,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
const parent = polyfillDocument.createElement('div');
|
|
643
|
+
const oldChild = polyfillDocument.createElement(
|
|
644
|
+
'throwing-replacement-element',
|
|
645
|
+
);
|
|
646
|
+
const replacement = polyfillDocument.createElement(
|
|
647
|
+
'throwing-replacement-element',
|
|
648
|
+
);
|
|
649
|
+
oldChild.id = 'old';
|
|
650
|
+
replacement.id = 'replacement';
|
|
651
|
+
parent.appendChild(oldChild);
|
|
652
|
+
polyfillDocument.body.appendChild(parent);
|
|
653
|
+
events.length = 0;
|
|
654
|
+
|
|
655
|
+
polyfillWindow[HOOKS].removeChild = (_parent, node) => {
|
|
656
|
+
events.push(`hook:remove:${(node as Element).id}`);
|
|
657
|
+
if (node === (oldChild as any)) throw hookError;
|
|
658
|
+
};
|
|
659
|
+
polyfillWindow[HOOKS].insertChild = (_parent, node) => {
|
|
660
|
+
events.push(`hook:insert:${(node as Element).id}`);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
expect(() => parent.replaceChild(replacement, oldChild)).toThrow(hookError);
|
|
664
|
+
|
|
665
|
+
expect(events).toEqual([
|
|
666
|
+
'hook:remove:old',
|
|
667
|
+
'hook:insert:replacement',
|
|
668
|
+
'callback:disconnected:old:false',
|
|
669
|
+
'callback:connected:replacement:true',
|
|
670
|
+
]);
|
|
671
|
+
expect(oldChild.parentNode).toBeNull();
|
|
672
|
+
expect(oldChild.isConnected).toBe(false);
|
|
673
|
+
expect(replacement.parentNode).toBe(parent);
|
|
674
|
+
expect(replacement.isConnected).toBe(true);
|
|
675
|
+
});
|
|
676
|
+
it('traverses a chain deeper than ten thousand nodes without overflowing the stack', () => {
|
|
677
|
+
const root = polyfillDocument.createElement('div');
|
|
678
|
+
let leaf = root;
|
|
679
|
+
|
|
680
|
+
for (let depth = 0; depth < WIDE_TREE_SIZE + 1; depth++) {
|
|
681
|
+
const child = polyfillDocument.createElement('div');
|
|
682
|
+
leaf.appendChild(child);
|
|
683
|
+
leaf = child;
|
|
684
|
+
}
|
|
685
|
+
leaf.id = 'deep-leaf';
|
|
686
|
+
leaf.appendChild(polyfillDocument.createTextNode('deep text'));
|
|
687
|
+
|
|
688
|
+
expect(root.textContent).toBe('deep text');
|
|
689
|
+
expect(root.querySelector('#deep-leaf')).toBe(leaf);
|
|
690
|
+
|
|
691
|
+
polyfillDocument.body.appendChild(root);
|
|
692
|
+
expect(root.isConnected).toBe(true);
|
|
693
|
+
expect(leaf.isConnected).toBe(true);
|
|
694
|
+
|
|
695
|
+
polyfillDocument.body.removeChild(root);
|
|
696
|
+
expect(root.isConnected).toBe(false);
|
|
697
|
+
expect(leaf.isConnected).toBe(false);
|
|
698
|
+
}, 20_000);
|
|
699
|
+
});
|