',
- expected: '',
- },
- {
- name: 'inserts colgroup and preserves attributes on implicitly grouped columns',
- markup: ' ',
- expected: '',
- },
- {
- name: 'closes foreign content when a new cell starts at an HTML integration point',
- markup: '',
- expected: '',
- },
-] as const;
-
-/** Registers malformed-markup tree construction cases across parsing entry points. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- for (const { name, markup, expected } of recoveryCases) {
- context.test(name, (fixture) => {
- const document = fixture.document as unknown as Document;
- const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser;
- const fragment = document.createElement('div');
- fragment.innerHTML = markup;
- expect(fragment.innerHTML).toBe(expected);
- const parsed = new DOMParser().parseFromString(markup, 'text/html');
- expect(parsed.body.innerHTML).toBe(expected);
- // Serialization must be stable after reparsing the repaired tree.
- fragment.innerHTML = expected;
- expect(fragment.innerHTML).toBe(expected);
- });
- }
- context.test('keeps foster-parented nodes inside template content', (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div');
- root.innerHTML = 'outside ';
- const template = root.firstElementChild as HTMLTemplateElement;
- expect(template.innerHTML).toBe('AC');
- expect(template.childNodes.length).toBe(0);
- expect(template.content.firstChild!.nodeValue).toBe('AC');
- expect(Array.from(root.children).map((node) => node.localName)).toEqual(['template', 'i']);
- expect(root.textContent).toBe('outside');
- });
- context.test(
- 'reconstructs formatting independently inside nested template content',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const template = document.createElement('template');
- template.innerHTML = 'outer12 3 end';
- const nested = template.content.querySelector('template')!;
- expect(nested.innerHTML).toBe('12 3 ');
- expect(template.content.firstElementChild!.textContent).toBe('outerend');
- expect(nested.content.querySelector('b')!.ownerDocument).toBe(nested.content.ownerDocument);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/parsing/serialization.ts b/packages/web-platform-tests/src/compatibility/parsing/serialization.ts
deleted file mode 100644
index d94c105..0000000
--- a/packages/web-platform-tests/src/compatibility/parsing/serialization.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-
-export const spec =
- 'https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('escapes markup characters and serializes void elements', ({ document }) => {
- const container = document.createElement('div');
- const span = document.createElement('span');
- span.setAttribute('title', 'a"&b');
- span.textContent = '<&>';
- container.append(span, document.createElement('br'));
- expect(container.innerHTML).toBe('<&> ');
- });
-
- context.test(
- 'escapes text and quoted attribute values without serializing comments as text',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div');
- e.setAttribute('title', 'a"&');
- e.append(document.createTextNode('<>&'), document.createComment('note'));
- expect(e.innerHTML).toBe('<>&');
- expect(e.outerHTML).toContain('title="a"&"');
- },
- );
-
- context.test(
- 'serializes void elements without end tags and preserves raw script text',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div');
- e.innerHTML = ' ';
- expect(e.innerHTML).toBe(' ');
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/registry.ts b/packages/web-platform-tests/src/compatibility/registry.ts
deleted file mode 100644
index f6e1cc0..0000000
--- a/packages/web-platform-tests/src/compatibility/registry.ts
+++ /dev/null
@@ -1,450 +0,0 @@
-import type { FeatureModule } from './context.ts';
-import type { DOMFeaturePath } from './features.ts';
-import * as expansion0 from './document/basic-creation.ts';
-import * as feature12 from './document/creation.ts';
-import * as feature14 from './document/lookup.ts';
-import * as feature13 from './document/import-node.ts';
-import * as expansion1 from './document/xml.ts';
-import * as extra1 from './document/parsing.ts';
-import * as interactionDocumentXmlSerialization from './document/xml-serialization.ts';
-import * as expansion2 from './node/basic-append.ts';
-import * as feature126 from './node/relationships.ts';
-import * as feature122 from './node/connectivity.ts';
-import * as feature125 from './node/live-collections.ts';
-import * as feature129 from './node/text-content.ts';
-import * as feature120 from './node/append.ts';
-import * as feature124 from './node/insert-before.ts';
-import * as feature127 from './node/remove.ts';
-import * as feature128 from './node/replace.ts';
-import * as feature123 from './node/fragments.ts';
-import * as feature130 from './node/variadic-mutation.ts';
-import * as feature121 from './node/cloning.ts';
-import * as expansion3 from './node/comparison.ts';
-import * as expansion4 from './node/normalization.ts';
-import * as feature119 from './node/adoption.ts';
-import * as expansion5 from './character-data/string-storage.ts';
-import * as feature0 from './character-data/data.ts';
-import * as feature1 from './character-data/editing.ts';
-import * as feature2 from './character-data/split-text.ts';
-import * as expansion6 from './element/child-elements.ts';
-import * as feature16 from './element/attributes.ts';
-import * as feature15 from './element/attribute-nodes.ts';
-import * as feature18 from './element/namespaces.ts';
-import * as feature17 from './element/class-list.ts';
-import * as expansion8 from './element/dataset.ts';
-import * as expansion9 from './element/reflection.ts';
-import * as expansion7 from './element/selectors.ts';
-import * as expansion12 from './traversal/node-iterator.ts';
-import * as feature164 from './traversal/tree-walker.ts';
-import * as feature131 from './parsing/fragments.ts';
-import * as feature132 from './parsing/namespaces.ts';
-import * as feature133 from './parsing/serialization.ts';
-import * as expansion10 from './parsing/contextual.ts';
-import * as expansion11 from './parsing/markup-mutation.ts';
-import * as interactionParsingRecovery from './parsing/recovery.ts';
-import * as feature162 from './templates/content.ts';
-import * as feature161 from './templates/cloning.ts';
-import * as feature163 from './templates/parsing.ts';
-import * as feature82 from './events/data/custom.ts';
-import * as feature83 from './events/data/focus.ts';
-import * as feature85 from './events/data/keyboard.ts';
-import * as feature86 from './events/data/mouse.ts';
-import * as feature84 from './events/data/input.ts';
-import * as feature95 from './events/dispatch/target.ts';
-import * as feature89 from './events/dispatch/capture.ts';
-import * as feature87 from './events/dispatch/bubbling.ts';
-import * as feature88 from './events/dispatch/cancellation.ts';
-import * as feature94 from './events/dispatch/stop-propagation.ts';
-import * as feature93 from './events/dispatch/stop-immediate-propagation.ts';
-import * as feature90 from './events/dispatch/path-snapshot.ts';
-import * as feature92 from './events/dispatch/reuse.ts';
-import * as feature91 from './events/dispatch/reentrancy.ts';
-import * as feature101 from './events/listeners/registration.ts';
-import * as feature98 from './events/listeners/objects.ts';
-import * as feature99 from './events/listeners/once.ts';
-import * as feature100 from './events/listeners/passive.ts';
-import * as feature96 from './events/listeners/abort-signal.ts';
-import * as feature102 from './events/listeners/snapshots.ts';
-import * as feature97 from './events/listeners/exception-reporting.ts';
-import * as feature116 from './mutation-observer/registration.ts';
-import * as feature114 from './mutation-observer/records/child-list.ts';
-import * as feature112 from './mutation-observer/records/attributes.ts';
-import * as feature113 from './mutation-observer/records/character-data.ts';
-import * as feature109 from './mutation-observer/filtering.ts';
-import * as feature111 from './mutation-observer/old-value.ts';
-import * as feature117 from './mutation-observer/subtree.ts';
-import * as feature118 from './mutation-observer/take-records.ts';
-import * as feature108 from './mutation-observer/disconnect.ts';
-import * as feature110 from './mutation-observer/microtask-delivery.ts';
-import * as feature107 from './mutation-observer/detached.ts';
-import * as feature115 from './mutation-observer/reentrancy.ts';
-import * as feature103 from './focus/eligibility.ts';
-import * as feature106 from './focus/transitions.ts';
-import * as feature105 from './focus/removal.ts';
-import * as interactionFocusInert from './focus/inert.ts';
-import * as feature104 from './focus/reentrancy.ts';
-import * as feature138 from './selection/range/boundaries.ts';
-import * as feature140 from './selection/range/comparison.ts';
-import * as feature144 from './selection/range/stringification.ts';
-import * as feature139 from './selection/range/clone-contents.ts';
-import * as feature141 from './selection/range/delete-contents.ts';
-import * as feature142 from './selection/range/extract-contents.ts';
-import * as feature143 from './selection/range/live-boundaries.ts';
-import * as extra0 from './selection/range/mutation.ts';
-import * as feature137 from './selection/document/range-management.ts';
-import * as feature135 from './selection/document/direction.ts';
-import * as feature134 from './selection/document/connectivity.ts';
-import * as feature136 from './selection/document/live-updates.ts';
-import * as interactionSelectionDocumentMutation from './selection/document/mutation.ts';
-import * as interactionSelectionDocumentChangeEvents from './selection/document/change-events.ts';
-import * as feature66 from './elements/progress.ts';
-import * as feature58 from './elements/meter.ts';
-import * as feature60 from './elements/option/attributes.ts';
-import * as feature62 from './elements/option/text.ts';
-import * as feature61 from './elements/option/selectedness.ts';
-import * as feature59 from './elements/optgroup.ts';
-import * as feature22 from './elements/datalist.ts';
-import * as feature54 from './elements/input/value.ts';
-import * as feature50 from './elements/input/sanitization.ts';
-import * as feature43 from './elements/input/color-value.ts';
-import * as feature47 from './elements/input/numeric-value.ts';
-import * as feature44 from './elements/input/date-value.ts';
-import * as feature52 from './elements/input/stepping.ts';
-import * as feature41 from './elements/input/checkedness.ts';
-import * as feature48 from './elements/input/radio-groups.ts';
-import * as feature51 from './elements/input/selection.ts';
-import * as feature49 from './elements/input/range-text.ts';
-import * as feature53 from './elements/input/validation.ts';
-import * as feature45 from './elements/input/form-association.ts';
-import * as feature46 from './elements/input/labels.ts';
-import * as feature40 from './elements/input/activation.ts';
-import * as feature42 from './elements/input/cloning.ts';
-import * as feature81 from './elements/textarea/value.ts';
-import * as feature79 from './elements/textarea/selection.ts';
-import * as feature78 from './elements/textarea/range-text.ts';
-import * as feature80 from './elements/textarea/validation.ts';
-import * as feature76 from './elements/textarea/form-association.ts';
-import * as feature77 from './elements/textarea/labels.ts';
-import * as feature75 from './elements/textarea/cloning.ts';
-import * as feature70 from './elements/select/options.ts';
-import * as feature68 from './elements/select/indexed-options.ts';
-import * as feature71 from './elements/select/selection.ts';
-import * as feature69 from './elements/select/mutation.ts';
-import * as feature72 from './elements/select/validation.ts';
-import * as feature67 from './elements/select/cloning.ts';
-import * as feature73 from './elements/selectedcontent.ts';
-import * as feature34 from './elements/form/controls.ts';
-import * as feature36 from './elements/form/named-controls.ts';
-import * as feature37 from './elements/form/reset.ts';
-import * as feature39 from './elements/form/validation.ts';
-import * as feature38 from './elements/form/submission.ts';
-import * as feature35 from './elements/form/form-data.ts';
-import * as interactionElementsFormDataEvents from './elements/form/data-events.ts';
-import * as feature20 from './elements/button/attributes.ts';
-import * as feature21 from './elements/button/form-association.ts';
-import * as feature19 from './elements/button/activation.ts';
-import * as feature56 from './elements/label/association.ts';
-import * as feature55 from './elements/label/activation.ts';
-import * as feature32 from './elements/fieldset/controls.ts';
-import * as feature33 from './elements/fieldset/disabled.ts';
-import * as feature57 from './elements/legend.ts';
-import * as feature63 from './elements/output/html-for.ts';
-import * as feature65 from './elements/output/value.ts';
-import * as feature64 from './elements/output/reset.ts';
-import * as feature24 from './elements/details/open.ts';
-import * as feature25 from './elements/details/toggle-events.ts';
-import * as feature23 from './elements/details/exclusive-groups.ts';
-import * as feature74 from './elements/summary/activation.ts';
-import * as feature31 from './elements/dialog/show.ts';
-import * as feature26 from './elements/dialog/close.ts';
-import * as feature29 from './elements/dialog/modal.ts';
-import * as feature30 from './elements/dialog/request-close.ts';
-import * as feature27 from './elements/dialog/focus.ts';
-import * as feature28 from './elements/dialog/form-submission.ts';
-import * as feature10 from './custom-elements/registration.ts';
-import * as feature5 from './custom-elements/construction.ts';
-import * as feature11 from './custom-elements/upgrading.ts';
-import * as feature7 from './custom-elements/observed-attributes.ts';
-import * as feature4 from './custom-elements/connection.ts';
-import * as feature3 from './custom-elements/adoption.ts';
-import * as feature8 from './custom-elements/reaction-ordering.ts';
-import * as feature9 from './custom-elements/reentrancy.ts';
-import * as feature6 from './custom-elements/exception-reporting.ts';
-import * as interactionCustomElementsFormAssociation from './custom-elements/form-association.ts';
-import * as feature152 from './shadow/roots/attachment.ts';
-import * as feature155 from './shadow/roots/host-eligibility.ts';
-import * as feature154 from './shadow/roots/connectivity.ts';
-import * as feature153 from './shadow/roots/cloning.ts';
-import * as feature151 from './shadow/roots/adoption.ts';
-import * as feature159 from './shadow/slots/named.ts';
-import * as feature157 from './shadow/slots/fallback.ts';
-import * as feature158 from './shadow/slots/manual.ts';
-import * as feature160 from './shadow/slots/slotchange.ts';
-import * as interactionShadowSlotsObserverOrdering from './shadow/slots/observer-ordering.ts';
-import * as feature148 from './shadow/events/retargeting.ts';
-import * as feature146 from './shadow/events/composed-path.ts';
-import * as feature147 from './shadow/events/related-target.ts';
-import * as feature150 from './shadow/focus/retargeting.ts';
-import * as feature149 from './shadow/focus/delegation.ts';
-import * as feature145 from './shadow/declarative.ts';
-import * as feature156 from './shadow/serialization.ts';
-import * as expansion13 from './style/inline.ts';
-import * as expansion15 from './style/parsing.ts';
-import * as expansion14 from './style/attribute-synchronization.ts';
-import * as expansion16 from './style/custom-properties.ts';
-import * as expansion17 from './style/stylesheets.ts';
-import * as expansion18 from './style/computed.ts';
-import * as extra2 from './style/cascade.ts';
-import * as interactionStyleLayers from './style/layers.ts';
-import * as interactionStyleConstructedStylesheets from './style/constructed-stylesheets.ts';
-import * as interactionStyleFocusState from './style/focus-state.ts';
-import * as interactionStyleControlSelectors from './style/control-selectors.ts';
-import * as interactionStyleRelationalSelectors from './style/relational-selectors.ts';
-import * as interactionStyleMutationObservation from './style/mutation-observation.ts';
-import * as inputKeyboardDelivery from './input/keyboard/delivery.ts';
-import * as inputKeyboardModifiers from './input/keyboard/modifiers.ts';
-import * as inputKeyboardNavigation from './input/keyboard/navigation.ts';
-import * as inputKeyboardActivation from './input/keyboard/activation.ts';
-import * as inputKeyboardEditing from './input/keyboard/editing.ts';
-import * as interactionInputKeyboardMultilineEditing from './input/keyboard/multiline-editing.ts';
-import * as interactionInputKeyboardNumberStepping from './input/keyboard/number-stepping.ts';
-import * as interactionInputKeyboardFocusStyles from './input/keyboard/focus-styles.ts';
-import * as interactionInputKeyboardFocusVisible from './input/keyboard/focus-visible.ts';
-import * as interactionInputKeyboardShadow from './input/keyboard/shadow.ts';
-import * as inputKeyboardImplicitSubmission from './input/keyboard/implicit-submission.ts';
-import * as inputKeyboardTrustedEvents from './input/keyboard/trusted-events.ts';
-import * as inputMouseDelivery from './input/mouse/delivery.ts';
-import * as inputMouseButtons from './input/mouse/buttons.ts';
-import * as inputMouseHover from './input/mouse/hover.ts';
-import * as inputMouseFocus from './input/mouse/focus.ts';
-import * as inputMouseActivation from './input/mouse/activation.ts';
-import * as interactionInputMouseStateStyles from './input/mouse/state-styles.ts';
-import * as interactionInputMouseFocusVisible from './input/mouse/focus-visible.ts';
-import * as inputMouseShadow from './input/mouse/shadow.ts';
-import * as inputMouseTrustedEvents from './input/mouse/trusted-events.ts';
-
-/** Exhaustiveness is checked by TypeScript and compatibility-harness.ts. */
-export const registry = {
- 'document.basicCreation': expansion0,
- 'document.creation': feature12,
- 'document.lookup': feature14,
- 'document.importNode': feature13,
- 'document.xml': expansion1,
- 'document.parsing': extra1,
- 'document.xmlSerialization': interactionDocumentXmlSerialization,
- 'node.basicAppend': expansion2,
- 'node.relationships': feature126,
- 'node.connectivity': feature122,
- 'node.liveCollections': feature125,
- 'node.textContent': feature129,
- 'node.append': feature120,
- 'node.insertBefore': feature124,
- 'node.remove': feature127,
- 'node.replace': feature128,
- 'node.fragments': feature123,
- 'node.variadicMutation': feature130,
- 'node.cloning': feature121,
- 'node.comparison': expansion3,
- 'node.normalization': expansion4,
- 'node.adoption': feature119,
- 'characterData.stringStorage': expansion5,
- 'characterData.data': feature0,
- 'characterData.editing': feature1,
- 'characterData.splitText': feature2,
- 'element.childElements': expansion6,
- 'element.attributes': feature16,
- 'element.attributeNodes': feature15,
- 'element.namespaces': feature18,
- 'element.classList': feature17,
- 'element.dataset': expansion8,
- 'element.reflection': expansion9,
- 'element.selectors': expansion7,
- 'traversal.nodeIterator': expansion12,
- 'traversal.treeWalker': feature164,
- 'parsing.fragments': feature131,
- 'parsing.namespaces': feature132,
- 'parsing.serialization': feature133,
- 'parsing.contextual': expansion10,
- 'parsing.markupMutation': expansion11,
- 'parsing.recovery': interactionParsingRecovery,
- 'templates.content': feature162,
- 'templates.cloning': feature161,
- 'templates.parsing': feature163,
- 'events.data.custom': feature82,
- 'events.data.focus': feature83,
- 'events.data.keyboard': feature85,
- 'events.data.mouse': feature86,
- 'events.data.input': feature84,
- 'events.dispatch.target': feature95,
- 'events.dispatch.capture': feature89,
- 'events.dispatch.bubbling': feature87,
- 'events.dispatch.cancellation': feature88,
- 'events.dispatch.stopPropagation': feature94,
- 'events.dispatch.stopImmediatePropagation': feature93,
- 'events.dispatch.pathSnapshot': feature90,
- 'events.dispatch.reuse': feature92,
- 'events.dispatch.reentrancy': feature91,
- 'events.listeners.registration': feature101,
- 'events.listeners.objects': feature98,
- 'events.listeners.once': feature99,
- 'events.listeners.passive': feature100,
- 'events.listeners.abortSignal': feature96,
- 'events.listeners.snapshots': feature102,
- 'events.listeners.exceptionReporting': feature97,
- 'mutationObserver.registration': feature116,
- 'mutationObserver.records.childList': feature114,
- 'mutationObserver.records.attributes': feature112,
- 'mutationObserver.records.characterData': feature113,
- 'mutationObserver.filtering': feature109,
- 'mutationObserver.oldValue': feature111,
- 'mutationObserver.subtree': feature117,
- 'mutationObserver.takeRecords': feature118,
- 'mutationObserver.disconnect': feature108,
- 'mutationObserver.microtaskDelivery': feature110,
- 'mutationObserver.detached': feature107,
- 'mutationObserver.reentrancy': feature115,
- 'focus.eligibility': feature103,
- 'focus.transitions': feature106,
- 'focus.removal': feature105,
- 'focus.inert': interactionFocusInert,
- 'focus.reentrancy': feature104,
- 'selection.range.boundaries': feature138,
- 'selection.range.comparison': feature140,
- 'selection.range.stringification': feature144,
- 'selection.range.cloneContents': feature139,
- 'selection.range.deleteContents': feature141,
- 'selection.range.extractContents': feature142,
- 'selection.range.liveBoundaries': feature143,
- 'selection.range.mutation': extra0,
- 'selection.document.rangeManagement': feature137,
- 'selection.document.direction': feature135,
- 'selection.document.connectivity': feature134,
- 'selection.document.liveUpdates': feature136,
- 'selection.document.mutation': interactionSelectionDocumentMutation,
- 'selection.document.changeEvents': interactionSelectionDocumentChangeEvents,
- 'elements.progress': feature66,
- 'elements.meter': feature58,
- 'elements.option.attributes': feature60,
- 'elements.option.text': feature62,
- 'elements.option.selectedness': feature61,
- 'elements.optgroup': feature59,
- 'elements.datalist': feature22,
- 'elements.input.value': feature54,
- 'elements.input.sanitization': feature50,
- 'elements.input.colorValue': feature43,
- 'elements.input.numericValue': feature47,
- 'elements.input.dateValue': feature44,
- 'elements.input.stepping': feature52,
- 'elements.input.checkedness': feature41,
- 'elements.input.radioGroups': feature48,
- 'elements.input.selection': feature51,
- 'elements.input.rangeText': feature49,
- 'elements.input.validation': feature53,
- 'elements.input.formAssociation': feature45,
- 'elements.input.labels': feature46,
- 'elements.input.activation': feature40,
- 'elements.input.cloning': feature42,
- 'elements.textarea.value': feature81,
- 'elements.textarea.selection': feature79,
- 'elements.textarea.rangeText': feature78,
- 'elements.textarea.validation': feature80,
- 'elements.textarea.formAssociation': feature76,
- 'elements.textarea.labels': feature77,
- 'elements.textarea.cloning': feature75,
- 'elements.select.options': feature70,
- 'elements.select.indexedOptions': feature68,
- 'elements.select.selection': feature71,
- 'elements.select.mutation': feature69,
- 'elements.select.validation': feature72,
- 'elements.select.cloning': feature67,
- 'elements.selectedcontent': feature73,
- 'elements.form.controls': feature34,
- 'elements.form.namedControls': feature36,
- 'elements.form.reset': feature37,
- 'elements.form.validation': feature39,
- 'elements.form.submission': feature38,
- 'elements.form.formData': feature35,
- 'elements.form.dataEvents': interactionElementsFormDataEvents,
- 'elements.button.attributes': feature20,
- 'elements.button.formAssociation': feature21,
- 'elements.button.activation': feature19,
- 'elements.label.association': feature56,
- 'elements.label.activation': feature55,
- 'elements.fieldset.controls': feature32,
- 'elements.fieldset.disabled': feature33,
- 'elements.legend': feature57,
- 'elements.output.htmlFor': feature63,
- 'elements.output.value': feature65,
- 'elements.output.reset': feature64,
- 'elements.details.open': feature24,
- 'elements.details.toggleEvents': feature25,
- 'elements.details.exclusiveGroups': feature23,
- 'elements.summary.activation': feature74,
- 'elements.dialog.show': feature31,
- 'elements.dialog.close': feature26,
- 'elements.dialog.modal': feature29,
- 'elements.dialog.requestClose': feature30,
- 'elements.dialog.focus': feature27,
- 'elements.dialog.formSubmission': feature28,
- 'customElements.registration': feature10,
- 'customElements.construction': feature5,
- 'customElements.upgrading': feature11,
- 'customElements.observedAttributes': feature7,
- 'customElements.connection': feature4,
- 'customElements.adoption': feature3,
- 'customElements.reactionOrdering': feature8,
- 'customElements.reentrancy': feature9,
- 'customElements.exceptionReporting': feature6,
- 'customElements.formAssociation': interactionCustomElementsFormAssociation,
- 'shadow.roots.attachment': feature152,
- 'shadow.roots.hostEligibility': feature155,
- 'shadow.roots.connectivity': feature154,
- 'shadow.roots.cloning': feature153,
- 'shadow.roots.adoption': feature151,
- 'shadow.slots.named': feature159,
- 'shadow.slots.fallback': feature157,
- 'shadow.slots.manual': feature158,
- 'shadow.slots.slotchange': feature160,
- 'shadow.slots.observerOrdering': interactionShadowSlotsObserverOrdering,
- 'shadow.events.retargeting': feature148,
- 'shadow.events.composedPath': feature146,
- 'shadow.events.relatedTarget': feature147,
- 'shadow.focus.retargeting': feature150,
- 'shadow.focus.delegation': feature149,
- 'shadow.declarative': feature145,
- 'shadow.serialization': feature156,
- 'style.inline': expansion13,
- 'style.parsing': expansion15,
- 'style.attributeSynchronization': expansion14,
- 'style.customProperties': expansion16,
- 'style.stylesheets': expansion17,
- 'style.computed': expansion18,
- 'style.cascade': extra2,
- 'style.layers': interactionStyleLayers,
- 'style.constructedStylesheets': interactionStyleConstructedStylesheets,
- 'style.focusState': interactionStyleFocusState,
- 'style.controlSelectors': interactionStyleControlSelectors,
- 'style.relationalSelectors': interactionStyleRelationalSelectors,
- 'style.mutationObservation': interactionStyleMutationObservation,
- 'input.keyboard.delivery': inputKeyboardDelivery,
- 'input.keyboard.modifiers': inputKeyboardModifiers,
- 'input.keyboard.navigation': inputKeyboardNavigation,
- 'input.keyboard.activation': inputKeyboardActivation,
- 'input.keyboard.editing': inputKeyboardEditing,
- 'input.keyboard.multilineEditing': interactionInputKeyboardMultilineEditing,
- 'input.keyboard.numberStepping': interactionInputKeyboardNumberStepping,
- 'input.keyboard.focusStyles': interactionInputKeyboardFocusStyles,
- 'input.keyboard.focusVisible': interactionInputKeyboardFocusVisible,
- 'input.keyboard.shadow': interactionInputKeyboardShadow,
- 'input.keyboard.implicitSubmission': inputKeyboardImplicitSubmission,
- 'input.keyboard.trustedEvents': inputKeyboardTrustedEvents,
- 'input.mouse.delivery': inputMouseDelivery,
- 'input.mouse.buttons': inputMouseButtons,
- 'input.mouse.hover': inputMouseHover,
- 'input.mouse.focus': inputMouseFocus,
- 'input.mouse.activation': inputMouseActivation,
- 'input.mouse.stateStyles': interactionInputMouseStateStyles,
- 'input.mouse.focusVisible': interactionInputMouseFocusVisible,
- 'input.mouse.shadow': inputMouseShadow,
- 'input.mouse.trustedEvents': inputMouseTrustedEvents,
-} satisfies Record;
diff --git a/packages/web-platform-tests/src/compatibility/selection/document/change-events.ts b/packages/web-platform-tests/src/compatibility/selection/document/change-events.ts
deleted file mode 100644
index a012e6d..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/document/change-events.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import type { CompatibilityContext, DOMFixture } from '../../context.ts';
-import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts';
-
-export const spec = 'https://w3c.github.io/selection-api/#selectionchange-event';
-export const requires = ['selection.document.rangeManagement', 'events.dispatch.target'] as const;
-// Adapted from WPT selection/onselectionchange-on-document.html.
-// Copyright © web-platform-tests contributors. BSD-3-Clause.
-// See ../../../../test/wpt/LICENSE and docs/wpt-ranges-parsing.md.
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/selection/onselectionchange-on-document.html';
-
-function task(): Promise {
- return new Promise((resolve) => setTimeout(resolve, 0));
-}
-
-async function setup(
- fixture: DOMFixture,
-): Promise<{ document: Document; root: HTMLDivElement; selection: Selection }> {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div');
- root.append('abc', document.createElement('b'), 'def');
- fixture.connect(root as unknown as DOMElementContract);
- const selection = document.getSelection()!;
- selection.removeAllRanges();
- fixture.cleanup(() => selection.removeAllRanges());
- // Drain any event from resetting the fixture before installing the test listener.
- await task();
- return { document, root, selection };
-}
-
-function listen(fixture: DOMFixture, callback: (event: Event) => void): void {
- const document = fixture.document as unknown as Document;
- document.addEventListener('selectionchange', callback);
- fixture.cleanup(() => document.removeEventListener('selectionchange', callback));
-}
-
-/** Registers asynchronous selection notification and coalescing cases. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'delivers selectionchange asynchronously at the document without bubbling or cancellation',
- async (fixture) => {
- const { document, root, selection } = await setup(fixture);
- const events: Event[] = [];
- listen(fixture, (event) => events.push(event));
- selection.collapse(root, 1);
- expect(events.length).toBe(0);
- await task();
- expect(events.length).toBe(1);
- expect([events[0]!.target, events[0]!.bubbles, events[0]!.cancelable]).toEqual([
- document,
- false,
- false,
- ]);
- },
- );
- context.test(
- 'coalesces several synchronous selection changes into one notification',
- async (fixture) => {
- const { root, selection } = await setup(fixture);
- const offsets: number[] = [];
- listen(fixture, () => offsets.push(selection.focusOffset));
- selection.collapse(root, 0);
- selection.collapse(root, 1);
- selection.collapse(root, 2);
- expect(offsets).toEqual([]);
- await task();
- expect(offsets).toEqual([2]);
- },
- );
- context.test(
- 'queues another notification when selection changes after delivery',
- async (fixture) => {
- const { root, selection } = await setup(fixture);
- const offsets: number[] = [];
- listen(fixture, () => offsets.push(selection.focusOffset));
- selection.collapse(root, 1);
- await task();
- expect(offsets).toEqual([1]);
- selection.collapse(root, 0);
- selection.collapse(root, 2);
- expect(offsets).toEqual([1]);
- await task();
- expect(offsets).toEqual([1, 2]);
- },
- );
- context.test(
- 'listener mutations schedule a later event and never recursively dispatch',
- async (fixture) => {
- const { root, selection } = await setup(fixture);
- const offsets: number[] = [];
- let inside = false,
- recursive = false;
- listen(fixture, () => {
- if (inside) recursive = true;
- inside = true;
- offsets.push(selection.focusOffset);
- if (offsets.length === 1) {
- selection.collapse(root, 2);
- selection.collapse(root, 0);
- }
- inside = false;
- });
- selection.collapse(root, 1);
- await task();
- await task();
- expect(offsets).toEqual([1, 0]);
- expect(recursive).toBe(false);
- },
- );
- context.test('mutating a retained range schedules a selection notification', async (fixture) => {
- const { root, selection } = await setup(fixture);
- selection.collapse(root.firstChild!, 0);
- await task();
- const range = selection.getRangeAt(0);
- const text: string[] = [];
- listen(fixture, () => text.push(selection.toString()));
- range.setEnd(root.firstChild!, 2);
- expect(text).toEqual([]);
- await task();
- expect(text).toEqual(['ab']);
- expect(selection.getRangeAt(0)).toBe(range);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/document/connectivity.ts b/packages/web-platform-tests/src/compatibility/selection/document/connectivity.ts
deleted file mode 100644
index fb11e0f..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/document/connectivity.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://w3c.github.io/selection-api/#dom-selection-addrange';
-const requiredGlobals = ['Document'] as const;
-export { requiredGlobals as globals };
-export const requires = ['selection.document.rangeManagement'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'ignores ranges rooted outside the selection document',
- ({ document, cleanup, globals }) => {
- const selection = document.getSelection();
- if (!selection) return expect.fail('Document selection expected');
- selection.removeAllRanges();
- cleanup(() => selection.removeAllRanges());
- const detached = document.createTextNode('detached');
- const range = document.createRange();
- range.selectNodeContents(detached);
- selection.addRange(range);
- expect(selection.rangeCount).toBe(0);
- const Document = globals('Document');
- const other = new Document();
- selection.addRange(other.createRange());
- expect(selection.rangeCount).toBe(0);
- },
- );
-
- context.test('ignores adding a range rooted in a detached tree', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.getSelection()!,
- t = document.createTextNode('abc'),
- r = document.createRange();
- s.removeAllRanges();
- r.setStart(t, 0);
- r.setEnd(t, 2);
- s.addRange(r);
- expect(s.rangeCount).toBe(0);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/document/direction.ts b/packages/web-platform-tests/src/compatibility/selection/document/direction.ts
deleted file mode 100644
index abcb970..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/document/direction.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts';
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://w3c.github.io/selection-api/#dom-selection-setbaseandextent';
-export const requires = ['selection.document.rangeManagement'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'distinguishes backward anchor and focus from ordered range endpoints',
- ({ document, connect, cleanup }) => {
- const root = document.createElement('div');
- const text = document.createTextNode('abcdef');
- root.append(text);
- connect(root);
- const selection = document.getSelection();
- if (!selection) return expect.fail('Document selection expected');
- cleanup(() => selection.removeAllRanges());
- selection.setBaseAndExtent(text, 5, text, 1);
- expect(selection.direction).toBe('backward');
- expect(selection.anchorOffset).toBe(5);
- expect(selection.focusOffset).toBe(1);
- const range = selection.getRangeAt(0);
- expect(range.startOffset).toBe(1);
- selection.extend(text, 6);
- expect(selection.direction).toBe('forward');
- expect(selection.toString()).toBe('f');
- },
- );
-
- context.test(
- 'setBaseAndExtent preserves backward anchor and focus while ordering the range',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- t = document.createTextNode('abcd');
- p.append(t);
- fixture.connect(p as unknown as DOMElementContract);
- const s = document.getSelection()!;
- s.setBaseAndExtent(t, 3, t, 1);
- expect(s.anchorNode).toBe(t);
- expect(s.anchorOffset).toBe(3);
- expect(s.focusOffset).toBe(1);
- expect(s.getRangeAt(0).startOffset).toBe(1);
- expect(s.getRangeAt(0).endOffset).toBe(3);
- expect(s.toString()).toBe('bc');
- s.collapseToStart();
- expect(s.anchorOffset).toBe(1);
- expect(s.isCollapsed).toBe(true);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/document/live-updates.ts b/packages/web-platform-tests/src/compatibility/selection/document/live-updates.ts
deleted file mode 100644
index 588799d..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/document/live-updates.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts';
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://w3c.github.io/selection-api/#responding-to-dom-mutations';
-export const requires = [
- 'selection.document.rangeManagement',
- 'selection.range.liveBoundaries',
-] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'reflects character-data mutations through its retained live range',
- ({ document, connect, cleanup }) => {
- const root = document.createElement('div');
- const text = document.createTextNode('abcdef');
- root.append(text);
- connect(root);
- const selection = document.getSelection();
- if (!selection) return expect.fail('Document selection expected');
- selection.removeAllRanges();
- cleanup(() => selection.removeAllRanges());
- const range = document.createRange();
- range.setStart(text, 2);
- range.setEnd(text, 5);
- selection.addRange(range);
- text.deleteData(0, 1);
- expect(selection.getRangeAt(0)).toBe(range);
- expect(selection.anchorOffset).toBe(1);
- expect(selection.focusOffset).toBe(4);
- expect(selection.toString()).toBe('cde');
- },
- );
-
- context.test('updates selection offsets when selected text is edited', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- t = document.createTextNode('abcd');
- p.append(t);
- fixture.connect(p as unknown as DOMElementContract);
- const s = document.getSelection()!;
- s.setBaseAndExtent(t, 1, t, 3);
- t.insertData(0, 'XX');
- expect(s.anchorOffset).toBe(3);
- expect(s.focusOffset).toBe(5);
- expect(s.toString()).toBe('bc');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/document/mutation.ts b/packages/web-platform-tests/src/compatibility/selection/document/mutation.ts
deleted file mode 100644
index 6934f4a..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/document/mutation.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import type { CompatibilityContext, DOMFixture } from '../../context.ts';
-import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts';
-
-export const spec = 'https://w3c.github.io/selection-api/#selection-interface';
-export const requires = [
- 'selection.document.rangeManagement',
- 'selection.range.deleteContents',
- 'document.xml',
-] as const;
-// Adapted from WPT selection/{selectAllChildren,removeRange,deleteFromDocument}.html.
-// Copyright © web-platform-tests contributors. deleteFromDocument author: Aryeh Gregor.
-// BSD-3-Clause; see ../../../../test/wpt/LICENSE and docs/wpt-ranges-parsing.md.
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/selection/removeRange.html';
-
-function setup(fixture: DOMFixture): {
- document: Document;
- root: HTMLDivElement;
- selection: Selection;
-} {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div');
- root.append(document.createTextNode('abc'), document.createElement('b'), 'def');
- root.children[0]!.textContent = 'middle';
- fixture.connect(root as unknown as DOMElementContract);
- const selection = document.getSelection()!;
- selection.removeAllRanges();
- fixture.cleanup(() => selection.removeAllRanges());
- return { document, root, selection };
-}
-
-/** Registers selection mutation, range identity, and failure cases. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'selectAllChildren replaces the range and selects child offsets forwards',
- (fixture) => {
- const { document, root, selection } = setup(fixture);
- const previous = document.createRange();
- previous.setStart(root.firstChild!, 1);
- previous.setEnd(root.firstChild!, 2);
- selection.addRange(previous);
- selection.selectAllChildren(root);
- expect(selection.getRangeAt(0)).not.toBe(previous);
- expect([
- selection.anchorNode,
- selection.anchorOffset,
- selection.focusNode,
- selection.focusOffset,
- ]).toEqual([root, 0, root, 3]);
- expect(selection.toString()).toBe('abcmiddledef');
- expect([previous.startOffset, previous.endOffset]).toEqual([1, 2]);
- },
- );
- context.test(
- 'selectAllChildren ignores detached and other-document nodes without replacing the range',
- (fixture) => {
- const { document, root, selection } = setup(fixture);
- selection.selectAllChildren(root);
- const range = selection.getRangeAt(0);
- selection.selectAllChildren(document.createElement('div'));
- expect(selection.getRangeAt(0)).toBe(range);
- selection.selectAllChildren(document.implementation.createHTMLDocument('').body);
- expect(selection.getRangeAt(0)).toBe(range);
- },
- );
- context.test(
- 'selectAllChildren rejects doctypes before altering the selected range',
- (fixture) => {
- const { document, root, selection } = setup(fixture);
- selection.selectAllChildren(root);
- const range = selection.getRangeAt(0);
- const doctype = document.implementation.createDocumentType('html', '', '');
- expect(() => selection.selectAllChildren(doctype)).toThrow(
- expect.objectContaining({ name: 'InvalidNodeTypeError' }),
- );
- expect(selection.getRangeAt(0)).toBe(range);
- },
- );
- context.test(
- 'selectAllChildren uses child counts even for text nodes and empty elements',
- (fixture) => {
- const { root, selection } = setup(fixture);
- for (const node of [
- root.firstChild!,
- root.appendChild(root.ownerDocument.createElement('i')),
- ]) {
- selection.selectAllChildren(node);
- expect([
- selection.anchorNode,
- selection.anchorOffset,
- selection.focusNode,
- selection.focusOffset,
- ]).toEqual([node, 0, node, 0]);
- expect(selection.isCollapsed).toBe(true);
- }
- },
- );
- context.test(
- 'removeRange uses identity and preserves the selection after a mismatch',
- (fixture) => {
- const { root, selection } = setup(fixture);
- selection.selectAllChildren(root);
- const range = selection.getRangeAt(0);
- expect(() => selection.removeRange(range.cloneRange())).toThrow(
- expect.objectContaining({ name: 'NotFoundError' }),
- );
- expect(selection.getRangeAt(0)).toBe(range);
- selection.removeRange(range);
- expect([selection.rangeCount, selection.anchorNode, selection.focusNode]).toEqual([
- 0,
- null,
- null,
- ]);
- expect(() => selection.removeRange(range)).toThrow(
- expect.objectContaining({ name: 'NotFoundError' }),
- );
- },
- );
- context.test('removeRange rejects values that are not Range objects', (fixture) => {
- const { root, selection } = setup(fixture);
- selection.selectAllChildren(root);
- const range = selection.getRangeAt(0);
- for (const invalid of [null, selection, {}, undefined]) {
- expect(() => selection.removeRange(invalid as unknown as Range)).toThrow(
- expect.objectContaining({ name: 'TypeError' }),
- );
- expect(selection.getRangeAt(0)).toBe(range);
- }
- });
- context.test('deleteFromDocument is a no-op for empty and collapsed selections', (fixture) => {
- const { root, selection } = setup(fixture);
- selection.deleteFromDocument();
- expect(selection.rangeCount).toBe(0);
- selection.collapse(root.firstChild!, 1);
- const range = selection.getRangeAt(0);
- selection.deleteFromDocument();
- expect(root.textContent).toBe('abcmiddledef');
- expect(selection.getRangeAt(0)).toBe(range);
- expect(selection.anchorOffset).toBe(1);
- });
- context.test(
- 'deleteFromDocument removes partial text and whole nodes through the retained range',
- (fixture) => {
- const { document, root, selection } = setup(fixture);
- const first = root.firstChild!,
- last = root.lastChild!,
- middle = root.children[0]!;
- const range = document.createRange();
- range.setStart(first, 1);
- range.setEnd(last, 2);
- selection.addRange(range);
- selection.deleteFromDocument();
- expect(root.textContent).toBe('af');
- expect(Array.from(root.childNodes)).toEqual([first, last]);
- expect(middle.parentNode).toBeNull();
- expect(middle.textContent).toBe('middle');
- expect(selection.getRangeAt(0)).toBe(range);
- expect([
- range.startContainer,
- range.startOffset,
- range.endContainer,
- range.endOffset,
- ]).toEqual([root, 1, root, 1]);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/document/range-management.ts b/packages/web-platform-tests/src/compatibility/selection/document/range-management.ts
deleted file mode 100644
index e392181..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/document/range-management.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts';
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://w3c.github.io/selection-api/#dom-selection-addrange';
-export const requires = ['selection.range.boundaries'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'stores one range by reference and clears it explicitly',
- ({ document, connect, cleanup }) => {
- const root = document.createElement('div');
- root.textContent = 'selection';
- connect(root);
- const selection = document.getSelection();
- if (!selection) return expect.fail('Document selection expected');
- selection.removeAllRanges();
- cleanup(() => selection.removeAllRanges());
- const range = document.createRange();
- range.selectNodeContents(root);
- selection.addRange(range);
- expect(selection.getRangeAt(0)).toBe(range);
- expect(selection.rangeCount).toBe(1);
- expect(selection.toString()).toBe('selection');
- selection.removeAllRanges();
- expect(selection.rangeCount).toBe(0);
- expect(selection.anchorNode).toBeNull();
- expect(() => selection.getRangeAt(0)).toThrow(
- expect.objectContaining({ name: 'IndexSizeError' }),
- );
- },
- );
-
- context.test('retains range identity and clears anchor and focus on removal', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div');
- e.textContent = 'abc';
- fixture.connect(e as unknown as DOMElementContract);
- const s = document.getSelection()!,
- r = document.createRange();
- r.selectNodeContents(e);
- s.removeAllRanges();
- s.addRange(r);
- expect(s.getRangeAt(0)).toBe(r);
- expect(s.rangeCount).toBe(1);
- s.removeAllRanges();
- expect(s.rangeCount).toBe(0);
- expect(s.anchorNode).toBeNull();
- expect(s.focusNode).toBeNull();
- expect(() => s.getRangeAt(0)).toThrow(expect.objectContaining({ name: 'IndexSizeError' }));
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/boundaries.ts b/packages/web-platform-tests/src/compatibility/selection/range/boundaries.ts
deleted file mode 100644
index e054f41..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/boundaries.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#interface-range';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'initializes at the document and counts UTF-16 and child-node offsets',
- ({ document }) => {
- const range = document.createRange();
- expect([
- range.startContainer,
- range.startOffset,
- range.endContainer,
- range.endOffset,
- ]).toEqual([document, 0, document, 0]);
- const root = document.createElement('div');
- const text = document.createTextNode('A😀B');
- root.append(text, document.createComment('comment'));
- range.selectNodeContents(root);
- expect(range.endOffset).toBe(2);
- range.setStart(text, 1);
- range.setEnd(text, 3);
- expect(range.startOffset).toBe(1);
- expect(range.endOffset).toBe(3);
- expect(range.collapsed).toBe(false);
- expect(range.commonAncestorContainer).toBe(text);
- expect(() => range.setEnd(text, 5)).toThrow(
- expect.objectContaining({ name: 'IndexSizeError' }),
- );
- expect(range.endOffset).toBe(3);
- range.setEnd(text, 0);
- expect(range.collapsed).toBe(true);
- expect(range.startOffset).toBe(0);
- },
- );
-
- context.test('rejects out of range offsets and invalid node kinds', (fixture) => {
- const document = fixture.document as unknown as Document;
- const r = document.createRange(),
- t = document.createTextNode('abc');
- expect(() => r.setStart(t, 4)).toThrow(expect.objectContaining({ name: 'IndexSizeError' }));
- const dt = document.implementation.createDocumentType('html', '', '');
- expect(() => r.setEnd(dt, 0)).toThrow(
- expect.objectContaining({ name: 'InvalidNodeTypeError' }),
- );
- });
-
- context.test('collapses to a new root when boundaries cross trees', (fixture) => {
- const document = fixture.document as unknown as Document;
- const a = document.createTextNode('abc'),
- b = document.createTextNode('def'),
- r = document.createRange();
- r.setStart(a, 1);
- r.setEnd(a, 2);
- r.setEnd(b, 1);
- expect(r.collapsed).toBe(true);
- expect(r.startContainer).toBe(b);
- expect(r.startOffset).toBe(1);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/clone-contents.ts b/packages/web-platform-tests/src/compatibility/selection/range/clone-contents.ts
deleted file mode 100644
index 865da87..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/clone-contents.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#dom-range-clonecontents';
-export const requires = ['selection.range.boundaries'] as const;
-// Coverage reference; this leaf is independently authored, not an imported WPT file.
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/master/dom/ranges/Range-cloneContents.html';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('clones partial ancestor shells without changing the source', ({ document }) => {
- const root = document.createElement('div');
- const a = document.createElement('b');
- const b = document.createElement('i');
- const first = document.createTextNode('abc');
- const last = document.createTextNode('xyz');
- a.append(first);
- b.append(last);
- root.append(a, b);
- const range = document.createRange();
- range.setStart(first, 1);
- range.setEnd(last, 2);
- const fragment = range.cloneContents();
- expect(fragment.textContent).toBe('bcxy');
- expect(fragment.childNodes.length).toBe(2);
- expect(fragment.firstChild?.nodeName).toBe(a.nodeName);
- expect(fragment.firstChild).not.toBe(a);
- expect(root.textContent).toBe('abcxyz');
- });
-
- context.test('clones a partial text selection without mutating boundaries or data', (fixture) => {
- const document = fixture.document as unknown as Document;
- const t = document.createTextNode('abcdef'),
- r = document.createRange();
- r.setStart(t, 1);
- r.setEnd(t, 4);
- const f = r.cloneContents();
- expect(f.textContent).toBe('bcd');
- expect(t.data).toBe('abcdef');
- expect(r.startOffset).toBe(1);
- expect(r.endOffset).toBe(4);
- expect(f.firstChild).not.toBe(t);
- });
-
- context.test('returns an empty fragment for a collapsed range', (fixture) => {
- const document = fixture.document as unknown as Document;
- const r = document.createRange(),
- t = document.createTextNode('abc');
- r.setStart(t, 1);
- expect(r.cloneContents().childNodes.length).toBe(0);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/comparison.ts b/packages/web-platform-tests/src/compatibility/selection/range/comparison.ts
deleted file mode 100644
index bb482a3..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/comparison.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#dom-range-compareboundarypoints';
-export const requires = ['selection.range.boundaries'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('compares boundaries and distinguishes points outside a range', ({ document }) => {
- const root = document.createElement('div');
- const left = document.createElement('span');
- const right = document.createElement('span');
- root.append(left, right);
- const range = document.createRange();
- range.selectNode(left);
- const clone = range.cloneRange();
- expect(clone.compareBoundaryPoints(range.START_TO_START, range)).toBe(0);
- expect(clone.compareBoundaryPoints(range.START_TO_END, range)).toBe(1);
- expect(clone.compareBoundaryPoints(range.END_TO_START, range)).toBe(-1);
- expect(range.isPointInRange(root, 0)).toBe(true);
- expect(range.comparePoint(root, 2)).toBe(1);
- expect(range.intersectsNode(right)).toBe(false);
- expect(() => range.comparePoint(document.createTextNode('other'), 0)).toThrow(
- expect.objectContaining({ name: 'WrongDocumentError' }),
- );
- });
-
- context.test(
- 'compares points before inside and after a range and rejects another tree',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const t = document.createTextNode('abcd'),
- r = document.createRange();
- r.setStart(t, 1);
- r.setEnd(t, 3);
- expect(r.comparePoint(t, 0)).toBe(-1);
- expect(r.comparePoint(t, 1)).toBe(0);
- expect(r.comparePoint(t, 3)).toBe(0);
- expect(r.comparePoint(t, 4)).toBe(1);
- expect(r.isPointInRange(t, 2)).toBe(true);
- expect(r.isPointInRange(document.createTextNode('x'), 0)).toBe(false);
- expect(() => r.comparePoint(document.createTextNode('x'), 0)).toThrow(
- expect.objectContaining({ name: 'WrongDocumentError' }),
- );
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/delete-contents.ts b/packages/web-platform-tests/src/compatibility/selection/range/delete-contents.ts
deleted file mode 100644
index 8f2bd99..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/delete-contents.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#dom-range-deletecontents';
-export const requires = ['selection.range.boundaries'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'deletes selected text and nodes while retaining partial ancestors',
- ({ document }) => {
- const root = document.createElement('div');
- const a = document.createElement('b');
- const b = document.createElement('i');
- const first = document.createTextNode('abc');
- const last = document.createTextNode('xyz');
- a.append(first);
- b.append(last);
- root.append(a, document.createElement('hr'), b);
- const range = document.createRange();
- range.setStart(first, 1);
- range.setEnd(last, 2);
- range.deleteContents();
- expect(root.textContent).toBe('az');
- expect(root.childNodes.length).toBe(2);
- expect(root.childNodes[0]).toBe(a);
- expect(root.childNodes[1]).toBe(b);
- expect(range.collapsed).toBe(true);
- },
- );
-
- context.test('deletes complete children without destroying removed node identity', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- a = document.createElement('i'),
- b = document.createElement('b');
- p.append(a, b);
- const r = document.createRange();
- r.selectNode(a);
- r.deleteContents();
- expect(p.firstChild).toBe(b);
- expect(a.parentNode).toBeNull();
- expect(r.collapsed).toBe(true);
- expect(r.startContainer).toBe(p);
- expect(r.startOffset).toBe(0);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/extract-contents.ts b/packages/web-platform-tests/src/compatibility/selection/range/extract-contents.ts
deleted file mode 100644
index daf14b6..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/extract-contents.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#dom-range-extractcontents';
-export const requires = ['selection.range.boundaries'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('moves wholly contained nodes while cloning partial ancestors', ({ document }) => {
- const root = document.createElement('div');
- const a = document.createElement('b');
- const middle = document.createElement('span');
- const b = document.createElement('i');
- const first = document.createTextNode('abc');
- const last = document.createTextNode('xyz');
- a.append(first);
- middle.textContent = 'middle';
- b.append(last);
- root.append(a, middle, b);
- const range = document.createRange();
- range.setStart(first, 1);
- range.setEnd(last, 2);
- const fragment = range.extractContents();
- expect(fragment.textContent).toBe('bcmiddlexy');
- expect(fragment.childNodes[1]).toBe(middle);
- expect(fragment.firstChild).not.toBe(a);
- expect(root.textContent).toBe('az');
- expect(range.collapsed).toBe(true);
- });
-
- context.test('extracts partial text and collapses the range', (fixture) => {
- const document = fixture.document as unknown as Document;
- const t = document.createTextNode('abcdef'),
- r = document.createRange();
- r.setStart(t, 1);
- r.setEnd(t, 4);
- expect(r.extractContents().textContent).toBe('bcd');
- expect(t.data).toBe('aef');
- expect(r.collapsed).toBe(true);
- expect(r.startOffset).toBe(1);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/live-boundaries.ts b/packages/web-platform-tests/src/compatibility/selection/range/live-boundaries.ts
deleted file mode 100644
index 4bac530..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/live-boundaries.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#concept-live-range';
-export const requires = [
- 'selection.range.boundaries',
- 'characterData.editing',
- 'characterData.splitText',
-] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'adjusts live boundaries during replacement, splitting, and removal',
- ({ document }) => {
- const root = document.createElement('div');
- const text = document.createTextNode('A😀B');
- root.append(text);
- const range = document.createRange();
- range.setStart(text, 1);
- range.setEnd(text, 4);
- text.replaceData(1, 2, 'X');
- expect([range.startOffset, range.endOffset]).toEqual([1, 3]);
- const split = text.splitText(1);
- expect(range.startContainer).toBe(text);
- expect(range.endContainer).toBe(split);
- expect(range.endOffset).toBe(2);
- split.remove();
- expect(range.endContainer).toBe(root);
- expect(range.endOffset).toBe(1);
- },
- );
- context.test(
- 'does not move a boundary when insertion occurs exactly at that boundary',
- ({ document }) => {
- const root = document.createElement('div');
- const a = document.createElement('span');
- const b = document.createElement('span');
- root.append(a, b);
- const range = document.createRange();
- range.setStart(root, 1);
- range.setEnd(root, 2);
- root.insertBefore(document.createElement('span'), b);
- expect(range.startOffset).toBe(1);
- expect(range.endOffset).toBe(3);
- },
- );
-
- context.test('adjusts offsets after text insertion and deletion', (fixture) => {
- const document = fixture.document as unknown as Document;
- const t = document.createTextNode('abcdef'),
- r = document.createRange();
- r.setStart(t, 2);
- r.setEnd(t, 5);
- t.insertData(1, 'XX');
- expect(r.startOffset).toBe(4);
- expect(r.endOffset).toBe(7);
- t.deleteData(2, 3);
- expect(r.startOffset).toBe(2);
- expect(r.endOffset).toBe(4);
- });
-
- context.test('relocates boundaries to the old parent when a subtree is removed', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- e = document.createElement('i'),
- t = document.createTextNode('abc');
- e.append(t);
- p.append(e);
- const r = document.createRange();
- r.setStart(t, 1);
- r.setEnd(t, 2);
- e.remove();
- expect(r.startContainer).toBe(p);
- expect(r.startOffset).toBe(0);
- expect(r.collapsed).toBe(true);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/mutation.ts b/packages/web-platform-tests/src/compatibility/selection/range/mutation.ts
deleted file mode 100644
index 987f18f..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/mutation.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#interface-range';
-export const requires = ['selection.range.boundaries', 'document.xml'] as const;
-// Additional matrices adapted from WPT Range-{insertNode,surroundContents}.html.
-// Copyright © web-platform-tests contributors. Original author: Aryeh Gregor.
-// BSD-3-Clause; see ../../../../test/wpt/LICENSE and docs/wpt-ranges-parsing.md.
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/dom/ranges/Range-surroundContents.html';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'inserts into a text boundary by splitting text and expands a collapsed range',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- t = document.createTextNode('abcd'),
- e = document.createElement('i'),
- r = document.createRange();
- p.append(t);
- r.setStart(t, 2);
- r.collapse(true);
- r.insertNode(e);
- expect(Array.from(p.childNodes).map((n) => n.nodeName)).toEqual(['#text', 'I', '#text']);
- expect(t.data).toBe('ab');
- expect(p.lastChild!.nodeValue).toBe('cd');
- expect(r.startContainer).toBe(t);
- expect(r.startOffset).toBe(2);
- expect(r.endContainer).toBe(p);
- expect(r.endOffset).toBe(2);
- },
- );
- context.test('surrounds selected text and selects the new parent', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- t = document.createTextNode('abcd'),
- r = document.createRange(),
- e = document.createElement('b');
- p.append(t);
- r.setStart(t, 1);
- r.setEnd(t, 3);
- r.surroundContents(e);
- expect(p.innerHTML).toBe('abc d');
- expect(r.startContainer).toBe(p);
- expect(r.startOffset).toBe(1);
- expect(r.endOffset).toBe(2);
- });
- context.test('rejects surrounding a partially selected non-text node atomically', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div');
- p.innerHTML = 'ab cd';
- const r = document.createRange();
- r.setStart(p.firstChild!.firstChild!, 1);
- r.setEnd(p, 2);
- expect(() => r.surroundContents(document.createElement('b'))).toThrow(
- expect.objectContaining({ name: 'InvalidStateError' }),
- );
- expect(p.innerHTML).toBe('ab cd');
- });
- context.test(
- 'selectNode rejects parentless nodes and selectNodeContents selects children',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- r = document.createRange();
- expect(() => r.selectNode(p)).toThrow(
- expect.objectContaining({ name: 'InvalidNodeTypeError' }),
- );
- p.append('a', 'b');
- r.selectNodeContents(p);
- expect(r.startOffset).toBe(0);
- expect(r.endOffset).toBe(2);
- r.collapse(false);
- expect(r.startOffset).toBe(2);
- expect(r.collapsed).toBe(true);
- },
- );
- context.test(
- 'rejects invalid surround wrapper kinds without extracting the selection',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const parent = document.createElement('div');
- const text = parent.appendChild(document.createTextNode('abcd'));
- const range = document.createRange();
- range.setStart(text, 1);
- range.setEnd(text, 3);
- for (const wrapper of [
- document,
- document.createDocumentFragment(),
- document.implementation.createDocumentType('html', '', ''),
- ]) {
- expect(() => range.surroundContents(wrapper)).toThrow(
- expect.objectContaining({ name: 'InvalidNodeTypeError' }),
- );
- expect(parent.textContent).toBe('abcd');
- expect(parent.firstChild).toBe(text);
- expect([
- range.startContainer,
- range.startOffset,
- range.endContainer,
- range.endOffset,
- ]).toEqual([text, 1, text, 3]);
- }
- },
- );
- context.test(
- 'surroundContents clears existing wrapper children and moves the wrapper by identity',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const parent = document.createElement('div');
- const first = document.createElement('i'),
- last = document.createElement('b');
- parent.append(first, last);
- const oldParent = document.createElement('div');
- const wrapper = oldParent.appendChild(document.createElement('section'));
- const discarded = wrapper.appendChild(document.createTextNode('discarded'));
- const range = document.createRange();
- range.selectNodeContents(parent);
- range.surroundContents(wrapper);
- expect(parent.firstChild).toBe(wrapper);
- expect(Array.from(wrapper.childNodes)).toEqual([first, last]);
- expect(discarded.parentNode).toBeNull();
- expect(discarded.data).toBe('discarded');
- expect(oldParent.childNodes.length).toBe(0);
- expect([
- range.startContainer,
- range.startOffset,
- range.endContainer,
- range.endOffset,
- ]).toEqual([parent, 0, parent, 1]);
- },
- );
- context.test(
- 'insertNode empties a fragment while preserving child identities and expanding a collapsed range',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const parent = document.createElement('div'),
- tail = document.createElement('i');
- parent.append(tail);
- const fragment = document.createDocumentFragment();
- const a = document.createElement('b'),
- b = document.createTextNode('text');
- fragment.append(a, b);
- const range = document.createRange();
- range.setStart(parent, 0);
- range.insertNode(fragment);
- expect(Array.from(parent.childNodes)).toEqual([a, b, tail]);
- expect(fragment.childNodes.length).toBe(0);
- expect([range.startOffset, range.endOffset]).toEqual([0, 2]);
- expect(range.startContainer).toBe(parent);
- expect(range.endContainer).toBe(parent);
- },
- );
- context.test(
- 'insertNode adopts nodes from another document and removes them from their old tree',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const other = document.implementation.createHTMLDocument('');
- const node = other.body.appendChild(other.createElement('i'));
- const child = node.appendChild(other.createTextNode('foreign'));
- const parent = document.createElement('div'),
- range = document.createRange();
- range.selectNodeContents(parent);
- range.insertNode(node);
- expect(parent.firstChild).toBe(node);
- expect(node.ownerDocument).toBe(document);
- expect(child.ownerDocument).toBe(document);
- expect(other.body.childNodes.length).toBe(0);
- expect([range.startOffset, range.endOffset]).toEqual([0, 1]);
- },
- );
- context.test('insertNode validates ancestor cycles before splitting boundary text', (fixture) => {
- const document = fixture.document as unknown as Document;
- const parent = document.createElement('div');
- const text = parent.appendChild(document.createTextNode('abcd'));
- const range = document.createRange();
- range.setStart(text, 2);
- expect(() => range.insertNode(parent)).toThrow(
- expect.objectContaining({ name: 'HierarchyRequestError' }),
- );
- expect(Array.from(parent.childNodes)).toEqual([text]);
- expect(text.data).toBe('abcd');
- expect(range.startContainer).toBe(text);
- expect(range.startOffset).toBe(2);
- expect(range.collapsed).toBe(true);
- });
- context.test('insertNode rejects comment and parentless text boundary containers', (fixture) => {
- const document = fixture.document as unknown as Document;
- const parent = document.createElement('div');
- const comment = parent.appendChild(document.createComment('abc'));
- const detached = document.createTextNode('abc');
- for (const boundary of [comment, detached]) {
- const range = document.createRange();
- range.setStart(boundary, 1);
- const node = document.createElement('i');
- expect(() => range.insertNode(node)).toThrow(
- expect.objectContaining({ name: 'HierarchyRequestError' }),
- );
- expect(node.parentNode).toBeNull();
- expect(boundary.data).toBe('abc');
- expect(range.startContainer).toBe(boundary);
- expect(range.startOffset).toBe(1);
- }
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/selection/range/stringification.ts b/packages/web-platform-tests/src/compatibility/selection/range/stringification.ts
deleted file mode 100644
index 5767a91..0000000
--- a/packages/web-platform-tests/src/compatibility/selection/range/stringification.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#dom-range-stringifier';
-export const requires = ['selection.range.boundaries'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('stringifies selected text but excludes comments', ({ document }) => {
- const root = document.createElement('div');
- const text = document.createTextNode('A😀B');
- root.append(text, document.createComment('ignored'), document.createTextNode('tail'));
- const range = document.createRange();
- range.selectNodeContents(root);
- expect(range.toString()).toBe('A😀Btail');
- range.setStart(text, 1);
- range.setEnd(text, 3);
- expect(range.toString()).toBe('😀');
- });
-
- context.test('stringifies only text portions and excludes comments', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div');
- p.innerHTML = 'abc de';
- const r = document.createRange();
- r.setStart(p.firstChild!, 0);
- r.setEnd(p.lastChild!, 1);
- expect(r.toString()).toBe('abcd');
- r.collapse(true);
- expect(r.toString()).toBe('');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/declarative.ts b/packages/web-platform-tests/src/compatibility/shadow/declarative.ts
deleted file mode 100644
index f1c2880..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/declarative.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead';
-export const requires = ['shadow.roots.attachment', 'parsing.fragments'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('constructs declarative roots beneath newly parsed hosts', ({ document }) => {
- const container = document.createElement('div');
- container.setHTMLUnsafe(
- '',
- );
- const host = container.firstElementChild;
- if (!host) return expect.fail('Parsed host expected');
- expect(host.shadowRoot?.textContent).toBe('shadow');
- expect(host.shadowRoot?.host).toBe(host);
- expect(host.childNodes.length).toBe(1);
- expect(host.firstChild?.textContent).toBe('light');
- });
- context.test('does not attach a root to the fragment context element', ({ document }) => {
- const host = document.createElement('div');
- host.setHTMLUnsafe('shadow light');
- expect(host.shadowRoot).toBeNull();
- expect(host.firstElementChild?.localName).toBe('template');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/events/composed-path.ts b/packages/web-platform-tests/src/compatibility/shadow/events/composed-path.ts
deleted file mode 100644
index acfb584..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/events/composed-path.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-// Adapted from WPT event-composed-path.html at b719612f963a05ece9c89a7dd7598c1c59618874.
-// Copyright © web-platform-tests contributors. Original author: Hayato Ito.
-// BSD-3-Clause; see ../../../../test/wpt/LICENSE.
-import type { CompatibilityContext } from '../../context.ts';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/shadow-dom/event-composed-path.html';
-export const spec = 'https://dom.spec.whatwg.org/#dom-event-composedpath';
-const requiredGlobals = ['Event'] as const;
-export { requiredGlobals as globals };
-export const requires = ['shadow.events.retargeting'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'hides closed-root internals from listeners outside the root',
- ({ document, globals }) => {
- const Event = globals('Event');
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'closed' });
- const child = document.createElement('span');
- root.append(child);
- const paths: unknown[] = [];
- child.addEventListener('probe', (event) => {
- paths.push(event.composedPath());
- });
- host.addEventListener('probe', (event) => {
- paths.push(event.composedPath());
- });
- const event = new Event('probe', { bubbles: true, composed: true });
- child.dispatchEvent(event);
- expect(paths).toEqual([[child, root, host], [host]]);
- expect(event.composedPath()).toEqual([]);
- },
- );
-
- context.test('hides closed-root internals from outside composedPath', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'closed' }),
- child = document.createElement('i');
- root.append(child);
- let inside: EventTarget[] = [],
- outside: EventTarget[] = [];
- child.addEventListener('x', (e) => (inside = e.composedPath()));
- host.addEventListener('x', (e) => (outside = e.composedPath()));
- const Event = fixture.globals('Event');
- child.dispatchEvent(new Event('x', { bubbles: true, composed: true }) as unknown as Event);
- expect(inside).toContain(child);
- expect(inside).toContain(root);
- expect(outside).not.toContain(child);
- expect(outside).not.toContain(root);
- expect(outside[0]).toBe(host);
- });
-
- for (const outerMode of ['open', 'closed'] as const) {
- for (const innerMode of ['open', 'closed'] as const) {
- context.test(
- `retargets and filters every listener path through ${outerMode}/${innerMode} nested roots`,
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const Event = fixture.globals('Event') as unknown as typeof globalThis.Event;
- const outerHost = document.createElement('div');
- const outerRoot = outerHost.attachShadow({ mode: outerMode });
- const innerHost = document.createElement('div');
- const innerRoot = innerHost.attachShadow({ mode: innerMode });
- const target = document.createElement('span');
- outerRoot.append(innerHost);
- innerRoot.append(target);
- const fullPath = [target, innerRoot, innerHost, outerRoot, outerHost];
- const calls: unknown[] = [];
- for (const current of fullPath)
- current.addEventListener('probe', (event) => {
- calls.push([current, event.target, event.composedPath()]);
- });
- const event = new Event('probe', { bubbles: true, composed: true });
- target.dispatchEvent(event);
- const middlePath = innerMode === 'closed' ? fullPath.slice(2) : fullPath;
- const outsidePath = outerMode === 'closed' ? [outerHost] : middlePath;
- expect(calls).toEqual([
- [target, target, fullPath],
- [innerRoot, target, fullPath],
- [innerHost, innerHost, middlePath],
- [outerRoot, innerHost, middlePath],
- [outerHost, outerHost, outsidePath],
- ]);
- expect(event.composedPath()).toEqual([]);
- },
- );
-
- context.test(
- `preserves a light-tree target through slots in ${outerMode}/${innerMode} nested roots`,
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const Event = fixture.globals('Event') as unknown as typeof globalThis.Event;
- const outerHost = document.createElement('div');
- const outerRoot = outerHost.attachShadow({ mode: outerMode });
- const innerHost = document.createElement('div');
- const innerRoot = innerHost.attachShadow({ mode: innerMode });
- const outerSlot = document.createElement('slot');
- const innerSlot = document.createElement('slot');
- const target = document.createElement('span');
- outerRoot.append(innerHost);
- innerHost.append(outerSlot);
- innerRoot.append(innerSlot);
- outerHost.append(target);
- const fullPath = [
- target,
- outerSlot,
- innerSlot,
- innerRoot,
- innerHost,
- outerRoot,
- outerHost,
- ];
- const middlePath =
- innerMode === 'closed'
- ? [target, outerSlot, innerHost, outerRoot, outerHost]
- : fullPath;
- const outsidePath = outerMode === 'closed' ? [target, outerHost] : middlePath;
- const calls: unknown[] = [];
- for (const current of fullPath)
- current.addEventListener('probe', (event) => {
- calls.push([current, event.target, event.composedPath()]);
- });
- const event = new Event('probe', { bubbles: true, composed: true });
- target.dispatchEvent(event);
- expect(calls).toEqual([
- [target, target, outsidePath],
- [outerSlot, target, middlePath],
- [innerSlot, target, fullPath],
- [innerRoot, target, fullPath],
- [innerHost, target, middlePath],
- [outerRoot, target, middlePath],
- [outerHost, target, outsidePath],
- ]);
- expect(event.composedPath()).toEqual([]);
- },
- ['shadow.slots.named'],
- );
- }
- }
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/events/related-target.ts b/packages/web-platform-tests/src/compatibility/shadow/events/related-target.ts
deleted file mode 100644
index 5041d4a..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/events/related-target.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch';
-const requiredGlobals = ['FocusEvent'] as const;
-export { requiredGlobals as globals };
-export const requires = ['shadow.events.retargeting'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'suppresses delivery when target and relatedTarget retarget to the same host',
- ({ document, globals }) => {
- const FocusEvent = globals('FocusEvent');
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const first = document.createElement('button');
- const second = document.createElement('button');
- root.append(first, second);
- let inside = 0;
- let outside = 0;
- root.addEventListener('focusout', () => inside++);
- host.addEventListener('focusout', () => outside++);
- first.dispatchEvent(
- new FocusEvent('focusout', { bubbles: true, composed: true, relatedTarget: second }),
- );
- expect(inside).toBe(1);
- expect(outside).toBe(0);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/events/retargeting.ts b/packages/web-platform-tests/src/compatibility/shadow/events/retargeting.ts
deleted file mode 100644
index ea856a2..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/events/retargeting.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#retarget';
-const requiredGlobals = ['Event'] as const;
-export { requiredGlobals as globals };
-export const requires = ['shadow.roots.attachment', 'events.dispatch.bubbling'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'retargets composed events to the host outside the shadow tree',
- ({ document, globals }) => {
- const Event = globals('Event');
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const child = document.createElement('span');
- root.append(child);
- const targets: unknown[] = [];
- child.addEventListener('probe', (event) => {
- targets.push(event.target);
- });
- host.addEventListener('probe', (event) => {
- targets.push(event.target);
- });
- child.dispatchEvent(new Event('probe', { bubbles: true, composed: true }));
- expect(targets).toEqual([child, host]);
- targets.length = 0;
- child.dispatchEvent(new Event('probe', { bubbles: true }));
- expect(targets).toEqual([child]);
- },
- );
-
- context.test('prevents noncomposed events from escaping a shadow root', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open' }),
- child = document.createElement('i');
- root.append(child);
- const seen: unknown[] = [];
- root.addEventListener('x', (e) => seen.push(e.target));
- host.addEventListener('x', () => seen.push('host'));
- const Event = fixture.globals('Event');
- child.dispatchEvent(new Event('x', { bubbles: true }) as unknown as Event);
- expect(seen).toEqual([child]);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/focus/delegation.ts b/packages/web-platform-tests/src/compatibility/shadow/focus/delegation.ts
deleted file mode 100644
index bc1121b..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/focus/delegation.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://html.spec.whatwg.org/multipage/interaction.html#focus-delegate';
-export const requires = ['shadow.focus.retargeting'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('delegates host focus to an eligible shadow descendant', ({ document, connect }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open', delegatesFocus: true });
- const control = document.createElement('button');
- root.append(control);
- connect(host);
- host.focus();
- expect(root.delegatesFocus).toBe(true);
- expect(document.activeElement).toBe(host);
- expect(root.activeElement).toBe(control);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/focus/retargeting.ts b/packages/web-platform-tests/src/compatibility/shadow/focus/retargeting.ts
deleted file mode 100644
index 041e6e3..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/focus/retargeting.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec =
- 'https://html.spec.whatwg.org/multipage/interaction.html#dom-documentorshadowroot-activeelement';
-export const requires = ['shadow.roots.attachment', 'focus.eligibility'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'exposes the host as document focus and the control as shadow focus',
- ({ document, connect }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const control = document.createElement('button');
- root.append(control);
- connect(host);
- control.focus();
- expect(document.activeElement).toBe(host);
- expect(root.activeElement).toBe(control);
- control.blur();
- expect(root.activeElement).toBeNull();
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/roots/adoption.ts b/packages/web-platform-tests/src/compatibility/shadow/roots/adoption.ts
deleted file mode 100644
index 2f2ec9e..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/roots/adoption.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#concept-node-adopt';
-const requiredGlobals = ['Document'] as const;
-export { requiredGlobals as globals };
-export const requires = ['shadow.roots.attachment', 'node.adoption'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('adopts shadow descendants with their host', ({ document, globals }) => {
- const Document = globals('Document');
- const other = new Document();
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'closed' });
- const child = document.createElement('span');
- root.append(child);
- other.adoptNode(host);
- expect(host.ownerDocument).toBe(other);
- expect(root.ownerDocument).toBe(other);
- expect(child.ownerDocument).toBe(other);
- expect(root.host).toBe(host);
- expect(root.firstChild).toBe(child);
- });
-
- context.test('updates shadow descendants ownership when the host is adopted', (fixture) => {
- const document = fixture.document as unknown as Document;
- const other = document.implementation.createHTMLDocument(''),
- host = other.createElement('div'),
- root = host.attachShadow({ mode: 'open' }),
- e = other.createElement('i');
- root.append(e);
- document.adoptNode(host);
- expect(root.ownerDocument).toBe(document);
- expect(e.ownerDocument).toBe(document);
- expect(root.host).toBe(host);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/roots/attachment.ts b/packages/web-platform-tests/src/compatibility/shadow/roots/attachment.ts
deleted file mode 100644
index 0dea158..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/roots/attachment.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#dom-element-attachshadow';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'exposes open roots and hides closed roots without changing their host',
- ({ document }) => {
- for (const mode of ['open', 'closed'] as const) {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode });
- expect(root.host).toBe(host);
- expect(root.mode).toBe(mode);
- expect(root.parentNode).toBeNull();
- expect(root.ownerDocument).toBe(document);
- expect(host.shadowRoot).toBe(mode === 'open' ? root : null);
- expect(() => host.attachShadow({ mode })).toThrow(
- expect.objectContaining({ name: 'NotSupportedError' }),
- );
- }
- },
- );
-
- context.test('keeps light and shadow trees separate with stable root identity', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- light = document.createElement('i');
- host.append(light);
- const root = host.attachShadow({ mode: 'open' }),
- child = document.createElement('b');
- root.append(child);
- expect(host.firstChild).toBe(light);
- expect(root.firstChild).toBe(child);
- expect(root.getRootNode()).toBe(root);
- expect(child.getRootNode()).toBe(root);
- expect(host.contains(child)).toBe(false);
- expect(host.shadowRoot).toBe(root);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/roots/cloning.ts b/packages/web-platform-tests/src/compatibility/shadow/roots/cloning.ts
deleted file mode 100644
index 37c6816..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/roots/cloning.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#concept-node-clone';
-const requiredGlobals = ['Element'] as const;
-export { requiredGlobals as globals };
-export const requires = ['shadow.roots.attachment', 'node.cloning'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('clones clonable roots and omits ordinary shadow roots', ({ document, globals }) => {
- const Element = globals('Element');
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open', clonable: true });
- root.append(document.createTextNode('shadow'));
- const clone = host.cloneNode(true);
- if (!(clone instanceof Element)) return expect.fail('Element clone expected');
- expect(clone.shadowRoot?.clonable).toBe(true);
- expect(clone.shadowRoot?.textContent).toBe('shadow');
- expect(clone.shadowRoot).not.toBe(root);
- const ordinary = document.createElement('div');
- ordinary.attachShadow({ mode: 'open' });
- const other = ordinary.cloneNode(true);
- if (!(other instanceof Element)) return expect.fail('Element clone expected');
- expect(other.shadowRoot).toBeNull();
- });
-
- context.test('ordinary shadow roots are not cloned without the clonable option', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div');
- host.attachShadow({ mode: 'open' }).append(document.createElement('i'));
- expect((host.cloneNode(true) as Element).shadowRoot).toBeNull();
- });
-
- context.test('clonable roots clone contents independently while keeping mode', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open', clonable: true });
- root.innerHTML = 'x ';
- const copy = host.cloneNode(true) as Element;
- expect(copy.shadowRoot!.mode).toBe('open');
- expect(copy.shadowRoot!.innerHTML).toBe('x ');
- expect(copy.shadowRoot!.firstChild).not.toBe(root.firstChild);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/roots/connectivity.ts b/packages/web-platform-tests/src/compatibility/shadow/roots/connectivity.ts
deleted file mode 100644
index 7c0b4bb..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/roots/connectivity.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts';
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#connected';
-export const requires = ['shadow.roots.attachment'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'follows host connectivity while preserving the shadow tree root',
- ({ document, connect }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const child = document.createElement('span');
- root.append(child);
- expect(child.isConnected).toBe(false);
- connect(host);
- expect(root.isConnected).toBe(true);
- expect(child.isConnected).toBe(true);
- expect(child.getRootNode()).toBe(root);
- expect(child.getRootNode({ composed: true })).toBe(document);
- expect(host.contains(child)).toBe(false);
- host.remove();
- expect(root.isConnected).toBe(false);
- expect(child.parentNode).toBe(root);
- },
- );
-
- context.test(
- 'composed roots change with host connectivity while ordinary roots stay shadow roots',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open' }),
- child = document.createElement('i');
- root.append(child);
- expect(child.isConnected).toBe(false);
- expect(child.getRootNode({ composed: true })).toBe(host);
- fixture.connect(host as unknown as DOMElementContract);
- expect(root.isConnected).toBe(true);
- expect(child.getRootNode({ composed: true })).toBe(document);
- expect(child.getRootNode()).toBe(root);
- host.remove();
- expect(root.isConnected).toBe(false);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/roots/host-eligibility.ts b/packages/web-platform-tests/src/compatibility/shadow/roots/host-eligibility.ts
deleted file mode 100644
index c45c646..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/roots/host-eligibility.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#dom-element-attachshadow';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('rejects HTML elements that cannot be shadow hosts', ({ document }) => {
- const input = document.createElement('input');
- expect(() => input.attachShadow({ mode: 'open' })).toThrow(
- expect.objectContaining({ name: 'NotSupportedError' }),
- );
- });
-
- context.test('rejects SVG elements and standard HTML non-host elements', (fixture) => {
- const document = fixture.document as unknown as Document;
- for (const host of [
- document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
- document.createElement('input'),
- document.createElement('a'),
- document.createElement('br'),
- ])
- expect(() => host.attachShadow({ mode: 'open' })).toThrow(
- expect.objectContaining({ name: 'NotSupportedError' }),
- );
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/serialization.ts b/packages/web-platform-tests/src/compatibility/shadow/serialization.ts
deleted file mode 100644
index 01cdbf6..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/serialization.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec =
- 'https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-element-gethtml';
-export const requires = ['shadow.roots.attachment', 'parsing.serialization'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('serializes explicitly included and serializable shadow roots', ({ document }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open', serializable: true });
- const child = document.createElement('span');
- child.textContent = 'shadow';
- root.append(child);
- host.append(document.createTextNode('light'));
- expect(host.getHTML()).toBe('light');
- const serialized = host.getHTML({ serializableShadowRoots: true });
- expect(serialized).toContain('shadowrootmode="open"');
- expect(serialized).toContain('shadow ');
- expect(serialized.endsWith('light')).toBe(true);
- expect(host.getHTML({ shadowRoots: [root] })).toBe(serialized);
- });
-
- context.test('innerHTML and outerHTML exclude shadow markup by default', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div');
- host.innerHTML = 'light ';
- host.attachShadow({ mode: 'open' }).innerHTML = 'shadow ';
- expect(host.innerHTML).toBe('light ');
- expect(host.outerHTML).toBe('light
');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/slots/fallback.ts b/packages/web-platform-tests/src/compatibility/shadow/slots/fallback.ts
deleted file mode 100644
index 0b9c9a1..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/slots/fallback.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#find-flattened-slotables';
-export const requires = ['shadow.slots.named'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'includes fallback children only in flattened assignment results',
- ({ document }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const slot = document.createElement('slot');
- const fallback = document.createElement('span');
- slot.append(fallback);
- root.append(slot);
- expect(slot.assignedNodes()).toEqual([]);
- expect(slot.assignedNodes({ flatten: true })).toEqual([fallback]);
- const child = document.createTextNode('assigned');
- host.append(child);
- expect(slot.assignedNodes({ flatten: true })).toEqual([child]);
- },
- );
-
- context.test('uses fallback only for flattened assignment and excludes comments', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open' }),
- slot = document.createElement('slot'),
- fallback = document.createTextNode('fallback');
- slot.append(fallback);
- root.append(slot);
- host.append(document.createComment('ignored'));
- expect(slot.assignedNodes()).toEqual([]);
- expect(slot.assignedNodes({ flatten: true })).toEqual([fallback]);
- const light = document.createTextNode('light');
- host.append(light);
- expect(slot.assignedNodes({ flatten: true })).toEqual([light]);
- expect(light.assignedSlot).toBe(slot);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/slots/manual.ts b/packages/web-platform-tests/src/compatibility/shadow/slots/manual.ts
deleted file mode 100644
index 2c3ca97..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/slots/manual.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://html.spec.whatwg.org/multipage/scripting.html#dom-slot-assign';
-export const requires = ['shadow.roots.attachment'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('honors manual assignment order without duplicating slottables', ({ document }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open', slotAssignment: 'manual' });
- const slot = document.createElement('slot');
- root.append(slot);
- const first = document.createElement('span');
- const second = document.createTextNode('second');
- host.append(first, second);
- expect(slot.assignedNodes()).toEqual([]);
- slot.assign(second, first, second);
- expect(root.slotAssignment).toBe('manual');
- expect(slot.assignedNodes()).toEqual([second, first]);
- expect(first.assignedSlot).toBe(slot);
- slot.assign();
- expect(slot.assignedNodes()).toEqual([]);
- });
-
- context.test(
- 'manual assignments replace previous assignments and update assignedSlot',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open', slotAssignment: 'manual' }),
- slot = document.createElement('slot'),
- a = document.createElement('i'),
- b = document.createTextNode('b');
- root.append(slot);
- host.append(a, b);
- slot.assign(a, b);
- expect(slot.assignedNodes()).toEqual([a, b]);
- slot.assign(b);
- expect(slot.assignedNodes()).toEqual([b]);
- expect(a.assignedSlot).toBeNull();
- expect(b.assignedSlot).toBe(slot);
- slot.assign();
- expect(slot.assignedNodes()).toEqual([]);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/slots/named.ts b/packages/web-platform-tests/src/compatibility/shadow/slots/named.ts
deleted file mode 100644
index 183f1bd..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/slots/named.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#find-slot';
-export const requires = ['shadow.roots.attachment'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('assigns matching light children to the first named slot', ({ document }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const first = document.createElement('slot');
- const duplicate = document.createElement('slot');
- first.name = duplicate.name = 'named';
- root.append(first, duplicate);
- const child = document.createElement('span');
- child.slot = 'named';
- const text = document.createTextNode('unmatched');
- host.append(child, text);
- expect(first.assignedNodes()).toEqual([child]);
- expect(first.assignedElements()).toEqual([child]);
- expect(duplicate.assignedNodes()).toEqual([]);
- expect(child.assignedSlot).toBe(first);
- child.slot = 'other';
- expect(first.assignedNodes()).toEqual([]);
- expect(child.assignedSlot).toBeNull();
- });
-
- context.test('assigns to the first matching slot and redistributes on renaming', (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open' }),
- first = document.createElement('slot'),
- second = document.createElement('slot'),
- light = document.createElement('i');
- first.name = second.name = 'x';
- root.append(first, second);
- light.slot = 'x';
- host.append(light);
- expect(first.assignedNodes()).toEqual([light]);
- expect(second.assignedNodes()).toEqual([]);
- expect(light.assignedSlot).toBe(first);
- first.name = 'y';
- expect(second.assignedNodes()).toEqual([light]);
- expect(light.assignedSlot).toBe(second);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/slots/observer-ordering.ts b/packages/web-platform-tests/src/compatibility/shadow/slots/observer-ordering.ts
deleted file mode 100644
index e1a4011..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/slots/observer-ordering.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-// Adapted from WPT slotchange-event.html at b719612f963a05ece9c89a7dd7598c1c59618874.
-// Copyright © web-platform-tests contributors. Original authors: Ryosuke Niwa and Hayato Ito.
-// BSD-3-Clause; see ../../../../test/wpt/LICENSE.
-import type { CompatibilityContext } from '../../context.ts';
-import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts';
-
-export const spec = 'https://dom.spec.whatwg.org/#notify-mutation-observers';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/shadow-dom/slotchange-event.html';
-export const globals = ['MutationObserver'] as const;
-export const requires = [
- 'shadow.slots.slotchange',
- 'mutationObserver.microtaskDelivery',
- 'mutationObserver.filtering',
-] as const;
-
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- for (const mode of ['open', 'closed'] as const) {
- for (const connected of [false, true]) {
- context.test(
- `orders observer callbacks and reentrant slot signals in a ${connected ? 'connected' : 'detached'} ${mode} root`,
- async (fixture) => {
- const document = fixture.document as unknown as Document;
- const MutationObserver = fixture.globals(
- 'MutationObserver',
- ) as unknown as typeof globalThis.MutationObserver;
- const host = document.createElement('div');
- const root = host.attachShadow({ mode });
- const slot = document.createElement('slot');
- root.append(slot);
- if (connected) fixture.connect(host as unknown as DOMElementContract);
- const observed = document.createElement('div');
- const calls: string[] = [];
- slot.addEventListener('slotchange', () => calls.push('slotchange'));
- const first = new MutationObserver(() => {
- calls.push('observer:id');
- host.append(document.createElement('span'));
- observed.setAttribute('title', 'next');
- });
- const second = new MutationObserver(() => calls.push('observer:title'));
- fixture.cleanup(() => first.disconnect());
- fixture.cleanup(() => second.disconnect());
- first.observe(observed, { attributes: true, attributeFilter: ['id'] });
- second.observe(observed, { attributes: true, attributeFilter: ['title'] });
- observed.id = 'start';
- host.append(document.createElement('div'));
- expect(calls).toEqual([]);
- await new Promise((resolve) => setTimeout(resolve, 0));
- expect(calls).toEqual(['observer:id', 'slotchange', 'observer:title', 'slotchange']);
- },
- );
- }
- }
-
- context.test(
- 'delivers pending slot signals after removal and after observer delivery',
- async (fixture) => {
- const document = fixture.document as unknown as Document;
- const MutationObserver = fixture.globals(
- 'MutationObserver',
- ) as unknown as typeof globalThis.MutationObserver;
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const slot = document.createElement('slot');
- root.append(slot);
- const calls: string[] = [];
- const observer = new MutationObserver(() => calls.push('observer'));
- fixture.cleanup(() => observer.disconnect());
- observer.observe(host, { childList: true });
- slot.addEventListener('slotchange', () => calls.push('slotchange'));
- host.append(document.createElement('span'));
- slot.remove();
- await new Promise((resolve) => setTimeout(resolve, 0));
- expect(calls).toEqual(['observer', 'slotchange']);
- expect(slot.assignedNodes()).toEqual([]);
- },
- );
-
- context.test('orders distinct slot signals by their first assignment change', async (fixture) => {
- const document = fixture.document as unknown as Document;
- const MutationObserver = fixture.globals(
- 'MutationObserver',
- ) as unknown as typeof globalThis.MutationObserver;
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const first = document.createElement('slot'),
- second = document.createElement('slot');
- first.name = 'first';
- second.name = 'second';
- root.append(first, second);
- const calls: string[] = [];
- const observer = new MutationObserver(() => calls.push('observer'));
- fixture.cleanup(() => observer.disconnect());
- observer.observe(host, { childList: true });
- first.addEventListener('slotchange', () => calls.push('first'));
- second.addEventListener('slotchange', () => calls.push('second'));
- for (const name of ['second', 'first', 'second']) {
- const node = document.createElement('span');
- node.slot = name;
- host.append(node);
- }
- await new Promise((resolve) => setTimeout(resolve, 0));
- expect(calls).toEqual(['observer', 'second', 'first']);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/shadow/slots/slotchange.ts b/packages/web-platform-tests/src/compatibility/shadow/slots/slotchange.ts
deleted file mode 100644
index b041999..0000000
--- a/packages/web-platform-tests/src/compatibility/shadow/slots/slotchange.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { CompatibilityContext } from '../../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#signal-a-slot-change';
-export const requires = ['shadow.slots.named', 'events.dispatch.target'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('coalesces slotchange at a microtask checkpoint', async ({ document }) => {
- const host = document.createElement('div');
- const root = host.attachShadow({ mode: 'open' });
- const slot = document.createElement('slot');
- root.append(slot);
- await Promise.resolve();
- let count = 0;
- slot.addEventListener('slotchange', (event) => {
- count++;
- expect(event.bubbles).toBe(true);
- expect(event.composed).toBe(false);
- expect(event.target).toBe(slot);
- });
- host.append(document.createTextNode('one'));
- host.append(document.createTextNode('two'));
- expect(count).toBe(0);
- await Promise.resolve();
- expect(count).toBe(1);
- });
-
- context.test(
- 'coalesces assignment changes into one asynchronous bubbling event',
- async (fixture) => {
- const document = fixture.document as unknown as Document;
- const host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open' }),
- slot = document.createElement('slot');
- root.append(slot);
- let count = 0;
- const targets: unknown[] = [];
- root.addEventListener('slotchange', (e) => {
- count++;
- targets.push(e.target);
- expect(e.composed).toBe(false);
- });
- host.append(document.createElement('i'), document.createTextNode('x'));
- expect(count).toBe(0);
- await new Promise((resolve) => queueMicrotask(resolve));
- expect(count).toBe(1);
- expect(targets).toEqual([slot]);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/style-fixture.ts b/packages/web-platform-tests/src/compatibility/style-fixture.ts
deleted file mode 100644
index fdbe0e6..0000000
--- a/packages/web-platform-tests/src/compatibility/style-fixture.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { DOMFixture } from './context.ts';
-import type { DOMElement } from './contracts/nodes.ts';
-
-/** Connect markup and a stylesheet using only the consumer's DOM and style bindings. */
-export function createStyleFixture(
- fixture: DOMFixture,
- { markup, css }: { markup: string; css: string },
-): {
- document: Document;
- root: HTMLDivElement;
- get: typeof getComputedStyle;
-} {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div');
- const style = document.createElement('style');
- root.innerHTML = markup;
- style.textContent = css;
- fixture.connect(style as unknown as DOMElement);
- fixture.connect(root as unknown as DOMElement);
- return { document, root, get: fixture.globals('getComputedStyle') as typeof getComputedStyle };
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/attribute-synchronization.ts b/packages/web-platform-tests/src/compatibility/style/attribute-synchronization.ts
deleted file mode 100644
index 932f8ec..0000000
--- a/packages/web-platform-tests/src/compatibility/style/attribute-synchronization.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://drafts.csswg.org/cssom/#dom-elementcssinlinestyle-style';
-export const requires = ['style.inline', 'element.attributes'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('updates style attribute from declaration mutations', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div');
- e.style.color = 'red';
- expect(e.getAttribute('style')).toBe('color: red;');
- e.style.removeProperty('color');
- expect(e.getAttribute('style')).toBe('');
- });
- context.test(
- 'updates retained declarations from attribute replacement and removal',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div'),
- s = e.style;
- e.setAttribute('style', 'color: red; display: block');
- expect(s.color).toBe('red');
- expect(s.display).toBe('block');
- e.setAttribute('style', 'color: blue');
- expect(s.color).toBe('blue');
- expect(s.display).toBe('');
- e.removeAttribute('style');
- expect(s.length).toBe(0);
- expect(e.style).toBe(s);
- },
- );
- context.test('replaces all declarations when cssText is assigned', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div');
- e.style.cssText = 'color:red; display:block';
- e.style.cssText = 'visibility: hidden';
- expect(e.style.color).toBe('');
- expect(e.style.length).toBe(1);
- expect(e.getAttribute('style')).toBe('visibility: hidden;');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/cascade.ts b/packages/web-platform-tests/src/compatibility/style/cascade.ts
deleted file mode 100644
index 75be18e..0000000
--- a/packages/web-platform-tests/src/compatibility/style/cascade.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts';
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://drafts.csswg.org/css-cascade/#cascade';
-export const requires = ['style.computed'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('uses inheritance, initial and unset independently of layout', (fixture) => {
- const document = fixture.document as unknown as Document;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle,
- p = document.createElement('div'),
- e = document.createElement('i');
- p.style.color = 'rgb(1, 2, 3)';
- p.style.visibility = 'hidden';
- p.append(e);
- fixture.connect(p as unknown as DOMElementContract);
- e.style.color = 'inherit';
- expect(get(e).color).toBe('rgb(1, 2, 3)');
- e.style.visibility = 'initial';
- expect(get(e).visibility).toBe('visible');
- e.style.visibility = 'unset';
- expect(get(e).visibility).toBe('hidden');
- });
- context.test('honors inline important over stylesheet important', (fixture) => {
- const document = fixture.document as unknown as Document;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle,
- s = document.createElement('style'),
- e = document.createElement('i');
- e.id = 'cascade';
- s.textContent = '#cascade {color:rgb(1, 1, 1)!important}';
- fixture.connect(s as unknown as DOMElementContract);
- fixture.connect(e as unknown as DOMElementContract);
- e.style.setProperty('color', 'rgb(2, 2, 2)', 'important');
- expect(get(e).color).toBe('rgb(2, 2, 2)');
- });
- context.test(
- 'resolves custom property cycles to a fallback without changing inline declarations',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle,
- e = document.createElement('div');
- fixture.connect(e as unknown as DOMElementContract);
- e.style.setProperty('--a', 'var(--b)');
- e.style.setProperty('--b', 'var(--a)');
- e.style.color = 'var(--a, rgb(3, 4, 5))';
- expect(get(e).color).toBe('rgb(3, 4, 5)');
- expect(e.style.getPropertyValue('--a')).toBe('var(--b)');
- },
- );
- context.test('updates cascade after rule removal and style element removal', (fixture) => {
- const document = fixture.document as unknown as Document;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle,
- p = document.createElement('div'),
- e = document.createElement('i'),
- s = document.createElement('style');
- p.style.color = 'rgb(9, 9, 9)';
- e.className = 'cascade';
- p.append(e);
- s.textContent = '.cascade {color:rgb(1, 1, 1)} .cascade {color:rgb(2, 2, 2)}';
- fixture.connect(p as unknown as DOMElementContract);
- fixture.connect(s as unknown as DOMElementContract);
- expect(get(e).color).toBe('rgb(2, 2, 2)');
- s.sheet!.deleteRule(1);
- expect(get(e).color).toBe('rgb(1, 1, 1)');
- s.remove();
- expect(get(e).color).toBe('rgb(9, 9, 9)');
- });
- context.test(
- 'scopes shadow styles and inherits host values into the shadow tree',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle,
- host = document.createElement('div'),
- root = host.attachShadow({ mode: 'open' }),
- e = document.createElement('i'),
- s = document.createElement('style'),
- outside = document.createElement('i');
- host.style.color = 'rgb(1, 2, 3)';
- root.append(e, s);
- s.textContent = 'i {visibility:hidden}';
- fixture.connect(host as unknown as DOMElementContract);
- fixture.connect(outside as unknown as DOMElementContract);
- expect(get(e).color).toBe('rgb(1, 2, 3)');
- expect(get(e).visibility).toBe('hidden');
- expect(get(outside).visibility).toBe('visible');
- },
- ['shadow.roots.attachment'],
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/computed.ts b/packages/web-platform-tests/src/compatibility/style/computed.ts
deleted file mode 100644
index dc8960e..0000000
--- a/packages/web-platform-tests/src/compatibility/style/computed.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts';
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle';
-export const requires = ['style.inline', 'style.stylesheets', 'style.customProperties'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('computes layout independent inherited properties and remains live', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- e = document.createElement('i');
- p.append(e);
- fixture.connect(p as unknown as DOMElementContract);
- p.style.color = 'rgb(1, 2, 3)';
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- const s = get(e);
- expect(s.color).toBe('rgb(1, 2, 3)');
- p.style.color = 'rgb(4, 5, 6)';
- expect(s.color).toBe('rgb(4, 5, 6)');
- e.style.color = 'rgb(7, 8, 9)';
- expect(s.color).toBe('rgb(7, 8, 9)');
- });
- context.test(
- 'applies specificity, source order, importance and inline declarations',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const style = document.createElement('style'),
- e = document.createElement('i');
- e.id = 'test';
- e.className = 'x';
- style.textContent =
- 'i {color:rgb(1, 1, 1)} .x {color:rgb(2, 2, 2)} #test {color:rgb(3, 3, 3)} #test {color:rgb(4, 4, 4)}';
- fixture.connect(style as unknown as DOMElementContract);
- fixture.connect(e as unknown as DOMElementContract);
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- expect(get(e).color).toBe('rgb(4, 4, 4)');
- e.style.color = 'rgb(5, 5, 5)';
- expect(get(e).color).toBe('rgb(5, 5, 5)');
- (style.sheet!.cssRules[0] as CSSStyleRule).style.setProperty(
- 'color',
- 'rgb(6, 6, 6)',
- 'important',
- );
- expect(get(e).color).toBe('rgb(6, 6, 6)');
- },
- );
- context.test('inherits custom properties and resolves variable fallbacks', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div'),
- e = document.createElement('i');
- p.append(e);
- fixture.connect(p as unknown as DOMElementContract);
- p.style.setProperty('--ink', 'rgb(1, 2, 3)');
- e.style.color = 'var(--ink)';
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- expect(get(e).color).toBe('rgb(1, 2, 3)');
- e.style.color = 'var(--missing, rgb(4, 5, 6))';
- expect(get(e).color).toBe('rgb(4, 5, 6)');
- });
- context.test('returns a readonly computed declaration', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div');
- fixture.connect(e as unknown as DOMElementContract);
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- expect(() => get(e).setProperty('color', 'red')).toThrow(
- expect.objectContaining({ name: 'NoModificationAllowedError' }),
- );
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/constructed-stylesheets.ts b/packages/web-platform-tests/src/compatibility/style/constructed-stylesheets.ts
deleted file mode 100644
index 259d9ce..0000000
--- a/packages/web-platform-tests/src/compatibility/style/constructed-stylesheets.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts';
-
-export const spec = 'https://drafts.csswg.org/cssom/#dom-cssstylesheet-replace';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/cssom/CSSStyleSheet-constructable-replace-cssRules.html';
-export const globals = ['CSSStyleSheet', 'getComputedStyle'] as const;
-export const requires = ['style.stylesheets', 'style.computed'] as const;
-
-// Adapted from WPT at b719612f963a05ece9c89a7dd7598c1c59618874.
-// Copyright © web-platform-tests contributors. Authors: Emilio Cobos Álvarez,
-// Mozilla, Rakina Zata Amni. BSD-3-Clause; see ../../../test/wpt/LICENSE
-// and ../../../docs/wpt-css-forms.md for the complete source inventory.
-
-/** Registers constructed stylesheet identity, adoption and mutation cases. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'constructs empty ownerless sheets with explicit media and disabled state',
- (fixture) => {
- const Sheet = fixture.globals('CSSStyleSheet') as typeof CSSStyleSheet;
- const sheet = new Sheet();
- expect(sheet.ownerNode).toBeNull();
- expect(sheet.ownerRule).toBeNull();
- expect(sheet.href).toBeNull();
- expect(sheet.title).toBeNull();
- expect(sheet.disabled).toBe(false);
- expect(sheet.cssRules.length).toBe(0);
- expect(sheet.media.length).toBe(0);
- const configured = new Sheet({ disabled: true, media: 'screen, print' });
- expect(configured.disabled).toBe(true);
- expect(Array.from(configured.media)).toEqual(['screen', 'print']);
- },
- );
- for (const sync of [true, false]) {
- context.test(
- `retains live cssRules across ${sync ? 'replaceSync' : 'replace'} and adoption`,
- async (fixture) => {
- const document = fixture.document as unknown as Document;
- const Sheet = fixture.globals('CSSStyleSheet') as typeof CSSStyleSheet;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- const sheet = new Sheet(),
- rules = sheet.cssRules;
- const parent = document.createElement('div'),
- target = document.createElement('i');
- parent.style.color = 'rgb(1, 2, 3)';
- target.className = 'constructed-target';
- parent.append(target);
- fixture.connect(parent as unknown as DOMElementContract);
- const previous = [...document.adoptedStyleSheets];
- fixture.cleanup(() => {
- document.adoptedStyleSheets = previous;
- });
- for (const color of ['red', 'green']) {
- if (sync)
- expect(sheet.replaceSync(`.constructed-target {color:${color}}`)).toBeUndefined();
- else expect(await sheet.replace(`.constructed-target {color:${color}}`)).toBe(sheet);
- expect(sheet.cssRules).toBe(rules);
- expect(rules.length).toBe(1);
- expect((rules[0] as CSSStyleRule).style.color).toBe(color);
- }
- document.adoptedStyleSheets.push(sheet);
- expect(get(target).color).toBe('rgb(0, 128, 0)');
- expect(document.adoptedStyleSheets.pop()).toBe(sheet);
- expect(get(target).color).toBe('rgb(1, 2, 3)');
- },
- );
- }
- context.test('adoption order and disabled toggles immediately change the cascade', (fixture) => {
- const document = fixture.document as unknown as Document;
- const Sheet = fixture.globals('CSSStyleSheet') as typeof CSSStyleSheet;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- const a = new Sheet(),
- b = new Sheet(),
- target = document.createElement('div');
- target.className = 'constructed-order';
- a.replaceSync('.constructed-order {color:red}');
- b.replaceSync('.constructed-order {color:blue}');
- fixture.connect(target as unknown as DOMElementContract);
- const previous = [...document.adoptedStyleSheets];
- fixture.cleanup(() => {
- document.adoptedStyleSheets = previous;
- });
- document.adoptedStyleSheets = [a, b];
- expect(get(target).color).toBe('rgb(0, 0, 255)');
- b.disabled = true;
- expect(get(target).color).toBe('rgb(255, 0, 0)');
- b.disabled = false;
- expect(get(target).color).toBe('rgb(0, 0, 255)');
- document.adoptedStyleSheets = [b, a];
- expect(get(target).color).toBe('rgb(255, 0, 0)');
- });
- context.test('rejects replacement and adoption of a style-element sheet', async (fixture) => {
- const document = fixture.document as unknown as Document;
- const style = document.createElement('style');
- style.textContent = 'i {color:red}';
- fixture.connect(style as unknown as DOMElementContract);
- const sheet = style.sheet!;
- const previous = [...document.adoptedStyleSheets];
- fixture.cleanup(() => {
- document.adoptedStyleSheets = previous;
- });
- expect(() => sheet.replaceSync('i {color:blue}')).toThrow(
- expect.objectContaining({ name: 'NotAllowedError' }),
- );
- await expect(sheet.replace('i {color:blue}')).rejects.toMatchObject({
- name: 'NotAllowedError',
- });
- expect(() => {
- document.adoptedStyleSheets = [sheet];
- }).toThrow(expect.objectContaining({ name: 'NotAllowedError' }));
- expect([...document.adoptedStyleSheets]).toEqual(previous);
- expect((sheet.cssRules[0] as CSSStyleRule).style.color).toBe('red');
- });
- context.test(
- 'updates all shadow adopters while retaining separate adoption lists',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const Sheet = fixture.globals('CSSStyleSheet') as typeof CSSStyleSheet;
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- const sheet = new Sheet();
- const roots = [0, 1].map(() => {
- const host = document.createElement('div');
- host.style.color = 'rgb(1, 2, 3)';
- const root = host.attachShadow({ mode: 'open' });
- root.append(document.createElement('i'));
- root.adoptedStyleSheets = [sheet];
- fixture.connect(host as unknown as DOMElementContract);
- return root;
- });
- sheet.replaceSync('i {color:red}');
- expect(roots.map((root) => get(root.firstElementChild!).color)).toEqual([
- 'rgb(255, 0, 0)',
- 'rgb(255, 0, 0)',
- ]);
- sheet.replaceSync('i {color:blue}');
- expect(roots.map((root) => get(root.firstElementChild!).color)).toEqual([
- 'rgb(0, 0, 255)',
- 'rgb(0, 0, 255)',
- ]);
- roots[0]!.adoptedStyleSheets = [];
- expect(roots.map((root) => get(root.firstElementChild!).color)).toEqual([
- 'rgb(1, 2, 3)',
- 'rgb(0, 0, 255)',
- ]);
- },
- ['shadow.roots.attachment'],
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/control-selectors.ts b/packages/web-platform-tests/src/compatibility/style/control-selectors.ts
deleted file mode 100644
index 5657392..0000000
--- a/packages/web-platform-tests/src/compatibility/style/control-selectors.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-// Adapted from WPT checked.html, valid-invalid.html and disabled.html at
-// b719612f963a05ece9c89a7dd7598c1c59618874. Copyright web-platform-tests contributors;
-// Denis Ah-Kang. BSD-3-Clause; see ../../../test/wpt/LICENSE and ../../../docs/wpt-state-styles.md.
-import type { CompatibilityContext } from '../context.ts';
-import { createStyleFixture } from '../style-fixture.ts';
-export const spec = 'https://html.spec.whatwg.org/multipage/semantics-other.html#pseudo-classes';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/html/semantics/selectors/pseudo-classes/valid-invalid.html';
-export const requires = [
- 'element.selectors',
- 'parsing.fragments',
- 'style.computed',
- 'elements.input.checkedness',
- 'elements.input.validation',
-] as const;
-
-export const globals = ['getComputedStyle'] as const;
-/** Registers control state, selector and computed style interactions. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'checked follows live state rather than the default attribute and changes with input type',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, {
- markup: ' ',
- css: 'input {color:rgb(1,2,3)} input:checked {color:rgb(4,5,6)}',
- });
- const target = root.querySelector('input')!;
- expect(target.matches(':checked')).toBe(true);
- expect(get(target).color).toBe('rgb(4, 5, 6)');
- target.checked = false;
- expect(target.hasAttribute('checked')).toBe(true);
- expect(target.matches(':checked')).toBe(false);
- expect(get(target).color).toBe('rgb(1, 2, 3)');
- target.checked = true;
- target.type = 'text';
- expect(target.matches(':checked')).toBe(false);
- expect(get(target).color).toBe('rgb(1, 2, 3)');
- target.type = 'checkbox';
- expect(target.matches(':checked')).toBe(true);
- },
- );
- context.test(
- 'option checked matching follows selectedness and select reset',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, {
- markup: '',
- css: 'option {color:rgb(1,2,3)} option:checked {color:rgb(4,5,6)}',
- });
- const form = root.querySelector('form')!,
- select = root.querySelector('select')!;
- const first = select.options[0]!,
- second = select.options[1]!;
- second.selected = true;
- expect(first.matches(':checked')).toBe(false);
- expect(second.matches(':checked')).toBe(true);
- expect(get(first).color).toBe('rgb(1, 2, 3)');
- expect(get(second).color).toBe('rgb(4, 5, 6)');
- form.reset();
- expect(first.matches(':checked')).toBe(true);
- expect(second.matches(':checked')).toBe(false);
- expect(get(first).color).toBe('rgb(4, 5, 6)');
- },
- ['elements.select.selection', 'elements.option.selectedness', 'elements.form.reset'],
- );
- context.test(
- 'required, pattern and custom validity invalidate selectors and retained styles',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, {
- markup: ' ',
- css: 'input:invalid {color:rgb(1,2,3)} input:valid {color:rgb(4,5,6)}',
- });
- const target = root.querySelector('input')!,
- retained = get(target);
- expect(target.matches(':invalid')).toBe(true);
- expect(retained.color).toBe('rgb(1, 2, 3)');
- target.value = 'AB';
- expect(target.matches(':valid')).toBe(true);
- expect(retained.color).toBe('rgb(4, 5, 6)');
- target.setCustomValidity('Rejected');
- expect(target.matches(':invalid')).toBe(true);
- expect(retained.color).toBe('rgb(1, 2, 3)');
- target.setCustomValidity('');
- target.value = 'bad';
- expect(target.matches(':invalid')).toBe(true);
- target.removeAttribute('pattern');
- expect(target.matches(':valid')).toBe(true);
- },
- );
- for (const tag of ['form', 'fieldset'] as const) {
- context.test(
- `${tag} aggregate validity updates on insertion, type changes and removal`,
- (fixture) => {
- const { document, root, get } = createStyleFixture(fixture, {
- markup: `<${tag}>${tag}>`,
- css: `${tag}:valid {color:rgb(1,2,3)} ${tag}:invalid {color:rgb(4,5,6)}`,
- });
- const parent = root.querySelector(tag)!;
- expect(parent.matches(':valid')).toBe(true);
- const target = document.createElement('input');
- target.type = 'text';
- target.min = '8';
- target.value = '4';
- parent.append(target);
- expect(parent.matches(':valid')).toBe(true);
- target.type = 'number';
- expect(parent.matches(':invalid')).toBe(true);
- expect(get(parent).color).toBe('rgb(4, 5, 6)');
- target.value = '9';
- expect(parent.matches(':valid')).toBe(true);
- expect(get(parent).color).toBe('rgb(1, 2, 3)');
- target.value = '4';
- target.remove();
- expect(parent.matches(':valid')).toBe(true);
- },
- ['elements.form.validation', 'elements.input.numericValue'],
- );
- }
- context.test(
- 'disabled fieldsets style descendants but exempt the first legend subtree',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, {
- markup:
- ' ',
- css: 'input:enabled {color:rgb(1,2,3)} input:disabled {color:rgb(4,5,6)}',
- });
- const fieldset = root.querySelector('fieldset')!,
- exempt = root.querySelector('#exempt')!,
- blocked = root.querySelector('#blocked')!;
- expect(blocked.disabled).toBe(false);
- expect(blocked.matches(':disabled')).toBe(true);
- expect(get(blocked).color).toBe('rgb(4, 5, 6)');
- expect(exempt.matches(':enabled')).toBe(true);
- expect(get(exempt).color).toBe('rgb(1, 2, 3)');
- fieldset.disabled = false;
- expect(blocked.matches(':enabled')).toBe(true);
- expect(get(blocked).color).toBe('rgb(1, 2, 3)');
- fieldset.disabled = true;
- fieldset.querySelector('legend')!.append(blocked);
- expect(blocked.matches(':enabled')).toBe(true);
- },
- ['elements.fieldset.disabled'],
- );
- context.test(
- 'disabled and readonly controls are excluded from valid and invalid matching',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, {
- markup: ' ',
- css: 'input {color:rgb(1,2,3)} input:invalid {color:rgb(4,5,6)}',
- });
- const target = root.querySelector('input')!;
- for (const property of ['disabled', 'readOnly'] as const) {
- target[property] = true;
- expect(target.matches(':valid, :invalid')).toBe(false);
- expect(get(target).color).toBe('rgb(1, 2, 3)');
- target[property] = false;
- expect(target.matches(':invalid')).toBe(true);
- expect(get(target).color).toBe('rgb(4, 5, 6)');
- }
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/custom-properties.ts b/packages/web-platform-tests/src/compatibility/style/custom-properties.ts
deleted file mode 100644
index be6a2e5..0000000
--- a/packages/web-platform-tests/src/compatibility/style/custom-properties.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-setproperty';
-export const requires = ['style.inline'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('preserves case sensitive custom properties and priorities', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.createElement('div').style;
- s.setProperty('--A', 'one');
- s.setProperty('--a', 'two', 'important');
- expect(s.getPropertyValue('--A')).toBe('one');
- expect(s.getPropertyValue('--a')).toBe('two');
- expect(s.getPropertyPriority('--a')).toBe('important');
- expect(s.removeProperty('--A')).toBe('one');
- expect(s.getPropertyValue('--a')).toBe('two');
- });
- context.test('round trips variable references without resolving inline values', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.createElement('div').style;
- s.setProperty('--size', '12px');
- s.setProperty('width', 'var(--size, 1px)');
- expect(s.getPropertyValue('width')).toBe('var(--size, 1px)');
- s.setProperty('--size', '20px');
- expect(s.getPropertyValue('width')).toBe('var(--size, 1px)');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/focus-state.ts b/packages/web-platform-tests/src/compatibility/style/focus-state.ts
deleted file mode 100644
index 48717ed..0000000
--- a/packages/web-platform-tests/src/compatibility/style/focus-state.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-// Adapted from WPT focus-within-011.html, revision b719612f963a05ece9c89a7dd7598c1c59618874.
-// Copyright web-platform-tests contributors; Ethan Lin and Mozilla. BSD-3-Clause.
-// See ../../../test/wpt/LICENSE and ../../../docs/wpt-state-styles.md.
-import type { CompatibilityContext } from '../context.ts';
-import { createStyleFixture } from '../style-fixture.ts';
-
-export const spec = 'https://drafts.csswg.org/selectors-4/#focus-within-pseudo';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/selectors/focus-within-011.html';
-export const requires = [
- 'element.selectors',
- 'parsing.fragments',
- 'style.computed',
- 'focus.transitions',
-] as const;
-export const globals = ['getComputedStyle'] as const;
-
-/** Registers selector matching and style invalidation across focus transitions. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- const markup =
- 'Outside ';
- const css =
- 'section, button {color:rgb(1,2,3)} section:focus-within {color:rgb(4,5,6)} button:focus {color:rgb(7,8,9)}';
- context.test(
- 'focus matches the target while focus-within includes its ancestors and itself',
- (fixture) => {
- const { document, root, get } = createStyleFixture(fixture, { markup: markup, css: css });
- const target = root.querySelector('#first')!;
- const parent = root.querySelector('section')!;
- expect(target.matches(':focus')).toBe(false);
- expect(parent.matches(':focus-within')).toBe(false);
- const retained = get(target);
- expect(retained.color).toBe('rgb(1, 2, 3)');
- target.focus();
- expect(document.activeElement).toBe(target);
- expect(document.querySelector(':focus')).toBe(target);
- expect(target.matches(':focus:focus-within')).toBe(true);
- expect(parent.matches(':focus')).toBe(false);
- expect(parent.matches(':focus-within')).toBe(true);
- expect(retained.color).toBe('rgb(7, 8, 9)');
- expect(get(parent).color).toBe('rgb(4, 5, 6)');
- },
- );
- context.test(
- 'moving focus between siblings invalidates both targets and retains ancestor state',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, { markup: markup, css: css });
- const first = root.querySelector('#first')!;
- const second = root.querySelector('#second')!;
- const parent = root.querySelector('section')!;
- first.focus();
- second.focus();
- expect(first.matches(':focus-within')).toBe(false);
- expect(get(first).color).toBe('rgb(1, 2, 3)');
- expect(second.matches(':focus')).toBe(true);
- expect(get(second).color).toBe('rgb(7, 8, 9)');
- expect(get(parent).color).toBe('rgb(4, 5, 6)');
- root.querySelector('#outside')!.focus();
- expect(parent.matches(':focus-within')).toBe(false);
- expect(get(parent).color).toBe('rgb(1, 2, 3)');
- },
- );
- context.test(
- 'blur clears matching and retained computed styles without changing attributes',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, { markup: markup, css: css });
- const target = root.querySelector('#first')!;
- const original = target.outerHTML;
- target.focus();
- const computed = get(target);
- target.blur();
- expect(target.matches(':focus, :focus-within')).toBe(false);
- expect(computed.color).toBe('rgb(1, 2, 3)');
- expect(target.outerHTML).toBe(original);
- },
- ['parsing.serialization'],
- );
- context.test(
- 'focus listeners observe the new matching state and computed declaration',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, { markup: markup, css: css });
- const target = root.querySelector('#first')!;
- const seen: unknown[] = [];
- target.addEventListener('focus', () =>
- seen.push(target.matches(':focus'), get(target).color),
- );
- target.focus();
- expect(seen).toEqual([true, 'rgb(7, 8, 9)']);
- },
- );
- context.test(
- 'removing a focused descendant clears ancestor selector and computed state',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, { markup: markup, css: css });
- const target = root.querySelector('#first')!;
- const parent = root.querySelector('section')!;
- target.focus();
- target.remove();
- expect(target.matches(':focus')).toBe(false);
- expect(parent.matches(':focus-within')).toBe(false);
- expect(get(parent).color).toBe('rgb(1, 2, 3)');
- },
- );
- for (const mode of ['open', 'closed'] as const) {
- context.test(
- `focus matching crosses a ${mode} shadow host without exposing internal queries`,
- (fixture) => {
- const { document, root, get } = createStyleFixture(fixture, {
- markup: '',
- css: '#host {color:rgb(1,2,3)} #host:focus {color:rgb(4,5,6)}',
- });
- const host = root.querySelector('#host')!;
- const shadow = host.attachShadow({ mode });
- const target = document.createElement('input');
- shadow.append(target);
- target.focus();
- expect(target.matches(':focus:focus-within')).toBe(true);
- expect(host.matches(':focus:focus-within')).toBe(true);
- expect(root.matches(':focus-within')).toBe(true);
- expect(document.querySelector(':focus')).toBe(host);
- expect(shadow.querySelector(':focus')).toBe(target);
- expect(get(host).color).toBe('rgb(4, 5, 6)');
- target.blur();
- expect(host.matches(':focus, :focus-within')).toBe(false);
- expect(get(host).color).toBe('rgb(1, 2, 3)');
- },
- ['shadow.focus.retargeting'],
- );
- }
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/inline.ts b/packages/web-platform-tests/src/compatibility/style/inline.ts
deleted file mode 100644
index bb13adb..0000000
--- a/packages/web-platform-tests/src/compatibility/style/inline.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://drafts.csswg.org/cssom/#the-cssstyledeclaration-interface';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('starts empty and keeps declaration identity', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('div'),
- s = e.style;
- expect(s.length).toBe(0);
- expect(s.cssText).toBe('');
- expect(s.item(0)).toBe('');
- expect(s.getPropertyValue('color')).toBe('');
- expect(s.getPropertyPriority('color')).toBe('');
- s.color = 'red';
- expect(e.style).toBe(s);
- expect(s.getPropertyValue('color')).toBe('red');
- });
- context.test('sets, replaces and removes properties with priorities', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.createElement('div').style;
- s.setProperty('color', 'red', 'important');
- s.setProperty('display', 'block');
- expect(s.length).toBe(2);
- expect(s.item(0)).toBe('color');
- expect(s.getPropertyPriority('color')).toBe('important');
- s.setProperty('color', 'blue');
- expect(s.item(0)).toBe('color');
- expect(s.getPropertyPriority('color')).toBe('');
- expect(s.removeProperty('color')).toBe('blue');
- expect(s.item(0)).toBe('display');
- expect(s.removeProperty('missing')).toBe('');
- });
- context.test('ignores invalid declarations and invalid priorities atomically', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.createElement('div').style;
- s.color = 'red';
- s.color = 'not-a-color';
- expect(s.color).toBe('red');
- s.setProperty('color', 'blue', 'urgent');
- expect(s.color).toBe('red');
- s.setProperty('unknown-property', 'x');
- expect(s.length).toBe(1);
- s.setProperty('color', '');
- expect(s.length).toBe(0);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/layers.ts b/packages/web-platform-tests/src/compatibility/style/layers.ts
deleted file mode 100644
index a6161d8..0000000
--- a/packages/web-platform-tests/src/compatibility/style/layers.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts';
-
-export const spec = 'https://drafts.csswg.org/css-cascade-5/#layering';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/css-cascade/layer-basic.html';
-export const globals = ['getComputedStyle'] as const;
-export const requires = ['style.computed'] as const;
-
-// Adapted from layer-basic.html and layer-important.html in WPT at
-// b719612f963a05ece9c89a7dd7598c1c59618874. Copyright © web-platform-tests
-// contributors. Authors: Antti Koivisto, Romain Menke. BSD-3-Clause;
-// see ../../../test/wpt/LICENSE and ../../../docs/wpt-css-forms.md.
-
-/** Registers layout-independent cascade layer ordering cases. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- const cases = [
- [
- 'unlayered normal rules outrank higher-specificity layered rules',
- '.layer-target {color:green} @layer {#layer-target {color:red}}',
- ],
- [
- 'later anonymous layers win independently of specificity',
- '@layer {#layer-target {color:red}} @layer {.layer-target {color:green}}',
- ],
- [
- 'reopening a named layer preserves its original order',
- '@layer a {.layer-target {color:red}} @layer b {.layer-target {color:green}} @layer a {.layer-target {color:red}}',
- ],
- [
- 'statement order controls layers whose blocks appear in reverse order',
- '@layer a, b; @layer b {.layer-target {color:green}} @layer a {.layer-target {color:red}}',
- ],
- [
- 'unlayered declarations inside a layer outrank its nested layers',
- '@layer a {.layer-target {color:green} @layer inner {#layer-target {color:red}}}',
- ],
- [
- 'important declarations reverse layer order and outrank unlayered important rules',
- '@layer a, b; @layer b {#layer-target {color:red!important}} @layer a {.layer-target {color:green!important}} #layer-target {color:red!important}',
- ],
- ] as const;
- for (const [name, css] of cases) {
- context.test(name, (fixture) => {
- const document = fixture.document as unknown as Document;
- const style = document.createElement('style'),
- target = document.createElement('div');
- target.id = 'layer-target';
- target.className = 'layer-target';
- style.textContent = css;
- fixture.connect(style as unknown as DOMElementContract);
- fixture.connect(target as unknown as DOMElementContract);
- const get = fixture.globals('getComputedStyle') as typeof getComputedStyle;
- expect(get(target).color).toBe('rgb(0, 128, 0)');
- });
- }
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/mutation-observation.ts b/packages/web-platform-tests/src/compatibility/style/mutation-observation.ts
deleted file mode 100644
index b658a4f..0000000
--- a/packages/web-platform-tests/src/compatibility/style/mutation-observation.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-
-export const spec = 'https://drafts.csswg.org/cssom/#update-style-attribute';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/cssom/MutationObserver-style.html';
-export const globals = ['MutationObserver'] as const;
-export const requires = [
- 'style.attributeSynchronization',
- 'mutationObserver.records.attributes',
- 'mutationObserver.microtaskDelivery',
- 'mutationObserver.oldValue',
- 'mutationObserver.filtering',
- 'mutationObserver.takeRecords',
- 'mutationObserver.disconnect',
-] as const;
-
-// Adapted and extended from WPT at b719612f963a05ece9c89a7dd7598c1c59618874.
-// Copyright © web-platform-tests contributors. BSD-3-Clause;
-// see ../../../test/wpt/LICENSE and ../../../docs/wpt-css-forms.md.
-
-/** Registers interactions between CSS declaration mutations and DOM observers. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- for (const cssText of [false, true]) {
- context.test(
- `${cssText ? 'repeated cssText assignments deliver' : 'repeated same-value property assignments suppress'} additional observations`,
- async (fixture) => {
- const document = fixture.document as unknown as Document;
- const Observer = fixture.globals('MutationObserver') as unknown as typeof MutationObserver;
- const target = document.createElement('div');
- const records: MutationRecord[] = [];
- const observer = new Observer((batch) => records.push(...batch));
- fixture.cleanup(() => observer.disconnect());
- observer.observe(target, { attributes: true, attributeOldValue: true });
- for (let i = 0; i < 2; i++) {
- if (cssText) target.style.cssText = 'color: red';
- else target.style.color = 'red';
- expect(records.length).toBe(i === 0 ? 0 : 1);
- await Promise.resolve();
- expect(records.length).toBe(cssText ? i + 1 : 1);
- }
- expect(records.map((record) => record.attributeName)).toEqual(
- cssText ? ['style', 'style'] : ['style'],
- );
- expect(records.map((record) => record.oldValue)).toEqual(
- cssText ? [null, 'color: red;'] : [null],
- );
- },
- );
- }
- context.test(
- 'batches declaration changes with ordered old serialized values',
- async (fixture) => {
- const document = fixture.document as unknown as Document;
- const Observer = fixture.globals('MutationObserver') as unknown as typeof MutationObserver;
- const target = document.createElement('div');
- const batches: MutationRecord[][] = [];
- const observer = new Observer((records) => batches.push(records));
- fixture.cleanup(() => observer.disconnect());
- observer.observe(target, {
- attributes: true,
- attributeOldValue: true,
- attributeFilter: ['style'],
- });
- target.style.setProperty('color', 'red');
- target.style.setProperty('color', 'blue', 'important');
- target.style.removeProperty('color');
- expect(batches.length).toBe(0);
- await Promise.resolve();
- expect(batches.length).toBe(1);
- expect(batches[0]!.map((record) => record.oldValue)).toEqual([
- null,
- 'color: red;',
- 'color: blue !important;',
- ]);
- for (const record of batches[0]!) {
- expect(record.target).toBe(target);
- expect(record.type).toBe('attributes');
- expect(record.attributeName).toBe('style');
- expect(record.attributeNamespace).toBeNull();
- }
- },
- );
- context.test(
- 'invalid declarations and removal of missing properties create no records',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const Observer = fixture.globals('MutationObserver') as unknown as typeof MutationObserver;
- const target = document.createElement('div');
- target.style.color = 'red';
- const observer = new Observer(() => {});
- fixture.cleanup(() => observer.disconnect());
- observer.observe(target, { attributes: true });
- target.style.color = 'not-a-color';
- target.style.setProperty('color', 'blue', 'invalid-priority');
- target.style.removeProperty('visibility');
- expect(observer.takeRecords()).toEqual([]);
- expect(target.style.color).toBe('red');
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/parsing.ts b/packages/web-platform-tests/src/compatibility/style/parsing.ts
deleted file mode 100644
index f3e059d..0000000
--- a/packages/web-platform-tests/src/compatibility/style/parsing.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://drafts.csswg.org/cssom/#serialize-a-css-declaration-block';
-export const requires = ['style.inline'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('recovers from invalid values and resolves duplicate priority', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.createElement('div').style;
- s.cssText = 'color:red; invalid: x; color:blue!important; color:green; display: block';
- expect(s.color).toBe('blue');
- expect(s.getPropertyPriority('color')).toBe('important');
- expect(s.display).toBe('block');
- expect(s.length).toBe(2);
- });
- context.test('expands and serializes shorthands', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.createElement('div').style;
- s.margin = '1px 2px';
- expect(s.marginTop).toBe('1px');
- expect(s.marginRight).toBe('2px');
- expect(s.marginBottom).toBe('1px');
- expect(s.marginLeft).toBe('2px');
- expect(s.getPropertyValue('margin')).toBe('1px 2px');
- s.removeProperty('margin');
- expect(s.marginTop).toBe('');
- });
- context.test('handles comments, quoted separators and case insensitive names', (fixture) => {
- const document = fixture.document as unknown as Document;
- const s = document.createElement('div').style;
- s.cssText = 'COLOR: /* comment */ red; font-family: "a;b";';
- expect(s.color).toBe('red');
- expect(s.fontFamily).toBe('"a;b"');
- expect(s.cssText).toContain('color: red;');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/relational-selectors.ts b/packages/web-platform-tests/src/compatibility/style/relational-selectors.ts
deleted file mode 100644
index 6a3be31..0000000
--- a/packages/web-platform-tests/src/compatibility/style/relational-selectors.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-// Adapted from WPT user-action-pseudo-classes-in-has.html at b719612f963a05ece9c89a7dd7598c1c59618874.
-// Copyright web-platform-tests contributors; Byungwoo Lee. BSD-3-Clause.
-// See ../../../test/wpt/LICENSE and ../../../docs/wpt-state-styles.md.
-import type { CompatibilityContext } from '../context.ts';
-import { createStyleFixture } from '../style-fixture.ts';
-export const spec = 'https://drafts.csswg.org/selectors-4/#relational';
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/selectors/invalidation/user-action-pseudo-classes-in-has.html';
-export const requires = [
- 'element.selectors',
- 'parsing.fragments',
- 'style.computed',
- 'style.focusState',
-] as const;
-export const globals = ['getComputedStyle'] as const;
-/** Registers relational matching and invalidation without geometry assertions. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('has focus invalidates an ancestor and a different descendant', (fixture) => {
- const { root, get } = createStyleFixture(fixture, {
- markup: '',
- css: 'section, i {color:rgb(1,2,3)} section:has(:focus) {color:rgb(4,5,6)} section:has(:focus) i {color:rgb(7,8,9)}',
- });
- const section = root.querySelector('section')!,
- button = root.querySelector('button')!,
- other = root.querySelector('i')!;
- expect(section.matches(':has(:focus)')).toBe(false);
- button.focus();
- expect(section.matches(':has(:focus)')).toBe(true);
- expect(get(section).color).toBe('rgb(4, 5, 6)');
- expect(get(other).color).toBe('rgb(7, 8, 9)');
- button.remove();
- expect(section.matches(':has(:focus)')).toBe(false);
- expect(get(other).color).toBe('rgb(1, 2, 3)');
- });
- context.test(
- 'relative child and sibling selectors update after class changes and moves',
- (fixture) => {
- const { root, get } = createStyleFixture(fixture, {
- markup: ' ',
- css: 'section {color:rgb(1,2,3)} section:has(> .marked) {color:rgb(4,5,6)} section:has(+ .marked) {color:rgb(7,8,9)}',
- });
- const section = root.querySelector('section')!,
- child = root.querySelector('i')!,
- sibling = root.querySelector('b')!;
- child.className = 'marked';
- expect(root.querySelector(':has(> .marked)')).toBe(section);
- expect(get(section).color).toBe('rgb(4, 5, 6)');
- child.className = '';
- expect(get(section).color).toBe('rgb(1, 2, 3)');
- sibling.className = 'marked';
- expect(section.matches(':has(+ .marked)')).toBe(true);
- expect(get(section).color).toBe('rgb(7, 8, 9)');
- root.prepend(sibling);
- expect(section.matches(':has(+ .marked)')).toBe(false);
- expect(get(section).color).toBe('rgb(1, 2, 3)');
- },
- );
- context.test(
- 'relational queries return static snapshots while fresh queries reflect changes',
- (fixture) => {
- const { root } = createStyleFixture(fixture, {
- markup: '',
- css: '',
- });
- const sections = root.querySelectorAll('section');
- const selected = root.querySelectorAll('section:has(.marked)');
- sections[1]!.append(sections[0]!.firstChild!);
- expect(Array.from(selected)).toEqual([sections[0]]);
- expect(Array.from(root.querySelectorAll('section:has(.marked)'))).toEqual([sections[1]]);
- },
- );
- context.test(
- 'nested has and empty relative selector lists reject with SyntaxError',
- (fixture) => {
- const { root } = createStyleFixture(fixture, { markup: '', css: '' });
- for (const selector of [':has(:has(i))', ':has()', ':has(>)']) {
- expect(() => root.querySelector(selector)).toThrow(
- expect.objectContaining({ name: 'SyntaxError' }),
- );
- expect(() => root.matches(selector)).toThrow(
- expect.objectContaining({ name: 'SyntaxError' }),
- );
- }
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/style/stylesheets.ts b/packages/web-platform-tests/src/compatibility/style/stylesheets.ts
deleted file mode 100644
index 82e3c7a..0000000
--- a/packages/web-platform-tests/src/compatibility/style/stylesheets.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts';
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://drafts.csswg.org/cssom/#the-cssstylesheet-interface';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('exposes live rule lists and inserts/deletes at explicit indices', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('style');
- e.textContent = 'i { color: red }';
- fixture.connect(e as unknown as DOMElementContract);
- const sheet = e.sheet!,
- rules = sheet.cssRules;
- expect(rules.length).toBe(1);
- expect(sheet.insertRule('b { color: blue }', 1)).toBe(1);
- expect(rules.length).toBe(2);
- expect((rules[1] as CSSStyleRule).selectorText).toBe('b');
- sheet.deleteRule(0);
- expect(rules.length).toBe(1);
- expect((rules[0] as CSSStyleRule).style.color).toBe('blue');
- });
- context.test('rejects invalid rules and out of bounds indices without mutation', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('style');
- fixture.connect(e as unknown as DOMElementContract);
- const s = e.sheet!;
- expect(() => s.insertRule('i {}', 1)).toThrow(
- expect.objectContaining({ name: 'IndexSizeError' }),
- );
- expect(() => s.insertRule('not a rule', 0)).toThrow(
- expect.objectContaining({ name: 'SyntaxError' }),
- );
- expect(() => s.deleteRule(0)).toThrow(expect.objectContaining({ name: 'IndexSizeError' }));
- expect(s.cssRules.length).toBe(0);
- });
- context.test('keeps style rules writable and linked to their parent sheet', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('style');
- e.textContent = 'i {color:red}';
- fixture.connect(e as unknown as DOMElementContract);
- const s = e.sheet!,
- rule = s.cssRules[0] as CSSStyleRule;
- expect(rule.parentStyleSheet).toBe(s);
- rule.style.color = 'blue';
- rule.selectorText = 'b';
- expect(rule.cssText).toContain('b');
- expect(rule.cssText).toContain('blue');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/templates/cloning.ts b/packages/web-platform-tests/src/compatibility/templates/cloning.ts
deleted file mode 100644
index 09f39f6..0000000
--- a/packages/web-platform-tests/src/compatibility/templates/cloning.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://html.spec.whatwg.org/multipage/scripting.html#the-template-element';
-const requiredGlobals = ['HTMLTemplateElement', 'Document'] as const;
-export { requiredGlobals as globals };
-export const requires = ['templates.content', 'node.cloning', 'document.importNode'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('clones and imports template contents independently', ({ document, globals }) => {
- const HTMLTemplateElement = globals('HTMLTemplateElement');
- const Document = globals('Document');
- const template = document.createElement('template');
- template.content.append(document.createTextNode('contents'));
- const clone = template.cloneNode(true);
- if (!(clone instanceof HTMLTemplateElement))
- return expect.fail('Template clone must be a template');
- expect(clone.content.textContent).toBe('contents');
- expect(clone.content.firstChild).not.toBe(template.content.firstChild);
- const other = new Document();
- const imported = other.importNode(template, true);
- if (!(imported instanceof HTMLTemplateElement))
- return expect.fail('Imported template must be a template');
- expect(imported.ownerDocument).toBe(other);
- expect(imported.content.ownerDocument).not.toBe(other);
- expect(imported.content.firstChild?.ownerDocument).toBe(imported.content.ownerDocument);
- });
-
- context.test('deep cloning duplicates template content without sharing nodes', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('template');
- e.innerHTML = 'x ';
- const shallow = e.cloneNode(false) as HTMLTemplateElement,
- deep = e.cloneNode(true) as HTMLTemplateElement;
- expect(shallow.content.childNodes.length).toBe(0);
- expect(deep.content.firstChild).not.toBe(e.content.firstChild);
- expect(deep.innerHTML).toBe('x ');
- deep.content.firstChild!.textContent = 'changed';
- expect(e.content.firstChild!.textContent).toBe('x');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/templates/content.ts b/packages/web-platform-tests/src/compatibility/templates/content.ts
deleted file mode 100644
index 6ef1f32..0000000
--- a/packages/web-platform-tests/src/compatibility/templates/content.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://html.spec.whatwg.org/multipage/scripting.html#the-template-element';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('keeps template content in an inert separate document fragment', ({ document }) => {
- const template = document.createElement('template');
- const child = document.createElement('span');
- template.content.append(child);
- expect(template.childNodes.length).toBe(0);
- expect(template.content.firstChild).toBe(child);
- expect(template.content.nodeType).toBe(11);
- expect(template.content.parentNode).toBeNull();
- expect(child.isConnected).toBe(false);
- expect(child.ownerDocument).toBe(template.content.ownerDocument);
- expect(template.content.ownerDocument).not.toBe(document);
- });
-
- context.test('keeps template contents outside ordinary children and textContent', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('template');
- e.innerHTML = 'x ';
- expect(e.childNodes.length).toBe(0);
- expect(e.textContent).toBe('');
- expect(e.content.firstChild!.textContent).toBe('x');
- expect(e.content.parentNode).toBeNull();
- expect(e.content.ownerDocument).not.toBe(document);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/templates/parsing.ts b/packages/web-platform-tests/src/compatibility/templates/parsing.ts
deleted file mode 100644
index 0842aa2..0000000
--- a/packages/web-platform-tests/src/compatibility/templates/parsing.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://html.spec.whatwg.org/multipage/scripting.html#the-template-element';
-export const requires = ['templates.content', 'parsing.fragments'] as const;
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'parses markup into template content, including nested templates',
- ({ document }) => {
- const template = document.createElement('template');
- template.innerHTML = 'one & two nested ';
- expect(template.childNodes.length).toBe(0);
- expect(template.content.childNodes.length).toBe(2);
- expect(template.content.firstChild?.textContent).toBe('one & two');
- expect(template.innerHTML).toBe('one & two nested ');
- },
- );
-
- context.test('parses nested templates into separate content fragments', (fixture) => {
- const document = fixture.document as unknown as Document;
- const e = document.createElement('template');
- e.innerHTML = 'x ';
- const nested = e.content.firstChild as HTMLTemplateElement;
- expect(nested.childNodes.length).toBe(0);
- expect(nested.content.firstChild!.textContent).toBe('x');
- expect(e.innerHTML).toBe('x ');
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/test-dom-compatibility.ts b/packages/web-platform-tests/src/compatibility/test-dom-compatibility.ts
deleted file mode 100644
index 16e0866..0000000
--- a/packages/web-platform-tests/src/compatibility/test-dom-compatibility.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { domFeaturesSchema, enabled, type DOMFeatures, type DOMFeaturePath } from './features.ts';
-import type {
- CompatibilityContext,
- CompatibilityRunner,
- CompatibilityTestOptions,
- FeatureModule,
-} from './context.ts';
-import { withFixture, type DOMFixtureOptions } from './fixture.ts';
-import { registry } from './registry.ts';
-
-/** Compatibility CompatibilityConfigurationError support. */
-export class CompatibilityConfigurationError extends Error {
- readonly details: { feature?: string; prerequisite?: string; global?: string };
- constructor(
- message: string,
- details: { feature?: string; prerequisite?: string; global?: string } = {},
- ) {
- super(message);
- this.name = 'CompatibilityConfigurationError';
- this.details = details;
- }
-}
-
-export interface DOMCompatibilityOptions extends DOMFixtureOptions {
- runner: CompatibilityRunner;
- features: DOMFeatures;
-}
-
-/** Registers only enabled cases. No skip/todo calls, implicit feature detection, or globals installation. */
-export function testDOMCompatibility(options: DOMCompatibilityOptions): void {
- const validation = domFeaturesSchema['~standard'].validate(options.features);
- if (validation instanceof Promise)
- throw new CompatibilityConfigurationError('Feature validation must be synchronous');
- if (validation.issues) {
- throw new CompatibilityConfigurationError(
- validation.issues.map((issue) => `${issue.path?.join('.')}: ${issue.message}`).join('\n'),
- );
- }
- const features = validation.value;
- const modules: Record = registry;
- const selected = Object.entries(modules).filter(([path]) =>
- enabled(features, path as DOMFeaturePath),
- );
- // Validate prerequisites before registering anything; never silently enable dependencies.
- for (const [path, leaf] of selected) {
- if (leaf.input?.length && !options.input)
- throw new CompatibilityConfigurationError(`${path} requires an input driver`, {
- feature: path,
- });
- for (const prerequisite of leaf.requires ?? []) {
- if (!enabled(features, prerequisite))
- throw new CompatibilityConfigurationError(`${path} requires ${prerequisite}`, {
- feature: path,
- prerequisite,
- });
- }
- }
- const context: CompatibilityContext = {
- runner: options.runner,
- test(name, run, request = {}) {
- const testOptions: CompatibilityTestOptions = Array.isArray(request)
- ? { requires: request }
- : (request as CompatibilityTestOptions);
- if (!(testOptions.requires ?? []).every((path) => enabled(features, path))) return;
- options.runner.it(name, () =>
- withFixture({ ...options, document: testOptions.document }, run),
- );
- },
- };
- function register(prefix: string, entries: typeof selected): void {
- const groups = new Map();
- for (const entry of entries) {
- const part = entry[0].slice(prefix.length).split('.')[0]!;
- const group = groups.get(part) ?? [];
- group.push(entry);
- groups.set(part, group);
- }
- for (const [name, group] of groups) {
- options.runner.describe(name, () => {
- const path = prefix + name;
- const leaf = group.find(([key]) => key === path);
- if (leaf) leaf[1].register(context);
- else register(`${path}.`, group);
- });
- }
- }
- register('', selected);
-}
diff --git a/packages/web-platform-tests/src/compatibility/traversal/node-iterator.ts b/packages/web-platform-tests/src/compatibility/traversal/node-iterator.ts
deleted file mode 100644
index a7218d9..0000000
--- a/packages/web-platform-tests/src/compatibility/traversal/node-iterator.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-export const spec = 'https://dom.spec.whatwg.org/#interface-nodeiterator';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test('iterates inclusive root in order and applies masks', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div');
- p.innerHTML = 'a b ';
- const it = document.createNodeIterator(p, 1);
- expect(it.nextNode()).toBe(p);
- expect(it.nextNode()).toBe(p.firstChild);
- expect(it.nextNode()).toBe(p.lastChild);
- expect(it.nextNode()).toBeNull();
- expect(it.previousNode()).toBe(p.lastChild);
- });
- context.test('skips rejected nodes but traverses their descendants', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div');
- p.innerHTML = ' ';
- const it = document.createNodeIterator(p, 1, (n) => (n.nodeName === 'I' ? 2 : 1));
- expect(it.nextNode()).toBe(p);
- expect(it.nextNode()).toBe(p.querySelector('b'));
- expect(it.nextNode()).toBeNull();
- });
- context.test('updates its reference after removing the current node', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div');
- p.innerHTML = ' ';
- const it = document.createNodeIterator(p, 1);
- it.nextNode();
- const first = it.nextNode()!;
- first.parentNode!.removeChild(first);
- expect(it.nextNode()).toBe(p.firstChild);
- });
-}
diff --git a/packages/web-platform-tests/src/compatibility/traversal/tree-walker.ts b/packages/web-platform-tests/src/compatibility/traversal/tree-walker.ts
deleted file mode 100644
index ef51956..0000000
--- a/packages/web-platform-tests/src/compatibility/traversal/tree-walker.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import type { CompatibilityContext } from '../context.ts';
-
-export const spec = 'https://dom.spec.whatwg.org/#interface-treewalker';
-// Additional filter cases adapted from WPT TreeWalker-acceptNode-filter.html,
-// itself adapted from WebKit LayoutTests/fast/dom/TreeWalker/script-tests/acceptNode-filter.js.
-// Copyright © web-platform-tests contributors. BSD-3-Clause.
-// See ../../../test/wpt/LICENSE and docs/wpt-ranges-parsing.md.
-export const wpt =
- 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/dom/traversal/TreeWalker-acceptNode-filter.html';
-/** Registers behavioral cases with the consumer runner. */
-export function register(context: CompatibilityContext): void {
- const { expect } = context.runner;
- context.test(
- 'walks accepted descendants in order and prunes rejected subtrees',
- ({ document }) => {
- const root = document.createElement('div');
- const a = document.createElement('span');
- const b = document.createElement('b');
- const nested = document.createElement('i');
- a.appendChild(nested);
- root.append(a, b);
- const walker = document.createTreeWalker(root, 1, {
- acceptNode(node) {
- return node === a ? 2 : 1;
- },
- });
- expect(walker.root).toBe(root);
- expect(walker.currentNode).toBe(root);
- expect(walker.nextNode()).toBe(b);
- expect(walker.nextNode()).toBeNull();
- expect(walker.parentNode()).toBe(root);
- },
- );
-
- context.test('distinguishes FILTER_SKIP from FILTER_REJECT subtrees', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div');
- p.innerHTML = ' ';
- const walker = document.createTreeWalker(p, 1, (node) =>
- node.nodeName === 'SECTION' ? 2 : node.nodeName === 'B' ? 3 : 1,
- );
- expect(walker.nextNode()).toBe(p.querySelector('u'));
- expect(walker.nextNode()).toBeNull();
- expect(walker.currentNode).toBe(p.querySelector('u'));
- });
-
- context.test('supports reverse and sibling traversal without moving on null', (fixture) => {
- const document = fixture.document as unknown as Document;
- const p = document.createElement('div');
- p.innerHTML = ' ';
- const walker = document.createTreeWalker(p, 1);
- expect(walker.firstChild()).toBe(p.firstChild);
- expect(walker.nextSibling()).toBe(p.lastChild);
- expect(walker.nextSibling()).toBeNull();
- expect(walker.currentNode).toBe(p.lastChild);
- expect(walker.previousSibling()).toBe(p.firstChild);
- expect(walker.parentNode()).toBe(p);
- });
- context.test('rejects missing and non-callable filter methods without advancing', (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div');
- root.append(document.createElement('i'));
- for (const filter of [{}, { acceptNode: 'invalid' }]) {
- const walker = document.createTreeWalker(root, 1, filter as unknown as NodeFilter);
- for (const traverse of [() => walker.firstChild(), () => walker.nextNode()]) {
- expect(traverse).toThrow(expect.objectContaining({ name: 'TypeError' }));
- expect(walker.currentNode).toBe(root);
- }
- }
- });
- context.test(
- 'propagates filter exceptions by identity and recovers on a later traversal',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div'),
- child = document.createElement('i');
- root.append(child);
- const failure = new Error('filter failure');
- let shouldThrow = true;
- const walker = document.createTreeWalker(root, 1, () => {
- if (shouldThrow) throw failure;
- return 1;
- });
- let caught: unknown;
- try {
- walker.nextNode();
- } catch (error) {
- caught = error;
- }
- expect(caught).toBe(failure);
- expect(walker.currentNode).toBe(root);
- shouldThrow = false;
- expect(walker.nextNode()).toBe(child);
- },
- );
- context.test(
- 'retrieves acceptNode for every invocation with the filter as its receiver',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div'),
- first = document.createElement('i'),
- second = document.createElement('b');
- root.append(first, second);
- let gets = 0;
- const receivers: unknown[] = [],
- nodes: Node[] = [];
- const filter = {
- get acceptNode() {
- gets++;
- return function (this: unknown, node: Node) {
- receivers.push(this);
- nodes.push(node);
- return 1;
- };
- },
- };
- const walker = document.createTreeWalker(root, 1, filter);
- expect(gets).toBe(0);
- expect(walker.nextNode()).toBe(first);
- expect(walker.nextNode()).toBe(second);
- expect(gets).toBe(2);
- expect(receivers).toEqual([filter, filter]);
- expect(nodes).toEqual([first, second]);
- },
- );
- context.test(
- 'propagates getter exceptions without changing currentNode or retaining the active flag',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div'),
- child = document.createElement('i');
- root.append(child);
- const failure = new Error('getter failure');
- let shouldThrow = true;
- const walker = document.createTreeWalker(root, 1, {
- get acceptNode() {
- if (shouldThrow) throw failure;
- return () => 1;
- },
- });
- let caught: unknown;
- try {
- walker.firstChild();
- } catch (error) {
- caught = error;
- }
- expect(caught).toBe(failure);
- expect(walker.currentNode).toBe(root);
- shouldThrow = false;
- expect(walker.firstChild()).toBe(child);
- },
- );
- context.test(
- 'calls a callable filter directly even when it has an acceptNode property',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div'),
- child = document.createElement('i');
- root.append(child);
- let methodCalls = 0;
- const filter = Object.assign(() => 1, {
- acceptNode: () => {
- methodCalls++;
- return 2;
- },
- });
- const walker = document.createTreeWalker(root, 1, filter);
- expect(walker.nextNode()).toBe(child);
- expect(methodCalls).toBe(0);
- },
- );
- context.test(
- 'rejects recursive filtering with InvalidStateError and allows later calls',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div'),
- first = document.createElement('i'),
- second = document.createElement('b');
- root.append(first, second);
- let invocations = 0;
- const walker = document.createTreeWalker(root, 1, () => {
- invocations++;
- expect(() => walker.nextNode()).toThrow(
- expect.objectContaining({ name: 'InvalidStateError' }),
- );
- return 1;
- });
- expect(walker.nextNode()).toBe(first);
- expect(walker.nextNode()).toBe(second);
- expect(invocations).toBe(2);
- },
- );
- context.test(
- 'applies whatToShow before invoking the filter and converts its result to unsigned short',
- (fixture) => {
- const document = fixture.document as unknown as Document;
- const root = document.createElement('div'),
- element = document.createElement('i');
- root.append('text', document.createComment('comment'), element);
- const visited: Node[] = [];
- let conversions = 0;
- const walker = document.createTreeWalker(root, 1, (node) => {
- visited.push(node);
- return {
- valueOf() {
- conversions++;
- return 65537;
- },
- } as unknown as number;
- });
- expect(walker.nextNode()).toBe(element);
- expect(walker.nextNode()).toBeNull();
- expect(visited).toEqual([element]);
- expect(conversions).toBe(1);
- },
- );
-}
diff --git a/packages/web-platform-tests/src/compatibility/unique-name.ts b/packages/web-platform-tests/src/compatibility/unique-name.ts
deleted file mode 100644
index d0a46db..0000000
--- a/packages/web-platform-tests/src/compatibility/unique-name.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-let sequence = 0;
-/** Provides uniqueName for compatibility testing. */
-export function uniqueName(): string {
- return `x-dom-compat-${Date.now()}-${++sequence}`;
-}
diff --git a/packages/web-platform-tests/src/index.ts b/packages/web-platform-tests/src/index.ts
index 708be6b..3379a73 100644
--- a/packages/web-platform-tests/src/index.ts
+++ b/packages/web-platform-tests/src/index.ts
@@ -1,10 +1,12 @@
export {
testDOMCompatibility,
CompatibilityConfigurationError,
-} from './compatibility/test-dom-compatibility.ts';
-export type { DOMCompatibilityOptions } from './compatibility/test-dom-compatibility.ts';
-export { domFeaturesSchema } from './compatibility/features.ts';
-export type { DOMFeatures, DOMFeaturePath } from './compatibility/features.ts';
+} from './shared/test-dom-compatibility.ts';
+export type {
+ DOMCompatibilityOptions,
+ WPTConfig,
+ WPTSelectors,
+} from './shared/test-dom-compatibility.ts';
export type {
CompatibilityRunner,
CompatibilityExpect,
@@ -12,14 +14,13 @@ export type {
CompatibilityContext,
CompatibilityTestOptions,
DOMFixture,
- FeatureModule,
-} from './compatibility/context.ts';
-export type { DOMFixtureOptions, DOMDocumentOptions } from './compatibility/fixture.ts';
-export type { DOMGlobalBindings } from './compatibility/globals.ts';
+ CompatibilityModule,
+} from './shared/context.ts';
+export type { DOMFixtureOptions, DOMDocumentOptions } from './shared/fixture.ts';
+export type { DOMGlobalBindings } from './shared/globals.ts';
export type {
DOMInputDriver,
DOMTestInput,
DOMInputCapability,
DOMMouseButton,
-} from './compatibility/input-driver.ts';
-export type { DOMElement } from './compatibility/contracts/nodes.ts';
+} from './shared/input-driver.ts';
diff --git a/packages/web-platform-tests/src/compatibility/context.ts b/packages/web-platform-tests/src/shared/context.ts
similarity index 68%
rename from packages/web-platform-tests/src/compatibility/context.ts
rename to packages/web-platform-tests/src/shared/context.ts
index ab2932f..f05714f 100644
--- a/packages/web-platform-tests/src/compatibility/context.ts
+++ b/packages/web-platform-tests/src/shared/context.ts
@@ -1,5 +1,3 @@
-import type { DOMFeaturePath } from './features.ts';
-import type { DOMDocument, DOMElement } from './contracts/nodes.ts';
import type { DOMGlobals } from './globals.ts';
import type { DOMInputCapability, DOMTestInput } from './input-driver.ts';
import type { DOMDocumentOptions } from './fixture.ts';
@@ -12,7 +10,6 @@ export interface CompatibilityMatchers {
toEqual(expected: unknown): unknown;
toBeNull(): unknown;
toBeUndefined(): unknown;
- toBeGreaterThan(expected: number): unknown;
toBeInstanceOf(expected: unknown): unknown;
toContain(expected: unknown): unknown;
toHaveLength(expected: number): unknown;
@@ -30,21 +27,26 @@ export interface CompatibilityExpect {
export interface CompatibilityRunner {
describe(name: string, register: () => void): unknown;
- it(name: string, run: () => void | Promise): unknown;
+ it: {
+ (name: string, run: () => void | Promise): unknown;
+ todo(name: string): unknown;
+ };
expect: CompatibilityExpect;
}
-export interface DOMFixture {
- readonly document: DOMDocument;
+export interface DOMFixture extends DOMGlobals {
+ readonly document: Document;
readonly input: DOMTestInput;
- globals(name: K): DOMGlobals[K];
+ /** WPT step_func: retain failures that event dispatch would otherwise swallow. */
+ step(
+ callback: (this: This, ...args: Args) => Result,
+ ): (this: This, ...args: Args) => Result | undefined;
cleanup(callback: () => void | Promise): void;
- connect(element: T): T;
+ connect(element: T): T;
}
-/** Optional prerequisites and document source for an individual case. */
+/** Source document requested by an individual case. */
export interface CompatibilityTestOptions {
- requires?: readonly DOMFeaturePath[];
document?: DOMDocumentOptions;
}
@@ -53,15 +55,11 @@ export interface CompatibilityContext {
test(
name: string,
run: (fixture: DOMFixture) => void | Promise,
- options?: readonly DOMFeaturePath[] | CompatibilityTestOptions,
+ options?: CompatibilityTestOptions,
): void;
}
-export interface FeatureModule {
- readonly spec: string;
- readonly wpt?: string;
- readonly globals?: readonly string[];
- readonly requires?: readonly DOMFeaturePath[];
+export interface CompatibilityModule {
readonly input?: readonly DOMInputCapability[];
register(context: CompatibilityContext): void;
}
diff --git a/packages/web-platform-tests/src/compatibility/fixture.ts b/packages/web-platform-tests/src/shared/fixture.ts
similarity index 80%
rename from packages/web-platform-tests/src/compatibility/fixture.ts
rename to packages/web-platform-tests/src/shared/fixture.ts
index 1270a85..e30422b 100644
--- a/packages/web-platform-tests/src/compatibility/fixture.ts
+++ b/packages/web-platform-tests/src/shared/fixture.ts
@@ -1,6 +1,10 @@
import type { DOMFixture } from './context.ts';
-import { MissingGlobalError, type DOMGlobalBindings, type DOMGlobals } from './globals.ts';
-import type { DOMDocument } from './contracts/nodes.ts';
+import {
+ globalNames,
+ MissingGlobalError,
+ type DOMGlobalBindings,
+ type DOMGlobals,
+} from './globals.ts';
import {
MissingInputError,
testInput,
@@ -26,8 +30,7 @@ export interface DOMFixtureOptions {
dispose?(document: D): void | Promise;
}
-/** Compatibility FixtureFailure support. */
-export class FixtureFailure extends AggregateError {
+class FixtureFailure extends AggregateError {
constructor(errors: unknown[]) {
super(errors, 'Compatibility case and fixture cleanup reported multiple failures');
this.name = 'FixtureFailure';
@@ -48,9 +51,8 @@ export async function withFixture(
const bindings =
typeof options.globals === 'function' ? options.globals(subject) : options.globals;
// Test contracts describe the expected APIs; enabled cases verify their behavior.
- const document = subject as DOMDocument;
- const globals = bindings as DOMGlobals;
- await run({
+ const document = subject as Document;
+ const fixture: Omit = {
document,
get input() {
if (!input) {
@@ -60,9 +62,14 @@ export async function withFixture(
}
return input;
},
- globals(name) {
- if (!(name in globals)) throw new MissingGlobalError(name);
- return globals[name];
+ step(callback) {
+ return function (...args) {
+ try {
+ return callback.apply(this, args);
+ } catch (error) {
+ failures.push(error);
+ }
+ };
},
cleanup(callback) {
cleanups.push(callback);
@@ -73,7 +80,18 @@ export async function withFixture(
cleanups.push(() => element.remove());
return element;
},
- });
+ };
+ for (const name of globalNames) {
+ Object.defineProperty(fixture, name, {
+ enumerable: true,
+ get() {
+ const value = bindings[name];
+ if (value === undefined) throw new MissingGlobalError(name);
+ return value;
+ },
+ });
+ }
+ await run(fixture as DOMFixture);
} catch (error) {
failures.push(error);
} finally {
diff --git a/packages/web-platform-tests/src/shared/globals.ts b/packages/web-platform-tests/src/shared/globals.ts
new file mode 100644
index 0000000..711a548
--- /dev/null
+++ b/packages/web-platform-tests/src/shared/globals.ts
@@ -0,0 +1,127 @@
+/** Implementations supply only the bindings needed by their selected cases. */
+export type DOMGlobalBindings = Record;
+
+/** Global bindings exposed by the fixture, resolved only when a case reads them. */
+export const globalNames = [
+ 'AbortController',
+ 'Array',
+ 'Attr',
+ 'CSSMediaRule',
+ 'CSSStyleSheet',
+ 'CharacterData',
+ 'Comment',
+ 'CustomElementRegistry',
+ 'CustomEvent',
+ 'DOMException',
+ 'DOMParser',
+ 'Date',
+ 'Document',
+ 'DocumentFragment',
+ 'DocumentType',
+ 'Element',
+ 'ErrorEvent',
+ 'Event',
+ 'EventTarget',
+ 'File',
+ 'FocusEvent',
+ 'FormData',
+ 'HTMLAnchorElement',
+ 'HTMLAreaElement',
+ 'HTMLAudioElement',
+ 'HTMLBRElement',
+ 'HTMLBaseElement',
+ 'HTMLBodyElement',
+ 'HTMLButtonElement',
+ 'HTMLCanvasElement',
+ 'HTMLCollection',
+ 'HTMLDListElement',
+ 'HTMLDataElement',
+ 'HTMLDataListElement',
+ 'HTMLDialogElement',
+ 'HTMLDirectoryElement',
+ 'HTMLDivElement',
+ 'HTMLElement',
+ 'HTMLEmbedElement',
+ 'HTMLFieldSetElement',
+ 'HTMLFontElement',
+ 'HTMLFormElement',
+ 'HTMLFrameElement',
+ 'HTMLFrameSetElement',
+ 'HTMLHRElement',
+ 'HTMLHeadElement',
+ 'HTMLHeadingElement',
+ 'HTMLHtmlElement',
+ 'HTMLIFrameElement',
+ 'HTMLImageElement',
+ 'HTMLInputElement',
+ 'HTMLLIElement',
+ 'HTMLLabelElement',
+ 'HTMLLegendElement',
+ 'HTMLLinkElement',
+ 'HTMLMapElement',
+ 'HTMLMetaElement',
+ 'HTMLMeterElement',
+ 'HTMLModElement',
+ 'HTMLOListElement',
+ 'HTMLObjectElement',
+ 'HTMLOptGroupElement',
+ 'HTMLOptionElement',
+ 'HTMLOutputElement',
+ 'HTMLParagraphElement',
+ 'HTMLParamElement',
+ 'HTMLPreElement',
+ 'HTMLProgressElement',
+ 'HTMLQuoteElement',
+ 'HTMLScriptElement',
+ 'HTMLSelectElement',
+ 'HTMLSlotElement',
+ 'HTMLSourceElement',
+ 'HTMLSpanElement',
+ 'HTMLStyleElement',
+ 'HTMLTableCaptionElement',
+ 'HTMLTableCellElement',
+ 'HTMLTableColElement',
+ 'HTMLTableElement',
+ 'HTMLTableRowElement',
+ 'HTMLTableSectionElement',
+ 'HTMLTemplateElement',
+ 'HTMLTextAreaElement',
+ 'HTMLTimeElement',
+ 'HTMLTitleElement',
+ 'HTMLTrackElement',
+ 'HTMLUListElement',
+ 'HTMLUnknownElement',
+ 'HTMLVideoElement',
+ 'InputEvent',
+ 'KeyboardEvent',
+ 'MouseEvent',
+ 'MutationObserver',
+ 'Node',
+ 'NodeFilter',
+ 'NodeList',
+ 'Option',
+ 'PointerEvent',
+ 'ProcessingInstruction',
+ 'Promise',
+ 'Range',
+ 'Selection',
+ 'ShadowRoot',
+ 'SubmitEvent',
+ 'Text',
+ 'ToggleEvent',
+ 'TypeError',
+ 'XMLDocument',
+ 'XMLSerializer',
+ 'customElements',
+ 'getComputedStyle',
+ 'window',
+] as const satisfies readonly (keyof typeof globalThis)[];
+
+export type DOMGlobals = Readonly>;
+
+export class MissingGlobalError extends Error {
+ constructor(name: string) {
+ super(`Missing globals.${name}`);
+ this.name = 'MissingGlobalError';
+ }
+}
diff --git a/packages/web-platform-tests/src/compatibility/input-driver.ts b/packages/web-platform-tests/src/shared/input-driver.ts
similarity index 86%
rename from packages/web-platform-tests/src/compatibility/input-driver.ts
rename to packages/web-platform-tests/src/shared/input-driver.ts
index 3b84f40..5e9f143 100644
--- a/packages/web-platform-tests/src/compatibility/input-driver.ts
+++ b/packages/web-platform-tests/src/shared/input-driver.ts
@@ -1,5 +1,3 @@
-import type { DOMElement } from './contracts/nodes.ts';
-
export type DOMInputCapability = 'keyboard' | 'text' | 'mouse';
export type DOMMouseButton = 0 | 1 | 2;
@@ -14,7 +12,9 @@ export interface DOMInputDriver {
insertText?(text: string): Promise;
mouse?: {
/** Resolve a point using host layout, then relocate there without intermediate steps. */
- move(element: DOMElement): Promise;
+ move(element: Element): Promise;
+ /** Move to coordinates relative to the fixture's viewport. */
+ moveViewport(x: number, y: number): Promise;
down(button: DOMMouseButton): Promise;
up(button: DOMMouseButton): Promise;
};
@@ -28,10 +28,11 @@ export interface DOMTestInput {
keyUp(key: string): Promise;
press(key: string): Promise;
insertText(text: string): Promise;
- move(element: DOMElement): Promise;
+ move(element: Element): Promise;
+ moveViewport(x: number, y: number): Promise;
down(button?: DOMMouseButton): Promise;
up(button?: DOMMouseButton): Promise;
- click(element: DOMElement, button?: DOMMouseButton): Promise;
+ click(element: Element, button?: DOMMouseButton): Promise;
}
/** Compatibility MissingInputError support. */
@@ -71,6 +72,9 @@ export function testInput(driver: DOMInputDriver): DOMTestInput {
async move(element) {
await mouse().move(element);
},
+ async moveViewport(x, y) {
+ await mouse().moveViewport(x, y);
+ },
async down(button = 0) {
await mouse().down(button);
},
diff --git a/packages/web-platform-tests/src/shared/registry.ts b/packages/web-platform-tests/src/shared/registry.ts
new file mode 100644
index 0000000..03d46cc
--- /dev/null
+++ b/packages/web-platform-tests/src/shared/registry.ts
@@ -0,0 +1,814 @@
+// Generated from src/wpt filenames by scripts/generate-registry.mjs.
+import type { CompatibilityModule } from './context.ts';
+import { register as register0 } from '../wpt/css/css-cascade/inherit-initial.html.ts';
+import { register as register1 } from '../wpt/css/css-cascade/layer-basic.html.ts';
+import { register as register2 } from '../wpt/css/css-cascade/layer-important.html.ts';
+import { register as register3 } from '../wpt/css/css-cascade/layer-vs-inline-style.html.ts';
+import { register as register4 } from '../wpt/css/css-cascade/unset-value-storage.html.ts';
+import { register as register5 } from '../wpt/css/css-variables/variable-cycles.html.ts';
+import { register as register6 } from '../wpt/css/css-variables/variable-reference-cssom.html.ts';
+import { register as register7 } from '../wpt/css/css-variables/variable-reference-shorthands-cssom.html.ts';
+import { register as register8 } from '../wpt/css/cssom/CSSStyleDeclaration-indexed-properties.html.ts';
+import { register as register9 } from '../wpt/css/cssom/CSSStyleSheet-constructable-invalidation.html.ts';
+import { register as register10 } from '../wpt/css/cssom/CSSStyleSheet-constructable-replace-cssRules.html.ts';
+import { register as register11 } from '../wpt/css/cssom/CSSStyleSheet-constructable.html.ts';
+import { register as register12 } from '../wpt/css/cssom/CSSStyleSheet.html.ts';
+import { register as register13 } from '../wpt/css/cssom/MutationObserver-style.html.ts';
+import { register as register14 } from '../wpt/css/cssom/computed-style-001.html.ts';
+import { register as register15 } from '../wpt/css/cssom/computed-style-set-property.html.ts';
+import { register as register16 } from '../wpt/css/cssom/css-style-attr-decl-block.html.ts';
+import { register as register17 } from '../wpt/css/cssom/css-style-attribute-modifications.html.ts';
+import { register as register18 } from '../wpt/css/cssom/cssom-setProperty-shorthand.html.ts';
+import {
+ register as register19,
+ input as input19,
+} from '../wpt/css/selectors/focus-visible-001.html.ts';
+import {
+ register as register20,
+ input as input20,
+} from '../wpt/css/selectors/focus-visible-002.html.ts';
+import {
+ register as register21,
+ input as input21,
+} from '../wpt/css/selectors/focus-visible-003.html.ts';
+import { register as register22 } from '../wpt/css/selectors/focus-within-focus-move.html.ts';
+import { register as register23 } from '../wpt/css/selectors/focus-within-removal.html.ts';
+import { register as register24 } from '../wpt/css/selectors/focus-within.html.ts';
+import { register as register25 } from '../wpt/css/selectors/has-basic.html.ts';
+import { register as register26 } from '../wpt/css/selectors/invalidation/input-pseudo-classes-in-has.html.ts';
+import {
+ register as register27,
+ input as input27,
+} from '../wpt/css/selectors/invalidation/user-action-pseudo-classes-in-has.html.ts';
+import { register as register28 } from '../wpt/css/selectors/is-where-basic.html.ts';
+import { register as register29 } from '../wpt/custom-elements/CustomElementRegistry.html.ts';
+import { register as register30 } from '../wpt/custom-elements/HTMLElement-constructor.html.ts';
+import { register as register31 } from '../wpt/custom-elements/adopted-callback.html.ts';
+import { register as register32 } from '../wpt/custom-elements/attribute-changed-callback.html.ts';
+import { register as register33 } from '../wpt/custom-elements/connected-callbacks.html.ts';
+import { register as register34 } from '../wpt/custom-elements/createElement-reentrant-construction.window.js.ts';
+import { register as register35 } from '../wpt/custom-elements/cross-realm-callback-report-exception.html.ts';
+import { register as register36 } from '../wpt/custom-elements/custom-element-reaction-queue.html.ts';
+import { register as register37 } from '../wpt/custom-elements/disconnected-callbacks.html.ts';
+import { register as register38 } from '../wpt/custom-elements/form-associated/ElementInternals-form.html.ts';
+import { register as register39 } from '../wpt/custom-elements/upgrading.html.ts';
+import { register as register40 } from '../wpt/dom/events/AddEventListenerOptions-once.any.js.ts';
+import { register as register41 } from '../wpt/dom/events/AddEventListenerOptions-passive.any.js.ts';
+import { register as register42 } from '../wpt/dom/events/AddEventListenerOptions-signal.any.js.ts';
+import { register as register43 } from '../wpt/dom/events/Event-constructors.any.js.ts';
+import { register as register44 } from '../wpt/dom/events/Event-defaultPrevented-after-dispatch.html.ts';
+import { register as register45 } from '../wpt/dom/events/Event-dispatch-bubbles-false.html.ts';
+import { register as register46 } from '../wpt/dom/events/Event-dispatch-bubbles-true.html.ts';
+import { register as register47 } from '../wpt/dom/events/Event-dispatch-handlers-changed.html.ts';
+import { register as register48 } from '../wpt/dom/events/Event-dispatch-multiple-cancelBubble.html.ts';
+import { register as register49 } from '../wpt/dom/events/Event-dispatch-multiple-stopPropagation.html.ts';
+import { register as register50 } from '../wpt/dom/events/Event-dispatch-propagation-stopped.html.ts';
+import { register as register51 } from '../wpt/dom/events/Event-dispatch-reenter.html.ts';
+import { register as register52 } from '../wpt/dom/events/Event-dispatch-target-moved.html.ts';
+import { register as register53 } from '../wpt/dom/events/Event-dispatch-throwing.html.ts';
+import { register as register54 } from '../wpt/dom/events/Event-stopImmediatePropagation.html.ts';
+import { register as register55 } from '../wpt/dom/events/EventListenerOptions-capture.html.ts';
+import { register as register56 } from '../wpt/dom/events/EventTarget-add-remove-listener.any.js.ts';
+import { register as register57 } from '../wpt/dom/events/EventTarget-addEventListener.any.js.ts';
+import { register as register58 } from '../wpt/dom/events/EventTarget-dispatchEvent-returnvalue.html.ts';
+import { register as register59 } from '../wpt/dom/events/EventTarget-dispatchEvent.html.ts';
+import { register as register60 } from '../wpt/dom/events/EventTarget-removeEventListener.any.js.ts';
+import { register as register61 } from '../wpt/dom/events/EventTarget-this-of-listener.html.ts';
+import { register as register62 } from '../wpt/dom/lists/DOMTokenList-coverage-for-attributes.html.ts';
+import { register as register63 } from '../wpt/dom/nodes/CharacterData-appendData.html.ts';
+import { register as register64 } from '../wpt/dom/nodes/CharacterData-data.html.ts';
+import { register as register65 } from '../wpt/dom/nodes/CharacterData-deleteData.html.ts';
+import { register as register66 } from '../wpt/dom/nodes/CharacterData-insertData.html.ts';
+import { register as register67 } from '../wpt/dom/nodes/CharacterData-replaceData.html.ts';
+import { register as register68 } from '../wpt/dom/nodes/CharacterData-substringData.html.ts';
+import { register as register69 } from '../wpt/dom/nodes/DOMImplementation-createDocument.html.ts';
+import { register as register70 } from '../wpt/dom/nodes/DOMImplementation-createHTMLDocument.html.ts';
+import { register as register71 } from '../wpt/dom/nodes/Document-adoptNode.html.ts';
+import { register as register72 } from '../wpt/dom/nodes/Document-createComment.html.ts';
+import { register as register73 } from '../wpt/dom/nodes/Document-createElement.html.ts';
+import { register as register74 } from '../wpt/dom/nodes/Document-createElementNS.html.ts';
+import { register as register75 } from '../wpt/dom/nodes/Document-createTextNode.html.ts';
+import { register as register76 } from '../wpt/dom/nodes/Document-getElementById.html.ts';
+import { register as register77 } from '../wpt/dom/nodes/Document-importNode.html.ts';
+import { register as register78 } from '../wpt/dom/nodes/DocumentFragment-constructor.html.ts';
+import { register as register79 } from '../wpt/dom/nodes/DocumentFragment-querySelectorAll-after-modification.html.ts';
+import { register as register80 } from '../wpt/dom/nodes/Element-classlist.html.ts';
+import { register as register81 } from '../wpt/dom/nodes/Element-closest-null-selectors.html.ts';
+import { register as register82 } from '../wpt/dom/nodes/Element-closest.html.ts';
+import { register as register83 } from '../wpt/dom/nodes/Element-hasAttributes.html.ts';
+import { register as register84 } from '../wpt/dom/nodes/Element-matches-active-pseudo-class.html.ts';
+import { register as register85 } from '../wpt/dom/nodes/Element-matches-complex-nth-last-of-type.html.ts';
+import { register as register86 } from '../wpt/dom/nodes/Element-matches-disabled-pseudo-class.html.ts';
+import { register as register87 } from '../wpt/dom/nodes/Element-matches-namespaced-elements.html.ts';
+import { register as register88 } from '../wpt/dom/nodes/Element-matches.html.ts';
+import { register as register89 } from '../wpt/dom/nodes/Element-querySelectorAll-context-element.html.ts';
+import { register as register90 } from '../wpt/dom/nodes/Element-setAttribute.html.ts';
+import { register as register91 } from '../wpt/dom/nodes/Element-setAttributeNodeNS.html.ts';
+import { register as register92 } from '../wpt/dom/nodes/MutationObserver-attributes.html.ts';
+import { register as register93 } from '../wpt/dom/nodes/MutationObserver-callback-order.window.js.ts';
+import { register as register94 } from '../wpt/dom/nodes/MutationObserver-callback-tricky.html.ts';
+import { register as register95 } from '../wpt/dom/nodes/MutationObserver-characterData.html.ts';
+import { register as register96 } from '../wpt/dom/nodes/MutationObserver-childList.html.ts';
+import { register as register97 } from '../wpt/dom/nodes/MutationObserver-overlapping-registrations.html.ts';
+import { register as register98 } from '../wpt/dom/nodes/MutationObserver-sanity.html.ts';
+import { register as register99 } from '../wpt/dom/nodes/MutationObserver-takeRecords.html.ts';
+import { register as register100 } from '../wpt/dom/nodes/MutationObserver-transient-registered-observers.window.js.ts';
+import { register as register101 } from '../wpt/dom/nodes/Node-childNodes.html.ts';
+import { register as register102 } from '../wpt/dom/nodes/Node-cloneNode.html.ts';
+import { register as register103 } from '../wpt/dom/nodes/Node-compareDocumentPosition.html.ts';
+import { register as register104 } from '../wpt/dom/nodes/Node-contains.html.ts';
+import { register as register105 } from '../wpt/dom/nodes/Node-insertBefore.html.ts';
+import { register as register106 } from '../wpt/dom/nodes/Node-isConnected-shadow-dom.html.ts';
+import { register as register107 } from '../wpt/dom/nodes/Node-isConnected.html.ts';
+import { register as register108 } from '../wpt/dom/nodes/Node-isEqualNode.html.ts';
+import { register as register109 } from '../wpt/dom/nodes/Node-isSameNode.html.ts';
+import { register as register110 } from '../wpt/dom/nodes/Node-mutation-adoptNode.html.ts';
+import { register as register111 } from '../wpt/dom/nodes/Node-nodeValue.html.ts';
+import { register as register112 } from '../wpt/dom/nodes/Node-normalize.html.ts';
+import { register as register113 } from '../wpt/dom/nodes/Node-parentElement.html.ts';
+import { register as register114 } from '../wpt/dom/nodes/Node-parentNode.html.ts';
+import { register as register115 } from '../wpt/dom/nodes/Node-removeChild.html.ts';
+import { register as register116 } from '../wpt/dom/nodes/Node-replaceChild.html.ts';
+import { register as register117 } from '../wpt/dom/nodes/Node-textContent.html.ts';
+import { register as register118 } from '../wpt/dom/nodes/ParentNode-append.html.ts';
+import { register as register119 } from '../wpt/dom/nodes/ParentNode-children.html.ts';
+import { register as register120 } from '../wpt/dom/nodes/ParentNode-prepend.html.ts';
+import { register as register121 } from '../wpt/dom/nodes/ParentNode-querySelector-All-comma-selector.html.ts';
+import { register as register122 } from '../wpt/dom/nodes/ParentNode-querySelector-All-multiple-not.html.ts';
+import { register as register123 } from '../wpt/dom/nodes/ParentNode-querySelector-All-scope.html.ts';
+import { register as register124 } from '../wpt/dom/nodes/ParentNode-querySelector-All-xht.xht.ts';
+import { register as register125 } from '../wpt/dom/nodes/ParentNode-querySelector-All.html.ts';
+import { register as register126 } from '../wpt/dom/nodes/ParentNode-querySelector-argument-stringification.html.ts';
+import { register as register127 } from '../wpt/dom/nodes/ParentNode-querySelector-attribute-change.html.ts';
+import { register as register128 } from '../wpt/dom/nodes/ParentNode-querySelector-basic-has.html.ts';
+import { register as register129 } from '../wpt/dom/nodes/ParentNode-querySelector-case-insensitive.html.ts';
+import { register as register130 } from '../wpt/dom/nodes/ParentNode-querySelector-class-change.html.ts';
+import { register as register131 } from '../wpt/dom/nodes/ParentNode-querySelector-dupe-id.html.ts';
+import { register as register132 } from '../wpt/dom/nodes/ParentNode-querySelector-empty-attribute-value.html.ts';
+import { register as register133 } from '../wpt/dom/nodes/ParentNode-querySelector-escapes.html.ts';
+import { register as register134 } from '../wpt/dom/nodes/ParentNode-querySelector-host-pseudo-class.html.ts';
+import { register as register135 } from '../wpt/dom/nodes/ParentNode-querySelector-hyphen-prefix.html.ts';
+import { register as register136 } from '../wpt/dom/nodes/ParentNode-querySelector-innerHTML.html.ts';
+import { register as register137 } from '../wpt/dom/nodes/ParentNode-querySelector-invalid-comment-like.html.ts';
+import { register as register138 } from '../wpt/dom/nodes/ParentNode-querySelector-is-nested-pseudo-class.html.ts';
+import { register as register139 } from '../wpt/dom/nodes/ParentNode-querySelector-is-not.html.ts';
+import { register as register140 } from '../wpt/dom/nodes/ParentNode-querySelector-last-child-combinator.html.ts';
+import { register as register141 } from '../wpt/dom/nodes/ParentNode-querySelector-matches.html.ts';
+import { register as register142 } from '../wpt/dom/nodes/ParentNode-querySelector-multiple-attributes-xml.xhtml.ts';
+import { register as register143 } from '../wpt/dom/nodes/ParentNode-querySelector-namespace-tagname-attribute.html.ts';
+import { register as register144 } from '../wpt/dom/nodes/ParentNode-querySelector-nested-nth-child.html.ts';
+import { register as register145 } from '../wpt/dom/nodes/ParentNode-querySelector-nested.html.ts';
+import { register as register146 } from '../wpt/dom/nodes/ParentNode-querySelector-nth-of-type-complex.html.ts';
+import { register as register147 } from '../wpt/dom/nodes/ParentNode-querySelector-orphan-scope.html.ts';
+import { register as register148 } from '../wpt/dom/nodes/ParentNode-querySelector-parent-context.html.ts';
+import { register as register149 } from '../wpt/dom/nodes/ParentNode-querySelector-scope-in-not.html.ts';
+import { register as register150 } from '../wpt/dom/nodes/ParentNode-querySelector-scope-numeric-id.xhtml.ts';
+import { register as register151 } from '../wpt/dom/nodes/ParentNode-querySelector-scope.html.ts';
+import { register as register152 } from '../wpt/dom/nodes/ParentNode-querySelector-shadowroot-nth-child.html.ts';
+import { register as register153 } from '../wpt/dom/nodes/ParentNode-querySelector-svg-not-root.html.ts';
+import { register as register154 } from '../wpt/dom/nodes/ParentNode-querySelector-tag-case.html.ts';
+import { register as register155 } from '../wpt/dom/nodes/ParentNode-querySelector-underscore-local-name.xhtml.ts';
+import { register as register156 } from '../wpt/dom/nodes/ParentNode-querySelector-xml-capitalization.window.js.ts';
+import { register as register157 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-case-insensitive-attribute-flag.html.ts';
+import { register as register158 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-namespace.html.ts';
+import { register as register159 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-nested-not-has.html.ts';
+import { register as register160 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-not-descendant.html.ts';
+import { register as register161 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-removed-elements.html.ts';
+import { register as register162 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-scope-change.html.ts';
+import { register as register163 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-scope-colon-id-class.html.ts';
+import { register as register164 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-svg-append.html.ts';
+import { register as register165 } from '../wpt/dom/nodes/ParentNode-querySelectorAll-svg-class.html.ts';
+import { register as register166 } from '../wpt/dom/nodes/ParentNode-querySelectors-exclusive.html.ts';
+import { register as register167 } from '../wpt/dom/nodes/ParentNode-querySelectors-namespaces.html.ts';
+import { register as register168 } from '../wpt/dom/nodes/ParentNode-querySelectors-space-and-dash-attribute-value.html.ts';
+import { register as register169 } from '../wpt/dom/nodes/Text-splitText.html.ts';
+import { register as register170 } from '../wpt/dom/nodes/append-on-Document.html.ts';
+import { register as register171 } from '../wpt/dom/nodes/attributes.html.ts';
+import { register as register172 } from '../wpt/dom/nodes/querySelector-empty-id.html.ts';
+import { register as register173 } from '../wpt/dom/nodes/querySelector-id-nth-child.html.ts';
+import { register as register174 } from '../wpt/dom/nodes/querySelector-mixed-case.html.ts';
+import { register as register175 } from '../wpt/dom/nodes/svg-template-querySelector.html.ts';
+import { register as register176 } from '../wpt/dom/ranges/Range-attribute-nodes.html.ts';
+import { register as register177 } from '../wpt/dom/ranges/Range-attributes.html.ts';
+import { register as register178 } from '../wpt/dom/ranges/Range-cloneContents-in-ShadowRoot.html.ts';
+import { register as register179 } from '../wpt/dom/ranges/Range-cloneContents.html.ts';
+import { register as register180 } from '../wpt/dom/ranges/Range-comparePoint-2.html.ts';
+import { register as register181 } from '../wpt/dom/ranges/Range-constructor.html.ts';
+import { register as register182 } from '../wpt/dom/ranges/Range-deleteContents-in-ShadowRoot.html.ts';
+import { register as register183 } from '../wpt/dom/ranges/Range-deleteContents.html.ts';
+import { register as register184 } from '../wpt/dom/ranges/Range-extractContents-in-ShadowRoot.html.ts';
+import { register as register185 } from '../wpt/dom/ranges/Range-extractContents.html.ts';
+import { register as register186 } from '../wpt/dom/ranges/Range-insertNode.html.ts';
+import { register as register187 } from '../wpt/dom/ranges/Range-intersectsNode-2.html.ts';
+import { register as register188 } from '../wpt/dom/ranges/Range-mutations-normalize.html.ts';
+import { register as register189 } from '../wpt/dom/ranges/Range-stringifier.html.ts';
+import { register as register190 } from '../wpt/dom/ranges/Range-surroundContents.html.ts';
+import { register as register191 } from '../wpt/dom/traversal/NodeIterator-removal.html.ts';
+import { register as register192 } from '../wpt/dom/traversal/NodeIterator.html.ts';
+import { register as register193 } from '../wpt/dom/traversal/TreeWalker-acceptNode-filter.html.ts';
+import { register as register194 } from '../wpt/dom/traversal/TreeWalker-basic.html.ts';
+import { register as register195 } from '../wpt/dom/traversal/TreeWalker-traversal-reject.html.ts';
+import { register as register196 } from '../wpt/dom/traversal/TreeWalker-traversal-skip.html.ts';
+import { register as register197 } from '../wpt/domparsing/Comment-serialization-double-hyphen.html.ts';
+import { register as register198 } from '../wpt/domparsing/DOMParser-parseFromString-html.html.ts';
+import { register as register199 } from '../wpt/domparsing/DOMParser-parseFromString-xml-parsererror.html.ts';
+import { register as register200 } from '../wpt/domparsing/DOMParser-parseFromString-xml.html.ts';
+import { register as register201 } from '../wpt/domparsing/XMLSerializer-serializeToString.html.ts';
+import { register as register202 } from '../wpt/domparsing/createContextualFragment.html.ts';
+import { register as register203 } from '../wpt/domparsing/innerhtml-04.html.ts';
+import { register as register204 } from '../wpt/domparsing/innerhtml-07.html.ts';
+import { register as register205 } from '../wpt/domparsing/innerhtml-08.html.ts';
+import { register as register206 } from '../wpt/domparsing/innerhtml-li-autoclosing.html.ts';
+import { register as register207 } from '../wpt/domparsing/insert-adjacent-parent-errors.html.ts';
+import { register as register208 } from '../wpt/domparsing/insert-adjacent.html.ts';
+import { register as register209 } from '../wpt/domparsing/outerhtml-01.html.ts';
+import { register as register210 } from '../wpt/domparsing/svg-template.html.ts';
+import { register as register211 } from '../wpt/html/dom/elements/global-attributes/dataset-delete.html.ts';
+import { register as register212 } from '../wpt/html/dom/elements/global-attributes/dataset-get.html.ts';
+import { register as register213 } from '../wpt/html/dom/elements/global-attributes/dataset-set.html.ts';
+import { register as register214 } from '../wpt/html/dom/reflection-misc.html.ts';
+import { register as register215 } from '../wpt/html/editing/focus/focus-management/focus-blur.html.ts';
+import { register as register216 } from '../wpt/html/editing/focus/focus-management/focus-on-all-elements.html.ts';
+import { register as register217 } from '../wpt/html/editing/focus/focus-management/focused-area-removal.html.ts';
+import { register as register218 } from '../wpt/html/editing/focus/focus-management/focusing-disconnected-noop.html.ts';
+import { register as register219 } from '../wpt/html/editing/focus/focus-management/focusing-focused-noop.html.ts';
+import { register as register220 } from '../wpt/html/editing/the-hidden-attribute/hidden-idl.html.ts';
+import { register as register221 } from '../wpt/html/interaction/focus/focus-management/focus-events.html.ts';
+import {
+ register as register222,
+ input as input222,
+} from '../wpt/html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/focus-tabindex-order.html.ts';
+import { register as register223 } from '../wpt/html/interaction/focus/tabindex-focus-flag.html.ts';
+import { register as register224 } from '../wpt/html/semantics/forms/constraints/form-validation-validate.html.ts';
+import {
+ register as register225,
+ input as input225,
+} from '../wpt/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html.ts';
+import { register as register226 } from '../wpt/html/semantics/forms/constraints/radio-valueMissing.html.ts';
+import { register as register227 } from '../wpt/html/semantics/forms/form-control-infrastructure/form_attribute.html.ts';
+import { register as register228 } from '../wpt/html/semantics/forms/form-submission-0/constructing-form-data-set.html.ts';
+import {
+ register as register229,
+ input as input229,
+} from '../wpt/html/semantics/forms/form-submission-0/implicit-submission.optional.html.ts';
+import { register as register230 } from '../wpt/html/semantics/forms/resetting-a-form/reset-form-2.html.ts';
+import { register as register231 } from '../wpt/html/semantics/forms/resetting-a-form/reset-form.html.ts';
+import { register as register232 } from '../wpt/html/semantics/forms/textfieldselection/defaultSelection.html.ts';
+import { register as register233 } from '../wpt/html/semantics/forms/textfieldselection/selection.html.ts';
+import { register as register234 } from '../wpt/html/semantics/forms/textfieldselection/textfieldselection-setRangeText.html.ts';
+import { register as register235 } from '../wpt/html/semantics/forms/the-button-element/button-click-submits.html.ts';
+import { register as register236 } from '../wpt/html/semantics/forms/the-button-element/button-type-enumerated-ascii-case-insensitive.html.ts';
+import { register as register237 } from '../wpt/html/semantics/forms/the-button-element/button-type.html.ts';
+import { register as register238 } from '../wpt/html/semantics/forms/the-datalist-element/datalistoptions.html.ts';
+import { register as register239 } from '../wpt/html/semantics/forms/the-fieldset-element/HTMLFieldSetElement.html.ts';
+import { register as register240 } from '../wpt/html/semantics/forms/the-fieldset-element/disabled-001.html.ts';
+import { register as register241 } from '../wpt/html/semantics/forms/the-form-element/form-elements-filter.html.ts';
+import { register as register242 } from '../wpt/html/semantics/forms/the-form-element/form-elements-form-owner.html.ts';
+import { register as register243 } from '../wpt/html/semantics/forms/the-form-element/form-elements-matches.html.ts';
+import { register as register244 } from '../wpt/html/semantics/forms/the-form-element/form-elements-nameditem-02.html.ts';
+import { register as register245 } from '../wpt/html/semantics/forms/the-form-element/form-elements-sameobject.html.ts';
+import { register as register246 } from '../wpt/html/semantics/forms/the-form-element/form-indexed-element.html.ts';
+import { register as register247 } from '../wpt/html/semantics/forms/the-form-element/form-nameditem.html.ts';
+import { register as register248 } from '../wpt/html/semantics/forms/the-form-element/form-requestsubmit.html.ts';
+import { register as register249 } from '../wpt/html/semantics/forms/the-input-element/checkbox-click-events.html.ts';
+import { register as register250 } from '../wpt/html/semantics/forms/the-input-element/cloning-steps.html.ts';
+import { register as register251 } from '../wpt/html/semantics/forms/the-input-element/color.html.ts';
+import { register as register252 } from '../wpt/html/semantics/forms/the-input-element/email.html.ts';
+import { register as register253 } from '../wpt/html/semantics/forms/the-input-element/input-checkvalidity.html.ts';
+import { register as register254 } from '../wpt/html/semantics/forms/the-input-element/input-setcustomvalidity.html.ts';
+import { register as register255 } from '../wpt/html/semantics/forms/the-input-element/input-validationmessage.html.ts';
+import { register as register256 } from '../wpt/html/semantics/forms/the-input-element/input-validity.html.ts';
+import { register as register257 } from '../wpt/html/semantics/forms/the-input-element/input-value-dirty-flag.html.ts';
+import { register as register258 } from '../wpt/html/semantics/forms/the-input-element/input-valueasdate-stepping.html.ts';
+import { register as register259 } from '../wpt/html/semantics/forms/the-input-element/input-valueasdate.html.ts';
+import { register as register260 } from '../wpt/html/semantics/forms/the-input-element/input-valueasnumber-stepping.html.ts';
+import { register as register261 } from '../wpt/html/semantics/forms/the-input-element/input-valueasnumber.html.ts';
+import { register as register262 } from '../wpt/html/semantics/forms/the-input-element/input-willvalidate.html.ts';
+import { register as register263 } from '../wpt/html/semantics/forms/the-input-element/number.html.ts';
+import { register as register264 } from '../wpt/html/semantics/forms/the-input-element/radio-click-events.html.ts';
+import { register as register265 } from '../wpt/html/semantics/forms/the-input-element/radio-disconnected-group-owner.html.ts';
+import { register as register266 } from '../wpt/html/semantics/forms/the-input-element/text.html.ts';
+import { register as register267 } from '../wpt/html/semantics/forms/the-input-element/url.html.ts';
+import { register as register268 } from '../wpt/html/semantics/forms/the-label-element/labelable-elements.html.ts';
+import { register as register269 } from '../wpt/html/semantics/forms/the-label-element/labels-live-attribute-changes.html.ts';
+import { register as register270 } from '../wpt/html/semantics/forms/the-label-element/proxy-click-to-associated-element.html.ts';
+import { register as register271 } from '../wpt/html/semantics/forms/the-legend-element/legend-form.html.ts';
+import { register as register272 } from '../wpt/html/semantics/forms/the-meter-element/meter.html.ts';
+import { register as register273 } from '../wpt/html/semantics/forms/the-optgroup-element/optgroup-removal.window.js.ts';
+import { register as register274 } from '../wpt/html/semantics/forms/the-option-element/option-label.html.ts';
+import { register as register275 } from '../wpt/html/semantics/forms/the-option-element/option-selected.html.ts';
+import { register as register276 } from '../wpt/html/semantics/forms/the-option-element/option-text-setter.html.ts';
+import { register as register277 } from '../wpt/html/semantics/forms/the-option-element/option-text-spaces.html.ts';
+import { register as register278 } from '../wpt/html/semantics/forms/the-option-element/option-value.html.ts';
+import { register as register279 } from '../wpt/html/semantics/forms/the-output-element/mutations.window.js.ts';
+import { register as register280 } from '../wpt/html/semantics/forms/the-output-element/output.html.ts';
+import { register as register281 } from '../wpt/html/semantics/forms/the-progress-element/progress.html.ts';
+import { register as register282 } from '../wpt/html/semantics/forms/the-select-element/common-HTMLOptionsCollection-namedItem.html.ts';
+import { register as register283 } from '../wpt/html/semantics/forms/the-select-element/common-HTMLOptionsCollection.html.ts';
+import { register as register284 } from '../wpt/html/semantics/forms/the-select-element/customizable-select/selectedcontent.html.ts';
+import { register as register285 } from '../wpt/html/semantics/forms/the-select-element/select-add.html.ts';
+import { register as register286 } from '../wpt/html/semantics/forms/the-select-element/select-named-getter.html.ts';
+import { register as register287 } from '../wpt/html/semantics/forms/the-select-element/select-remove.html.ts';
+import { register as register288 } from '../wpt/html/semantics/forms/the-select-element/select-selectedOptions.html.ts';
+import { register as register289 } from '../wpt/html/semantics/forms/the-select-element/select-validity.html.ts';
+import { register as register290 } from '../wpt/html/semantics/forms/the-select-element/select-value.html.ts';
+import { register as register291 } from '../wpt/html/semantics/forms/the-select-element/selected-index.html.ts';
+import { register as register292 } from '../wpt/html/semantics/forms/the-textarea-element/cloning-steps.html.ts';
+import { register as register293 } from '../wpt/html/semantics/forms/the-textarea-element/textarea-setcustomvalidity.html.ts';
+import { register as register294 } from '../wpt/html/semantics/forms/the-textarea-element/textarea-validity-clone.html.ts';
+import { register as register295 } from '../wpt/html/semantics/forms/the-textarea-element/value-defaultValue-textContent.html.ts';
+import { register as register296 } from '../wpt/html/semantics/interactive-elements/the-details-element/name-attribute.html.ts';
+import { register as register297 } from '../wpt/html/semantics/interactive-elements/the-details-element/toggleEvent.html.ts';
+import { register as register298 } from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-close.html.ts';
+import { register as register299 } from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-focusing-steps-disconnected.html.ts';
+import {
+ register as register300,
+ input as input300,
+} from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-form-submission.html.ts';
+import { register as register301 } from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-open-2.html.ts';
+import { register as register302 } from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-open.html.ts';
+import { register as register303 } from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-requestclose.html.ts';
+import { register as register304 } from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-return-value.html.ts';
+import { register as register305 } from '../wpt/html/semantics/interactive-elements/the-dialog-element/dialog-showModal.html.ts';
+import { register as register306 } from '../wpt/html/semantics/interactive-elements/the-summary-element/activation-behavior.html.ts';
+import { register as register307 } from '../wpt/html/semantics/scripting-1/the-template-element/additions-to-the-steps-to-clone-a-node/template-clone-children.html.ts';
+import { register as register308 } from '../wpt/html/semantics/scripting-1/the-template-element/definitions/template-contents.html.ts';
+import { register as register309 } from '../wpt/html/semantics/scripting-1/the-template-element/innerhtml-on-templates/innerhtml.html.ts';
+import { register as register310 } from '../wpt/html/semantics/selectors/pseudo-classes/checked.html.ts';
+import { register as register311 } from '../wpt/html/semantics/selectors/pseudo-classes/disabled.html.ts';
+import { register as register312 } from '../wpt/html/semantics/selectors/pseudo-classes/valid-invalid.html.ts';
+import { register as register313 } from '../wpt/html/syntax/serializing-html-fragments/processing-instructions.html.ts';
+import { register as register314 } from '../wpt/html/syntax/serializing-html-fragments/serializing.html.ts';
+import { register as register315 } from '../wpt/inert/inert-does-not-match-disabled-selector.html.ts';
+import { register as register316 } from '../wpt/inert/inert-in-shadow-dom.html.ts';
+import { register as register317 } from '../wpt/inert/inert-node-is-unfocusable.html.ts';
+import { register as register318 } from '../wpt/inert/nested-inert-unfocusable.html.ts';
+import {
+ register as register319,
+ input as input319,
+} from '../wpt/input-events/input-events-arrow-key-on-number-input-prevent-default.html.ts';
+import {
+ register as register320,
+ input as input320,
+} from '../wpt/input-events/input-events-arrow-key-on-number-input.html.ts';
+import {
+ register as register321,
+ input as input321,
+} from '../wpt/input-events/input-events-typing.html.ts';
+import { register as register322 } from '../wpt/selection/collapseToStartEnd.html.ts';
+import { register as register323 } from '../wpt/selection/document-selectionchange.html.ts';
+import { register as register324 } from '../wpt/selection/getRangeAt.html.ts';
+import { register as register325 } from '../wpt/selection/move-selection-range-into-different-root.tentative.html.ts';
+import { register as register326 } from '../wpt/selection/removeRange.html.ts';
+import { register as register327 } from '../wpt/selection/selection-range-after-editinghost-removed.html.ts';
+import { register as register328 } from '../wpt/selection/setBaseAndExtent.html.ts';
+import { register as register329 } from '../wpt/shadow-dom/Document-prototype-adoptNode.html.ts';
+import { register as register330 } from '../wpt/shadow-dom/Element-interface-attachShadow.html.ts';
+import { register as register331 } from '../wpt/shadow-dom/Element-interface-shadowRoot-attribute.html.ts';
+import { register as register332 } from '../wpt/shadow-dom/HTMLSlotElement-interface.html.ts';
+import { register as register333 } from '../wpt/shadow-dom/Node-prototype-cloneNode.html.ts';
+import { register as register334 } from '../wpt/shadow-dom/ShadowRoot-interface.html.ts';
+import { register as register335 } from '../wpt/shadow-dom/Slottable-mixin.html.ts';
+import { register as register336 } from '../wpt/shadow-dom/attach-shadow-non-html-namespace.html.ts';
+import { register as register337 } from '../wpt/shadow-dom/declarative/declarative-shadow-dom-basic.html.ts';
+import { register as register338 } from '../wpt/shadow-dom/declarative/gethtml.html.ts';
+import { register as register339 } from '../wpt/shadow-dom/event-composed-path.html.ts';
+import { register as register340 } from '../wpt/shadow-dom/event-inside-shadow-tree.html.ts';
+import { register as register341 } from '../wpt/shadow-dom/event-with-related-target.html.ts';
+import {
+ register as register342,
+ input as input342,
+} from '../wpt/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html.ts';
+import { register as register343 } from '../wpt/shadow-dom/focus/ShadowRoot-delegatesFocus.html.ts';
+import {
+ register as register344,
+ input as input344,
+} from '../wpt/shadow-dom/focus/click-focus-delegatesFocus-click.html.ts';
+import { register as register345 } from '../wpt/shadow-dom/focus/focus-method-with-delegatesFocus.html.ts';
+import { register as register346 } from '../wpt/shadow-dom/imperative-slot-api.html.ts';
+import { register as register347 } from '../wpt/shadow-dom/shadow-root-clonable.html.ts';
+import { register as register348 } from '../wpt/shadow-dom/slotchange-event.html.ts';
+import { register as register349 } from '../wpt/shadow-dom/slotchange.html.ts';
+import { register as register350 } from '../wpt/shadow-dom/slots-fallback.html.ts';
+import { register as register351 } from '../wpt/uievents/constructors/event-getmodifierstate.html.ts';
+import { register as register352 } from '../wpt/uievents/constructors/inputevent-constructor.html.ts';
+import { register as register353 } from '../wpt/uievents/focusevents/blur-relatedTarget.html.ts';
+import {
+ register as register354,
+ input as input354,
+} from '../wpt/uievents/interface/keyboard-click-event.html.ts';
+import {
+ register as register355,
+ input as input355,
+} from '../wpt/uievents/keyboard/keyboardevent-composed.html.ts';
+import {
+ register as register356,
+ input as input356,
+} from '../wpt/uievents/keyboard/keydown-input-events.html.ts';
+import {
+ register as register357,
+ input as input357,
+} from '../wpt/uievents/keyboard/modifier-keys-combinations.html.ts';
+import {
+ register as register358,
+ input as input358,
+} from '../wpt/uievents/keyboard/modifier-keys.html.ts';
+import {
+ register as register359,
+ input as input359,
+} from '../wpt/uievents/mouse/attributes.html.ts';
+import {
+ register as register360,
+ input as input360,
+} from '../wpt/uievents/mouse/mouseevent_move_button.html.ts';
+import { register as register361 } from '../wpt/uievents/order-of-events/focus-events/focus-automated-blink-webkit.html.ts';
+import {
+ register as register362,
+ input as input362,
+} from '../wpt/uievents/order-of-events/focus-events/focus-contained.html.ts';
+import {
+ register as register363,
+ input as input363,
+} from '../wpt/uievents/order-of-events/mouse-events/click-cancel.html.ts';
+import {
+ register as register364,
+ input as input364,
+} from '../wpt/uievents/order-of-events/mouse-events/click-on-div.html.ts';
+import {
+ register as register365,
+ input as input365,
+} from '../wpt/uievents/order-of-events/mouse-events/mouseover-out.html.ts';
+import { register as register366 } from '../wpt/xhr/formdata/constructor-formelement.html.ts';
+import { register as register367 } from '../wpt/xhr/formdata/constructor-submitter.html.ts';
+
+// prettier-ignore
+export const registry = {
+ 'css/css-cascade/inherit-initial.html': { register: register0 },
+ 'css/css-cascade/layer-basic.html': { register: register1 },
+ 'css/css-cascade/layer-important.html': { register: register2 },
+ 'css/css-cascade/layer-vs-inline-style.html': { register: register3 },
+ 'css/css-cascade/unset-value-storage.html': { register: register4 },
+ 'css/css-variables/variable-cycles.html': { register: register5 },
+ 'css/css-variables/variable-reference-cssom.html': { register: register6 },
+ 'css/css-variables/variable-reference-shorthands-cssom.html': { register: register7 },
+ 'css/cssom/CSSStyleDeclaration-indexed-properties.html': { register: register8 },
+ 'css/cssom/CSSStyleSheet-constructable-invalidation.html': { register: register9 },
+ 'css/cssom/CSSStyleSheet-constructable-replace-cssRules.html': { register: register10 },
+ 'css/cssom/CSSStyleSheet-constructable.html': { register: register11 },
+ 'css/cssom/CSSStyleSheet.html': { register: register12 },
+ 'css/cssom/MutationObserver-style.html': { register: register13 },
+ 'css/cssom/computed-style-001.html': { register: register14 },
+ 'css/cssom/computed-style-set-property.html': { register: register15 },
+ 'css/cssom/css-style-attr-decl-block.html': { register: register16 },
+ 'css/cssom/css-style-attribute-modifications.html': { register: register17 },
+ 'css/cssom/cssom-setProperty-shorthand.html': { register: register18 },
+ 'css/selectors/focus-visible-001.html': { register: register19, input: input19 },
+ 'css/selectors/focus-visible-002.html': { register: register20, input: input20 },
+ 'css/selectors/focus-visible-003.html': { register: register21, input: input21 },
+ 'css/selectors/focus-within-focus-move.html': { register: register22 },
+ 'css/selectors/focus-within-removal.html': { register: register23 },
+ 'css/selectors/focus-within.html': { register: register24 },
+ 'css/selectors/has-basic.html': { register: register25 },
+ 'css/selectors/invalidation/input-pseudo-classes-in-has.html': { register: register26 },
+ 'css/selectors/invalidation/user-action-pseudo-classes-in-has.html': { register: register27, input: input27 },
+ 'css/selectors/is-where-basic.html': { register: register28 },
+ 'custom-elements/CustomElementRegistry.html': { register: register29 },
+ 'custom-elements/HTMLElement-constructor.html': { register: register30 },
+ 'custom-elements/adopted-callback.html': { register: register31 },
+ 'custom-elements/attribute-changed-callback.html': { register: register32 },
+ 'custom-elements/connected-callbacks.html': { register: register33 },
+ 'custom-elements/createElement-reentrant-construction.window.js': { register: register34 },
+ 'custom-elements/cross-realm-callback-report-exception.html': { register: register35 },
+ 'custom-elements/custom-element-reaction-queue.html': { register: register36 },
+ 'custom-elements/disconnected-callbacks.html': { register: register37 },
+ 'custom-elements/form-associated/ElementInternals-form.html': { register: register38 },
+ 'custom-elements/upgrading.html': { register: register39 },
+ 'dom/events/AddEventListenerOptions-once.any.js': { register: register40 },
+ 'dom/events/AddEventListenerOptions-passive.any.js': { register: register41 },
+ 'dom/events/AddEventListenerOptions-signal.any.js': { register: register42 },
+ 'dom/events/Event-constructors.any.js': { register: register43 },
+ 'dom/events/Event-defaultPrevented-after-dispatch.html': { register: register44 },
+ 'dom/events/Event-dispatch-bubbles-false.html': { register: register45 },
+ 'dom/events/Event-dispatch-bubbles-true.html': { register: register46 },
+ 'dom/events/Event-dispatch-handlers-changed.html': { register: register47 },
+ 'dom/events/Event-dispatch-multiple-cancelBubble.html': { register: register48 },
+ 'dom/events/Event-dispatch-multiple-stopPropagation.html': { register: register49 },
+ 'dom/events/Event-dispatch-propagation-stopped.html': { register: register50 },
+ 'dom/events/Event-dispatch-reenter.html': { register: register51 },
+ 'dom/events/Event-dispatch-target-moved.html': { register: register52 },
+ 'dom/events/Event-dispatch-throwing.html': { register: register53 },
+ 'dom/events/Event-stopImmediatePropagation.html': { register: register54 },
+ 'dom/events/EventListenerOptions-capture.html': { register: register55 },
+ 'dom/events/EventTarget-add-remove-listener.any.js': { register: register56 },
+ 'dom/events/EventTarget-addEventListener.any.js': { register: register57 },
+ 'dom/events/EventTarget-dispatchEvent-returnvalue.html': { register: register58 },
+ 'dom/events/EventTarget-dispatchEvent.html': { register: register59 },
+ 'dom/events/EventTarget-removeEventListener.any.js': { register: register60 },
+ 'dom/events/EventTarget-this-of-listener.html': { register: register61 },
+ 'dom/lists/DOMTokenList-coverage-for-attributes.html': { register: register62 },
+ 'dom/nodes/CharacterData-appendData.html': { register: register63 },
+ 'dom/nodes/CharacterData-data.html': { register: register64 },
+ 'dom/nodes/CharacterData-deleteData.html': { register: register65 },
+ 'dom/nodes/CharacterData-insertData.html': { register: register66 },
+ 'dom/nodes/CharacterData-replaceData.html': { register: register67 },
+ 'dom/nodes/CharacterData-substringData.html': { register: register68 },
+ 'dom/nodes/DOMImplementation-createDocument.html': { register: register69 },
+ 'dom/nodes/DOMImplementation-createHTMLDocument.html': { register: register70 },
+ 'dom/nodes/Document-adoptNode.html': { register: register71 },
+ 'dom/nodes/Document-createComment.html': { register: register72 },
+ 'dom/nodes/Document-createElement.html': { register: register73 },
+ 'dom/nodes/Document-createElementNS.html': { register: register74 },
+ 'dom/nodes/Document-createTextNode.html': { register: register75 },
+ 'dom/nodes/Document-getElementById.html': { register: register76 },
+ 'dom/nodes/Document-importNode.html': { register: register77 },
+ 'dom/nodes/DocumentFragment-constructor.html': { register: register78 },
+ 'dom/nodes/DocumentFragment-querySelectorAll-after-modification.html': { register: register79 },
+ 'dom/nodes/Element-classlist.html': { register: register80 },
+ 'dom/nodes/Element-closest-null-selectors.html': { register: register81 },
+ 'dom/nodes/Element-closest.html': { register: register82 },
+ 'dom/nodes/Element-hasAttributes.html': { register: register83 },
+ 'dom/nodes/Element-matches-active-pseudo-class.html': { register: register84 },
+ 'dom/nodes/Element-matches-complex-nth-last-of-type.html': { register: register85 },
+ 'dom/nodes/Element-matches-disabled-pseudo-class.html': { register: register86 },
+ 'dom/nodes/Element-matches-namespaced-elements.html': { register: register87 },
+ 'dom/nodes/Element-matches.html': { register: register88 },
+ 'dom/nodes/Element-querySelectorAll-context-element.html': { register: register89 },
+ 'dom/nodes/Element-setAttribute.html': { register: register90 },
+ 'dom/nodes/Element-setAttributeNodeNS.html': { register: register91 },
+ 'dom/nodes/MutationObserver-attributes.html': { register: register92 },
+ 'dom/nodes/MutationObserver-callback-order.window.js': { register: register93 },
+ 'dom/nodes/MutationObserver-callback-tricky.html': { register: register94 },
+ 'dom/nodes/MutationObserver-characterData.html': { register: register95 },
+ 'dom/nodes/MutationObserver-childList.html': { register: register96 },
+ 'dom/nodes/MutationObserver-overlapping-registrations.html': { register: register97 },
+ 'dom/nodes/MutationObserver-sanity.html': { register: register98 },
+ 'dom/nodes/MutationObserver-takeRecords.html': { register: register99 },
+ 'dom/nodes/MutationObserver-transient-registered-observers.window.js': { register: register100 },
+ 'dom/nodes/Node-childNodes.html': { register: register101 },
+ 'dom/nodes/Node-cloneNode.html': { register: register102 },
+ 'dom/nodes/Node-compareDocumentPosition.html': { register: register103 },
+ 'dom/nodes/Node-contains.html': { register: register104 },
+ 'dom/nodes/Node-insertBefore.html': { register: register105 },
+ 'dom/nodes/Node-isConnected-shadow-dom.html': { register: register106 },
+ 'dom/nodes/Node-isConnected.html': { register: register107 },
+ 'dom/nodes/Node-isEqualNode.html': { register: register108 },
+ 'dom/nodes/Node-isSameNode.html': { register: register109 },
+ 'dom/nodes/Node-mutation-adoptNode.html': { register: register110 },
+ 'dom/nodes/Node-nodeValue.html': { register: register111 },
+ 'dom/nodes/Node-normalize.html': { register: register112 },
+ 'dom/nodes/Node-parentElement.html': { register: register113 },
+ 'dom/nodes/Node-parentNode.html': { register: register114 },
+ 'dom/nodes/Node-removeChild.html': { register: register115 },
+ 'dom/nodes/Node-replaceChild.html': { register: register116 },
+ 'dom/nodes/Node-textContent.html': { register: register117 },
+ 'dom/nodes/ParentNode-append.html': { register: register118 },
+ 'dom/nodes/ParentNode-children.html': { register: register119 },
+ 'dom/nodes/ParentNode-prepend.html': { register: register120 },
+ 'dom/nodes/ParentNode-querySelector-All-comma-selector.html': { register: register121 },
+ 'dom/nodes/ParentNode-querySelector-All-multiple-not.html': { register: register122 },
+ 'dom/nodes/ParentNode-querySelector-All-scope.html': { register: register123 },
+ 'dom/nodes/ParentNode-querySelector-All-xht.xht': { register: register124 },
+ 'dom/nodes/ParentNode-querySelector-All.html': { register: register125 },
+ 'dom/nodes/ParentNode-querySelector-argument-stringification.html': { register: register126 },
+ 'dom/nodes/ParentNode-querySelector-attribute-change.html': { register: register127 },
+ 'dom/nodes/ParentNode-querySelector-basic-has.html': { register: register128 },
+ 'dom/nodes/ParentNode-querySelector-case-insensitive.html': { register: register129 },
+ 'dom/nodes/ParentNode-querySelector-class-change.html': { register: register130 },
+ 'dom/nodes/ParentNode-querySelector-dupe-id.html': { register: register131 },
+ 'dom/nodes/ParentNode-querySelector-empty-attribute-value.html': { register: register132 },
+ 'dom/nodes/ParentNode-querySelector-escapes.html': { register: register133 },
+ 'dom/nodes/ParentNode-querySelector-host-pseudo-class.html': { register: register134 },
+ 'dom/nodes/ParentNode-querySelector-hyphen-prefix.html': { register: register135 },
+ 'dom/nodes/ParentNode-querySelector-innerHTML.html': { register: register136 },
+ 'dom/nodes/ParentNode-querySelector-invalid-comment-like.html': { register: register137 },
+ 'dom/nodes/ParentNode-querySelector-is-nested-pseudo-class.html': { register: register138 },
+ 'dom/nodes/ParentNode-querySelector-is-not.html': { register: register139 },
+ 'dom/nodes/ParentNode-querySelector-last-child-combinator.html': { register: register140 },
+ 'dom/nodes/ParentNode-querySelector-matches.html': { register: register141 },
+ 'dom/nodes/ParentNode-querySelector-multiple-attributes-xml.xhtml': { register: register142 },
+ 'dom/nodes/ParentNode-querySelector-namespace-tagname-attribute.html': { register: register143 },
+ 'dom/nodes/ParentNode-querySelector-nested-nth-child.html': { register: register144 },
+ 'dom/nodes/ParentNode-querySelector-nested.html': { register: register145 },
+ 'dom/nodes/ParentNode-querySelector-nth-of-type-complex.html': { register: register146 },
+ 'dom/nodes/ParentNode-querySelector-orphan-scope.html': { register: register147 },
+ 'dom/nodes/ParentNode-querySelector-parent-context.html': { register: register148 },
+ 'dom/nodes/ParentNode-querySelector-scope-in-not.html': { register: register149 },
+ 'dom/nodes/ParentNode-querySelector-scope-numeric-id.xhtml': { register: register150 },
+ 'dom/nodes/ParentNode-querySelector-scope.html': { register: register151 },
+ 'dom/nodes/ParentNode-querySelector-shadowroot-nth-child.html': { register: register152 },
+ 'dom/nodes/ParentNode-querySelector-svg-not-root.html': { register: register153 },
+ 'dom/nodes/ParentNode-querySelector-tag-case.html': { register: register154 },
+ 'dom/nodes/ParentNode-querySelector-underscore-local-name.xhtml': { register: register155 },
+ 'dom/nodes/ParentNode-querySelector-xml-capitalization.window.js': { register: register156 },
+ 'dom/nodes/ParentNode-querySelectorAll-case-insensitive-attribute-flag.html': { register: register157 },
+ 'dom/nodes/ParentNode-querySelectorAll-namespace.html': { register: register158 },
+ 'dom/nodes/ParentNode-querySelectorAll-nested-not-has.html': { register: register159 },
+ 'dom/nodes/ParentNode-querySelectorAll-not-descendant.html': { register: register160 },
+ 'dom/nodes/ParentNode-querySelectorAll-removed-elements.html': { register: register161 },
+ 'dom/nodes/ParentNode-querySelectorAll-scope-change.html': { register: register162 },
+ 'dom/nodes/ParentNode-querySelectorAll-scope-colon-id-class.html': { register: register163 },
+ 'dom/nodes/ParentNode-querySelectorAll-svg-append.html': { register: register164 },
+ 'dom/nodes/ParentNode-querySelectorAll-svg-class.html': { register: register165 },
+ 'dom/nodes/ParentNode-querySelectors-exclusive.html': { register: register166 },
+ 'dom/nodes/ParentNode-querySelectors-namespaces.html': { register: register167 },
+ 'dom/nodes/ParentNode-querySelectors-space-and-dash-attribute-value.html': { register: register168 },
+ 'dom/nodes/Text-splitText.html': { register: register169 },
+ 'dom/nodes/append-on-Document.html': { register: register170 },
+ 'dom/nodes/attributes.html': { register: register171 },
+ 'dom/nodes/querySelector-empty-id.html': { register: register172 },
+ 'dom/nodes/querySelector-id-nth-child.html': { register: register173 },
+ 'dom/nodes/querySelector-mixed-case.html': { register: register174 },
+ 'dom/nodes/svg-template-querySelector.html': { register: register175 },
+ 'dom/ranges/Range-attribute-nodes.html': { register: register176 },
+ 'dom/ranges/Range-attributes.html': { register: register177 },
+ 'dom/ranges/Range-cloneContents-in-ShadowRoot.html': { register: register178 },
+ 'dom/ranges/Range-cloneContents.html': { register: register179 },
+ 'dom/ranges/Range-comparePoint-2.html': { register: register180 },
+ 'dom/ranges/Range-constructor.html': { register: register181 },
+ 'dom/ranges/Range-deleteContents-in-ShadowRoot.html': { register: register182 },
+ 'dom/ranges/Range-deleteContents.html': { register: register183 },
+ 'dom/ranges/Range-extractContents-in-ShadowRoot.html': { register: register184 },
+ 'dom/ranges/Range-extractContents.html': { register: register185 },
+ 'dom/ranges/Range-insertNode.html': { register: register186 },
+ 'dom/ranges/Range-intersectsNode-2.html': { register: register187 },
+ 'dom/ranges/Range-mutations-normalize.html': { register: register188 },
+ 'dom/ranges/Range-stringifier.html': { register: register189 },
+ 'dom/ranges/Range-surroundContents.html': { register: register190 },
+ 'dom/traversal/NodeIterator-removal.html': { register: register191 },
+ 'dom/traversal/NodeIterator.html': { register: register192 },
+ 'dom/traversal/TreeWalker-acceptNode-filter.html': { register: register193 },
+ 'dom/traversal/TreeWalker-basic.html': { register: register194 },
+ 'dom/traversal/TreeWalker-traversal-reject.html': { register: register195 },
+ 'dom/traversal/TreeWalker-traversal-skip.html': { register: register196 },
+ 'domparsing/Comment-serialization-double-hyphen.html': { register: register197 },
+ 'domparsing/DOMParser-parseFromString-html.html': { register: register198 },
+ 'domparsing/DOMParser-parseFromString-xml-parsererror.html': { register: register199 },
+ 'domparsing/DOMParser-parseFromString-xml.html': { register: register200 },
+ 'domparsing/XMLSerializer-serializeToString.html': { register: register201 },
+ 'domparsing/createContextualFragment.html': { register: register202 },
+ 'domparsing/innerhtml-04.html': { register: register203 },
+ 'domparsing/innerhtml-07.html': { register: register204 },
+ 'domparsing/innerhtml-08.html': { register: register205 },
+ 'domparsing/innerhtml-li-autoclosing.html': { register: register206 },
+ 'domparsing/insert-adjacent-parent-errors.html': { register: register207 },
+ 'domparsing/insert-adjacent.html': { register: register208 },
+ 'domparsing/outerhtml-01.html': { register: register209 },
+ 'domparsing/svg-template.html': { register: register210 },
+ 'html/dom/elements/global-attributes/dataset-delete.html': { register: register211 },
+ 'html/dom/elements/global-attributes/dataset-get.html': { register: register212 },
+ 'html/dom/elements/global-attributes/dataset-set.html': { register: register213 },
+ 'html/dom/reflection-misc.html': { register: register214 },
+ 'html/editing/focus/focus-management/focus-blur.html': { register: register215 },
+ 'html/editing/focus/focus-management/focus-on-all-elements.html': { register: register216 },
+ 'html/editing/focus/focus-management/focused-area-removal.html': { register: register217 },
+ 'html/editing/focus/focus-management/focusing-disconnected-noop.html': { register: register218 },
+ 'html/editing/focus/focus-management/focusing-focused-noop.html': { register: register219 },
+ 'html/editing/the-hidden-attribute/hidden-idl.html': { register: register220 },
+ 'html/interaction/focus/focus-management/focus-events.html': { register: register221 },
+ 'html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/focus-tabindex-order.html': { register: register222, input: input222 },
+ 'html/interaction/focus/tabindex-focus-flag.html': { register: register223 },
+ 'html/semantics/forms/constraints/form-validation-validate.html': { register: register224 },
+ 'html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html': { register: register225, input: input225 },
+ 'html/semantics/forms/constraints/radio-valueMissing.html': { register: register226 },
+ 'html/semantics/forms/form-control-infrastructure/form_attribute.html': { register: register227 },
+ 'html/semantics/forms/form-submission-0/constructing-form-data-set.html': { register: register228 },
+ 'html/semantics/forms/form-submission-0/implicit-submission.optional.html': { register: register229, input: input229 },
+ 'html/semantics/forms/resetting-a-form/reset-form-2.html': { register: register230 },
+ 'html/semantics/forms/resetting-a-form/reset-form.html': { register: register231 },
+ 'html/semantics/forms/textfieldselection/defaultSelection.html': { register: register232 },
+ 'html/semantics/forms/textfieldselection/selection.html': { register: register233 },
+ 'html/semantics/forms/textfieldselection/textfieldselection-setRangeText.html': { register: register234 },
+ 'html/semantics/forms/the-button-element/button-click-submits.html': { register: register235 },
+ 'html/semantics/forms/the-button-element/button-type-enumerated-ascii-case-insensitive.html': { register: register236 },
+ 'html/semantics/forms/the-button-element/button-type.html': { register: register237 },
+ 'html/semantics/forms/the-datalist-element/datalistoptions.html': { register: register238 },
+ 'html/semantics/forms/the-fieldset-element/HTMLFieldSetElement.html': { register: register239 },
+ 'html/semantics/forms/the-fieldset-element/disabled-001.html': { register: register240 },
+ 'html/semantics/forms/the-form-element/form-elements-filter.html': { register: register241 },
+ 'html/semantics/forms/the-form-element/form-elements-form-owner.html': { register: register242 },
+ 'html/semantics/forms/the-form-element/form-elements-matches.html': { register: register243 },
+ 'html/semantics/forms/the-form-element/form-elements-nameditem-02.html': { register: register244 },
+ 'html/semantics/forms/the-form-element/form-elements-sameobject.html': { register: register245 },
+ 'html/semantics/forms/the-form-element/form-indexed-element.html': { register: register246 },
+ 'html/semantics/forms/the-form-element/form-nameditem.html': { register: register247 },
+ 'html/semantics/forms/the-form-element/form-requestsubmit.html': { register: register248 },
+ 'html/semantics/forms/the-input-element/checkbox-click-events.html': { register: register249 },
+ 'html/semantics/forms/the-input-element/cloning-steps.html': { register: register250 },
+ 'html/semantics/forms/the-input-element/color.html': { register: register251 },
+ 'html/semantics/forms/the-input-element/email.html': { register: register252 },
+ 'html/semantics/forms/the-input-element/input-checkvalidity.html': { register: register253 },
+ 'html/semantics/forms/the-input-element/input-setcustomvalidity.html': { register: register254 },
+ 'html/semantics/forms/the-input-element/input-validationmessage.html': { register: register255 },
+ 'html/semantics/forms/the-input-element/input-validity.html': { register: register256 },
+ 'html/semantics/forms/the-input-element/input-value-dirty-flag.html': { register: register257 },
+ 'html/semantics/forms/the-input-element/input-valueasdate-stepping.html': { register: register258 },
+ 'html/semantics/forms/the-input-element/input-valueasdate.html': { register: register259 },
+ 'html/semantics/forms/the-input-element/input-valueasnumber-stepping.html': { register: register260 },
+ 'html/semantics/forms/the-input-element/input-valueasnumber.html': { register: register261 },
+ 'html/semantics/forms/the-input-element/input-willvalidate.html': { register: register262 },
+ 'html/semantics/forms/the-input-element/number.html': { register: register263 },
+ 'html/semantics/forms/the-input-element/radio-click-events.html': { register: register264 },
+ 'html/semantics/forms/the-input-element/radio-disconnected-group-owner.html': { register: register265 },
+ 'html/semantics/forms/the-input-element/text.html': { register: register266 },
+ 'html/semantics/forms/the-input-element/url.html': { register: register267 },
+ 'html/semantics/forms/the-label-element/labelable-elements.html': { register: register268 },
+ 'html/semantics/forms/the-label-element/labels-live-attribute-changes.html': { register: register269 },
+ 'html/semantics/forms/the-label-element/proxy-click-to-associated-element.html': { register: register270 },
+ 'html/semantics/forms/the-legend-element/legend-form.html': { register: register271 },
+ 'html/semantics/forms/the-meter-element/meter.html': { register: register272 },
+ 'html/semantics/forms/the-optgroup-element/optgroup-removal.window.js': { register: register273 },
+ 'html/semantics/forms/the-option-element/option-label.html': { register: register274 },
+ 'html/semantics/forms/the-option-element/option-selected.html': { register: register275 },
+ 'html/semantics/forms/the-option-element/option-text-setter.html': { register: register276 },
+ 'html/semantics/forms/the-option-element/option-text-spaces.html': { register: register277 },
+ 'html/semantics/forms/the-option-element/option-value.html': { register: register278 },
+ 'html/semantics/forms/the-output-element/mutations.window.js': { register: register279 },
+ 'html/semantics/forms/the-output-element/output.html': { register: register280 },
+ 'html/semantics/forms/the-progress-element/progress.html': { register: register281 },
+ 'html/semantics/forms/the-select-element/common-HTMLOptionsCollection-namedItem.html': { register: register282 },
+ 'html/semantics/forms/the-select-element/common-HTMLOptionsCollection.html': { register: register283 },
+ 'html/semantics/forms/the-select-element/customizable-select/selectedcontent.html': { register: register284 },
+ 'html/semantics/forms/the-select-element/select-add.html': { register: register285 },
+ 'html/semantics/forms/the-select-element/select-named-getter.html': { register: register286 },
+ 'html/semantics/forms/the-select-element/select-remove.html': { register: register287 },
+ 'html/semantics/forms/the-select-element/select-selectedOptions.html': { register: register288 },
+ 'html/semantics/forms/the-select-element/select-validity.html': { register: register289 },
+ 'html/semantics/forms/the-select-element/select-value.html': { register: register290 },
+ 'html/semantics/forms/the-select-element/selected-index.html': { register: register291 },
+ 'html/semantics/forms/the-textarea-element/cloning-steps.html': { register: register292 },
+ 'html/semantics/forms/the-textarea-element/textarea-setcustomvalidity.html': { register: register293 },
+ 'html/semantics/forms/the-textarea-element/textarea-validity-clone.html': { register: register294 },
+ 'html/semantics/forms/the-textarea-element/value-defaultValue-textContent.html': { register: register295 },
+ 'html/semantics/interactive-elements/the-details-element/name-attribute.html': { register: register296 },
+ 'html/semantics/interactive-elements/the-details-element/toggleEvent.html': { register: register297 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-close.html': { register: register298 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-focusing-steps-disconnected.html': { register: register299 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-form-submission.html': { register: register300, input: input300 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-open-2.html': { register: register301 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-open.html': { register: register302 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-requestclose.html': { register: register303 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-return-value.html': { register: register304 },
+ 'html/semantics/interactive-elements/the-dialog-element/dialog-showModal.html': { register: register305 },
+ 'html/semantics/interactive-elements/the-summary-element/activation-behavior.html': { register: register306 },
+ 'html/semantics/scripting-1/the-template-element/additions-to-the-steps-to-clone-a-node/template-clone-children.html': { register: register307 },
+ 'html/semantics/scripting-1/the-template-element/definitions/template-contents.html': { register: register308 },
+ 'html/semantics/scripting-1/the-template-element/innerhtml-on-templates/innerhtml.html': { register: register309 },
+ 'html/semantics/selectors/pseudo-classes/checked.html': { register: register310 },
+ 'html/semantics/selectors/pseudo-classes/disabled.html': { register: register311 },
+ 'html/semantics/selectors/pseudo-classes/valid-invalid.html': { register: register312 },
+ 'html/syntax/serializing-html-fragments/processing-instructions.html': { register: register313 },
+ 'html/syntax/serializing-html-fragments/serializing.html': { register: register314 },
+ 'inert/inert-does-not-match-disabled-selector.html': { register: register315 },
+ 'inert/inert-in-shadow-dom.html': { register: register316 },
+ 'inert/inert-node-is-unfocusable.html': { register: register317 },
+ 'inert/nested-inert-unfocusable.html': { register: register318 },
+ 'input-events/input-events-arrow-key-on-number-input-prevent-default.html': { register: register319, input: input319 },
+ 'input-events/input-events-arrow-key-on-number-input.html': { register: register320, input: input320 },
+ 'input-events/input-events-typing.html': { register: register321, input: input321 },
+ 'selection/collapseToStartEnd.html': { register: register322 },
+ 'selection/document-selectionchange.html': { register: register323 },
+ 'selection/getRangeAt.html': { register: register324 },
+ 'selection/move-selection-range-into-different-root.tentative.html': { register: register325 },
+ 'selection/removeRange.html': { register: register326 },
+ 'selection/selection-range-after-editinghost-removed.html': { register: register327 },
+ 'selection/setBaseAndExtent.html': { register: register328 },
+ 'shadow-dom/Document-prototype-adoptNode.html': { register: register329 },
+ 'shadow-dom/Element-interface-attachShadow.html': { register: register330 },
+ 'shadow-dom/Element-interface-shadowRoot-attribute.html': { register: register331 },
+ 'shadow-dom/HTMLSlotElement-interface.html': { register: register332 },
+ 'shadow-dom/Node-prototype-cloneNode.html': { register: register333 },
+ 'shadow-dom/ShadowRoot-interface.html': { register: register334 },
+ 'shadow-dom/Slottable-mixin.html': { register: register335 },
+ 'shadow-dom/attach-shadow-non-html-namespace.html': { register: register336 },
+ 'shadow-dom/declarative/declarative-shadow-dom-basic.html': { register: register337 },
+ 'shadow-dom/declarative/gethtml.html': { register: register338 },
+ 'shadow-dom/event-composed-path.html': { register: register339 },
+ 'shadow-dom/event-inside-shadow-tree.html': { register: register340 },
+ 'shadow-dom/event-with-related-target.html': { register: register341 },
+ 'shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html': { register: register342, input: input342 },
+ 'shadow-dom/focus/ShadowRoot-delegatesFocus.html': { register: register343 },
+ 'shadow-dom/focus/click-focus-delegatesFocus-click.html': { register: register344, input: input344 },
+ 'shadow-dom/focus/focus-method-with-delegatesFocus.html': { register: register345 },
+ 'shadow-dom/imperative-slot-api.html': { register: register346 },
+ 'shadow-dom/shadow-root-clonable.html': { register: register347 },
+ 'shadow-dom/slotchange-event.html': { register: register348 },
+ 'shadow-dom/slotchange.html': { register: register349 },
+ 'shadow-dom/slots-fallback.html': { register: register350 },
+ 'uievents/constructors/event-getmodifierstate.html': { register: register351 },
+ 'uievents/constructors/inputevent-constructor.html': { register: register352 },
+ 'uievents/focusevents/blur-relatedTarget.html': { register: register353 },
+ 'uievents/interface/keyboard-click-event.html': { register: register354, input: input354 },
+ 'uievents/keyboard/keyboardevent-composed.html': { register: register355, input: input355 },
+ 'uievents/keyboard/keydown-input-events.html': { register: register356, input: input356 },
+ 'uievents/keyboard/modifier-keys-combinations.html': { register: register357, input: input357 },
+ 'uievents/keyboard/modifier-keys.html': { register: register358, input: input358 },
+ 'uievents/mouse/attributes.html': { register: register359, input: input359 },
+ 'uievents/mouse/mouseevent_move_button.html': { register: register360, input: input360 },
+ 'uievents/order-of-events/focus-events/focus-automated-blink-webkit.html': { register: register361 },
+ 'uievents/order-of-events/focus-events/focus-contained.html': { register: register362, input: input362 },
+ 'uievents/order-of-events/mouse-events/click-cancel.html': { register: register363, input: input363 },
+ 'uievents/order-of-events/mouse-events/click-on-div.html': { register: register364, input: input364 },
+ 'uievents/order-of-events/mouse-events/mouseover-out.html': { register: register365, input: input365 },
+ 'xhr/formdata/constructor-formelement.html': { register: register366 },
+ 'xhr/formdata/constructor-submitter.html': { register: register367 },
+} satisfies Record;
diff --git a/packages/web-platform-tests/src/shared/restore-scripts.ts b/packages/web-platform-tests/src/shared/restore-scripts.ts
new file mode 100644
index 0000000..016148d
--- /dev/null
+++ b/packages/web-platform-tests/src/shared/restore-scripts.ts
@@ -0,0 +1,25 @@
+class ScriptFixtureError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'ScriptFixtureError';
+ }
+}
+
+/** Restore script fixture nodes without executing their contents or loading their sources. */
+export function restoreScriptNodes(document: Document, scripts: readonly string[]): void {
+ const placeholders = document.querySelectorAll('script');
+ if (placeholders.length !== scripts.length) {
+ throw new ScriptFixtureError(
+ `Expected ${scripts.length} script placeholders, found ${placeholders.length}`,
+ );
+ }
+ const template = document.createElement('template');
+ for (const [index, markup] of scripts.entries()) {
+ template.innerHTML = markup;
+ const script = template.content.firstChild;
+ if (template.content.childNodes.length !== 1 || script?.nodeName !== 'SCRIPT') {
+ throw new ScriptFixtureError(`Invalid script fixture at index ${index}`);
+ }
+ placeholders[index]!.replaceWith(script);
+ }
+}
diff --git a/packages/web-platform-tests/src/shared/selection.ts b/packages/web-platform-tests/src/shared/selection.ts
new file mode 100644
index 0000000..b01e2e3
--- /dev/null
+++ b/packages/web-platform-tests/src/shared/selection.ts
@@ -0,0 +1,98 @@
+import type { DOMFixtureOptions } from './fixture.ts';
+
+/** Source globs select files; arrays select exact case names or regular expressions within them. */
+export type WPTSelectors = Readonly>;
+
+/** All adapted cases run by default. Explicit includes override exclusions. */
+export interface WPTConfig {
+ /** Bindings for the implementation, optionally resolved for each fresh document. */
+ globals?: DOMFixtureOptions['globals'];
+ include?: WPTSelectors;
+ exclude?: WPTSelectors;
+}
+
+/** Invalid or stale test selection. */
+export class CompatibilityConfigurationError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'CompatibilityConfigurationError';
+ }
+}
+
+interface WPTCase {
+ source: string;
+ name: string;
+}
+
+function sourcePattern(pattern: string): RegExp {
+ let expression = '^';
+ for (let index = 0; index < pattern.length; index++) {
+ const character = pattern[index]!;
+ if (character === '*') {
+ if (pattern[index + 1] === '*') {
+ index++;
+ if (pattern[index + 1] === '/') {
+ index++;
+ expression += '(?:.*/)?';
+ } else expression += '.*';
+ } else expression += '[^/]*';
+ } else expression += character.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
+ }
+ return new RegExp(expression + '$');
+}
+
+function matches(
+ cases: readonly T[],
+ { selectors, kind }: { selectors: WPTSelectors | undefined; kind: 'include' | 'exclude' },
+): Set {
+ const selected = new Set();
+ if (selectors === undefined) return selected;
+ if (!selectors || typeof selectors !== 'object' || Array.isArray(selectors))
+ throw new CompatibilityConfigurationError(
+ `${kind} must map source paths to true or case selectors`,
+ );
+ for (const [source, names] of Object.entries(selectors)) {
+ if (!source || (names !== true && (!Array.isArray(names) || names.length === 0)))
+ throw new CompatibilityConfigurationError(
+ `Invalid ${kind} selector for ${JSON.stringify(source)}`,
+ );
+ const pattern = sourcePattern(source);
+ const sourceCases = cases.filter((test) => pattern.test(test.source));
+ if (sourceCases.length === 0)
+ throw new CompatibilityConfigurationError(
+ `${kind} source ${JSON.stringify(source)} matched no WPT tests`,
+ );
+ if (names === true) {
+ for (const test of sourceCases) selected.add(test);
+ continue;
+ }
+ for (const name of names) {
+ if ((typeof name !== 'string' || name.length === 0) && !(name instanceof RegExp))
+ throw new CompatibilityConfigurationError(`Invalid ${kind} case selector for ${source}`);
+ const expression = name instanceof RegExp ? new RegExp(name.source, name.flags) : undefined;
+ let found = false;
+ for (const test of sourceCases) {
+ if (expression) expression.lastIndex = 0;
+ if (expression ? expression.test(test.name) : test.name === name) {
+ selected.add(test);
+ found = true;
+ }
+ }
+ if (!found)
+ throw new CompatibilityConfigurationError(
+ `${kind} case ${String(name)} in ${source} matched no WPT tests`,
+ );
+ }
+ }
+ return selected;
+}
+
+/** Validate against the whole catalogue before applying either list. */
+export function selectTests(
+ cases: readonly T[],
+ config: Pick = {},
+): T[] {
+ const included = matches(cases, { selectors: config.include, kind: 'include' });
+ const excluded = matches(cases, { selectors: config.exclude, kind: 'exclude' });
+ return cases.filter((test) => included.has(test) || !excluded.has(test));
+}
diff --git a/packages/web-platform-tests/src/shared/selectors.ts b/packages/web-platform-tests/src/shared/selectors.ts
new file mode 100644
index 0000000..e0938e8
--- /dev/null
+++ b/packages/web-platform-tests/src/shared/selectors.ts
@@ -0,0 +1,49 @@
+import type { DOMFixture } from './context.ts';
+import type { Selector } from '../wpt/dom/nodes/selectors.js.ts';
+import { elements, specialElements } from '../wpt/dom/nodes/ParentNode-querySelector-All.js.ts';
+
+export type Root = Document | Element | DocumentFragment;
+export type Kind = 'document' | 'detached' | 'fragment' | 'element' | 'empty';
+export const roots = {
+ document: 'Document',
+ detached: 'Detached Element',
+ fragment: 'Fragment',
+ element: 'In-document Element',
+} as const;
+
+export function prepare({ document }: DOMFixture, kind: Kind): Root {
+ const element = document.getElementById('root')!;
+ specialElements(document, element);
+ if (kind === 'document') return document;
+ if (kind === 'empty') return document.createElement('div');
+ if (kind === 'detached') return element.cloneNode(true) as Element;
+ if (kind === 'fragment') {
+ const fragment = document.createDocumentFragment();
+ fragment.appendChild(element.cloneNode(true));
+ return fragment;
+ }
+ // The upstream connected-element queries must exclude this indistinguishable sibling tree.
+ const clone = element.cloneNode(true);
+ for (const descendant of elements(clone)) descendant.setAttribute('data-clone', '');
+ document.body.appendChild(clone);
+ return element;
+}
+
+export function prepareQueryMutations({ document }: DOMFixture, root: Root): void {
+ const connected = document.getElementById('root')!;
+ // Both connected mutations precede valid queries in the upstream wrapper.
+ document.body.appendChild(document.createElement('div'));
+ connected.appendChild(document.createElement('div'));
+ if (root !== document && root !== connected) root.appendChild(document.createElement('div'));
+}
+
+export function permitted(
+ row: Selector,
+ context: { kind: Kind; xml: boolean; mask: number },
+): boolean {
+ return (
+ Boolean(row.testType & context.mask) &&
+ !row.exclude?.includes(context.kind) &&
+ !row.exclude?.includes(context.xml ? 'xhtml' : 'html')
+ );
+}
diff --git a/packages/web-platform-tests/src/shared/test-dom-compatibility.ts b/packages/web-platform-tests/src/shared/test-dom-compatibility.ts
new file mode 100644
index 0000000..59fd7e1
--- /dev/null
+++ b/packages/web-platform-tests/src/shared/test-dom-compatibility.ts
@@ -0,0 +1,58 @@
+import type { CompatibilityRunner, CompatibilityTestOptions, DOMFixture } from './context.ts';
+import { withFixture, type DOMFixtureOptions } from './fixture.ts';
+import { registry } from './registry.ts';
+import { selectTests, type WPTConfig } from './selection.ts';
+
+export { CompatibilityConfigurationError } from './selection.ts';
+export type { WPTConfig, WPTSelectors } from './selection.ts';
+
+export interface DOMCompatibilityOptions extends Omit, 'globals'> {
+ runner: CompatibilityRunner;
+ /** Omit to run every adapted case. */
+ config?: WPTConfig;
+}
+
+interface RegisteredCase {
+ source: string;
+ name: string;
+ run(fixture: DOMFixture): void | Promise;
+ options: CompatibilityTestOptions;
+}
+
+/** Select upstream files and cases independently; supporting APIs are exercised by each case. */
+export function testDOMCompatibility(options: DOMCompatibilityOptions): void {
+ const cases: RegisteredCase[] = [];
+ for (const [source, leaf] of Object.entries(registry)) {
+ leaf.register({
+ runner: options.runner,
+ test(name, run, request = {}) {
+ cases.push({ source, name, run, options: request });
+ },
+ });
+ }
+ const groups = new Map();
+ for (const test of selectTests(cases, options.config)) {
+ const group = groups.get(test.source) ?? [];
+ group.push(test);
+ groups.set(test.source, group);
+ }
+ if (groups.size === 0) {
+ options.runner.it.todo('implement DOM features and enable their WPT cases');
+ return;
+ }
+ for (const [source, tests] of groups) {
+ options.runner.describe(source, () => {
+ for (const test of tests)
+ options.runner.it(test.name, () =>
+ withFixture(
+ {
+ ...options,
+ globals: options.config?.globals ?? {},
+ document: test.options.document,
+ },
+ test.run,
+ ),
+ );
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-cascade/inherit-initial.html.ts b/packages/web-platform-tests/src/wpt/css/css-cascade/inherit-initial.html.ts
new file mode 100644
index 0000000..4698b66
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-cascade/inherit-initial.html.ts
@@ -0,0 +1,75 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup1 = `
+
+CSS Cascading and Inheritance test: Root element inherits from initial values
+
+
+
+
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ "z-index:inherit on root element should compute to 'auto'.",
+ ({ document, getComputedStyle }) => {
+ expect(getComputedStyle(document.documentElement).zIndex).toBe('auto');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ "position:inherit on root element should compute to 'static'.",
+ ({ document, getComputedStyle }) => {
+ expect(getComputedStyle(document.documentElement).position).toBe('static');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ "overflow:inherit on root element should compute to 'visible'.",
+ ({ document, getComputedStyle }) => {
+ expect(getComputedStyle(document.documentElement).overflow).toBe('visible');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ "background-color:inherit on root element should compute to 'rgba(0, 0, 0, 0)'.",
+ ({ document, getComputedStyle }) => {
+ expect(getComputedStyle(document.documentElement).backgroundColor).toBe('rgba(0, 0, 0, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-cascade/layer-basic.html.ts b/packages/web-platform-tests/src/wpt/css/css-cascade/layer-basic.html.ts
new file mode 100644
index 0000000..b9858cd
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-cascade/layer-basic.html.ts
@@ -0,0 +1,536 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ {
+ const testCases = [
+ {
+ title: 'A1 Anonymous layers',
+ style: `
+ @layer { }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'A2 Anonymous layers',
+ style: `
+ target { color: green; }
+ @layer {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'A3 Anonymous layers',
+ style: `
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'A4 Anonymous layers',
+ style: `
+ @layer {
+ target { color: red; }
+ }
+ @layer {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A5 Anonymous layers',
+ style: `
+ @layer {
+ target { color: green; }
+ @layer {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'A6 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A7 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: red; }
+ }
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A8 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ }
+ target { color: red; }
+ }
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'A9 Anonymous layers',
+ style: `
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ target { color: red; }
+ }
+ @layer {
+ @layer {
+ @layer {
+ target { color: red; }
+ }
+ }
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'B1 Named layers',
+ style: `
+ @layer A {
+ }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'B2 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ target { color: green; }
+ `,
+ },
+ {
+ title: 'B3 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ @layer A {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'B4 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ @layer A {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'B5 Named layers',
+ style: `
+ @layer A {
+ target { color: green; }
+ @layer A {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B6 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer A {
+ @layer A {
+ target { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B7 Named layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B8 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ @layer A {
+ target { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B9 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ @layer A {
+ target.first { color: green; }
+ }
+ }
+ @layer A {
+ @layer A {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'B10 Named layers',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B {
+ @layer A {
+ target.first { color: green; }
+ }
+ }
+ @layer A {
+ @layer B {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'C1 Named layers shorthand',
+ style: `
+ @layer A.A {
+ target { color: red; }
+ }
+ @layer B.A {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'C2 Named layers shorthand',
+ style: `
+ @layer A.A {
+ target { color: red; }
+ }
+ @layer B.A {
+ target.first { color: green; }
+ }
+ @layer A.A {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'C3 Named layers shorthand',
+ style: `
+ @layer A.A {
+ target { color: red; }
+ }
+ @layer B.A {
+ target.first { color: green; }
+ }
+ @layer A.B {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'C4 Named layers shorthand',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B.A {
+ target { color: green; }
+ }
+ @layer A.A
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'C5 Named layers shorthand',
+ style: `
+ @layer A {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer B.A {
+ target { color: green; }
+ }
+ @layer A.B {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'D1 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ target { color: red; }
+ }
+ @layer {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'D2 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ @layer A {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'D3 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ @layer A {
+ @layer {
+ target { color: green; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'D4 Mixed named and anonymous layers',
+ style: `
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ @layer {
+ target { color: green; }
+ }
+ @layer A {
+ @layer {
+ target { color: red; }
+ }
+ }
+ `,
+ },
+ {
+ title: 'D5 Mixed named and anonymous layers',
+ style: `
+ @layer {
+ @layer A {
+ target { color: red; }
+ }
+ }
+ @layer {
+ target { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'E1 Statement syntax',
+ style: `
+ @layer A, B, C;
+ @layer A {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ @layer B {
+ target.first { color: red; }
+ }
+ @layer C {
+ target.first { color: green; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'E2 Statement syntax',
+ style: `
+ @layer A, C, B;
+ @layer A {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ @layer B {
+ target.first { color: green; }
+ }
+ @layer C {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ `,
+ },
+ {
+ title: 'E3 Statement syntax',
+ style: `
+ @layer C, B, A;
+ @layer A {
+ target.first { color: green; }
+ target.second { color: green; }
+ }
+ @layer B {
+ target.first { color: red; }
+ }
+ @layer C {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'E4 Statement syntax',
+ style: `
+ @layer B, A.B, A.A;
+ @layer A {
+ @layer A {
+ target.first { color: green; }
+ }
+ @layer B {
+ target.first { color: red; }
+ target.second { color: green; }
+ }
+ }
+ @layer B {
+ target { color: red; }
+ }
+ `,
+ },
+ {
+ title: 'E5 Statement syntax',
+ style: `
+ @layer A.B, B, A.A;
+ @layer A {
+ @layer A {
+ target.first { color: red; }
+ }
+ @layer B {
+ target.first { color: red; }
+ target.second { color: red; }
+ }
+ }
+ @layer B {
+ target { color: green; }
+ }
+ `,
+ },
+ ];
+ for (const testCase of testCases) {
+ context.test(
+ testCase['title'],
+ ({ document, getComputedStyle }) => {
+ const styleElement = document.createElement('style');
+ styleElement.textContent = testCase['style'];
+ document.head.append(styleElement);
+
+ let targets = document.querySelectorAll('target');
+ for (const target of targets)
+ expect(
+ getComputedStyle(target).color,
+ testCase['title'] + ", target '" + target.classList[0] + "'",
+ ).toBe('rgb(0, 128, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+CSS Cascade Layers: Basic functionality
+
+
+
+
+
+
+
+
+
+
+
+
+
+`,
+ },
+ },
+ );
+ }
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-cascade/layer-important.html.ts b/packages/web-platform-tests/src/wpt/css/css-cascade/layer-important.html.ts
new file mode 100644
index 0000000..d0d07b9
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-cascade/layer-important.html.ts
@@ -0,0 +1,119 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ {
+ const testCases = [
+ {
+ title: 'A1 Unlayered !important style',
+ style: `
+ target { color: green !important; }
+ target { color: red; }
+ `,
+ },
+ {
+ title: 'B1 Same specificity, layered !important first',
+ style: `
+ @layer { target { color: green !important; } }
+ target { color: red; }
+ `,
+ },
+ {
+ title: 'C1 Same specificity, layered !important second',
+ style: `
+ target { color: red; }
+ @layer { target { color: green !important; } }
+ `,
+ },
+ {
+ title: 'D1 Same specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ @layer { target { color: red !important; } }
+ target { color: red !important; }
+ `,
+ },
+ {
+ title: 'D2 Same specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ target { color: red !important; }
+ @layer { target { color: red !important; } }
+ `,
+ },
+ {
+ title: 'D3 Same specificity, all !important',
+ style: `
+ target { color: red !important; }
+ @layer { target { color: green !important; } }
+ @layer { target { color: red !important; } }
+ `,
+ },
+ {
+ title: 'D4 Same specificity, all !important',
+ style: `
+ @layer A, B;
+ @layer B { target { color: red !important; } }
+ @layer A { target { color: green !important; } }
+ target { color: red !important; }
+ `,
+ },
+ {
+ title: 'E1 Different specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ @layer { target { color: red !important; } }
+ target.first { color: red !important; }
+ `,
+ },
+ {
+ title: 'E2 Different specificity, all !important',
+ style: `
+ @layer { target { color: green !important; } }
+ @layer { target.first { color: red !important; } }
+ target { color: red !important; }
+ `,
+ },
+ ];
+ for (const testCase of testCases) {
+ context.test(
+ testCase['title'],
+ ({ document, getComputedStyle }) => {
+ const styleElement = document.createElement('style');
+ styleElement.textContent = testCase['style'];
+ document.head.append(styleElement);
+
+ let targets = document.querySelectorAll('target');
+ for (const target of targets)
+ expect(
+ getComputedStyle(target).color,
+ testCase['title'] + ", target '" + target.classList[0] + "'",
+ ).toBe('rgb(0, 128, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+CSS Cascade Layers: !important
+
+
+
+
+
+
+
+
+
+
+
+
+
+`,
+ },
+ },
+ );
+ }
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-cascade/layer-vs-inline-style.html.ts b/packages/web-platform-tests/src/wpt/css/css-cascade/layer-vs-inline-style.html.ts
new file mode 100644
index 0000000..5bbc96d
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-cascade/layer-vs-inline-style.html.ts
@@ -0,0 +1,73 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ {
+ const testCases = [
+ {
+ title: 'Normal inline style > normal layered style',
+ style: '@layer { #target { background-color: red; }}',
+ inlineStyle: 'background-color: green',
+ },
+ {
+ title: 'Normal inline style < important layered style',
+ style: '@layer { #target { background-color: green !important; }}',
+ inlineStyle: 'background-color: red',
+ },
+ {
+ title: 'Important inline style > normal layered style',
+ style: '@layer { #target { background-color: red; }}',
+ inlineStyle: 'background-color: green !important',
+ },
+ {
+ title: 'Important inline style > important layered style',
+ style: '@layer { #target { background-color: red !important; }}',
+ inlineStyle: 'background-color: green !important',
+ },
+ ];
+ for (const testCase of testCases) {
+ context.test(
+ testCase['title'],
+ ({ document, getComputedStyle }) => {
+ const target = document.getElementById('target')!,
+ reference = document.getElementById('reference')!;
+ var documentStyle = document.createElement('style');
+ documentStyle.appendChild(document.createTextNode(testCase['style']));
+ document.head.appendChild(documentStyle);
+
+ (target as unknown as { style: string }).style = testCase['inlineStyle'];
+
+ expect(getComputedStyle(target).backgroundColor).toBe(
+ getComputedStyle(reference).backgroundColor,
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+
+
+
+
+
+
+
+
+`,
+ },
+ },
+ );
+ }
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-cascade/unset-value-storage.html.ts b/packages/web-platform-tests/src/wpt/css/css-cascade/unset-value-storage.html.ts
new file mode 100644
index 0000000..e77b66d
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-cascade/unset-value-storage.html.ts
@@ -0,0 +1,37 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Storage of "unset" value',
+ ({ document }) => {
+ let properties = ['color', 'border', 'border-left', 'border-color', 'border-right-style'];
+ let style = (document.styleSheets[0]!.cssRules[0] as CSSStyleRule).style;
+ for (let prop of properties) {
+ expect(style.getPropertyValue(prop), `${prop} is expected to be "unset"`).toBe('unset');
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+Storage of "unset" value
+
+
+
+
+
+
+
+\x20\x20
+`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-variables/variable-cycles.html.ts b/packages/web-platform-tests/src/wpt/css/css-variables/variable-cycles.html.ts
new file mode 100644
index 0000000..57ca7eb
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-variables/variable-cycles.html.ts
@@ -0,0 +1,144 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [declarations, expected_invalid, expected_valid, description] of [
+ [['--a:var(--a)'], ['--a'], [], 'Self-cycle'],
+ [['--a:var(--b)', '--b:var(--a)'], ['--a', '--b'], [], 'Simple a/b cycle'],
+ [
+ ['--a:var(--b, cycle)', '--b:var(--c, cycle)', '--c:var(--a, cycle)'],
+ ['--a', '--b', '--c'],
+ [],
+ 'Three-var cycle',
+ ],
+ [
+ [
+ '--x:var(--y, valid)',
+ '--y:var(--a, valid)',
+ '--a:var(--b, cycle)',
+ '--b:var(--c, cycle)',
+ '--c:var(--a, cycle)',
+ ],
+ ['--a', '--b', '--c'],
+ ['--x', '--y'],
+ 'Cycle that starts in the middle of a chain',
+ ],
+ [
+ [
+ '--x:var(--a, valid)',
+ '--a:var(--b, cycle)',
+ '--b:var(--c, cycle)',
+ '--c:var(--a, cycle) var(--y)',
+ '--y:valid',
+ ],
+ ['--a', '--b', '--c'],
+ ['--x', '--y'],
+ 'Cycle with extra edge',
+ ],
+ [
+ [
+ '--x:var(--a, valid)',
+ '--a:var(--b, cycle)',
+ '--b:var(--c, cycle) var(--y)',
+ '--c:var(--a, cycle)',
+ '--y:valid',
+ ],
+ ['--a', '--b', '--c'],
+ ['--x', '--y'],
+ 'Cycle with extra edge (2)',
+ ],
+ [
+ [
+ '--x:var(--a, valid)',
+ '--a:var(--b, cycle)',
+ '--b:var(--c, cycle)',
+ '--c:var(--a, cycle) var(--y)',
+ '--y:var(--z)',
+ '--z:valid',
+ ],
+ ['--a', '--b', '--c'],
+ ['--x', '--y', '--z'],
+ 'Cycle with extra edge (3)',
+ ],
+ [
+ [
+ '--x:var(--a, valid)',
+ '--a:var(--b, cycle)',
+ '--b:var(--c, cycle) var(--a, cycle)',
+ '--c:var(--d, cycle)',
+ '--d:var(--b, cycle)',
+ ],
+ ['--a', '--b', '--c', '--d'],
+ ['--x'],
+ 'Cycle with secondary cycle',
+ ],
+ [
+ [
+ '--x:var(--a, valid)',
+ '--a:var(--b, cycle)',
+ '--b:var(--c, cycle)',
+ '--c:var(--d, cycle) var(--a, cycle)',
+ '--d:var(--b, cycle) var(--y)',
+ '--y:valid',
+ ],
+ ['--a', '--b', '--c', '--d'],
+ ['--x', '--y'],
+ 'Cycle with overlapping secondary cycle',
+ ],
+ [
+ [
+ '--x:var(--a, valid)',
+ '--a:var(--b, cycle) var(--y, valid) var(--c, cycle)',
+ '--b:var(--a, cycle) ',
+ '--c:var(--d, cycle)',
+ '--d:var(--a, cycle)',
+ '--y:valid',
+ ],
+ ['--a', '--b', '--c', '--d'],
+ ['--x', '--y'],
+ 'Cycle with deeper secondary cycle',
+ ],
+ [
+ [
+ '--x:var(--a, valid)',
+ '--a:var(--y, var(--b, cycle))',
+ '--b:var(--y, var(--c, cycle))',
+ '--c:var(--y, var(--a, cycle))',
+ '--y:valid',
+ ],
+ [],
+ ['--a', '--b', '--c', '--x', '--y'],
+ 'Cycle in unused fallback',
+ ],
+ ] as [string[], string[], string[], string][]) {
+ context.test(
+ description,
+ ({ document, getComputedStyle }) => {
+ const main = document.getElementById('main')!;
+
+ let element = document.createElement('div');
+
+ try {
+ declarations.push('--sanity:valid');
+ (element as unknown as { style: string }).style = declarations.join(';');
+ main.append(element);
+ let cs = getComputedStyle(element);
+
+ for (let e of expected_invalid) expect(cs.getPropertyValue(e), `${e}`).toBe('');
+ for (let e of expected_valid) expect(cs.getPropertyValue(e), `${e}`).not.toBe('');
+
+ expect(cs.getPropertyValue('--sanity'), '--sanity').toBe('valid');
+ } finally {
+ element.remove();
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nTest that custom property cycles behave correctly \n\n\n\n \n\n',
+ },
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-variables/variable-reference-cssom.html.ts b/packages/web-platform-tests/src/wpt/css/css-variables/variable-reference-cssom.html.ts
new file mode 100644
index 0000000..2218ce1
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-variables/variable-reference-cssom.html.ts
@@ -0,0 +1,94 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup1 = `
+
+
+ CSS variable references - via CSSOM
+
+
+
+\x20\x20\x20\x20
+
+\x20\x20\x20\x20
+
+\x20\x20\x20\x20
+\x20\x20\x20\x20
+
+
+
+
+
+
+
+
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Verify correct results using CSSOM',
+ ({ document }) => {
+ let target = document.getElementById('target1')!;
+
+ target.style.setProperty('background-color', 'var(--prop)');
+ expect(
+ target.style.backgroundColor,
+ 'background-color property value after calling setProperty',
+ ).toBe('var(--prop)');
+ expect(
+ target.style.getPropertyValue('background-color'),
+ "getPropertyValue('background-color') after calling setProperty",
+ ).toBe('var(--prop)');
+
+ target.style.removeProperty('background-color');
+ expect(
+ target.style.backgroundColor,
+ 'background-color property value after calling removeProperty',
+ ).toBe('');
+ expect(
+ target.style.getPropertyValue('background-color'),
+ "getPropertyValue('background-color') after calling removeProperty",
+ ).toBe('');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ 'Verify correct results with CSSOM overriding markup-set values',
+ ({ document }) => {
+ let target = document.getElementById('target1')!;
+
+ target.style.setProperty('background-color', 'var(--prop)');
+ expect(
+ target.style.backgroundColor,
+ 'background-color property value after calling setProperty',
+ ).toBe('var(--prop)');
+ expect(
+ target.style.getPropertyValue('background-color'),
+ "getPropertyValue('background-color') after calling setProperty",
+ ).toBe('var(--prop)');
+
+ target.style.removeProperty('background-color');
+ expect(
+ target.style.backgroundColor,
+ 'background-color property value after calling removeProperty',
+ ).toBe('');
+ expect(
+ target.style.getPropertyValue('background-color'),
+ "getPropertyValue('background-color') after calling removeProperty",
+ ).toBe('');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/css-variables/variable-reference-shorthands-cssom.html.ts b/packages/web-platform-tests/src/wpt/css/css-variables/variable-reference-shorthands-cssom.html.ts
new file mode 100644
index 0000000..b6ad135
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/css-variables/variable-reference-shorthands-cssom.html.ts
@@ -0,0 +1,54 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'CSS variable references - shorthand properties - via CSSOM',
+ ({ document }) => {
+ let target = document.getElementById('target')!;
+
+ target.style.setProperty('margin', 'var(--prop)');
+ expect(target.style.margin, 'margin property value after calling setProperty').toBe(
+ 'var(--prop)',
+ );
+ expect(
+ target.style.getPropertyValue('margin'),
+ "getPropertyValue('margin') after calling setProperty",
+ ).toBe('var(--prop)');
+
+ target.style.removeProperty('margin');
+ expect(target.style.margin, 'margin property value after calling removeProperty').toBe('');
+ expect(
+ target.style.getPropertyValue('margin'),
+ "getPropertyValue('margin') after calling removeProperty",
+ ).toBe('');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+ CSS variable references - shorthand properties - via CSSOM
+
+
+
+\x20\x20\x20\x20
+\x20\x20\x20\x20
+
+\x20\x20\x20\x20
+\x20\x20\x20\x20
+
+
+
+
+
+
+
+
+
+`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleDeclaration-indexed-properties.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleDeclaration-indexed-properties.html.ts
new file mode 100644
index 0000000..d54286b
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleDeclaration-indexed-properties.html.ts
@@ -0,0 +1,128 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'indexed access returns property names in insertion order',
+ ({ document }) => {
+ const div = document.getElementById('div')!;
+
+ div.style.setProperty('color', 'red');
+ div.style.setProperty('background-color', 'blue');
+
+ expect(div.style[0]).toBe('color');
+ expect(div.style[1]).toBe('background-color');
+ expect(div.style[2]).toBe(undefined);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCSSStyleDeclaration indexed property access \n\n\n\n\n
\n\n',
+ },
+ },
+ );
+ context.test(
+ 'indexed access works after setting cssText',
+ ({ document }) => {
+ const div = document.getElementById('div')!;
+
+ div.style.cssText = 'margin-top: 5px; padding: 10px';
+
+ expect(div.style[0]).toBe('margin-top');
+ expect(div.style[1]).toBe('padding-top');
+ expect(div.style[2]).toBe('padding-right');
+ expect(div.style[3]).toBe('padding-bottom');
+ expect(div.style[4]).toBe('padding-left');
+ expect(div.style[div.style.length]).toBe(undefined);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCSSStyleDeclaration indexed property access \n\n\n\n\n
\n\n',
+ },
+ },
+ );
+ context.test(
+ 'indexed access updates after removeProperty',
+ ({ document }) => {
+ const div = document.getElementById('div')!;
+
+ div.style.cssText = 'color: red; background-color: blue';
+ div.style.removeProperty('color');
+
+ expect(div.style.length).toBe(1);
+ expect(div.style[0]).toBe('background-color');
+ expect(div.style[1]).toBe(undefined);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCSSStyleDeclaration indexed property access \n\n\n\n\n
\n\n',
+ },
+ },
+ );
+ context.test(
+ 'indexed access on empty style returns undefined',
+ ({ document }) => {
+ const div = document.getElementById('div')!;
+
+ div.style.cssText = '';
+
+ expect(div.style.length).toBe(0);
+ expect(div.style[0]).toBe(undefined);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCSSStyleDeclaration indexed property access \n\n\n\n\n
\n\n',
+ },
+ },
+ );
+ context.test(
+ 'in operator works for indexed properties',
+ ({ document }) => {
+ const div = document.getElementById('div')!;
+
+ div.style.cssText = 'color: red';
+
+ expect('1' in div.style).toBe(false);
+ expect('0' in div.style).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCSSStyleDeclaration indexed property access \n\n\n\n\n
\n\n',
+ },
+ },
+ );
+ context.test(
+ 'indexed properties are enumerable',
+ ({ document }) => {
+ const div = document.getElementById('div')!;
+
+ div.style.cssText = 'color: red; background-color: blue';
+
+ const keys = [];
+ for (const key in div.style) {
+ if (key === '0' || key === '1') {
+ keys.push(key);
+ }
+ }
+ expect(keys.length).toBe(2);
+ expect(keys[0]).toBe('0');
+ expect(keys[1]).toBe('1');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCSSStyleDeclaration indexed property access \n\n\n\n\n
\n\n',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable-invalidation.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable-invalidation.html.ts
new file mode 100644
index 0000000..bee8fe2
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable-invalidation.html.ts
@@ -0,0 +1,89 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup2 = `
+
+CSSStyleSheet rule mutation invalidation
+
+
+
+
+Should be green.
+Should be green.
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'mutating constructed CSSStyleSheet applied to root invalidates styles',
+ async ({ document, CSSStyleSheet, getComputedStyle, cleanup }) => {
+ const span1 = document.getElementById('span1')!,
+ span2 = document.getElementById('span2')!;
+
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync('span {color:var(--color, red);}');
+ document.adoptedStyleSheets = [sheet];
+ cleanup(() => {
+ document.adoptedStyleSheets = [];
+ });
+ expect(getComputedStyle(span1).color, 'Sheet should apply').toBe('rgb(255, 0, 0)');
+ (sheet.rules[0] as CSSStyleRule).style.setProperty('--color', 'green');
+ expect(getComputedStyle(span1).color, 'Sheet should invalidate style').toBe('rgb(0, 128, 0)');
+ document.adoptedStyleSheets = [];
+ expect(getComputedStyle(span1).color, 'Removing sheet should apply').toBe('rgb(0, 0, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ context.test(
+ 'mutating constructed CSSStyleSheet applied to shadowdom invalidates styles',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const span1 = document.getElementById('span1')!,
+ span2 = document.getElementById('span2')!;
+
+ span1.attachShadow({ mode: 'open' });
+ span1.shadowRoot!.append(document.createElement('slot'));
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync(':host {color:var(--color, red);}');
+ span1.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(span1).color, 'Sheet should apply').toBe('rgb(255, 0, 0)');
+ (sheet.rules[0] as CSSStyleRule).style.setProperty('--color', 'green');
+ expect(getComputedStyle(span1).color, 'Sheet should invalidate style').toBe('rgb(0, 128, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ context.test(
+ 'mutating dependent constructed CSSStyleSheet applied to shadowdom invalidates styles',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const span1 = document.getElementById('span1')!,
+ span2 = document.getElementById('span2')!;
+
+ span2.attachShadow({ mode: 'open' });
+ span2.shadowRoot!.append(document.createElement('slot'));
+ const sheet1 = new CSSStyleSheet();
+ const sheet2 = new CSSStyleSheet();
+ sheet1.replaceSync(':host {color:var(--color, hotpink);}');
+ sheet2.replaceSync(':host {--color: blue}');
+ const style2 = (sheet2.rules[0] as CSSStyleRule).style;
+ span2.shadowRoot!.adoptedStyleSheets = [sheet1, sheet2];
+ expect(getComputedStyle(span2).color, 'Sheet should apply').toBe('rgb(0, 0, 255)');
+ style2.setProperty('--color', 'green');
+ expect(getComputedStyle(span2).color, 'Sheet should invalidate style').toBe('rgb(0, 128, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable-replace-cssRules.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable-replace-cssRules.html.ts
new file mode 100644
index 0000000..adeddff
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable-replace-cssRules.html.ts
@@ -0,0 +1,53 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const sync of [true, false]) {
+ context.test(
+ `cssRules tests (sync: ${sync})`,
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ let sheet = new CSSStyleSheet();
+ let rules = sheet.cssRules;
+ function replace(text: string) {
+ return sync ? sheet.replaceSync(text) : sheet.replace(text);
+ }
+ function assert_color(color: string) {
+ expect(sheet.cssRules, 'StyleSheet.cssRules is [SameObject]').toBe(rules);
+ expect(rules.length, 'Should have one rule').toBe(1);
+ expect((rules[0] as CSSStyleRule).style.color, 'Should be the right css rule').toBe(
+ color,
+ );
+ }
+ async function replace_and_test(color: string) {
+ await replace(`p { color: ${color} }`);
+ expect(sheet.cssRules, 'StyleSheet.cssRules is [SameObject]').toBe(rules);
+ expect(rules.length, 'Should have one rule').toBe(1);
+ expect((rules[0] as CSSStyleRule).style.color, 'Should be the right css rule').toBe(
+ color,
+ );
+ }
+
+ await replace_and_test('red');
+ await replace_and_test('green');
+ document.adoptedStyleSheets.push(sheet);
+ expect(getComputedStyle(document.querySelector('p')!).color, 'Sheet should apply').toBe(
+ 'rgb(0, 128, 0)',
+ );
+ (document.adoptedStyleSheets.pop as (...args: unknown[]) => CSSStyleSheet | undefined)(
+ sheet,
+ );
+ expect(
+ getComputedStyle(document.querySelector('p')!).color,
+ 'Sheet should stop applying',
+ ).not.toBe('rgb(0, 128, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCSSStyleSheet.replace reflects the right cssRules. \n\n\n\n\n\n\n
\n\n',
+ },
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable.html.ts
new file mode 100644
index 0000000..2c3b296
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet-constructable.html.ts
@@ -0,0 +1,1107 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup1 = `
+CSSStyleSheet constructor and adoptedStyleSheets
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+function prepare(document: Document, CSSStyleSheet: typeof globalThis.CSSStyleSheet) {
+ const greenStyleText = '.green { color: green; }';
+ const redStyleTexts = ['.red { color: red; }', '.red + span + span { color: red; }'];
+ const blueStyleTexts = ['.blue { color: blue; }', '.blue + span + span { color: blue; }'];
+ const whiteStyleText = '* { color: white; }';
+ const yellowStyleText = '.yellow { color: yellow; }';
+
+ const firstDiv = document.querySelector('#firstSection > div')! as HTMLElement;
+ const secondDiv = firstDiv.cloneNode(true) as HTMLElement as HTMLElement;
+ const shadowHost = document.querySelector('#shadowHost')!;
+ const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
+ shadowRoot.appendChild(secondDiv);
+
+ const greenSpan = firstDiv.children[0]!;
+ const redSpan = firstDiv.children[1]!;
+ const blueSpan = firstDiv.children[2]!;
+ const whiteSpan = firstDiv.children[3]!;
+ const yellowSpan = firstDiv.children[4]!;
+ const greenShadowSpan = secondDiv.children[0]!;
+ const redShadowSpan = secondDiv.children[1]!;
+ const blueShadowSpan = secondDiv.children[2]!;
+ const whiteShadowSpan = secondDiv.children[3]!;
+ const yellowShadowSpan = secondDiv.children[4]!;
+
+ const thirdSection = document.getElementById('thirdSection')!;
+ const divNonConstructed = document.getElementById('divNonConstructed')!;
+ const shadowRootNonConstructed = divNonConstructed.attachShadow({ mode: 'open' });
+ const nonConstructedStyle = document.createElement('style');
+ shadowRootNonConstructed.appendChild(nonConstructedStyle);
+ nonConstructedStyle.sheet!.insertRule('.nonConstructed { color: red; }', 0);
+ const nonConstructedStyleSheet = nonConstructedStyle.sheet!;
+
+ function createAllSheetsPromise() {
+ const greenSheet = new CSSStyleSheet();
+ const redSheet = new CSSStyleSheet({ media: 'screen, print' });
+ const blueSheet = new CSSStyleSheet({ disabled: true });
+ const whiteSheet = new CSSStyleSheet({ disabled: true });
+ const yellowSheet = new CSSStyleSheet({ disabled: false });
+
+ const greenPromise = greenSheet.replace(greenStyleText);
+ const redPromise = redSheet.replace(redStyleTexts[0]! + redStyleTexts[1]!);
+ const bluePromise = blueSheet.replace(blueStyleTexts[0]! + blueStyleTexts[1]!);
+ const whitePromise = whiteSheet.replace(whiteStyleText);
+ const yellowPromise = yellowSheet.replace(yellowStyleText);
+ return [greenPromise, redPromise, bluePromise, whitePromise, yellowPromise] as const;
+ }
+
+ function attachShadowDiv(host: HTMLElement) {
+ const shadowRoot = host.attachShadow({ mode: 'open' });
+ const shadowDiv = document.createElement('div');
+ shadowRoot.appendChild(shadowDiv);
+ return shadowDiv;
+ }
+
+ const import_text = '@import url("support/constructable-import.css");';
+ return {
+ greenStyleText,
+ redStyleTexts,
+ blueStyleTexts,
+ whiteStyleText,
+ yellowStyleText,
+ firstDiv,
+ secondDiv,
+ shadowHost,
+ shadowRoot,
+ greenSpan,
+ redSpan,
+ blueSpan,
+ whiteSpan,
+ yellowSpan,
+ greenShadowSpan,
+ redShadowSpan,
+ blueShadowSpan,
+ whiteShadowSpan,
+ yellowShadowSpan,
+ thirdSection,
+ divNonConstructed,
+ nonConstructedStyleSheet,
+ createAllSheetsPromise,
+ attachShadowDiv,
+ import_text,
+ };
+}
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'document.adoptedStyleSheets should initially have length 0.',
+ ({ document }) => {
+ const redStyleTexts = ['.red { color: red; }', '.red + span + span { color: red; }'] as const;
+
+ expect(document.adoptedStyleSheets.length).toBe(0);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ 'new CSSStyleSheet produces empty CSSStyleSheet',
+ ({ CSSStyleSheet }) => {
+ const redStyleTexts = ['.red { color: red; }', '.red + span + span { color: red; }'] as const;
+
+ const sheet = new CSSStyleSheet({ disabled: true, media: 'screen, print' });
+ expect(
+ sheet.title,
+ 'The title attribute must return the title or null if title is the empty string',
+ ).toBe(null);
+ expect(sheet.ownerNode).toBe(null);
+ expect(sheet.ownerRule).toBe(null);
+ expect(sheet.media.length).toBe(2);
+ expect(sheet.media.item(0)).toBe('screen');
+ expect(sheet.media.item(1)).toBe('print');
+ expect(sheet.disabled).toBe(true);
+ expect(sheet.cssRules.length).toBe(0);
+
+ sheet.insertRule(redStyleTexts[0]);
+ expect(sheet.cssRules.length).toBe(1);
+ expect(sheet.cssRules[0]!.cssText).toBe(redStyleTexts[0]);
+
+ sheet.insertRule(redStyleTexts[1]);
+ expect(sheet.cssRules.length).toBe(2);
+ expect(sheet.cssRules[0]!.cssText).toBe(redStyleTexts[1]);
+
+ const sheet2 = new CSSStyleSheet({});
+ expect(
+ sheet2.title,
+ 'The title attribute must return the title or null if title is the empty string',
+ ).toBe(null);
+ expect(sheet2.ownerNode).toBe(null);
+ expect(sheet2.ownerRule).toBe(null);
+ expect(sheet2.media.length).toBe(0);
+ expect(sheet2.disabled).toBe(false);
+ expect(sheet2.cssRules.length).toBe(0);
+
+ sheet2.insertRule(redStyleTexts[1]);
+ expect(sheet2.cssRules.length).toBe(1);
+ expect(sheet2.cssRules[0]!.cssText).toBe(redStyleTexts[1]);
+
+ sheet2.deleteRule(0);
+ expect(sheet2.cssRules.length).toBe(0);
+
+ const sheet3 = new CSSStyleSheet();
+ expect(
+ sheet3.title,
+ 'The title attribute must return the title or null if title is the empty string',
+ ).toBe(null);
+ expect(sheet3.ownerNode).toBe(null);
+ expect(sheet3.ownerRule).toBe(null);
+ expect(sheet3.media.length).toBe(0);
+ expect(sheet3.disabled).toBe(false);
+ expect(sheet3.cssRules.length).toBe(0);
+
+ sheet3.insertRule(redStyleTexts[1]);
+ expect(sheet3.cssRules.length).toBe(1);
+ expect(sheet3.cssRules[0]!.cssText).toBe(redStyleTexts[1]);
+
+ sheet3.deleteRule(0);
+ expect(sheet3.cssRules.length).toBe(0);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ 'title can be set in the CSSStyleSheet constructor',
+ ({ CSSStyleSheet }) => {
+ const redStyleTexts = ['.red { color: red; }', '.red + span + span { color: red; }'] as const;
+
+ const sheet = new CSSStyleSheet({ title: 'something' } as CSSStyleSheetInit);
+ expect(
+ sheet.title,
+ 'title and alternate are not supported by the constructor. https://github.com/WICG/construct-stylesheets/issues/105',
+ ).toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ 'CSSStyleSheet.replace produces Promise',
+ async ({ CSSStyleSheet }) => {
+ const redStyleTexts = ['.red { color: red; }', '.red + span + span { color: red; }'] as const;
+
+ const sheet = new CSSStyleSheet({ disabled: true, media: 'screen, print' });
+ const promise_sheet = sheet.replace(redStyleTexts[0]);
+ return promise_sheet.then(function (sheet) {
+ expect(
+ sheet.title,
+ 'The title attribute must return the title or null if title is the empty string',
+ ).toBe(null);
+ expect(sheet.ownerNode).toBe(null);
+ expect(sheet.ownerRule).toBe(null);
+ expect(sheet.media.length).toBe(2);
+ expect(sheet.media.item(0)).toBe('screen');
+ expect(sheet.media.item(1)).toBe('print');
+ expect(sheet.disabled).toBe(true);
+ expect(sheet.cssRules.length).toBe(1);
+ expect(sheet.cssRules[0]!.cssText).toBe(redStyleTexts[0]);
+
+ sheet.insertRule(redStyleTexts[1]);
+ expect(sheet.cssRules.length).toBe(2);
+ expect(sheet.cssRules[0]!.cssText).toBe(redStyleTexts[1]);
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+
+ context.test(
+ 'Constructed style sheets can be applied on document',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { greenSpan, redSpan, blueSpan, whiteSpan, yellowSpan, createAllSheetsPromise } =
+ prepare(document, CSSStyleSheet);
+
+ return Promise.all(createAllSheetsPromise()).then((values) => {
+ const greenStyleSheet = values[0]!;
+ const redStyleSheet = values[1]!;
+ const blueStyleSheet = values[2]!;
+ const whiteStyleSheet = values[3]!;
+ const yellowStyleSheet = values[4]!;
+
+ // Lists of style sheets can be created, assigned and read.
+
+ // disabled stylesheets aren't applied
+ document.adoptedStyleSheets = [whiteStyleSheet];
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ // disable dsheets don't block other styles from applying
+ document.adoptedStyleSheets = [greenStyleSheet, blueStyleSheet];
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 128, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ document.adoptedStyleSheets = [redStyleSheet, yellowStyleSheet];
+
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(255, 255, 0)');
+
+ document.adoptedStyleSheets = [
+ redStyleSheet,
+ yellowStyleSheet,
+ greenStyleSheet,
+ blueStyleSheet,
+ ];
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 128, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(255, 255, 0)');
+ document.adoptedStyleSheets = [];
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Constructed style sheets can be applied on shadow root',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const {
+ shadowRoot,
+ greenShadowSpan,
+ redShadowSpan,
+ blueShadowSpan,
+ whiteShadowSpan,
+ yellowShadowSpan,
+ createAllSheetsPromise,
+ } = prepare(document, CSSStyleSheet);
+
+ return Promise.all(createAllSheetsPromise()).then((values) => {
+ const greenStyleSheet = values[0]!;
+ const redStyleSheet = values[1]!;
+ const blueStyleSheet = values[2]!;
+ const whiteStyleSheet = values[3]!;
+ const yellowStyleSheet = values[4]!;
+ shadowRoot.adoptedStyleSheets = [whiteStyleSheet];
+ expect(getComputedStyle(greenShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowShadowSpan).color).toBe('rgb(0, 0, 0)');
+
+ shadowRoot.adoptedStyleSheets = [greenStyleSheet, blueStyleSheet];
+ expect(getComputedStyle(greenShadowSpan).color).toBe('rgb(0, 128, 0)');
+ expect(getComputedStyle(redShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowShadowSpan).color).toBe('rgb(0, 0, 0)');
+
+ shadowRoot.adoptedStyleSheets = [redStyleSheet, yellowStyleSheet];
+ expect(getComputedStyle(greenShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redShadowSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteShadowSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowShadowSpan).color).toBe('rgb(255, 255, 0)');
+
+ shadowRoot.adoptedStyleSheets = [
+ redStyleSheet,
+ yellowStyleSheet,
+ greenStyleSheet,
+ blueStyleSheet,
+ ];
+ expect(getComputedStyle(greenShadowSpan).color).toBe('rgb(0, 128, 0)');
+ expect(getComputedStyle(redShadowSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteShadowSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowShadowSpan).color).toBe('rgb(255, 255, 0)');
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Re-attaching shadow host with adopted stylesheets work',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { shadowHost, shadowRoot, greenShadowSpan, redShadowSpan, createAllSheetsPromise } =
+ prepare(document, CSSStyleSheet);
+
+ return Promise.all(createAllSheetsPromise()).then((values) => {
+ const greenStyleSheet = values[0]!;
+ const redStyleSheet = values[1]!;
+ shadowRoot.adoptedStyleSheets = [greenStyleSheet];
+ expect(getComputedStyle(greenShadowSpan).color, 'Style applies connected').toBe(
+ 'rgb(0, 128, 0)',
+ );
+ expect(getComputedStyle(redShadowSpan).color, 'Style applies when connected').toBe(
+ 'rgb(0, 0, 0)',
+ );
+ let hostParent = shadowHost.parentNode!;
+ hostParent.removeChild(shadowHost);
+ expect(getComputedStyle(greenShadowSpan).color, "Style doesn't apply when detached").toBe(
+ '',
+ );
+ expect(getComputedStyle(redShadowSpan).color, "Style doesn't apply when detached").toBe('');
+ shadowRoot.adoptedStyleSheets = [redStyleSheet, greenStyleSheet];
+ hostParent.appendChild(shadowHost);
+ expect(getComputedStyle(greenShadowSpan).color, 'Style applies after reattach').toBe(
+ 'rgb(0, 128, 0)',
+ );
+ expect(getComputedStyle(redShadowSpan).color, 'Style applies after reattach').toBe(
+ 'rgb(255, 0, 0)',
+ );
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Attaching a shadow root that already has adopted stylesheets work',
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const {} = prepare(document, CSSStyleSheet);
+
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync(':host { color: red; }');
+ const host = document.createElement('div');
+ let sr = host.attachShadow({ mode: 'open' });
+ sr.adoptedStyleSheets = [sheet];
+ document.body.appendChild(host);
+ expect(getComputedStyle(host).color, 'Style applies when connected').toBe('rgb(255, 0, 0)');
+ sheet.replaceSync(':host { color: blue; }');
+ expect(getComputedStyle(host).color, 'Style update applies when connected').toBe(
+ 'rgb(0, 0, 255)',
+ );
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Re-attaching shadow host and updating attributes work',
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const {} = prepare(document, CSSStyleSheet);
+
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync(':host([red]) { color: red; } :host(.blue) { color: blue; }');
+ const host = document.createElement('div');
+ host.toggleAttribute('red');
+ document.body.appendChild(host);
+ expect(getComputedStyle(host).color, 'No style applies yet').toBe('rgb(0, 0, 0)');
+
+ let sr = host.attachShadow({ mode: 'open' });
+ sr.adoptedStyleSheets = [sheet];
+
+ expect(getComputedStyle(host).color, 'Style applies after adding style').toBe(
+ 'rgb(255, 0, 0)',
+ );
+ document.body.removeChild(host);
+ document.body.appendChild(host);
+ expect(getComputedStyle(host).color, 'Style applies after reattachment').toBe(
+ 'rgb(255, 0, 0)',
+ );
+ host.toggleAttribute('red');
+ expect(
+ getComputedStyle(host).color,
+ 'Attribute updates to the element after reattachment apply',
+ ).toBe('rgb(0, 0, 0)');
+ host.classList.toggle('blue');
+ expect(
+ getComputedStyle(host).color,
+ 'Class updates to the element after reattachment apply',
+ ).toBe('rgb(0, 0, 255)');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Changes to constructed stylesheets through CSSOM is reflected',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { redStyleTexts, greenSpan, redSpan, blueSpan, whiteSpan, yellowSpan } = prepare(
+ document,
+ CSSStyleSheet,
+ );
+
+ const plainSheet = new CSSStyleSheet();
+ const redStyleSheetPromise = plainSheet.replace(redStyleTexts[0]!);
+ return redStyleSheetPromise.then(function (redStyleSheet) {
+ document.adoptedStyleSheets = [redStyleSheet];
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ redStyleSheet.insertRule(redStyleTexts[1]!);
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ redStyleSheet.deleteRule(1);
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ (redStyleSheet.cssRules[0]! as CSSStyleRule).style.color = 'white';
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(255, 255, 255)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Constructed stylesheet can be used and modified in multiple TreeScopes',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const {
+ redStyleTexts,
+ shadowRoot,
+ greenSpan,
+ redSpan,
+ blueSpan,
+ whiteSpan,
+ yellowSpan,
+ greenShadowSpan,
+ redShadowSpan,
+ blueShadowSpan,
+ whiteShadowSpan,
+ yellowShadowSpan,
+ } = prepare(document, CSSStyleSheet);
+
+ const plainSheet = new CSSStyleSheet();
+ const redStyleSheetPromise = plainSheet.replace(redStyleTexts[0]!);
+ return redStyleSheetPromise.then(function (redStyleSheet) {
+ document.adoptedStyleSheets = [redStyleSheet];
+ shadowRoot.adoptedStyleSheets = [redStyleSheet];
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ expect(getComputedStyle(greenShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redShadowSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowShadowSpan).color).toBe('rgb(0, 0, 0)');
+
+ shadowRoot.adoptedStyleSheets[0]!.insertRule(redStyleTexts[1]!);
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ expect(getComputedStyle(greenShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redShadowSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueShadowSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteShadowSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowShadowSpan).color).toBe('rgb(0, 0, 0)');
+ document.adoptedStyleSheets = [];
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Stylesheets constructed on the main Document cannot be used in iframes',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { redStyleTexts, firstDiv, greenSpan, redSpan, blueSpan, whiteSpan, yellowSpan } =
+ prepare(document, CSSStyleSheet);
+
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ const thirdDiv = firstDiv.cloneNode(true) as HTMLElement;
+ iframe.contentDocument!.body.appendChild(thirdDiv);
+ const greenIframeSpan = thirdDiv.children[0]!;
+ const redIframeSpan = thirdDiv.children[1]!;
+ const blueIframeSpan = thirdDiv.children[2]!;
+ const whiteIframeSpan = thirdDiv.children[3]!;
+ const yellowIframeSpan = thirdDiv.children[4]!;
+
+ const plainSheet = new CSSStyleSheet();
+ const redStyleSheetPromise = plainSheet.replace(redStyleTexts[0]!);
+ return redStyleSheetPromise.then(function (redStyleSheet) {
+ expect(() => {
+ iframe.contentDocument!.adoptedStyleSheets = [redStyleSheet];
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NotAllowedError',
+ code: 0,
+ constructor: (iframe.contentWindow as Window & typeof globalThis).DOMException,
+ }),
+ );
+ expect(getComputedStyle(greenIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowIframeSpan).color).toBe('rgb(0, 0, 0)');
+
+ document.adoptedStyleSheets = [redStyleSheet];
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ document.adoptedStyleSheets[0]!.insertRule(redStyleTexts[1]!);
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Stylesheet constructed on iframe cannot be used in the main Document',
+ async ({ document, CSSStyleSheet, getComputedStyle, DOMException }) => {
+ const { redStyleTexts, firstDiv, greenSpan, redSpan, blueSpan, whiteSpan, yellowSpan } =
+ prepare(document, CSSStyleSheet);
+
+ const iframe = document.createElement('iframe');
+ const iframeLoaded = new Promise((resolve) => iframe.addEventListener('load', resolve));
+ document.body.appendChild(iframe);
+ await iframeLoaded;
+ const thirdDiv = firstDiv.cloneNode(true) as HTMLElement;
+ iframe.contentDocument!.body.appendChild(thirdDiv);
+ const greenIframeSpan = thirdDiv.children[0]!;
+ const redIframeSpan = thirdDiv.children[1]!;
+ const blueIframeSpan = thirdDiv.children[2]!;
+ const whiteIframeSpan = thirdDiv.children[3]!;
+ const yellowIframeSpan = thirdDiv.children[4]!;
+
+ // Make sure both the main Document and the iframe are not styled
+ const emptyStyleSheet = new CSSStyleSheet();
+ document.adoptedStyleSheets = [emptyStyleSheet];
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ expect(getComputedStyle(greenIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowIframeSpan).color).toBe('rgb(0, 0, 0)');
+
+ const iframePlainSheet = new (
+ iframe.contentWindow as Window & typeof globalThis
+ ).CSSStyleSheet();
+ const iframeRedStyleSheetPromise = iframePlainSheet.replace(redStyleTexts[0]!);
+ return iframeRedStyleSheetPromise.then(function (iframeRedStyleSheet) {
+ expect(() => {
+ document.adoptedStyleSheets = [iframeRedStyleSheet];
+ }).toThrow(
+ expect.objectContaining({ name: 'NotAllowedError', code: 0, constructor: DOMException }),
+ );
+ expect(getComputedStyle(greenSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(blueSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowSpan).color).toBe('rgb(0, 0, 0)');
+
+ iframe.contentDocument!.adoptedStyleSheets = [iframeRedStyleSheet];
+ expect(getComputedStyle(greenIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redIframeSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(yellowIframeSpan).color).toBe('rgb(0, 0, 0)');
+
+ iframe.contentDocument!.adoptedStyleSheets[0]!.insertRule(redStyleTexts[1]!);
+ expect(getComputedStyle(greenIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(redIframeSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(blueIframeSpan).color).toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(whiteIframeSpan).color).toBe('rgb(255, 0, 0)');
+ expect(getComputedStyle(yellowIframeSpan).color).toBe('rgb(0, 0, 0)');
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Adding non-constructed stylesheet to AdoptedStyleSheets is not allowed when the owner document of the stylesheet is in the same document tree as the AdoptedStyleSheets',
+ ({ document, CSSStyleSheet, getComputedStyle, DOMException }) => {
+ const { divNonConstructed, nonConstructedStyleSheet } = prepare(document, CSSStyleSheet);
+
+ expect(getComputedStyle(divNonConstructed).color).toBe('rgb(0, 0, 0)');
+ expect(() => {
+ document.adoptedStyleSheets = [nonConstructedStyleSheet];
+ }).toThrow(
+ expect.objectContaining({ name: 'NotAllowedError', code: 0, constructor: DOMException }),
+ );
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Adding non-constructed stylesheet to AdoptedStyleSheets is not allowed when the owner document of the stylesheet and the AdoptedStyleSheets are in different document trees',
+ ({ document, CSSStyleSheet, getComputedStyle, DOMException }) => {
+ const { divNonConstructed, nonConstructedStyleSheet } = prepare(document, CSSStyleSheet);
+
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ const iframeDiv = iframe.contentDocument!.createElement('div');
+ iframeDiv.classList.add('nonConstructed');
+ iframe.contentDocument!.body.appendChild(iframeDiv);
+
+ expect(getComputedStyle(iframeDiv).color).toBe('rgb(0, 0, 0)');
+ expect(() => {
+ iframe.contentDocument!.adoptedStyleSheets = [nonConstructedStyleSheet];
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NotAllowedError',
+ code: 0,
+ constructor: (iframe.contentWindow as Window & typeof globalThis).DOMException,
+ }),
+ );
+ expect(getComputedStyle(iframeDiv).color).toBe('rgb(0, 0, 0)');
+
+ const iframeStyle = iframe.contentDocument!.createElement('style');
+ iframe.contentDocument!.body.appendChild(iframeStyle);
+ iframeStyle.sheet!.insertRule('.nonConstructedSpan { color: red; }');
+ const iframeStyleSheet = iframeStyle.sheet!;
+ const nonConstructedSpan = document.createElement('span');
+ nonConstructedSpan.classList.add('.nonConstructedSpan');
+ divNonConstructed.appendChild(nonConstructedSpan);
+
+ expect(getComputedStyle(iframeDiv).color).toBe('rgb(0, 0, 0)');
+ expect(() => {
+ document.adoptedStyleSheets = [iframeStyleSheet];
+ }).toThrow(
+ expect.objectContaining({ name: 'NotAllowedError', code: 0, constructor: DOMException }),
+ );
+ expect(getComputedStyle(iframeDiv).color).toBe('rgb(0, 0, 0)');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'CSSStyleSheet.replaceSync replaces stylesheet text synchronously',
+ ({ document, CSSStyleSheet }) => {
+ const { redStyleTexts } = prepare(document, CSSStyleSheet);
+
+ const sheet = new CSSStyleSheet();
+ expect(sheet.cssRules.length).toBe(0);
+
+ sheet.replaceSync(redStyleTexts[0]!);
+ expect(sheet.cssRules.length).toBe(1);
+ expect(redStyleTexts[0]!).toBe(sheet.cssRules[0]!.cssText);
+
+ sheet.replaceSync(redStyleTexts[1]!);
+ expect(sheet.cssRules.length).toBe(1);
+ expect(redStyleTexts[1]!).toBe(sheet.cssRules[0]!.cssText);
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'CSSStyleSheet.replaceSync correctly updates the style of its adopters synchronously',
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { redStyleTexts, shadowRoot, thirdSection, attachShadowDiv } = prepare(
+ document,
+ CSSStyleSheet,
+ );
+
+ // Attach a div inside a shadow root with the class ".red".
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ shadowDiv.classList.add('red');
+ // Create empty stylesheet.
+ const sheet = new CSSStyleSheet();
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ // Replace the stylesheet text that will color it red.
+ sheet.replaceSync(redStyleTexts[0]!);
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(255, 0, 0)');
+ expect(sheet.cssRules.length).toBe(1);
+ expect(sheet.cssRules[0]!.cssText).toBe(redStyleTexts[0]!);
+ sheet.insertRule(redStyleTexts[1]!);
+ expect(sheet.cssRules.length).toBe(2);
+ expect(sheet.cssRules[0]!.cssText).toBe(redStyleTexts[1]!);
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Adopted sheets are ordered after non-adopted sheets in the shadow root',
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv } = prepare(document, CSSStyleSheet);
+
+ // Attach a div inside a shadow root with the class ".red".
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ shadowDiv.classList.add('target');
+
+ // Create empty stylesheet.
+ const sheet = new CSSStyleSheet();
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+
+ // Replace the stylesheet text that will color it red.
+ sheet.replaceSync('.target { color: red; }');
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(255, 0, 0)');
+
+ // Create a style element that will set colors to white.
+ const style = document.createElement('style');
+ style.textContent = '.target { color: white; }';
+ span.shadowRoot!.appendChild(style);
+ expect(
+ getComputedStyle(shadowDiv).color,
+ 'non-adopted styles should be ordered before adopted styles',
+ ).toBe('rgb(255, 0, 0)');
+
+ span.shadowRoot!.adoptedStyleSheets = [];
+ expect(
+ getComputedStyle(shadowDiv).color,
+ 'with no adopted styles in conflict, the non-adopted style should take effect',
+ ).toBe('rgb(255, 255, 255)');
+
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(
+ getComputedStyle(shadowDiv).color,
+ 'the adopted style should be ordered after the non-adopted style',
+ ).toBe('rgb(255, 0, 0)');
+
+ sheet.disabled = true;
+ expect(
+ getComputedStyle(shadowDiv).color,
+ 'with the adopted sheet disabled, the non-adopted style should take effect',
+ ).toBe('rgb(255, 255, 255)');
+
+ sheet.disabled = false;
+ expect(
+ getComputedStyle(shadowDiv).color,
+ 'the adopted sheet re-enabled, it should take effect again',
+ ).toBe('rgb(255, 0, 0)');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Adopted sheets are ordered after non-adopted sheets in the document',
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { thirdSection } = prepare(document, CSSStyleSheet);
+
+ // Attach a div inside a shadow root with the class ".red".
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ span.classList.add('target');
+
+ // Create empty stylesheet.
+ const sheet = new CSSStyleSheet();
+ document.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(span).color).toBe('rgb(0, 0, 0)');
+
+ // Replace the stylesheet text that will color it red.
+ sheet.replaceSync('.target { color: red; }');
+ expect(getComputedStyle(span).color).toBe('rgb(255, 0, 0)');
+
+ // Create a style element that will set colors to white.
+ const style = document.createElement('style');
+ style.textContent = '.target { color: white; }';
+ span.appendChild(style);
+ expect(
+ getComputedStyle(span).color,
+ 'non-adopted styles should be ordered before adopted styles',
+ ).toBe('rgb(255, 0, 0)');
+
+ document.adoptedStyleSheets = [];
+ expect(
+ getComputedStyle(span).color,
+ 'with no adopted styles in conflict, the non-adopted style should take effect',
+ ).toBe('rgb(255, 255, 255)');
+
+ document.adoptedStyleSheets = [sheet];
+ expect(
+ getComputedStyle(span).color,
+ 'the adopted style should be ordered after the non-adopted style',
+ ).toBe('rgb(255, 0, 0)');
+
+ sheet.disabled = true;
+ expect(
+ getComputedStyle(span).color,
+ 'with the adopted sheet disabled, the non-adopted style should take effect',
+ ).toBe('rgb(255, 255, 255)');
+
+ sheet.disabled = false;
+ expect(
+ getComputedStyle(span).color,
+ 'the adopted sheet re-enabled, it should take effect again',
+ ).toBe('rgb(255, 0, 0)');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Inserting an @import rule through insertRule on a constructed stylesheet throws an exception',
+ ({ document, CSSStyleSheet, DOMException }) => {
+ const { import_text } = prepare(document, CSSStyleSheet);
+
+ expect(() => {
+ new CSSStyleSheet().insertRule(import_text);
+ }).toThrow(
+ expect.objectContaining({ name: 'SyntaxError', code: 12, constructor: DOMException }),
+ );
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'CSSStyleSheet.replace allows, but ignores, import rule inside',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv, import_text } = prepare(
+ document,
+ CSSStyleSheet,
+ );
+
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ const sheet = new CSSStyleSheet();
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ // Replace and assert that the imported rule is NOT applied.
+ const sheet_promise = sheet.replace(import_text);
+ return sheet_promise
+ .then((sheet) => {
+ // replace() ignores @import rules:
+ expect(sheet.cssRules.length).toBe(0);
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ })
+ .catch((reason) => {
+ expect.unreachable(`Promise was rejected (${reason}) when it should have been resolved`);
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'CSSStyleSheet.replace ignores @import rule but still loads other rules',
+ async ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv, import_text } = prepare(
+ document,
+ CSSStyleSheet,
+ );
+
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ const targetSpan = document.createElement('span');
+ targetSpan.classList.add('target');
+ shadowDiv.appendChild(targetSpan);
+ const sheet = new CSSStyleSheet();
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ // Replace and assert that the imported rule is NOT applied, but regular rule does apply.
+ const sheet_promise = sheet.replace(import_text + '.target { color: blue; }');
+ return sheet_promise
+ .then((sheet) => {
+ expect(sheet.cssRules.length).toBe(1);
+ // @import not applied:
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ // regular rule applied:
+ expect(getComputedStyle(targetSpan).color).toBe('rgb(0, 0, 255)');
+ })
+ .catch((reason) => {
+ expect.unreachable(`Promise was rejected (${reason}) when it should have been resolved`);
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'CSSStyleSheet.replaceSync allows, but ignores, import rule inside',
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv, import_text } = prepare(
+ document,
+ CSSStyleSheet,
+ );
+
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ const sheet = new CSSStyleSheet();
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ // Replace and assert that the imported rule is NOT applied.
+ try {
+ sheet.replaceSync(import_text);
+ // replaceSync() ignores @import rules:
+ expect(sheet.cssRules.length).toBe(0);
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ } catch (reason) {
+ expect.unreachable(`replaceSync threw an exception (${reason}) when it shouldn't have`);
+ }
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'CSSStyleSheet.replace does not reject on failed imports',
+ async ({ document, CSSStyleSheet }) => {
+ const {} = prepare(document, CSSStyleSheet);
+
+ const sheet = new CSSStyleSheet();
+ const sheet_promise = sheet.replace("@import url('not-there.css');");
+
+ return sheet_promise
+ .then((sheet) => {
+ // No exception here
+ })
+ .catch((reason) => {
+ expect.unreachable('Promise was rejected');
+ });
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Cloning a shadow host will not clone shadow root, and also adoptedStyleSheets',
+ ({ document, CSSStyleSheet }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv } = prepare(document, CSSStyleSheet);
+
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ const sheet = new CSSStyleSheet();
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+
+ const newSpan = span.cloneNode(true) as HTMLElement;
+ expect(newSpan.shadowRoot).toBe(null);
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Importing a shadow host will not copy shadow root, and also adoptedStyleSheets',
+ ({ document, CSSStyleSheet }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv } = prepare(document, CSSStyleSheet);
+
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ const sheet = new CSSStyleSheet();
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ const newSpan = iframe.contentDocument!.importNode(span, true);
+ iframe.contentDocument!.body.appendChild(newSpan);
+ expect(newSpan.shadowRoot).toBe(null);
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Adopting a shadow host will empty adoptedStyleSheets if adopting to a different document',
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv } = prepare(document, CSSStyleSheet);
+
+ const span = document.createElement('span');
+ thirdSection.appendChild(span);
+ const shadowDiv = attachShadowDiv(span);
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync('* { color: red; }');
+ span.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(255, 0, 0)');
+
+ document.adoptNode(span);
+ expect(span.shadowRoot!.adoptedStyleSheets.length).toBe(1);
+ expect(span.shadowRoot!.adoptedStyleSheets[0]!).toBe(sheet);
+
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentDocument!.adoptNode(span);
+ iframe.contentDocument!.body.appendChild(span);
+ expect(span.shadowRoot).not.toBe(null);
+ expect(span.shadowRoot!.adoptedStyleSheets.length).toBe(0);
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ `Adopting a shadow host's ancestor will empty adoptedStyleSheets if adopting to a different document`,
+ ({ document, CSSStyleSheet, getComputedStyle }) => {
+ const { shadowRoot, thirdSection, attachShadowDiv } = prepare(document, CSSStyleSheet);
+
+ const span = document.createElement('span');
+ const div = document.createElement('div');
+ thirdSection.appendChild(span);
+ span.appendChild(div);
+ const shadowDiv = attachShadowDiv(div);
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync('* { color: red; }');
+ div.shadowRoot!.adoptedStyleSheets = [sheet];
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(255, 0, 0)');
+
+ document.adoptNode(span);
+ expect(div.shadowRoot!.adoptedStyleSheets.length).toBe(1);
+ expect(div.shadowRoot!.adoptedStyleSheets[0]!).toBe(sheet);
+
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ iframe.contentDocument!.adoptNode(span);
+ iframe.contentDocument!.body.appendChild(span);
+ expect(div.shadowRoot).not.toBe(null);
+ expect(div.shadowRoot!.adoptedStyleSheets.length).toBe(0);
+ expect(getComputedStyle(shadowDiv).color).toBe('rgb(0, 0, 0)');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Forcing a style update after adding an adopted stylesheet on a disconnected shadow root should not crash.',
+ ({ document, CSSStyleSheet }) => {
+ const {} = prepare(document, CSSStyleSheet);
+
+ const host = document.createElement('div');
+ const root = host.attachShadow({ mode: 'open' });
+ root.adoptedStyleSheets = [new CSSStyleSheet()];
+ document.body.offsetTop;
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'Modifying an adopted stylesheet on a disconnected shadow root should not crash.',
+ ({ document, CSSStyleSheet }) => {
+ const { thirdSection } = prepare(document, CSSStyleSheet);
+
+ const host = document.createElement('div');
+ thirdSection.appendChild(host);
+ const root = host.attachShadow({ mode: 'open' });
+ const sheet = new CSSStyleSheet();
+ root.adoptedStyleSheets = [sheet];
+ host.remove();
+ sheet.replaceSync('');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet.html.ts
new file mode 100644
index 0000000..2d0fe62
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/CSSStyleSheet.html.ts
@@ -0,0 +1,515 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup = `
+
+
+
+ CSSOM - CSSStyleSheet interface
+\x20\x20\x20\x20
+\x20\x20\x20\x20
+\x20\x20\x20\x20
+\x20\x20\x20\x20
+
+
+
+\x20\x20\x20\x20
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'preconditions',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+
+ expect(styleSheet, "CSSStyleSheet and LinkStyle's sheet attribute").toBe(
+ (document.getElementById('my-stylesheet')! as HTMLStyleElement).sheet,
+ );
+ expect(styleSheet.cssRules.length, 'CSSStyleSheet cssRules attribute').toBe(2);
+ expect(styleSheet.cssRules[0]!.cssText, 'CSSStyleSheet cssRules attribute').toBe(
+ 'body { width: 50%; }',
+ );
+ expect(styleSheet.cssRules[1]!.cssText, 'CSSStyleSheet cssRules attribute').toBe(
+ '#foo { height: 100px; }',
+ );
+ expect(styleSheet.cssRules[2], 'CSSStyleSheet cssRules attribute').toBe(undefined);
+
+ expect(emptyStyleSheet, "CSSStyleSheet and LinkStyle's sheet attribute").toBe(
+ (document.getElementById('empty-stylesheet')! as HTMLStyleElement).sheet,
+ );
+ expect(emptyStyleSheet.cssRules.length, 'CSSStyleSheet cssRules attribute').toBe(0);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'insertRule with #bar selector',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after insertRule function',
+ ).toBe(3);
+ expect(styleSheet.cssRules[0]!.cssText, 'CSSStyleSheet cssRules attribute').toBe(
+ 'body { width: 50%; }',
+ );
+ expect(
+ styleSheet.cssRules[1]!.cssText,
+ 'CSSStyleSheet cssRules attribute after insertRule function',
+ ).toBe('#bar { margin: 10px; }');
+ expect(
+ styleSheet.cssRules[2]!.cssText,
+ 'CSSStyleSheet cssRules attribute after insertRule function',
+ ).toBe('#foo { height: 100px; }');
+ expect(
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty,
+ '[SameObject] cssRules attribute after insertRule function',
+ ).toBe(1);
+ expect(
+ (styleSheet.cssRules[2] as CSSRule & { randomProperty: number }).randomProperty,
+ '[SameObject] cssRules attribute after insertRule function',
+ ).toBe(2);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'insertRule with no argument throws',
+ (fixture) => {
+ const { document } = fixture;
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ const TypeError = fixture.TypeError;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ expect(function () {
+ (styleSheet.insertRule as () => number)();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'insertRule with index greater than length throws',
+ ({ document, DOMException }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ expect(function () {
+ styleSheet.insertRule('#bar { margin: 10px; }', styleSheet.cssRules.length + 1);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'deleteRule(1)',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after deleteRule function',
+ ).toBe(2);
+ expect(
+ styleSheet.cssRules[0]!.cssText,
+ 'CSSStyleSheet cssRules attribute after deleteRule function',
+ ).toBe('body { width: 50%; }');
+ expect(
+ styleSheet.cssRules[1]!.cssText,
+ 'CSSStyleSheet cssRules attribute after deleteRule function',
+ ).toBe('#foo { height: 100px; }');
+ expect(
+ styleSheet.cssRules[2],
+ 'CSSStyleSheet cssRules attribute after deleteRule function',
+ ).toBe(undefined);
+ expect(
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty,
+ '[SameObject] cssRules attribute after deleteRule function',
+ ).toBe(1);
+ expect(
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty,
+ '[SameObject] cssRules attribute after deleteRule function',
+ ).toBe(2);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'deleteRule with no argument throws',
+ (fixture) => {
+ const { document } = fixture;
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ const TypeError = fixture.TypeError;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ expect(function () {
+ (styleSheet.deleteRule as () => void)();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'deleteRule on empty style sheet throws',
+ ({ document, DOMException }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ expect(function () {
+ emptyStyleSheet.deleteRule(0);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'removeRule with no argument removes first rule',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after removeRule function',
+ ).toBe(1);
+ expect(
+ styleSheet.cssRules[0]!.cssText,
+ 'CSSStyleSheet cssRules attribute after removeRule function',
+ ).toBe('#foo { height: 100px; }');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'removeRule on empty style sheet throws',
+ ({ document, DOMException }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ expect(function () {
+ emptyStyleSheet.removeRule(0);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'addRule with @media rule',
+ (fixture) => {
+ const { document } = fixture;
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ const CSSMediaRule = fixture.CSSMediaRule;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ expect(styleSheet.addRule('@media all', '#foo { color: red }')).toBe(-1);
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after addRule function',
+ ).toBe(2);
+ expect(
+ styleSheet.cssRules[1] instanceof CSSMediaRule,
+ 'CSSStyleSheet addRule does some silly string concatenation',
+ ).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'removeRule(1)',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ styleSheet.addRule('@media all', '#foo { color: red }');
+ styleSheet.removeRule(1);
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after removeRule function with index',
+ ).toBe(1);
+ expect(
+ styleSheet.cssRules[0]!.cssText,
+ 'CSSStyleSheet cssRules attribute after deleteRule function with index',
+ ).toBe('#foo { height: 100px; }');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'addRule with #foo selectors',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ styleSheet.addRule('@media all', '#foo { color: red }');
+ styleSheet.removeRule(1);
+ expect(styleSheet.addRule('#foo', 'color: red')).toBe(-1);
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after addRule function with simple selector',
+ ).toBe(2);
+ expect(
+ styleSheet.cssRules[1]!.cssText,
+ 'CSSStyleSheet cssRules attribute after addRule function without index appends to the end',
+ ).toBe('#foo { color: red; }');
+
+ expect(styleSheet.addRule('#foo', 'color: blue', 0)).toBe(-1);
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after addRule function with simple selector with index',
+ ).toBe(3);
+ expect(
+ styleSheet.cssRules[0]!.cssText,
+ 'addRule function with index performs an insertion',
+ ).toBe('#foo { color: blue; }');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'addRule with no argument adds "undefined" selector',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ styleSheet.addRule('@media all', '#foo { color: red }');
+ styleSheet.removeRule(1);
+ styleSheet.addRule('#foo', 'color: red');
+ styleSheet.addRule('#foo', 'color: blue', 0);
+ expect(styleSheet.addRule()).toBe(-1);
+ expect(
+ styleSheet.cssRules.length,
+ 'CSSStyleSheet cssRules attribute after addRule function without arguments',
+ ).toBe(4);
+ expect(styleSheet.cssRules[3]!.cssText, 'addRule arguments default to undefined').toBe(
+ 'undefined { }',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'addRule with index greater than length throws',
+ ({ document, DOMException }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ styleSheet.addRule('@media all', '#foo { color: red }');
+ styleSheet.removeRule(1);
+ styleSheet.addRule('#foo', 'color: red');
+ styleSheet.addRule('#foo', 'color: blue', 0);
+ styleSheet.addRule();
+ expect(function () {
+ styleSheet.addRule('#foo', 'color: red', styleSheet.cssRules.length + 1);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'cssRules and rules are the same object',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ styleSheet.addRule('@media all', '#foo { color: red }');
+ styleSheet.removeRule(1);
+ styleSheet.addRule('#foo', 'color: red');
+ styleSheet.addRule('#foo', 'color: blue', 0);
+ styleSheet.addRule();
+ expect(styleSheet.cssRules).toBe(styleSheet.rules);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'cssRules returns the same object twice',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ styleSheet.addRule('@media all', '#foo { color: red }');
+ styleSheet.removeRule(1);
+ styleSheet.addRule('#foo', 'color: red');
+ styleSheet.addRule('#foo', 'color: blue', 0);
+ styleSheet.addRule();
+ expect(styleSheet.cssRules).toBe(styleSheet.cssRules);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'rules returns the same object twice',
+ ({ document }) => {
+ const styleSheet = document.styleSheets[0]!,
+ emptyStyleSheet = document.styleSheets[1]!;
+ (styleSheet.cssRules[0] as CSSRule & { randomProperty: number }).randomProperty = 1;
+ (styleSheet.cssRules[1] as CSSRule & { randomProperty: number }).randomProperty = 2;
+ styleSheet.insertRule('#bar { margin: 10px; }', 1);
+ styleSheet.deleteRule(1);
+ styleSheet.removeRule();
+ styleSheet.addRule('@media all', '#foo { color: red }');
+ styleSheet.removeRule(1);
+ styleSheet.addRule('#foo', 'color: red');
+ styleSheet.addRule('#foo', 'color: blue', 0);
+ styleSheet.addRule();
+ expect(styleSheet.rules).toBe(styleSheet.rules);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/MutationObserver-style.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/MutationObserver-style.html.ts
new file mode 100644
index 0000000..2874619
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/MutationObserver-style.html.ts
@@ -0,0 +1,43 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Updating style property with the same value does not trigger an observation callback',
+ async ({ document, MutationObserver, cleanup }) => {
+ let called = 0;
+ const el = document.createElement('div');
+ document.body.appendChild(el);
+ const m = new MutationObserver(() => {
+ called++;
+ });
+ cleanup(() => m.disconnect());
+ m.observe(el, { attributes: true });
+ el.style.height = '100px';
+ await Promise.resolve();
+ expect(called, 'times callback called').toBe(1);
+ el.style.height = '100px';
+ await Promise.resolve();
+ expect(called, 'times callback called').toBe(1);
+ },
+ );
+ context.test(
+ 'Updating cssText triggers an observation callback even if the value is the same',
+ async ({ document, MutationObserver, cleanup }) => {
+ let called = 0;
+ const el = document.createElement('div');
+ document.body.appendChild(el);
+ const m = new MutationObserver(() => {
+ called++;
+ });
+ cleanup(() => m.disconnect());
+ m.observe(el, { attributes: true });
+ el.style.cssText = 'height:100px';
+ await Promise.resolve();
+ expect(called, 'times callback called').toBe(1);
+ el.style.cssText = 'height:100px';
+ await Promise.resolve();
+ expect(called, 'times callback called').toBe(2);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/computed-style-001.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/computed-style-001.html.ts
new file mode 100644
index 0000000..e1c9774
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/computed-style-001.html.ts
@@ -0,0 +1,116 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup1 = `
+
+
+ CSS Test: getComputedStyle
+\x20\x20
+\x20\x20
+\x20\x20
+\x20\x20
+
+
+\x20\x20
+\x20\x20
+
+
+
+ Test not run - javascript required.
+
+
+\x20
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'read_only',
+ ({ document, getComputedStyle, DOMException, window }) => {
+ const inner = document.getElementById('inside')!;
+ let innerStyle = getComputedStyle(inner);
+
+ innerStyle = getComputedStyle(inner);
+ expect(function () {
+ innerStyle.cssText = 'color: blue;';
+ }, 'do not allow setting cssText on a readonly CSSStyleDeclaration').toThrow(
+ expect.objectContaining({
+ name: 'NoModificationAllowedError',
+ code: 7,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ innerStyle.setProperty('color', 'blue');
+ }, 'do not allow calling setProperty on a readonly CSSStyleDeclaration').toThrow(
+ expect.objectContaining({
+ name: 'NoModificationAllowedError',
+ code: 7,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ innerStyle.color = 'blue';
+ }, 'do not allow setting a property on a readonly CSSStyleDeclaration').toThrow(
+ expect.objectContaining({
+ name: 'NoModificationAllowedError',
+ code: 7,
+ constructor: window.DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ 'inherited_property_values',
+ ({ document, getComputedStyle }) => {
+ const inner = document.getElementById('inside')!;
+ let innerStyle = getComputedStyle(inner);
+
+ expect(innerStyle.getPropertyValue('font-size')).toBe('100px');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+
+ context.test(
+ 'property_values',
+ ({ document, getComputedStyle }) => {
+ const inner = document.getElementById('inside')!;
+ const innerStyle = getComputedStyle(inner);
+ expect(innerStyle.getPropertyValue('height')).toBe('100px');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+ context.test(
+ 'relative_property_values',
+ ({ document, getComputedStyle }) => {
+ const inner = document.getElementById('inside')!;
+ const innerStyle = getComputedStyle(inner);
+ expect(innerStyle.getPropertyValue('width')).toBe('100px');
+ },
+ { document: { contentType: 'text/html', content: markup1 } },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/computed-style-set-property.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/computed-style-set-property.html.ts
new file mode 100644
index 0000000..fb4873e
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/computed-style-set-property.html.ts
@@ -0,0 +1,107 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup2 = `
+NoModificationAllowedError when mutating read only properties
+
+
+
+
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Computed style parent (should be null)',
+ ({ document, getComputedStyle }) => {
+ expect(getComputedStyle(document.body, null).parentRule).toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ context.test(
+ 'Exception thrown when trying to change a computed style declaration via property',
+ ({ document, getComputedStyle, window }) => {
+ expect(function () {
+ getComputedStyle(document.body, null).color = 'blue';
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NoModificationAllowedError',
+ code: 7,
+ constructor: window.DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ context.test(
+ 'Exception thrown when trying to change a computed style declaration via setProperty',
+ ({ document, getComputedStyle, DOMException }) => {
+ expect(function () {
+ getComputedStyle(document.body, null).setProperty('color', 'blue');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NoModificationAllowedError',
+ code: 7,
+ constructor: DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ context.test(
+ 'Exception thrown when trying to change a computed style alias via property',
+ ({ document, getComputedStyle, window }) => {
+ expect(function () {
+ getComputedStyle(document.body, null).webkitTransition = '';
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NoModificationAllowedError',
+ code: 7,
+ constructor: window.DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ context.test(
+ 'Exception thrown when trying to change a computed style alias via setProperty',
+ ({ document, getComputedStyle, DOMException }) => {
+ expect(function () {
+ getComputedStyle(document.body, null).setProperty('webkitTransition', '');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NoModificationAllowedError',
+ code: 7,
+ constructor: DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/css-style-attr-decl-block.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/css-style-attr-decl-block.html.ts
new file mode 100644
index 0000000..44e0f7c
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/css-style-attr-decl-block.html.ts
@@ -0,0 +1,179 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ function createTestElement(document: Document, style: string) {
+ let wrapper = document.createElement('div');
+ wrapper.innerHTML = `
`;
+ return wrapper.querySelector('#test')!;
+ }
+ context.test(
+ 'Style attribute should create CSS declaration block based on its content',
+ ({ document }) => {
+ let elem = createTestElement(document, 'z-index: 10;');
+ expect(elem.style.cssText).toBe('z-index: 10;');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '\n \n\n\n\n\n\n\n',
+ },
+ },
+ );
+ context.test(
+ 'Changes to style attribute should reflect on CSS declaration block',
+ ({ document }) => {
+ let elem = createTestElement(document, 'z-index: 20;');
+ let style = elem.style;
+ expect(style.cssText).toBe('z-index: 20;');
+ function assert_css_text(value: string, action: string) {
+ expect(style.cssText, 'CSS declaration block after ' + action).toBe(value);
+ }
+ elem.setAttribute('style', 'z-index: 21;');
+ assert_css_text('z-index: 21;', 'changing the style attribute');
+ elem.removeAttribute('style');
+ assert_css_text('', 'removing the style attribute');
+ elem.setAttribute('style', 'position: absolute;');
+ assert_css_text('position: absolute;', 'adding style attribute again');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '\n \n\n\n\n\n\n\n',
+ },
+ },
+ );
+ context.test(
+ 'Changes to CSS declaration block should reflect on style attribute',
+ ({ document }) => {
+ let elem = createTestElement(document, 'z-index: 30;');
+ let style = elem.style;
+ expect(style.cssText).toBe('z-index: 30;');
+ function assert_attr(value: string, action: string) {
+ expect(elem.getAttribute('style'), 'style attribute after ' + action).toBe(value);
+ }
+ style.setProperty('z-index', '31');
+ assert_attr('z-index: 31;', 'changing property in CSS declaration block');
+ style.removeProperty('z-index');
+ assert_attr('', 'removing property from CSS declaration block');
+ style.setProperty('position', 'absolute');
+ assert_attr('position: absolute;', 'adding property to CSS declaration block');
+ style.cssText = 'z-index: 32;';
+ assert_attr('z-index: 32;', 'changing cssText');
+ style.cssText = 'z-index: 33; invalid';
+ assert_attr('z-index: 33;', 'changing cssText to a partial invalid value');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '\n \n\n\n\n\n\n\n',
+ },
+ },
+ );
+ context.test(
+ 'Changes to CSS declaration block should queue mutation record for style attribute',
+ ({ document, MutationObserver }) => {
+ let elem = createTestElement(document, 'z-index: 40;');
+ let style = elem.style;
+ expect(style.cssText).toBe('z-index: 40;');
+ // Create an observer for the element.
+ let observer = new MutationObserver(function () {});
+ observer.observe(elem, { attributes: true, attributeOldValue: true });
+ function assert_record_with_old_value(oldValue: string, action: string) {
+ let records = observer.takeRecords();
+ expect(records.length, 'number of mutation records after ' + action).toBe(1);
+ let record = records[0]!;
+ expect(record.type, 'mutation type after ' + action).toBe('attributes');
+ expect(record.attributeName, 'mutated attribute after ' + action).toBe('style');
+ expect(record.oldValue, 'old value after ' + action).toBe(oldValue);
+ }
+ style.setProperty('z-index', '41');
+ assert_record_with_old_value('z-index: 40;', 'changing property in CSS declaration block');
+ style.cssText = 'z-index: 42;';
+ assert_record_with_old_value('z-index: 41;', 'changing cssText');
+ style.cssText = 'z-index: 42;';
+ assert_record_with_old_value('z-index: 42;', 'changing cssText with the same content');
+ style.removeProperty('z-index');
+ assert_record_with_old_value('z-index: 42;', 'removing property from CSS declaration block');
+ // Mutation to shorthand properties should also trigger only one mutation record.
+ style.setProperty('margin', '1px');
+ assert_record_with_old_value('', 'adding shorthand property to CSS declaration block');
+ style.removeProperty('margin');
+ assert_record_with_old_value(
+ 'margin: 1px;',
+ 'removing shorthand property from CSS declaration block',
+ );
+ // Final sanity check.
+ expect(elem.getAttribute('style')).toBe('');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '\n \n\n\n\n\n\n\n',
+ },
+ },
+ );
+ context.test(
+ "Removing non-existing property or setting invalid value on CSS declaration block shouldn't queue mutation record",
+ ({ document, MutationObserver }) => {
+ let elem = createTestElement(document, 'z-index: 50; invalid');
+ let style = elem.style;
+ expect(style.cssText).toBe('z-index: 50;');
+ // Create an observer for the element.
+ let observer = new MutationObserver(function () {});
+ observer.observe(elem, { attributes: true });
+ function assert_no_record(action: string) {
+ let records = observer.takeRecords();
+ expect(records.length, 'expect no record after ' + action).toBe(0);
+ }
+ style.setProperty('z-index', 'invalid');
+ assert_no_record('setting invalid value to property');
+ // Longhand property.
+ style.removeProperty('position');
+ assert_no_record('removing non-existing longhand property');
+ style.setProperty('position', '');
+ assert_no_record('setting empty string to non-existing longhand property');
+ // Shorthand property.
+ style.removeProperty('margin');
+ assert_no_record('removing non-existing shorthand property');
+ style.setProperty('margin', '');
+ assert_no_record('setting empty string to non-existing shorthand property');
+ // Check that the value really isn't changed.
+ expect(
+ elem.getAttribute('style'),
+ 'style attribute after removing non-existing properties',
+ ).toBe('z-index: 50; invalid');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '\n \n\n\n\n\n\n\n',
+ },
+ },
+ );
+ context.test(
+ 'Expansion of all:unset and all:revert treated identically',
+ ({ document, cleanup }) => {
+ let e1 = document.createElement('div');
+ let e2 = document.createElement('div');
+ document.body.append(e1, e2);
+ cleanup(() => {
+ e1.remove();
+ e2.remove();
+ });
+ e1.style.cssText = 'all:revert;border-bottom-left-radius:1px;';
+ e2.style.cssText = 'all:unset;border-bottom-left-radius:1px;';
+ let processed = e1.style.cssText
+ .split(';')
+ .map((x) => x.replace(/revert$/, 'unset'))
+ .join(';');
+ expect(processed).toBe(e2.style.cssText);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '\n \n\n\n\n\n\n\n',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/css-style-attribute-modifications.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/css-style-attribute-modifications.html.ts
new file mode 100644
index 0000000..3e6305a
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/css-style-attribute-modifications.html.ts
@@ -0,0 +1,22 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ "Mutating the style declaration doesn't remove the style attribute",
+ ({ document }) => {
+ let el = document.getElementById('test')!;
+ el.style.color = '';
+ expect(el.hasAttribute('style')).toBe(true);
+
+ el.removeAttribute('style');
+ expect(el.hasAttribute('style')).toBe(false);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '\n\n\n\n\n
\n\n',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/cssom/cssom-setProperty-shorthand.html.ts b/packages/web-platform-tests/src/wpt/css/cssom/cssom-setProperty-shorthand.html.ts
new file mode 100644
index 0000000..550ed59
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/cssom/cssom-setProperty-shorthand.html.ts
@@ -0,0 +1,133 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup = `
+
+
+ CSSOM: CSSStyleDeclaration (set|remove)PropertyValue sets/removes shorthand properties
+\x20\x20\x20\x20\x20\x20\x20\x20
+\x20\x20\x20\x20\x20\x20\x20\x20
+
+\x20\x20\x20\x20\x20\x20\x20\x20
+
+
+\x20\x20\x20\x20\x20\x20\x20\x20
+\x20\x20\x20\x20\x20\x20\x20\x20
+
+
+
+
+
+
+
+\x20\x20\x20\x20\x20\x20\x20\x20
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ function canSetProperty(element: HTMLElement, propertyName: string, priority: string) {
+ element.style.setProperty(propertyName, 'initial', priority);
+ return element.style.getPropertyValue(propertyName) == 'initial';
+ }
+ function canRemoveProperty(element: HTMLElement, propertyName: string) {
+ element.style.removeProperty(propertyName);
+ return element.style.getPropertyValue(propertyName) != 'initial';
+ }
+ for (const propertyName of [
+ 'font',
+ 'border-top',
+ 'border-right',
+ 'border-bottom',
+ 'border-left',
+ 'border',
+ 'border-color',
+ 'border-style',
+ 'border-width',
+ 'background-position',
+ 'background-repeat',
+ 'border-spacing',
+ 'list-style',
+ 'margin',
+ 'outline',
+ 'padding',
+ 'background',
+ 'overflow',
+ 'border-radius',
+ ]) {
+ context.test(
+ 'shorthand ' + propertyName + ' can be set with setProperty',
+ ({ document }) => {
+ const element = document.createElement('span');
+
+ expect(
+ canSetProperty(element, propertyName, ''),
+ 'can setPropertyValue with shorthand',
+ ).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'shorthand ' + propertyName + ' can be removed with removeProperty',
+ ({ document }) => {
+ const element = document.createElement('span');
+
+ canSetProperty(element, propertyName, '');
+
+ expect(
+ canRemoveProperty(element, propertyName),
+ 'can setPropertyValue with shorthand',
+ ).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'shorthand ' + propertyName + ' can be set with setProperty and priority !important',
+ ({ document }) => {
+ const element = document.createElement('span');
+
+ expect(
+ canSetProperty(element, propertyName, 'important'),
+ 'can setPropertyValue with shorthand',
+ ).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'shorthand ' +
+ propertyName +
+ ' can be removed with removeProperty even when set with !important',
+ ({ document }) => {
+ const element = document.createElement('span');
+
+ canSetProperty(element, propertyName, 'important');
+
+ expect(
+ canRemoveProperty(element, propertyName),
+ 'can setPropertyValue with shorthand',
+ ).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-001.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-001.html.ts
new file mode 100644
index 0000000..df8a01c
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-001.html.ts
@@ -0,0 +1,85 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export const input = ['keyboard'] as const;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Keyboard focus should match :focus-visible',
+ async ({ document, getComputedStyle, step, input }) => {
+ const el = document.getElementById('el')! as HTMLElement;
+ const focused = new Promise((resolve) =>
+ el.addEventListener(
+ 'focus',
+ step(() => {
+ expect(
+ getComputedStyle(el).outlineColor,
+ `outlineColor for ${el.tagName}#${el.id} should be green`,
+ ).toBe('rgb(0, 128, 0)');
+ expect(
+ getComputedStyle(el).backgroundColor,
+ `backgroundColor for ${el.tagName}#${el.id} should NOT be red`,
+ ).not.toBe('rgb(255, 0, 0)');
+
+ let focusVisiblePseudoAll = document.querySelectorAll(':focus-visible');
+ expect(focusVisiblePseudoAll.length, "Only 1 element matches ':focus-visible'").toBe(1);
+ let focusVisiblePseudo = document.querySelector(':focus-visible');
+ expect(el, "${el.tagName}#${el.id} matches ':focus-visible'").toBe(focusVisiblePseudo);
+ resolve();
+ }),
+ ),
+ );
+ el.focus();
+ await input.press('Tab');
+ await focused;
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+
+ CSS Test (Selectors): Keyboard focus enables :focus-visible
+
+
+\x20\x20
+\x20\x20
+\x20\x20
+\x20\x20
+\x20\x20
+
+
+
+ This test checks that using the Tab key to navigate focus to an element triggers :focus-visible matching.
+
+ If the user-agent does not claim to support the :focus-visible pseudo-class then SKIP this test.
+ Use the TAB key on the keyboard to focus the element below that says "Focus me."
+ If the element has a red background, then the test result is FAILURE. If the element has a green outline, then the test result is SUCCESS.
+
+
+ Focus me.
+\x20\x20
+
+
+`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-002.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-002.html.ts
new file mode 100644
index 0000000..4a711d6
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-002.html.ts
@@ -0,0 +1,119 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export const input = ['mouse'] as const;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (let i = 0; i < 14; i++)
+ context.test(
+ `Focus element ${i === 13 ? 'TEXTAREA' : 'INPUT'}#input${i + 1} via mouse should match :focus-visible as it supports keyboard input`,
+ async (fixture) => {
+ const { document } = fixture;
+ const target = document.getElementById('input' + (i + 1))!;
+ const getComputedStyle = fixture.getComputedStyle;
+ const focused = new Promise((resolve) =>
+ target.addEventListener('focus', () => resolve()),
+ );
+ await fixture.input.click(target);
+ await focused;
+ expect(
+ getComputedStyle(target).outlineColor,
+ `outlineColor for ${target.tagName}#${target.id} should be green`,
+ ).toBe('rgb(0, 128, 0)');
+ expect(
+ getComputedStyle(target).backgroundColor,
+ `backgroundColor for ${target.tagName}#${target.id} should NOT be red`,
+ ).not.toBe('rgb(255, 0, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+
+ CSS Test (Selectors): :focus-visible always matches on texty input elements
+
+
+
+\x20\x20
+\x20\x20
+\x20\x20
+\x20\x20
+\x20\x20
+
+
+
+ This test checks that :focus-visible always matches on <input> elements which take text input, regardless of focus mechanism.
+
+ If the user-agent does not claim to support the :focus-visible pseudo-class then SKIP this test.
+ Click each form element below to focus it.
+ If the element has a red background, then the test result is FAILURE. If the element has a green outline, then the test result is SUCCESS.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+\x20\x20
+
+
+`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-003.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-003.html.ts
new file mode 100644
index 0000000..85a2be6
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/focus-visible-003.html.ts
@@ -0,0 +1,127 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export const input = ['mouse'] as const;
+
+const markup = `
+
+
+
+ CSS Test (Selectors): :focus-visible does not match on non-texty inputs
+
+
+\x20\x20
+\x20\x20
+\x20\x20
+\x20\x20
+\x20\x20
+
+
+
+ This test checks that :focus-visible is not triggered by mouse focus on <input> elements which do not take text input.
+
+ If the user-agent does not claim to support the :focus-visible pseudo-class then SKIP this test.
+ Click each element element below to focus it.
+ If the element has a red outline, then the test result is FAILURE. If the element has a green background, then the test result is SUCCESS.
+
+
+
+ Focus me
+
+
+ Focus me
+
+
+ Focus me
+
+
+ Focus me
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Focus me.
+
+
+ Focus me.
+
+
+
+
+
+
+ Focus me.
+
+
+
+ Focus me.
+
+\x20\x20
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ const elements = [
+ ['SPAN', 'el-1'],
+ ['SPAN', 'el-2'],
+ ['SPAN', 'el-3'],
+ ['BUTTON', 'el-4'],
+ ['INPUT', 'el-5'],
+ ['INPUT', 'el-6'],
+ ['INPUT', 'el-7'],
+ ['INPUT', 'el-8'],
+ ['INPUT', 'el-9'],
+ ['INPUT', 'el-10'],
+ ['INPUT', 'el-12'],
+ ['INPUT', 'el-13'],
+ ];
+ for (const [tagName, id] of elements) {
+ context.test(
+ `Focus element ${tagName}#${id} via mouse should NOT match :focus-visible as it does NOT support keyboard input`,
+ async ({ document, input, getComputedStyle }) => {
+ const target = document.getElementById(id!)!;
+ const focused = new Promise((resolve) =>
+ target.addEventListener('focus', () => resolve()),
+ );
+ await input.click(target);
+ await focused;
+ expect(
+ getComputedStyle(target).backgroundColor,
+ `backgroundColor for ${target.tagName}#${target.id} should be lime`,
+ ).toBe('rgb(0, 255, 0)');
+ expect(
+ getComputedStyle(target).outlineColor,
+ `outlineColor for ${target.tagName}#${target.id} should NOT be red`,
+ ).not.toBe('rgb(255, 0, 0)');
+ },
+ { document: { contentType: 'text/html', content: markup } },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/focus-within-focus-move.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/focus-within-focus-move.html.ts
new file mode 100644
index 0000000..210805d
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/focus-within-focus-move.html.ts
@@ -0,0 +1,48 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ "Moving focus from its own focus() call doesn't leave stale :focus-within state",
+ ({ document }) => {
+ let wrapper = document.getElementById('wrapper')!;
+ let outer = document.getElementById('outer')!;
+ let tab = document.getElementById('tab')!;
+ let input = document.getElementById('input')!;
+ let outside = document.getElementById('outside')!;
+
+ input.focus();
+ expect(document.activeElement, 'activeElement after focus').toBe(input);
+ expect(outer.matches(':focus-within'), 'outer matches :focus-within').toBe(true);
+ expect(wrapper.matches(':focus-within'), 'wrapper matches :focus-within').toBe(true);
+ // This ends up shifting to `outside` rather than `tab`.
+ tab.focus();
+ expect(document.activeElement, 'activeElement after trying to focus sibling').toBe(outside);
+ expect(wrapper.matches(':focus-within'), 'wrapper still matches :focus-within').toBe(true);
+ expect(outer.matches(':focus-within'), 'outer no longer matches :focus-within').toBe(false);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+Moving focus from its own focus() call doesn't leave stale :focus-within state
+
+
+
+
+
+
+
+
+`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/focus-within-removal.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/focus-within-removal.html.ts
new file mode 100644
index 0000000..729df2a
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/focus-within-removal.html.ts
@@ -0,0 +1,53 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ "Removing an element from its own focus() call doesn't leave stale :focus-within state",
+ ({ document }) => {
+ let root = document.getElementById('root')!;
+ let container = document.getElementById('container')!;
+ let input = document.querySelector('input')!;
+
+ input.focus();
+ expect(document.activeElement, 'activeElement after focus').toBe(input);
+ expect(container.matches(':focus-within'), 'container matches :focus-within').toBe(true);
+ expect(root.matches(':focus-within'), 'root also matches :focus-within').toBe(true);
+ // This fires from within the next focus() call
+ input.addEventListener('focusout', () => {
+ root.innerHTML = '';
+ });
+ // container is focusable, but gets removed before we get a chance at focusing it.
+ container.focus();
+ expect(document.activeElement, 'activeElement after trying to focus sibling').toBe(
+ document.body,
+ );
+ expect(container.parentNode, 'container should get removed').toBe(null);
+ expect(container.matches(':focus-within'), 'container no longer matches :focus-within').toBe(
+ false,
+ );
+ expect(root.matches(':focus-within'), 'root no longer matches :focus-within').toBe(false);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+Removing an element from its own focus() call doesn't leave stale :focus-within state
+
+
+
+
+
+
+
+
+`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/focus-within.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/focus-within.html.ts
new file mode 100644
index 0000000..016ebf5
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/focus-within.html.ts
@@ -0,0 +1,83 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup1 = `
+
+
+:focus-within pseudo
+
+
+
+
+
+
+ Focus here:
+ focus me
+
+
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ ':focus-within applies to focused element',
+ async ({ document, cleanup }) => {
+ const container = document.getElementById('container')!;
+ const button = document.getElementById('button')!;
+ async function waitForWindowFocus() {
+ // Wait for the test window to gain focus before testing synchronous focus changes.
+ const sentinel = document.createElement('button');
+ document.body.append(sentinel);
+ cleanup(() => sentinel.remove());
+ await new Promise((resolve) => {
+ sentinel.addEventListener('focus', resolve, { once: true });
+ sentinel.focus();
+ });
+ sentinel.blur();
+ }
+
+ await waitForWindowFocus();
+ cleanup(() => button.blur());
+ button.focus();
+ expect(button.matches(':focus')).toBe(true);
+ expect(button.matches(':focus-within')).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ ':focus-within applies to ancestor',
+ async ({ document, cleanup }) => {
+ const container = document.getElementById('container')!;
+ const button = document.getElementById('button')!;
+ async function waitForWindowFocus() {
+ // Wait for the test window to gain focus before testing synchronous focus changes.
+ const sentinel = document.createElement('button');
+ document.body.append(sentinel);
+ cleanup(() => sentinel.remove());
+ await new Promise((resolve) => {
+ sentinel.addEventListener('focus', resolve, { once: true });
+ sentinel.focus();
+ });
+ sentinel.blur();
+ }
+
+ await waitForWindowFocus();
+ cleanup(() => button.blur());
+ button.focus();
+ expect(container.matches(':focus')).toBe(false);
+ expect(container.matches(':focus-within')).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/has-basic.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/has-basic.html.ts
new file mode 100644
index 0000000..9a570b5
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/has-basic.html.ts
@@ -0,0 +1,437 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function prepare({ document, connect }: DOMFixture, markup: string): Document {
+ const root = document.createElement('div');
+ root.innerHTML = markup;
+ connect(root);
+ return document;
+ }
+
+ const { expect } = context.runner;
+
+ // 4.3.7 from https://drafts.csswg.org/css-syntax/#consume-escaped-code-point
+
+ // - escape hex digit
+
+ // - hex digit special replacement
+ // 1. zero points
+
+ // 2. surrogate points
+
+ // 3. out of range points
+
+ // - escape EOF
+
+ // - escape anythong else
+
+ // non edge cases
+
+ // ident tests case from CSS tests of chromium source: https://goo.gl/3Cxdov
+
+ context.test('WPT has-basic.html: :has(#a) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const actual = Array.from(main.querySelectorAll(':has(#a)'));
+ const expected: Element[] = [];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT has-basic.html: :has(.ancestor) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const actual = Array.from(main.querySelectorAll(':has(.ancestor)'));
+ const expected: Element[] = [a];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT has-basic.html: :has(.target) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const b = document.getElementById('b')!;
+ const f = document.getElementById('f')!;
+ const h = document.getElementById('h')!;
+ const actual = Array.from(main.querySelectorAll(':has(.target)'));
+ const expected: Element[] = [a, b, f, h];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT has-basic.html: :has(.descendant) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const b = document.getElementById('b')!;
+ const c = document.getElementById('c')!;
+ const f = document.getElementById('f')!;
+ const h = document.getElementById('h')!;
+ const j = document.getElementById('j')!;
+ const actual = Array.from(main.querySelectorAll(':has(.descendant)'));
+ const expected: Element[] = [a, b, c, f, h, j];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT has-basic.html: .parent:has(.target) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const b = document.getElementById('b')!;
+ const f = document.getElementById('f')!;
+ const h = document.getElementById('h')!;
+ const actual = Array.from(main.querySelectorAll('.parent:has(.target)'));
+ const expected: Element[] = [b, f, h];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test(
+ 'WPT has-basic.html: :has(.sibling ~ .target) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const b = document.getElementById('b')!;
+ const actual = Array.from(main.querySelectorAll(':has(.sibling ~ .target)'));
+ const expected: Element[] = [a, b];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT has-basic.html: .parent:has(.sibling ~ .target) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const b = document.getElementById('b')!;
+ const actual = Array.from(main.querySelectorAll('.parent:has(.sibling ~ .target)'));
+ const expected: Element[] = [b];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT has-basic.html: :has(:is(.target ~ .sibling .descendant)) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const h = document.getElementById('h')!;
+ const j = document.getElementById('j')!;
+ const actual = Array.from(main.querySelectorAll(':has(:is(.target ~ .sibling .descendant))'));
+ const expected: Element[] = [a, h, j];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT has-basic.html: .parent:has(:is(.target ~ .sibling .descendant)) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const h = document.getElementById('h')!;
+ const actual = Array.from(
+ main.querySelectorAll('.parent:has(:is(.target ~ .sibling .descendant))'),
+ );
+ const expected: Element[] = [h];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT has-basic.html: .sibling:has(.descendant) ~ .target matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const e = document.getElementById('e')!;
+ const actual = Array.from(main.querySelectorAll('.sibling:has(.descendant) ~ .target'));
+ const expected: Element[] = [e];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test('WPT has-basic.html: :has(> .parent) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const actual = Array.from(main.querySelectorAll(':has(> .parent)'));
+ const expected: Element[] = [a];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT has-basic.html: :has(> .target) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const b = document.getElementById('b')!;
+ const f = document.getElementById('f')!;
+ const h = document.getElementById('h')!;
+ const actual = Array.from(main.querySelectorAll(':has(> .target)'));
+ const expected: Element[] = [b, f, h];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test(
+ 'WPT has-basic.html: :has(> .parent, > .target) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const b = document.getElementById('b')!;
+ const f = document.getElementById('f')!;
+ const h = document.getElementById('h')!;
+ const actual = Array.from(main.querySelectorAll(':has(> .parent, > .target)'));
+ const expected: Element[] = [a, b, f, h];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test('WPT has-basic.html: :has(+ #h) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll(':has(+ #h)'));
+ const expected: Element[] = [f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT has-basic.html: .parent:has(~ #h) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const b = document.getElementById('b')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll('.parent:has(~ #h)'));
+ const expected: Element[] = [b, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test(
+ 'WPT has-basic.html: .sibling:has(.descendant) matches expected element',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const c = document.getElementById('c')!;
+ expect(main.querySelector('.sibling:has(.descendant)')).toBe(c);
+ },
+ );
+
+ context.test(
+ 'WPT has-basic.html: closest(.ancestor:has(.descendant)) returns expected element',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const h = document.getElementById('h')!;
+ const k = document.getElementById('k')!;
+ expect(k.closest('.ancestor:has(.descendant)')).toBe(h);
+ },
+ );
+
+ context.test(
+ 'WPT has-basic.html: :has(.target ~ .sibling .descendant) matches expectedly',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n ',
+ );
+ const h = document.getElementById('h')!;
+ expect(h.matches(':has(.target ~ .sibling .descendant)')).toBe(true);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/invalidation/input-pseudo-classes-in-has.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/invalidation/input-pseudo-classes-in-has.html.ts
new file mode 100644
index 0000000..e6abf80
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/invalidation/input-pseudo-classes-in-has.html.ts
@@ -0,0 +1,325 @@
+import type { CompatibilityContext } from '../../../../shared/context.ts';
+
+const markup = `
+
+CSS Selectors Invalidation: input pseudo classes in :has() argument
+
+
+
+
+
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ ':checked & :indeterminate invalidation on ',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ let input: HTMLInputElement | null = null;
+ cleanup(() => {
+ if (input) {
+ // Need to put the input back for the rest of the tests.
+ subject.prepend(input);
+ }
+ checkme.checked = false;
+ });
+ checkme.checked = false;
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ checkme.checked = true;
+ expect(getComputedStyle(subject).color, 'ancestor should be green').toBe('rgb(0, 128, 0)');
+ checkme.indeterminate = true;
+ expect(getComputedStyle(subject).color, 'ancestor should be yellowgreen').toBe(
+ 'rgb(154, 205, 50)',
+ );
+ input = checkme;
+ checkme.remove();
+ input.indeterminate = false;
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+
+ subject.prepend(input);
+ input = null;
+ checkme.checked = true;
+ expect(getComputedStyle(subject).color, 'ancestor should be green').toBe('rgb(0, 128, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':indeterminate invalidation on ',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ progress.setAttribute('value', '50');
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ progress.removeAttribute('value');
+ expect(getComputedStyle(subject).color, 'ancestor should be orange').toBe('rgb(255, 165, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':disabled invalidation',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ checkme.disabled = false;
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ checkme.disabled = true;
+ expect(getComputedStyle(subject).color, 'ancestor should be blue').toBe('rgb(0, 0, 255)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':read-only invalidation',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ textinput.readOnly = false;
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ textinput.readOnly = true;
+ expect(getComputedStyle(subject).color, 'ancestor should be skyblue').toBe(
+ 'rgb(135, 206, 235)',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':valid invalidation',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ textinput.value = '';
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ textinput.value = 'text input';
+ expect(getComputedStyle(subject).color, 'ancestor should be lightgreen').toBe(
+ 'rgb(144, 238, 144)',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':default invalidation with input[type=radio]',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ radioinput.removeAttribute('type');
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ radioinput.type = 'radio';
+ expect(getComputedStyle(subject).color, 'ancestor should be lightblue').toBe(
+ 'rgb(173, 216, 230)',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':required invalidation',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ numberinput.required = false;
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ numberinput.required = true;
+ expect(getComputedStyle(subject).color, 'ancestor should be pink').toBe('rgb(255, 192, 203)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':out-of-range invalidation',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ numberinput.value = 5 as unknown as string;
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ numberinput.value = 12 as unknown as string;
+ expect(getComputedStyle(subject).color, 'ancestor should be darkgreen').toBe(
+ 'rgb(0, 100, 0)',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':placeholder-shown invalidation',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ textinput.removeAttribute('placeholder');
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ textinput.placeholder = 'placeholder text';
+ expect(getComputedStyle(subject).color, 'ancestor should be navy').toBe('rgb(0, 0, 128)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ ':default invalidation with input[type=checkbox] and assignment to .checked',
+ ({ document, getComputedStyle, cleanup }) => {
+ const subject = document.getElementById('subject') as HTMLElement;
+ const checkme = document.getElementById('checkme') as HTMLInputElement;
+ const textinput = document.getElementById('textinput') as HTMLInputElement;
+ const radioinput = document.getElementById('radioinput') as HTMLInputElement;
+ const numberinput = document.getElementById('numberinput') as HTMLInputElement;
+ const progress = document.getElementById('progress') as HTMLProgressElement;
+ const checkboxinput = document.getElementById('checkboxinput') as HTMLInputElement;
+
+ cleanup(() => {
+ checkboxinput.checked = false;
+ checkboxinput.removeAttribute('checked');
+ });
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ checkboxinput.checked = true;
+ expect(getComputedStyle(subject).color, 'ancestor should still be black').toBe(
+ 'rgb(0, 0, 0)',
+ );
+ checkboxinput.setAttribute('checked', '');
+ expect(getComputedStyle(subject).color, 'ancestor should be purple').toBe('rgb(128, 0, 128)');
+ checkboxinput.checked = false;
+ expect(getComputedStyle(subject).color, 'ancestor should still be purple').toBe(
+ 'rgb(128, 0, 128)',
+ );
+ checkboxinput.removeAttribute('checked');
+ expect(getComputedStyle(subject).color, 'ancestor should be black').toBe('rgb(0, 0, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/invalidation/user-action-pseudo-classes-in-has.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/invalidation/user-action-pseudo-classes-in-has.html.ts
new file mode 100644
index 0000000..f8919c5
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/invalidation/user-action-pseudo-classes-in-has.html.ts
@@ -0,0 +1,145 @@
+import type { CompatibilityContext } from '../../../../shared/context.ts';
+
+export const input = ['keyboard', 'mouse'] as const;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'user-action pseudo classes in :has() argument',
+ async ({ document, getComputedStyle, input }) => {
+ const subject1 = document.getElementById('subject1')!;
+ const subject2 = document.getElementById('subject2')!;
+ const subject3 = document.getElementById('subject3')!;
+ const subject4 = document.getElementById('subject4')!;
+ const hoverme = document.getElementById('hoverme')!;
+ const unhoverme = document.getElementById('unhoverme')!;
+ const focusme1 = document.getElementById('focusme1')!;
+ const focusme2 = document.getElementById('focusme2')!;
+ const focusme3 = document.getElementById('focusme3')!;
+ const focusme4 = document.getElementById('focusme4')!;
+
+ expect(getComputedStyle(subject1).color, 'subject1 initially black').toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(subject2).color, 'subject3 initially black').toBe('rgb(0, 0, 0)');
+
+ await input.move(hoverme);
+ expect(getComputedStyle(subject1).color, 'subject1 should be blue').toBe('rgb(0, 0, 255)');
+ expect(getComputedStyle(subject3).color, 'subject3 should be navy').toBe('rgb(0, 0, 128)');
+
+ await input.move(unhoverme);
+ expect(getComputedStyle(subject1).color, 'subject1 should be back to black').toBe(
+ 'rgb(0, 0, 0)',
+ );
+ expect(getComputedStyle(subject3).color, 'subject3 should be back to black').toBe(
+ 'rgb(0, 0, 0)',
+ );
+
+ await input.move(hoverme);
+ await input.down();
+ expect(getComputedStyle(subject1).color, 'subject1 should be skyblue').toBe(
+ 'rgb(135, 206, 235)',
+ );
+ expect(getComputedStyle(subject3).color, 'subject3 should be lightblue').toBe(
+ 'rgb(173, 216, 230)',
+ );
+
+ // Clean up `pointerDown` from above. We want to test invalidation from
+ // `:hover:active` to `:hover`, but there's no guarantee that pointer
+ // state will stay the same between actions.
+ await input.up();
+ await input.move(unhoverme);
+
+ // Perform the entire activation chain again, then perform `pointerUp`.
+ await input.move(hoverme);
+ await input.down();
+ await input.up();
+ expect(getComputedStyle(subject1).color, 'subject1 should be blue').toBe('rgb(0, 0, 255)');
+ expect(getComputedStyle(subject3).color, 'subject3 should be navy').toBe('rgb(0, 0, 128)');
+
+ await input.move(focusme1);
+ await input.down();
+ await input.up();
+ expect(getComputedStyle(subject1).color, 'subject1 should be green').toBe('rgb(0, 128, 0)');
+ expect(getComputedStyle(subject3).color, 'subject3 should be darkgreen').toBe(
+ 'rgb(0, 100, 0)',
+ );
+
+ document.body.focus();
+ await input.press('Tab');
+ expect(getComputedStyle(subject1).color, 'subject1 should be yellowgreen').toBe(
+ 'rgb(154, 205, 50)',
+ );
+ expect(getComputedStyle(subject3).color, 'subject3 should be greenyellow').toBe(
+ 'rgb(173, 255, 47)',
+ );
+
+ document.body.focus();
+ await input.press('Tab');
+ expect(getComputedStyle(subject1).color, 'subject1 should be lightgreen').toBe(
+ 'rgb(144, 238, 144)',
+ );
+ expect(getComputedStyle(subject3).color, 'subject3 should be violet').toBe(
+ 'rgb(238, 130, 238)',
+ );
+
+ focusme3.remove();
+ expect(getComputedStyle(subject1).color, 'subject1 should be black').toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(subject3).color, 'subject3 should be black').toBe('rgb(0, 0, 0)');
+
+ document.body.focus();
+ await input.press('Tab');
+ expect(getComputedStyle(subject2).color, 'subject2 should be green').toBe('rgb(0, 128, 0)');
+ expect(getComputedStyle(subject4).color, 'subject4 should be darkgreen').toBe(
+ 'rgb(0, 100, 0)',
+ );
+
+ focusme4.remove();
+ expect(getComputedStyle(subject2).color, 'subject2 should be black').toBe('rgb(0, 0, 0)');
+ expect(getComputedStyle(subject4).color, 'subject4 should be black').toBe('rgb(0, 0, 0)');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+CSS Selectors Invalidation: user-action pseudo classes in :has() argument
+
+
+
+
+
+
+
+
+
+
+
No :hover
+
Hover and click me
+
Focus me
+
Focus me
+
+
+
+
+
+
+`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/css/selectors/is-where-basic.html.ts b/packages/web-platform-tests/src/wpt/css/selectors/is-where-basic.html.ts
new file mode 100644
index 0000000..b82cb2e
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/css/selectors/is-where-basic.html.ts
@@ -0,0 +1,403 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function prepare({ document, connect }: DOMFixture, markup: string): Document {
+ const root = document.createElement('div');
+ root.innerHTML = markup;
+ connect(root);
+ return document;
+ }
+
+ const { expect } = context.runner;
+
+ // 4.3.7 from https://drafts.csswg.org/css-syntax/#consume-escaped-code-point
+
+ // - escape hex digit
+
+ // - hex digit special replacement
+ // 1. zero points
+
+ // 2. surrogate points
+
+ // 3. out of range points
+
+ // - escape EOF
+
+ // - escape anythong else
+
+ // non edge cases
+
+ // ident tests case from CSS tests of chromium source: https://goo.gl/3Cxdov
+
+ context.test('WPT is-where-basic.html: :is() matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const actual = Array.from(main.querySelectorAll(':is()'));
+ const expected: Element[] = [];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT is-where-basic.html: :is(#a) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const actual = Array.from(main.querySelectorAll(':is(#a)'));
+ const expected: Element[] = [a];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT is-where-basic.html: :is(#a, #f) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll(':is(#a, #f)'));
+ const expected: Element[] = [a, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test(
+ 'WPT is-where-basic.html: :is(#a, #c) :where(#a #d, #c #f) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const d = document.getElementById('d')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll(':is(#a, #c) :where(#a #d, #c #f)'));
+ const expected: Element[] = [d, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT is-where-basic.html: #c > :is(#c > #f) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll('#c > :is(#c > #f)'));
+ const expected: Element[] = [f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT is-where-basic.html: #c > :is(#b > #f) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const actual = Array.from(main.querySelectorAll('#c > :is(#b > #f)'));
+ const expected: Element[] = [];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test('WPT is-where-basic.html: #a div:is(#d) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const d = document.getElementById('d')!;
+ const actual = Array.from(main.querySelectorAll('#a div:is(#d)'));
+ const expected: Element[] = [d];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT is-where-basic.html: :is(div) > div matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const d = document.getElementById('d')!;
+ const e = document.getElementById('e')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll(':is(div) > div'));
+ const expected: Element[] = [d, e, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT is-where-basic.html: :is(*) > div matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const d = document.getElementById('d')!;
+ const b = document.getElementById('b')!;
+ const e = document.getElementById('e')!;
+ const c = document.getElementById('c')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll(':is(*) > div'));
+ const expected: Element[] = [a, b, c, d, e, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test('WPT is-where-basic.html: :is(*) div matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const d = document.getElementById('d')!;
+ const b = document.getElementById('b')!;
+ const e = document.getElementById('e')!;
+ const c = document.getElementById('c')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll(':is(*) div'));
+ const expected: Element[] = [a, b, c, d, e, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test(
+ 'WPT is-where-basic.html: div > :where(#e, #f) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const e = document.getElementById('e')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll('div > :where(#e, #f)'));
+ const expected: Element[] = [e, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test('WPT is-where-basic.html: div > :where(*) matches expected elements', (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const d = document.getElementById('d')!;
+ const e = document.getElementById('e')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll('div > :where(*)'));
+ const expected: Element[] = [d, e, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ });
+
+ context.test(
+ 'WPT is-where-basic.html: :is(*) > :where(*) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const a = document.getElementById('a')!;
+ const d = document.getElementById('d')!;
+ const b = document.getElementById('b')!;
+ const e = document.getElementById('e')!;
+ const c = document.getElementById('c')!;
+ const f = document.getElementById('f')!;
+ const actual = Array.from(main.querySelectorAll(':is(*) > :where(*)'));
+ const expected: Element[] = [a, b, c, d, e, f];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT is-where-basic.html: :is(#a + #b) + :is(#c) matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const c = document.getElementById('c')!;
+ const actual = Array.from(main.querySelectorAll(':is(#a + #b) + :is(#c)'));
+ const expected: Element[] = [c];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+
+ context.test(
+ 'WPT is-where-basic.html: :is(#a, #b) + div matches expected elements',
+ (fixture) => {
+ const document = prepare(
+ fixture,
+ '\n \n \n \n ',
+ );
+ const main = document.getElementById('main')!;
+ const b = document.getElementById('b')!;
+ const c = document.getElementById('c')!;
+ const actual = Array.from(main.querySelectorAll(':is(#a, #b) + div'));
+ const expected: Element[] = [b, c];
+ expect(
+ actual
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ ).toBe(
+ expected
+ .map((element) => element.id)
+ .toSorted()
+ .join(),
+ );
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/CustomElementRegistry.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/CustomElementRegistry.html.ts
new file mode 100644
index 0000000..d85c9ed
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/CustomElementRegistry.html.ts
@@ -0,0 +1,1453 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'CustomElementRegistry interface must have define as a method',
+ ({ customElements, CustomElementRegistry }) => {
+ expect(
+ 'define' in CustomElementRegistry.prototype,
+ '"define" exists on CustomElementRegistry.prototype',
+ ).toBe(true);
+ expect('define' in customElements, '"define" exists on window.customElements').toBe(true);
+ },
+ );
+ context.test(
+ 'customElements.define must throw when the element interface is not a constructor',
+ ({ customElements, TypeError }) => {
+ expect(function () {
+ customElements.define('badname', 1 as unknown as CustomElementConstructor);
+ }, 'customElements.define must throw a TypeError when the element interface is a number').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ expect(function () {
+ customElements.define('badname', '123' as unknown as CustomElementConstructor);
+ }, 'customElements.define must throw a TypeError when the element interface is a string').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ expect(function () {
+ customElements.define('badname', {} as unknown as CustomElementConstructor);
+ }, 'customElements.define must throw a TypeError when the element interface is an object').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ expect(function () {
+ customElements.define('badname', [] as unknown as CustomElementConstructor);
+ }, 'customElements.define must throw a TypeError when the element interface is an array').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ },
+ );
+ context.test(
+ 'customElements.define must not throw the constructor is HTMLElement',
+ ({ HTMLElement, customElements }) => {
+ customElements.define('custom-html-element', HTMLElement);
+ },
+ );
+ context.test(
+ 'customElements.define must throw with an invalid name',
+ ({ HTMLElement, customElements, DOMException }) => {
+ class MyCustomElement extends HTMLElement {}
+
+ expect(function () {
+ customElements.define(null as unknown as string, MyCustomElement);
+ }, 'customElements.define must throw a SyntaxError if the tag name is null').toThrow(
+ expect.objectContaining({
+ name: 'SyntaxError',
+ code: 12,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ customElements.define('', MyCustomElement);
+ }, 'customElements.define must throw a SyntaxError if the tag name is empty').toThrow(
+ expect.objectContaining({
+ name: 'SyntaxError',
+ code: 12,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ customElements.define('abc', MyCustomElement);
+ }, 'customElements.define must throw a SyntaxError if the tag name does not contain "-"').toThrow(
+ expect.objectContaining({
+ name: 'SyntaxError',
+ code: 12,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ customElements.define('a-Bc', MyCustomElement);
+ }, 'customElements.define must throw a SyntaxError if the tag name contains an upper case letter').toThrow(
+ expect.objectContaining({
+ name: 'SyntaxError',
+ code: 12,
+ constructor: DOMException,
+ }),
+ );
+
+ var builtinTagNames = [
+ 'annotation-xml',
+ 'color-profile',
+ 'font-face',
+ 'font-face-src',
+ 'font-face-uri',
+ 'font-face-format',
+ 'font-face-name',
+ 'missing-glyph',
+ ];
+
+ for (var tagName of builtinTagNames) {
+ expect(
+ function () {
+ customElements.define(tagName, MyCustomElement);
+ },
+ 'customElements.define must throw a SyntaxError if the tag name is "' + tagName + '"',
+ ).toThrow(
+ expect.objectContaining({
+ name: 'SyntaxError',
+ code: 12,
+ constructor: DOMException,
+ }),
+ );
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must throw when there is already a custom element of the same name',
+ ({ HTMLElement, customElements, DOMException }) => {
+ class SomeCustomElement extends HTMLElement {}
+
+ var calls: PropertyKey[] = [];
+ var OtherCustomElement = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ calls.push(name);
+ return Reflect.get(target, name);
+ },
+ });
+
+ customElements.define('some-custom-element', SomeCustomElement);
+ expect(function () {
+ customElements.define('some-custom-element', OtherCustomElement);
+ }, 'customElements.define must throw a NotSupportedError if the specified tag name is already used').toThrow(
+ expect.objectContaining({
+ name: 'NotSupportedError',
+ code: 9,
+ constructor: DOMException,
+ }),
+ );
+ {
+ const actualArray = calls;
+ const expectedArray: never[] = [];
+ expect(
+ actualArray.length,
+ 'customElements.define must validate the custom element name before getting the prototype of the constructor',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must validate the custom element name before getting the prototype of the constructor',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must throw a NotSupportedError when there is already a custom element with the same class',
+ ({ HTMLElement, customElements, DOMException }) => {
+ class AnotherCustomElement extends HTMLElement {}
+
+ customElements.define('another-custom-element', AnotherCustomElement);
+ expect(function () {
+ customElements.define('some-other-element', AnotherCustomElement);
+ }, 'customElements.define must throw a NotSupportedError if the specified class already defines an element').toThrow(
+ expect.objectContaining({
+ name: 'NotSupportedError',
+ code: 9,
+ constructor: DOMException,
+ }),
+ );
+ },
+ );
+ context.test(
+ 'customElements.define must throw a NotSupportedError when element definition is running flag is set',
+ ({ HTMLElement, customElements, DOMException }) => {
+ var outerCalls: PropertyKey[] = [];
+ var OuterCustomElement = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ outerCalls.push(name);
+ customElements.define('inner-custom-element', InnerCustomElement);
+ return Reflect.get(target, name);
+ },
+ });
+ var innerCalls: PropertyKey[] = [];
+ var InnerCustomElement = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ outerCalls.push(name);
+ return Reflect.get(target, name);
+ },
+ });
+
+ expect(function () {
+ customElements.define('outer-custom-element', OuterCustomElement);
+ }, 'customElements.define must throw a NotSupportedError if the specified class already defines an element').toThrow(
+ expect.objectContaining({
+ name: 'NotSupportedError',
+ code: 9,
+ constructor: DOMException,
+ }),
+ );
+ {
+ const actualArray = outerCalls;
+ const expectedArray = ['prototype'];
+ expect(actualArray.length, 'customElements.define must get "prototype"').toBe(
+ expectedArray.length,
+ );
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index], 'customElements.define must get "prototype"').toBe(
+ expectedArray[index],
+ );
+ }
+ {
+ const actualArray = innerCalls;
+ const expectedArray: never[] = [];
+ expect(
+ actualArray.length,
+ 'customElements.define must throw a NotSupportedError when element definition is running flag is set' +
+ ' before getting the prototype of the constructor',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must throw a NotSupportedError when element definition is running flag is set' +
+ ' before getting the prototype of the constructor',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must check IsConstructor on the constructor before checking the element definition is running flag',
+ ({ HTMLElement, customElements, TypeError }) => {
+ var calls: PropertyKey[] = [];
+ var ElementWithBadInnerConstructor = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ calls.push(name);
+ customElements.define('inner-custom-element', 1 as unknown as CustomElementConstructor);
+ return Reflect.get(target, name);
+ },
+ });
+
+ expect(function () {
+ customElements.define(
+ 'element-with-bad-inner-constructor',
+ ElementWithBadInnerConstructor,
+ );
+ }, 'customElements.define must throw a NotSupportedError if IsConstructor(constructor) is false').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype'];
+ expect(actualArray.length, 'customElements.define must get "prototype"').toBe(
+ expectedArray.length,
+ );
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index], 'customElements.define must get "prototype"').toBe(
+ expectedArray[index],
+ );
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must validate the custom element name before checking the element definition is running flag',
+ ({ HTMLElement, customElements, DOMException }) => {
+ var calls: PropertyKey[] = [];
+ var ElementWithBadInnerName = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ calls.push(name);
+ customElements.define('badname', class extends HTMLElement {});
+ return Reflect.get(target, name);
+ },
+ });
+
+ expect(function () {
+ customElements.define('element-with-bad-inner-name', ElementWithBadInnerName);
+ }, 'customElements.define must throw a SyntaxError if the specified name is not a valid custom element name').toThrow(
+ expect.objectContaining({
+ name: 'SyntaxError',
+ code: 12,
+ constructor: DOMException,
+ }),
+ );
+
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype'];
+ expect(actualArray.length, 'customElements.define must get "prototype"').toBe(
+ expectedArray.length,
+ );
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index], 'customElements.define must get "prototype"').toBe(
+ expectedArray[index],
+ );
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must define an instantiatable custom element',
+ ({ HTMLElement, customElements }) => {
+ class MyCustomElement extends HTMLElement {}
+ customElements.define('my-custom-element', MyCustomElement);
+
+ var instance = new MyCustomElement();
+ expect(
+ instance instanceof MyCustomElement,
+ 'An instance of a custom HTML element be an instance of the associated interface',
+ ).toBe(true);
+
+ expect(
+ instance instanceof HTMLElement,
+ 'An instance of a custom HTML element must inherit from HTMLElement',
+ ).toBe(true);
+
+ expect(
+ instance.localName,
+ 'An instance of a custom element must use the associated tag name',
+ ).toBe('my-custom-element');
+
+ expect(instance.namespaceURI, 'A custom element HTML must use HTML namespace').toBe(
+ 'http://www.w3.org/1999/xhtml',
+ );
+ },
+ );
+ context.test(
+ 'CustomElementRegistry interface must have get as a method',
+ ({ customElements, CustomElementRegistry }) => {
+ expect(
+ 'get' in CustomElementRegistry.prototype,
+ '"get" exists on CustomElementRegistry.prototype',
+ ).toBe(true);
+ expect('get' in customElements, '"get" exists on window.customElements').toBe(true);
+ },
+ );
+ context.test(
+ 'customElements.get must return undefined when the registry does not contain an entry with the given name',
+ ({ customElements }) => {
+ expect(customElements.get('a-b')).toBe(undefined);
+ },
+ );
+ context.test(
+ 'customElements.get must return undefined when the registry does not contain an entry with the given name even if the name was not a valid custom element name',
+ ({ customElements }) => {
+ expect(customElements.get('html')).toBe(undefined);
+ expect(customElements.get('span')).toBe(undefined);
+ expect(customElements.get('div')).toBe(undefined);
+ expect(customElements.get('g')).toBe(undefined);
+ expect(customElements.get('ab')).toBe(undefined);
+ },
+ );
+ context.test(
+ 'customElements.get return the constructor of the entry with the given name when there is a matching entry.',
+ ({ HTMLElement, customElements }) => {
+ expect(customElements.get('existing-custom-element')).toBe(undefined);
+ class ExistingCustomElement extends HTMLElement {}
+ customElements.define('existing-custom-element', ExistingCustomElement);
+ expect(customElements.get('existing-custom-element')).toBe(ExistingCustomElement);
+ },
+ );
+ context.test(
+ 'customElements.whenDefined must return a promise for a valid custom element name',
+ ({ customElements, Promise }) => {
+ expect(customElements.whenDefined('some-name') instanceof Promise).toBe(true);
+ },
+ );
+ context.test(
+ 'customElements.whenDefined must return the same promise each time invoked for a valid custom element name which has not been defined',
+ ({ customElements }) => {
+ expect(customElements.whenDefined('some-name')).toBe(
+ customElements.whenDefined('some-name'),
+ );
+ },
+ );
+ context.test(
+ 'customElements.whenDefined must return an unresolved promise when the registry does not contain the entry with the given name',
+ async ({ customElements, Promise }) => {
+ var resolved = false;
+ var rejected = false;
+ customElements.whenDefined('a-b').then(
+ function () {
+ resolved = true;
+ },
+ function () {
+ rejected = true;
+ },
+ );
+ return Promise.resolve().then(function () {
+ expect(
+ resolved,
+ 'The promise returned by "whenDefined" must not be resolved until a custom element is defined',
+ ).toBe(false);
+ expect(
+ rejected,
+ 'The promise returned by "whenDefined" must not be rejected until a custom element is defined',
+ ).toBe(false);
+ });
+ },
+ );
+ context.test(
+ 'customElements.whenDefined must return a rejected promise when the given name is not a valid custom element name',
+ async ({ customElements, Promise }) => {
+ var promise = customElements.whenDefined('badname') as Promise & {
+ resolved?: unknown;
+ rejected?: unknown;
+ };
+ promise.then(
+ function (value) {
+ promise.resolved = value;
+ },
+ function (value) {
+ promise.rejected = value;
+ },
+ );
+
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask',
+ ).toBe(false);
+
+ return Promise.resolve().then(function () {
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must be resolved when a custom element is defined',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected when a custom element is defined',
+ ).toBe(true);
+ });
+ },
+ );
+ context.test(
+ 'customElements.whenDefined must return a resolved promise when the registry contains the entry with the given name',
+ async ({ HTMLElement, customElements, Promise }) => {
+ class PreexistingCustomElement extends HTMLElement {}
+ customElements.define('preexisting-custom-element', PreexistingCustomElement);
+
+ var promise = customElements.whenDefined(
+ 'preexisting-custom-element',
+ ) as Promise & { resolved?: unknown; rejected?: unknown };
+ promise.then(
+ function (value) {
+ promise.resolved = value;
+ },
+ function (value) {
+ promise.rejected = value;
+ },
+ );
+
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask',
+ ).toBe(false);
+
+ return Promise.resolve().then(function () {
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must be resolved when a custom element is defined',
+ ).toBe(true);
+ expect(
+ promise.resolved,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined',
+ ).toBe(PreexistingCustomElement);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected when a custom element is defined',
+ ).toBe(false);
+ });
+ },
+ );
+ context.test(
+ 'customElements.whenDefined must return a new resolved promise each time invoked when the registry contains the entry with the given name',
+ async ({ HTMLElement, customElements, Promise }) => {
+ class AnotherExistingCustomElement extends HTMLElement {}
+ customElements.define('another-existing-custom-element', AnotherExistingCustomElement);
+
+ var promise1 = customElements.whenDefined(
+ 'another-existing-custom-element',
+ ) as Promise & { resolved?: unknown; rejected?: unknown };
+ var promise2 = customElements.whenDefined(
+ 'another-existing-custom-element',
+ ) as Promise & { resolved?: unknown; rejected?: unknown };
+ promise1.then(
+ function (value) {
+ promise1.resolved = value;
+ },
+ function (value) {
+ promise1.rejected = value;
+ },
+ );
+ promise2.then(
+ function (value) {
+ promise2.resolved = value;
+ },
+ function (value) {
+ promise2.rejected = value;
+ },
+ );
+
+ expect(promise1).not.toBe(promise2);
+ expect(
+ 'resolved' in promise1,
+ 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'resolved' in promise2,
+ 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise1,
+ 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise2,
+ 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask',
+ ).toBe(false);
+
+ return Promise.resolve().then(function () {
+ expect(
+ 'resolved' in promise1,
+ 'The promise returned by "whenDefined" must be resolved when a custom element is defined',
+ ).toBe(true);
+ expect(
+ promise1.resolved,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined',
+ ).toBe(AnotherExistingCustomElement);
+ expect(
+ 'rejected' in promise1,
+ 'The promise returned by "whenDefined" must not be rejected when a custom element is defined',
+ ).toBe(false);
+
+ expect(
+ 'resolved' in promise2,
+ 'The promise returned by "whenDefined" must be resolved when a custom element is defined',
+ ).toBe(true);
+ expect(
+ promise2.resolved,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined',
+ ).toBe(AnotherExistingCustomElement);
+ expect(
+ 'rejected' in promise2,
+ 'The promise returned by "whenDefined" must not be rejected when a custom element is defined',
+ ).toBe(false);
+ });
+ },
+ );
+ context.test(
+ 'A promise returned by customElements.whenDefined must be resolved by "define"',
+ async ({ HTMLElement, customElements, Promise }) => {
+ class ElementDefinedAfterWhenDefined extends HTMLElement {}
+ var promise = customElements.whenDefined(
+ 'element-defined-after-whendefined',
+ ) as Promise & { resolved?: unknown; rejected?: unknown };
+ promise.then(
+ function (value) {
+ promise.resolved = value;
+ },
+ function (value) {
+ promise.rejected = value;
+ },
+ );
+
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask',
+ ).toBe(false);
+
+ var promiseAfterDefine: Promise & {
+ resolved?: unknown;
+ rejected?: unknown;
+ };
+ return Promise.resolve()
+ .then(function () {
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must not be resolved until the element is defined',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected until the element is defined',
+ ).toBe(false);
+ expect(
+ customElements.whenDefined('element-defined-after-whendefined'),
+ '"whenDefined" must return the same unresolved promise before the custom element is defined',
+ ).toBe(promise);
+ customElements.define(
+ 'element-defined-after-whendefined',
+ ElementDefinedAfterWhenDefined,
+ );
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask',
+ ).toBe(false);
+
+ promiseAfterDefine = customElements.whenDefined('element-defined-after-whendefined');
+ promiseAfterDefine.then(
+ function (value) {
+ promiseAfterDefine.resolved = value;
+ },
+ function (value) {
+ promiseAfterDefine.rejected = value;
+ },
+ );
+ expect(
+ promiseAfterDefine,
+ '"whenDefined" must return a resolved promise once the custom element is defined',
+ ).not.toBe(promise);
+ expect(
+ 'resolved' in promiseAfterDefine,
+ 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask',
+ ).toBe(false);
+ expect(
+ 'rejected' in promiseAfterDefine,
+ 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask',
+ ).toBe(false);
+ })
+ .then(function () {
+ expect(
+ 'resolved' in promise,
+ 'The promise returned by "whenDefined" must be resolved when a custom element is defined',
+ ).toBe(true);
+ expect(
+ promise.resolved,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined',
+ ).toBe(ElementDefinedAfterWhenDefined);
+ expect(
+ 'rejected' in promise,
+ 'The promise returned by "whenDefined" must not be rejected when a custom element is defined',
+ ).toBe(false);
+
+ expect(
+ 'resolved' in promiseAfterDefine,
+ 'The promise returned by "whenDefined" must be resolved when a custom element is defined',
+ ).toBe(true);
+ expect(
+ promiseAfterDefine.resolved,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined',
+ ).toBe(ElementDefinedAfterWhenDefined);
+ expect(
+ 'rejected' in promiseAfterDefine,
+ 'The promise returned by "whenDefined" must not be rejected when a custom element is defined',
+ ).toBe(false);
+ });
+ },
+ );
+ context.test(
+ 'A promise returned by customElements.whenDefined must be resolved with the defined class',
+ async ({ HTMLElement, customElements }) => {
+ class ResolvedCustomElement extends HTMLElement {}
+ customElements.define('resolved-custom-element', ResolvedCustomElement);
+ return customElements.whenDefined('resolved-custom-element').then(function (value) {
+ expect(
+ value,
+ 'The promise returned by "whenDefined" must resolve with the defined class',
+ ).toBe(ResolvedCustomElement);
+ });
+ },
+ );
+ context.test(
+ 'A promise returned by customElements.whenDefined must be resolved with the defined class once such class is defined',
+ async ({ HTMLElement, customElements }) => {
+ var promise = customElements
+ .whenDefined('not-resolved-yet-custom-element')
+ .then(function (value) {
+ expect(
+ value,
+ 'The promise returned by "whenDefined" must resolve with the defined class once such class is defined',
+ ).toBe(NotResolvedYetCustomElement) as Promise & {
+ resolved?: unknown;
+ rejected?: unknown;
+ };
+ });
+ class NotResolvedYetCustomElement extends HTMLElement {}
+ customElements.define('not-resolved-yet-custom-element', NotResolvedYetCustomElement);
+ return promise;
+ },
+ );
+ }
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'customElements.define must upgrade elements in the shadow-including tree order',
+ ({ document, HTMLElement, customElements }) => {
+ var disconnectedElement = document.createElement('some-custom');
+ var connectedElementBeforeShadowHost = document.createElement('some-custom');
+ var connectedElementAfterShadowHost = document.createElement('some-custom');
+ var elementInShadowTree = document.createElement('some-custom');
+ var childElementOfShadowHost = document.createElement('some-custom');
+ var customShadowHost = document.createElement('some-custom');
+ var elementInNestedShadowTree = document.createElement('some-custom');
+
+ var container = document.createElement('div');
+ var shadowHost = document.createElement('div');
+ var shadowRoot = shadowHost.attachShadow({ mode: 'closed' });
+ container.appendChild(connectedElementBeforeShadowHost);
+ container.appendChild(shadowHost);
+ container.appendChild(connectedElementAfterShadowHost);
+ shadowHost.appendChild(childElementOfShadowHost);
+ shadowRoot.appendChild(elementInShadowTree);
+ shadowRoot.appendChild(customShadowHost);
+
+ var innerShadowRoot = customShadowHost.attachShadow({ mode: 'closed' });
+ innerShadowRoot.appendChild(elementInNestedShadowTree);
+
+ var calls: HTMLElement[] = [];
+ class SomeCustomElement extends HTMLElement {
+ constructor() {
+ super();
+ calls.push(this);
+ }
+ }
+
+ document.body.appendChild(container);
+ customElements.define('some-custom', SomeCustomElement);
+ {
+ const actualArray = calls;
+ const expectedArray = [
+ connectedElementBeforeShadowHost,
+ elementInShadowTree,
+ customShadowHost,
+ elementInNestedShadowTree,
+ childElementOfShadowHost,
+ connectedElementAfterShadowHost,
+ ];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ },
+ );
+ }
+
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'customElements.define unset the element definition is running flag before upgrading custom elements',
+ ({ HTMLElement, customElements, document, step }) => {
+ var unresolvedElement = document.createElement('constructor-calls-define');
+ document.body.appendChild(unresolvedElement);
+ var elementUpgradedDuringUpgrade = document.createElement('defined-during-upgrade');
+ document.body.appendChild(elementUpgradedDuringUpgrade);
+
+ var DefinedDuringUpgrade = class extends HTMLElement {};
+
+ class ConstructorCallsDefine extends HTMLElement {
+ constructor() {
+ customElements.define('defined-during-upgrade', DefinedDuringUpgrade);
+ step(() => expect(unresolvedElement instanceof ConstructorCallsDefine).toBe(false))();
+ step(() =>
+ expect(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade).toBe(true),
+ )();
+ super();
+ step(() => expect(unresolvedElement instanceof ConstructorCallsDefine).toBe(true))();
+ step(() =>
+ expect(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade).toBe(true),
+ )();
+ }
+ }
+
+ step(() => expect(unresolvedElement instanceof ConstructorCallsDefine).toBe(false))();
+ step(() =>
+ expect(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade).toBe(false),
+ )();
+
+ customElements.define('constructor-calls-define', ConstructorCallsDefine);
+ },
+ );
+ context.test(
+ 'Custom Elements: CustomElementRegistry interface',
+ ({ DOMException, HTMLElement, customElements }) => {
+ var calls: PropertyKey[] = [];
+ var ElementWithBadInnerName = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ calls.push(name);
+ customElements.define('badname', class extends HTMLElement {});
+ return (target as unknown as Record)[name];
+ },
+ });
+
+ expect(function () {
+ customElements.define('element-with-bad-inner-name', ElementWithBadInnerName);
+ }, 'customElements.define must throw a SyntaxError if the specified name is not a valid custom element name').toThrow(
+ expect.objectContaining({ name: 'SyntaxError', code: 12, constructor: DOMException }),
+ );
+
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype'];
+ expect(actualArray.length, 'customElements.define must get "prototype"').toBe(
+ expectedArray.length,
+ );
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index], 'customElements.define must get "prototype"').toBe(
+ expectedArray[index],
+ );
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must get "prototype", "disabledFeatures", and "formAssociated" property of the constructor',
+ ({ HTMLElement, customElements }) => {
+ var calls: PropertyKey[] = [];
+ var proxy = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ calls.push(name);
+ return (target as unknown as Record)[name];
+ },
+ });
+ customElements.define('proxy-element', proxy as unknown as CustomElementConstructor);
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype', 'disabledFeatures', 'formAssociated'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while getting "prototype" property of the constructor',
+ ({ HTMLElement, customElements }) => {
+ var err = { name: 'expectedError' };
+ var proxy = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ throw err;
+ },
+ });
+ {
+ let thrown: unknown;
+ let didThrow = false;
+ try {
+ (function () {
+ customElements.define(
+ 'element-with-string-prototype',
+ proxy as unknown as CustomElementConstructor,
+ );
+ })();
+ } catch (error) {
+ didThrow = true;
+ thrown = error;
+ }
+ expect(didThrow).toBe(true);
+ expect(thrown).toBe(err);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must throw when "prototype" property of the constructor is not an object',
+ ({ HTMLElement, TypeError, customElements }) => {
+ var returnedValue: unknown;
+ var proxy = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ return returnedValue;
+ },
+ });
+
+ returnedValue = null;
+ expect(function () {
+ customElements.define(
+ 'element-with-string-prototype',
+ proxy as unknown as CustomElementConstructor,
+ );
+ }, 'customElements.define must throw when "prototype" property of the constructor is null').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ returnedValue = undefined;
+ expect(function () {
+ customElements.define(
+ 'element-with-string-prototype',
+ proxy as unknown as CustomElementConstructor,
+ );
+ }, 'customElements.define must throw when "prototype" property of the constructor is undefined').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ returnedValue = 'hello';
+ expect(function () {
+ customElements.define(
+ 'element-with-string-prototype',
+ proxy as unknown as CustomElementConstructor,
+ );
+ }, 'customElements.define must throw when "prototype" property of the constructor is a string').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ returnedValue = 1;
+ expect(function () {
+ customElements.define(
+ 'element-with-string-prototype',
+ proxy as unknown as CustomElementConstructor,
+ );
+ }, 'customElements.define must throw when "prototype" property of the constructor is a number').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ },
+ );
+ context.test(
+ 'customElements.define must get callbacks of the constructor prototype',
+ ({ Element, customElements }) => {
+ const moveBefore_supported = 'moveBefore' in Element.prototype;
+
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ var calls: PropertyKey[] = [];
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls.push(name);
+ return (target as unknown as Record)[name];
+ },
+ });
+ customElements.define(
+ 'element-with-proxy-prototype',
+ constructor as unknown as CustomElementConstructor,
+ );
+ {
+ const actualArray = calls;
+ const expectedArray = moveBefore_supported
+ ? [
+ 'connectedCallback',
+ 'disconnectedCallback',
+ 'connectedMoveCallback',
+ 'adoptedCallback',
+ 'attributeChangedCallback',
+ ]
+ : [
+ 'connectedCallback',
+ 'disconnectedCallback',
+ 'adoptedCallback',
+ 'attributeChangedCallback',
+ ];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while getting callbacks on the constructor prototype',
+ ({ customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ var calls: PropertyKey[] = [];
+ var err = { name: 'expectedError' };
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'disconnectedCallback') throw err;
+ return (target as unknown as Record)[name];
+ },
+ });
+ {
+ let thrown: unknown;
+ let didThrow = false;
+ try {
+ (function () {
+ customElements.define(
+ 'element-with-throwing-callback',
+ constructor as unknown as CustomElementConstructor,
+ );
+ })();
+ } catch (error) {
+ didThrow = true;
+ thrown = error;
+ }
+ expect(didThrow).toBe(true);
+ expect(thrown).toBe(err);
+ }
+ {
+ const actualArray = calls;
+ const expectedArray = ['connectedCallback', 'disconnectedCallback'];
+ expect(
+ actualArray.length,
+ 'customElements.define must not get callbacks after one of the get throws',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must not get callbacks after one of the get throws',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while converting a callback value to Function callback type',
+ ({ Element, TypeError, customElements }) => {
+ const moveBefore_supported = 'moveBefore' in Element.prototype;
+
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ var calls: PropertyKey[] = [];
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'adoptedCallback') return 1;
+ return (target as unknown as Record)[name];
+ },
+ });
+ expect(function () {
+ customElements.define(
+ 'element-with-throwing-callback',
+ constructor as unknown as CustomElementConstructor,
+ );
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ {
+ const actualArray = calls;
+ const expectedArray = moveBefore_supported
+ ? [
+ 'connectedCallback',
+ 'disconnectedCallback',
+ 'connectedMoveCallback',
+ 'adoptedCallback',
+ ]
+ : ['connectedCallback', 'disconnectedCallback', 'adoptedCallback'];
+ expect(
+ actualArray.length,
+ 'customElements.define must not get callbacks after one of the conversion throws',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must not get callbacks after one of the conversion throws',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must get "observedAttributes" property on the constructor prototype when "attributeChangedCallback" is present',
+ ({ Element, customElements }) => {
+ const moveBefore_supported = 'moveBefore' in Element.prototype;
+
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ constructor.prototype.attributeChangedCallback = function () {};
+ var prototypeCalls: (PropertyKey | number)[] = [];
+ var callOrder = 0;
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ if (name == 'prototype' || name == 'observedAttributes')
+ throw 'Unexpected access to observedAttributes';
+ prototypeCalls.push(callOrder++);
+ prototypeCalls.push(name);
+ return (target as unknown as Record)[name];
+ },
+ });
+ var constructorCalls: (PropertyKey | number)[] = [];
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ constructorCalls.push(callOrder++);
+ constructorCalls.push(name);
+ return (target as unknown as Record)[name];
+ },
+ });
+ customElements.define(
+ 'element-with-attribute-changed-callback',
+ proxy as unknown as CustomElementConstructor,
+ );
+ {
+ const actualArray = prototypeCalls;
+ const expectedArray = moveBefore_supported
+ ? [
+ 1,
+ 'connectedCallback',
+ 2,
+ 'disconnectedCallback',
+ 3,
+ 'connectedMoveCallback',
+ 4,
+ 'adoptedCallback',
+ 5,
+ 'attributeChangedCallback',
+ ]
+ : [
+ 1,
+ 'connectedCallback',
+ 2,
+ 'disconnectedCallback',
+ 3,
+ 'adoptedCallback',
+ 4,
+ 'attributeChangedCallback',
+ ];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ {
+ const actualArray = constructorCalls;
+ const expectedArray = [
+ 0,
+ 'prototype',
+ 6,
+ 'observedAttributes',
+ 7,
+ 'disabledFeatures',
+ 8,
+ 'formAssociated',
+ ];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while getting observedAttributes on the constructor prototype',
+ ({ customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ constructor.prototype.attributeChangedCallback = function () {};
+ var calls: PropertyKey[] = [];
+ var err = { name: 'expectedError' };
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'observedAttributes') throw err;
+ return (target as unknown as Record)[name];
+ },
+ });
+ {
+ let thrown: unknown;
+ let didThrow = false;
+ try {
+ (function () {
+ customElements.define(
+ 'element-with-throwing-observed-attributes',
+ proxy as unknown as CustomElementConstructor,
+ );
+ })();
+ } catch (error) {
+ didThrow = true;
+ thrown = error;
+ }
+ expect(didThrow).toBe(true);
+ expect(thrown).toBe(err);
+ }
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype', 'observedAttributes'];
+ expect(
+ actualArray.length,
+ 'customElements.define must get "prototype" and "observedAttributes" on the constructor',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must get "prototype" and "observedAttributes" on the constructor',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while converting the value of observedAttributes to sequence',
+ ({ TypeError, customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ constructor.prototype.attributeChangedCallback = function () {};
+ var calls: PropertyKey[] = [];
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'observedAttributes') return 1;
+ return (target as unknown as Record)[name];
+ },
+ });
+ expect(function () {
+ customElements.define(
+ 'element-with-invalid-observed-attributes',
+ proxy as unknown as CustomElementConstructor,
+ );
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype', 'observedAttributes'];
+ expect(
+ actualArray.length,
+ 'customElements.define must get "prototype" and "observedAttributes" on the constructor',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must get "prototype" and "observedAttributes" on the constructor',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while iterating over observedAttributes to sequence',
+ ({ customElements }) => {
+ var err = { name: 'SomeError' };
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ constructor.prototype.attributeChangedCallback = function () {};
+ constructor.observedAttributes = {
+ [Symbol.iterator]: function* () {
+ yield 'foo';
+ throw err;
+ },
+ };
+ {
+ let thrown: unknown;
+ let didThrow = false;
+ try {
+ (function () {
+ customElements.define(
+ 'element-with-generator-observed-attributes',
+ constructor as unknown as CustomElementConstructor,
+ );
+ })();
+ } catch (error) {
+ didThrow = true;
+ thrown = error;
+ }
+ expect(didThrow).toBe(true);
+ expect(thrown).toBe(err);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on observedAttributes',
+ ({ TypeError, customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ constructor.prototype.attributeChangedCallback = function () {};
+ constructor.observedAttributes = { [Symbol.iterator]: 1 };
+ expect(function () {
+ customElements.define(
+ 'element-with-observed-attributes-with-uncallable-iterator',
+ constructor as unknown as CustomElementConstructor,
+ );
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+ context.test(
+ 'customElements.define must not throw even if "observedAttributes" fails to convert if "attributeChangedCallback" is not defined',
+ ({ customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ constructor.observedAttributes = 1;
+ customElements.define(
+ 'element-without-callback-with-invalid-observed-attributes',
+ constructor as unknown as CustomElementConstructor,
+ );
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while getting disabledFeatures on the constructor prototype',
+ ({ customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ var calls: PropertyKey[] = [];
+ var err = { name: 'expectedError' };
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'disabledFeatures') throw err;
+ return (target as unknown as Record)[name];
+ },
+ });
+ {
+ let thrown: unknown;
+ let didThrow = false;
+ try {
+ (() =>
+ customElements.define(
+ 'element-with-throwing-disabled-features',
+ proxy as unknown as CustomElementConstructor,
+ ))();
+ } catch (error) {
+ didThrow = true;
+ thrown = error;
+ }
+ expect(didThrow).toBe(true);
+ expect(thrown).toBe(err);
+ }
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype', 'disabledFeatures'];
+ expect(
+ actualArray.length,
+ 'customElements.define must get "prototype" and "disabledFeatures" on the constructor',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must get "prototype" and "disabledFeatures" on the constructor',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while converting the value of disabledFeatures to sequence',
+ ({ TypeError, customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ var calls: PropertyKey[] = [];
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'disabledFeatures') return 1;
+ return (target as unknown as Record)[name];
+ },
+ });
+ expect(() =>
+ customElements.define(
+ 'element-with-invalid-disabled-features',
+ proxy as unknown as CustomElementConstructor,
+ ),
+ ).toThrow(expect.objectContaining({ constructor: TypeError }));
+ {
+ const actualArray = calls;
+ const expectedArray = ['prototype', 'disabledFeatures'];
+ expect(
+ actualArray.length,
+ 'customElements.define must get "prototype" and "disabledFeatures" on the constructor',
+ ).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(
+ actualArray[index],
+ 'customElements.define must get "prototype" and "disabledFeatures" on the constructor',
+ ).toBe(expectedArray[index]);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while iterating over disabledFeatures to sequence',
+ ({ customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ var err = { name: 'SomeError' };
+ constructor.disabledFeatures = {
+ [Symbol.iterator]: function* () {
+ yield 'foo';
+ throw err;
+ },
+ };
+ {
+ let thrown: unknown;
+ let didThrow = false;
+ try {
+ (() =>
+ customElements.define(
+ 'element-with-generator-disabled-features',
+ constructor as unknown as CustomElementConstructor,
+ ))();
+ } catch (error) {
+ didThrow = true;
+ thrown = error;
+ }
+ expect(didThrow).toBe(true);
+ expect(thrown).toBe(err);
+ }
+ },
+ );
+ context.test(
+ 'customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on disabledFeatures',
+ ({ TypeError, customElements }) => {
+ var constructor: (() => void) & {
+ observedAttributes?: unknown;
+ disabledFeatures?: unknown;
+ } = function () {};
+ constructor.disabledFeatures = { [Symbol.iterator]: 1 };
+ expect(() =>
+ customElements.define(
+ 'element-with-disabled-features-with-uncallable-iterator',
+ constructor as unknown as CustomElementConstructor,
+ ),
+ ).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+ }
+
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'customElements.define must not throw when defining another custom element in a different global object during Get(constructor, "prototype")',
+ async ({ document, HTMLElement, customElements, step, cleanup }) => {
+ await new Promise((resolve) => {
+ const iframe = document.createElement('iframe');
+ cleanup(() => iframe.remove());
+ iframe.onload = () => {
+ step(() => {
+ const innerWindow = iframe.contentWindow as Window & typeof globalThis;
+ var InnerCustomElement = class extends innerWindow.HTMLElement {};
+ var calls: PropertyKey[] = [];
+ var proxy = new Proxy(class extends HTMLElement {}, {
+ get(target, name) {
+ calls.push(name);
+ if (name === 'prototype')
+ innerWindow.customElements.define('another-custom-element', InnerCustomElement);
+ return (target as unknown as Record)[name];
+ },
+ });
+ customElements.define('element-with-inner-element-define', proxy);
+ const expected = ['prototype', 'disabledFeatures', 'formAssociated'];
+ expect(
+ calls.length,
+ 'customElements.define must get "prototype", "disabledFeatures", and "formAssociated" on the constructor',
+ ).toBe(expected.length);
+ for (let i = 0; i < expected.length; i++)
+ expect(
+ calls[i],
+ 'customElements.define must get "prototype", "disabledFeatures", and "formAssociated" on the constructor',
+ ).toBe(expected[i]);
+ expect(
+ iframe.contentDocument!.createElement('another-custom-element') instanceof
+ InnerCustomElement,
+ ).toBe(true);
+ })();
+ document.body.removeChild(iframe);
+ resolve();
+ };
+ document.body.appendChild(iframe);
+ });
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/HTMLElement-constructor.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/HTMLElement-constructor.html.ts
new file mode 100644
index 0000000..ff3bd54
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/HTMLElement-constructor.html.ts
@@ -0,0 +1,218 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'HTMLElement constructor must throw a TypeError when NewTarget is equal to itself',
+ ({ HTMLElement, customElements, TypeError }) => {
+ customElements.define('html-custom-element', HTMLElement);
+ expect(function () {
+ new HTMLElement();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must throw a TypeError when NewTarget is equal to itself via a Proxy object',
+ ({ HTMLElement, customElements, TypeError }) => {
+ customElements.define('html-proxy-custom-element', new Proxy(HTMLElement, {}));
+ expect(function () {
+ new HTMLElement();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must throw TypeError when it has not been defined by customElements.define',
+ ({ HTMLElement, TypeError }) => {
+ class SomeCustomElement extends HTMLElement {}
+ expect(function () {
+ new SomeCustomElement();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+ context.test(
+ 'Custom element constructor must throw TypeError when it does not extend HTMLElement',
+ ({ customElements, HTMLParagraphElement, TypeError }) => {
+ class SomeCustomElement extends HTMLParagraphElement {}
+ customElements.define('some-custom-element', SomeCustomElement);
+ expect(function () {
+ new SomeCustomElement();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+ context.test(
+ 'Custom element constructor must throw TypeError when it does not extend the proper element interface',
+ ({ customElements, HTMLButtonElement, TypeError }) => {
+ class SomeCustomButtonElement extends HTMLButtonElement {}
+ customElements.define('some-custom-button-element', SomeCustomButtonElement, {
+ extends: 'p',
+ });
+ expect(function () {
+ new SomeCustomButtonElement();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must infer the tag name from the element interface',
+ ({ document, HTMLElement, customElements, Element, Node }) => {
+ class CustomElementWithInferredTagName extends HTMLElement {}
+ customElements.define('inferred-name', CustomElementWithInferredTagName);
+
+ var instance = new CustomElementWithInferredTagName();
+ expect(instance instanceof Element, 'A custom element must inherit from Element').toBe(true);
+ expect(instance instanceof Node, 'A custom element must inherit from Node').toBe(true);
+ expect(instance.localName).toBe('inferred-name');
+ expect(instance.nodeName).toBe('INFERRED-NAME');
+ expect(instance.namespaceURI, 'A custom HTML element must use HTML namespace').toBe(
+ 'http://www.w3.org/1999/xhtml',
+ );
+
+ document.body.appendChild(instance);
+ expect(
+ document.body.lastChild,
+ 'document.body.appendChild must be able to insert a custom element',
+ ).toBe(instance);
+ expect(
+ document.querySelector('inferred-name'),
+ 'document.querySelector must be able to find a custom element by its tag name',
+ ).toBe(instance);
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must allow subclassing a custom element',
+ ({ HTMLElement, customElements }) => {
+ class ConcreteCustomElement extends HTMLElement {}
+ class SubCustomElement extends ConcreteCustomElement {}
+ customElements.define('concrete-custom-element', ConcreteCustomElement);
+ customElements.define('sub-custom-element', SubCustomElement);
+
+ var instance = new ConcreteCustomElement();
+ expect(instance instanceof ConcreteCustomElement).toBe(true);
+ expect(instance instanceof SubCustomElement).toBe(false);
+ expect(instance.localName).toBe('concrete-custom-element');
+ expect(instance.nodeName).toBe('CONCRETE-CUSTOM-ELEMENT');
+
+ var instance = new SubCustomElement();
+ expect(instance instanceof ConcreteCustomElement).toBe(true);
+ expect(instance instanceof SubCustomElement).toBe(true);
+ expect(instance.localName).toBe('sub-custom-element');
+ expect(instance.nodeName).toBe('SUB-CUSTOM-ELEMENT');
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must allow subclassing an user-defined subclass of HTMLElement',
+ ({ HTMLElement, customElements }) => {
+ class AbstractCustomElement extends HTMLElement {}
+ class ConcreteSubCustomElement extends AbstractCustomElement {}
+ customElements.define('concrete-sub-custom-element', ConcreteSubCustomElement);
+
+ var instance = new ConcreteSubCustomElement();
+ expect(instance instanceof AbstractCustomElement).toBe(true);
+ expect(instance instanceof ConcreteSubCustomElement).toBe(true);
+ expect(instance.localName).toBe('concrete-sub-custom-element');
+ expect(instance.nodeName).toBe('CONCRETE-SUB-CUSTOM-ELEMENT');
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must only get .prototype once, calling proxy constructor directly',
+ ({ HTMLElement, customElements }) => {
+ class SomeCustomElement extends HTMLElement {}
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function (target, prop, receiver) {
+ if (prop == 'prototype') {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ },
+ });
+ customElements.define('success-counting-element-1', countingProxy);
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ var instance = new countingProxy();
+ expect(getCount, 'Should have gotten .prototype once').toBe(1);
+ expect(instance instanceof countingProxy).toBe(true);
+ expect(instance instanceof HTMLElement).toBe(true);
+ expect(instance instanceof SomeCustomElement).toBe(true);
+ expect(instance.localName).toBe('success-counting-element-1');
+ expect(instance.nodeName).toBe('SUCCESS-COUNTING-ELEMENT-1');
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must only get .prototype once, calling proxy constructor via Reflect',
+ ({ HTMLElement, customElements }) => {
+ class SomeCustomElement extends HTMLElement {}
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function (target, prop, receiver) {
+ if (prop == 'prototype') {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ },
+ });
+ customElements.define('success-counting-element-2', countingProxy);
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ var instance = Reflect.construct(HTMLElement, [], countingProxy);
+ expect(getCount, 'Should have gotten .prototype once').toBe(1);
+ expect(instance instanceof countingProxy).toBe(true);
+ expect(instance instanceof HTMLElement).toBe(true);
+ expect(instance instanceof SomeCustomElement).toBe(true);
+ expect(instance.localName).toBe('success-counting-element-2');
+ expect(instance.nodeName).toBe('SUCCESS-COUNTING-ELEMENT-2');
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must only get .prototype once, calling proxy constructor via Reflect with no inheritance',
+ ({ HTMLElement, customElements }) => {
+ class SomeCustomElement {}
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function (target, prop, receiver) {
+ if (prop == 'prototype') {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ },
+ });
+ customElements.define(
+ 'success-counting-element-3',
+ countingProxy as unknown as CustomElementConstructor,
+ );
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ var instance = Reflect.construct(HTMLElement, [], countingProxy);
+ expect(getCount, 'Should have gotten .prototype once').toBe(1);
+ expect(instance instanceof countingProxy).toBe(true);
+ expect(instance instanceof SomeCustomElement).toBe(true);
+ expect(instance.localName).toBe(undefined);
+ expect(instance.nodeName).toBe(undefined);
+ },
+ );
+ context.test(
+ 'HTMLElement constructor must not get .prototype until it finishes its registration sanity checks, calling via Reflect',
+ ({ HTMLElement, TypeError }) => {
+ class SomeCustomElement extends HTMLElement {}
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function (target, prop, receiver) {
+ if (prop == 'prototype') {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ },
+ });
+
+ // Purposefully don't register it.
+ expect(function () {
+ Reflect.construct(HTMLElement, [], countingProxy);
+ }, "Should not be able to construct an HTMLElement named 'button'").toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ expect(getCount, 'Should never have gotten .prototype').toBe(0);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/adopted-callback.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/adopted-callback.html.ts
new file mode 100644
index 0000000..eae101d
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/adopted-callback.html.ts
@@ -0,0 +1,452 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+import { documentNames, document_types } from './resources/custom-elements-helpers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Inserting a custom element into the owner document must not enqueue and invoke adoptedCallback',
+ ({ document, HTMLElement, customElements }) => {
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ var instance = document.createElement('my-custom-element');
+ calls = [];
+ document.body.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ },
+ );
+ for (const [index, documentName] of documentNames.entries()) {
+ if (index === 0) continue;
+ context.test(
+ 'Inserting a custom element into ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ calls = [];
+ doc.documentElement.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Moving a custom element from the owner document into ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ document.body.appendChild(instance);
+ calls = [];
+ doc.documentElement.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', 'adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting an ancestor of custom element into ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ calls = [];
+ doc.documentElement.appendChild(parent);
+ {
+ const actualArray = calls;
+ const expectedArray = ['adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Moving an ancestor of custom element from the owner document into ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ document.body.appendChild(parent);
+ calls = [];
+ doc.documentElement.appendChild(parent);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', 'adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting a custom element into a shadow tree in ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ doc.documentElement.appendChild(host);
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting the shadow host of a custom element into ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = document.createElement('div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ doc.documentElement.appendChild(host);
+ {
+ const actualArray = calls;
+ const expectedArray = ['adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Moving the shadow host of a custom element from the owner document into ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = document.createElement('div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ shadowRoot.appendChild(instance);
+ document.body.appendChild(host);
+
+ calls = [];
+ doc.documentElement.appendChild(host);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', 'adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ "Moving the shadow host's shadow of a custom element from the owner document into " +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ const instance = document.createElement('my-custom-element');
+ const host = document.createElement('div');
+ const shadowRoot = host.attachShadow({ mode: 'closed' });
+ shadowRoot.appendChild(instance);
+ document.body.appendChild(host);
+
+ calls = [];
+ doc.documentElement.appendChild(shadowRoot);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', 'adopted', document, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ "Moving the 's content of a custom element from the owner document into " +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ const instance = document.createElement('my-custom-element');
+ const template = document.createElement('template');
+ const templateContent = template.content;
+ templateContent.appendChild(instance);
+ document.body.appendChild(template);
+
+ calls = [];
+ doc.documentElement.appendChild(templateContent);
+ if (doc === templateContent.ownerDocument) {
+ {
+ const actualArray = calls;
+ const expectedArray = ['connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ } else {
+ {
+ const actualArray = calls;
+ const expectedArray = ['adopted', templateContent.ownerDocument, doc, 'connected'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting a custom element into a detached shadow tree that belongs to ' +
+ documentName +
+ ' must enqueue and invoke adoptedCallback',
+ async (fixture) => {
+ const { document, HTMLElement, customElements } = fixture;
+ const getDocument = document_types(document, fixture.Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected');
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ calls.push('adopted');
+ calls.push(oldDocument);
+ calls.push(newDocument);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected');
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['adopted', document, doc];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/attribute-changed-callback.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/attribute-changed-callback.html.ts
new file mode 100644
index 0000000..6cec9a7
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/attribute-changed-callback.html.ts
@@ -0,0 +1,615 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+import {
+ define_new_custom_element,
+ create_attribute_changed_callback_log,
+ assert_attribute_log_entry,
+ type AttributeArguments,
+ type CallbackLog,
+} from './resources/custom-elements-helpers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'setAttribute and removeAttribute must enqueue and invoke attributeChangedCallback',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ const instance = document.createElement(customElement.name);
+ {
+ const actualArray = customElement.takeLog().types();
+ const expectedArray = ['constructed'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+
+ instance.setAttribute('title', 'foo');
+ expect(instance.getAttribute('title')).toBe('foo');
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'title',
+ oldValue: null,
+ newValue: 'foo',
+ namespace: null,
+ });
+
+ instance.removeAttribute('title');
+ expect(instance.getAttribute('title')).toBe(null);
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'title',
+ oldValue: 'foo',
+ newValue: null,
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'setAttributeNS and removeAttributeNS must enqueue and invoke attributeChangedCallback',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ var instance = document.createElement(customElement.name);
+ {
+ const actualArray = customElement.takeLog().types();
+ const expectedArray = ['constructed'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+
+ instance.setAttributeNS('http://www.w3.org/2000/svg', 'title', 'hello');
+ expect(instance.getAttribute('title')).toBe('hello');
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'title',
+ oldValue: null,
+ newValue: 'hello',
+ namespace: 'http://www.w3.org/2000/svg',
+ });
+
+ instance.removeAttributeNS('http://www.w3.org/2000/svg', 'title');
+ expect(instance.getAttribute('title')).toBe(null);
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'title',
+ oldValue: 'hello',
+ newValue: null,
+ namespace: 'http://www.w3.org/2000/svg',
+ });
+ },
+ );
+ context.test(
+ 'setAttributeNode and removeAttributeNode must enqueue and invoke attributeChangedCallback for an HTML attribute',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ var instance = document.createElement(customElement.name);
+ {
+ const actualArray = customElement.takeLog().types();
+ const expectedArray = ['constructed'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+
+ var attr = document.createAttribute('id');
+ attr.value = 'bar';
+ instance.setAttributeNode(attr);
+
+ expect(instance.getAttribute('id')).toBe('bar');
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'id',
+ oldValue: null,
+ newValue: 'bar',
+ namespace: null,
+ });
+
+ instance.removeAttributeNode(attr);
+ expect(instance.getAttribute('id')).toBe(null);
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'id',
+ oldValue: 'bar',
+ newValue: null,
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'setAttributeNode and removeAttributeNS must enqueue and invoke attributeChangedCallback for an SVG attribute',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ const instance = document.createElement(customElement.name);
+ {
+ const actualArray = customElement.takeLog().types();
+ const expectedArray = ['constructed'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+
+ const attr = document.createAttributeNS('http://www.w3.org/2000/svg', 'r');
+ attr.value = '100';
+ instance.setAttributeNode(attr);
+
+ expect(instance.getAttribute('r')).toBe('100');
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'r',
+ oldValue: null,
+ newValue: '100',
+ namespace: 'http://www.w3.org/2000/svg',
+ });
+
+ instance.removeAttributeNode(attr);
+ expect(instance.getAttribute('r')).toBe(null);
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'r',
+ oldValue: '100',
+ newValue: null,
+ namespace: 'http://www.w3.org/2000/svg',
+ });
+ },
+ );
+ context.test(
+ 'toggleAttribute must enqueue and invoke attributeChangedCallback',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ const instance = document.createElement(customElement.name);
+ {
+ const actualArray = customElement.takeLog().types();
+ const expectedArray = ['constructed'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+
+ instance.toggleAttribute('title', true);
+ expect(instance.hasAttribute('title')).toBe(true);
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'title',
+ oldValue: null,
+ newValue: '',
+ namespace: null,
+ });
+
+ instance.toggleAttribute('title');
+ expect(instance.hasAttribute('title')).toBe(false);
+ var logEntries = customElement.takeLog();
+ {
+ const actualArray = logEntries.types();
+ const expectedArray = ['attributeChanged'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ assert_attribute_log_entry(expect, logEntries.last(), {
+ name: 'title',
+ oldValue: '',
+ newValue: null,
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'Mutating attributeChangedCallback after calling customElements.define must not affect the callback being invoked',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ const callsToOld: CallbackLog[] = [];
+ const callsToNew: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ declare attributeChangedCallback: (...args: AttributeArguments) => void;
+ }
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ callsToOld.push(create_attribute_changed_callback_log(this, ...args));
+ };
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('element-with-mutated-attribute-changed-callback', CustomElement);
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ callsToNew.push(create_attribute_changed_callback_log(this, ...args));
+ };
+
+ const instance = document.createElement('element-with-mutated-attribute-changed-callback');
+ instance.setAttribute('title', 'hi');
+ expect(instance.getAttribute('title')).toBe('hi');
+ {
+ const actualArray = callsToNew;
+ const expectedArray: never[] = [];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ expect(callsToOld.length).toBe(1);
+ assert_attribute_log_entry(expect, callsToOld[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: 'hi',
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'attributedChangedCallback must not be invoked when the observed attributes does not contain the attribute',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ const calls: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ attributeChangedCallback(...args: AttributeArguments) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ }
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('element-not-observing-id-attribute', CustomElement);
+
+ const instance = document.createElement('element-not-observing-id-attribute');
+ expect(calls.length).toBe(0);
+ instance.setAttribute('title', 'hi');
+ expect(calls.length).toBe(1);
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: 'hi',
+ namespace: null,
+ });
+ instance.setAttribute('id', 'some');
+ expect(calls.length).toBe(1);
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: 'hi',
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'Mutating observedAttributes after calling customElements.define must not affect the set of attributes for which attributedChangedCallback is invoked',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ const calls: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ declare attributeChangedCallback: (...args: AttributeArguments) => void;
+ }
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ };
+ CustomElement.observedAttributes = ['title', 'lang'];
+ customElements.define('element-with-mutated-observed-attributes', CustomElement);
+ CustomElement.observedAttributes = ['title', 'id'];
+
+ const instance = document.createElement('element-with-mutated-observed-attributes');
+ instance.setAttribute('title', 'hi');
+ expect(calls.length).toBe(1);
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: 'hi',
+ namespace: null,
+ });
+
+ instance.setAttribute('id', 'some');
+ expect(calls.length).toBe(1);
+
+ instance.setAttribute('lang', 'en');
+ expect(calls.length).toBe(2);
+ assert_attribute_log_entry(expect, calls[1]!, {
+ name: 'lang',
+ oldValue: null,
+ newValue: 'en',
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'attributedChangedCallback must be enqueued for attributes specified in a non-Array iterable observedAttributes',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ var calls: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ declare attributeChangedCallback: (...args: AttributeArguments) => void;
+ }
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ };
+ CustomElement.observedAttributes = {
+ [Symbol.iterator]: function* () {
+ yield 'lang';
+ yield 'style';
+ },
+ };
+ customElements.define('element-with-generator-observed-attributes', CustomElement);
+
+ var instance = document.createElement('element-with-generator-observed-attributes');
+ instance.setAttribute('lang', 'en');
+ expect(calls.length).toBe(1);
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'lang',
+ oldValue: null,
+ newValue: 'en',
+ namespace: null,
+ });
+
+ instance.setAttribute('lang', 'ja');
+ expect(calls.length).toBe(2);
+ assert_attribute_log_entry(expect, calls[1]!, {
+ name: 'lang',
+ oldValue: 'en',
+ newValue: 'ja',
+ namespace: null,
+ });
+
+ instance.setAttribute('title', 'hello');
+ expect(calls.length).toBe(2);
+
+ instance.setAttribute('style', 'font-size: 2rem');
+ expect(calls.length).toBe(3);
+ assert_attribute_log_entry(expect, calls[2]!, {
+ name: 'style',
+ oldValue: null,
+ newValue: 'font-size: 2rem',
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'attributedChangedCallback must be enqueued for style attribute change by mutating inline style declaration',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ var calls: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ declare attributeChangedCallback: (...args: AttributeArguments) => void;
+ }
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ };
+ CustomElement.observedAttributes = ['style'];
+ customElements.define('element-with-style-attribute-observation', CustomElement);
+
+ var instance = document.createElement('element-with-style-attribute-observation');
+ expect(calls.length).toBe(0);
+
+ instance.style.fontSize = '10px';
+ expect(calls.length).toBe(1);
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'style',
+ oldValue: null,
+ newValue: 'font-size: 10px;',
+ namespace: null,
+ });
+
+ instance.style.fontSize = '20px';
+ expect(calls.length).toBe(2);
+ assert_attribute_log_entry(expect, calls[1]!, {
+ name: 'style',
+ oldValue: 'font-size: 10px;',
+ newValue: 'font-size: 20px;',
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'attributedChangedCallback must not be enqueued when mutating inline style declaration if the style attribute is not observed',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ var calls: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ declare attributeChangedCallback: (...args: AttributeArguments) => void;
+ }
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ };
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('element-with-no-style-attribute-observation', CustomElement);
+
+ var instance = document.createElement('element-with-no-style-attribute-observation');
+ expect(calls.length).toBe(0);
+ instance.style.fontSize = '10px';
+ expect(calls.length).toBe(0);
+ instance.title = 'hello';
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: 'hello',
+ namespace: null,
+ });
+ },
+ );
+ context.test(
+ 'Upgrading a parser created element must enqueue and invoke attributeChangedCallback for an HTML attribute',
+ ({ HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ var calls: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ declare attributeChangedCallback: (...args: AttributeArguments) => void;
+ }
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ };
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('parser-created-element', CustomElement);
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: '',
+ namespace: null,
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '
',
+ },
+ },
+ );
+ context.test(
+ 'Upgrading a cloned element must enqueue and invoke attributeChangedCallback for an HTML attribute',
+ ({ document, HTMLElement, customElements }) => {
+ var customElement = define_new_custom_element(HTMLElement, customElements, [
+ 'title',
+ 'id',
+ 'r',
+ ]);
+
+ var calls: CallbackLog[] = [];
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ declare attributeChangedCallback: (...args: AttributeArguments) => void;
+ }
+ CustomElement.prototype.attributeChangedCallback = function (...args: AttributeArguments) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ };
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('cloned-element-with-attribute', CustomElement);
+
+ var instance = document.createElement('cloned-element-with-attribute');
+ expect(calls.length).toBe(0);
+ instance.title = '';
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: '',
+ namespace: null,
+ });
+
+ calls = [];
+ var clone = instance.cloneNode(false);
+ assert_attribute_log_entry(expect, calls[0]!, {
+ name: 'title',
+ oldValue: null,
+ newValue: '',
+ namespace: null,
+ });
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/connected-callbacks.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/connected-callbacks.html.ts
new file mode 100644
index 0000000..f2db538
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/connected-callbacks.html.ts
@@ -0,0 +1,176 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+import { documentNames, document_types } from './resources/custom-elements-helpers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [index, documentName] of documentNames.entries()) {
+ context.test(
+ 'Inserting a custom element into ' +
+ documentName +
+ ' must enqueue and invoke connectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ calls = [];
+ doc.documentElement.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['connected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting an ancestor of custom element into ' +
+ documentName +
+ ' must enqueue and invoke connectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ calls = [];
+ doc.documentElement.appendChild(parent);
+ {
+ const actualArray = calls;
+ const expectedArray = ['connected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting a custom element into a shadow tree in ' +
+ documentName +
+ ' must enqueue and invoke connectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ doc.documentElement.appendChild(host);
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['connected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting the shadow host of a custom element into ' +
+ documentName +
+ ' must enqueue and invoke connectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ doc.documentElement.appendChild(host);
+ {
+ const actualArray = calls;
+ const expectedArray = ['connected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Inserting a custom element into a detached shadow tree that belongs to ' +
+ documentName +
+ ' must not enqueue and invoke connectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray: never[] = [];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/createElement-reentrant-construction.window.js.ts b/packages/web-platform-tests/src/wpt/custom-elements/createElement-reentrant-construction.window.js.ts
new file mode 100644
index 0000000..5b380c4
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/createElement-reentrant-construction.window.js.ts
@@ -0,0 +1,65 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Re-entrant createElement() of the same element before super()',
+ ({ document, HTMLElement, customElements }) => {
+ let nested: HTMLElement | undefined;
+ let needsReentry = true;
+ class Reentrant extends HTMLElement {
+ constructor() {
+ if (needsReentry) {
+ needsReentry = false;
+ nested = document.createElement('reentrant-before-super');
+ }
+ super();
+ }
+ }
+ customElements.define('reentrant-before-super', Reentrant);
+
+ const outer = document.createElement('reentrant-before-super');
+
+ expect(
+ nested instanceof Reentrant,
+ 'the re-entrant createElement() constructs an element',
+ ).toBe(true);
+ expect(nested!.localName).toBe('reentrant-before-super');
+ expect(outer instanceof Reentrant, 'the outer createElement() constructs an element').toBe(
+ true,
+ );
+ expect(outer.localName).toBe('reentrant-before-super');
+ expect(outer, 'the two createElement() calls construct distinct elements').not.toBe(nested);
+ },
+ );
+ context.test(
+ 'Re-entrant createElement() of the same element after super()',
+ ({ document, HTMLElement, customElements }) => {
+ let nested: HTMLElement | undefined;
+ let needsReentry = true;
+ class Reentrant extends HTMLElement {
+ constructor() {
+ super();
+ if (needsReentry) {
+ needsReentry = false;
+ nested = document.createElement('reentrant-after-super');
+ }
+ }
+ }
+ customElements.define('reentrant-after-super', Reentrant);
+
+ const outer = document.createElement('reentrant-after-super');
+
+ expect(
+ nested instanceof Reentrant,
+ 'the re-entrant createElement() constructs an element',
+ ).toBe(true);
+ expect(nested!.localName).toBe('reentrant-after-super');
+ expect(outer instanceof Reentrant, 'the outer createElement() constructs an element').toBe(
+ true,
+ );
+ expect(outer.localName).toBe('reentrant-after-super');
+ expect(outer, 'the two createElement() calls construct distinct elements').not.toBe(nested);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/cross-realm-callback-report-exception.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/cross-realm-callback-report-exception.html.ts
new file mode 100644
index 0000000..27cb2d1
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/cross-realm-callback-report-exception.html.ts
@@ -0,0 +1,232 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+import { create_window_in_test } from './resources/custom-elements-helpers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test('constructor', async (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView as Window & typeof globalThis & { onerrorCalls: string[] };
+ const frames = [
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ ] as const;
+ let onerrorCalls: string[] = [];
+ window.onerror = () => {
+ onerrorCalls.push('top');
+ };
+ frames[0].onerror = () => {
+ onerrorCalls.push('frame0');
+ };
+ frames[1].onerror = () => {
+ onerrorCalls.push('frame1');
+ };
+ frames[2].onerror = () => {
+ onerrorCalls.push('frame2');
+ };
+ const sourceThrowError = 'throw new parent.frames[2].Error("PASS")';
+
+ window.onerrorCalls = onerrorCalls = [];
+
+ const XFoo = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define('x-foo-constructor', XFoo as CustomElementConstructor);
+
+ frames[0].document.createElement('x-foo-constructor');
+ {
+ const actualArray = onerrorCalls;
+ const expectedArray = ['frame1'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ context.test('connectedCallback', async (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView as Window & typeof globalThis & { onerrorCalls: string[] };
+ const frames = [
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ ] as const;
+ let onerrorCalls: string[] = [];
+ window.onerror = () => {
+ onerrorCalls.push('top');
+ };
+ frames[0].onerror = () => {
+ onerrorCalls.push('frame0');
+ };
+ frames[1].onerror = () => {
+ onerrorCalls.push('frame1');
+ };
+ frames[2].onerror = () => {
+ onerrorCalls.push('frame2');
+ };
+ const sourceThrowError = 'throw new parent.frames[2].Error("PASS")';
+
+ window.onerrorCalls = onerrorCalls = [];
+
+ const XFooConnected = class extends frames[0].HTMLElement {
+ declare static observedAttributes: string[];
+ declare connectedCallback: Function;
+ declare disconnectedCallback: Function;
+ declare attributeChangedCallback: Function;
+ declare adoptedCallback: Function;
+ };
+ XFooConnected.prototype.connectedCallback = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define('x-foo-connected', XFooConnected);
+
+ const el = frames[0].document.createElement('x-foo-connected');
+ frames[0].document.body.append(el);
+
+ {
+ const actualArray = onerrorCalls;
+ const expectedArray = ['frame1'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ context.test('disconnectedCallback', async (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView as Window & typeof globalThis & { onerrorCalls: string[] };
+ const frames = [
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ ] as const;
+ let onerrorCalls: string[] = [];
+ window.onerror = () => {
+ onerrorCalls.push('top');
+ };
+ frames[0].onerror = () => {
+ onerrorCalls.push('frame0');
+ };
+ frames[1].onerror = () => {
+ onerrorCalls.push('frame1');
+ };
+ frames[2].onerror = () => {
+ onerrorCalls.push('frame2');
+ };
+ const sourceThrowError = 'throw new parent.frames[2].Error("PASS")';
+
+ window.onerrorCalls = onerrorCalls = [];
+
+ const XFooDisconnected = class extends frames[0].HTMLElement {
+ declare static observedAttributes: string[];
+ declare connectedCallback: Function;
+ declare disconnectedCallback: Function;
+ declare attributeChangedCallback: Function;
+ declare adoptedCallback: Function;
+ };
+ XFooDisconnected.prototype.disconnectedCallback = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define('x-foo-disconnected', XFooDisconnected);
+
+ const el = frames[0].document.createElement('x-foo-disconnected');
+ frames[0].document.body.append(el);
+ el.remove();
+
+ {
+ const actualArray = onerrorCalls;
+ const expectedArray = ['frame1'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ context.test('attributeChangedCallback', async (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView as Window & typeof globalThis & { onerrorCalls: string[] };
+ const frames = [
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ ] as const;
+ let onerrorCalls: string[] = [];
+ window.onerror = () => {
+ onerrorCalls.push('top');
+ };
+ frames[0].onerror = () => {
+ onerrorCalls.push('frame0');
+ };
+ frames[1].onerror = () => {
+ onerrorCalls.push('frame1');
+ };
+ frames[2].onerror = () => {
+ onerrorCalls.push('frame2');
+ };
+ const sourceThrowError = 'throw new parent.frames[2].Error("PASS")';
+
+ window.onerrorCalls = onerrorCalls = [];
+
+ const XFooAttributeChanged = class extends frames[0].HTMLElement {
+ declare static observedAttributes: string[];
+ declare connectedCallback: Function;
+ declare disconnectedCallback: Function;
+ declare attributeChangedCallback: Function;
+ declare adoptedCallback: Function;
+ };
+ XFooAttributeChanged.observedAttributes = ['foo'];
+ XFooAttributeChanged.prototype.attributeChangedCallback = new frames[1].Function(
+ sourceThrowError,
+ );
+ frames[0].customElements.define('x-foo-attribute-changed', XFooAttributeChanged);
+
+ const el = frames[0].document.createElement('x-foo-attribute-changed');
+ frames[0].document.body.append(el);
+ el.setAttribute('foo', 'bar');
+
+ {
+ const actualArray = onerrorCalls;
+ const expectedArray = ['frame1'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ context.test('adoptedCallback', async (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView as Window & typeof globalThis & { onerrorCalls: string[] };
+ const frames = [
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ await create_window_in_test(document, fixture),
+ ] as const;
+ let onerrorCalls: string[] = [];
+ window.onerror = () => {
+ onerrorCalls.push('top');
+ };
+ frames[0].onerror = () => {
+ onerrorCalls.push('frame0');
+ };
+ frames[1].onerror = () => {
+ onerrorCalls.push('frame1');
+ };
+ frames[2].onerror = () => {
+ onerrorCalls.push('frame2');
+ };
+ const sourceThrowError = 'throw new parent.frames[2].Error("PASS")';
+
+ window.onerrorCalls = onerrorCalls = [];
+
+ const XFooAdopted = class extends frames[0].HTMLElement {
+ declare static observedAttributes: string[];
+ declare connectedCallback: Function;
+ declare disconnectedCallback: Function;
+ declare attributeChangedCallback: Function;
+ declare adoptedCallback: Function;
+ };
+ XFooAdopted.prototype.adoptedCallback = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define('x-foo-adopted', XFooAdopted);
+
+ const el = frames[0].document.createElement('x-foo-adopted');
+ document.body.append(el);
+
+ {
+ const actualArray = onerrorCalls;
+ const expectedArray = ['frame1'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/custom-element-reaction-queue.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/custom-element-reaction-queue.html.ts
new file mode 100644
index 0000000..abcfb52
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/custom-element-reaction-queue.html.ts
@@ -0,0 +1,310 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+import {
+ create_window_in_test,
+ create_constructor_log,
+ create_connected_callback_log,
+ create_disconnected_callback_log,
+ create_adopted_callback_log,
+ create_attribute_changed_callback_log,
+ assert_constructor_log_entry,
+ assert_connected_log_entry,
+ assert_adopted_log_entry,
+ assert_attribute_log_entry,
+ type CallbackLog,
+ type AttributeArguments,
+} from './resources/custom-elements-helpers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Upgrading a custom element must invoke attributeChangedCallback and connectedCallback before start upgrading another element',
+ async (fixture) => {
+ const { document } = fixture;
+ const contentWindow = await create_window_in_test(document, fixture);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.write('');
+ contentDocument.write('');
+
+ const element1 = contentDocument.getElementById('first-element')!;
+ const element2 = contentDocument.getElementById('second-element')!;
+ expect(Object.getPrototypeOf(element1)).toBe(contentWindow.HTMLElement.prototype);
+ expect(Object.getPrototypeOf(element2)).toBe(contentWindow.HTMLElement.prototype);
+
+ let log: CallbackLog[] = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args: unknown[]) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args: AttributeArguments) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() {
+ return ['id'];
+ }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ expect(Object.getPrototypeOf(element1)).toBe(TestElement.prototype);
+ expect(Object.getPrototypeOf(element2)).toBe(TestElement.prototype);
+
+ expect(log.length).toBe(6);
+ assert_constructor_log_entry(expect, log[0]!, element1);
+ assert_attribute_log_entry(expect, log[1]!, {
+ name: 'id',
+ oldValue: null,
+ newValue: 'first-element',
+ namespace: null,
+ });
+ assert_connected_log_entry(expect, log[2]!, element1);
+ assert_constructor_log_entry(expect, log[3]!, element2);
+ assert_attribute_log_entry(expect, log[4]!, {
+ name: 'id',
+ oldValue: null,
+ newValue: 'second-element',
+ namespace: null,
+ });
+ assert_connected_log_entry(expect, log[5]!, element2);
+ },
+ );
+ context.test(
+ 'Upgrading a custom element must not invoke attributeChangedCallback for the attribute that is changed during upgrading',
+ async (fixture) => {
+ const { document } = fixture;
+ const contentWindow = await create_window_in_test(document, fixture);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.write('');
+
+ const element = contentDocument.querySelector('test-element');
+ expect(Object.getPrototypeOf(element)).toBe(contentWindow.HTMLElement.prototype);
+
+ let log: CallbackLog[] = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ this.id = 'foo';
+ this.setAttribute('id', 'foo');
+ this.removeAttribute('id');
+ this.style.fontSize = '10px';
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args: unknown[]) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args: AttributeArguments) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() {
+ return ['id', 'style'];
+ }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ expect(Object.getPrototypeOf(element)).toBe(TestElement.prototype);
+
+ expect(log.length).toBe(2);
+ assert_constructor_log_entry(expect, log[0]!, element);
+ assert_connected_log_entry(expect, log[1]!, element);
+ },
+ );
+ context.test(
+ 'Upgrading a custom element must not invoke disconnectedCallback if the element is disconnected during upgrading',
+ async (fixture) => {
+ const { document } = fixture;
+ const contentWindow = await create_window_in_test(document, fixture);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.write('');
+
+ const element = contentDocument.querySelector('test-element');
+ expect(Object.getPrototypeOf(element)).toBe(contentWindow.HTMLElement.prototype);
+
+ let log: CallbackLog[] = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ this.remove();
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args: unknown[]) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ disconnectedCallback(...args: unknown[]) {
+ log.push(create_disconnected_callback_log(this, ...args));
+ }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ expect(Object.getPrototypeOf(element)).toBe(TestElement.prototype);
+
+ expect(log.length).toBe(2);
+ assert_constructor_log_entry(expect, log[0]!, element);
+ assert_connected_log_entry(expect, log[1]!, element);
+ },
+ );
+ context.test(
+ 'Upgrading a disconnected custom element must not invoke connectedCallback if the element is connected during upgrading',
+ async (fixture) => {
+ const { document } = fixture;
+ const contentWindow = await create_window_in_test(document, fixture);
+
+ const contentDocument = contentWindow.document;
+ const element = contentDocument.createElement('test-element');
+ expect(Object.getPrototypeOf(element)).toBe(contentWindow.HTMLElement.prototype);
+
+ let log: CallbackLog[] = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ contentDocument.documentElement.appendChild(this);
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args: unknown[]) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ contentWindow.customElements.upgrade(element);
+
+ expect(Object.getPrototypeOf(element)).toBe(TestElement.prototype);
+
+ expect(log.length).toBe(1);
+ assert_constructor_log_entry(expect, log[0]!, element);
+ },
+ );
+ context.test(
+ 'Mutating a undefined custom element while upgrading a custom element must not enqueue or invoke reactions on the mutated element',
+ async (fixture) => {
+ const { document } = fixture;
+ const contentWindow = await create_window_in_test(document, fixture);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.write('');
+ contentDocument.write('');
+
+ const element1 = contentDocument.getElementById('first-element')!;
+ const element2 = contentDocument.getElementById('second-element')!;
+ expect(Object.getPrototypeOf(element1)).toBe(contentWindow.HTMLElement.prototype);
+ expect(Object.getPrototypeOf(element2)).toBe(contentWindow.HTMLElement.prototype);
+
+ let log: CallbackLog[] = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ if (this == element1) {
+ element2.setAttribute('title', 'hi');
+ element2.removeAttribute('title');
+ element2.setAttribute('class', 'foo');
+ }
+ }
+ connectedCallback(...args: unknown[]) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args: AttributeArguments) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() {
+ return ['id', 'class', 'title'];
+ }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ expect(Object.getPrototypeOf(element1)).toBe(TestElement.prototype);
+ expect(Object.getPrototypeOf(element2)).toBe(TestElement.prototype);
+
+ expect(log.length).toBe(7);
+ assert_constructor_log_entry(expect, log[0]!, element1);
+ assert_attribute_log_entry(expect, log[1]!, {
+ name: 'id',
+ oldValue: null,
+ newValue: 'first-element',
+ namespace: null,
+ });
+ assert_connected_log_entry(expect, log[2]!, element1);
+ assert_constructor_log_entry(expect, log[3]!, element2);
+ assert_attribute_log_entry(expect, log[4]!, {
+ name: 'id',
+ oldValue: null,
+ newValue: 'second-element',
+ namespace: null,
+ });
+ assert_attribute_log_entry(expect, log[5]!, {
+ name: 'class',
+ oldValue: null,
+ newValue: 'foo',
+ namespace: null,
+ });
+ assert_connected_log_entry(expect, log[6]!, element2);
+ },
+ );
+ context.test(
+ 'Mutating another custom element inside adopted callback must invoke all pending callbacks on the mutated element',
+ async (fixture) => {
+ const { document } = fixture;
+ const contentWindow = await create_window_in_test(document, fixture);
+
+ let log: CallbackLog[] = [];
+ let element1: HTMLElement;
+ let element2: HTMLElement;
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ adoptedCallback(...args: unknown[]) {
+ log.push(create_adopted_callback_log(this, ...args));
+ if (this == element1) element3.setAttribute('id', 'foo');
+ }
+ connectedCallback(...args: unknown[]) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args: AttributeArguments) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() {
+ return ['id', 'class'];
+ }
+ }
+
+ contentWindow.customElements.define('test-element', TestElement);
+
+ let contentDocument = contentWindow.document;
+ element1 = contentDocument.createElement('test-element');
+ element2 = contentDocument.createElement('test-element');
+ const element3 = contentDocument.createElement('test-element');
+ expect(Object.getPrototypeOf(element1)).toBe(TestElement.prototype);
+ expect(Object.getPrototypeOf(element2)).toBe(TestElement.prototype);
+ expect(Object.getPrototypeOf(element3)).toBe(TestElement.prototype);
+
+ expect(log.length).toBe(3);
+ assert_constructor_log_entry(expect, log[0]!, element1);
+ assert_constructor_log_entry(expect, log[1]!, element2);
+ assert_constructor_log_entry(expect, log[2]!, element3);
+ log = [];
+
+ const container = contentDocument.createElement('div');
+ container.appendChild(element1);
+ container.appendChild(element2);
+ container.appendChild(element3);
+
+ const anotherDocument = document.implementation.createHTMLDocument();
+ anotherDocument.documentElement.appendChild(container);
+
+ expect(log.length).toBe(7);
+ assert_adopted_log_entry(expect, log[0]!, element1);
+ assert_adopted_log_entry(expect, log[1]!, element3);
+ assert_connected_log_entry(expect, log[2]!, element3);
+ assert_attribute_log_entry(expect, log[3]!, {
+ name: 'id',
+ oldValue: null,
+ newValue: 'foo',
+ namespace: null,
+ });
+ assert_connected_log_entry(expect, log[4]!, element1);
+ assert_adopted_log_entry(expect, log[5]!, element2);
+ assert_connected_log_entry(expect, log[6]!, element2);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/disconnected-callbacks.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/disconnected-callbacks.html.ts
new file mode 100644
index 0000000..1771adb
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/disconnected-callbacks.html.ts
@@ -0,0 +1,181 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+import { documentNames, document_types } from './resources/custom-elements-helpers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [index, documentName] of documentNames.entries()) {
+ context.test(
+ 'Removing a custom element from ' +
+ documentName +
+ ' must enqueue and invoke disconnectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ doc.documentElement.appendChild(instance);
+ calls = [];
+ doc.documentElement.removeChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Removing an ancestor of custom element from ' +
+ documentName +
+ ' must enqueue and invoke disconnectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ doc.documentElement.appendChild(parent);
+ calls = [];
+ doc.documentElement.removeChild(parent);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Removing a custom element from a shadow tree in ' +
+ documentName +
+ ' must enqueue and invoke disconnectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ doc.documentElement.appendChild(host);
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ shadowRoot.removeChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Removing the shadow host of a custom element from a' +
+ documentName +
+ ' must enqueue and invoke disconnectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ shadowRoot.appendChild(instance);
+ doc.documentElement.appendChild(host);
+
+ calls = [];
+ doc.documentElement.removeChild(host);
+ {
+ const actualArray = calls;
+ const expectedArray = ['disconnected', instance];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ context.test(
+ 'Removing a custom element from a detached shadow tree that belongs to ' +
+ documentName +
+ ' must not enqueue and invoke disconnectedCallback',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ const getDocument = document_types(document, Document)[index]!.create;
+ var calls: unknown[] = [];
+ class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ calls.push('connected', this);
+ }
+ disconnectedCallback() {
+ calls.push('disconnected', this);
+ }
+ }
+ customElements.define('my-custom-element', MyCustomElement);
+
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({ mode: 'closed' });
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ shadowRoot.removeChild(instance);
+ {
+ const actualArray = calls;
+ const expectedArray: never[] = [];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ });
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/form-associated/ElementInternals-form.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/form-associated/ElementInternals-form.html.ts
new file mode 100644
index 0000000..4108b55
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/form-associated/ElementInternals-form.html.ts
@@ -0,0 +1,103 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup = `
+form attribute of ElementInternals
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ "ElementInternals.form should return the target element's form owner",
+ ({ document, HTMLElement, customElements }) => {
+ const form = document.forms[0];
+ expect(
+ (document.getElementById('custom-1') as HTMLElement & { i: ElementInternals }).i.form,
+ ).toBe(form);
+ expect(
+ (document.getElementById('custom-3') as HTMLElement & { i: ElementInternals }).i.form,
+ ).toBe(form);
+
+ // Test upgrading.
+ customElements.define(
+ 'custom-input-upgrade',
+ class extends HTMLElement {
+ static get formAssociated() {
+ return true;
+ }
+ declare internals_: ElementInternals;
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() {
+ return this.internals_;
+ }
+ },
+ );
+ expect(
+ (document.getElementById('custom-2') as HTMLElement & { i: ElementInternals }).i.form,
+ ).toBe(form);
+ expect(
+ (document.getElementById('custom-4') as HTMLElement & { i: ElementInternals }).i.form,
+ ).toBe(form);
+
+ // Test changing name attribuate.
+ let custom5 = document.getElementById('custom-5') as HTMLElement & { i: ElementInternals };
+ expect(custom5.i.form).toBe(null);
+ custom5.setAttribute('form', 'custom-form');
+ expect(custom5.i.form).toBe(form);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'ElementInternals.form should throws NotSupportedError if the target element is not a form-associated custom element',
+ ({ HTMLElement, customElements, DOMException }) => {
+ class NotFormAssociatedElement extends HTMLElement {}
+ customElements.define('not-form-associated-element', NotFormAssociatedElement);
+ const element = new NotFormAssociatedElement();
+ const i = element.attachInternals();
+ expect(() => i.form).toThrow(
+ expect.objectContaining({
+ name: 'NotSupportedError',
+ code: 9,
+ constructor: DOMException,
+ }),
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/resources/custom-elements-helpers.js.ts b/packages/web-platform-tests/src/wpt/custom-elements/resources/custom-elements-helpers.js.ts
new file mode 100644
index 0000000..3c06344
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/resources/custom-elements-helpers.js.ts
@@ -0,0 +1,251 @@
+import type { CompatibilityExpect, DOMFixture } from '../../../shared/context.ts';
+export type AttributeArguments = [
+ name: string,
+ oldValue: string | null,
+ newValue: string | null,
+ namespace: string | null,
+];
+export interface CallbackLog {
+ type: string;
+ element: Element;
+ name?: string;
+ namespace?: string | null;
+ oldValue?: string | null;
+ newValue?: string | null;
+ actualValue?: string | null;
+ oldDocument?: Document;
+ newDocument?: Document;
+}
+export const documentNames = [
+ 'the document',
+ 'the document of the template elements',
+ 'a new document',
+ 'a cloned document',
+ 'a document created by createHTMLDocument',
+ 'an HTML document created by createDocument',
+ 'the document of an iframe',
+] as const;
+export function document_types(document: Document, Document: typeof globalThis.Document) {
+ return [
+ {
+ name: 'the document',
+ create: function () {
+ return Promise.resolve(document);
+ },
+ isOwner: true,
+ hasBrowsingContext: true,
+ },
+ {
+ name: 'the document of the template elements',
+ create: function () {
+ return new Promise(function (resolve) {
+ var template = document.createElementNS(
+ 'http://www.w3.org/1999/xhtml',
+ 'template',
+ ) as HTMLTemplateElement;
+ var doc = template.content.ownerDocument!;
+ if (!doc.documentElement) doc.appendChild(doc.createElement('html'));
+ resolve(doc);
+ });
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'a new document',
+ create: function () {
+ return new Promise(function (resolve) {
+ var doc = new Document();
+ doc.appendChild(doc.createElement('html'));
+ resolve(doc);
+ });
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'a cloned document',
+ create: function () {
+ return new Promise(function (resolve) {
+ var doc = document.cloneNode(false) as Document;
+ doc.appendChild(doc.createElement('html'));
+ resolve(doc);
+ });
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'a document created by createHTMLDocument',
+ create: function () {
+ return Promise.resolve(document.implementation.createHTMLDocument());
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'an HTML document created by createDocument',
+ create: function () {
+ return Promise.resolve(
+ document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null),
+ );
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'the document of an iframe',
+ create: function () {
+ return new Promise(function (resolve, reject) {
+ var iframe = document.createElement('iframe');
+ iframe.onload = function () {
+ resolve(iframe.contentDocument!);
+ };
+ iframe.onerror = function () {
+ reject('Failed to load an empty iframe');
+ };
+ document.body.appendChild(iframe);
+ });
+ },
+ hasBrowsingContext: true,
+ },
+ ];
+}
+export function create_window_in_test(
+ document: Document,
+ { cleanup }: DOMFixture,
+ srcdoc?: string,
+) {
+ let p = new Promise((resolve) => {
+ let f = document.createElement('iframe');
+ f.srcdoc = srcdoc ? srcdoc : '';
+ f.onload = (event) => {
+ let w = f.contentWindow as Window & typeof globalThis;
+ cleanup(() => f.remove());
+ resolve(w);
+ };
+ document.body.appendChild(f);
+ });
+ return p;
+}
+
+export function create_constructor_log(element: Element, ..._args: unknown[]) {
+ return { type: 'constructed', element: element };
+}
+
+export function assert_constructor_log_entry(
+ expect: CompatibilityExpect,
+ log: CallbackLog,
+ element: Element | null,
+) {
+ expect(log.type).toBe('constructed');
+ expect(log.element).toBe(element);
+}
+
+export function create_connected_callback_log(element: Element, ..._args: unknown[]) {
+ return { type: 'connected', element: element };
+}
+
+export function assert_connected_log_entry(
+ expect: CompatibilityExpect,
+ log: CallbackLog,
+ element: Element | null,
+) {
+ expect(log.type).toBe('connected');
+ expect(log.element).toBe(element);
+}
+
+export function create_disconnected_callback_log(element: Element, ..._args: unknown[]) {
+ return { type: 'disconnected', element: element };
+}
+
+export function assert_adopted_log_entry(
+ expect: CompatibilityExpect,
+ log: CallbackLog,
+ element: Element | null,
+) {
+ expect(log.type).toBe('adopted');
+ expect(log.element).toBe(element);
+}
+
+export function create_adopted_callback_log(element: Element, ..._args: unknown[]) {
+ return { type: 'adopted', element: element };
+}
+
+export function create_attribute_changed_callback_log(
+ element: Element,
+ ...[name, oldValue, newValue, namespace]: AttributeArguments
+) {
+ return {
+ type: 'attributeChanged',
+ element: element,
+ name: name,
+ namespace: namespace,
+ oldValue: oldValue,
+ newValue: newValue,
+ actualValue: element.getAttributeNS(namespace, name),
+ };
+}
+
+export function assert_attribute_log_entry(
+ expect: CompatibilityExpect,
+ log: CallbackLog,
+ expected: {
+ name: string;
+ oldValue: string | null;
+ newValue: string | null;
+ namespace: string | null;
+ },
+) {
+ expect(log.type).toBe('attributeChanged');
+ expect(log.name).toBe(expected.name);
+ expect(log.oldValue).toBe(expected.oldValue);
+ expect(log.newValue).toBe(expected.newValue);
+ expect(log.actualValue).toBe(expected.newValue);
+ expect(log.namespace).toBe(expected.namespace);
+}
+
+export function define_new_custom_element(
+ HTMLElement: typeof globalThis.HTMLElement,
+ customElements: CustomElementRegistry,
+ observedAttributes: Iterable,
+) {
+ let log: CallbackLog[] = [];
+ let name = 'custom-element-' + define_new_custom_element._element_number++;
+
+ class CustomElement extends HTMLElement {
+ declare static observedAttributes: Iterable;
+ constructor() {
+ super();
+ log.push({ type: 'constructed', element: this });
+ }
+ attributeChangedCallback(...args: AttributeArguments) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ connectedCallback() {
+ log.push({ type: 'connected', element: this });
+ }
+ disconnectedCallback() {
+ log.push({ type: 'disconnected', element: this });
+ }
+ adoptedCallback(oldDocument: Document, newDocument: Document) {
+ log.push({
+ type: 'adopted',
+ element: this,
+ oldDocument: oldDocument,
+ newDocument: newDocument,
+ });
+ }
+ }
+ CustomElement.observedAttributes = observedAttributes;
+
+ customElements.define(name, CustomElement);
+
+ return {
+ name: name,
+ class: CustomElement,
+ takeLog: function () {
+ let currentLog = log as CallbackLog[] & { types(): string[]; last(): CallbackLog };
+ log = [];
+ currentLog.types = () => currentLog.map((entry) => entry.type);
+ currentLog.last = () => currentLog[currentLog.length - 1]!;
+ return currentLog;
+ },
+ };
+}
+define_new_custom_element._element_number = 1;
diff --git a/packages/web-platform-tests/src/wpt/custom-elements/upgrading.html.ts b/packages/web-platform-tests/src/wpt/custom-elements/upgrading.html.ts
new file mode 100644
index 0000000..1fe8554
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/custom-elements/upgrading.html.ts
@@ -0,0 +1,465 @@
+import type { CompatibilityContext } from '../../shared/context.ts';
+import { documentNames, document_types } from './resources/custom-elements-helpers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [index, documentName] of documentNames.entries()) {
+ if (index === 0) continue;
+ if (index !== 6) {
+ context.test(
+ 'Creating an element in ' +
+ documentName +
+ ' must not enqueue a custom element upgrade reaction' +
+ ' because the document does not have a browsing context',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ return getDocument().then(function (doc) {
+ expect(
+ doc.createElement('predefined-custom-element') instanceof PredefinedCustomElement,
+ ).toBe(false);
+ });
+ },
+ );
+ context.test(
+ 'Creating an element in ' +
+ documentName +
+ ' and inserting into the document must not enqueue a custom element upgrade reaction',
+ async ({ document, HTMLElement, customElements, Element, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ var name = generateNextCustomElementName();
+ var unresolvedElement = document.createElement(name);
+
+ expect(
+ Object.getPrototypeOf(unresolvedElement),
+ '[[Prototype]] internal slot of the unresolved custom element must be the HTMLElement prototype',
+ ).toBe(HTMLElement.prototype);
+
+ return getDocument().then(function (doc) {
+ var unresolvedElementInDoc = doc.createElement(name);
+ var prototype = (
+ unresolvedElementInDoc.namespaceURI == 'http://www.w3.org/1999/xhtml'
+ ? HTMLElement
+ : Element
+ ).prototype;
+
+ expect(
+ Object.getPrototypeOf(unresolvedElementInDoc),
+ '[[Prototype]] internal slot of the unresolved custom element must be the ' +
+ prototype.toString() +
+ ' prototype',
+ ).toBe(prototype);
+ var someCustomElement = class extends HTMLElement {};
+ customElements.define(name, someCustomElement);
+ expect(
+ Object.getPrototypeOf(unresolvedElementInDoc),
+ '"define" must not upgrade a disconnected unresolved custom elements',
+ ).toBe(prototype);
+ doc.documentElement.appendChild(unresolvedElementInDoc);
+ expect(
+ Object.getPrototypeOf(unresolvedElementInDoc),
+ 'Inserting an element into a document without a browsing context must not enqueue a custom element upgrade reaction',
+ ).toBe(prototype);
+ });
+ },
+ );
+ context.test(
+ 'Creating an element in ' +
+ documentName +
+ ' and adopting back to a document with browsing context must enqueue a custom element upgrade reaction',
+ async ({ document, HTMLElement, customElements, Element, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ var name = generateNextCustomElementName();
+ var unresolvedElement = document.createElement(name);
+
+ expect(
+ Object.getPrototypeOf(unresolvedElement),
+ '[[Prototype]] internal slot of the unresolved custom element must be the HTMLElement prototype',
+ ).toBe(HTMLElement.prototype);
+
+ return getDocument().then(function (doc) {
+ var unresolvedElementInDoc = doc.createElement(name);
+ var prototype = (
+ unresolvedElementInDoc.namespaceURI == 'http://www.w3.org/1999/xhtml'
+ ? HTMLElement
+ : Element
+ ).prototype;
+
+ expect(
+ Object.getPrototypeOf(unresolvedElementInDoc),
+ '[[Prototype]] internal slot of the unresolved custom element must be the ' +
+ prototype.toString() +
+ ' prototype',
+ ).toBe(prototype);
+ var someCustomElement = class extends HTMLElement {};
+ customElements.define(name, someCustomElement);
+ expect(
+ Object.getPrototypeOf(unresolvedElementInDoc),
+ '"define" must not upgrade a disconnected unresolved custom elements',
+ ).toBe(prototype);
+ document.body.appendChild(unresolvedElementInDoc);
+
+ if (unresolvedElementInDoc.namespaceURI == 'http://www.w3.org/1999/xhtml') {
+ expect(
+ Object.getPrototypeOf(unresolvedElementInDoc),
+ 'Inserting an element into a document with a browsing context must enqueue a custom element upgrade reaction',
+ ).toBe(someCustomElement.prototype);
+ } else {
+ expect(
+ Object.getPrototypeOf(unresolvedElementInDoc),
+ 'Looking up a custom element definition must return null if the element is not in the HTML namespace',
+ ).toBe(prototype);
+ }
+ });
+ },
+ );
+ } else {
+ context.test(
+ 'Creating an element in ' +
+ documentName +
+ ' must not enqueue a custom element upgrade reaction if there is no matching definition',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ return getDocument().then(function (doc) {
+ expect(
+ doc.createElement('predefined-custom-element') instanceof PredefinedCustomElement,
+ ).toBe(false);
+ });
+ },
+ );
+ context.test(
+ 'Creating an element in ' +
+ documentName +
+ ' must enqueue a custom element upgrade reaction if there is a matching definition',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView as Window & typeof globalThis;
+ class DistinctPredefinedCustomElement extends docWindow.HTMLElement {}
+ docWindow.customElements.define(
+ 'predefined-custom-element',
+ DistinctPredefinedCustomElement,
+ );
+ expect(
+ doc.createElement('predefined-custom-element') instanceof
+ DistinctPredefinedCustomElement,
+ ).toBe(true);
+ });
+ },
+ );
+ context.test(
+ '"define" in ' +
+ documentName +
+ ' must not enqueue a custom element upgrade reaction on a disconnected unresolved custom element',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView as Window & typeof globalThis;
+ class UnresolvedElement extends docWindow.HTMLElement {}
+ var unresolvedElementInDoc = doc.createElement('unresolved-element');
+
+ expect(Object.getPrototypeOf(unresolvedElement)).toBe(HTMLElement.prototype);
+ expect(Object.getPrototypeOf(unresolvedElementInDoc)).toBe(
+ docWindow.HTMLElement.prototype,
+ );
+
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+
+ expect(Object.getPrototypeOf(unresolvedElement)).toBe(HTMLElement.prototype);
+ expect(Object.getPrototypeOf(unresolvedElementInDoc)).toBe(
+ docWindow.HTMLElement.prototype,
+ );
+ });
+ },
+ );
+ context.test(
+ 'Inserting an unresolved custom element into ' +
+ documentName +
+ ' must enqueue a custom element upgrade reaction',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView as Window & typeof globalThis;
+ class UnresolvedElement extends docWindow.HTMLElement {}
+ var unresolvedElementInDoc = doc.createElement('unresolved-element');
+
+ expect(Object.getPrototypeOf(unresolvedElement)).toBe(HTMLElement.prototype);
+ expect(Object.getPrototypeOf(unresolvedElementInDoc)).toBe(
+ docWindow.HTMLElement.prototype,
+ );
+
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+ doc.documentElement.appendChild(unresolvedElementInDoc);
+
+ expect(Object.getPrototypeOf(unresolvedElement)).toBe(HTMLElement.prototype);
+ expect(Object.getPrototypeOf(unresolvedElementInDoc)).toBe(UnresolvedElement.prototype);
+ });
+ },
+ );
+ context.test(
+ '"define" in ' +
+ documentName +
+ ' must enqueue a custom element upgrade reaction on a connected unresolved custom element',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView as Window & typeof globalThis;
+ class UnresolvedElement extends docWindow.HTMLElement {}
+ var unresolvedElementInDoc = doc.createElement('unresolved-element');
+ doc.documentElement.appendChild(unresolvedElementInDoc);
+
+ expect(Object.getPrototypeOf(unresolvedElement)).toBe(HTMLElement.prototype);
+ expect(Object.getPrototypeOf(unresolvedElementInDoc)).toBe(
+ docWindow.HTMLElement.prototype,
+ );
+
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+
+ expect(Object.getPrototypeOf(unresolvedElement)).toBe(HTMLElement.prototype);
+ expect(Object.getPrototypeOf(unresolvedElementInDoc)).toBe(UnresolvedElement.prototype);
+ });
+ },
+ );
+ context.test(
+ 'Adopting (and leaving disconnceted) an unresolved custom element into ' +
+ documentName +
+ ' must not enqueue a custom element upgrade reaction',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView as Window & typeof globalThis;
+ class UnresolvedElement extends docWindow.HTMLElement {}
+ expect(unresolvedElement instanceof UnresolvedElement).toBe(false);
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+ doc.adoptNode(unresolvedElement);
+ expect(unresolvedElement instanceof UnresolvedElement).toBe(false);
+ });
+ },
+ );
+ context.test(
+ 'Adopting and inserting an unresolved custom element into ' +
+ documentName +
+ ' must enqueue a custom element upgrade reaction',
+ async ({ document, HTMLElement, customElements, Document }) => {
+ class PredefinedCustomElement extends HTMLElement {}
+ customElements.define('predefined-custom-element', PredefinedCustomElement);
+ var customElementNumber = 1;
+ function generateNextCustomElementName() {
+ return 'custom-' + customElementNumber++;
+ }
+ const getDocument = document_types(document, Document)[index]!.create;
+
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView as Window & typeof globalThis;
+ class UnresolvedElement extends docWindow.HTMLElement {}
+ expect(unresolvedElement instanceof UnresolvedElement).toBe(false);
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+ doc.documentElement.appendChild(unresolvedElement);
+ expect(unresolvedElement instanceof UnresolvedElement).toBe(true);
+ });
+ },
+ );
+ }
+ }
+
+ context.test(
+ 'Infinite constructor recursion with upgrade(this) should not be possible',
+ ({ HTMLElement, customElements, document }) => {
+ var log: (HTMLElement | string)[][] = [];
+
+ customElements.define(
+ 'infinite-cloning-element-1',
+ class extends HTMLElement {
+ constructor() {
+ super();
+ log.push([this, 'begin']);
+ // Potential infinite recursion:
+ customElements.upgrade(this);
+ log.push([this, 'end']);
+ }
+ },
+ );
+
+ expect(log.length).toBe(2);
+ const instance = document.querySelector('infinite-cloning-element-1');
+ {
+ const actualArray = log[0]!;
+ const expectedArray = [instance, 'begin'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ {
+ const actualArray = log[1]!;
+ const expectedArray = [instance, 'end'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n\n\nCustom Elements: Enqueue a custom element upgrade reaction \n \n \n \n \n \n\n\n\n\n\n \n \n \n
\n\n\n\n',
+ },
+ },
+ );
+ context.test(
+ 'Infinite constructor recursion with appendChild should not be possible',
+ ({ HTMLElement, customElements, document }) => {
+ var log: (HTMLElement | string)[][] = [];
+
+ customElements.define(
+ 'infinite-cloning-element-2',
+ class extends HTMLElement {
+ constructor() {
+ super();
+ log.push([this, 'begin']);
+ const b = document.querySelector('#b')!;
+ b.remove();
+ // While this constructor is running for "a", "b" is still
+ // undefined, and so inserting it into the document will enqueue a
+ // second upgrade reaction for "b" in addition to the one enqueued
+ // by defining x-foo.
+ document.body.appendChild(b);
+ log.push([this, 'end']);
+ }
+ },
+ );
+
+ expect(log.length).toBe(4);
+ const instanceA = document.querySelector('#a');
+ const instanceB = document.querySelector('#b');
+ {
+ const actualArray = log[0]!;
+ const expectedArray = [instanceA, 'begin'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ {
+ const actualArray = log[1]!;
+ const expectedArray = [instanceB, 'begin'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ {
+ const actualArray = log[2]!;
+ const expectedArray = [instanceB, 'end'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ {
+ const actualArray = log[3]!;
+ const expectedArray = [instanceA, 'end'];
+ expect(actualArray.length).toBe(expectedArray.length);
+ for (let index = 0; index < expectedArray.length; index++)
+ expect(actualArray[index]).toBe(expectedArray[index]);
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n\n\nCustom Elements: Enqueue a custom element upgrade reaction \n \n \n \n \n \n\n\n\n\n\n \n \n \n
\n\n\n\n',
+ },
+ },
+ );
+
+ context.test(
+ "If definition's disable shadow is true and element's shadow root is " +
+ 'non-null, then throw a "NotSupportedError" DOMException.',
+ ({ document, HTMLElement, customElements, DOMException, cleanup }) => {
+ const window = document.defaultView!;
+ class ShadowDisabledElement extends HTMLElement {
+ static get disabledFeatures() {
+ return ['shadow'];
+ }
+ }
+ let error: unknown = null;
+ const listener = (e: ErrorEvent) => {
+ error = e.error;
+ e.preventDefault();
+ };
+ window.addEventListener('error', listener, { once: true });
+ cleanup(() => window.removeEventListener('error', listener));
+ let element = document.createElement('shadow-disabled');
+ element.attachShadow({ mode: 'open' });
+ customElements.define('shadow-disabled', ShadowDisabledElement);
+ customElements.upgrade(element);
+ expect(element instanceof ShadowDisabledElement, 'Upgrading should fail.').toBe(false);
+ expect(error instanceof DOMException).toBe(true);
+ expect((error as DOMException).name).toBe('NotSupportedError');
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/common.js.ts b/packages/web-platform-tests/src/wpt/dom/common.js.ts
new file mode 100644
index 0000000..4af706a
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/common.js.ts
@@ -0,0 +1,1463 @@
+import type { DOMFixture, CompatibilityExpect } from '../../shared/context.ts';
+export const nodeNames = [
+ 'paras[0]',
+ 'paras[0].firstChild',
+ 'paras[1].firstChild',
+ 'paras[5].firstChild',
+ 'foreignPara1',
+ 'foreignPara1.firstChild',
+ 'detachedPara1',
+ 'detachedPara1.firstChild',
+ 'document',
+ 'detachedDiv',
+ 'foreignDoc',
+ 'foreignPara2',
+ 'xmlDoc',
+ 'xmlElement',
+ 'detachedTextNode',
+ 'foreignTextNode',
+ 'processingInstruction',
+ 'detachedProcessingInstruction',
+ 'comment',
+ 'detachedComment',
+ 'docfrag',
+ 'doctype',
+ 'foreignDoctype',
+ 'paras[1]',
+ 'detachedPara2',
+ 'detachedPara2.firstChild',
+ 'testDiv',
+ 'detachedXmlElement',
+ 'detachedForeignTextNode',
+ 'xmlTextNode',
+ 'detachedXmlTextNode',
+ 'xmlComment',
+ 'foreignComment',
+ 'detachedForeignComment',
+ 'detachedXmlComment',
+ 'foreignDocfrag',
+ 'xmlDocfrag',
+ 'xmlDoctype',
+] as const;
+export function createNodes({
+ document,
+ Document,
+}: DOMFixture): Record<(typeof nodeNames)[number], Node> {
+ let testDiv = document.querySelector('#test');
+ if (testDiv) {
+ testDiv.parentNode!.removeChild(testDiv);
+ }
+ testDiv = document.createElement('div');
+ testDiv.id = 'test';
+ document.body.insertBefore(testDiv, document.body.firstChild);
+
+ const paras = [];
+ paras.push(document.createElement('p'));
+ paras[0]!.setAttribute('id', 'a');
+ paras[0]!.textContent = 'A\u0308b\u0308c\u0308d\u0308e\u0308f\u0308g\u0308h\u0308\n';
+ testDiv.appendChild(paras[0]!);
+
+ paras.push(document.createElement('p'));
+ paras[1]!.setAttribute('id', 'b');
+ paras[1]!.setAttribute('style', 'display:none');
+ paras[1]!.textContent = 'Ijklmnop\n';
+ testDiv.appendChild(paras[1]!);
+
+ paras.push(document.createElement('p'));
+ paras[2]!.setAttribute('id', 'c');
+ paras[2]!.textContent = 'Qrstuvwx';
+ testDiv.appendChild(paras[2]!);
+
+ paras.push(document.createElement('p'));
+ paras[3]!.setAttribute('id', 'd');
+ paras[3]!.setAttribute('style', 'display:none');
+ paras[3]!.textContent = 'Yzabcdef';
+ testDiv.appendChild(paras[3]!);
+
+ paras.push(document.createElement('p'));
+ paras[4]!.setAttribute('id', 'e');
+ paras[4]!.setAttribute('style', 'display:none');
+ paras[4]!.textContent = 'Ghijklmn';
+ testDiv.appendChild(paras[4]!);
+
+ paras.push(document.createElement('p'));
+ const xmlDocument = new Document();
+ paras[5]!.appendChild(xmlDocument.createCDATASection('1234'));
+ paras[5]!.appendChild(xmlDocument.createCDATASection('5678'));
+ paras[5]!.append('9012');
+ testDiv.appendChild(paras[5]!);
+
+ const detachedDiv = document.createElement('div');
+
+ const detachedPara1 = document.createElement('p');
+ detachedPara1.appendChild(document.createTextNode('Opqrstuv'));
+
+ const detachedPara2 = document.createElement('p');
+ detachedPara2.appendChild(document.createTextNode('Wxyzabcd'));
+ detachedDiv.appendChild(detachedPara1);
+ detachedDiv.appendChild(detachedPara2);
+
+ const foreignDoc = document.implementation.createHTMLDocument('');
+
+ const foreignPara1 = foreignDoc.createElement('p');
+ foreignPara1.appendChild(foreignDoc.createTextNode('Efghijkl'));
+
+ const foreignPara2 = foreignDoc.createElement('p');
+ foreignPara2.appendChild(foreignDoc.createTextNode('Mnopqrst'));
+ foreignDoc.body.appendChild(foreignPara1);
+ foreignDoc.body.appendChild(foreignPara2);
+
+ const xmlDoctype = document.implementation.createDocumentType('qorflesnorf', 'abcde', 'x"\'y');
+
+ const xmlDoc = document.implementation.createDocument(
+ null,
+ null as unknown as string,
+ xmlDoctype,
+ );
+
+ const detachedXmlElement = xmlDoc.createElement('everyone-hates-hyphenated-element-names');
+
+ const detachedTextNode = document.createTextNode('Uvwxyzab');
+
+ const detachedForeignTextNode = foreignDoc.createTextNode('Cdefghij');
+
+ const detachedXmlTextNode = xmlDoc.createTextNode('Klmnopqr');
+
+ const detachedProcessingInstruction = xmlDoc.createProcessingInstruction(
+ 'whippoorwill',
+ 'chirp chirp chirp',
+ );
+
+ const detachedComment = document.createComment('Stuvwxyz');
+
+ const detachedForeignComment = foreignDoc.createComment('אריה יהודה');
+
+ const detachedXmlComment = xmlDoc.createComment('בן חיים אליעזר');
+
+ const docfrag = document.createDocumentFragment();
+
+ const foreignDocfrag = foreignDoc.createDocumentFragment();
+
+ const xmlDocfrag = xmlDoc.createDocumentFragment();
+
+ const xmlElement = xmlDoc.createElement('igiveuponcreativenames');
+
+ const xmlTextNode = xmlDoc.createTextNode('do re mi fa so la ti');
+ xmlElement.appendChild(xmlTextNode);
+
+ const processingInstruction = xmlDoc.createProcessingInstruction(
+ 'somePI',
+ 'Did you know that ":syn sync fromstart" is very useful when using vim to edit large amounts of JavaScript embedded in HTML?',
+ );
+ xmlDoc.appendChild(xmlElement);
+ xmlDoc.appendChild(processingInstruction);
+
+ const xmlComment = xmlDoc.createComment(
+ 'I maliciously created a comment that will break incautious XML serializers, but Firefox threw an exception, so all I got was this lousy T-shirt',
+ );
+ xmlDoc.appendChild(xmlComment);
+
+ const comment = document.createComment('Alphabet soup?');
+ testDiv.appendChild(comment);
+
+ const foreignComment = foreignDoc.createComment(
+ '"Commenter" and "commentator" mean different things. I\'ve seen non-native speakers trip up on this.',
+ );
+ foreignDoc.appendChild(foreignComment);
+
+ const foreignTextNode = foreignDoc.createTextNode(
+ "I admit that I harbor doubts about whether we really need so many things to test, but it's too late to stop now.",
+ );
+ foreignDoc.body.appendChild(foreignTextNode);
+
+ const doctype = document.doctype;
+
+ const foreignDoctype = foreignDoc.doctype;
+
+ return {
+ 'paras[0]': paras[0]!,
+ 'paras[0].firstChild': paras[0]!.firstChild!,
+ 'paras[1].firstChild': paras[1]!.firstChild!,
+ 'paras[5].firstChild': paras[5]!.firstChild!,
+ foreignPara1: foreignPara1,
+ 'foreignPara1.firstChild': foreignPara1.firstChild!,
+ detachedPara1: detachedPara1,
+ 'detachedPara1.firstChild': detachedPara1.firstChild!,
+ document: document,
+ detachedDiv: detachedDiv,
+ foreignDoc: foreignDoc,
+ foreignPara2: foreignPara2,
+ xmlDoc: xmlDoc,
+ xmlElement: xmlElement,
+ detachedTextNode: detachedTextNode,
+ foreignTextNode: foreignTextNode,
+ processingInstruction: processingInstruction,
+ detachedProcessingInstruction: detachedProcessingInstruction,
+ comment: comment,
+ detachedComment: detachedComment,
+ docfrag: docfrag,
+ doctype: doctype!,
+ foreignDoctype: foreignDoctype!,
+ 'paras[1]': paras[1]!,
+ detachedPara2: detachedPara2,
+ 'detachedPara2.firstChild': detachedPara2.firstChild!,
+ testDiv: testDiv,
+ detachedXmlElement: detachedXmlElement,
+ detachedForeignTextNode: detachedForeignTextNode,
+ xmlTextNode: xmlTextNode,
+ detachedXmlTextNode: detachedXmlTextNode,
+ xmlComment: xmlComment,
+ foreignComment: foreignComment,
+ detachedForeignComment: detachedForeignComment,
+ detachedXmlComment: detachedXmlComment,
+ foreignDocfrag: foreignDocfrag,
+ xmlDocfrag: xmlDocfrag,
+ xmlDoctype: xmlDoctype,
+ };
+}
+export function furthestAncestor(node: Node): Node {
+ let root = node;
+ while (root.parentNode != null) {
+ root = root.parentNode;
+ }
+ return root;
+}
+export function previousNode(node: Node): Node | null {
+ if (node.previousSibling) {
+ node = node.previousSibling;
+ while (node.hasChildNodes()) {
+ node = node.lastChild!;
+ }
+ return node;
+ }
+ return node.parentNode;
+}
+
+export function isAncestor(ancestor: Node | null, descendant: Node | null): boolean {
+ if (!ancestor || !descendant) return false;
+ while (descendant && descendant != ancestor) descendant = descendant.parentNode;
+ return descendant == ancestor;
+}
+export function isInclusiveAncestor(ancestor: Node | null, descendant: Node | null): boolean {
+ return ancestor === descendant || isAncestor(ancestor, descendant);
+}
+
+// Written by Aryeh Gregor
+export const testRanges = [
+ '[paras[0].firstChild, 0, paras[0].firstChild, 0]',
+ '[paras[0].firstChild, 0, paras[0].firstChild, 1]',
+ '[paras[0].firstChild, 2, paras[0].firstChild, 8]',
+ '[paras[0].firstChild, 2, paras[0].firstChild, 9]',
+ '[paras[1].firstChild, 0, paras[1].firstChild, 0]',
+ '[paras[1].firstChild, 2, paras[1].firstChild, 9]',
+ '[paras[5].firstChild, 2, paras[5].lastChild, 4]',
+ '[paras[5].firstChild, 1, paras[5].firstChild, 3]',
+ '[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]',
+ '[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]',
+ '[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]',
+ '[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]',
+ '[document.documentElement, 0, document.documentElement, 1]',
+ '[document.documentElement, 0, document.documentElement, 2]',
+ '[document.documentElement, 1, document.documentElement, 2]',
+ '[document.head, 1, document.head, 1]',
+ '[document.body, 4, document.body, 5]',
+ '[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]',
+ '[paras[0], 0, paras[0], 1]',
+ '[detachedPara1, 0, detachedPara1, 1]',
+ '[paras[0].firstChild, 0, paras[1].firstChild, 0]',
+ '[paras[0].firstChild, 0, paras[1].firstChild, 8]',
+ '[paras[0].firstChild, 3, paras[3], 1]',
+ '[paras[0], 0, paras[0].firstChild, 7]',
+ '[testDiv, 2, paras[4], 1]',
+ '[document, 0, document, 1]',
+ '[document, 0, document, 2]',
+ '[comment, 2, comment, 3]',
+ '[testDiv, 0, comment, 5]',
+ '[foreignDoc, 1, foreignComment, 2]',
+ '[foreignDoc.body, 0, foreignTextNode, 36]',
+ '[xmlDoc, 1, xmlComment, 0]',
+ '[detachedTextNode, 0, detachedTextNode, 8]',
+ '[detachedForeignTextNode, 0, detachedForeignTextNode, 8]',
+ '[detachedXmlTextNode, 0, detachedXmlTextNode, 8]',
+ '[detachedComment, 3, detachedComment, 4]',
+ '[detachedForeignComment, 0, detachedForeignComment, 1]',
+ '[detachedXmlComment, 2, detachedXmlComment, 6]',
+ '[docfrag, 0, docfrag, 0]',
+ '[processingInstruction, 0, processingInstruction, 4]',
+ '[paras[1].firstChild, 0, paras[1].firstChild, 1]',
+ '[paras[1].firstChild, 2, paras[1].firstChild, 8]',
+ '[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]',
+ '[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]',
+ '[foreignDoc.head, 1, foreignDoc.head, 1]',
+ '[foreignDoc.body, 0, foreignDoc.body, 0]',
+ '[paras[0], 0, paras[0], 0]',
+ '[detachedPara1, 0, detachedPara1, 0]',
+ '[testDiv, 1, paras[2].firstChild, 5]',
+ '[document.documentElement, 1, document.body, 0]',
+ '[foreignDoc.documentElement, 1, foreignDoc.body, 0]',
+ '[document, 1, document, 2]',
+ '[paras[2].firstChild, 4, comment, 2]',
+ '[paras[3], 1, comment, 8]',
+ '[foreignDoc, 0, foreignDoc, 0]',
+ '[xmlDoc, 0, xmlDoc, 0]',
+ '[detachedForeignTextNode, 7, detachedForeignTextNode, 7]',
+ '[detachedXmlTextNode, 7, detachedXmlTextNode, 7]',
+ '[detachedComment, 5, detachedComment, 5]',
+ '[detachedForeignComment, 4, detachedForeignComment, 4]',
+ '[foreignDocfrag, 0, foreignDocfrag, 0]',
+ '[xmlDocfrag, 0, xmlDocfrag, 0]',
+] as const;
+export const testRangesShort = [
+ '[paras[0].firstChild, 0, paras[0].firstChild, 0]',
+ '[paras[0].firstChild, 0, paras[0].firstChild, 1]',
+ '[paras[0].firstChild, 2, paras[0].firstChild, 8]',
+ '[paras[0].firstChild, 2, paras[0].firstChild, 9]',
+ '[paras[1].firstChild, 0, paras[1].firstChild, 0]',
+ '[paras[1].firstChild, 2, paras[1].firstChild, 9]',
+ '[paras[5].firstChild, 2, paras[5].lastChild, 4]',
+ '[paras[5].firstChild, 1, paras[5].firstChild, 3]',
+ '[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]',
+ '[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]',
+ '[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]',
+ '[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]',
+ '[document.documentElement, 0, document.documentElement, 1]',
+ '[document.documentElement, 0, document.documentElement, 2]',
+ '[document.documentElement, 1, document.documentElement, 2]',
+ '[document.head, 1, document.head, 1]',
+ '[document.body, 4, document.body, 5]',
+ '[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]',
+ '[paras[0], 0, paras[0], 1]',
+ '[detachedPara1, 0, detachedPara1, 1]',
+ '[paras[0].firstChild, 0, paras[1].firstChild, 0]',
+ '[paras[0].firstChild, 0, paras[1].firstChild, 8]',
+ '[paras[0].firstChild, 3, paras[3], 1]',
+ '[paras[0], 0, paras[0].firstChild, 7]',
+ '[testDiv, 2, paras[4], 1]',
+ '[document, 0, document, 1]',
+ '[document, 0, document, 2]',
+ '[comment, 2, comment, 3]',
+ '[testDiv, 0, comment, 5]',
+ '[foreignDoc, 1, foreignComment, 2]',
+ '[foreignDoc.body, 0, foreignTextNode, 36]',
+ '[xmlDoc, 1, xmlComment, 0]',
+ '[detachedTextNode, 0, detachedTextNode, 8]',
+ '[detachedForeignTextNode, 0, detachedForeignTextNode, 8]',
+ '[detachedXmlTextNode, 0, detachedXmlTextNode, 8]',
+ '[detachedComment, 3, detachedComment, 4]',
+ '[detachedForeignComment, 0, detachedForeignComment, 1]',
+ '[detachedXmlComment, 2, detachedXmlComment, 6]',
+ '[docfrag, 0, docfrag, 0]',
+ '[processingInstruction, 0, processingInstruction, 4]',
+] as const;
+export const testNodesShort = [
+ 'paras[0]',
+ 'paras[0].firstChild',
+ 'paras[1].firstChild',
+ 'paras[5].firstChild',
+ 'foreignPara1',
+ 'foreignPara1.firstChild',
+ 'detachedPara1',
+ 'detachedPara1.firstChild',
+ 'document',
+ 'detachedDiv',
+ 'foreignDoc',
+ 'foreignPara2',
+ 'xmlDoc',
+ 'xmlElement',
+ 'detachedTextNode',
+ 'foreignTextNode',
+ 'processingInstruction',
+ 'detachedProcessingInstruction',
+ 'comment',
+ 'detachedComment',
+ 'docfrag',
+ 'doctype',
+ 'foreignDoctype',
+] as const;
+
+export function createRangeEnvironment(
+ document: Document,
+ globals: Pick,
+ expect: CompatibilityExpect,
+) {
+ const { Document, Node } = globals;
+ const format_value = String;
+ var testDiv: any,
+ paras: any,
+ detachedDiv: any,
+ detachedPara1: any,
+ detachedPara2: any,
+ foreignDoc: any,
+ foreignPara1: any,
+ foreignPara2: any,
+ xmlDoc: any,
+ xmlElement: any,
+ detachedXmlElement: any,
+ detachedTextNode: any,
+ foreignTextNode: any,
+ detachedForeignTextNode: any,
+ xmlTextNode: any,
+ detachedXmlTextNode: any,
+ processingInstruction: any,
+ detachedProcessingInstruction: any,
+ comment: any,
+ detachedComment: any,
+ foreignComment: any,
+ detachedForeignComment: any,
+ xmlComment: any,
+ detachedXmlComment: any,
+ docfrag: any,
+ foreignDocfrag: any,
+ xmlDocfrag: any,
+ doctype: any,
+ foreignDoctype: any,
+ xmlDoctype: any;
+ var testRangesShort: any, testRanges: any, testPoints: any, testNodesShort: any, testNodes: any;
+ function setupRangeTests(): any {
+ testDiv = document.querySelector('#test');
+ if (testDiv) {
+ testDiv.parentNode.removeChild(testDiv);
+ }
+ testDiv = document.createElement('div');
+ testDiv.id = 'test';
+ document.body.insertBefore(testDiv, document.body.firstChild);
+
+ paras = [];
+ paras.push(document.createElement('p'));
+ paras[0].setAttribute('id', 'a');
+ // Test some diacritics, to make sure browsers are using code units here
+ // and not something like grapheme clusters.
+ paras[0].textContent = 'A\u0308b\u0308c\u0308d\u0308e\u0308f\u0308g\u0308h\u0308\n';
+ testDiv.appendChild(paras[0]);
+
+ paras.push(document.createElement('p'));
+ paras[1].setAttribute('id', 'b');
+ paras[1].setAttribute('style', 'display:none');
+ paras[1].textContent = 'Ijklmnop\n';
+ testDiv.appendChild(paras[1]);
+
+ paras.push(document.createElement('p'));
+ paras[2].setAttribute('id', 'c');
+ paras[2].textContent = 'Qrstuvwx';
+ testDiv.appendChild(paras[2]);
+
+ paras.push(document.createElement('p'));
+ paras[3].setAttribute('id', 'd');
+ paras[3].setAttribute('style', 'display:none');
+ paras[3].textContent = 'Yzabcdef';
+ testDiv.appendChild(paras[3]);
+
+ paras.push(document.createElement('p'));
+ paras[4].setAttribute('id', 'e');
+ paras[4].setAttribute('style', 'display:none');
+ paras[4].textContent = 'Ghijklmn';
+ testDiv.appendChild(paras[4]);
+
+ paras.push(document.createElement('p'));
+ const xmlDocument = new Document();
+ paras[5].appendChild(xmlDocument.createCDATASection('1234'));
+ paras[5].appendChild(xmlDocument.createCDATASection('5678'));
+ paras[5].append('9012');
+ testDiv.appendChild(paras[5]);
+
+ detachedDiv = document.createElement('div');
+ detachedPara1 = document.createElement('p');
+ detachedPara1.appendChild(document.createTextNode('Opqrstuv'));
+ detachedPara2 = document.createElement('p');
+ detachedPara2.appendChild(document.createTextNode('Wxyzabcd'));
+ detachedDiv.appendChild(detachedPara1);
+ detachedDiv.appendChild(detachedPara2);
+
+ // Opera doesn't automatically create a doctype for a new HTML document,
+ // contrary to spec. It also doesn't let you add doctypes to documents
+ // after the fact through any means I've tried. So foreignDoc in Opera
+ // will have no doctype, foreignDoctype will be null, and Opera will fail
+ // some tests somewhat mysteriously as a result.
+ foreignDoc = document.implementation.createHTMLDocument('');
+ foreignPara1 = foreignDoc.createElement('p');
+ foreignPara1.appendChild(foreignDoc.createTextNode('Efghijkl'));
+ foreignPara2 = foreignDoc.createElement('p');
+ foreignPara2.appendChild(foreignDoc.createTextNode('Mnopqrst'));
+ foreignDoc.body.appendChild(foreignPara1);
+ foreignDoc.body.appendChild(foreignPara2);
+
+ // Now we get to do really silly stuff, which nobody in the universe is
+ // ever going to actually do, but the spec defines behavior, so too bad.
+ // Testing is fun!
+ xmlDoctype = document.implementation.createDocumentType('qorflesnorf', 'abcde', 'x"\'y');
+ xmlDoc = document.implementation.createDocument(null, null, xmlDoctype);
+ detachedXmlElement = xmlDoc.createElement('everyone-hates-hyphenated-element-names');
+ detachedTextNode = document.createTextNode('Uvwxyzab');
+ detachedForeignTextNode = foreignDoc.createTextNode('Cdefghij');
+ detachedXmlTextNode = xmlDoc.createTextNode('Klmnopqr');
+ // PIs only exist in XML documents, so don't bother with document or
+ // foreignDoc.
+ detachedProcessingInstruction = xmlDoc.createProcessingInstruction(
+ 'whippoorwill',
+ 'chirp chirp chirp',
+ );
+ detachedComment = document.createComment('Stuvwxyz');
+ // Hurrah, we finally got to "z" at the end!
+ detachedForeignComment = foreignDoc.createComment('אריה יהודה');
+ detachedXmlComment = xmlDoc.createComment('בן חיים אליעזר');
+
+ // We should also test with document fragments that actually contain stuff
+ // . . . but, maybe later.
+ docfrag = document.createDocumentFragment();
+ foreignDocfrag = foreignDoc.createDocumentFragment();
+ xmlDocfrag = xmlDoc.createDocumentFragment();
+
+ xmlElement = xmlDoc.createElement('igiveuponcreativenames');
+ xmlTextNode = xmlDoc.createTextNode('do re mi fa so la ti');
+ xmlElement.appendChild(xmlTextNode);
+ processingInstruction = xmlDoc.createProcessingInstruction(
+ 'somePI',
+ 'Did you know that ":syn sync fromstart" is very useful when using vim to edit large amounts of JavaScript embedded in HTML?',
+ );
+ xmlDoc.appendChild(xmlElement);
+ xmlDoc.appendChild(processingInstruction);
+ xmlComment = xmlDoc.createComment(
+ 'I maliciously created a comment that will break incautious XML serializers, but Firefox threw an exception, so all I got was this lousy T-shirt',
+ );
+ xmlDoc.appendChild(xmlComment);
+
+ comment = document.createComment('Alphabet soup?');
+ testDiv.appendChild(comment);
+
+ foreignComment = foreignDoc.createComment(
+ '"Commenter" and "commentator" mean different things. I\'ve seen non-native speakers trip up on this.',
+ );
+ foreignDoc.appendChild(foreignComment);
+ foreignTextNode = foreignDoc.createTextNode(
+ "I admit that I harbor doubts about whether we really need so many things to test, but it's too late to stop now.",
+ );
+ foreignDoc.body.appendChild(foreignTextNode);
+
+ doctype = document.doctype;
+ foreignDoctype = foreignDoc.doctype;
+
+ testRangesShort = [
+ // Various ranges within the text node children of different
+ // paragraphs. All should be valid.
+ '[paras[0].firstChild, 0, paras[0].firstChild, 0]',
+ '[paras[0].firstChild, 0, paras[0].firstChild, 1]',
+ '[paras[0].firstChild, 2, paras[0].firstChild, 8]',
+ '[paras[0].firstChild, 2, paras[0].firstChild, 9]',
+ '[paras[1].firstChild, 0, paras[1].firstChild, 0]',
+ '[paras[1].firstChild, 2, paras[1].firstChild, 9]',
+ '[paras[5].firstChild, 2, paras[5].lastChild, 4]',
+ '[paras[5].firstChild, 1, paras[5].firstChild, 3]',
+ '[detachedPara1.firstChild, 0, detachedPara1.firstChild, 0]',
+ '[detachedPara1.firstChild, 2, detachedPara1.firstChild, 8]',
+ '[foreignPara1.firstChild, 0, foreignPara1.firstChild, 0]',
+ '[foreignPara1.firstChild, 2, foreignPara1.firstChild, 8]',
+ // Now try testing some elements, not just text nodes.
+ '[document.documentElement, 0, document.documentElement, 1]',
+ '[document.documentElement, 0, document.documentElement, 2]',
+ '[document.documentElement, 1, document.documentElement, 2]',
+ '[document.head, 1, document.head, 1]',
+ '[document.body, 4, document.body, 5]',
+ '[foreignDoc.documentElement, 0, foreignDoc.documentElement, 1]',
+ '[paras[0], 0, paras[0], 1]',
+ '[detachedPara1, 0, detachedPara1, 1]',
+ // Now try some ranges that span elements.
+ '[paras[0].firstChild, 0, paras[1].firstChild, 0]',
+ '[paras[0].firstChild, 0, paras[1].firstChild, 8]',
+ '[paras[0].firstChild, 3, paras[3], 1]',
+ // How about something that spans a node and its descendant?
+ '[paras[0], 0, paras[0].firstChild, 7]',
+ '[testDiv, 2, paras[4], 1]',
+ // Then a few more interesting things just for good measure.
+ '[document, 0, document, 1]',
+ '[document, 0, document, 2]',
+ '[comment, 2, comment, 3]',
+ '[testDiv, 0, comment, 5]',
+ '[foreignDoc, 1, foreignComment, 2]',
+ '[foreignDoc.body, 0, foreignTextNode, 36]',
+ '[xmlDoc, 1, xmlComment, 0]',
+ '[detachedTextNode, 0, detachedTextNode, 8]',
+ '[detachedForeignTextNode, 0, detachedForeignTextNode, 8]',
+ '[detachedXmlTextNode, 0, detachedXmlTextNode, 8]',
+ '[detachedComment, 3, detachedComment, 4]',
+ '[detachedForeignComment, 0, detachedForeignComment, 1]',
+ '[detachedXmlComment, 2, detachedXmlComment, 6]',
+ '[docfrag, 0, docfrag, 0]',
+ '[processingInstruction, 0, processingInstruction, 4]',
+ ];
+
+ testRanges = testRangesShort.concat([
+ '[paras[1].firstChild, 0, paras[1].firstChild, 1]',
+ '[paras[1].firstChild, 2, paras[1].firstChild, 8]',
+ '[detachedPara1.firstChild, 0, detachedPara1.firstChild, 1]',
+ '[foreignPara1.firstChild, 0, foreignPara1.firstChild, 1]',
+ '[foreignDoc.head, 1, foreignDoc.head, 1]',
+ '[foreignDoc.body, 0, foreignDoc.body, 0]',
+ '[paras[0], 0, paras[0], 0]',
+ '[detachedPara1, 0, detachedPara1, 0]',
+ '[testDiv, 1, paras[2].firstChild, 5]',
+ '[document.documentElement, 1, document.body, 0]',
+ '[foreignDoc.documentElement, 1, foreignDoc.body, 0]',
+ '[document, 1, document, 2]',
+ '[paras[2].firstChild, 4, comment, 2]',
+ '[paras[3], 1, comment, 8]',
+ '[foreignDoc, 0, foreignDoc, 0]',
+ '[xmlDoc, 0, xmlDoc, 0]',
+ '[detachedForeignTextNode, 7, detachedForeignTextNode, 7]',
+ '[detachedXmlTextNode, 7, detachedXmlTextNode, 7]',
+ '[detachedComment, 5, detachedComment, 5]',
+ '[detachedForeignComment, 4, detachedForeignComment, 4]',
+ '[foreignDocfrag, 0, foreignDocfrag, 0]',
+ '[xmlDocfrag, 0, xmlDocfrag, 0]',
+ ]);
+
+ testPoints = [
+ // Various positions within the page, some invalid. Remember that
+ // paras[0] is visible, and paras[1] is display: none.
+ '[paras[0].firstChild, -1]',
+ '[paras[0].firstChild, 0]',
+ '[paras[0].firstChild, 1]',
+ '[paras[0].firstChild, 2]',
+ '[paras[0].firstChild, 8]',
+ '[paras[0].firstChild, 9]',
+ '[paras[0].firstChild, 10]',
+ '[paras[0].firstChild, 65535]',
+ '[paras[1].firstChild, -1]',
+ '[paras[1].firstChild, 0]',
+ '[paras[1].firstChild, 1]',
+ '[paras[1].firstChild, 2]',
+ '[paras[1].firstChild, 8]',
+ '[paras[1].firstChild, 9]',
+ '[paras[1].firstChild, 10]',
+ '[paras[1].firstChild, 65535]',
+ '[paras[5].firstChild, 2]',
+ '[paras[5].firstChild, 20]',
+ '[detachedPara1.firstChild, 0]',
+ '[detachedPara1.firstChild, 1]',
+ '[detachedPara1.firstChild, 8]',
+ '[detachedPara1.firstChild, 9]',
+ '[foreignPara1.firstChild, 0]',
+ '[foreignPara1.firstChild, 1]',
+ '[foreignPara1.firstChild, 8]',
+ '[foreignPara1.firstChild, 9]',
+ // Now try testing some elements, not just text nodes.
+ '[document.documentElement, -1]',
+ '[document.documentElement, 0]',
+ '[document.documentElement, 1]',
+ '[document.documentElement, 2]',
+ '[document.documentElement, 7]',
+ '[document.head, 1]',
+ '[document.body, 3]',
+ '[foreignDoc.documentElement, 0]',
+ '[foreignDoc.documentElement, 1]',
+ '[foreignDoc.head, 0]',
+ '[foreignDoc.body, 1]',
+ '[paras[0], 0]',
+ '[paras[0], 1]',
+ '[paras[0], 2]',
+ '[paras[1], 0]',
+ '[paras[1], 1]',
+ '[paras[1], 2]',
+ '[detachedPara1, 0]',
+ '[detachedPara1, 1]',
+ '[testDiv, 0]',
+ '[testDiv, 3]',
+ // Then a few more interesting things just for good measure.
+ '[document, -1]',
+ '[document, 0]',
+ '[document, 1]',
+ '[document, 2]',
+ '[document, 3]',
+ '[comment, -1]',
+ '[comment, 0]',
+ '[comment, 4]',
+ '[comment, 96]',
+ '[foreignDoc, 0]',
+ '[foreignDoc, 1]',
+ '[foreignComment, 2]',
+ '[foreignTextNode, 0]',
+ '[foreignTextNode, 36]',
+ '[xmlDoc, -1]',
+ '[xmlDoc, 0]',
+ '[xmlDoc, 1]',
+ '[xmlDoc, 5]',
+ '[xmlComment, 0]',
+ '[xmlComment, 4]',
+ '[processingInstruction, 0]',
+ '[processingInstruction, 5]',
+ '[processingInstruction, 9]',
+ '[detachedTextNode, 0]',
+ '[detachedTextNode, 8]',
+ '[detachedForeignTextNode, 0]',
+ '[detachedForeignTextNode, 8]',
+ '[detachedXmlTextNode, 0]',
+ '[detachedXmlTextNode, 8]',
+ '[detachedProcessingInstruction, 12]',
+ '[detachedComment, 3]',
+ '[detachedComment, 5]',
+ '[detachedForeignComment, 0]',
+ '[detachedForeignComment, 4]',
+ '[detachedXmlComment, 2]',
+ '[docfrag, 0]',
+ '[foreignDocfrag, 0]',
+ '[xmlDocfrag, 0]',
+ '[doctype, 0]',
+ '[doctype, -17]',
+ '[doctype, 1]',
+ '[foreignDoctype, 0]',
+ '[xmlDoctype, 0]',
+ ];
+
+ testNodesShort = [
+ 'paras[0]',
+ 'paras[0].firstChild',
+ 'paras[1].firstChild',
+ 'paras[5].firstChild',
+ 'foreignPara1',
+ 'foreignPara1.firstChild',
+ 'detachedPara1',
+ 'detachedPara1.firstChild',
+ 'document',
+ 'detachedDiv',
+ 'foreignDoc',
+ 'foreignPara2',
+ 'xmlDoc',
+ 'xmlElement',
+ 'detachedTextNode',
+ 'foreignTextNode',
+ 'processingInstruction',
+ 'detachedProcessingInstruction',
+ 'comment',
+ 'detachedComment',
+ 'docfrag',
+ 'doctype',
+ 'foreignDoctype',
+ ];
+
+ testNodes = testNodesShort.concat([
+ 'paras[1]',
+ 'detachedPara2',
+ 'detachedPara2.firstChild',
+ 'testDiv',
+ 'detachedXmlElement',
+ 'detachedForeignTextNode',
+ 'xmlTextNode',
+ 'detachedXmlTextNode',
+ 'xmlComment',
+ 'foreignComment',
+ 'detachedForeignComment',
+ 'detachedXmlComment',
+ 'foreignDocfrag',
+ 'xmlDocfrag',
+ 'xmlDoctype',
+ ]);
+ }
+
+ function nodeLength(node: any): any {
+ // "The length of a node node depends on node:
+ //
+ // "DocumentType
+ // "0."
+ if (node.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ return 0;
+ }
+ // "If node is a CharacterData node, then return node’s data’s length."
+ // Browsers don't historically support the length attribute on
+ // ProcessingInstruction, so to avoid spurious failures, do
+ // node.data.length instead of node.length.
+ if (
+ isText(node) ||
+ node.nodeType == Node.PROCESSING_INSTRUCTION_NODE ||
+ node.nodeType == Node.COMMENT_NODE
+ ) {
+ return node.data.length;
+ }
+ // "Any other node
+ // "Its number of children."
+ return node.childNodes.length;
+ }
+
+ function furthestAncestor(node: any): any {
+ var root = node;
+ while (root.parentNode != null) {
+ root = root.parentNode;
+ }
+ return root;
+ }
+
+ function isAncestorContainer(node1: any, node2: any): any {
+ return node1 == node2 || node2.compareDocumentPosition(node1) & Node.DOCUMENT_POSITION_CONTAINS;
+ }
+
+ function nextNode(node: any): any {
+ if (node.hasChildNodes()) {
+ return node.firstChild;
+ }
+ return nextNodeDescendants(node);
+ }
+
+ function previousNode(node: any): any {
+ if (node.previousSibling) {
+ node = node.previousSibling;
+ while (node.hasChildNodes()) {
+ node = node.lastChild;
+ }
+ return node;
+ }
+ return node.parentNode;
+ }
+
+ function nextNodeDescendants(node: any): any {
+ while (node && !node.nextSibling) {
+ node = node.parentNode;
+ }
+ if (!node) {
+ return null;
+ }
+ return node.nextSibling;
+ }
+
+ function ownerDocument(node: any): any {
+ return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
+ }
+
+ function isAncestor(ancestor: any, descendant: any): any {
+ if (!ancestor || !descendant) {
+ return false;
+ }
+ while (descendant && descendant != ancestor) {
+ descendant = descendant.parentNode;
+ }
+ return descendant == ancestor;
+ }
+
+ function isInclusiveAncestor(ancestor: any, descendant: any): any {
+ return ancestor === descendant || isAncestor(ancestor, descendant);
+ }
+
+ function isDescendant(descendant: any, ancestor: any): any {
+ return isAncestor(ancestor, descendant);
+ }
+
+ function isInclusiveDescendant(descendant: any, ancestor: any): any {
+ return descendant === ancestor || isDescendant(descendant, ancestor);
+ }
+
+ function getPosition(nodeA: any, offsetA: any, nodeB: any, offsetB: any): any {
+ // "If node A is the same as node B, return equal if offset A equals offset
+ // B, before if offset A is less than offset B, and after if offset A is
+ // greater than offset B."
+ if (nodeA == nodeB) {
+ if (offsetA == offsetB) {
+ return 'equal';
+ }
+ if (offsetA < offsetB) {
+ return 'before';
+ }
+ if (offsetA > offsetB) {
+ return 'after';
+ }
+ }
+
+ // "If node A is after node B in tree order, compute the position of (node
+ // B, offset B) relative to (node A, offset A). If it is before, return
+ // after. If it is after, return before."
+ if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_FOLLOWING) {
+ var pos = getPosition(nodeB, offsetB, nodeA, offsetA);
+ if (pos == 'before') {
+ return 'after';
+ }
+ if (pos == 'after') {
+ return 'before';
+ }
+ }
+
+ // "If node A is an ancestor of node B:"
+ if (nodeB.compareDocumentPosition(nodeA) & Node.DOCUMENT_POSITION_CONTAINS) {
+ // "Let child equal node B."
+ var child = nodeB;
+
+ // "While child is not a child of node A, set child to its parent."
+ while (child.parentNode != nodeA) {
+ child = child.parentNode;
+ }
+
+ // "If the index of child is less than offset A, return after."
+ if (indexOf(child) < offsetA) {
+ return 'after';
+ }
+ }
+
+ // "Return before."
+ return 'before';
+ }
+
+ function isContained(node: any, range: any): any {
+ var pos1 = getPosition(node, 0, range.startContainer, range.startOffset);
+ var pos2 = getPosition(node, nodeLength(node), range.endContainer, range.endOffset);
+
+ return (
+ furthestAncestor(node) == furthestAncestor(range.startContainer) &&
+ pos1 == 'after' &&
+ pos2 == 'before'
+ );
+ }
+
+ function isPartiallyContained(node: any, range: any): any {
+ var cond1 = isAncestorContainer(node, range.startContainer);
+ var cond2 = isAncestorContainer(node, range.endContainer);
+ return (cond1 && !cond2) || (cond2 && !cond1);
+ }
+
+ function indexOf(node: any): any {
+ if (!node.parentNode) {
+ // No preceding sibling nodes, right?
+ return 0;
+ }
+ var i = 0;
+ while (node != node.parentNode.childNodes[i]) {
+ i++;
+ }
+ return i;
+ }
+
+ function myExtractContents(range: any): any {
+ // "Let frag be a new DocumentFragment whose ownerDocument is the same as
+ // the ownerDocument of the context object's start node."
+ var ownerDoc =
+ range.startContainer.nodeType == Node.DOCUMENT_NODE
+ ? range.startContainer
+ : range.startContainer.ownerDocument;
+ var frag = ownerDoc.createDocumentFragment();
+
+ // "If the context object's start and end are the same, abort this method,
+ // returning frag."
+ if (range.startContainer == range.endContainer && range.startOffset == range.endOffset) {
+ return frag;
+ }
+
+ // "Let original start node, original start offset, original end node, and
+ // original end offset be the context object's start and end nodes and
+ // offsets, respectively."
+ var originalStartNode = range.startContainer;
+ var originalStartOffset = range.startOffset;
+ var originalEndNode = range.endContainer;
+ var originalEndOffset = range.endOffset;
+
+ // "If original start node is original end node and it is a CharacterData node, then:"
+ if (
+ range.startContainer == range.endContainer &&
+ (isText(range.startContainer) ||
+ range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE ||
+ range.startContainer.nodeType == Node.COMMENT_NODE)
+ ) {
+ // "Let clone be the result of calling cloneNode(false) on original
+ // start node."
+ var clone = originalStartNode.cloneNode(false);
+
+ // "Set the data of clone to the result of calling
+ // substringData(original start offset, original end offset − original
+ // start offset) on original start node."
+ clone.data = originalStartNode.substringData(
+ originalStartOffset,
+ originalEndOffset - originalStartOffset,
+ );
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Call deleteData(original start offset, original end offset −
+ // original start offset) on original start node."
+ originalStartNode.deleteData(originalStartOffset, originalEndOffset - originalStartOffset);
+
+ // "Abort this method, returning frag."
+ return frag;
+ }
+
+ // "Let common ancestor equal original start node."
+ var commonAncestor = originalStartNode;
+
+ // "While common ancestor is not an ancestor container of original end
+ // node, set common ancestor to its own parent."
+ while (!isAncestorContainer(commonAncestor, originalEndNode)) {
+ commonAncestor = commonAncestor.parentNode;
+ }
+
+ // "If original start node is an ancestor container of original end node,
+ // let first partially contained child be null."
+ var firstPartiallyContainedChild: any;
+ if (isAncestorContainer(originalStartNode, originalEndNode)) {
+ firstPartiallyContainedChild = null;
+ // "Otherwise, let first partially contained child be the first child of
+ // common ancestor that is partially contained in the context object."
+ } else {
+ for (var i = 0; i < commonAncestor.childNodes.length; i++) {
+ if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
+ firstPartiallyContainedChild = commonAncestor.childNodes[i];
+ break;
+ }
+ }
+ if (!firstPartiallyContainedChild) {
+ throw 'Spec bug: no first partially contained child!';
+ }
+ }
+
+ // "If original end node is an ancestor container of original start node,
+ // let last partially contained child be null."
+ var lastPartiallyContainedChild: any;
+ if (isAncestorContainer(originalEndNode, originalStartNode)) {
+ lastPartiallyContainedChild = null;
+ // "Otherwise, let last partially contained child be the last child of
+ // common ancestor that is partially contained in the context object."
+ } else {
+ for (var i = commonAncestor.childNodes.length - 1; i >= 0; i--) {
+ if (isPartiallyContained(commonAncestor.childNodes[i], range)) {
+ lastPartiallyContainedChild = commonAncestor.childNodes[i];
+ break;
+ }
+ }
+ if (!lastPartiallyContainedChild) {
+ throw 'Spec bug: no last partially contained child!';
+ }
+ }
+
+ // "Let contained children be a list of all children of common ancestor
+ // that are contained in the context object, in tree order."
+ //
+ // "If any member of contained children is a DocumentType, raise a
+ // HIERARCHY_REQUEST_ERR exception and abort these steps."
+ var containedChildren: any[] = [];
+ for (var i = 0; i < commonAncestor.childNodes.length; i++) {
+ if (isContained(commonAncestor.childNodes[i], range)) {
+ if (commonAncestor.childNodes[i].nodeType == Node.DOCUMENT_TYPE_NODE) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+ containedChildren.push(commonAncestor.childNodes[i]);
+ }
+ }
+
+ // "If original start node is an ancestor container of original end node,
+ // set new node to original start node and new offset to original start
+ // offset."
+ var newNode: any, newOffset: any;
+ if (isAncestorContainer(originalStartNode, originalEndNode)) {
+ newNode = originalStartNode;
+ newOffset = originalStartOffset;
+ // "Otherwise:"
+ } else {
+ // "Let reference node equal original start node."
+ var referenceNode = originalStartNode;
+
+ // "While reference node's parent is not null and is not an ancestor
+ // container of original end node, set reference node to its parent."
+ while (
+ referenceNode.parentNode &&
+ !isAncestorContainer(referenceNode.parentNode, originalEndNode)
+ ) {
+ referenceNode = referenceNode.parentNode;
+ }
+
+ // "Set new node to the parent of reference node, and new offset to one
+ // plus the index of reference node."
+ newNode = referenceNode.parentNode;
+ newOffset = 1 + indexOf(referenceNode);
+ }
+
+ // "If first partially contained child is a CharacterData node, then:"
+ if (
+ firstPartiallyContainedChild &&
+ (isText(firstPartiallyContainedChild) ||
+ firstPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE ||
+ firstPartiallyContainedChild.nodeType == Node.COMMENT_NODE)
+ ) {
+ // "Let clone be the result of calling cloneNode(false) on original
+ // start node."
+ var clone = originalStartNode.cloneNode(false);
+
+ // "Set the data of clone to the result of calling substringData() on
+ // original start node, with original start offset as the first
+ // argument and (length of original start node − original start offset)
+ // as the second."
+ clone.data = originalStartNode.substringData(
+ originalStartOffset,
+ nodeLength(originalStartNode) - originalStartOffset,
+ );
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Call deleteData() on original start node, with original start
+ // offset as the first argument and (length of original start node −
+ // original start offset) as the second."
+ originalStartNode.deleteData(
+ originalStartOffset,
+ nodeLength(originalStartNode) - originalStartOffset,
+ );
+ // "Otherwise, if first partially contained child is not null:"
+ } else if (firstPartiallyContainedChild) {
+ // "Let clone be the result of calling cloneNode(false) on first
+ // partially contained child."
+ var clone = firstPartiallyContainedChild.cloneNode(false);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Let subrange be a new Range whose start is (original start node,
+ // original start offset) and whose end is (first partially contained
+ // child, length of first partially contained child)."
+ var subrange = ownerDoc.createRange();
+ subrange.setStart(originalStartNode, originalStartOffset);
+ subrange.setEnd(firstPartiallyContainedChild, nodeLength(firstPartiallyContainedChild));
+
+ // "Let subfrag be the result of calling extractContents() on
+ // subrange."
+ var subfrag = myExtractContents(subrange);
+
+ // "For each child of subfrag, in order, append that child to clone as
+ // its last child."
+ for (var i = 0; i < subfrag.childNodes.length; i++) {
+ clone.appendChild(subfrag.childNodes[i]);
+ }
+ }
+
+ // "For each contained child in contained children, append contained child
+ // as the last child of frag."
+ for (var i = 0; i < containedChildren.length; i++) {
+ frag.appendChild(containedChildren[i]);
+ }
+
+ // "If last partially contained child is a CharacterData node, then:"
+ if (
+ lastPartiallyContainedChild &&
+ (isText(lastPartiallyContainedChild) ||
+ lastPartiallyContainedChild.nodeType == Node.PROCESSING_INSTRUCTION_NODE ||
+ lastPartiallyContainedChild.nodeType == Node.COMMENT_NODE)
+ ) {
+ // "Let clone be the result of calling cloneNode(false) on original
+ // end node."
+ var clone = originalEndNode.cloneNode(false);
+
+ // "Set the data of clone to the result of calling substringData(0,
+ // original end offset) on original end node."
+ clone.data = originalEndNode.substringData(0, originalEndOffset);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Call deleteData(0, original end offset) on original end node."
+ originalEndNode.deleteData(0, originalEndOffset);
+ // "Otherwise, if last partially contained child is not null:"
+ } else if (lastPartiallyContainedChild) {
+ // "Let clone be the result of calling cloneNode(false) on last
+ // partially contained child."
+ var clone = lastPartiallyContainedChild.cloneNode(false);
+
+ // "Append clone as the last child of frag."
+ frag.appendChild(clone);
+
+ // "Let subrange be a new Range whose start is (last partially
+ // contained child, 0) and whose end is (original end node, original
+ // end offset)."
+ var subrange = ownerDoc.createRange();
+ subrange.setStart(lastPartiallyContainedChild, 0);
+ subrange.setEnd(originalEndNode, originalEndOffset);
+
+ // "Let subfrag be the result of calling extractContents() on
+ // subrange."
+ var subfrag = myExtractContents(subrange);
+
+ // "For each child of subfrag, in order, append that child to clone as
+ // its last child."
+ for (var i = 0; i < subfrag.childNodes.length; i++) {
+ clone.appendChild(subfrag.childNodes[i]);
+ }
+ }
+
+ // "Set the context object's start and end to (new node, new offset)."
+ range.setStart(newNode, newOffset);
+ range.setEnd(newNode, newOffset);
+
+ // "Return frag."
+ return frag;
+ }
+
+ function myInsertNode(range: any, node: any): any {
+ // "If range's start node is a ProcessingInstruction or Comment node, or is
+ // a Text node whose parent is null, or is node, throw an
+ // "HierarchyRequestError" exception and terminate these steps."
+ if (
+ range.startContainer.nodeType == Node.PROCESSING_INSTRUCTION_NODE ||
+ range.startContainer.nodeType == Node.COMMENT_NODE ||
+ (isText(range.startContainer) && !range.startContainer.parentNode) ||
+ range.startContainer == node
+ ) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ // "Let referenceNode be null."
+ var referenceNode: any = null;
+
+ // "If range's start node is a Text node, set referenceNode to that Text node."
+ if (isText(range.startContainer)) {
+ referenceNode = range.startContainer;
+
+ // "Otherwise, set referenceNode to the child of start node whose index is
+ // start offset, and null if there is no such child."
+ } else {
+ if (range.startOffset < range.startContainer.childNodes.length) {
+ referenceNode = range.startContainer.childNodes[range.startOffset];
+ } else {
+ referenceNode = null;
+ }
+ }
+
+ // "Let parent be range's start node if referenceNode is null, and
+ // referenceNode's parent otherwise."
+ var parent_ = referenceNode === null ? range.startContainer : referenceNode.parentNode;
+
+ // "Ensure pre-insertion validity of node into parent before
+ // referenceNode."
+ var error = ensurePreInsertionValidity(node, parent_, referenceNode);
+ if (error) {
+ return error;
+ }
+
+ // "If range's start node is a Text node, set referenceNode to the result
+ // of splitting it with offset range's start offset."
+ if (isText(range.startContainer)) {
+ referenceNode = range.startContainer.splitText(range.startOffset);
+ }
+
+ // "If node is referenceNode, set referenceNode to its next sibling."
+ if (node == referenceNode) {
+ referenceNode = referenceNode.nextSibling;
+ }
+
+ // "If node's parent is not null, remove node from its parent."
+ if (node.parentNode) {
+ node.parentNode.removeChild(node);
+ }
+
+ // "Let newOffset be parent's length if referenceNode is null, and
+ // referenceNode's index otherwise."
+ var newOffset = referenceNode === null ? nodeLength(parent_) : indexOf(referenceNode);
+
+ // "Increase newOffset by node's length if node is a DocumentFragment node,
+ // and one otherwise."
+ newOffset += node.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? nodeLength(node) : 1;
+
+ // "Pre-insert node into parent before referenceNode."
+ parent_.insertBefore(node, referenceNode);
+
+ // "If range's start and end are the same, set range's end to (parent,
+ // newOffset)."
+ if (range.startContainer == range.endContainer && range.startOffset == range.endOffset) {
+ range.setEnd(parent_, newOffset);
+ }
+ }
+
+ function isElement(node: any): any {
+ return node.nodeType == Node.ELEMENT_NODE;
+ }
+
+ function isText(node: any): any {
+ return node.nodeType == Node.TEXT_NODE || node.nodeType == Node.CDATA_SECTION_NODE;
+ }
+
+ function isDoctype(node: any): any {
+ return node.nodeType == Node.DOCUMENT_TYPE_NODE;
+ }
+
+ function ensurePreInsertionValidity(node: any, parent_: any, child: any): any {
+ // "If parent is not a Document, DocumentFragment, or Element node, throw a
+ // HierarchyRequestError."
+ if (
+ parent_.nodeType != Node.DOCUMENT_NODE &&
+ parent_.nodeType != Node.DOCUMENT_FRAGMENT_NODE &&
+ parent_.nodeType != Node.ELEMENT_NODE
+ ) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ // "If node is a host-including inclusive ancestor of parent, throw a
+ // HierarchyRequestError."
+ //
+ // XXX Does not account for host
+ if (isInclusiveAncestor(node, parent_)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ // "If child is not null and its parent is not parent, throw a NotFoundError
+ // exception."
+ if (child && child.parentNode != parent_) {
+ return 'NOT_FOUND_ERR';
+ }
+
+ // "If node is not a DocumentFragment, DocumentType, Element, or CharacterData node,
+ // then throw a HierarchyRequestError."
+ if (
+ node.nodeType != Node.DOCUMENT_FRAGMENT_NODE &&
+ node.nodeType != Node.DOCUMENT_TYPE_NODE &&
+ node.nodeType != Node.ELEMENT_NODE &&
+ !isText(node) &&
+ node.nodeType != Node.PROCESSING_INSTRUCTION_NODE &&
+ node.nodeType != Node.COMMENT_NODE
+ ) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ // "If either node is a Text node and parent is a document, or node is a
+ // doctype and parent is not a document, throw a HierarchyRequestError."
+ if (
+ (isText(node) && parent_.nodeType == Node.DOCUMENT_NODE) ||
+ (node.nodeType == Node.DOCUMENT_TYPE_NODE && parent_.nodeType != Node.DOCUMENT_NODE)
+ ) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ // "If parent is a document, and any of the statements below, switched on
+ // node, are true, throw a HierarchyRequestError."
+ if (parent_.nodeType == Node.DOCUMENT_NODE) {
+ switch (node.nodeType) {
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ // "If node has more than one element child or has a Text node
+ // child. Otherwise, if node has one element child and either
+ // parent has an element child, child is a doctype, or child is not
+ // null and a doctype is following child."
+ if ([].filter.call(node.childNodes, isElement).length > 1) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if ([].some.call(node.childNodes, isText)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if ([].filter.call(node.childNodes, isElement).length == 1) {
+ if ([].some.call(parent_.childNodes, isElement)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if (child && child.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if (child && [].slice.call(parent_.childNodes, indexOf(child) + 1).filter(isDoctype)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+ }
+ break;
+
+ case Node.ELEMENT_NODE:
+ // "parent has an element child, child is a doctype, or child is
+ // not null and a doctype is following child."
+ if ([].some.call(parent_.childNodes, isElement)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if (child.nodeType == Node.DOCUMENT_TYPE_NODE) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if (child && [].slice.call(parent_.childNodes, indexOf(child) + 1).filter(isDoctype)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+ break;
+
+ case Node.DOCUMENT_TYPE_NODE:
+ // "parent has a doctype child, an element is preceding child, or
+ // child is null and parent has an element child."
+ if ([].some.call(parent_.childNodes, isDoctype)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if (child && [].slice.call(parent_.childNodes, 0, indexOf(child)).some(isElement)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+
+ if (!child && [].some.call(parent_.childNodes, isElement)) {
+ return 'HIERARCHY_REQUEST_ERR';
+ }
+ break;
+ }
+ }
+ }
+
+ function assertNodesEqual(actual: any, expected: any, msg: any): any {
+ if (!actual.isEqualNode(expected)) {
+ msg = 'Actual and expected mismatch for ' + msg + '. ';
+
+ while (actual && expected) {
+ expect(
+ actual.nodeType === expected.nodeType &&
+ actual.nodeName === expected.nodeName &&
+ actual.nodeValue === expected.nodeValue,
+ 'First differing node: expected ' +
+ format_value(expected) +
+ ', got ' +
+ format_value(actual) +
+ ' [' +
+ msg +
+ ']',
+ ).toBe(true);
+ actual = nextNode(actual);
+ expected = nextNode(expected);
+ }
+
+ expect.unreachable("DOMs were not equal but we couldn't figure out why");
+ }
+ }
+
+ function getDomExceptionName(e: any): any {
+ var ret: any = null;
+ for (var prop in e) {
+ if (/^[A-Z_]+_ERR$/.test(prop) && e[prop] == e.code) {
+ return prop;
+ }
+ }
+
+ throw 'Exception seems to not be a DOMException? ' + e;
+ }
+
+ function rangeFromEndpoints(endpoints: any): any {
+ // If we just use document instead of the ownerDocument of endpoints[0],
+ // WebKit will throw on setStart/setEnd. This is a WebKit bug, but it's in
+ // range, not selection, so we don't want to fail anything for it.
+ var range = ownerDocument(endpoints[0]).createRange();
+ range.setStart(endpoints[0], endpoints[1]);
+ range.setEnd(endpoints[2], endpoints[3]);
+ return range;
+ }
+ setupRangeTests();
+ return {
+ setupRangeTests,
+ nodeLength,
+ furthestAncestor,
+ isAncestorContainer,
+ nextNode,
+ previousNode,
+ nextNodeDescendants,
+ ownerDocument,
+ isAncestor,
+ isInclusiveAncestor,
+ isDescendant,
+ isInclusiveDescendant,
+ getPosition,
+ isContained,
+ isPartiallyContained,
+ indexOf,
+ myExtractContents,
+ myInsertNode,
+ isElement,
+ isText,
+ isDoctype,
+ ensurePreInsertionValidity,
+ assertNodesEqual,
+ getDomExceptionName,
+ rangeFromEndpoints,
+ get testDiv() {
+ return testDiv;
+ },
+ resolve(expression: string): any {
+ return eval(expression);
+ },
+ };
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-once.any.js.ts b/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-once.any.js.ts
new file mode 100644
index 0000000..75ae992
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-once.any.js.ts
@@ -0,0 +1,104 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test('Once listener should be invoked only once', ({ EventTarget, Event }) => {
+ let invoked_once = false;
+ let invoked_normal = false;
+ function handler_once() {
+ invoked_once = true;
+ }
+ function handler_normal() {
+ invoked_normal = true;
+ }
+
+ const et = new EventTarget();
+ et.addEventListener('test', handler_once, { once: true });
+ et.addEventListener('test', handler_normal);
+ et.dispatchEvent(new Event('test'));
+ expect(invoked_once, 'Once handler should be invoked').toBe(true);
+ expect(invoked_normal, 'Normal handler should be invoked').toBe(true);
+
+ invoked_once = false;
+ invoked_normal = false;
+ et.dispatchEvent(new Event('test'));
+ expect(invoked_once, "Once handler shouldn't be invoked again").toBe(false);
+ expect(invoked_normal, 'Normal handler should be invoked again').toBe(true);
+ et.removeEventListener('test', handler_normal);
+ });
+ context.test(
+ 'Once listener should be invoked only once even if the event is nested',
+ ({ EventTarget, Event }) => {
+ const et = new EventTarget();
+ let invoked_count = 0;
+ function handler() {
+ invoked_count++;
+ if (invoked_count == 1) et.dispatchEvent(new Event('test'));
+ }
+ et.addEventListener('test', handler, { once: true });
+ et.dispatchEvent(new Event('test'));
+ expect(invoked_count, 'Once handler should only be invoked once').toBe(1);
+
+ invoked_count = 0;
+ function handler2() {
+ invoked_count++;
+ if (invoked_count == 1) et.addEventListener('test', handler2, { once: true });
+ if (invoked_count <= 2) et.dispatchEvent(new Event('test'));
+ }
+ et.addEventListener('test', handler2, { once: true });
+ et.dispatchEvent(new Event('test'));
+ expect(invoked_count, 'Once handler should only be invoked once after each adding').toBe(2);
+ },
+ );
+ context.test(
+ 'Once listener should be added / removed like normal listeners',
+ ({ EventTarget, Event }) => {
+ let invoked_count = 0;
+ function handler() {
+ invoked_count++;
+ }
+
+ const et = new EventTarget();
+
+ et.addEventListener('test', handler, { once: true });
+ et.addEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ expect(invoked_count, 'The handler should only be added once').toBe(1);
+
+ invoked_count = 0;
+ et.dispatchEvent(new Event('test'));
+ expect(invoked_count, 'The handler was added as a once listener').toBe(0);
+
+ invoked_count = 0;
+ et.addEventListener('test', handler, { once: true });
+ et.removeEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ expect(invoked_count, 'The handler should have been removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Multiple once listeners should be invoked even if the stopImmediatePropagation is set',
+ ({ EventTarget, Event }) => {
+ const et = new EventTarget();
+
+ let invoked_count = 0;
+
+ for (let n = 4; n > 0; n--) {
+ et.addEventListener(
+ 'test',
+ (e) => {
+ invoked_count++;
+ e.stopImmediatePropagation();
+ },
+ { once: true },
+ );
+ }
+
+ for (let n = 4; n > 0; n--) {
+ et.dispatchEvent(new Event('test'));
+ }
+
+ expect(invoked_count, 'The listeners should be invoked').toBe(4);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-passive.any.js.ts b/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-passive.any.js.ts
new file mode 100644
index 0000000..c979c7a
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-passive.any.js.ts
@@ -0,0 +1,173 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ function testPassiveValue(
+ { EventTarget, Event, step }: DOMFixture,
+ optionsValue: unknown,
+ expectedDefaultPrevented: boolean,
+ existingEventTarget?: EventTarget,
+ ) {
+ let defaultPrevented: boolean | undefined = undefined;
+ let handler = step(function handler(e: Event) {
+ expect(e.defaultPrevented, 'Event prematurely marked defaultPrevented').toBe(false);
+ e.preventDefault();
+ defaultPrevented = e.defaultPrevented;
+ });
+ const et = existingEventTarget || new EventTarget();
+ et.addEventListener('test', handler, optionsValue as AddEventListenerOptions);
+ let uncanceled = et.dispatchEvent(new Event('test', { bubbles: true, cancelable: true }));
+
+ expect(
+ defaultPrevented,
+ 'Incorrect defaultPrevented for options: ' + JSON.stringify(optionsValue),
+ ).toBe(expectedDefaultPrevented);
+ expect(uncanceled, 'Incorrect return value from dispatchEvent').toBe(!expectedDefaultPrevented);
+
+ et.removeEventListener('test', handler, optionsValue as AddEventListenerOptions);
+ }
+ function testPassiveValueOnReturnValue(
+ { EventTarget, Event, step }: DOMFixture,
+ optionsValue: unknown,
+ expectedDefaultPrevented: boolean,
+ ) {
+ let defaultPrevented: boolean | undefined = undefined;
+ let handler = step((e: Event) => {
+ expect(e.defaultPrevented, 'Event prematurely marked defaultPrevented').toBe(false);
+ e.returnValue = false;
+ defaultPrevented = e.defaultPrevented;
+ });
+ const et = new EventTarget();
+ et.addEventListener('test', handler, optionsValue as AddEventListenerOptions);
+ let uncanceled = et.dispatchEvent(new Event('test', { bubbles: true, cancelable: true }));
+
+ expect(
+ defaultPrevented,
+ 'Incorrect defaultPrevented for options: ' + JSON.stringify(optionsValue),
+ ).toBe(expectedDefaultPrevented);
+ expect(uncanceled, 'Incorrect return value from dispatchEvent').toBe(!expectedDefaultPrevented);
+
+ et.removeEventListener('test', handler, optionsValue as AddEventListenerOptions);
+ }
+ function testPassiveWithOtherHandlers(
+ fixture: DOMFixture,
+ optionsValue: unknown,
+ expectedDefaultPrevented: boolean,
+ ) {
+ const { EventTarget } = fixture;
+ let handlerInvoked1 = false;
+ let dummyHandler1 = function () {
+ handlerInvoked1 = true;
+ };
+ let handlerInvoked2 = false;
+ let dummyHandler2 = function () {
+ handlerInvoked2 = true;
+ };
+
+ const et = new EventTarget();
+ et.addEventListener('test', dummyHandler1, { passive: true });
+ et.addEventListener('test', dummyHandler2);
+
+ testPassiveValue(fixture, optionsValue, expectedDefaultPrevented, et);
+
+ expect(handlerInvoked1, 'Extra passive handler not invoked').toBe(true);
+ expect(handlerInvoked2, 'Extra non-passive handler not invoked').toBe(true);
+
+ et.removeEventListener('test', dummyHandler1);
+ et.removeEventListener('test', dummyHandler2);
+ }
+ function testOptionEquivalence(
+ { EventTarget, Event }: DOMFixture,
+ optionValue1: unknown,
+ optionValue2: unknown,
+ expectedEquality: boolean,
+ ) {
+ let invocationCount = 0;
+ let handler = function handler(e: Event) {
+ invocationCount++;
+ };
+ const et = new EventTarget();
+ et.addEventListener('test', handler, optionValue1 as AddEventListenerOptions);
+ et.addEventListener('test', handler, optionValue2 as AddEventListenerOptions);
+ et.dispatchEvent(new Event('test', { bubbles: true }));
+ expect(
+ invocationCount,
+ 'equivalence of options ' +
+ JSON.stringify(optionValue1) +
+ ' and ' +
+ JSON.stringify(optionValue2),
+ ).toBe(expectedEquality ? 1 : 2);
+ et.removeEventListener('test', handler, optionValue1 as AddEventListenerOptions);
+ et.removeEventListener('test', handler, optionValue2 as AddEventListenerOptions);
+ }
+ context.test('Supports passive option on addEventListener only', ({ EventTarget }) => {
+ let supportsPassive = false;
+ let query_options = {
+ get passive() {
+ supportsPassive = true;
+ return false;
+ },
+ get dummy() {
+ expect.unreachable('dummy value getter invoked');
+ return false;
+ },
+ };
+
+ const et = new EventTarget();
+ et.addEventListener('test_event', null, query_options);
+ expect(supportsPassive, "addEventListener doesn't support the passive option").toBe(true);
+
+ supportsPassive = false;
+ et.removeEventListener('test_event', null, query_options as EventListenerOptions);
+ expect(
+ supportsPassive,
+ 'removeEventListener supports the passive option when it should not',
+ ).toBe(false);
+ });
+ context.test(
+ 'preventDefault should be ignored if-and-only-if the passive option is true',
+ (fixture) => {
+ testPassiveValue(fixture, undefined, true);
+ testPassiveValue(fixture, {}, true);
+ testPassiveValue(fixture, { passive: false }, true);
+ testPassiveValue(fixture, { passive: true }, false);
+ testPassiveValue(fixture, { passive: 0 }, true);
+ testPassiveValue(fixture, { passive: 1 }, false);
+ },
+ );
+ context.test(
+ 'returnValue should be ignored if-and-only-if the passive option is true',
+ (fixture) => {
+ testPassiveValueOnReturnValue(fixture, undefined, true);
+ testPassiveValueOnReturnValue(fixture, {}, true);
+ testPassiveValueOnReturnValue(fixture, { passive: false }, true);
+ testPassiveValueOnReturnValue(fixture, { passive: true }, false);
+ testPassiveValueOnReturnValue(fixture, { passive: 0 }, true);
+ testPassiveValueOnReturnValue(fixture, { passive: 1 }, false);
+ },
+ );
+ context.test(
+ 'passive behavior of one listener should be unaffected by the presence of other listeners',
+ (fixture) => {
+ testPassiveWithOtherHandlers(fixture, {}, true);
+ testPassiveWithOtherHandlers(fixture, { passive: false }, true);
+ testPassiveWithOtherHandlers(fixture, { passive: true }, false);
+ },
+ );
+ context.test('Equivalence of option values', (fixture) => {
+ // Sanity check options that should be treated as distinct handlers
+ testOptionEquivalence(fixture, { capture: true }, { capture: false, passive: false }, false);
+ testOptionEquivalence(fixture, { capture: true }, { passive: true }, false);
+
+ // Option values that should be treated as equivalent
+ testOptionEquivalence(fixture, {}, { passive: false }, true);
+ testOptionEquivalence(fixture, { passive: true }, { passive: false }, true);
+ testOptionEquivalence(fixture, undefined, { passive: true }, true);
+ testOptionEquivalence(
+ fixture,
+ { capture: true, passive: false },
+ { capture: true, passive: true },
+ true,
+ );
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-signal.any.js.ts b/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-signal.any.js.ts
new file mode 100644
index 0000000..4952a51
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/AddEventListenerOptions-signal.any.js.ts
@@ -0,0 +1,186 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Passing an AbortSignal to addEventListener options should allow removing a listener',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'Adding a signal still adds a listener').toBe(1);
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'The listener was not added with the once flag').toBe(2);
+ controller.abort();
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'Aborting on the controller removes the listener').toBe(2);
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'Passing an aborted signal never adds the handler').toBe(2);
+ },
+ );
+ context.test(
+ 'Passing an AbortSignal to addEventListener does not prevent removeEventListener',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.removeEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'The listener was still removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Passing an AbortSignal to addEventListener works with the once flag',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal, once: true });
+ controller.abort();
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'The listener was still removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Removing a once listener works with a passed signal',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal, once: true });
+ et.removeEventListener('test', handler);
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'The listener was still removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Passing an AbortSignal to multiple listeners',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('first', handler, { signal: controller.signal, once: true });
+ et.addEventListener('second', handler, { signal: controller.signal, once: true });
+ controller.abort();
+ et.dispatchEvent(new Event('first'));
+ et.dispatchEvent(new Event('second'));
+ expect(count, 'The listener was still removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Passing an AbortSignal to addEventListener works with the capture flag',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener('test', handler, { signal: controller.signal, capture: true });
+ controller.abort();
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'The listener was still removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Aborting from a listener does not call future listeners',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener(
+ 'test',
+ () => {
+ controller.abort();
+ },
+ { signal: controller.signal },
+ );
+ et.addEventListener('test', handler, { signal: controller.signal });
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'The listener was still removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Adding then aborting a listener in another listener does not call it',
+ ({ EventTarget, Event, AbortController }) => {
+ let count = 0;
+ function handler() {
+ count++;
+ }
+ const et = new EventTarget();
+ const controller = new AbortController();
+ et.addEventListener(
+ 'test',
+ () => {
+ et.addEventListener('test', handler, { signal: controller.signal });
+ controller.abort();
+ },
+ { signal: controller.signal },
+ );
+ et.dispatchEvent(new Event('test'));
+ expect(count, 'The listener was still removed').toBe(0);
+ },
+ );
+ context.test(
+ 'Aborting from a nested listener should remove it',
+ ({ EventTarget, Event, AbortController }) => {
+ const et = new EventTarget();
+ const ac = new AbortController();
+ let count = 0;
+ et.addEventListener(
+ 'foo',
+ () => {
+ et.addEventListener(
+ 'foo',
+ () => {
+ count++;
+ if (count > 5) ac.abort();
+ et.dispatchEvent(new Event('foo'));
+ },
+ { signal: ac.signal },
+ );
+ et.dispatchEvent(new Event('foo'));
+ },
+ { once: true },
+ );
+ et.dispatchEvent(new Event('foo'));
+ },
+ );
+ context.test('Passing null as the signal should throw', ({ EventTarget, TypeError }) => {
+ const et = new EventTarget();
+ expect(() => {
+ et.addEventListener('foo', () => {}, { signal: null as unknown as AbortSignal });
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ });
+ context.test(
+ 'Passing null as the signal should throw (listener is also null)',
+ ({ EventTarget, TypeError }) => {
+ const et = new EventTarget();
+ expect(() => {
+ et.addEventListener('foo', null, { signal: null as unknown as AbortSignal });
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-constructors.any.js.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-constructors.any.js.ts
new file mode 100644
index 0000000..ab9ecef
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-constructors.any.js.ts
@@ -0,0 +1,152 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'CustomEvent constructor preserves detail and ignores extra dictionary members',
+ ({ CustomEvent }) => {
+ let ev = new CustomEvent('$', {
+ detail: 54,
+ sweet: 'x',
+ sweet2: 'x',
+ cancelable: true,
+ } as CustomEventInit);
+ expect(ev.type).toBe('$');
+ expect(ev.bubbles).toBe(false);
+ expect(ev.cancelable).toBe(true);
+ expect((ev as CustomEvent & { sweet?: unknown }).sweet).toBe(undefined);
+ expect(ev.detail).toBe(54);
+ },
+ );
+ }
+ {
+ const { expect } = context.runner;
+ context.test('Event constructor requires new with an empty type', ({ Event, TypeError }) => {
+ expect(
+ () => (Event as unknown as (type: string) => Event)(''),
+ "Calling Event constructor without 'new' must throw",
+ ).toThrow(expect.objectContaining({ constructor: TypeError }));
+ });
+ context.test('Event constructor requires a type argument', ({ Event, TypeError }) => {
+ expect(function () {
+ new (Event as unknown as new () => Event)();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ });
+ context.test('Event constructor propagates type conversion errors', ({ Event }) => {
+ let test_error = { name: 'test' };
+ let caught: unknown;
+ try {
+ new Event({
+ toString() {
+ throw test_error;
+ },
+ } as unknown as string);
+ } catch (error) {
+ caught = error;
+ }
+ expect(caught).toBe(test_error);
+ });
+ context.test('Event constructor defaults with an empty type', ({ Event }) => {
+ let ev = new Event('');
+ expect(ev.type).toBe('');
+ expect(ev.target).toBe(null);
+ expect(ev.srcElement).toBe(null);
+ expect(ev.currentTarget).toBe(null);
+ expect(ev.eventPhase).toBe(Event.NONE);
+ expect(ev.bubbles).toBe(false);
+ expect(ev.cancelable).toBe(false);
+ expect(ev.defaultPrevented).toBe(false);
+ expect(ev.returnValue).toBe(true);
+ expect(ev.isTrusted).toBe(false);
+ expect(ev.timeStamp > 0).toBe(true);
+ expect('initEvent' in ev).toBe(true);
+ });
+ context.test('Event constructor defaults with type test', ({ Event }) => {
+ let ev = new Event('test');
+ expect(ev.type).toBe('test');
+ expect(ev.target).toBe(null);
+ expect(ev.srcElement).toBe(null);
+ expect(ev.currentTarget).toBe(null);
+ expect(ev.eventPhase).toBe(Event.NONE);
+ expect(ev.bubbles).toBe(false);
+ expect(ev.cancelable).toBe(false);
+ expect(ev.defaultPrevented).toBe(false);
+ expect(ev.returnValue).toBe(true);
+ expect(ev.isTrusted).toBe(false);
+ expect(ev.timeStamp > 0).toBe(true);
+ expect('initEvent' in ev).toBe(true);
+ });
+ context.test('Event constructor requires new with type test', ({ Event, TypeError }) => {
+ expect(function () {
+ (Event as unknown as (type: string) => Event)('test');
+ }, 'Calling Event constructor without "new" must throw').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ });
+ context.test('Event constructor honors bubbles', ({ Event }) => {
+ let ev = new Event('I am an event', { bubbles: true, cancelable: false } as EventInit);
+ expect(ev.type).toBe('I am an event');
+ expect(ev.bubbles).toBe(true);
+ expect(ev.cancelable).toBe(false);
+ });
+ context.test('Event constructor ignores bubblesIGNORED', ({ Event }) => {
+ let ev = new Event('@', { bubblesIGNORED: true, cancelable: true } as EventInit);
+ expect(ev.type).toBe('@');
+ expect(ev.bubbles).toBe(false);
+ expect(ev.cancelable).toBe(true);
+ });
+ context.test(
+ 'Event constructor ignores a dictionary member containing a null character',
+ ({ Event }) => {
+ let ev = new Event('@', { 'bubbles\0IGNORED': true, cancelable: true } as EventInit);
+ expect(ev.type).toBe('@');
+ expect(ev.bubbles).toBe(false);
+ expect(ev.cancelable).toBe(true);
+ },
+ );
+ context.test('Event constructor honors cancelable', ({ Event }) => {
+ let ev = new Event('Xx', { cancelable: true } as EventInit);
+ expect(ev.type).toBe('Xx');
+ expect(ev.bubbles).toBe(false);
+ expect(ev.cancelable).toBe(true);
+ });
+ context.test('Event constructor accepts an empty dictionary', ({ Event }) => {
+ let ev = new Event('Xx', {} as EventInit);
+ expect(ev.type).toBe('Xx');
+ expect(ev.bubbles).toBe(false);
+ expect(ev.cancelable).toBe(false);
+ });
+ context.test('Event constructor ignores sweet', ({ Event }) => {
+ let ev = new Event('Xx', { bubbles: true, cancelable: false, sweet: 'x' } as EventInit);
+ expect(ev.type).toBe('Xx');
+ expect(ev.bubbles).toBe(true);
+ expect(ev.cancelable).toBe(false);
+ expect((ev as Event & { sweet?: unknown }).sweet).toBe(undefined);
+ });
+ context.test('Event constructor reads dictionary members in order', ({ Event }) => {
+ let called: string[] = [];
+ let ev = new Event('Xx', {
+ get cancelable() {
+ called.push('cancelable');
+ return false;
+ },
+ get bubbles() {
+ called.push('bubbles');
+ return true;
+ },
+ get sweet() {
+ called.push('sweet');
+ return 'x';
+ },
+ } as EventInit);
+ expect(called).toHaveLength(2);
+ expect(called[0]).toBe('bubbles');
+ expect(called[1]).toBe('cancelable');
+ expect(ev.type).toBe('Xx');
+ expect(ev.bubbles).toBe(true);
+ expect(ev.cancelable).toBe(false);
+ expect((ev as Event & { sweet?: unknown }).sweet).toBe(undefined);
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-defaultPrevented-after-dispatch.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-defaultPrevented-after-dispatch.html.ts
new file mode 100644
index 0000000..04e01fa
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-defaultPrevented-after-dispatch.html.ts
@@ -0,0 +1,67 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Default prevention via preventDefault',
+ ({ document, step }) => {
+ const window = document.defaultView!;
+
+ let EVENT = 'foo';
+ let TARGET = document.getElementById('target')!;
+ let evt = document.createEvent('Event');
+ evt.initEvent(EVENT, true, true);
+
+ TARGET.addEventListener(
+ EVENT,
+ step(function (e: Event) {
+ e.preventDefault();
+ expect(e.defaultPrevented, 'during dispatch').toBe(true);
+ }),
+ true,
+ );
+ TARGET.dispatchEvent(evt);
+
+ expect(evt.defaultPrevented, 'after dispatch').toBe(true);
+ expect(evt.target).toBe(TARGET);
+ expect(evt.srcElement).toBe(TARGET);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+ context.test(
+ 'Default prevention via returnValue',
+ ({ document, step }) => {
+ const window = document.defaultView!;
+
+ let EVENT = 'foo';
+ let TARGET = document.getElementById('target')!;
+ let evt = document.createEvent('Event');
+ evt.initEvent(EVENT, true, true);
+
+ TARGET.addEventListener(
+ EVENT,
+ step(function (e: Event) {
+ e.returnValue = false;
+ expect(e.defaultPrevented, 'during dispatch').toBe(true);
+ }),
+ true,
+ );
+ TARGET.dispatchEvent(evt);
+
+ expect(evt.defaultPrevented, 'after dispatch').toBe(true);
+ expect(evt.target).toBe(TARGET);
+ expect(evt.srcElement).toBe(TARGET);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-bubbles-false.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-bubbles-false.html.ts
new file mode 100644
index 0000000..8756263
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-bubbles-false.html.ts
@@ -0,0 +1,235 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup = `
+
+
+ Shady Grove
+ Aeolian
+
+
+ Over the river, Charlie
+ Dorian
+
+
+
`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ {
+ function targetsForDocumentChain(document: Document): EventTarget[] {
+ return [
+ document,
+ document.documentElement,
+ document.getElementsByTagName('body')[0]!,
+ document.getElementById('table')!,
+ document.getElementById('table-body')!,
+ document.getElementById('parent')!,
+ ];
+ }
+
+ function testChain(
+ document: Document,
+ targetParents: EventTarget[],
+ phases: number[],
+ event_type: string,
+ ) {
+ let target = document.getElementById('target')!;
+ let targets = targetParents.concat(target);
+ let expected_targets = targets.concat(target);
+
+ let actual_targets: (EventTarget | null)[] = [],
+ actual_phases: number[] = [];
+ let test_event = function (evt: Event) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ };
+
+ for (let i = 0; i < targets.length; i++) {
+ targets[i]!.addEventListener(event_type, test_event, true);
+ targets[i]!.addEventListener(event_type, test_event, false);
+ }
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, false, true);
+
+ target.dispatchEvent(evt);
+
+ {
+ const actual = actual_targets;
+ const expected = expected_targets;
+ expect(actual.length, 'targets').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'targets').toBe(expected[index]);
+ }
+ {
+ const actual = actual_phases;
+ const expected = phases;
+ expect(actual.length, 'phases').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'phases').toBe(expected[index]);
+ }
+ }
+
+ context.test(
+ 'bubbles=false: ' + 'In window.document with click event',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ ];
+
+ let chainWithWindow = ([window] as EventTarget[]).concat(targetsForDocumentChain(document));
+ testChain(
+ document,
+ chainWithWindow,
+ ([Event.CAPTURING_PHASE] as number[]).concat(phasesForDocumentChain),
+ 'click',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=false: ' + 'In window.document with load event',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ ];
+
+ testChain(document, targetsForDocumentChain(document), phasesForDocumentChain, 'load');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=false: ' + 'In window.document.cloneNode(true)',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ ];
+
+ let documentClone = document.cloneNode(true) as Document;
+ testChain(
+ documentClone,
+ targetsForDocumentChain(documentClone),
+ phasesForDocumentChain,
+ 'click',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=false: ' + 'In new Document()',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+ const Document = fixture.Document;
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ ];
+
+ let newDocument = new Document();
+ newDocument.appendChild(document.documentElement.cloneNode(true));
+ testChain(
+ newDocument,
+ targetsForDocumentChain(newDocument),
+ phasesForDocumentChain,
+ 'click',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=false: ' + 'In DOMImplementation.createHTMLDocument()',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ ];
+
+ let HTMLDocument = document.implementation.createHTMLDocument();
+ HTMLDocument.body.appendChild(document.getElementById('table')!.cloneNode(true));
+ testChain(
+ HTMLDocument,
+ targetsForDocumentChain(HTMLDocument),
+ phasesForDocumentChain,
+ 'click',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-bubbles-true.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-bubbles-true.html.ts
new file mode 100644
index 0000000..4736d8c
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-bubbles-true.html.ts
@@ -0,0 +1,274 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup = `
+
+
+ Shady Grove
+ Aeolian
+
+
+ Over the river, Charlie
+ Dorian
+
+
+
`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ {
+ function concatReverse(a: EventTarget[]) {
+ return a.concat(
+ a
+ .map(function (x) {
+ return x;
+ })
+ .reverse(),
+ );
+ }
+
+ function targetsForDocumentChain(document: Document): EventTarget[] {
+ return [
+ document,
+ document.documentElement,
+ document.getElementsByTagName('body')[0]!,
+ document.getElementById('table')!,
+ document.getElementById('table-body')!,
+ document.getElementById('parent')!,
+ ];
+ }
+
+ function testChain(
+ document: Document,
+ targetParents: EventTarget[],
+ phases: number[],
+ event_type: string,
+ ) {
+ let target = document.getElementById('target')!;
+ let targets = targetParents.concat(target);
+ let expected_targets = concatReverse(targets);
+
+ let actual_targets: (EventTarget | null)[] = [],
+ actual_phases: number[] = [];
+ let test_event = function (evt: Event) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ };
+
+ for (let i = 0; i < targets.length; i++) {
+ targets[i]!.addEventListener(event_type, test_event, true);
+ targets[i]!.addEventListener(event_type, test_event, false);
+ }
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, true, true);
+
+ target.dispatchEvent(evt);
+
+ {
+ const actual = actual_targets;
+ const expected = expected_targets;
+ expect(actual.length, 'targets').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'targets').toBe(expected[index]);
+ }
+ {
+ const actual = actual_phases;
+ const expected = phases;
+ expect(actual.length, 'phases').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'phases').toBe(expected[index]);
+ }
+ }
+
+ context.test(
+ 'bubbles=true: ' + 'In window.document with click event',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ let chainWithWindow = ([window] as EventTarget[]).concat(targetsForDocumentChain(document));
+ let phases = ([Event.CAPTURING_PHASE] as number[]).concat(
+ phasesForDocumentChain,
+ Event.BUBBLING_PHASE,
+ );
+ testChain(document, chainWithWindow, phases, 'click');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=true: ' + 'In window.document with load event',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ testChain(document, targetsForDocumentChain(document), phasesForDocumentChain, 'load');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=true: ' + 'In window.document.cloneNode(true)',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ let documentClone = document.cloneNode(true) as Document;
+ testChain(
+ documentClone,
+ targetsForDocumentChain(documentClone),
+ phasesForDocumentChain,
+ 'click',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=true: ' + 'In new Document()',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+ const Document = fixture.Document;
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ let newDocument = new Document();
+ newDocument.appendChild(document.documentElement.cloneNode(true));
+ testChain(
+ newDocument,
+ targetsForDocumentChain(newDocument),
+ phasesForDocumentChain,
+ 'click',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'bubbles=true: ' + 'In DOMImplementation.createHTMLDocument()',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ const phasesForDocumentChain = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ let HTMLDocument = document.implementation.createHTMLDocument();
+ HTMLDocument.body.appendChild(document.getElementById('table')!.cloneNode(true));
+ testChain(
+ HTMLDocument,
+ targetsForDocumentChain(HTMLDocument),
+ phasesForDocumentChain,
+ 'click',
+ );
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-handlers-changed.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-handlers-changed.html.ts
new file mode 100644
index 0000000..dc0d965
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-handlers-changed.html.ts
@@ -0,0 +1,114 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Dispatch additional events inside an event listener',
+ ({ document, step }) => {
+ const window = document.defaultView!;
+
+ let event_type = 'bar';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let tbody = document.getElementById('table-body')!;
+ let table = document.getElementById('table')!;
+ let body = document.body;
+ let html = document.documentElement;
+ let targets = [window, document, html, body, table, tbody, parent, target];
+ let expected_targets = [
+ window,
+ document,
+ html,
+ body,
+ table,
+ tbody,
+ parent,
+ target,
+ target,
+ target, // The additional listener for target runs as we copy its listeners twice
+ parent,
+ tbody,
+ table,
+ body,
+ html,
+ document,
+ window,
+ ];
+ let expected_listeners = [0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 1, 1, 1, 1, 1, 1];
+
+ let actual_targets: (EventTarget | null)[] = [],
+ actual_listeners: number[] = [];
+ let test_event_function = function (i: number) {
+ return step(function (evt: Event) {
+ actual_targets.push(evt.currentTarget);
+ actual_listeners.push(i);
+
+ if (
+ evt.eventPhase != evt.BUBBLING_PHASE &&
+ (evt.currentTarget as EventTarget & { foo?: number }).foo != 1
+ ) {
+ evt.currentTarget!.removeEventListener(event_type, event_handlers[0]!, true);
+ evt.currentTarget!.addEventListener(event_type, event_handlers[2]!, true);
+ (evt.currentTarget as EventTarget & { foo?: number }).foo = 1;
+ }
+
+ if (
+ evt.eventPhase != evt.CAPTURING_PHASE &&
+ (evt.currentTarget as EventTarget & { foo?: number }).foo != 3
+ ) {
+ evt.currentTarget!.removeEventListener(event_type, event_handlers[0]!, false);
+ evt.currentTarget!.addEventListener(event_type, event_handlers[3]!, false);
+ (evt.currentTarget as EventTarget & { foo?: number }).foo = 3;
+ }
+ });
+ };
+ let event_handlers = [
+ test_event_function(0),
+ test_event_function(1),
+ test_event_function(2),
+ test_event_function(3),
+ ];
+
+ for (let i = 0; i < targets.length; ++i) {
+ targets[i]!.addEventListener(event_type, event_handlers[0]!, true);
+ targets[i]!.addEventListener(event_type, event_handlers[1]!, false);
+ }
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, true, true);
+ target.dispatchEvent(evt);
+
+ {
+ const actual = actual_targets;
+ const expected = expected_targets;
+ expect(actual.length, 'actual_targets').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'actual_targets').toBe(expected[index]);
+ }
+ {
+ const actual = actual_listeners;
+ const expected = expected_listeners;
+ expect(actual.length, 'actual_listeners').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'actual_listeners').toBe(expected[index]);
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+ Shady Grove
+ Aeolian
+
+
+ Over the river, Charlie
+ Dorian
+
+
+
`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-multiple-cancelBubble.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-multiple-cancelBubble.html.ts
new file mode 100644
index 0000000..cf7bf68
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-multiple-cancelBubble.html.ts
@@ -0,0 +1,55 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Multiple dispatchEvent() and cancelBubble',
+ ({ document }) => {
+ const window = document.defaultView!;
+
+ let event_type = 'foo';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let actual_result: (EventTarget | null)[];
+ let test_event = function (evt: Event) {
+ actual_result.push(evt.currentTarget);
+
+ if (parent == evt.currentTarget) {
+ evt.cancelBubble = true;
+ }
+ };
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, true, true);
+
+ target.addEventListener(event_type, test_event, false);
+ parent.addEventListener(event_type, test_event, false);
+ document.addEventListener(event_type, test_event, false);
+ window.addEventListener(event_type, test_event, false);
+
+ actual_result = [];
+ target.dispatchEvent(evt);
+ expect(actual_result).toHaveLength(2);
+ expect(actual_result[0]).toBe(target);
+ expect(actual_result[1]).toBe(parent);
+
+ actual_result = [];
+ parent.dispatchEvent(evt);
+ expect(actual_result).toHaveLength(1);
+ expect(actual_result[0]).toBe(parent);
+
+ actual_result = [];
+ document.dispatchEvent(evt);
+ expect(actual_result).toHaveLength(2);
+ expect(actual_result[0]).toBe(document);
+ expect(actual_result[1]).toBe(window);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '
\n \n
',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-multiple-stopPropagation.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-multiple-stopPropagation.html.ts
new file mode 100644
index 0000000..23d1ace
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-multiple-stopPropagation.html.ts
@@ -0,0 +1,55 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Multiple dispatchEvent() and stopPropagation()',
+ ({ document }) => {
+ const window = document.defaultView!;
+
+ let event_type = 'foo';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let actual_result: (EventTarget | null)[];
+ let test_event = function (evt: Event) {
+ actual_result.push(evt.currentTarget);
+
+ if (parent == evt.currentTarget) {
+ evt.stopPropagation();
+ }
+ };
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, true, true);
+
+ target.addEventListener(event_type, test_event, false);
+ parent.addEventListener(event_type, test_event, false);
+ document.addEventListener(event_type, test_event, false);
+ window.addEventListener(event_type, test_event, false);
+
+ actual_result = [];
+ target.dispatchEvent(evt);
+ expect(actual_result).toHaveLength(2);
+ expect(actual_result[0]).toBe(target);
+ expect(actual_result[1]).toBe(parent);
+
+ actual_result = [];
+ parent.dispatchEvent(evt);
+ expect(actual_result).toHaveLength(1);
+ expect(actual_result[0]).toBe(parent);
+
+ actual_result = [];
+ document.dispatchEvent(evt);
+ expect(actual_result).toHaveLength(2);
+ expect(actual_result[0]).toBe(document);
+ expect(actual_result[1]).toBe(window);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '
\n \n
',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-propagation-stopped.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-propagation-stopped.html.ts
new file mode 100644
index 0000000..969caa7
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-propagation-stopped.html.ts
@@ -0,0 +1,71 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Calling stopPropagation() prior to dispatchEvent()',
+ ({ document }) => {
+ const window = document.defaultView!;
+
+ let event = 'foo';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let tbody = document.getElementById('table-body')!;
+ let table = document.getElementById('table')!;
+ let body = document.body;
+ let html = document.documentElement;
+ let current_targets = [window, document, html, body, table, tbody, parent, target];
+ let expected_targets: (EventTarget | null)[] = [];
+ let actual_targets: (EventTarget | null)[] = [];
+ let expected_phases: number[] = [];
+ let actual_phases: number[] = [];
+
+ let test_event = function (evt: Event) {
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ };
+
+ for (let i = 0; i < current_targets.length; ++i) {
+ current_targets[i]!.addEventListener(event, test_event, true);
+ current_targets[i]!.addEventListener(event, test_event, false);
+ }
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event, true, true);
+ evt.stopPropagation();
+ target.dispatchEvent(evt);
+
+ {
+ const actual = actual_targets;
+ const expected = expected_targets;
+ expect(actual.length, 'actual_targets').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'actual_targets').toBe(expected[index]);
+ }
+ {
+ const actual = actual_phases;
+ const expected = expected_phases;
+ expect(actual.length, 'actual_phases').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'actual_phases').toBe(expected[index]);
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+ Shady Grove
+ Aeolian
+
+
+ Over the river, Charlie
+ Dorian
+
+
+
`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-reenter.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-reenter.html.ts
new file mode 100644
index 0000000..22339b2
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-reenter.html.ts
@@ -0,0 +1,111 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Dispatch additional events inside an event listener',
+ ({ document, step }) => {
+ const window = document.defaultView!;
+
+ let event_type = 'foo';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let tbody = document.getElementById('table-body')!;
+ let table = document.getElementById('table')!;
+ let body = document.body;
+ let html = document.documentElement;
+ let targets = [window, document, html, body, table, tbody, parent, target];
+ let expected_targets = [
+ window,
+ document,
+ html,
+ body,
+ table,
+ target,
+ parent,
+ tbody,
+ table,
+ body,
+ html,
+ document,
+ window,
+ tbody,
+ parent,
+ target,
+ ];
+
+ let expected_types = [
+ 'foo',
+ 'foo',
+ 'foo',
+ 'foo',
+ 'foo',
+ 'bar',
+ 'bar',
+ 'bar',
+ 'bar',
+ 'bar',
+ 'bar',
+ 'bar',
+ 'bar',
+ 'foo',
+ 'foo',
+ 'foo',
+ ];
+
+ let actual_targets: (EventTarget | null)[] = [],
+ actual_types: string[] = [];
+ let test_event = step(function (evt: Event) {
+ actual_targets.push(evt.currentTarget);
+ actual_types.push(evt.type);
+
+ if (table == evt.currentTarget && event_type == evt.type) {
+ let e = document.createEvent('Event');
+ e.initEvent('bar', true, true);
+ target.dispatchEvent(e);
+ }
+ });
+
+ for (let i = 0; i < targets.length; ++i) {
+ targets[i]!.addEventListener(event_type, test_event, true);
+ targets[i]!.addEventListener('bar', test_event, false);
+ }
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, false, true);
+ target.dispatchEvent(evt);
+
+ {
+ const actual = actual_targets;
+ const expected = expected_targets;
+ expect(actual.length, 'actual_targets').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'actual_targets').toBe(expected[index]);
+ }
+ {
+ const actual = actual_types;
+ const expected = expected_types;
+ expect(actual.length, 'actual_types').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'actual_types').toBe(expected[index]);
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+ Shady Grove
+ Aeolian
+
+
+ Over the river, Charlie
+ Dorian
+
+
+
`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-target-moved.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-target-moved.html.ts
new file mode 100644
index 0000000..fccf1f1
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-target-moved.html.ts
@@ -0,0 +1,103 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Event propagation path when an element in it is moved within the DOM',
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+
+ let event_type = 'foo';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let tbody = document.getElementById('table-body')!;
+ let table = document.getElementById('table')!;
+ let body = document.body;
+ let html = document.documentElement;
+ let targets = [window, document, html, body, table, tbody, parent, target];
+ let expected_targets = targets.concat([
+ target,
+ parent,
+ tbody,
+ table,
+ body,
+ html,
+ document,
+ window,
+ ]);
+ let phases = [
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.CAPTURING_PHASE,
+ Event.AT_TARGET,
+ Event.AT_TARGET,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ Event.BUBBLING_PHASE,
+ ];
+
+ let actual_targets: (EventTarget | null)[] = [],
+ actual_phases: number[] = [];
+ let test_event = fixture.step(function (evt: Event) {
+ if (parent === target.parentNode) {
+ let table_row = document.getElementById('table-row')!;
+ table_row.appendChild(parent.removeChild(target));
+ }
+
+ actual_targets.push(evt.currentTarget);
+ actual_phases.push(evt.eventPhase);
+ });
+
+ for (let i = 0; i < targets.length; i++) {
+ targets[i]!.addEventListener(event_type, test_event, true);
+ targets[i]!.addEventListener(event_type, test_event, false);
+ }
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, true, true);
+ target.dispatchEvent(evt);
+
+ {
+ const actual = actual_targets;
+ const expected = expected_targets;
+ expect(actual.length, 'targets').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'targets').toBe(expected[index]);
+ }
+ {
+ const actual = actual_phases;
+ const expected = phases;
+ expect(actual.length, 'phases').toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index], 'phases').toBe(expected[index]);
+ }
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: `
+
+
+ Shady Grove
+ Aeolian
+
+
+ Over the river, Charlie
+ Dorian
+
+
+
`,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-throwing.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-throwing.html.ts
new file mode 100644
index 0000000..6eabddf
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-dispatch-throwing.html.ts
@@ -0,0 +1,71 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Throwing in event listener with a single listeners',
+ ({ document, window, Event, cleanup, step }) => {
+ const previousOnError = window.onerror;
+ const allowUncaughtException = (event: ErrorEvent) => event.preventDefault();
+ window.addEventListener('error', allowUncaughtException);
+ cleanup(() => {
+ window.onerror = previousOnError;
+ window.removeEventListener('error', allowUncaughtException);
+ });
+
+ let errorEvents = 0;
+ window.onerror = step(function (e: string | Event) {
+ expect(typeof e).toBe('string');
+ ++errorEvents;
+ });
+
+ let element = document.createElement('div');
+
+ element.addEventListener('click', function () {
+ throw new Error('Error from only listener');
+ });
+
+ element.dispatchEvent(new Event('click'));
+
+ expect(errorEvents).toBe(1);
+ },
+ );
+ context.test(
+ 'Throwing in event listener with multiple listeners',
+ ({ document, window, Event, cleanup, step }) => {
+ const previousOnError = window.onerror;
+ const allowUncaughtException = (event: ErrorEvent) => event.preventDefault();
+ window.addEventListener('error', allowUncaughtException);
+ cleanup(() => {
+ window.onerror = previousOnError;
+ window.removeEventListener('error', allowUncaughtException);
+ });
+
+ let errorEvents = 0;
+ window.onerror = step(function (e: string | Event) {
+ expect(typeof e).toBe('string');
+ ++errorEvents;
+ });
+
+ let element = document.createElement('div');
+
+ let secondCalled = false;
+
+ element.addEventListener('click', function () {
+ throw new Error('Error from first listener');
+ });
+ element.addEventListener(
+ 'click',
+ step(function () {
+ secondCalled = true;
+ }),
+ false,
+ );
+
+ element.dispatchEvent(new Event('click'));
+
+ expect(errorEvents).toBe(1);
+ expect(secondCalled).toBe(true);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/Event-stopImmediatePropagation.html.ts b/packages/web-platform-tests/src/wpt/dom/events/Event-stopImmediatePropagation.html.ts
new file mode 100644
index 0000000..9cb5d19
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/Event-stopImmediatePropagation.html.ts
@@ -0,0 +1,32 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ "Event's stopImmediatePropagation",
+ (fixture) => {
+ const { document } = fixture;
+ const window = document.defaultView!;
+ const Event = fixture.Event;
+ const target = document.querySelector('#target')!;
+
+ let timesCalled = 0;
+ target.addEventListener(
+ 'test',
+ fixture.step((e: Event) => {
+ ++timesCalled;
+ e.stopImmediatePropagation();
+ expect(e.cancelBubble, 'The stop propagation flag must have been set').toBe(true);
+ }),
+ );
+ target.addEventListener('test', () => {
+ ++timesCalled;
+ });
+
+ const e = new Event('test');
+ target.dispatchEvent(e);
+ expect(timesCalled, 'The second listener must not have been called').toBe(1);
+ },
+ { document: { contentType: 'text/html', content: '
' } },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/EventListenerOptions-capture.html.ts b/packages/web-platform-tests/src/wpt/dom/events/EventListenerOptions-capture.html.ts
new file mode 100644
index 0000000..58130b5
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/EventListenerOptions-capture.html.ts
@@ -0,0 +1,132 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test('Capture boolean should be honored correctly', ({ document, Event, step }) => {
+ function testCaptureValue(captureValue: unknown, expectedValue: number) {
+ let handlerPhase: number | undefined = undefined;
+ let handler = step(function handler(e: Event) {
+ expect(handlerPhase, 'Handler invoked after remove').toBe(undefined);
+ handlerPhase = e.eventPhase;
+ });
+ document.addEventListener('test', handler, captureValue as boolean | AddEventListenerOptions);
+ document.body.dispatchEvent(new Event('test', { bubbles: true }));
+ document.removeEventListener(
+ 'test',
+ handler,
+ captureValue as boolean | AddEventListenerOptions,
+ );
+ document.body.dispatchEvent(new Event('test', { bubbles: true }));
+ expect(handlerPhase, 'Incorrect event phase for value: ' + JSON.stringify(captureValue)).toBe(
+ expectedValue,
+ );
+ }
+
+ testCaptureValue(true, Event.CAPTURING_PHASE);
+ testCaptureValue(false, Event.BUBBLING_PHASE);
+ testCaptureValue(null, Event.BUBBLING_PHASE);
+ testCaptureValue(undefined, Event.BUBBLING_PHASE);
+ testCaptureValue(2.3, Event.CAPTURING_PHASE);
+ testCaptureValue(-1000.3, Event.CAPTURING_PHASE);
+ testCaptureValue(NaN, Event.BUBBLING_PHASE);
+ testCaptureValue(+0.0, Event.BUBBLING_PHASE);
+ testCaptureValue(-0.0, Event.BUBBLING_PHASE);
+ testCaptureValue('', Event.BUBBLING_PHASE);
+ testCaptureValue('AAAA', Event.CAPTURING_PHASE);
+ });
+ context.test('Capture option should be honored correctly', ({ document, Event, step }) => {
+ function testCaptureValue(captureValue: unknown, expectedValue: number) {
+ let handlerPhase: number | undefined = undefined;
+ let handler = step(function handler(e: Event) {
+ expect(handlerPhase, 'Handler invoked after remove').toBe(undefined);
+ handlerPhase = e.eventPhase;
+ });
+ document.addEventListener('test', handler, captureValue as boolean | AddEventListenerOptions);
+ document.body.dispatchEvent(new Event('test', { bubbles: true }));
+ document.removeEventListener(
+ 'test',
+ handler,
+ captureValue as boolean | AddEventListenerOptions,
+ );
+ document.body.dispatchEvent(new Event('test', { bubbles: true }));
+ expect(handlerPhase, 'Incorrect event phase for value: ' + JSON.stringify(captureValue)).toBe(
+ expectedValue,
+ );
+ }
+
+ testCaptureValue({}, Event.BUBBLING_PHASE);
+ testCaptureValue({ capture: true }, Event.CAPTURING_PHASE);
+ testCaptureValue({ capture: false }, Event.BUBBLING_PHASE);
+ testCaptureValue({ capture: 2 }, Event.CAPTURING_PHASE);
+ testCaptureValue({ capture: 0 }, Event.BUBBLING_PHASE);
+ });
+ context.test('Supports capture option', ({ document }) => {
+ let supportsCapture = false;
+ let query_options = {
+ get capture() {
+ supportsCapture = true;
+ return false;
+ },
+ get dummy() {
+ expect.unreachable('dummy value getter invoked');
+ return false;
+ },
+ };
+
+ document.addEventListener('test_event', null as unknown as EventListener, query_options);
+ expect(supportsCapture, "addEventListener doesn't support the capture option").toBe(true);
+ supportsCapture = false;
+ document.removeEventListener('test_event', null as unknown as EventListener, query_options);
+ expect(supportsCapture, "removeEventListener doesn't support the capture option").toBe(true);
+ });
+ context.test('Equivalence of option values', ({ document, Event, step }) => {
+ function testOptionEquality(
+ addOptionValue: unknown,
+ removeOptionValue: unknown,
+ expectedEquality: boolean,
+ ) {
+ let handlerInvoked = false;
+ let handler = step(function handler(e: Event) {
+ expect(handlerInvoked, 'Handler invoked multiple times').toBe(false);
+ handlerInvoked = true;
+ });
+ document.addEventListener(
+ 'test',
+ handler,
+ addOptionValue as boolean | AddEventListenerOptions,
+ );
+ document.removeEventListener(
+ 'test',
+ handler,
+ removeOptionValue as boolean | AddEventListenerOptions,
+ );
+ document.body.dispatchEvent(new Event('test', { bubbles: true }));
+ expect(
+ !handlerInvoked,
+ 'equivalence of options ' +
+ JSON.stringify(addOptionValue) +
+ ' and ' +
+ JSON.stringify(removeOptionValue),
+ ).toBe(expectedEquality);
+ if (handlerInvoked)
+ document.removeEventListener(
+ 'test',
+ handler,
+ addOptionValue as boolean | AddEventListenerOptions,
+ );
+ }
+
+ // Option values that should be treated as equivalent
+ testOptionEquality({}, false, true);
+ testOptionEquality({ capture: false }, false, true);
+ testOptionEquality(true, { capture: true }, true);
+ testOptionEquality({ capture: null }, undefined, true);
+ testOptionEquality({ capture: true }, { dummy: false, capture: 1 }, true);
+ testOptionEquality({ dummy: true }, false, true);
+
+ // Option values that should be treated as distinct
+ testOptionEquality(true, false, false);
+ testOptionEquality(true, { capture: false }, false);
+ testOptionEquality({}, true, false);
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/EventTarget-add-remove-listener.any.js.ts b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-add-remove-listener.any.js.ts
new file mode 100644
index 0000000..6091ecc
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-add-remove-listener.any.js.ts
@@ -0,0 +1,24 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ function listener(evt: Event) {
+ evt.preventDefault();
+ return false;
+ }
+ context.test(
+ 'Removing an event listener without explicit capture arg should succeed',
+ ({ EventTarget, Event }) => {
+ const et = new EventTarget();
+ et.addEventListener('x', listener, false);
+ let event = new Event('x', { cancelable: true });
+ let ret = et.dispatchEvent(event);
+ expect(ret).toBe(false);
+
+ et.removeEventListener('x', listener);
+ event = new Event('x', { cancelable: true });
+ ret = et.dispatchEvent(event);
+ expect(ret).toBe(true);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/EventTarget-addEventListener.any.js.ts b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-addEventListener.any.js.ts
new file mode 100644
index 0000000..70a7c0c
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-addEventListener.any.js.ts
@@ -0,0 +1,11 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test('Adding a null event listener should succeed', ({ EventTarget }) => {
+ const et = new EventTarget();
+ expect(et.addEventListener('x', null as unknown as EventListener, false)).toBe(undefined);
+ expect(et.addEventListener('x', null as unknown as EventListener, true)).toBe(undefined);
+ expect(et.addEventListener('x', null as unknown as EventListener)).toBe(undefined);
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/EventTarget-dispatchEvent-returnvalue.html.ts b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-dispatchEvent-returnvalue.html.ts
new file mode 100644
index 0000000..d3478f8
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-dispatchEvent-returnvalue.html.ts
@@ -0,0 +1,94 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+const markup = `
+
+
+ Shady Grove
+ Aeolian
+
+
+ Over the river, Charlie
+ Dorian
+
+
+
`;
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Return value of EventTarget.dispatchEvent() affected by preventDefault().',
+ ({ document }) => {
+ const window = document.defaultView!;
+
+ let event_type = 'foo';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let default_prevented;
+ let return_value;
+
+ parent.addEventListener(event_type, function (e: Event) {}, true);
+ target.addEventListener(
+ event_type,
+ function (e: Event) {
+ evt.preventDefault();
+ default_prevented = evt.defaultPrevented;
+ return_value = evt.returnValue;
+ },
+ true,
+ );
+ target.addEventListener(event_type, function (e: Event) {}, true);
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, true, true);
+
+ expect(parent.dispatchEvent(evt)).toBe(true);
+ expect(target.dispatchEvent(evt)).toBe(false);
+ expect(default_prevented).toBe(true);
+ expect(return_value).toBe(false);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Return value of EventTarget.dispatchEvent() affected by returnValue.',
+ ({ document }) => {
+ const window = document.defaultView!;
+
+ let event_type = 'foo';
+ let target = document.getElementById('target')!;
+ let parent = document.getElementById('parent')!;
+ let default_prevented;
+ let return_value;
+
+ parent.addEventListener(event_type, function (e: Event) {}, true);
+ target.addEventListener(
+ event_type,
+ function (e: Event) {
+ evt.returnValue = false;
+ default_prevented = evt.defaultPrevented;
+ return_value = evt.returnValue;
+ },
+ true,
+ );
+ target.addEventListener(event_type, function (e: Event) {}, true);
+
+ let evt = document.createEvent('Event');
+ evt.initEvent(event_type, true, true);
+
+ expect(parent.dispatchEvent(evt)).toBe(true);
+ expect(target.dispatchEvent(evt)).toBe(false);
+ expect(default_prevented).toBe(true);
+ expect(return_value).toBe(false);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/EventTarget-dispatchEvent.html.ts b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-dispatchEvent.html.ts
new file mode 100644
index 0000000..08cdf14
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-dispatchEvent.html.ts
@@ -0,0 +1,108 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'Capturing event listeners should be called before non-capturing ones',
+ ({ document, Event, step }) => {
+ let results: (number | string)[] = [];
+ let b = document.createElement('b');
+ b.addEventListener(
+ 'x',
+ step(function () {
+ results.push(1);
+ }),
+ true,
+ );
+ b.addEventListener(
+ 'x',
+ step(function () {
+ results.push(2);
+ }),
+ false,
+ );
+ b.addEventListener(
+ 'x',
+ step(function () {
+ results.push(3);
+ }),
+ true,
+ );
+ b.dispatchEvent(new Event('x'));
+ expect(results).toHaveLength(3);
+ expect(results[0]).toBe(1);
+ expect(results[1]).toBe(3);
+ expect(results[2]).toBe(2);
+ },
+ );
+ }
+ {
+ const { expect } = context.runner;
+ context.test(
+ "If the event's dispatch flag is set, an InvalidStateError must be thrown.",
+ ({ document, step, DOMException }) => {
+ let e = document.createEvent('Event');
+ e.initEvent('type', false, false);
+
+ let target = document.createElement('div');
+ target.addEventListener(
+ 'type',
+ step(function () {
+ expect(function () {
+ target.dispatchEvent(e);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'InvalidStateError',
+ code: 11,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ document.dispatchEvent(e);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'InvalidStateError',
+ code: 11,
+ constructor: DOMException,
+ }),
+ );
+ }),
+ false,
+ );
+
+ expect(target.dispatchEvent(e), 'dispatchEvent must return true').toBe(true);
+ },
+ );
+ }
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'Event listeners added during dispatch should be called',
+ ({ document, Event, step }) => {
+ let results: (number | string)[] = [];
+ let outerb = document.createElement('b');
+ let middleb = outerb.appendChild(document.createElement('b'));
+ let innerb = middleb.appendChild(document.createElement('b'));
+ outerb.addEventListener(
+ 'x',
+ step(function () {
+ middleb.addEventListener(
+ 'x',
+ step(function () {
+ results.push('middle');
+ }),
+ true,
+ );
+ results.push('outer');
+ }),
+ true,
+ );
+ innerb.dispatchEvent(new Event('x'));
+ expect(results).toHaveLength(2);
+ expect(results[0]).toBe('outer');
+ expect(results[1]).toBe('middle');
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/EventTarget-removeEventListener.any.js.ts b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-removeEventListener.any.js.ts
new file mode 100644
index 0000000..9bc5329
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-removeEventListener.any.js.ts
@@ -0,0 +1,12 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test('removing a null event listener should succeed', ({ window }) => {
+ expect(window.removeEventListener('x', null as unknown as EventListener, false)).toBe(
+ undefined,
+ );
+ expect(window.removeEventListener('x', null as unknown as EventListener, true)).toBe(undefined);
+ expect(window.removeEventListener('x', null as unknown as EventListener)).toBe(undefined);
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/events/EventTarget-this-of-listener.html.ts b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-this-of-listener.html.ts
new file mode 100644
index 0000000..695c274
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/events/EventTarget-this-of-listener.html.ts
@@ -0,0 +1,183 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'the this value inside the event listener callback should be the node',
+ ({ document, CustomEvent, step }) => {
+ const nodes = [
+ document.createElement('p'),
+ document.createTextNode('some text'),
+ document.createDocumentFragment(),
+ document.createComment('a comment'),
+ document.createProcessingInstruction('target', 'data'),
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ node.addEventListener(
+ 'someevent',
+ step(function (this: Node) {
+ ++callCount;
+ expect(this).toBe(node);
+ }),
+ );
+
+ node.dispatchEvent(new CustomEvent('someevent'));
+ }
+
+ expect(callCount).toBe(nodes.length);
+ },
+ );
+ }
+ {
+ const { expect } = context.runner;
+ context.test(
+ 'the this value inside the event listener object handleEvent should be the object',
+ ({ document, CustomEvent, step }) => {
+ const nodes = [
+ document.createElement('p'),
+ document.createTextNode('some text'),
+ document.createDocumentFragment(),
+ document.createComment('a comment'),
+ document.createProcessingInstruction('target', 'data'),
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = {
+ handleEvent: step(function (this: EventListenerObject) {
+ ++callCount;
+ expect(this).toBe(handler);
+ }),
+ };
+
+ node.addEventListener('someevent', handler);
+
+ node.dispatchEvent(new CustomEvent('someevent'));
+ }
+
+ expect(callCount).toBe(nodes.length);
+ },
+ );
+ context.test(
+ 'dispatchEvent should invoke the current handleEvent method of the object',
+ ({ document, CustomEvent, step }) => {
+ const nodes = [
+ document.createElement('p'),
+ document.createTextNode('some text'),
+ document.createDocumentFragment(),
+ document.createComment('a comment'),
+ document.createProcessingInstruction('target', 'data'),
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = {
+ handleEvent: step(function (this: EventListenerObject) {
+ expect.unreachable('should not call the old handleEvent method');
+ }),
+ };
+
+ node.addEventListener('someevent', handler);
+ handler.handleEvent = step(function (this: EventListenerObject) {
+ ++callCount;
+ expect(this).toBe(handler);
+ });
+
+ node.dispatchEvent(new CustomEvent('someevent'));
+ }
+
+ expect(callCount).toBe(nodes.length);
+ },
+ );
+ context.test(
+ 'addEventListener should not require handleEvent to be defined on object listeners',
+ ({ document, CustomEvent, step }) => {
+ const nodes = [
+ document.createElement('p'),
+ document.createTextNode('some text'),
+ document.createDocumentFragment(),
+ document.createComment('a comment'),
+ document.createProcessingInstruction('target', 'data'),
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = {} as EventListenerObject;
+
+ node.addEventListener('someevent', handler);
+ handler.handleEvent = step(function (this: EventListenerObject) {
+ ++callCount;
+ expect(this).toBe(handler);
+ });
+
+ node.dispatchEvent(new CustomEvent('someevent'));
+ }
+
+ expect(callCount).toBe(nodes.length);
+ },
+ );
+ context.test(
+ 'handleEvent properties added to a function before addEventListener are not reached',
+ ({ document, CustomEvent, step }) => {
+ const nodes = [
+ document.createElement('p'),
+ document.createTextNode('some text'),
+ document.createDocumentFragment(),
+ document.createComment('a comment'),
+ document.createProcessingInstruction('target', 'data'),
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = step(function (this: Node) {
+ ++callCount;
+ expect(this).toBe(node);
+ }) as EventListener & { handleEvent?: () => void };
+
+ handler.handleEvent = step(() => {
+ expect.unreachable('should not call the handleEvent method on a function');
+ });
+
+ node.addEventListener('someevent', handler);
+
+ node.dispatchEvent(new CustomEvent('someevent'));
+ }
+
+ expect(callCount).toBe(nodes.length);
+ },
+ );
+ context.test(
+ 'handleEvent properties added to a function after addEventListener are not reached',
+ ({ document, CustomEvent, step }) => {
+ const nodes = [
+ document.createElement('p'),
+ document.createTextNode('some text'),
+ document.createDocumentFragment(),
+ document.createComment('a comment'),
+ document.createProcessingInstruction('target', 'data'),
+ ];
+
+ let callCount = 0;
+ for (const node of nodes) {
+ const handler = step(function (this: Node) {
+ ++callCount;
+ expect(this).toBe(node);
+ }) as EventListener & { handleEvent?: () => void };
+
+ node.addEventListener('someevent', handler);
+
+ handler.handleEvent = step(() => {
+ expect.unreachable('should not call the handleEvent method on a function');
+ });
+
+ node.dispatchEvent(new CustomEvent('someevent'));
+ }
+
+ expect(callCount).toBe(nodes.length);
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/lists/DOMTokenList-coverage-for-attributes.html.ts b/packages/web-platform-tests/src/wpt/dom/lists/DOMTokenList-coverage-for-attributes.html.ts
new file mode 100644
index 0000000..c07e059
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/lists/DOMTokenList-coverage-for-attributes.html.ts
@@ -0,0 +1,46 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ const pairs = [
+ { attr: 'classList', sup: ['anyElement'] },
+ { attr: 'relList', sup: ['a', 'area', 'link'] },
+ { attr: 'htmlFor', sup: ['output'] },
+ { attr: 'sandbox', sup: ['iframe'] },
+ { attr: 'sizes', sup: ['link'] },
+ ];
+ const namespaces = [
+ 'http://www.w3.org/1999/xhtml',
+ 'http://www.w3.org/2000/svg',
+ 'http://www.w3.org/1998/Math/MathML',
+ 'http://example.com/',
+ '',
+ ];
+ const elements = ['a', 'area', 'link', 'iframe', 'output', 'td', 'th'];
+ for (const pair of pairs) {
+ const supported = (localName: string, namespaceURI: string | null) =>
+ pair.attr === 'classList' ||
+ (pair.attr === 'relList' &&
+ localName === 'a' &&
+ (namespaceURI === 'http://www.w3.org/2000/svg' ||
+ namespaceURI === 'http://www.w3.org/1998/Math/MathML')) ||
+ (namespaceURI === 'http://www.w3.org/1999/xhtml' && pair.sup.includes(localName));
+ for (const ns of namespaces) {
+ for (const el of elements) {
+ context.test(
+ `${el}.${pair.attr} in ${ns || null} namespace should be ${supported(el, ns) ? 'DOMTokenList' : 'undefined'}.`,
+ ({ document }) => {
+ const new_el = document.createElementNS(ns, el) as Element & Record;
+ if (supported(new_el.localName, new_el.namespaceURI)) {
+ expect(Object.prototype.toString.call(new_el[pair.attr])).toBe(
+ '[object DOMTokenList]',
+ );
+ } else {
+ expect(new_el[pair.attr]).toBe(undefined);
+ }
+ },
+ );
+ }
+ }
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-appendData.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-appendData.html.ts
new file mode 100644
index 0000000..43c92ac
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-appendData.html.ts
@@ -0,0 +1,60 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [type, create] of [
+ ['Text', 'createTextNode'],
+ ['Comment', 'createComment'],
+ ] as const) {
+ context.test(type + ".appendData('bar')", ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.appendData('bar');
+ expect(node.data).toBe('testbar');
+ });
+ context.test(type + ".appendData('')", ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.appendData('');
+ expect(node.data).toBe('test');
+ });
+ context.test(type + '.appendData(non-ASCII)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+ node.appendData(', append more 資料,測試資料');
+ expect(node.data).toBe('test, append more 資料,測試資料');
+ expect(node.length).toBe(25);
+ });
+ context.test(type + '.appendData(null)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.appendData(null as unknown as string);
+ expect(node.data).toBe('testnull');
+ });
+ context.test(type + '.appendData(undefined)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.appendData(undefined as unknown as string);
+ expect(node.data).toBe('testundefined');
+ });
+ context.test(type + ".appendData('', 'bar')", ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+ (node.appendData as (...args: string[]) => void)('', 'bar');
+ expect(node.data).toBe('test');
+ });
+ context.test(type + '.appendData()', ({ document, TypeError }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(function () {
+ (node.appendData as (...args: unknown[]) => string)();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ expect(node.data).toBe('test');
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-data.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-data.html.ts
new file mode 100644
index 0000000..49c1a9f
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-data.html.ts
@@ -0,0 +1,135 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ {
+ const { expect } = context.runner;
+
+ context.test('Comment' + '.data initial value', ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+ expect(node.length).toBe(4);
+ });
+ context.test('Comment' + '.data = null', ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+
+ node.data = null as unknown as string;
+ expect(node.data).toBe('');
+ expect(node.length).toBe(0);
+ });
+ context.test('Comment' + '.data = undefined', ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+
+ node.data = undefined as unknown as string;
+ expect(node.data).toBe('undefined');
+ expect(node.length).toBe(9);
+ });
+ context.test('Comment' + '.data = 0', ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+
+ node.data = 0 as unknown as string;
+ expect(node.data).toBe('0');
+ expect(node.length).toBe(1);
+ });
+ context.test('Comment' + ".data = ''", ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+
+ node.data = '';
+ expect(node.data).toBe('');
+ expect(node.length).toBe(0);
+ });
+ context.test('Comment' + ".data = '--'", ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+
+ node.data = '--';
+ expect(node.data).toBe('--');
+ expect(node.length).toBe(2);
+ });
+ context.test('Comment' + ".data = '資料'", ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+
+ node.data = '資料';
+ expect(node.data).toBe('資料');
+ expect(node.length).toBe(2);
+ });
+ context.test('Comment' + ".data = '🌠 test 🌠 TEST'", ({ document }) => {
+ let node = document.createComment('test');
+ expect(node.data).toBe('test');
+
+ node.data = '🌠 test 🌠 TEST';
+ expect(node.data).toBe('🌠 test 🌠 TEST');
+ expect(node.length).toBe(15); // Counting UTF-16 code units
+ });
+ }
+
+ {
+ const { expect } = context.runner;
+
+ context.test('Text' + '.data initial value', ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+ expect(node.length).toBe(4);
+ });
+ context.test('Text' + '.data = null', ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+
+ node.data = null as unknown as string;
+ expect(node.data).toBe('');
+ expect(node.length).toBe(0);
+ });
+ context.test('Text' + '.data = undefined', ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+
+ node.data = undefined as unknown as string;
+ expect(node.data).toBe('undefined');
+ expect(node.length).toBe(9);
+ });
+ context.test('Text' + '.data = 0', ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+
+ node.data = 0 as unknown as string;
+ expect(node.data).toBe('0');
+ expect(node.length).toBe(1);
+ });
+ context.test('Text' + ".data = ''", ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+
+ node.data = '';
+ expect(node.data).toBe('');
+ expect(node.length).toBe(0);
+ });
+ context.test('Text' + ".data = '--'", ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+
+ node.data = '--';
+ expect(node.data).toBe('--');
+ expect(node.length).toBe(2);
+ });
+ context.test('Text' + ".data = '資料'", ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+
+ node.data = '資料';
+ expect(node.data).toBe('資料');
+ expect(node.length).toBe(2);
+ });
+ context.test('Text' + ".data = '🌠 test 🌠 TEST'", ({ document }) => {
+ let node = document.createTextNode('test');
+ expect(node.data).toBe('test');
+
+ node.data = '🌠 test 🌠 TEST';
+ expect(node.data).toBe('🌠 test 🌠 TEST');
+ expect(node.length).toBe(15); // Counting UTF-16 code units
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-deleteData.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-deleteData.html.ts
new file mode 100644
index 0000000..f1f1b6c
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-deleteData.html.ts
@@ -0,0 +1,114 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [type, create] of [
+ ['Text', 'createTextNode'],
+ ['Comment', 'createComment'],
+ ] as const) {
+ context.test(type + '.deleteData() out of bounds', ({ document, DOMException }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(function () {
+ node.deleteData(5, 10);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.deleteData(5, 0);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.deleteData(-1, 10);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.deleteData(-1, 0);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ });
+ context.test(type + '.deleteData() at the start', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.deleteData(0, 2);
+ expect(node.data).toBe('st');
+ });
+ context.test(type + '.deleteData() at the end', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.deleteData(2, 10);
+ expect(node.data).toBe('te');
+ });
+ context.test(type + '.deleteData() in the middle', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.deleteData(1, 1);
+ expect(node.data).toBe('tst');
+ });
+ context.test(type + '.deleteData() with zero count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.deleteData(2, 0);
+ expect(node.data).toBe('test');
+
+ node.deleteData(0, 0);
+ expect(node.data).toBe('test');
+ });
+ context.test(type + '.deleteData() with small negative count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.deleteData(2, -1);
+ expect(node.data).toBe('te');
+ });
+ context.test(type + '.deleteData() with large negative count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.deleteData(1, -0x100000000 + 2);
+ expect(node.data).toBe('tt');
+ });
+ context.test(type + '.deleteData() with non-ascii data', ({ document }) => {
+ let node = document[create]('test');
+ node.data = 'This is the character data test, append more 資料,更多測試資料';
+
+ node.deleteData(40, 5);
+ expect(node.data).toBe('This is the character data test, append 資料,更多測試資料');
+ node.deleteData(45, 2);
+ expect(node.data).toBe('This is the character data test, append 資料,更多資料');
+ });
+ context.test(type + '.deleteData() with non-BMP data', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.data = '🌠 test 🌠 TEST';
+
+ node.deleteData(5, 8); // Counting UTF-16 code units
+ expect(node.data).toBe('🌠 teST');
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-insertData.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-insertData.html.ts
new file mode 100644
index 0000000..b0a02a7
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-insertData.html.ts
@@ -0,0 +1,109 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [type, create] of [
+ ['Text', 'createTextNode'],
+ ['Comment', 'createComment'],
+ ] as const) {
+ context.test(type + '.insertData() out of bounds', ({ document, DOMException }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(function () {
+ node.insertData(5, 'x');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.insertData(5, '');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ });
+ context.test(type + '.insertData() negative out of bounds', ({ document, DOMException }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(function () {
+ node.insertData(-1, 'x');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.insertData(-0x100000000 + 5, 'x');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ });
+ context.test(type + '.insertData() negative in bounds', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.insertData(-0x100000000 + 2, 'X');
+ expect(node.data).toBe('teXst');
+ });
+ context.test(type + ".insertData('')", ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.insertData(0, '');
+ expect(node.data).toBe('test');
+ });
+ context.test(type + '.insertData() at the start', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.insertData(0, 'X');
+ expect(node.data).toBe('Xtest');
+ });
+ context.test(type + '.insertData() in the middle', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.insertData(2, 'X');
+ expect(node.data).toBe('teXst');
+ });
+ context.test(type + '.insertData() at the end', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.insertData(4, 'ing');
+ expect(node.data).toBe('testing');
+ });
+ context.test(type + '.insertData() with non-ascii data', ({ document }) => {
+ let node = document[create]('test');
+ node.data = 'This is the character data, append more 資料,測試資料';
+
+ node.insertData(26, ' test');
+ expect(node.data).toBe('This is the character data test, append more 資料,測試資料');
+ node.insertData(48, '更多');
+ expect(node.data).toBe('This is the character data test, append more 資料,更多測試資料');
+ });
+ context.test(type + '.insertData() with non-BMP data', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.data = '🌠 test 🌠 TEST';
+
+ node.insertData(5, '--'); // Counting UTF-16 code units
+ expect(node.data).toBe('🌠 te--st 🌠 TEST');
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-replaceData.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-replaceData.html.ts
new file mode 100644
index 0000000..b631343
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-replaceData.html.ts
@@ -0,0 +1,172 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [type, create] of [
+ ['Text', 'createTextNode'],
+ ['Comment', 'createComment'],
+ ] as const) {
+ context.test(type + '.replaceData() with invalid offset', ({ document, DOMException }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(function () {
+ node.replaceData(5, 1, 'x');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.replaceData(5, 0, '');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.replaceData(-1, 1, 'x');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.replaceData(-1, 0, '');
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(node.data).toBe('test');
+ });
+ context.test(type + '.replaceData() with clamped count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(2, 10, 'yo');
+ expect(node.data).toBe('teyo');
+ });
+ context.test(type + '.replaceData() with negative clamped count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(2, -1, 'yo');
+ expect(node.data).toBe('teyo');
+ });
+ context.test(type + '.replaceData() before the start', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(0, 0, 'yo');
+ expect(node.data).toBe('yotest');
+ });
+ context.test(type + '.replaceData() at the start (shorter)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(0, 2, 'y');
+ expect(node.data).toBe('yst');
+ });
+ context.test(type + '.replaceData() at the start (equal length)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(0, 2, 'yo');
+ expect(node.data).toBe('yost');
+ });
+ context.test(type + '.replaceData() at the start (longer)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(0, 2, 'yoa');
+ expect(node.data).toBe('yoast');
+ });
+ context.test(type + '.replaceData() in the middle (shorter)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(1, 2, 'o');
+ expect(node.data).toBe('tot');
+ });
+ context.test(type + '.replaceData() in the middle (equal length)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(1, 2, 'yo');
+ expect(node.data).toBe('tyot');
+ });
+ context.test(type + '.replaceData() in the middle (longer)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(1, 1, 'waddup');
+ expect(node.data).toBe('twaddupst');
+ node.replaceData(1, 1, 'yup');
+ expect(node.data).toBe('tyupaddupst');
+ });
+ context.test(type + '.replaceData() at the end (shorter)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(1, 20, 'yo');
+ expect(node.data).toBe('tyo');
+ });
+ context.test(type + '.replaceData() at the end (same length)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(2, 20, 'yo');
+ expect(node.data).toBe('teyo');
+ });
+ context.test(type + '.replaceData() at the end (longer)', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(4, 20, 'yo');
+ expect(node.data).toBe('testyo');
+ });
+ context.test(type + '.replaceData() the whole string', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(0, 4, 'quux');
+ expect(node.data).toBe('quux');
+ });
+ context.test(type + '.replaceData() with the empty string', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.replaceData(0, 4, '');
+ expect(node.data).toBe('');
+ });
+ context.test(type + '.replaceData() with non-ASCII data', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.data = 'This is the character data test, append 資料,更多資料';
+
+ node.replaceData(33, 6, 'other');
+ expect(node.data).toBe('This is the character data test, other 資料,更多資料');
+ node.replaceData(44, 2, '文字');
+ expect(node.data).toBe('This is the character data test, other 資料,更多文字');
+ });
+ context.test(type + '.replaceData() with non-BMP data', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.data = '🌠 test 🌠 TEST';
+
+ node.replaceData(5, 8, '--'); // Counting UTF-16 code units
+ expect(node.data).toBe('🌠 te--ST');
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-substringData.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-substringData.html.ts
new file mode 100644
index 0000000..89b2f82
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/CharacterData-substringData.html.ts
@@ -0,0 +1,147 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ for (const [type, create] of [
+ ['Text', 'createTextNode'],
+ ['Comment', 'createComment'],
+ ] as const) {
+ context.test(type + '.substringData() with too few arguments', ({ document, TypeError }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(function () {
+ (node.substringData as (...args: unknown[]) => string)();
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ expect(function () {
+ (node.substringData as (...args: unknown[]) => string)(0);
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ });
+ context.test(type + '.substringData() with too many arguments', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect((node.substringData as (...args: unknown[]) => string)(0, 1, 'test')).toBe('t');
+ });
+ context.test(type + '.substringData() with invalid offset', ({ document, DOMException }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(function () {
+ node.substringData(5, 0);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.substringData(6, 0);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ expect(function () {
+ node.substringData(-1, 0);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'IndexSizeError',
+ code: 1,
+ constructor: DOMException,
+ }),
+ );
+ });
+ context.test(type + '.substringData() with in-bounds offset', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(0, 1)).toBe('t');
+ expect(node.substringData(1, 1)).toBe('e');
+ expect(node.substringData(2, 1)).toBe('s');
+ expect(node.substringData(3, 1)).toBe('t');
+ expect(node.substringData(4, 1)).toBe('');
+ });
+ context.test(type + '.substringData() with zero count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(0, 0)).toBe('');
+ expect(node.substringData(1, 0)).toBe('');
+ expect(node.substringData(2, 0)).toBe('');
+ expect(node.substringData(3, 0)).toBe('');
+ expect(node.substringData(4, 0)).toBe('');
+ });
+ context.test(type + '.substringData() with very large offset', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(0x100000000 + 0, 1)).toBe('t');
+ expect(node.substringData(0x100000000 + 1, 1)).toBe('e');
+ expect(node.substringData(0x100000000 + 2, 1)).toBe('s');
+ expect(node.substringData(0x100000000 + 3, 1)).toBe('t');
+ expect(node.substringData(0x100000000 + 4, 1)).toBe('');
+ });
+ context.test(type + '.substringData() with negative offset', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(-0x100000000 + 2, 1)).toBe('s');
+ });
+ context.test(type + '.substringData() with string offset', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData('test' as unknown as number, 3)).toBe('tes');
+ });
+ context.test(type + '.substringData() with in-bounds count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(0, 1)).toBe('t');
+ expect(node.substringData(0, 2)).toBe('te');
+ expect(node.substringData(0, 3)).toBe('tes');
+ expect(node.substringData(0, 4)).toBe('test');
+ });
+ context.test(type + '.substringData() with large count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(0, 5)).toBe('test');
+ expect(node.substringData(2, 20)).toBe('st');
+ });
+ context.test(type + '.substringData() with very large count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(2, 0x100000000 + 1)).toBe('s');
+ });
+ context.test(type + '.substringData() with negative count', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ expect(node.substringData(0, -1)).toBe('test');
+ expect(node.substringData(0, -0x100000000 + 2)).toBe('te');
+ });
+ context.test(type + '.substringData() with non-ASCII data', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.data = 'This is the character data test, other 資料,更多文字';
+
+ expect(node.substringData(12, 4)).toBe('char');
+ expect(node.substringData(39, 2)).toBe('資料');
+ });
+ context.test(type + '.substringData() with non-BMP data', ({ document }) => {
+ let node = document[create]('test');
+ expect(node.data).toBe('test');
+
+ node.data = '🌠 test 🌠 TEST';
+
+ expect(node.substringData(5, 8)).toBe('st 🌠 TE'); // Counting UTF-16 code units
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/DOMImplementation-createDocument.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/DOMImplementation-createDocument.html.ts
new file mode 100644
index 0000000..a260e0f
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/DOMImplementation-createDocument.html.ts
@@ -0,0 +1,217 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+import { createElementNSTests } from './Document-createElementNS.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ const htmlNamespace = 'http://www.w3.org/1999/xhtml',
+ svgNamespace = 'http://www.w3.org/2000/svg',
+ mathMLNamespace = 'http://www.w3.org/1998/Math/MathML';
+ function createDocumentRows(document: Document, TypeError: typeof globalThis.TypeError) {
+ const tests = createElementNSTests.map(function (t) {
+ return [t[0], t[1], null, t[2]] as const;
+ }),
+ extra = [
+ /* Arrays with four elements:
+ * the namespace argument
+ * the qualifiedName argument
+ * the doctype argument
+ * the expected exception, or null if none
+ */
+ [null, null, false, TypeError],
+ [null, '', null, null],
+ [undefined, null, undefined, null],
+ [undefined, undefined, undefined, null],
+ [undefined, '', undefined, null],
+ ['http://example.com/', null, null, null],
+ ['http://example.com/', '', null, null],
+ ['/', null, null, null],
+ ['/', '', null, null],
+ ['http://www.w3.org/XML/1998/namespace', null, null, null],
+ ['http://www.w3.org/XML/1998/namespace', '', null, null],
+ ['http://www.w3.org/2000/xmlns/', null, null, null],
+ ['http://www.w3.org/2000/xmlns/', '', null, null],
+ ['foo:', null, null, null],
+ ['foo:', '', null, null],
+ [null, null, document.implementation.createDocumentType('foo', '', ''), null],
+ [null, null, document.doctype, null], // This causes a horrible WebKit bug (now fixed in trunk).
+ [
+ null,
+ null,
+ (function () {
+ var foo = document.implementation.createDocumentType('bar', '', '');
+ document.implementation.createDocument(null, null as unknown as string, foo);
+ return foo;
+ })(),
+ null,
+ ], // DOCTYPE already associated with a document.
+ [
+ null,
+ null,
+ (function () {
+ var bar = document.implementation.createDocument(null, null as unknown as string, null);
+ return bar.implementation.createDocumentType('baz', '', '');
+ })(),
+ null,
+ ], // DOCTYPE created by a different implementation.
+ [
+ null,
+ null,
+ (function () {
+ var bar = document.implementation.createDocument(null, null as unknown as string, null);
+ var magic = bar.implementation.createDocumentType('quz', '', '');
+ bar.implementation.createDocument(null, null as unknown as string, magic);
+ return magic;
+ })(),
+ null,
+ ], // DOCTYPE created by a different implementation and already associated with a document.
+ [null, 'foo', document.implementation.createDocumentType('foo', '', ''), null],
+ ['foo', null, document.implementation.createDocumentType('foo', '', ''), null],
+ ['foo', 'bar', document.implementation.createDocumentType('foo', '', ''), null],
+ [htmlNamespace, '', null, null],
+ [svgNamespace, '', null, null],
+ [mathMLNamespace, '', null, null],
+ [null, 'html', null, null],
+ [null, 'svg', null, null],
+ [null, 'math', null, null],
+ [
+ null,
+ '',
+ document.implementation.createDocumentType(
+ 'html',
+ '-//W3C//DTD XHTML 1.0 Transitional//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd',
+ ),
+ ],
+ [
+ null,
+ '',
+ document.implementation.createDocumentType(
+ 'svg',
+ '-//W3C//DTD SVG 1.1//EN',
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
+ ),
+ ],
+ [
+ null,
+ '',
+ document.implementation.createDocumentType(
+ 'math',
+ '-//W3C//DTD MathML 2.0//EN',
+ 'http://www.w3.org/Math/DTD/mathml2/mathml2.dtd',
+ ),
+ ],
+ ] as const;
+ return [...tests, ...extra];
+ }
+
+ for (let row = 0; row < createElementNSTests.length + 32; row++) {
+ context.test(
+ 'createDocument row ' + row + '',
+ ({ document, TypeError, Node, XMLDocument, DOMException }) => {
+ const [namespace, qualifiedName, doctype, expected] = createDocumentRows(
+ document,
+ TypeError,
+ )[row]!;
+
+ if (expected != null) {
+ if (typeof expected == 'string') {
+ if (expected === 'NAMESPACE_ERR') {
+ expect(function () {
+ document.implementation.createDocument(
+ namespace as string | null,
+ qualifiedName as string,
+ doctype as DocumentType | null,
+ );
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NamespaceError',
+ code: 14,
+ constructor: DOMException,
+ }),
+ );
+ } else {
+ expect(function () {
+ document.implementation.createDocument(
+ namespace as string | null,
+ qualifiedName as string,
+ doctype as DocumentType | null,
+ );
+ }).toThrow(
+ expect.objectContaining({
+ name: 'InvalidCharacterError',
+ code: 5,
+ constructor: DOMException,
+ }),
+ );
+ }
+ } else {
+ expect(function () {
+ document.implementation.createDocument(
+ namespace as string | null,
+ qualifiedName as string,
+ doctype as DocumentType | null,
+ );
+ }).toThrow(expect.objectContaining({ constructor: expected }));
+ }
+ } else {
+ let doc = document.implementation.createDocument(
+ namespace as string | null,
+ qualifiedName as string,
+ doctype as DocumentType | null,
+ );
+ expect(doc.nodeType).toBe(Node.DOCUMENT_NODE);
+ expect(doc.nodeType).toBe(doc.DOCUMENT_NODE);
+ expect(doc.nodeName).toBe('#document');
+ expect(doc.nodeValue).toBe(null);
+ expect(Object.getPrototypeOf(doc)).toBe(XMLDocument.prototype);
+ let omitRootElement = qualifiedName === null || String(qualifiedName) === '';
+ if (omitRootElement) {
+ expect(doc.documentElement).toBe(null);
+ } else {
+ let element = doc.documentElement;
+ expect(element).not.toBe(null);
+ expect(element.nodeType).toBe(Node.ELEMENT_NODE);
+ expect(element.ownerDocument).toBe(doc);
+ let qualified = String(qualifiedName);
+ let names = [];
+ let firstColonIndex = qualified.indexOf(':');
+ if (firstColonIndex >= 0) {
+ names = [
+ qualified.substring(0, firstColonIndex),
+ qualified.substring(firstColonIndex + 1),
+ ];
+ } else {
+ names = [null, qualified];
+ }
+ expect(element.prefix, 'element.prefix').toBe(names[0]);
+ expect(element.localName, 'element.localName').toBe(names[1]);
+ expect(element.namespaceURI, 'element.namespaceURI').toBe(
+ namespace === undefined || namespace === '' ? null : namespace,
+ );
+ }
+ if (!doctype) {
+ expect(doc.doctype).toBe(null);
+ } else {
+ expect(doc.doctype).toBe(doctype);
+ expect(doc.doctype!.ownerDocument).toBe(doc);
+ }
+ expect(doc.childNodes.length).toBe(Number(!omitRootElement) + Number(!!doctype));
+ }
+ },
+ );
+ }
+ context.test('createDocument with missing arguments', ({ document, TypeError }) => {
+ expect(function () {
+ (document.implementation.createDocument as (...args: unknown[]) => XMLDocument)();
+ }, 'createDocument() should throw').toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+
+ expect(function () {
+ (document.implementation.createDocument as (...args: unknown[]) => XMLDocument)('');
+ }, "createDocument('') should throw").toThrow(
+ expect.objectContaining({ constructor: TypeError }),
+ );
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/DOMImplementation-createHTMLDocument.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/DOMImplementation-createHTMLDocument.html.ts
new file mode 100644
index 0000000..0f2a7d3
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/DOMImplementation-createHTMLDocument.html.ts
@@ -0,0 +1,234 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ const titles = [
+ ['', '', ''],
+ [null, 'null', 'null'],
+ [undefined, undefined, ''],
+ ['foo bar baz', 'foo bar baz', 'foo bar baz'],
+ ['foo\t\tbar baz', 'foo\t\tbar baz', 'foo bar baz'],
+ ['foo\n\nbar baz', 'foo\n\nbar baz', 'foo bar baz'],
+ ['foo\f\fbar baz', 'foo\f\fbar baz', 'foo bar baz'],
+ ['foo\r\rbar baz', 'foo\r\rbar baz', 'foo bar baz'],
+ ] as const;
+
+ for (const [title, expectedtitle, normalizedtitle] of titles) {
+ context.test(
+ 'createHTMLDocument(' + String(title) + ')',
+ ({
+ document,
+ Document,
+ Node,
+ DocumentType,
+ HTMLHtmlElement,
+ HTMLHeadElement,
+ HTMLTitleElement,
+ HTMLBodyElement,
+ }) => {
+ function checkDoc(
+ doc: Document,
+ expectedtitle: string | undefined,
+ normalizedtitle: string,
+ ) {
+ expect(doc instanceof Document, 'Should be a Document').toBe(true);
+ expect(doc instanceof Node, 'Should be a Node').toBe(true);
+ expect(doc.childNodes.length, 'Document should have two child nodes').toBe(2);
+
+ let doctype = doc.doctype!;
+ expect(doctype instanceof DocumentType, 'Doctype should be a DocumentType').toBe(true);
+ expect(doctype instanceof Node, 'Doctype should be a Node').toBe(true);
+ expect(doctype.name).toBe('html');
+ expect(doctype.publicId).toBe('');
+ expect(doctype.systemId).toBe('');
+
+ let documentElement = doc.documentElement;
+ expect(
+ documentElement instanceof HTMLHtmlElement,
+ 'Document element should be a HTMLHtmlElement',
+ ).toBe(true);
+ expect(
+ documentElement.childNodes.length,
+ 'Document element should have two child nodes',
+ ).toBe(2);
+ expect(documentElement.localName).toBe('html');
+ expect(documentElement.tagName).toBe('HTML');
+
+ let head = documentElement.firstChild as HTMLHeadElement;
+ expect(head instanceof HTMLHeadElement, 'Head should be a HTMLHeadElement').toBe(true);
+ expect(head.localName).toBe('head');
+ expect(head.tagName).toBe('HEAD');
+
+ if (expectedtitle !== undefined) {
+ expect(head.childNodes.length).toBe(1);
+
+ let title = head.firstChild as HTMLTitleElement;
+ expect(title instanceof HTMLTitleElement, 'Title should be a HTMLTitleElement').toBe(
+ true,
+ );
+ expect(title.localName).toBe('title');
+ expect(title.tagName).toBe('TITLE');
+ expect(title.childNodes.length).toBe(1);
+ expect((title.firstChild as Text).data).toBe(expectedtitle);
+ } else {
+ expect(head.childNodes.length).toBe(0);
+ }
+
+ let body = documentElement.lastChild as HTMLBodyElement;
+ expect(body instanceof HTMLBodyElement, 'Body should be a HTMLBodyElement').toBe(true);
+ expect(body.localName).toBe('body');
+ expect(body.tagName).toBe('BODY');
+ expect(body.childNodes.length).toBe(0);
+ }
+ const doc = document.implementation.createHTMLDocument(title as string | undefined);
+ checkDoc(doc, expectedtitle, normalizedtitle);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ 'DOMImplementation.createHTMLDocument
',
+ },
+ },
+ );
+ }
+ context.test(
+ 'Missing title argument',
+ ({
+ document,
+ Document,
+ Node,
+ DocumentType,
+ HTMLHtmlElement,
+ HTMLHeadElement,
+ HTMLTitleElement,
+ HTMLBodyElement,
+ }) => {
+ function checkDoc(doc: Document, expectedtitle: string | undefined, normalizedtitle: string) {
+ expect(doc instanceof Document, 'Should be a Document').toBe(true);
+ expect(doc instanceof Node, 'Should be a Node').toBe(true);
+ expect(doc.childNodes.length, 'Document should have two child nodes').toBe(2);
+
+ let doctype = doc.doctype!;
+ expect(doctype instanceof DocumentType, 'Doctype should be a DocumentType').toBe(true);
+ expect(doctype instanceof Node, 'Doctype should be a Node').toBe(true);
+ expect(doctype.name).toBe('html');
+ expect(doctype.publicId).toBe('');
+ expect(doctype.systemId).toBe('');
+
+ let documentElement = doc.documentElement;
+ expect(
+ documentElement instanceof HTMLHtmlElement,
+ 'Document element should be a HTMLHtmlElement',
+ ).toBe(true);
+ expect(
+ documentElement.childNodes.length,
+ 'Document element should have two child nodes',
+ ).toBe(2);
+ expect(documentElement.localName).toBe('html');
+ expect(documentElement.tagName).toBe('HTML');
+
+ let head = documentElement.firstChild as HTMLHeadElement;
+ expect(head instanceof HTMLHeadElement, 'Head should be a HTMLHeadElement').toBe(true);
+ expect(head.localName).toBe('head');
+ expect(head.tagName).toBe('HEAD');
+
+ if (expectedtitle !== undefined) {
+ expect(head.childNodes.length).toBe(1);
+
+ let title = head.firstChild as HTMLTitleElement;
+ expect(title instanceof HTMLTitleElement, 'Title should be a HTMLTitleElement').toBe(
+ true,
+ );
+ expect(title.localName).toBe('title');
+ expect(title.tagName).toBe('TITLE');
+ expect(title.childNodes.length).toBe(1);
+ expect((title.firstChild as Text).data).toBe(expectedtitle);
+ } else {
+ expect(head.childNodes.length).toBe(0);
+ }
+
+ let body = documentElement.lastChild as HTMLBodyElement;
+ expect(body instanceof HTMLBodyElement, 'Body should be a HTMLBodyElement').toBe(true);
+ expect(body.localName).toBe('body');
+ expect(body.tagName).toBe('BODY');
+ expect(body.childNodes.length).toBe(0);
+ }
+ const doc = document.implementation.createHTMLDocument();
+ checkDoc(doc, undefined, '');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ 'DOMImplementation.createHTMLDocument
',
+ },
+ },
+ );
+ context.test(
+ 'createHTMLDocument(): metadata',
+ ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('test');
+ expect(doc.URL).toBe('about:blank');
+ expect(doc.documentURI).toBe('about:blank');
+ expect(doc.compatMode).toBe('CSS1Compat');
+ expect(doc.characterSet).toBe('UTF-8');
+ expect(doc.contentType).toBe('text/html');
+ expect(doc.createElement('DIV').localName).toBe('div');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ 'DOMImplementation.createHTMLDocument
',
+ },
+ },
+ );
+ context.test(
+ 'createHTMLDocument(): characterSet aliases',
+ ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('test');
+ expect(doc.characterSet, 'characterSet').toBe('UTF-8');
+ expect(doc.charset, 'charset').toBe('UTF-8');
+ expect(doc.inputEncoding, 'inputEncoding').toBe('UTF-8');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ 'DOMImplementation.createHTMLDocument
',
+ },
+ },
+ );
+ context.test(
+ 'createHTMLDocument(): URL parsing',
+ ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('test');
+ let a = doc.createElement('a');
+ // In UTF-8: 0xC3 0xA4
+ a.href = 'http://example.org/?\u00E4';
+ expect(a.href).toBe('http://example.org/?%C3%A4');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ 'DOMImplementation.createHTMLDocument
',
+ },
+ },
+ );
+ context.test(
+ 'createHTMLDocument(): document location getter is null',
+ ({ document }) => {
+ let doc = document.implementation.createHTMLDocument();
+ expect(doc.location).toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ 'DOMImplementation.createHTMLDocument
',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-adoptNode.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-adoptNode.html.ts
new file mode 100644
index 0000000..c29959e
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-adoptNode.html.ts
@@ -0,0 +1,104 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ context.test(
+ "Adopting an Element called 'x<' should work.",
+ ({ document }) => {
+ let y = document.getElementsByTagName('x<')[0]!;
+ let child = y.firstChild!;
+ expect(y.parentNode).toBe(document.body);
+ expect(y.ownerDocument).toBe(document);
+ expect(document.adoptNode(y)).toBe(y);
+ expect(y.parentNode).toBe(null);
+ expect(y.firstChild).toBe(child);
+ expect(y.ownerDocument).toBe(document);
+ expect(child.ownerDocument).toBe(document);
+ let doc = document.implementation.createDocument(null, null as unknown as string, null);
+ expect(doc.adoptNode(y)).toBe(y);
+ expect(y.parentNode).toBe(null);
+ expect(y.firstChild).toBe(child);
+ expect(y.ownerDocument).toBe(doc);
+ expect(child.ownerDocument).toBe(doc);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '
x ',
+ },
+ },
+ );
+ context.test("Adopting an Element called ':good:times:' should work.", ({ document }) => {
+ let x = document.createElement(':good:times:');
+ expect(document.adoptNode(x)).toBe(x);
+ let doc = document.implementation.createDocument(null, null as unknown as string, null);
+ expect(doc.adoptNode(x)).toBe(x);
+ expect(x.parentNode).toBe(null);
+ expect(x.ownerDocument).toBe(doc);
+ });
+ context.test('Explicitly adopting a DocumentType should work.', ({ document }) => {
+ let doctype = document.doctype!;
+ expect(doctype.parentNode).toBe(document);
+ expect(doctype.ownerDocument).toBe(document);
+ expect(document.adoptNode(doctype)).toBe(doctype);
+ expect(doctype.parentNode).toBe(null);
+ expect(doctype.ownerDocument).toBe(document);
+ });
+ context.test('Adopting a Document should throw.', ({ document, DOMException }) => {
+ let doc = document.implementation.createDocument(null, null as unknown as string, null);
+ expect(function () {
+ document.adoptNode(doc);
+ }).toThrow(
+ expect.objectContaining({
+ name: 'NotSupportedError',
+ code: 9,
+ constructor: DOMException,
+ }),
+ );
+ });
+ context.test('Adopting an Element should adopt its Attr nodes.', ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ let div = doc.createElement('div');
+ div.setAttribute('data-a', '1');
+ let attr = div.getAttributeNode('data-a')!;
+ expect(attr.ownerDocument).toBe(doc);
+ expect(document.adoptNode(div)).toBe(div);
+ expect(div.ownerDocument).toBe(document);
+ expect(attr.ownerDocument, 'existing Attr node must be adopted with its element').toBe(
+ document,
+ );
+ expect(div.getAttributeNode('data-a')!, 'Attr node identity must be preserved').toBe(attr);
+ expect(div.getAttributeNode('data-a')!.ownerDocument).toBe(document);
+ });
+ context.test(
+ 'Inserting an Element into another document should adopt its Attr nodes.',
+ ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ let div = doc.createElement('div');
+ let attr = doc.createAttribute('data-b');
+ div.setAttributeNode(attr);
+ document.body.appendChild(div);
+ expect(div.ownerDocument).toBe(document);
+ expect(
+ attr.ownerDocument,
+ 'Attr node must be adopted when its element is inserted into another document',
+ ).toBe(document);
+ div.remove();
+ },
+ );
+ context.test('Adopting an Element should adopt its namespaced attributes.', ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ let el = doc.createElement('a');
+ el.setAttributeNS('x', 'y', '1');
+ document.adoptNode(el);
+ expect(el.ownerDocument).toBe(document);
+ expect(
+ el.getAttributeNode('y')!.ownerDocument,
+ 'Attr node created after adoption must belong to the new document',
+ ).toBe(document);
+ expect(el.getAttributeNodeNS('x', 'y')!.ownerDocument).toBe(document);
+ expect(el.attributes[0]!.ownerDocument).toBe(document);
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-createComment.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createComment.html.ts
new file mode 100644
index 0000000..477d8ba
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createComment.html.ts
@@ -0,0 +1,29 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ for (const value of ['\u000b', 'a -- b', 'a-', '-b', null, undefined]) {
+ context.test(
+ `createComment(${String(value)})`,
+ ({ document, Comment, CharacterData, Node }) => {
+ let c = document.createComment(value as unknown as string);
+ let expected = String(value);
+ expect(c instanceof Comment).toBe(true);
+ expect(c instanceof CharacterData).toBe(true);
+ expect(c instanceof Node).toBe(true);
+ expect(c.ownerDocument).toBe(document);
+ expect(c.data, 'data').toBe(expected);
+ expect(c.nodeValue, 'nodeValue').toBe(expected);
+ expect(c.textContent, 'textContent').toBe(expected);
+ expect(c.length).toBe(expected.length);
+ expect(c.nodeType).toBe(8);
+ expect(c.nodeName).toBe('#comment');
+ expect(c.hasChildNodes()).toBe(false);
+ expect(c.childNodes.length).toBe(0);
+ expect(c.firstChild).toBe(null);
+ expect(c.lastChild).toBe(null);
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElement.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElement.html.ts
new file mode 100644
index 0000000..7974250
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElement.html.ts
@@ -0,0 +1,105 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ function toASCIIUppercase(str: string) {
+ let diff = 'a'.charCodeAt(0) - 'A'.charCodeAt(0);
+ let res = '';
+ for (let i = 0; i < str.length; ++i) {
+ if ('a' <= str[i]! && str[i]! <= 'z') {
+ res += String.fromCharCode(str.charCodeAt(i) - diff);
+ } else {
+ res += str[i]!;
+ }
+ }
+ return res;
+ }
+ function toASCIILowercase(str: string) {
+ let diff = 'a'.charCodeAt(0) - 'A'.charCodeAt(0);
+ let res = '';
+ for (let i = 0; i < str.length; ++i) {
+ if ('A' <= str[i]! && str[i]! <= 'Z') {
+ res += String.fromCharCode(str.charCodeAt(i) + diff);
+ } else {
+ res += str[i]!;
+ }
+ }
+ return res;
+ }
+ let HTMLNS = 'http://www.w3.org/1999/xhtml',
+ valid = [
+ undefined,
+ null,
+ 'foo',
+ 'f1oo',
+ 'foo1',
+ 'f\u0BC6',
+ 'foo\u0BC6',
+ ':',
+ ':foo',
+ 'f:oo',
+ 'foo:',
+ 'f:o:o',
+ 'f::oo',
+ 'f::oo:',
+ 'foo:0',
+ 'foo:_',
+ // combining char after :, invalid QName but valid Name
+ 'foo:\u0BC6',
+ 'foo:foo\u0BC6',
+ 'foo\u0BC6:foo',
+ 'xml',
+ 'xmlns',
+ 'xmlfoo',
+ 'xml:foo',
+ 'xmlns:foo',
+ 'xmlfoo:bar',
+ 'svg',
+ 'math',
+ 'FOO',
+ // Test that non-ASCII chars don't get uppercased/lowercased
+ 'mar\u212a',
+ '\u0130nput',
+ '\u0131nput',
+ '\u0300foo',
+ 'f}oo',
+ 'foo}',
+ '\ufffffoo',
+ 'f\uffffoo',
+ 'foo\uffff',
+ 'f', '', '-foo', '.foo'];
+
+ for (const t of valid) {
+ context.test('createElement(' + String(t) + ') in HTML document', (fixture) => {
+ const { document } = fixture;
+ const desc = 'HTML document';
+ const Element = fixture.Element;
+ const Node = fixture.Node;
+ let elt = document.createElement(t as string);
+ expect(elt instanceof Element, 'instanceof Element').toBe(true);
+ expect(elt instanceof Node, 'instanceof Node').toBe(true);
+ expect(elt.localName, 'localName').toBe(
+ desc == 'HTML document' ? toASCIILowercase(String(t)) : String(t),
+ );
+ expect(elt.tagName, 'tagName').toBe(
+ desc == 'HTML document' ? toASCIIUppercase(String(t)) : String(t),
+ );
+ expect(elt.prefix, 'prefix').toBe(null);
+ expect(elt.namespaceURI, 'namespaceURI').toBe(HTMLNS);
+ });
+ }
+ for (const arg of invalid) {
+ context.test('createElement(' + arg + ') in HTML document', ({ document, DOMException }) => {
+ expect(() => document.createElement(arg)).toThrow(
+ expect.objectContaining({
+ name: 'InvalidCharacterError',
+ code: 5,
+ constructor: DOMException,
+ }),
+ );
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElementNS.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElementNS.html.ts
new file mode 100644
index 0000000..0b8ee22
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElementNS.html.ts
@@ -0,0 +1,235 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+import { createElementNSTests } from './Document-createElementNS.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ const tests = [
+ ...createElementNSTests,
+ ...([
+ /* Arrays with three elements:
+ * the namespace argument
+ * the qualifiedName argument
+ * the expected exception, or null if none
+ */
+ ['', '', 'INVALID_CHARACTER_ERR'],
+ [null, '', 'INVALID_CHARACTER_ERR'],
+ [undefined, '', 'INVALID_CHARACTER_ERR'],
+ ['http://example.com/', null, null],
+ ['http://example.com/', '', 'INVALID_CHARACTER_ERR'],
+ ['/', null, null],
+ ['/', '', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/XML/1998/namespace', null, null],
+ ['http://www.w3.org/XML/1998/namespace', '', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/2000/xmlns/', null, 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', '', 'INVALID_CHARACTER_ERR'],
+ ['foo:', null, null],
+ ['foo:', '', 'INVALID_CHARACTER_ERR'],
+ ] as const),
+ ];
+
+ for (const t of tests) {
+ context.test(
+ 'createElementNS test in HTML document: ' + t.map(String).join(', '),
+ (fixture) => {
+ const { document } = fixture;
+ const doc = document;
+ const Node = fixture.Node;
+ let namespace = t[0],
+ qualifiedName = t[1],
+ expected = t[2];
+ if (expected != null) {
+ expect(() =>
+ doc.createElementNS(namespace as string | null, qualifiedName as string),
+ ).toThrow(
+ expect.objectContaining({
+ name: expected === 'NAMESPACE_ERR' ? 'NamespaceError' : 'InvalidCharacterError',
+ code: expected === 'NAMESPACE_ERR' ? 14 : 5,
+ constructor: fixture.DOMException,
+ }),
+ );
+ } else {
+ let element = doc.createElementNS(namespace as string | null, qualifiedName as string);
+ expect(element).not.toBe(null);
+ expect(element.nodeType).toBe(Node.ELEMENT_NODE);
+ expect(element.nodeType).toBe(element.ELEMENT_NODE);
+ expect(element.nodeValue).toBe(null);
+ expect(element.ownerDocument).toBe(doc);
+ let qualified = String(qualifiedName);
+ let names = [];
+ let firstColonIndex = qualified.indexOf(':');
+ if (firstColonIndex >= 0) {
+ names = [
+ qualified.substring(0, firstColonIndex),
+ qualified.substring(firstColonIndex + 1),
+ ];
+ } else {
+ names = [null, qualified];
+ }
+ expect(element.prefix, 'element.prefix').toBe(names[0]);
+ expect(element.localName, 'element.localName').toBe(names[1]);
+ expect(element.tagName, 'element.tagName').toBe(qualified);
+ expect(element.nodeName, 'element.nodeName').toBe(qualified);
+ expect(element.namespaceURI, 'element.namespaceURI').toBe(
+ namespace === undefined || namespace === '' ? null : namespace,
+ );
+ }
+ },
+ );
+ }
+ context.test(
+ 'Lower-case HTML element without a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let HTMLNS = 'http://www.w3.org/1999/xhtml';
+ let element = document.createElementNS(HTMLNS, 'span');
+ expect(element.namespaceURI).toBe(HTMLNS);
+ expect(element.prefix).toBe(null);
+ expect(element.localName).toBe('span');
+ expect(element.tagName).toBe('SPAN');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should be an HTMLElement').toBe(true);
+ expect(element instanceof HTMLSpanElement, 'Should be an HTMLSpanElement').toBe(true);
+ },
+ );
+ context.test(
+ 'Lower-case HTML element with a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let HTMLNS = 'http://www.w3.org/1999/xhtml';
+ let element = document.createElementNS(HTMLNS, 'html:span');
+ expect(element.namespaceURI).toBe(HTMLNS);
+ expect(element.prefix).toBe('html');
+ expect(element.localName).toBe('span');
+ expect(element.tagName).toBe('HTML:SPAN');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should be an HTMLElement').toBe(true);
+ expect(element instanceof HTMLSpanElement, 'Should be an HTMLSpanElement').toBe(true);
+ },
+ );
+ context.test(
+ 'Lower-case non-HTML element without a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let element = document.createElementNS('test', 'span');
+ expect(element.namespaceURI).toBe('test');
+ expect(element.prefix).toBe(null);
+ expect(element.localName).toBe('span');
+ expect(element.tagName).toBe('span');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should not be an HTMLElement').toBe(false);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+ context.test(
+ 'Lower-case non-HTML element with a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let element = document.createElementNS('test', 'html:span');
+ expect(element.namespaceURI).toBe('test');
+ expect(element.prefix).toBe('html');
+ expect(element.localName).toBe('span');
+ expect(element.tagName).toBe('html:span');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should not be an HTMLElement').toBe(false);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+ context.test(
+ 'Upper-case HTML element without a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement, HTMLUnknownElement }) => {
+ let HTMLNS = 'http://www.w3.org/1999/xhtml';
+ let element = document.createElementNS(HTMLNS, 'SPAN');
+ expect(element.namespaceURI).toBe(HTMLNS);
+ expect(element.prefix).toBe(null);
+ expect(element.localName).toBe('SPAN');
+ expect(element.tagName).toBe('SPAN');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should be an HTMLElement').toBe(true);
+ expect(element instanceof HTMLUnknownElement, 'Should be an HTMLUnknownElement').toBe(true);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+ context.test(
+ 'Upper-case HTML element with a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let HTMLNS = 'http://www.w3.org/1999/xhtml';
+ let element = document.createElementNS(HTMLNS, 'html:SPAN');
+ expect(element.namespaceURI).toBe(HTMLNS);
+ expect(element.prefix).toBe('html');
+ expect(element.localName).toBe('SPAN');
+ expect(element.tagName).toBe('HTML:SPAN');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should be an HTMLElement').toBe(true);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+ context.test(
+ 'Upper-case non-HTML element without a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let element = document.createElementNS('test', 'SPAN');
+ expect(element.namespaceURI).toBe('test');
+ expect(element.prefix).toBe(null);
+ expect(element.localName).toBe('SPAN');
+ expect(element.tagName).toBe('SPAN');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should not be an HTMLElement').toBe(false);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+ context.test(
+ 'Upper-case non-HTML element with a prefix',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let element = document.createElementNS('test', 'html:SPAN');
+ expect(element.namespaceURI).toBe('test');
+ expect(element.prefix).toBe('html');
+ expect(element.localName).toBe('SPAN');
+ expect(element.tagName).toBe('html:SPAN');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should not be an HTMLElement').toBe(false);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+ context.test('null namespace', ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let element = document.createElementNS(null, 'span');
+ expect(element.namespaceURI).toBe(null);
+ expect(element.prefix).toBe(null);
+ expect(element.localName).toBe('span');
+ expect(element.tagName).toBe('span');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should not be an HTMLElement').toBe(false);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ });
+ context.test(
+ 'undefined namespace',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let element = document.createElementNS(undefined as unknown as string, 'span');
+ expect(element.namespaceURI).toBe(null);
+ expect(element.prefix).toBe(null);
+ expect(element.localName).toBe('span');
+ expect(element.tagName).toBe('span');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should not be an HTMLElement').toBe(false);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+ context.test(
+ 'empty string namespace',
+ ({ document, Node, Element, HTMLElement, HTMLSpanElement }) => {
+ let element = document.createElementNS('', 'span');
+ expect(element.namespaceURI).toBe(null);
+ expect(element.prefix).toBe(null);
+ expect(element.localName).toBe('span');
+ expect(element.tagName).toBe('span');
+ expect(element instanceof Node, 'Should be a Node').toBe(true);
+ expect(element instanceof Element, 'Should be an Element').toBe(true);
+ expect(element instanceof HTMLElement, 'Should not be an HTMLElement').toBe(false);
+ expect(element instanceof HTMLSpanElement, 'Should not be an HTMLSpanElement').toBe(false);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElementNS.js.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElementNS.js.ts
new file mode 100644
index 0000000..536f3a7
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createElementNS.js.ts
@@ -0,0 +1,193 @@
+export const createElementNSTests: readonly (readonly [
+ string | null | undefined,
+ string | null | undefined,
+ string | null,
+])[] = [
+ /* Arrays with three elements:
+ * the namespace argument
+ * the qualifiedName argument
+ * the expected exception, or null if none
+ */
+ [null, null, null],
+ [null, undefined, null],
+ [null, 'foo', null],
+ [null, '1foo', 'INVALID_CHARACTER_ERR'],
+ [null, 'f1oo', null],
+ [null, 'foo1', null],
+ [null, '\u0BC6foo', null],
+ [null, '\u037Efoo', null],
+ [null, '}foo', 'INVALID_CHARACTER_ERR'],
+ [null, 'f}oo', null],
+ [null, 'foo}', null],
+ [null, '\uFFFFfoo', null],
+ [null, 'f\uFFFFoo', null],
+ [null, 'foo\uFFFF', null],
+ [null, '', 'INVALID_CHARACTER_ERR'],
+ [null, '', 'INVALID_CHARACTER_ERR'],
+ [null, 'f', 'INVALID_CHARACTER_ERR'],
+ ['http://example.com/', 'fo', 'INVALID_CHARACTER_ERR'],
+ ['http://example.com/', 'namespaceURI:,', 'INVALID_CHARACTER_ERR'],
+ ['http://example.com/', 'namespaceURI:a ', 'INVALID_CHARACTER_ERR'],
+ ['http://example.com/', 'namespaceURI:"', 'INVALID_CHARACTER_ERR'],
+ ['/', 'foo', null],
+ ['/', '1foo', 'INVALID_CHARACTER_ERR'],
+ ['/', 'f1oo', null],
+ ['/', 'foo1', null],
+ ['/', ':foo', 'INVALID_CHARACTER_ERR'],
+ ['/', 'f:oo', null],
+ ['/', 'foo:', 'INVALID_CHARACTER_ERR'],
+ ['/', 'xml', null],
+ ['/', 'xmlns', 'NAMESPACE_ERR'],
+ ['/', 'xmlfoo', null],
+ ['/', 'xml:foo', 'NAMESPACE_ERR'],
+ ['/', 'xmlns:foo', 'NAMESPACE_ERR'],
+ ['/', 'xmlfoo:bar', null],
+ ['http://www.w3.org/XML/1998/namespace', 'foo', null],
+ ['http://www.w3.org/XML/1998/namespace', '1foo', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/XML/1998/namespace', 'f1oo', null],
+ ['http://www.w3.org/XML/1998/namespace', 'foo1', null],
+ ['http://www.w3.org/XML/1998/namespace', ':foo', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/XML/1998/namespace', 'f:oo', null],
+ ['http://www.w3.org/XML/1998/namespace', 'foo:', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/XML/1998/namespace', 'xml', null],
+ ['http://www.w3.org/XML/1998/namespace', 'xmlns', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/XML/1998/namespace', 'xmlfoo', null],
+ ['http://www.w3.org/XML/1998/namespace', 'xml:foo', null],
+ ['http://www.w3.org/XML/1998/namespace', 'xmlns:foo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/XML/1998/namespace', 'xmlfoo:bar', null],
+ ['http://www.w3.org/XML/1998/namespaces', 'xml:foo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/xml/1998/namespace', 'xml:foo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'foo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', '1foo', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'f1oo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'foo1', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', ':foo', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'f:oo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'foo:', 'INVALID_CHARACTER_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'xml', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'xmlns', null],
+ ['http://www.w3.org/2000/xmlns/', 'xmlfoo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'xml:foo', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'xmlns:foo', null],
+ ['http://www.w3.org/2000/xmlns/', 'xmlfoo:bar', 'NAMESPACE_ERR'],
+ ['http://www.w3.org/2000/xmlns/', 'foo:xmlns', 'NAMESPACE_ERR'],
+ ['foo:', 'foo', null],
+ ['foo:', '1foo', 'INVALID_CHARACTER_ERR'],
+ ['foo:', 'f1oo', null],
+ ['foo:', 'foo1', null],
+ ['foo:', ':foo', 'INVALID_CHARACTER_ERR'],
+ ['foo:', 'f:oo', null],
+ ['foo:', 'foo:', 'INVALID_CHARACTER_ERR'],
+ ['foo:', 'xml', null],
+ ['foo:', 'xmlns', 'NAMESPACE_ERR'],
+ ['foo:', 'xmlfoo', null],
+ ['foo:', 'xml:foo', 'NAMESPACE_ERR'],
+ ['foo:', 'xmlns:foo', 'NAMESPACE_ERR'],
+ ['foo:', 'xmlfoo:bar', null],
+];
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-createTextNode.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createTextNode.html.ts
new file mode 100644
index 0000000..d928fdd
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-createTextNode.html.ts
@@ -0,0 +1,26 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ for (const value of ['\u000b', 'a -- b', 'a-', '-b', null, undefined]) {
+ context.test(`createTextNode(${String(value)})`, ({ document, Text, CharacterData, Node }) => {
+ let c = document.createTextNode(value as unknown as string);
+ let expected = String(value);
+ expect(c instanceof Text).toBe(true);
+ expect(c instanceof CharacterData).toBe(true);
+ expect(c instanceof Node).toBe(true);
+ expect(c.ownerDocument).toBe(document);
+ expect(c.data, 'data').toBe(expected);
+ expect(c.nodeValue, 'nodeValue').toBe(expected);
+ expect(c.textContent, 'textContent').toBe(expected);
+ expect(c.length).toBe(expected.length);
+ expect(c.nodeType).toBe(3);
+ expect(c.nodeName).toBe('#text');
+ expect(c.hasChildNodes()).toBe(false);
+ expect(c.childNodes.length).toBe(0);
+ expect(c.firstChild).toBe(null);
+ expect(c.lastChild).toBe(null);
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-getElementById.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-getElementById.html.ts
new file mode 100644
index 0000000..2d48099
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-getElementById.html.ts
@@ -0,0 +1,514 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const markup = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+ const { expect } = context.runner;
+
+ context.test(
+ 'Calling document.getElementById with an empty string argument.',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ expect(document.getElementById('')!).toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Calling document.getElementById with a null argument.',
+ ({ document, cleanup }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let element = document.createElement('div');
+ element.setAttribute('id', 'null');
+ document.body.appendChild(element);
+ cleanup(function () {
+ document.body.removeChild(element);
+ });
+ expect(document.getElementById(null as unknown as string)!).toBe(element);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Calling document.getElementById with an undefined argument.',
+ ({ document, cleanup }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let element = document.createElement('div');
+ element.setAttribute('id', 'undefined');
+ document.body.appendChild(element);
+ cleanup(function () {
+ document.body.removeChild(element);
+ });
+ expect(document.getElementById(undefined as unknown as string)!).toBe(element);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'on static page',
+ (fixture) => {
+ const { document } = fixture;
+ const gBody = document.getElementsByTagName('body')[0]!;
+ const HTMLDivElement = fixture.HTMLDivElement;
+
+ let bar = document.getElementById('test1')!;
+ expect(bar, 'should not be null').not.toBe(null);
+ expect(bar.tagName, 'should have expected tag name.').toBe('DIV');
+ expect(bar instanceof HTMLDivElement, 'should be a valid Element instance').toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Document.getElementById with a script-inserted element',
+ (fixture) => {
+ const { document } = fixture;
+ const gBody = document.getElementsByTagName('body')[0]!;
+ const HTMLDivElement = fixture.HTMLDivElement;
+
+ let TEST_ID = 'test2';
+
+ let test = document.createElement('div');
+ test.setAttribute('id', TEST_ID);
+ gBody.appendChild(test);
+
+ // test: appended element
+ let result = document.getElementById(TEST_ID)!;
+ expect(result, 'should not be null.').not.toBe(null);
+ expect(result.tagName, "should have appended element's tag name").toBe('DIV');
+ expect(result instanceof HTMLDivElement, 'should be a valid Element instance').toBe(true);
+
+ // test: removed element
+ gBody.removeChild(test);
+ let removed = document.getElementById(TEST_ID)!;
+ // `document.getElementById()` returns `null` if there is none.
+ // https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid
+ expect(removed, 'should not get removed element.').toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'update `id` attribute via setAttribute/removeAttribute',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ // setup fixtures.
+ let TEST_ID = 'test3';
+ let test = document.createElement('div');
+ test.setAttribute('id', TEST_ID);
+ gBody.appendChild(test);
+
+ // update id
+ let UPDATED_ID = 'test3-updated';
+ test.setAttribute('id', UPDATED_ID);
+ let e = document.getElementById(UPDATED_ID)!;
+ expect(e, 'should get the element with id.').toBe(test);
+
+ let old = document.getElementById(TEST_ID)!;
+ expect(old, "shouldn't get the element by the old id.").toBe(null);
+
+ // remove id.
+ test.removeAttribute('id');
+ let e2 = document.getElementById(UPDATED_ID)!;
+ expect(e2, 'should return null when the passed id is none in document.').toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Ensure that the id attribute only affects elements present in a document',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test4-should-not-exist';
+
+ let e = document.createElement('div');
+ e.setAttribute('id', TEST_ID);
+
+ expect(document.getElementById(TEST_ID)!, 'should be null').toBe(null);
+ document.body.appendChild(e);
+ expect(document.getElementById(TEST_ID)!, 'should be the appended element').toBe(e);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ "in tree order, within the context object's tree",
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ // the method should return the 1st element.
+ let TEST_ID = 'test5';
+ let target = document.getElementById(TEST_ID)!;
+ expect(target, 'should not be null').not.toBe(null);
+ expect(target.getAttribute('data-name'), 'should return the 1st').toBe('1st');
+
+ // even if after the new element was appended.
+ let element4 = document.createElement('div');
+ element4.setAttribute('id', TEST_ID);
+ element4.setAttribute('data-name', '4th');
+ gBody.appendChild(element4);
+ let target2 = document.getElementById(TEST_ID)!;
+ expect(target2, 'should not be null').not.toBe(null);
+ expect(target2.getAttribute('data-name'), 'should be the 1st').toBe('1st');
+
+ // should return the next element after removed the subtree including the 1st element.
+ target2.parentNode!.removeChild(target2);
+ let target3 = document.getElementById(TEST_ID)!;
+ expect(target3, 'should not be null').not.toBe(null);
+ expect(target3.getAttribute('data-name'), 'should be the 4th').toBe('4th');
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Modern browsers optimize this method with using internal id cache. ' +
+ 'This test checks that their optimization should effect only append to `Document`, not append to `Node`.',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test6';
+ let s = document.createElement('div');
+ s.setAttribute('id', TEST_ID);
+ // append to Element, not Document.
+ document.createElement('div').appendChild(s);
+
+ expect(document.getElementById(TEST_ID)!, 'should be null').toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ "changing attribute's value via `Attr` gotten from `Element.attribute`.",
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test7';
+ let element = document.createElement('div');
+ element.setAttribute('id', TEST_ID);
+ gBody.appendChild(element);
+
+ let target = document.getElementById(TEST_ID)!;
+ expect(target, 'should return the element before changing the value').toBe(element);
+
+ element.attributes[0]!.value = TEST_ID + '-updated';
+ let target2 = document.getElementById(TEST_ID)!;
+ expect(target2, 'should return null after updated id via Attr.value').toBe(null);
+ let target3 = document.getElementById(TEST_ID + '-updated')!;
+ expect(target3, 'should be equal to the updated element.').toBe(element);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'add id attribute via innerHTML',
+ (fixture) => {
+ const { document } = fixture;
+ const gBody = document.getElementsByTagName('body')[0]!;
+ const HTMLDivElement = fixture.HTMLDivElement;
+
+ let TEST_ID = 'test8';
+
+ // setup fixture
+ let element = document.createElement('div');
+ element.setAttribute('id', TEST_ID + '-fixture');
+ gBody.appendChild(element);
+
+ // add id-ed element with using innerHTML
+ element.innerHTML = "
";
+ let test = document.getElementById(TEST_ID)!;
+ expect(test, 'should not be null').toBe(element.firstChild);
+ expect(test.tagName, 'should have expected tag name.').toBe('DIV');
+ expect(test instanceof HTMLDivElement, 'should be a valid Element instance').toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'remove id attribute via innerHTML',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test9';
+
+ // add fixture
+ let container = document.createElement('div');
+ container.setAttribute('id', TEST_ID + '-fixture');
+ gBody.appendChild(container);
+
+ let element = document.createElement('div');
+ element.setAttribute('id', TEST_ID);
+ container.appendChild(element);
+
+ // check 'getElementById' should get the 'element'
+ expect(document.getElementById(TEST_ID)!, 'should not be null').toBe(element);
+
+ // remove id-ed element with using innerHTML (clear 'element')
+ container.innerHTML = '';
+ let test = document.getElementById(TEST_ID)!;
+ expect(test, 'should be null.').toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'add id attribute via outerHTML',
+ (fixture) => {
+ const { document } = fixture;
+ const gBody = document.getElementsByTagName('body')[0]!;
+ const HTMLDivElement = fixture.HTMLDivElement;
+
+ let TEST_ID = 'test10';
+
+ // setup fixture
+ let element = document.createElement('div');
+ element.setAttribute('id', TEST_ID + '-fixture');
+ gBody.appendChild(element);
+
+ // add id-ed element with using outerHTML
+ element.outerHTML = "
";
+ let test = document.getElementById(TEST_ID)!;
+ expect(test, 'should not be null').not.toBe(null);
+ expect(test.tagName, 'should have expected tag name.').toBe('DIV');
+ expect(test instanceof HTMLDivElement, 'should be a valid Element instance').toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'remove id attribute via outerHTML',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test11';
+
+ let element = document.createElement('div');
+ element.setAttribute('id', TEST_ID);
+ gBody.appendChild(element);
+
+ let test = document.getElementById(TEST_ID)!;
+ expect(test, 'should be equal to the appended element.').toBe(element);
+
+ // remove id-ed element with using outerHTML
+ element.outerHTML = '
';
+ test = document.getElementById(TEST_ID)!;
+ expect(test, 'should be null.').toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'update `id` attribute via element.id',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ // setup fixtures.
+ let TEST_ID = 'test12';
+ let test = document.createElement('div');
+ test.id = TEST_ID;
+ gBody.appendChild(test);
+
+ // update id
+ let UPDATED_ID = TEST_ID + '-updated';
+ test.id = UPDATED_ID;
+ let e = document.getElementById(UPDATED_ID)!;
+ expect(e, 'should get the element with id.').toBe(test);
+
+ let old = document.getElementById(TEST_ID)!;
+ expect(old, "shouldn't get the element by the old id.").toBe(null);
+
+ // remove id.
+ test.id = '';
+ let e2 = document.getElementById(UPDATED_ID)!;
+ expect(e2, 'should return null when the passed id is none in document.').toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ "where insertion order and tree order don't match",
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test13';
+
+ let create_same_id_element = function (order: string) {
+ let element = document.createElement('div');
+ element.setAttribute('id', TEST_ID);
+ element.setAttribute('data-order', order); // for debug
+ return element;
+ };
+
+ // create fixture
+ let container = document.createElement('div');
+ container.setAttribute('id', TEST_ID + '-fixture');
+ gBody.appendChild(container);
+
+ let element1 = create_same_id_element('1');
+ let element2 = create_same_id_element('2');
+ let element3 = create_same_id_element('3');
+ let element4 = create_same_id_element('4');
+
+ // append element: 2 -> 4 -> 3 -> 1
+ container.appendChild(element2);
+ container.appendChild(element4);
+ container.insertBefore(element3, element4);
+ container.insertBefore(element1, element2);
+
+ let test = document.getElementById(TEST_ID)!;
+ expect(test, 'should return 1st element').toBe(element1);
+ container.removeChild(element1);
+
+ test = document.getElementById(TEST_ID)!;
+ expect(test, 'should return 2nd element').toBe(element2);
+ container.removeChild(element2);
+
+ test = document.getElementById(TEST_ID)!;
+ expect(test, 'should return 3rd element').toBe(element3);
+ container.removeChild(element3);
+
+ test = document.getElementById(TEST_ID)!;
+ expect(test, 'should return 4th element').toBe(element4);
+ container.removeChild(element4);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Inserting an id by inserting its parent node',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test14';
+ let a = document.createElement('a');
+ let b = document.createElement('b');
+ a.appendChild(b);
+ b.id = TEST_ID;
+ expect(document.getElementById(TEST_ID)!).toBe(null);
+
+ gBody.appendChild(a);
+ expect(document.getElementById(TEST_ID)!).toBe(b);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'Document.getElementById must not return nodes not present in document',
+ ({ document }) => {
+ const gBody = document.getElementsByTagName('body')[0]!;
+
+ let TEST_ID = 'test15';
+ let outer = document.getElementById('outer')!;
+ let middle = document.getElementById('middle')!;
+ let inner = document.getElementById('inner')!;
+ outer.removeChild(middle);
+
+ let new_el = document.createElement('h1');
+ new_el.id = 'heading';
+ inner.appendChild(new_el);
+ // the new element is not part of the document since
+ // "middle" element was removed previously
+ expect(document.getElementById('heading')!).toBe(null);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Document-importNode.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Document-importNode.html.ts
new file mode 100644
index 0000000..abad3b4
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Document-importNode.html.ts
@@ -0,0 +1,63 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ context.test("No 'deep' argument.", ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ let div = doc.body.appendChild(doc.createElement('div'));
+ div.appendChild(doc.createElement('span'));
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ let newDiv = document.importNode(div);
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ expect(newDiv.ownerDocument).toBe(document);
+ expect(newDiv.firstChild).toBe(null);
+ });
+ context.test("Undefined 'deep' argument.", ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ let div = doc.body.appendChild(doc.createElement('div'));
+ div.appendChild(doc.createElement('span'));
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ let newDiv = document.importNode(div, undefined);
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ expect(newDiv.ownerDocument).toBe(document);
+ expect(newDiv.firstChild).toBe(null);
+ });
+ context.test("True 'deep' argument.", ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ let div = doc.body.appendChild(doc.createElement('div'));
+ div.appendChild(doc.createElement('span'));
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ let newDiv = document.importNode(div, true);
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ expect(newDiv.ownerDocument).toBe(document);
+ expect(newDiv.firstChild!.ownerDocument).toBe(document);
+ });
+ context.test("False 'deep' argument.", ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ let div = doc.body.appendChild(doc.createElement('div'));
+ div.appendChild(doc.createElement('span'));
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ let newDiv = document.importNode(div, false);
+ expect(div.ownerDocument).toBe(doc);
+ expect(div.firstChild!.ownerDocument).toBe(doc);
+ expect(newDiv.ownerDocument).toBe(document);
+ expect(newDiv.firstChild).toBe(null);
+ });
+ context.test('Import an Attr node with namespace/prefix correctly.', ({ document }) => {
+ let doc = document.implementation.createHTMLDocument('Title');
+ doc.body.setAttributeNS('http://example.com/', 'p:name', 'value');
+ let originalAttr = doc.body.getAttributeNodeNS('http://example.com/', 'name')!;
+ let imported = document.importNode(originalAttr, true);
+ expect(imported.prefix).toBe(originalAttr.prefix);
+ expect(imported.namespaceURI).toBe(originalAttr.namespaceURI);
+ expect(imported.localName).toBe(originalAttr.localName);
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/DocumentFragment-constructor.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/DocumentFragment-constructor.html.ts
new file mode 100644
index 0000000..d1c45dc
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/DocumentFragment-constructor.html.ts
@@ -0,0 +1,19 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ context.test(
+ 'Sets the owner document to the current global object associated document',
+ ({ document, DocumentFragment }) => {
+ const fragment = new DocumentFragment();
+ expect(fragment.ownerDocument).toBe(document);
+ },
+ );
+ context.test('Create a valid document DocumentFragment', ({ document, DocumentFragment }) => {
+ const fragment = new DocumentFragment();
+ const text = document.createTextNode('');
+ fragment.appendChild(text);
+ expect(fragment.firstChild).toBe(text);
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/DocumentFragment-querySelectorAll-after-modification.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/DocumentFragment-querySelectorAll-after-modification.html.ts
new file mode 100644
index 0000000..e450c33
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/DocumentFragment-querySelectorAll-after-modification.html.ts
@@ -0,0 +1,23 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function title(source: string, name: string): string {
+ return `WPT ${source}: ${name}`;
+ }
+
+ const { expect } = context.runner;
+
+ context.test(
+ title(
+ 'DocumentFragment-querySelectorAll-after-modification.html',
+ 'querySelectorAll should still work on DocumentFragments after they are modified',
+ ),
+ ({ document }) => {
+ const frag = document.createDocumentFragment();
+ frag.appendChild(document.createElement('div'));
+ expect(Array.from(frag.querySelectorAll('img')), 'before modification').toEqual([]);
+ frag.appendChild(document.createElement('div'));
+ expect(Array.from(frag.querySelectorAll('img')), 'after modification').toEqual([]);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-classlist.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-classlist.html.ts
new file mode 100644
index 0000000..1aaab0d
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-classlist.html.ts
@@ -0,0 +1,649 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ const XHTML_NS = 'http://www.w3.org/1999/xhtml';
+ const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
+
+ function setClass(e: Element, newVal: string | null) {
+ if (newVal === null) {
+ e.removeAttribute('class');
+ } else {
+ e.setAttribute('class', newVal);
+ }
+ }
+
+ function checkModification(
+ nodeIndex: number,
+ funcName: 'add' | 'remove' | 'toggle' | 'replace',
+ args: unknown,
+ expectedRes: unknown,
+ before: string | null,
+ after: string | null,
+ expectedException: string | undefined,
+ desc: string,
+ ) {
+ let e: Element;
+ const argumentsList: unknown[] = Array.isArray(args) ? args : [args];
+
+ context.test(
+ 'classList.' +
+ funcName +
+ '(' +
+ argumentsList.map(String).join(', ') +
+ ') with attribute value ' +
+ String(before) +
+ desc,
+ (fixture) => {
+ const { document } = fixture;
+ e = createNodes(document)[nodeIndex]!;
+ const MutationObserver = fixture.MutationObserver;
+
+ let res: unknown;
+ let shouldThrow = typeof expectedException === 'string';
+ if (shouldThrow) {
+ // If an exception is thrown, the class attribute shouldn't change.
+ after = before;
+ }
+ setClass(e, before);
+
+ let obs: MutationObserver | undefined;
+ // If we have MutationObservers available, do some checks to make
+ // sure attribute sets are happening at sane times.
+ if (MutationObserver) {
+ obs = new MutationObserver(() => {});
+ obs.observe(e, { attributes: true });
+ }
+ if (shouldThrow) {
+ expect(function () {
+ let list = e.classList;
+ res = Reflect.apply(list[funcName], list, argumentsList);
+ }).toThrow(
+ expect.objectContaining({
+ name: expectedException,
+ code: expectedException === 'SyntaxError' ? 12 : 5,
+ constructor: fixture.DOMException,
+ }),
+ );
+ } else {
+ let list = e.classList;
+ res = Reflect.apply(list[funcName], list, argumentsList);
+ }
+ if (obs) {
+ let mutationRecords = obs.takeRecords();
+ obs.disconnect();
+ if (shouldThrow) {
+ expect(mutationRecords.length, 'There should have been no mutation').toBe(0);
+ } else if (funcName == 'replace') {
+ expect(
+ mutationRecords.length == 1,
+ 'Should have a mutation exactly when replace() returns true',
+ ).toBe(expectedRes);
+ } else {
+ // For other functions, would need to check when exactly
+ // mutations are supposed to happen.
+ }
+ }
+ if (!shouldThrow) {
+ expect(res, 'wrong return value').toBe(expectedRes);
+ }
+
+ let expectedAfter = after;
+
+ expect(e.getAttribute('class'), 'wrong class after modification').toBe(expectedAfter);
+ },
+ {
+ document: { contentType: 'text/html', content: '
' },
+ },
+ );
+ }
+
+ function assignToClassListStrict(e: Element) {
+ 'use strict';
+ (e as unknown as { classList: string }).classList = 'foo';
+ e.removeAttribute('class');
+ }
+
+ function assignToClassList(e: Element) {
+ let expected = e.classList;
+ (e as unknown as { classList: string }).classList = 'foo';
+ expect(e.classList, 'classList should be unchanged after assignment').toBe(expected);
+ e.removeAttribute('class');
+ }
+
+ function testClassList(nodeIndex: number, desc: string) {
+ let e: Element;
+
+ // assignment
+
+ context.test(
+ 'Assigning to classList' + desc,
+ ({ document }) => {
+ e = createNodes(document)[nodeIndex]!;
+
+ assignToClassListStrict(e);
+ assignToClassList(e);
+ },
+ {
+ document: { contentType: 'text/html', content: '
' },
+ },
+ );
+
+ // supports
+ context.test(
+ '.supports() must throw TypeError' + desc,
+ (fixture) => {
+ const { document } = fixture;
+ e = createNodes(document)[nodeIndex]!;
+ const TypeError = fixture.TypeError;
+
+ expect(function () {
+ e.classList.supports('a');
+ }).toThrow(expect.objectContaining({ constructor: TypeError }));
+ },
+ {
+ document: { contentType: 'text/html', content: '
' },
+ },
+ );
+
+ // length attribute
+
+ function checkLength(value: string | null, length: number) {
+ context.test(
+ 'classList.length when ' + (value === null ? 'removed' : 'set to ' + String(value)) + desc,
+ ({ document }) => {
+ e = createNodes(document)[nodeIndex]!;
+
+ setClass(e, value);
+ expect(e.classList.length).toBe(length);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+ }
+
+ checkLength(null, 0);
+ checkLength('', 0);
+ checkLength(' \t \f', 0);
+ checkLength('a', 1);
+ checkLength('a A', 2);
+ checkLength('\r\na\t\f', 1);
+ checkLength('a a', 1);
+ checkLength('a a a a a a', 1);
+ checkLength('a a b b', 2);
+ checkLength('a A B b', 4);
+ checkLength('a b c c b a a b c c', 3);
+ checkLength(' a a b', 2);
+ checkLength('a\tb\nc\fd\re f', 6);
+
+ // [Stringifies]
+
+ function checkStringifier(value: string | null, expected: string) {
+ context.test(
+ 'classList.toString() when ' +
+ (value === null ? 'removed' : 'set to ' + String(value)) +
+ desc,
+ ({ document }) => {
+ e = createNodes(document)[nodeIndex]!;
+
+ setClass(e, value);
+ expect(e.classList.toString()).toBe(expected);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+ }
+
+ checkStringifier(null, '');
+ checkStringifier('foo', 'foo');
+ checkStringifier(' a a b', ' a a b');
+
+ // item() method
+
+ function checkItems(attributeValue: string | null, expectedValues: string[]) {
+ function checkItemFunction(index: number, expected: string | null | undefined) {
+ expect(e.classList.item(index), 'classList.item(' + index + ')').toBe(expected);
+ }
+
+ function checkItemArray(index: number, expected: string | undefined) {
+ expect(e.classList[index], 'classList[' + index + ']').toBe(expected);
+ }
+
+ context.test(
+ 'classList.item() when set to ' + String(attributeValue) + desc,
+ ({ document }) => {
+ e = createNodes(document)[nodeIndex]!;
+
+ setClass(e, attributeValue);
+
+ checkItemFunction(-1, null);
+ checkItemArray(-1, undefined);
+
+ let i = 0;
+ while (i < expectedValues.length) {
+ checkItemFunction(i, expectedValues[i]);
+ checkItemArray(i, expectedValues[i]);
+ i++;
+ }
+
+ checkItemFunction(i, null);
+ checkItemArray(i, undefined);
+
+ checkItemFunction(0xffffffff, null);
+ checkItemArray(0xffffffff, undefined);
+
+ checkItemFunction(0xfffffffe, null);
+ checkItemArray(0xfffffffe, undefined);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+ }
+
+ checkItems(null, []);
+ checkItems('a', ['a']);
+ checkItems('aa AA aa', ['aa', 'AA']);
+ checkItems('a b', ['a', 'b']);
+ checkItems(' a a b', ['a', 'b']);
+ checkItems('\t\n\f\r a\t\n\f\r b\t\n\f\r ', ['a', 'b']);
+
+ // contains() method
+
+ function checkContains(
+ attributeValue: string | null,
+ args: (string | null | undefined)[],
+ expectedRes: boolean | boolean[],
+ ) {
+ if (!Array.isArray(expectedRes)) {
+ expectedRes = Array(args.length).fill(expectedRes);
+ }
+
+ for (let i = 0; i < args.length; i++) {
+ context.test(
+ 'classList.contains(' +
+ String(args[i]) +
+ ') when set to ' +
+ String(attributeValue) +
+ desc,
+ ({ document }) => {
+ e = createNodes(document)[nodeIndex]!;
+
+ setClass(e, attributeValue);
+ expect(
+ e.classList.contains(args[i] as string),
+ 'classList.contains("' + args[i] + '")',
+ ).toBe((expectedRes as boolean[])[i]);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+ }
+ }
+
+ checkContains(null, ['a', '', ' '], false);
+ checkContains('', ['a'], false);
+
+ checkContains('a', ['a'], true);
+ checkContains('a', ['aa', 'b', 'A', 'a.', 'a)', , "a'", 'a"', 'a$', 'a~', 'a?', 'a\\'], false);
+
+ // All "ASCII whitespace" per spec, before and after
+ checkContains('a', ['a\t', '\ta', 'a\n', '\na', 'a\f', '\fa', 'a\r', '\ra', 'a ', ' a'], false);
+
+ checkContains('aa AA', ['aa', 'AA', 'aA'], [true, true, false]);
+ checkContains('a a a', ['a', 'aa', 'b'], [true, false, false]);
+ checkContains('a b c', ['a', 'b'], true);
+
+ checkContains('null undefined', [null, undefined], true);
+ checkContains('\t\n\f\r a\t\n\f\r b\t\n\f\r ', ['a', 'b'], true);
+
+ // add() method
+
+ function checkAdd(
+ before: string | null,
+ argument: unknown,
+ after: string | null,
+ param?: string,
+ ) {
+ let expectedException: string | undefined = undefined;
+ let noop = false;
+ if (param == 'noop') {
+ noop = true;
+ } else {
+ expectedException = param;
+ }
+ checkModification(
+ nodeIndex,
+ 'add',
+ argument,
+ undefined,
+ before,
+ after,
+ expectedException,
+ desc,
+ );
+ // Also check force toggle. The only difference is that it doesn't run the
+ // update steps for a no-op.
+ if (!Array.isArray(argument)) {
+ checkModification(
+ nodeIndex,
+ 'toggle',
+ [argument, true],
+ true,
+ before,
+ noop ? before : after,
+ expectedException,
+ desc,
+ );
+ }
+ }
+
+ checkAdd(null, '', null, 'SyntaxError');
+ checkAdd(null, ['a', ''], null, 'SyntaxError');
+ checkAdd(null, ' ', null, 'InvalidCharacterError');
+ checkAdd(null, '\ta', null, 'InvalidCharacterError');
+ checkAdd(null, 'a\t', null, 'InvalidCharacterError');
+ checkAdd(null, '\na', null, 'InvalidCharacterError');
+ checkAdd(null, 'a\n', null, 'InvalidCharacterError');
+ checkAdd(null, '\fa', null, 'InvalidCharacterError');
+ checkAdd(null, 'a\f', null, 'InvalidCharacterError');
+ checkAdd(null, '\ra', null, 'InvalidCharacterError');
+ checkAdd(null, 'a\r', null, 'InvalidCharacterError');
+ checkAdd(null, ' a', null, 'InvalidCharacterError');
+ checkAdd(null, 'a ', null, 'InvalidCharacterError');
+ checkAdd(null, ['a', ' '], null, 'InvalidCharacterError');
+ checkAdd(null, ['a', 'aa '], null, 'InvalidCharacterError');
+
+ checkAdd('a', 'a', 'a');
+ checkAdd('aa', 'AA', 'aa AA');
+ checkAdd('a b c', 'a', 'a b c');
+ checkAdd('a a a b', 'a', 'a b', 'noop');
+ checkAdd(null, 'a', 'a');
+ checkAdd('', 'a', 'a');
+ checkAdd(' ', 'a', 'a');
+ checkAdd(' \f', 'a', 'a');
+ checkAdd('a', 'b', 'a b');
+ checkAdd('a b c', 'd', 'a b c d');
+ checkAdd('a b c ', 'd', 'a b c d');
+ checkAdd(' a a b', 'c', 'a b c');
+ checkAdd(' a a b', 'a', 'a b', 'noop');
+ checkAdd('\t\n\f\r a\t\n\f\r b\t\n\f\r ', 'c', 'a b c');
+
+ // multiple add
+ checkAdd('a b c ', ['d', 'e'], 'a b c d e');
+ checkAdd('a b c ', ['a', 'a'], 'a b c');
+ checkAdd('a b c ', ['d', 'd'], 'a b c d');
+ checkAdd('a b c a ', [], 'a b c');
+ checkAdd(null, ['a', 'b'], 'a b');
+ checkAdd('', ['a', 'b'], 'a b');
+
+ checkAdd(null, null, 'null');
+ checkAdd(null, undefined, 'undefined');
+
+ // remove() method
+
+ function checkRemove(
+ before: string | null,
+ argument: unknown,
+ after: string | null,
+ param?: string,
+ ) {
+ let expectedException: string | undefined = undefined;
+ let noop = false;
+ if (param == 'noop') {
+ noop = true;
+ } else {
+ expectedException = param;
+ }
+ checkModification(
+ nodeIndex,
+ 'remove',
+ argument,
+ undefined,
+ before,
+ after,
+ expectedException,
+ desc,
+ );
+ // Also check force toggle. The only difference is that it doesn't run the
+ // update steps for a no-op.
+ if (!Array.isArray(argument)) {
+ checkModification(
+ nodeIndex,
+ 'toggle',
+ [argument, false],
+ false,
+ before,
+ noop ? before : after,
+ expectedException,
+ desc,
+ );
+ }
+ }
+
+ checkRemove(null, '', null, 'SyntaxError');
+ checkRemove(null, ' ', null, 'InvalidCharacterError');
+ checkRemove('\ta', '\ta', '\ta', 'InvalidCharacterError');
+ checkRemove('a\t', 'a\t', 'a\t', 'InvalidCharacterError');
+ checkRemove('\na', '\na', '\na', 'InvalidCharacterError');
+ checkRemove('a\n', 'a\n', 'a\n', 'InvalidCharacterError');
+ checkRemove('\fa', '\fa', '\fa', 'InvalidCharacterError');
+ checkRemove('a\f', 'a\f', 'a\f', 'InvalidCharacterError');
+ checkRemove('\ra', '\ra', '\ra', 'InvalidCharacterError');
+ checkRemove('a\r', 'a\r', 'a\r', 'InvalidCharacterError');
+ checkRemove(' a', ' a', ' a', 'InvalidCharacterError');
+ checkRemove('a ', 'a ', 'a ', 'InvalidCharacterError');
+ checkRemove('aa ', 'aa ', null, 'InvalidCharacterError');
+
+ checkRemove(null, 'a', null);
+ checkRemove('', 'a', '');
+ checkRemove('a b c', 'd', 'a b c', 'noop');
+ checkRemove('a b c', 'A', 'a b c', 'noop');
+ checkRemove(' a a a ', 'a', '');
+ checkRemove('a b', 'a', 'b');
+ checkRemove('a b ', 'a', 'b');
+ checkRemove('a a b', 'a', 'b');
+ checkRemove('aa aa bb', 'aa', 'bb');
+ checkRemove('a a b a a c a a', 'a', 'b c');
+
+ checkRemove('a b c', 'b', 'a c');
+ checkRemove('aaa bbb ccc', 'bbb', 'aaa ccc');
+ checkRemove(' a b c ', 'b', 'a c');
+ checkRemove('a b b b c', 'b', 'a c');
+
+ checkRemove('a b c', 'c', 'a b');
+ checkRemove(' a b c ', 'c', 'a b');
+ checkRemove('a b c c c', 'c', 'a b');
+
+ checkRemove('a b a c a d a', 'a', 'b c d');
+ checkRemove('AA BB aa CC AA dd aa', 'AA', 'BB aa CC dd');
+
+ checkRemove('\ra\na\ta\f', 'a', '');
+ checkRemove('\t\n\f\r a\t\n\f\r b\t\n\f\r ', 'a', 'b');
+
+ // multiple remove
+ checkRemove('a b c ', ['d', 'e'], 'a b c');
+ checkRemove('a b c ', ['a', 'b'], 'c');
+ checkRemove('a b c ', ['a', 'c'], 'b');
+ checkRemove('a b c ', ['a', 'a'], 'b c');
+ checkRemove('a b c ', ['d', 'd'], 'a b c');
+ checkRemove('a b c ', [], 'a b c');
+ checkRemove(null, ['a', 'b'], null);
+ checkRemove('', ['a', 'b'], '');
+ checkRemove('a a', [], 'a');
+
+ checkRemove('null', null, '');
+ checkRemove('undefined', undefined, '');
+
+ // toggle() method
+
+ function checkToggle(
+ before: string | null,
+ argument: unknown,
+ expectedRes: boolean | null,
+ after: string | null,
+ expectedException?: string,
+ ) {
+ checkModification(
+ nodeIndex,
+ 'toggle',
+ argument,
+ expectedRes,
+ before,
+ after,
+ expectedException,
+ desc,
+ );
+ }
+
+ checkToggle(null, '', null, null, 'SyntaxError');
+ checkToggle(null, 'aa ', null, null, 'InvalidCharacterError');
+
+ checkToggle(null, 'a', true, 'a');
+ checkToggle('', 'a', true, 'a');
+ checkToggle(' ', 'a', true, 'a');
+ checkToggle(' \f', 'a', true, 'a');
+ checkToggle('a', 'b', true, 'a b');
+ checkToggle('a', 'A', true, 'a A');
+ checkToggle('a b c', 'd', true, 'a b c d');
+ checkToggle(' a a b', 'd', true, 'a b d');
+
+ checkToggle('a', 'a', false, '');
+ checkToggle(' a a a ', 'a', false, '');
+ checkToggle(' A A A ', 'a', true, 'A a');
+ checkToggle(' a b c ', 'b', false, 'a c');
+ checkToggle(' a b c b b', 'b', false, 'a c');
+ checkToggle(' a b c ', 'c', false, 'a b');
+ checkToggle(' a b c ', 'a', false, 'b c');
+ checkToggle(' a a b', 'b', false, 'a');
+ checkToggle('\t\n\f\r a\t\n\f\r b\t\n\f\r ', 'a', false, 'b');
+ checkToggle('\t\n\f\r a\t\n\f\r b\t\n\f\r ', 'c', true, 'a b c');
+
+ checkToggle('null', null, false, '');
+ checkToggle('', null, true, 'null');
+ checkToggle('undefined', undefined, false, '');
+ checkToggle('', undefined, true, 'undefined');
+
+ // replace() method
+ function checkReplace(
+ before: string | null,
+ token: unknown,
+ newToken: unknown,
+ expectedRes: boolean | null,
+ after: string | null,
+ expectedException?: string,
+ ) {
+ checkModification(
+ nodeIndex,
+ 'replace',
+ [token, newToken],
+ expectedRes,
+ before,
+ after,
+ expectedException,
+ desc,
+ );
+ }
+
+ checkReplace(null, '', 'a', null, null, 'SyntaxError');
+ checkReplace(null, '', ' ', null, null, 'SyntaxError');
+ checkReplace(null, ' ', 'a', null, null, 'InvalidCharacterError');
+ checkReplace(null, '\ta', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'a\t', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, '\na', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'a\n', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, '\fa', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'a\f', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, '\ra', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'a\r', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, ' a', 'b', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'a ', 'b', null, null, 'InvalidCharacterError');
+
+ checkReplace(null, 'a', '', null, null, 'SyntaxError');
+ checkReplace(null, ' ', '', null, null, 'SyntaxError');
+ checkReplace(null, 'a', ' ', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', '\ta', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', 'a\t', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', '\na', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', 'a\n', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', '\fa', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', 'a\f', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', '\ra', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', 'a\r', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', ' a', null, null, 'InvalidCharacterError');
+ checkReplace(null, 'b', 'a ', null, null, 'InvalidCharacterError');
+
+ checkReplace('a', 'a', 'a', true, 'a');
+ checkReplace('a', 'a', 'b', true, 'b');
+ checkReplace('a', 'A', 'b', false, 'a');
+ checkReplace('a b', 'b', 'A', true, 'a A');
+ checkReplace('a b', 'c', 'a', false, 'a b');
+ checkReplace('a b c', 'd', 'e', false, 'a b c');
+ // https://github.com/whatwg/dom/issues/443
+ checkReplace('a a a b', 'a', 'a', true, 'a b');
+ checkReplace('a a a b', 'c', 'd', false, 'a a a b');
+ checkReplace(null, 'a', 'b', false, null);
+ checkReplace('', 'a', 'b', false, '');
+ checkReplace(' ', 'a', 'b', false, ' ');
+ checkReplace(' a \f', 'a', 'b', true, 'b');
+ checkReplace('a b c', 'b', 'd', true, 'a d c');
+ checkReplace('a b c', 'c', 'a', true, 'a b');
+ checkReplace('c b a', 'c', 'a', true, 'a b');
+ checkReplace('a b a', 'a', 'c', true, 'c b');
+ checkReplace('a b a', 'b', 'c', true, 'a c');
+ checkReplace(' a a b', 'a', 'c', true, 'c b');
+ checkReplace(' a a b', 'b', 'c', true, 'a c');
+ checkReplace('\t\n\f\r a\t\n\f\r b\t\n\f\r ', 'a', 'c', true, 'c b');
+ checkReplace('\t\n\f\r a\t\n\f\r b\t\n\f\r ', 'b', 'c', true, 'a c');
+
+ checkReplace('a null', null, 'b', true, 'a b');
+ checkReplace('a b', 'a', null, true, 'null b');
+ checkReplace('a undefined', undefined, 'b', true, 'a b');
+ checkReplace('a b', 'a', undefined, true, 'undefined b');
+ }
+
+ function createNodes(document: Document) {
+ const content = document.getElementById('content')!;
+ const htmlNode = document.createElement('div');
+ content.appendChild(htmlNode);
+
+ const xhtmlNode = document.createElementNS(XHTML_NS, 'div');
+ content.appendChild(xhtmlNode);
+
+ const mathMLNode = document.createElementNS(MATHML_NS, 'math');
+ content.appendChild(mathMLNode);
+
+ const xmlNode = document.createElementNS(null, 'foo');
+ content.appendChild(xmlNode);
+
+ const fooNode = document.createElementNS('http://example.org/foo', 'foo');
+ content.appendChild(fooNode);
+
+ return [htmlNode, xhtmlNode, mathMLNode, xmlNode, fooNode];
+ }
+ for (const [nodeIndex, desc] of [
+ ' (HTML node)',
+ ' (XHTML node)',
+ ' (MathML node)',
+ ' (XML node with null namespace)',
+ ' (foo node)',
+ ].entries()) {
+ testClassList(nodeIndex, desc);
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-closest-null-selectors.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-closest-null-selectors.html.ts
new file mode 100644
index 0000000..1f88593
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-closest-null-selectors.html.ts
@@ -0,0 +1,22 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function html({ document, connect }: DOMFixture, markup: string): Document {
+ const container = document.createElement('div');
+ container.innerHTML = markup;
+ for (const child of Array.from(container.children)) connect(child);
+ return document;
+ }
+
+ const { expect } = context.runner;
+
+ for (const selector of ['#null', '.null']) {
+ context.test(
+ `WPT Element-closest-null-selectors.html: element.closest('${selector}') should return null`,
+ (fixture) => {
+ const document = html(fixture, 'Some text
');
+ expect(document.getElementById('testP')!.closest(selector)).toBeNull();
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-closest.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-closest.html.ts
new file mode 100644
index 0000000..e7f4291
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-closest.html.ts
@@ -0,0 +1,82 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function html({ document, connect }: DOMFixture, markup: string): Document {
+ const container = document.createElement('div');
+ container.innerHTML = markup;
+ for (const child of Array.from(container.children)) connect(child);
+ return document;
+ }
+ const closestMarkup = `
+
+
`;
+ const closestCases = [
+ ['select', 'test12', 'test3'],
+ ['fieldset', 'test13', 'test2'],
+ ['div', 'test13', 'test6'],
+ ['body', 'test3', 'body'],
+ ['[default]', 'test4', 'test4'],
+ ['[selected]', 'test4', ''],
+ ['[selected]', 'test11', 'test11'],
+ ['[name="form-a"]', 'test12', 'test5'],
+ ['form[name="form-a"]', 'test13', 'test5'],
+ ['input[required]', 'test9', 'test9'],
+ ['select[required]', 'test9', ''],
+ ['div:not(.div1)', 'test13', 'test7'],
+ ['div.div3', 'test6', 'test8'],
+ ['div#test7', 'test1', 'test7'],
+ ['.div3 > .div2', 'test12', 'test7'],
+ ['.div3 > .div1', 'test12', ''],
+ ['form > input[required]', 'test9', ''],
+ ['fieldset > select[required]', 'test12', 'test3'],
+ ['input + fieldset', 'test6', ''],
+ ['form + form', 'test3', 'test5'],
+ ['form + form', 'test5', 'test5'],
+ [':empty', 'test10', 'test10'],
+ [':last-child', 'test11', 'test2'],
+ [':first-child', 'test12', 'test3'],
+ [':invalid', 'test11', 'test2'],
+ [':scope', 'test4', 'test4'],
+ ['select > :scope', 'test4', 'test4'],
+ ['div > :scope', 'test4', ''],
+ [':has(> :scope)', 'test4', 'test3'],
+ ] as const;
+
+ const { expect } = context.runner;
+
+ for (const [selector, elementId, targetId] of closestCases) {
+ context.test(
+ `WPT Element-closest.html: Element.closest with context node '${elementId}' and selector '${selector}'`,
+ (fixture) => {
+ const document = html(fixture, closestMarkup);
+ const oldId = document.body.getAttribute('id');
+ document.body.id = 'body';
+ fixture.cleanup(() => {
+ if (oldId === null) document.body.removeAttribute('id');
+ else document.body.id = oldId;
+ });
+ const result = document.getElementById(elementId)!.closest(selector);
+ if (result === null) expect(targetId).toBe('');
+ else expect(result.id).toBe(targetId);
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-hasAttributes.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-hasAttributes.html.ts
new file mode 100644
index 0000000..000dc74
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-hasAttributes.html.ts
@@ -0,0 +1,68 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ // XXX
+
+ // XXX
+
+ context.test(
+ 'element.hasAttributes() must return false when the element does not have attribute.',
+ ({ document }) => {
+ let buttonElement = document.getElementsByTagName('button')[0]!;
+ expect(
+ buttonElement.hasAttributes(),
+ 'hasAttributes() on empty element must return false.',
+ ).toBe(false);
+
+ let emptyDiv = document.createElement('div');
+ expect(
+ emptyDiv.hasAttributes(),
+ 'hasAttributes() on dynamically created empty element must return false.',
+ ).toBe(false);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+ context.test(
+ 'element.hasAttributes() must return true when the element has attribute.',
+ ({ document }) => {
+ let divWithId = document.getElementById('foo')!;
+ expect(
+ divWithId.hasAttributes(),
+ 'hasAttributes() on element with id attribute must return true.',
+ ).toBe(true);
+
+ let divWithClass = document.createElement('div');
+ divWithClass.setAttribute('class', 'foo');
+ expect(
+ divWithClass.hasAttributes(),
+ 'hasAttributes() on dynamically created element with class attribute must return true.',
+ ).toBe(true);
+
+ let pWithCustomAttr = document.getElementsByTagName('p')[0]!;
+ expect(
+ pWithCustomAttr.hasAttributes(),
+ 'hasAttributes() on element with custom attribute must return true.',
+ ).toBe(true);
+
+ let divWithCustomAttr = document.createElement('div');
+ divWithCustomAttr.setAttribute('data-custom', 'foo');
+ expect(
+ divWithCustomAttr.hasAttributes(),
+ 'hasAttributes() on dynamically created element with custom attribute must return true.',
+ ).toBe(true);
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: '
',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-active-pseudo-class.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-active-pseudo-class.html.ts
new file mode 100644
index 0000000..d135e84
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-active-pseudo-class.html.ts
@@ -0,0 +1,36 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function html({ document, connect }: DOMFixture, markup: string): Document {
+ const container = document.createElement('div');
+ container.innerHTML = markup;
+ for (const child of Array.from(container.children)) connect(child);
+ return document;
+ }
+
+ const { expect } = context.runner;
+
+ for (const [id, title] of [
+ ['test', 'Regular div should not match :active by default'],
+ ['button', 'Button should not match :active by default'],
+ ['link', 'Link should not match :active by default'],
+ ] as const) {
+ context.test(`WPT Element-matches-active-pseudo-class.html: ${title}`, (fixture) => {
+ const document = html(
+ fixture,
+ 'Test element
\nButton element \nLink element ',
+ );
+ expect(document.getElementById(id)!.matches(':active')).toBe(false);
+ });
+ }
+ context.test(
+ 'WPT Element-matches-active-pseudo-class.html: No elements should match :active by default via querySelectorAll',
+ (fixture) => {
+ const document = html(
+ fixture,
+ 'Test element
\nButton element \nLink element ',
+ );
+ expect(document.querySelectorAll(':active').length).toBe(0);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-complex-nth-last-of-type.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-complex-nth-last-of-type.html.ts
new file mode 100644
index 0000000..73f1b0d
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-complex-nth-last-of-type.html.ts
@@ -0,0 +1,15 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ context.test(
+ 'WPT Element-matches-complex-nth-last-of-type.html: element.matches() with complex :nth-last-of-type selector',
+ ({ document }) => {
+ const div = document.createElement('div');
+ div.innerHTML = '';
+ const c = div.querySelector('.c')!;
+ expect(c.matches('.a:nth-last-of-type(1) .b .c')).toBe(false);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-disabled-pseudo-class.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-disabled-pseudo-class.html.ts
new file mode 100644
index 0000000..d7f085b
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-disabled-pseudo-class.html.ts
@@ -0,0 +1,128 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function html({ document, connect }: DOMFixture, markup: string): Document {
+ const container = document.createElement('div');
+ container.innerHTML = markup;
+ for (const child of Array.from(container.children)) connect(child);
+ return document;
+ }
+ const disabledMarkup = `
+
+
+
+ Button
+
+
+
+
+
+ Button
+
+
+
+
+A Legend `;
+
+ const { expect } = context.runner;
+
+ for (const [id, title] of [
+ [
+ 'input-in-disabled-fieldset',
+ 'Input in disabled fieldset should match :disabled pseudo-class',
+ ],
+ [
+ 'select-in-disabled-fieldset',
+ 'Select in disabled fieldset should match :disabled pseudo-class',
+ ],
+ [
+ 'button-in-disabled-fieldset',
+ 'Button in disabled fieldset should match :disabled pseudo-class',
+ ],
+ [
+ 'textarea-in-disabled-fieldset',
+ 'Textarea in disabled fieldset should match :disabled pseudo-class',
+ ],
+ ['disabled-fieldset', 'Disabled fieldset itself should match :disabled pseudo-class'],
+ ] as const) {
+ context.test(`WPT Element-matches-disabled-pseudo-class.html: ${title}`, (fixture) => {
+ const document = html(fixture, disabledMarkup);
+ expect(document.getElementById(id)!.matches(':disabled')).toBe(true);
+ });
+ }
+ context.test(
+ 'WPT Element-matches-disabled-pseudo-class.html: Input in enabled fieldset should match :enabled pseudo-class',
+ (fixture) => {
+ const document = html(fixture, disabledMarkup);
+ const input = document.getElementById('input-in-enabled-fieldset')!;
+ expect(input.matches(':enabled')).toBe(true);
+ expect(input.matches(':disabled')).toBe(false);
+ },
+ );
+ context.test(
+ "WPT Element-matches-disabled-pseudo-class.html: querySelectorAll(':disabled') should select all disabled elements in DOM order",
+ (fixture) => {
+ const document = html(fixture, disabledMarkup);
+ expect(
+ Array.from(document.querySelectorAll(':disabled')).map((element) => element.id),
+ ).toEqual([
+ 'disabled-fieldset',
+ 'input-in-disabled-fieldset',
+ 'select-in-disabled-fieldset',
+ 'button-in-disabled-fieldset',
+ 'textarea-in-disabled-fieldset',
+ 'disabled-input',
+ ]);
+ },
+ );
+ context.test(
+ "WPT Element-matches-disabled-pseudo-class.html: querySelectorAll(':enabled') should not select elements in disabled fieldset",
+ (fixture) => {
+ const document = html(fixture, disabledMarkup);
+ const ids = Array.from(document.querySelectorAll(':enabled')).map((element) => element.id);
+ for (const id of [
+ 'enabled-fieldset',
+ 'input-in-enabled-fieldset',
+ 'select-in-enabled-fieldset',
+ 'button-in-enabled-fieldset',
+ 'textarea-in-enabled-fieldset',
+ 'enabled-input',
+ ])
+ expect(ids.includes(id)).toBe(true);
+ for (const id of [
+ 'disabled-fieldset',
+ 'input-in-disabled-fieldset',
+ 'select-in-disabled-fieldset',
+ 'button-in-disabled-fieldset',
+ 'textarea-in-disabled-fieldset',
+ 'disabled-input',
+ ])
+ expect(ids.includes(id)).toBe(false);
+ },
+ );
+ context.test(
+ "WPT Element-matches-disabled-pseudo-class.html: querySelector('input:enabled') should return the first enabled input",
+ (fixture) => {
+ const document = html(fixture, disabledMarkup);
+ expect(document.querySelector('input:enabled')!.id).toBe('input-in-enabled-fieldset');
+ },
+ );
+ context.test(
+ "WPT Element-matches-disabled-pseudo-class.html: querySelectorAll('input:disabled') should select inputs with disabled attribute and in disabled fieldset",
+ (fixture) => {
+ const document = html(fixture, disabledMarkup);
+ expect(
+ Array.from(document.querySelectorAll('input:disabled')).map((input) => input.id),
+ ).toEqual(['input-in-disabled-fieldset', 'disabled-input']);
+ },
+ );
+ context.test(
+ 'WPT Element-matches-disabled-pseudo-class.html: Fieldset with legend should not be considered disabled by default',
+ (fixture) => {
+ const document = html(fixture, disabledMarkup);
+ const fieldset = document.getElementById('fieldset-with-legend')!;
+ expect(fieldset.matches(':disabled')).toBe(false);
+ expect(fieldset.matches(':enabled')).toBe(true);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-namespaced-elements.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-namespaced-elements.html.ts
new file mode 100644
index 0000000..e7ca3d1
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches-namespaced-elements.html.ts
@@ -0,0 +1,23 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ for (const method of ['matches'] as const) {
+ for (const { namespace, name, selector, title } of [
+ {
+ namespace: '',
+ name: 'element',
+ selector: 'element',
+ title: `empty string namespace, ${method}`,
+ },
+ { namespace: 'urn:ns', name: 'h', selector: 'h', title: `has a namespace, ${method}` },
+ { namespace: 'urn:ns', name: 'h', selector: '*|h', title: `has a namespace, *|, ${method}` },
+ ]) {
+ context.test(`WPT Element-matches-namespaced-elements.html: ${title}`, ({ document }) => {
+ const element = document.createElementNS(namespace, name);
+ expect(element[method](selector)).toBe(true);
+ });
+ }
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches.html.ts
new file mode 100644
index 0000000..f6316bc
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches.html.ts
@@ -0,0 +1,6 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+import { registerMatches } from './Element-matches.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ registerMatches(context);
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches.js.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches.js.ts
new file mode 100644
index 0000000..5ee01d8
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-matches.js.ts
@@ -0,0 +1,93 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+import { prepare, permitted, roots, type Root, type Kind } from '../../../shared/selectors.ts';
+import { invalidSelectors, validSelectors, scopedSelectors, TEST_MATCH } from './selectors.js.ts';
+import { html } from './ParentNode-querySelector-All-content.html.ts';
+
+export function registerMatches(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ function test(
+ options: { kind: Kind; name: string },
+ run: (fixture: DOMFixture, root: Root) => void,
+ ): void {
+ context.test(
+ `WPT Element-matches.html: ${options.name}`,
+ (fixture) => run(fixture, prepare(fixture, options.kind)),
+ {
+ document: { contentType: 'text/html', content: html, fragment: 'target' },
+ },
+ );
+ }
+ for (const [kind, label] of Object.entries(roots) as [Exclude, string][]) {
+ const isElement = kind === 'element' || kind === 'detached';
+ test(
+ { kind, name: `${label} ${isElement ? 'supports' : 'should not support'} matches` },
+ (_fixture, root) => {
+ expect('matches' in root).toBe(isElement);
+ if (isElement) expect(Object.hasOwn(root, 'matches')).toBe(false);
+ },
+ );
+ if (isElement)
+ for (const row of invalidSelectors) {
+ test(
+ { kind, name: `${label}.matches: ${row.name}: ${row.selector}` },
+ ({ DOMException }, root) => {
+ try {
+ (root as Element).matches(row.selector);
+ expect.fail('Expected SyntaxError');
+ } catch (error) {
+ expect(error).toBeInstanceOf(DOMException);
+ expect(error).toMatchObject({ name: 'SyntaxError', code: 12 });
+ }
+ },
+ );
+ }
+ }
+ for (const tag of ['div', 'null', 'undefined']) {
+ for (const value of [null, undefined, 'missing'] as const) {
+ const suffix = value === 'missing' ? ' no parameter' : `(${String(value)})`;
+ test(
+ { kind: 'document', name: `${tag.toUpperCase()} Element.matches${suffix}` },
+ ({ TypeError }, root) => {
+ const element =
+ tag === 'div'
+ ? (root as Document).getElementById('root')!
+ : (root as Document).createElement(tag);
+ if (value === 'missing')
+ expect(() => Reflect.apply(element.matches, element, [])).toThrow(TypeError);
+ else expect(Reflect.apply(element.matches, element, [value])).toBe(tag === String(value));
+ },
+ );
+ }
+ }
+ for (const [kind, label] of [
+ ['document', 'In-document'],
+ ['detached', 'Detached'],
+ ['fragment', 'Fragment'],
+ ] as const) {
+ const rows = kind === 'document' ? [...validSelectors, ...scopedSelectors] : validSelectors;
+ for (const row of rows) {
+ if (!permitted(row, { kind, xml: false, mask: TEST_MATCH })) continue;
+ const reference = row.ctx ? 'refNode Element' : 'no refNodes';
+ test(
+ {
+ kind,
+ name: `${label} Element.matches: ${row.name} (with ${reference}): ${row.selector}`,
+ },
+ (_fixture, root) => {
+ for (const [ids, expected] of [
+ [row.expect, true],
+ [row.unexpected ?? [], false],
+ ] as const) {
+ for (const id of ids) {
+ const element = root.querySelector(`#${id}`)!;
+ const args = row.ctx ? [row.selector, root.querySelector(row.ctx)] : [row.selector];
+ expect(Reflect.apply(element.matches, element, args), `#${id}: ${row.selector}`).toBe(
+ expected,
+ );
+ }
+ }
+ },
+ );
+ }
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-querySelectorAll-context-element.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-querySelectorAll-context-element.html.ts
new file mode 100644
index 0000000..ab019c5
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-querySelectorAll-context-element.html.ts
@@ -0,0 +1,34 @@
+import type { CompatibilityContext, DOMFixture } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ function title(source: string, name: string): string {
+ return `WPT ${source}: ${name}`;
+ }
+ function mount({ document, connect }: DOMFixture, markup: string): HTMLElement {
+ const container = document.createElement('main');
+ container.innerHTML = markup;
+ connect(container);
+ return container;
+ }
+
+ const { expect } = context.runner;
+
+ for (const [name, selector, fromDocument] of [
+ ['Query from document', '.root table tbody tr', true],
+ ['Query without root class', 'table tbody tr', false],
+ ['Query with root class', '.root table tbody tr', false],
+ ] as const) {
+ context.test(title('Element-querySelectorAll-context-element.html', name), (fixture) => {
+ const { document } = fixture;
+ const container = mount(
+ fixture,
+ '',
+ );
+ const root = container.firstElementChild!.firstElementChild!;
+ const target = root.firstElementChild!.firstElementChild!.firstElementChild!;
+ expect(Array.from((fromDocument ? document : root).querySelectorAll(selector))).toEqual([
+ target,
+ ]);
+ });
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-setAttribute.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-setAttribute.html.ts
new file mode 100644
index 0000000..e95786a
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-setAttribute.html.ts
@@ -0,0 +1,36 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ // XXX
+
+ // XXX
+
+ context.test(
+ 'setAttribute should change the first attribute, irrespective of namespace',
+ ({ document }) => {
+ const el = document.createElement('p');
+ el.setAttributeNS('foo', 'x', 'first');
+ el.setAttributeNS('foo2', 'x', 'second');
+
+ el.setAttribute('x', 'changed');
+
+ expect(el.attributes.length).toBe(2);
+ expect(el.getAttribute('x')).toBe('changed');
+ expect(el.getAttributeNS('foo', 'x')).toBe('changed');
+ expect(el.getAttributeNS('foo2', 'x')).toBe('second');
+ },
+ );
+ context.test('setAttribute should lowercase before setting', ({ document }) => {
+ // https://github.com/whatwg/dom/issues/31
+
+ const el = document.createElement('p');
+ el.setAttribute('FOO', 'bar');
+
+ expect(el.getAttribute('foo')).toBe('bar');
+ expect(el.getAttribute('FOO')).toBe('bar');
+ expect(el.getAttributeNS('', 'foo')).toBe('bar');
+ expect(el.getAttributeNS('', 'FOO')).toBe(null);
+ });
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/Element-setAttributeNodeNS.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/Element-setAttributeNodeNS.html.ts
new file mode 100644
index 0000000..cef5345
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/Element-setAttributeNodeNS.html.ts
@@ -0,0 +1,56 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+
+ for (const namespacedFirst of [false, true]) {
+ context.test(
+ `Replacing an attribute prefix preserves name lookup order with namespacedFirst=${namespacedFirst}`,
+ ({ document }) => {
+ const element = document.createElement('div');
+ if (namespacedFirst) {
+ element.setAttributeNS('urn:test', 'p:id', 'namespaced');
+ }
+ element.setAttributeNS(null, 'id', 'plain');
+ if (!namespacedFirst) {
+ element.setAttributeNS('urn:test', 'p:id', 'namespaced');
+ }
+
+ const replacement = document.createAttributeNS('urn:test', 'id');
+ replacement.value = 'replacement';
+ element.setAttributeNodeNS(replacement);
+
+ expect(element.getAttribute('p:id')).toBe(null);
+ expect(element.hasAttribute('p:id')).toBe(false);
+ expect(element.getAttribute('id')).toBe(namespacedFirst ? 'replacement' : 'plain');
+ expect(element.getAttributeNS(null, 'id')).toBe('plain');
+ expect(element.getAttributeNS('urn:test', 'id')).toBe('replacement');
+
+ element.removeAttributeNS('urn:test', 'id');
+ expect(element.getAttribute('id')).toBe('plain');
+ expect(element.id).toBe('plain');
+ },
+ );
+ }
+ context.test(
+ 'Replacing an attribute prefix preserves other attributes with the old qualified name',
+ ({ document }) => {
+ const element = document.createElement('div');
+ element.setAttributeNS('urn:test', 'p:id', 'namespaced');
+ const attribute = document.createAttribute('p:id');
+ attribute.value = 'plain';
+ element.setAttributeNode(attribute);
+
+ const replacement = document.createAttributeNS('urn:test', 'q:id');
+ replacement.value = 'replacement';
+ element.setAttributeNodeNS(replacement);
+
+ expect(element.getAttribute('p:id')).toBe('plain');
+ expect(element.getAttributeNode('p:id')).toBe(attribute);
+ expect(element.getAttribute('q:id')).toBe('replacement');
+ element.removeAttribute('q:id');
+ expect(element.getAttribute('p:id')).toBe('plain');
+ expect(element.getAttribute('q:id')).toBe(null);
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-attributes.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-attributes.html.ts
new file mode 100644
index 0000000..9703b1a
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-attributes.html.ts
@@ -0,0 +1,1499 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+import { checkRecords } from './mutationobservers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ {
+ const markup = `
+
+MutationObservers: attributes mutations
+
+
+
+MutationObservers: attributes mutations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+ const { expect } = context.runner;
+ context.test(
+ 'attributes/attributeFilter Element.id/Element.className: update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n1000 = document.getElementById('n1000')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n1000', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n1000, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n1000, {
+ attributes: true,
+ attributeOldValue: true,
+ attributeFilter: ['id'],
+ });
+ (function () {
+ n1000.id = 'abc';
+ n1000.className = 'c01';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes/attributeFilter Element.id/Element.className: multiple filter update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n1001 = document.getElementById('n1001')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n1001', attributeName: 'id' },
+ { type: 'attributes', oldValue: 'c01', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n1001, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n1001, {
+ attributes: true,
+ attributeOldValue: true,
+ attributeFilter: ['id', 'class'],
+ });
+ (function () {
+ n1001.id = 'abc';
+ n1001.className = 'c02';
+ n1001.setAttribute('lang', 'fr');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributeFilter alone Element.id/Element.className: multiple filter update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n2001 = document.getElementById('n2001')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'id' },
+ { type: 'attributes', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n2001, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n2001, { attributeFilter: ['id', 'class'] });
+ (function () {
+ n2001.id = 'abcd';
+ n2001.className = 'c02';
+ n2001.setAttribute('lang', 'fr');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ }
+ {
+ const markup1 = `
+
+MutationObservers: attributes mutations
+
+
+
+MutationObservers: attributes mutations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+ const { expect } = context.runner;
+ context.test(
+ 'attributes Element.id: update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n00 = document.getElementById('n00')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n00', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n00, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n00, { attributes: true, attributeOldValue: true });
+ (function () {
+ n00.id = 'n000';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ context.test(
+ 'attributeOldValue alone Element.id: update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n2000 = document.getElementById('n2000')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n2000', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n2000, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n2000, { attributeOldValue: true });
+ (function () {
+ n2000.id = 'abc';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup1,
+ },
+ },
+ );
+ }
+ {
+ const markup = `
+
+MutationObservers: attributes mutations
+
+
+
+MutationObservers: attributes mutations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+ const { expect } = context.runner;
+ context.test(
+ 'attributes Element.id: update, no oldValue, mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n = document.getElementById('n')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n, { attributes: true });
+ (function () {
+ n.id = 'n000';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.id: empty string update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n01 = document.getElementById('n01')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n01', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n01, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n01, { attributes: true, attributeOldValue: true });
+ (function () {
+ n01.id = '';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.id: same value mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n02 = document.getElementById('n02')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n02', attributeName: 'id' },
+ { type: 'attributes', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n02, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n02, { attributes: true, attributeOldValue: true });
+ (function () {
+ n02.id = 'n02';
+ n02.setAttribute('class', 'c01');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.unknown: IDL attribute no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n03 = document.getElementById('n03')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n03', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n03, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n03, { attributes: true, attributeOldValue: true });
+ (function () {
+ (n03 as HTMLElement & { unknown: string }).unknown = 'c02';
+ n03.id = 'n030';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes HTMLInputElement.type: type update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n04 = document.getElementById('n04')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'text', attributeName: 'type' },
+ { type: 'attributes', oldValue: 'n04', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n04, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n04, { attributes: true, attributeOldValue: true });
+ (function () {
+ (n04 as HTMLInputElement).type = 'unknown';
+ n04.id = 'n040';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.className: new value mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n10 = document.getElementById('n10')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n10, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n10, { attributes: true, attributeOldValue: true });
+ (function () {
+ n10.className = 'c01';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.className: empty string update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n11 = document.getElementById('n11')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n11, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n11, { attributes: true, attributeOldValue: true });
+ (function () {
+ n11.className = '';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.className: same value mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n12 = document.getElementById('n12')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n12, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n12, { attributes: true, attributeOldValue: true });
+ (function () {
+ n12.className = 'c01';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.className: same multiple values mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n13 = document.getElementById('n13')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n13, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n13, { attributes: true, attributeOldValue: true });
+ (function () {
+ n13.className = 'c01 c02';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.add: single token addition mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n20 = document.getElementById('n20')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n20, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n20, { attributes: true, attributeOldValue: true });
+ (function () {
+ n20.classList.add('c01');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.add: multiple tokens addition mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n21 = document.getElementById('n21')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n21, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n21, { attributes: true, attributeOldValue: true });
+ (function () {
+ n21.classList.add('c01', 'c02', 'c03');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.add: syntax err/no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n22 = document.getElementById('n22')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n22', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n22, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n22, { attributes: true, attributeOldValue: true });
+ (function () {
+ try {
+ n22.classList.add('c01', '', 'c03');
+ } catch (e) {}
+ n22.id = 'n220';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.add: invalid character/no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n23 = document.getElementById('n23')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n23', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n23, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n23, { attributes: true, attributeOldValue: true });
+ (function () {
+ try {
+ n23.classList.add('c01', 'c 02', 'c03');
+ } catch (e) {}
+ n23.id = 'n230';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.add: same value mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n24 = document.getElementById('n24')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ { type: 'attributes', oldValue: 'n24', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n24, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n24, { attributes: true, attributeOldValue: true });
+ (function () {
+ n24.classList.add('c02');
+ n24.id = 'n240';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.remove: single token removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n30 = document.getElementById('n30')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n30, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n30, { attributes: true, attributeOldValue: true });
+ (function () {
+ n30.classList.remove('c01');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.remove: multiple tokens removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n31 = document.getElementById('n31')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n31, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n31, { attributes: true, attributeOldValue: true });
+ (function () {
+ n31.classList.remove('c01', 'c02');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.remove: missing token removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n32 = document.getElementById('n32')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ { type: 'attributes', oldValue: 'n32', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n32, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n32, { attributes: true, attributeOldValue: true });
+ (function () {
+ n32.classList.remove('c03');
+ n32.id = 'n320';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.toggle: token removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n40 = document.getElementById('n40')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n40, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n40, { attributes: true, attributeOldValue: true });
+ (function () {
+ n40.classList.toggle('c01');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.toggle: token addition mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n41 = document.getElementById('n41')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n41, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n41, { attributes: true, attributeOldValue: true });
+ (function () {
+ n41.classList.toggle('c03');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.toggle: forced token removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n42 = document.getElementById('n42')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n42, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n42, { attributes: true, attributeOldValue: true });
+ (function () {
+ n42.classList.toggle('c01', false);
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.toggle: forced missing token removal no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n43 = document.getElementById('n43')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n43', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n43, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n43, { attributes: true, attributeOldValue: true });
+ (function () {
+ n43.classList.toggle('c03', false);
+ n43.id = 'n430';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.toggle: forced existing token addition no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n44 = document.getElementById('n44')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n44', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n44, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n44, { attributes: true, attributeOldValue: true });
+ (function () {
+ n44.classList.toggle('c01', true);
+ n44.id = 'n440';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.classList.toggle: forced token addition mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n45 = document.getElementById('n45')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n45, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n45, { attributes: true, attributeOldValue: true });
+ (function () {
+ n45.classList.toggle('c03', true);
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.attributes.value: update mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n50 = document.getElementById('n50')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01 c02', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n50, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n50, { attributes: true, attributeOldValue: true });
+ (function () {
+ for (let i = 0; i < n50.attributes.length; i++) {
+ let attr = n50.attributes[i]!;
+ if (attr.localName === 'class') {
+ attr.value = 'c03';
+ }
+ }
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.attributes.value: same id mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n51 = document.getElementById('n51')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n51', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n51, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n51, { attributes: true, attributeOldValue: true });
+ (function () {
+ n51.attributes[0]!.value = 'n51';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.setAttribute: id mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n60 = document.getElementById('n60')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n60', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n60, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n60, { attributes: true, attributeOldValue: true });
+ (function () {
+ n60.setAttribute('id', 'n601');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.setAttribute: same class mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n61 = document.getElementById('n61')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n61, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n61, { attributes: true, attributeOldValue: true });
+ (function () {
+ n61.setAttribute('class', 'c01');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.setAttribute: classname mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n62 = document.getElementById('n62')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'classname' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n62, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n62, { attributes: true, attributeOldValue: true });
+ (function () {
+ n62.setAttribute('classname', 'c01');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.removeAttribute: removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n70 = document.getElementById('n70')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'c01', attributeName: 'class' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n70, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n70, { attributes: true, attributeOldValue: true });
+ (function () {
+ n70.removeAttribute('class');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.removeAttribute: removal no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n71 = document.getElementById('n71')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n71', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n71, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n71, { attributes: true, attributeOldValue: true });
+ (function () {
+ n71.removeAttribute('class');
+ n71.id = 'n710';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'childList HTMLInputElement.removeAttribute: type removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n72 = document.getElementById('n72')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'text', attributeName: 'type' },
+ { type: 'attributes', oldValue: 'n72', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n72, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n72, { attributes: true, attributeOldValue: true });
+ (function () {
+ n72.removeAttribute('type');
+ n72.id = 'n720';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.setAttributeNS: creation mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n80 = document.getElementById('n80')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ {
+ type: 'attributes',
+ attributeName: 'private',
+ attributeNamespace: 'http://example.org/',
+ },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n80, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n80, { attributes: true, attributeOldValue: true });
+ (function () {
+ n80.setAttributeNS('http://example.org/', 'private', '42');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.setAttributeNS: prefixed attribute creation mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n81 = document.getElementById('n81')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ {
+ type: 'attributes',
+ attributeName: 'lang',
+ attributeNamespace: 'http://www.w3.org/XML/1998/namespace',
+ },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n81, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n81, { attributes: true, attributeOldValue: true });
+ (function () {
+ n81.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang', '42');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.removeAttributeNS: removal mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n90 = document.getElementById('n90')!;
+ n90.setAttributeNS('http://example.org/', 'private', '42');
+
+ const mutationRecordSequence: Parameters[3] = [
+ {
+ type: 'attributes',
+ oldValue: '42',
+ attributeName: 'private',
+ attributeNamespace: 'http://example.org/',
+ },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n90, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n90, { attributes: true, attributeOldValue: true });
+ (function () {
+ n90.removeAttributeNS('http://example.org/', 'private');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.removeAttributeNS: removal no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n91 = document.getElementById('n91')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n91', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n91, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n91, { attributes: true, attributeOldValue: true });
+ (function () {
+ n91.removeAttributeNS('http://example.org/', 'private');
+ n91.id = 'n910';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'attributes Element.removeAttributeNS: prefixed attribute removal no mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n92 = document.getElementById('n92')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', oldValue: 'n92', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n92, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n92, { attributes: true, attributeOldValue: true });
+ (function () {
+ n92.removeAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:lang');
+ n92.id = 'n920';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'childList false: no childList mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n3000 = document.getElementById('n3000')!;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'attributes', attributeName: 'id' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n3000, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n3000, { subtree: true, childList: false, attributes: true });
+ (function () {
+ n3000.textContent = 'CHANGED';
+ n3000.id = 'abc';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ }
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-callback-order.window.js.ts b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-callback-order.window.js.ts
new file mode 100644
index 0000000..c9ed90a
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-callback-order.window.js.ts
@@ -0,0 +1,82 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ function nextTask() {
+ return new Promise((resolve) => setTimeout(resolve, 0));
+ }
+ context.test(
+ 'Mutation observers are notified in the order they were created',
+ async ({ document, MutationObserver }) => {
+ const firstTarget = document.createElement('div');
+ const secondTarget = document.createElement('div');
+ const order: string[] = [];
+ const first = new MutationObserver(() => order.push('first'));
+ const second = new MutationObserver(() => order.push('second'));
+ first.observe(firstTarget, { attributes: true });
+ second.observe(secondTarget, { attributes: true });
+
+ secondTarget.setAttribute('data-test', '');
+ firstTarget.setAttribute('data-test', '');
+ await nextTask();
+
+ {
+ const actual = order;
+ const expected = ['first', 'second'];
+ expect(actual.length).toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index]).toBe(expected[index]);
+ }
+ },
+ );
+ context.test(
+ 'Mutation observers are notified in the order they were created, also when one observes a removed subtree',
+ async ({ document, MutationObserver }) => {
+ const parent = document.createElement('div');
+ const child = parent.appendChild(document.createElement('div'));
+ const order: string[] = [];
+ const first = new MutationObserver(() => order.push('first'));
+ const second = new MutationObserver(() => order.push('second'));
+ first.observe(parent, { childList: true });
+ second.observe(parent, { attributes: true, subtree: true });
+
+ // This queues a record for first, while second keeps observing child.
+ child.remove();
+ child.setAttribute('data-test', '');
+ await nextTask();
+
+ {
+ const actual = order;
+ const expected = ['first', 'second'];
+ expect(actual.length).toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index]).toBe(expected[index]);
+ }
+ },
+ );
+ context.test(
+ 'The mutation observer microtask is only queued once a record is queued for an observer',
+ async ({ document, MutationObserver }) => {
+ const target = document.createElement('div');
+ const order: string[] = [];
+ const observer = new MutationObserver(() => order.push('observer'));
+ observer.observe(target, { attributes: true });
+
+ // No observer gets a record for this mutation, so it does not queue the mutation observer
+ // microtask.
+ document.createElement('div').append('text');
+ Promise.resolve().then(() => order.push('promise'));
+ // This queues a record and therefore the mutation observer microtask, after the promise job.
+ target.setAttribute('data-test', '');
+ await nextTask();
+
+ {
+ const actual = order;
+ const expected = ['promise', 'observer'];
+ expect(actual.length).toBe(expected.length);
+ for (let index = 0; index < expected.length; index++)
+ expect(actual[index]).toBe(expected[index]);
+ }
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-callback-tricky.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-callback-tricky.html.ts
new file mode 100644
index 0000000..840aa74
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-callback-tricky.html.ts
@@ -0,0 +1,56 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+
+export function register(context: CompatibilityContext): void {
+ const { expect } = context.runner;
+ context.test(
+ 'Calling observe in callback should not lead to more calls',
+ async ({ document, MutationObserver }) => {
+ let called = 0;
+ const div1 = document.createElement('div');
+ document.body.appendChild(div1);
+ const m = new MutationObserver(() => {
+ called++;
+ const div2 = document.createElement('div');
+ document.body.appendChild(div2);
+ m.observe(div2, { attributes: true });
+ });
+ m.observe(div1, { attributes: true });
+ div1.setAttribute('data-test', 'test');
+ return Promise.resolve().then(() => {
+ expect(called, 'times callback called').toBe(1);
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCertain actions within the callback does not lead to infinite loop \n\n\n\n\n',
+ },
+ },
+ );
+ context.test(
+ 'Disconnect and re-observe in callback should not lead to more calls',
+ async ({ document, MutationObserver }) => {
+ let called = 0;
+ const div1 = document.createElement('div');
+ document.body.appendChild(div1);
+ const m = new MutationObserver(() => {
+ called++;
+ m.disconnect();
+ m.observe(div1, { attributes: true });
+ });
+ m.observe(div1, { attributes: true });
+ div1.setAttribute('data-test', 'test');
+ return Promise.resolve().then(() => {
+ expect(called, 'times callback called').toBe(1);
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content:
+ '\n \nCertain actions within the callback does not lead to infinite loop \n\n\n\n\n',
+ },
+ },
+ );
+}
diff --git a/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-characterData.html.ts b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-characterData.html.ts
new file mode 100644
index 0000000..108e99d
--- /dev/null
+++ b/packages/web-platform-tests/src/wpt/dom/nodes/MutationObserver-characterData.html.ts
@@ -0,0 +1,691 @@
+import type { CompatibilityContext } from '../../../shared/context.ts';
+import { checkRecords } from './mutationobservers.js.ts';
+
+export function register(context: CompatibilityContext): void {
+ {
+ const markup2 = `
+
+MutationObservers: characterData mutations
+
+
+
+MutationObservers: characterData mutations
+
+
+
+
+text content
+
+text content
+
+CHAN
+CHANGED
+CHANGED
+
+CHGED
+CHANGED
+CHANGED
+
+CCCHANGED
+CHANGED
+
+CCCHANGED
+CHANGED
+
+
+
+
+
+CHANN
+CHANN
+
+CHANN
+CHANN
+
+CHANN
+
+
+
+
+`;
+ const { expect } = context.runner;
+ context.test(
+ 'characterData Text.data: simple mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n00 = document.getElementById('n00')!.firstChild! as CharacterData;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'characterData', oldValue: 'text content' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n00, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n00, { characterData: true, characterDataOldValue: true });
+ (function () {
+ n00.data = 'CHANGED';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ context.test(
+ 'characterData/characterDataOldValue alone Text.data: simple mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n90 = document.getElementById('n90')!.firstChild! as CharacterData;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'characterData', oldValue: 'CHANN' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n90, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n90, { characterDataOldValue: true });
+ (function () {
+ n90.data = 'CHANGED';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup2,
+ },
+ },
+ );
+ }
+ {
+ const markup = `
+
+MutationObservers: characterData mutations
+
+
+
+MutationObservers: characterData mutations
+
+
+
+
+text content
+
+text content
+
+CHAN
+CHANGED
+CHANGED
+
+CHGED
+CHANGED
+CHANGED
+
+CCCHANGED
+CHANGED
+
+CCCHANGED
+CHANGED
+
+
+
+
+
+CHANN
+CHANN
+
+CHANN
+CHANN
+
+CHANN
+
+
+
+
+`;
+ const { expect } = context.runner;
+ context.test(
+ 'characterData Text.data: simple mutation without oldValue',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n = document.getElementById('n')!.firstChild! as CharacterData;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'characterData' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n, { characterData: true });
+ (function () {
+ n.data = 'NEW VALUE';
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'characterData Text.appendData: simple mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n10 = document.getElementById('n10')!.firstChild! as CharacterData;
+
+ const mutationRecordSequence: Parameters[3] = [
+ { type: 'characterData', oldValue: 'CHAN' },
+ ];
+ await new Promise((resolve) => {
+ const observer = new MutationObserver((mrl, obs) => {
+ step(() => {
+ checkRecords(expect, n10, mrl, mutationRecordSequence);
+ })();
+ resolve();
+ });
+ cleanup(() => observer.disconnect());
+ observer.observe(n10, { characterData: true, characterDataOldValue: true });
+ (function () {
+ n10.appendData('GED');
+ })();
+ });
+ },
+ {
+ document: {
+ contentType: 'text/html',
+ content: markup,
+ },
+ },
+ );
+ context.test(
+ 'characterData Text.appendData: empty string mutation',
+ async ({ document, MutationObserver, step, cleanup }) => {
+ let n11 = document.getElementById('n11')!.firstChild! as CharacterData;
+
+ const mutationRecordSequence: Parameters