diff --git a/.changeset/wpt-suite-fidelity.md b/.changeset/wpt-suite-fidelity.md new file mode 100644 index 0000000..a4b4356 --- /dev/null +++ b/.changeset/wpt-suite-fidelity.md @@ -0,0 +1,7 @@ +--- +'@bomb.sh/web-platform-tests': patch +--- + +Align the DOM compatibility suite with upstream WPT cases at a shared revision, mirror upstream source paths for tests and helpers, and remove locally authored scenarios and unused code. Keep local harness code in `src/shared`. Expose typed document and global bindings through the test fixture. Configure global bindings alongside upstream file and case selection in `wpt.config.ts`, with includes taking precedence over exclusions instead of requiring other test groups to be enabled. Register a todo when every case is excluded. + +Complete supported upstream matrices and record the concrete fixture limitations behind remaining omissions. diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9cc1f8a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +# Preserve upstream WPT conventions. The shared harness files remain linted. +packages/web-platform-tests/src/compatibility/*/**/*.ts +packages/web-platform-tests/src/wpt/**/*.ts diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 44a3008..04f3145 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -53,4 +53,7 @@ jobs: - run: pnpm --filter @bomb.sh/web-platform-tests typecheck - run: pnpm --filter @bomb.sh/web-platform-tests exec playwright install --with-deps chromium - run: pnpm --filter @bomb.sh/web-platform-tests test:wpt-selectors - - run: pnpm --filter @bomb.sh/web-platform-tests test:browser:input + - name: Report Chromium input compatibility + # Browser conformance differences are informational, not UI build failures. + continue-on-error: true + run: pnpm --filter @bomb.sh/web-platform-tests test:browser:input diff --git a/packages/ui/src/dom.compatibility.test.ts b/packages/ui/src/dom.compatibility.test.ts index a972738..01055f5 100644 --- a/packages/ui/src/dom.compatibility.test.ts +++ b/packages/ui/src/dom.compatibility.test.ts @@ -1,11 +1,10 @@ import { describe, it, expect } from 'vitest'; import { testDOMCompatibility } from '@bomb.sh/web-platform-tests'; -import { Document, Element, Node, Text } from './dom.ts'; -import { domFeatures } from '../test/dom-features.ts'; +import { Document } from './dom.ts'; +import config from '../test/wpt.config.ts'; testDOMCompatibility({ runner: { describe, it, expect }, - features: domFeatures, + config, create: () => new Document(() => {}), - globals: { Document, Element, Node, Text }, }); diff --git a/packages/ui/test/README.md b/packages/ui/test/README.md index 50a19e0..c21c2fd 100644 --- a/packages/ui/test/README.md +++ b/packages/ui/test/README.md @@ -1,6 +1,6 @@ # UI tests -Tests live alongside the implementation in `src/`; this directory contains their shared fixtures and the DOM feature manifest. Run the UI tests from the workspace root: +Tests live alongside the implementation in `src/`; this directory contains their shared fixtures and WPT configuration. Run the UI tests from the workspace root: ```sh pnpm --filter @clack/ui test @@ -18,25 +18,24 @@ Rendering, scheduling, notifications, and lifecycle behavior belong in the UI te ## DOM compatibility -[`dom.compatibility.test.ts`](../src/dom.compatibility.test.ts) registers the shared compatibility suite with Vitest. Its synchronous factory creates a fresh instance of the real [`Document`](../src/dom.ts) for each case, with a no-op render notification callback. The supplied global bindings are the UI's `Document`, `Element`, `Node`, and `Text` classes. This fixture owns no external resources, so it needs no disposal callback. +[`dom.compatibility.test.ts`](../src/dom.compatibility.test.ts) registers the shared compatibility suite with Vitest. Its synchronous factory creates a fresh instance of the real [`Document`](../src/dom.ts) for each case, with a no-op render notification callback. This fixture owns no external resources, so it needs no disposal callback. -[`dom-features.ts`](dom-features.ts) is the source of truth for the features this integration verifies. It satisfies the suite's `DOMFeatures` type and declares every leaf explicitly. The suite also validates the manifest at runtime: omitted, unknown, and non-boolean feature values are configuration errors. +[`wpt.config.ts`](wpt.config.ts) supplies the implementation's `globals` and selects the upstream files and cases enforced by this integration. Add new global bindings there. Omitting `config` runs all adapted cases; `exclude` removes matches and explicit `include` rules take precedence. Source keys support `*` and `**`; values are `true` for a whole source or arrays of exact case names and regular expressions. Unmatched rules are configuration errors. No other test groups must be enabled to run a case. -A `true` leaf enables its registered cases; a `false` leaf leaves those cases unregistered. The suite does not infer support from available methods or turn disabled features into skipped tests. Required feature dependencies must also be enabled; missing dependencies fail configuration before any tests are registered. Individual cases with additional feature requirements run only when those requirements are enabled. +The configuration explicitly excludes unsupported directories. Add includes or narrow exclusions as cases pass. Selecting a source runs all of our adapted cases from that file; it does not imply complete coverage of the upstream file or interface. -Feature names describe the behavior being asserted. For example, `node.basicAppend` covers detached tree construction, while `node.append` also requires moving nodes and rejecting cycles. Passing a narrow feature does not establish support for the broader interface or complete standards conformance. The shared [feature catalogue](../../web-platform-tests/src/compatibility/features.ts) and [registry](../../web-platform-tests/src/compatibility/registry.ts) lead to each feature's cases, prerequisites, and specification references. +Include the source or case in the configuration while implementing it. Vitest's `-t` can narrow the configured selection by source path and case name: -When implementing a feature: +```sh +pnpm --filter @clack/ui test src/dom.compatibility.test.ts -t 'Document-createElement.html.*createElement\(foo\)' +``` -1. Read its registered cases and prerequisites to understand the behavior the flag promises. -2. Implement that behavior in the UI, then enable the relevant leaves in `dom-features.ts`. -3. Extend the compatibility fixture when the cases need additional constructors, document parsing options, host input, or resource cleanup. -4. Run the UI tests and add implementation-specific regressions for rendering, notifications, or lifecycle effects. +If the shared package is already built, avoid rebuilding by using `pnpm --filter @clack/ui exec bsh test src/dom.compatibility.test.ts` with the same name filter. -Keep support decisions in the manifest rather than duplicating flag values or test counts in documentation. +When implementing behavior, read the source-named file under [`src/wpt`](../../web-platform-tests/src/wpt), implement its operations, and update the configuration once its cases pass. Extend the fixture when cases need document parsing, host input, or cleanup. Implementation-specific rendering, notification, and lifecycle checks remain in the UI tests. ## Host input -Host input features require a [`DOMInputDriver`](../../web-platform-tests/src/compatibility/input-driver.ts) that delivers input through the normal terminal input pipeline. A terminal adapter must resolve mouse targets through the UI's layout, await operation completion, and release held input during cleanup, including after failures. Calling `dispatchEvent`, invoking `.click()`, or directly assigning control values does not exercise that host behavior. +Host input cases require a [`DOMInputDriver`](../../web-platform-tests/src/shared/input-driver.ts) that delivers input through the normal terminal input pipeline. A terminal adapter must resolve mouse targets through the UI's layout, await operation completion, and release held input during cleanup, including after failures. Calling `dispatchEvent`, invoking `.click()`, or directly assigning control values does not exercise that host behavior. -Keyboard/mouse delivery, platform interaction policies, and trusted-event support are separate feature decisions. Enable each only when the implementation and its adapter can satisfy its cases. The shared suite's [browser tooling](../../web-platform-tests/README.md#development) validates the input contract with Playwright; the UI adapter must use terminal layout and input semantics. +Select keyboard/mouse delivery, platform interaction policies, and trusted-event cases independently as the implementation and its adapter support them. The shared suite's [browser tooling](../../web-platform-tests/README.md#development) validates the input contract with Playwright; the UI adapter must use terminal layout and input semantics. diff --git a/packages/ui/test/dom-features.ts b/packages/ui/test/dom-features.ts deleted file mode 100644 index c762123..0000000 --- a/packages/ui/test/dom-features.ts +++ /dev/null @@ -1,306 +0,0 @@ -import type { DOMFeatures } from '@bomb.sh/web-platform-tests'; - -export const domFeatures = { - document: { - basicCreation: true, - creation: false, - lookup: false, - importNode: false, - xml: false, - parsing: false, - xmlSerialization: false, - }, - node: { - basicAppend: true, - relationships: false, - connectivity: false, - liveCollections: false, - textContent: false, - append: false, - insertBefore: false, - remove: false, - replace: false, - fragments: false, - variadicMutation: false, - cloning: false, - comparison: false, - normalization: false, - adoption: false, - }, - characterData: { - stringStorage: true, - data: false, - editing: false, - splitText: false, - }, - element: { - childElements: true, - attributes: false, - attributeNodes: false, - namespaces: false, - classList: false, - dataset: false, - reflection: false, - selectors: false, - }, - traversal: { - nodeIterator: false, - treeWalker: false, - }, - parsing: { - fragments: false, - namespaces: false, - serialization: false, - contextual: false, - markupMutation: false, - recovery: false, - }, - templates: { - content: false, - cloning: false, - parsing: false, - }, - events: { - data: { - custom: false, - focus: false, - keyboard: false, - mouse: false, - input: false, - }, - dispatch: { - target: false, - capture: false, - bubbling: false, - cancellation: false, - stopPropagation: false, - stopImmediatePropagation: false, - pathSnapshot: false, - reuse: false, - reentrancy: false, - }, - listeners: { - registration: false, - objects: false, - once: false, - passive: false, - abortSignal: false, - snapshots: false, - exceptionReporting: false, - }, - }, - mutationObserver: { - registration: false, - records: { - childList: false, - attributes: false, - characterData: false, - }, - filtering: false, - oldValue: false, - subtree: false, - takeRecords: false, - disconnect: false, - microtaskDelivery: false, - detached: false, - reentrancy: false, - }, - focus: { - eligibility: false, - transitions: false, - removal: false, - inert: false, - reentrancy: false, - }, - selection: { - range: { - boundaries: false, - comparison: false, - stringification: false, - cloneContents: false, - deleteContents: false, - extractContents: false, - liveBoundaries: false, - mutation: false, - }, - document: { - rangeManagement: false, - direction: false, - connectivity: false, - liveUpdates: false, - mutation: false, - changeEvents: false, - }, - }, - elements: { - progress: false, - meter: false, - option: { - attributes: false, - text: false, - selectedness: false, - }, - optgroup: false, - datalist: false, - input: { - value: false, - sanitization: false, - colorValue: false, - numericValue: false, - dateValue: false, - stepping: false, - checkedness: false, - radioGroups: false, - selection: false, - rangeText: false, - validation: false, - formAssociation: false, - labels: false, - activation: false, - cloning: false, - }, - textarea: { - value: false, - selection: false, - rangeText: false, - validation: false, - formAssociation: false, - labels: false, - cloning: false, - }, - select: { - options: false, - indexedOptions: false, - selection: false, - mutation: false, - validation: false, - cloning: false, - }, - selectedcontent: false, - form: { - controls: false, - namedControls: false, - reset: false, - validation: false, - submission: false, - formData: false, - dataEvents: false, - }, - button: { - attributes: false, - formAssociation: false, - activation: false, - }, - label: { - association: false, - activation: false, - }, - fieldset: { - controls: false, - disabled: false, - }, - legend: false, - output: { - htmlFor: false, - value: false, - reset: false, - }, - details: { - open: false, - toggleEvents: false, - exclusiveGroups: false, - }, - summary: { - activation: false, - }, - dialog: { - show: false, - close: false, - modal: false, - requestClose: false, - focus: false, - formSubmission: false, - }, - }, - customElements: { - registration: false, - construction: false, - upgrading: false, - observedAttributes: false, - connection: false, - adoption: false, - reactionOrdering: false, - reentrancy: false, - exceptionReporting: false, - formAssociation: false, - }, - shadow: { - roots: { - attachment: false, - hostEligibility: false, - connectivity: false, - cloning: false, - adoption: false, - }, - slots: { - named: false, - fallback: false, - manual: false, - slotchange: false, - observerOrdering: false, - }, - events: { - retargeting: false, - composedPath: false, - relatedTarget: false, - }, - focus: { - retargeting: false, - delegation: false, - }, - declarative: false, - serialization: false, - }, - style: { - inline: false, - parsing: false, - attributeSynchronization: false, - customProperties: false, - stylesheets: false, - computed: false, - cascade: false, - layers: false, - constructedStylesheets: false, - focusState: false, - controlSelectors: false, - relationalSelectors: false, - mutationObservation: false, - }, - input: { - keyboard: { - delivery: false, - modifiers: false, - navigation: false, - activation: false, - editing: false, - multilineEditing: false, - numberStepping: false, - focusStyles: false, - focusVisible: false, - shadow: false, - implicitSubmission: false, - trustedEvents: false, - }, - mouse: { - delivery: false, - buttons: false, - hover: false, - focus: false, - activation: false, - stateStyles: false, - focusVisible: false, - shadow: false, - trustedEvents: false, - }, - }, -} satisfies DOMFeatures; diff --git a/packages/ui/test/wpt.config.ts b/packages/ui/test/wpt.config.ts new file mode 100644 index 0000000..c967da8 --- /dev/null +++ b/packages/ui/test/wpt.config.ts @@ -0,0 +1,19 @@ +import type { WPTConfig } from '@bomb.sh/web-platform-tests'; +import { Document, Element, Node, Text } from '../src/dom.ts'; + +export default { + globals: { Document, Element, Node, Text }, + exclude: { + 'css/**': true, + 'custom-elements/**': true, + 'dom/**': true, + 'domparsing/**': true, + 'html/**': true, + 'inert/**': true, + 'input-events/**': true, + 'selection/**': true, + 'shadow-dom/**': true, + 'uievents/**': true, + 'xhr/**': true, + }, +} satisfies WPTConfig; diff --git a/packages/web-platform-tests/AGENTS.md b/packages/web-platform-tests/AGENTS.md new file mode 100644 index 0000000..9c4afa9 --- /dev/null +++ b/packages/web-platform-tests/AGENTS.md @@ -0,0 +1,71 @@ +# WPT provenance and correspondence + +This package is the long-term source of upstream WPT coverage for the UI. + +Every shared DOM conformance case must faithfully reflect an existing, inspected +upstream Web Platform Test. Do not invent cases, add local assertions to upstream +cases, extend their inputs or matrices, or infer new tests from the specification. +Independent subtleties, regressions, and implementation-specific checks belong in +tests outside this package's WPT suite, even when they test standards behavior. +A citation to a related upstream test does not make a local addition a WPT case. + +This requirement applies to all adapted cases, including excluded cases. +Tests of the runner and fixtures are infrastructure tests, separate from WPT cases. + +The suite shares one upstream revision in `WPT_REVISION`. Use that revision +for source review; do not choose a separate revision for each case or file. + +Files under `src/wpt` mirror the exact upstream path, spelling, and capitalization +with `.ts` appended. For example, `src/wpt/dom/nodes/Document-createElement.html.ts` +corresponds to `dom/nodes/Document-createElement.html`. The registry derives source +identity from that convention; do not repeat it in runtime metadata or per-case +annotations. Keep each file's cases together. Upstream helpers and data also use +their natural paths under `src/wpt`. Only test entrypoints export `register`; +support files are imported by those tests. Local harness code belongs in +`src/shared`. Keep setup used by a single source in that source file. + +Keep ordinary `context.test` and `expect` calls with readable case names. Do not +introduce per-case registration wrappers. Destructure document and global bindings +from the typed fixture; their expected types are asserted once at the fixture +boundary. Wrap callback assertions with `fixture.step` so the runner observes +failures. Shared attribution and license information belong in `test/wpt`. + +When adding or migrating a case: + +1. Read the upstream source at the suite revision. Use its path for the adapted + filename and identify any helper/data sources in comments. Run `pnpm generate` + after adding, moving, or removing source files. +2. Preserve the upstream inputs, setup, operation order, assertions, exceptions, + node identities, document ownership, and matrix rows. Separate documents, + detached trees, and retained collections are meaningful test conditions. +3. Limit adaptations to integration with the consumer's runner, fixtures, and + input driver. Comment only on non-obvious adaptations, preserving the + tested behavior without adding or removing behavioral checks. Keep the code + close enough to upstream for a side-by-side review. +4. Record omitted upstream cases and their reasons in [docs/omissions.md](docs/omissions.md). + Selecting a subset of a file does not establish coverage of the entire file. + Do not merge several upstream cases into a smaller example or drop assertions + to accommodate an incomplete implementation. +5. Keep upstream authorship and license notices. A specification URL, a directory + link, or a reference to a merely similar test is insufficient provenance. + Do not claim that an existing local case was derived from WPT + without establishing that correspondence; replace it with a verified + adaptation when necessary. +6. Verify the source mapping and registered cases, review assertion correspondence, + and run browser validation and the affected consumer tests. Matching names or + passing against a browser alone does not establish correspondence. + +Fix missing implementation behavior or explicitly exclude its cases in the consumer configuration. +Never weaken a shared case, change its fixture semantics, or silently omit it +just to make the UI implementation pass. + +Test selection uses upstream source paths and original case names. All adapted +cases run by default; explicit includes override exclusions. Do not gate a case +on another test group's selection or silently omit cases based on API detection. +Supporting DOM behavior is exercised by the selected case itself. Document +creation options, global bindings, and input drivers remain fixture contracts, +checked when the selected case uses them. + +The selector omission check is an existing example of +traceability, not a waiver for other features. The entire suite must meet this +requirement before it can be described as a fully traceable WPT adaptation. diff --git a/packages/web-platform-tests/README.md b/packages/web-platform-tests/README.md index e4f2c7c..68f1f8e 100644 --- a/packages/web-platform-tests/README.md +++ b/packages/web-platform-tests/README.md @@ -4,31 +4,60 @@ Private workspace package containing reusable DOM compatibility assertions. It c The suite includes adaptations of [Web Platform Tests (WPT)](https://github.com/web-platform-tests/wpt) covering DOM operations, state/style interactions, input, shadow DOM, forms, CSSOM, selection, traversal, and parsing. -Each adapted file records its upstream sources and original author attribution in comments or source metadata. The original WPT BSD-3-Clause notice and the html5lib parsing corpus MIT notice are retained. See [standards references and licenses](docs/references.md) for the pinned sources, attribution, and applicable licenses. +The suite uses one upstream WPT revision, recorded in [WPT_REVISION](WPT_REVISION). Browse that revision [here](https://github.com/web-platform-tests/wpt/tree/b719612f963a05ece9c89a7dd7598c1c59618874). Files under `src/wpt` mirror upstream paths with `.ts` appended; the revision applies to all files and cases. See [attribution and licenses](test/wpt/README.md). + +### Required WPT correspondence + +Each file under `src/wpt` corresponds to its upstream path with the final `.ts` removed. For example, `src/wpt/dom/nodes/Document-createElement.html.ts` adapts `dom/nodes/Document-createElement.html`. Keep upstream spelling and capitalization for tests, helpers, and data. Local harness code lives in `src/shared`; comments explain non-obvious adaptations. + +Keep ordinary `context.test` and `expect` calls and readable case names. Preserve upstream inputs, setup, operation order, assertions, exceptions, node identities, and relationships between documents. A file's presence identifies the selected adaptations, not coverage of every upstream case. [Omissions](docs/omissions.md) records skipped upstream cases and why. + +The WPT suite must contain only faithful adaptations of existing upstream cases. Do not invent scenarios, add local assertions, extend upstream input matrices, or create specification-derived cases here. Independently authored subtleties, regressions, and implementation-specific checks belong in separate tests outside this suite. Necessary harness adaptations must preserve the upstream behavior being tested. Tests of the harness itself are separate infrastructure tests. + +The shared suite is the long-term home of upstream WPT coverage for the UI. Implementation-specific rendering, notification, and lifecycle checks belong in the corresponding UI integration tests. ```ts -import { testDOMCompatibility, type DOMFeatures } from '@bomb.sh/web-platform-tests'; +import { testDOMCompatibility } from '@bomb.sh/web-platform-tests'; import { describe, it, expect } from 'vitest'; -import { features, createDocument, globals, dispose } from './implementation-fixture.ts'; +import { createDocument, dispose } from './implementation-fixture.ts'; +import config from './wpt.config.ts'; testDOMCompatibility({ runner: { describe, it, expect }, - features, + config, create: createDocument, - globals, dispose, // input: document => createHostInputSession(document), }); ``` -### Selecting features to verify +### Selecting tests + +The `config` option is optional; omitting it runs all adapted cases. A `WPTConfig` supplies source-path selectors in `include` and `exclude`; an explicit include always wins. Each value is `true` for the entire source file or an array of exact case names and regular expressions matching case names. + +When selection excludes every case, the harness registers a todo through `runner.it.todo` to start implementing DOM features and enable their WPT cases. + +```ts +import type { WPTConfig } from '@bomb.sh/web-platform-tests'; +import { globals } from './implementation-fixture.ts'; + +export default { + globals, + exclude: { 'dom/nodes/Node-insertBefore.html': true }, + include: { 'dom/nodes/Node-insertBefore.html': [/reference child/] }, +} satisfies WPTConfig; +``` + +Source paths support `*` within a path segment and `**` across directories; `**/` also matches zero directories. Other characters are literal. Case-name strings match exactly. Empty, invalid, or unmatched selectors fail configuration before any cases register, so stale exclusions remain visible. -Supply a runner with `describe`, `it`, and Vitest-compatible `expect` matchers, plus a set of `features` conforming to the `DOMFeatures` interface. Every feature must be explicitly declared supported or unsupported. The suite registers cases for enabled features and checks their prerequisites before registration. +Cases have no test-to-test prerequisite gates. A selected case exercises its supporting DOM operations and reports failures normally. Globals and host input are resolved lazily when the case uses them. The runner can narrow selected cases further with its usual name filter. + +Run `pnpm generate` after adding, moving, or removing source files. The build also regenerates the registry from filenames. Only files exporting `register` are test entrypoints; upstream support files are imported by those tests. `node scripts/generate-registry.mjs --check` verifies that the checked-in registry is current. ### Harness -* `create(options?)`: A synchronous or asynchronous factory for a fresh `Document`. When a case supplies `DOMDocumentOptions`, honor its markup, MIME type, and URL fragment; the selector matrix uses these to test HTML, XHTML, and `:target`. -* `globals`: An object containing the implementation's global bindings, or a function that returns them for a document. For example, a test looks up `HTMLInputElement` on this object. +* `create(options?)`: A synchronous or asynchronous factory for a fresh `Document`. Cases using the default HTML document expect its doctype, `html`, `head`, and `body` tree. When a case supplies `DOMDocumentOptions`, honor its markup, MIME type, and URL fragment; the selector matrix uses these to test HTML, XHTML, and `:target`. +* `config.globals`: An object containing the implementation's global bindings, or a function that returns them for each fresh document. Tests destructure typed bindings from the fixture, such as `({ document, Node, Text })`. Omitting globals supplies no bindings; accessing a missing binding throws. * `input`: An optional factory for a `DOMInputDriver` session that delivers keyboard, text, and mouse input through the implementation's normal host input pipeline. This allows testing of default actions like focus on click, form submission on `Enter`, and trusted events. * `dispose(document)`: Optional cleanup for implementation-owned resources after each case, including failures. @@ -48,30 +77,20 @@ pnpm --filter @bomb.sh/web-platform-tests test:browser ``` To validate the test suite itself, run it against an actual browser runtime. Browser dependencies are development-only. -The automation CLI uses Playwright's Chromium with an isolated browser context and fresh standards-mode iframe per case. +The automation CLI uses Playwright's Chromium with a fresh iframe per case and a new browser context every 500 cases to bound memory use. -The [selector WPT omission check](docs/wpt-selectors.md) compares the adapted registrations with actual pinned upstream case names. Its cache is ignored, and results are printed rather than committed. +The [selector audit](scripts/check-wpt-selectors.mjs), run by `test:wpt-selectors` and CI, checks registered case names, matrix data, API masks, and fixture bytes against the pinned upstream sources. It covers selector API files in `dom/nodes` plus `is-where-basic.html` and `has-basic.html`; the script records exclusions. Downloads are cached in ignored `node_modules/.cache`, and reports go to stdout. After changing selector adaptations or the suite revision, run this audit and browser validation, and review the assertions against upstream. The suite asserts resulting DOM behavior, never browser pixel values. Browser target resolution uses the browser's own rectangles and iframe coordinate offsets; a terminal adapter must use its own renderer. -#### Known Chromium disagreements - -The following eight cases are allowed to fail when validating the suite in Chromium. They remain enabled, and the runner reports their failures and returns a nonzero exit code. Other failures require investigation. +#### Browser results -| Feature and case | Chromium behavior | Retained expectation/reference | -| --- | --- | --- | -| [`document.xmlSerialization`](src/compatibility/document/xml-serialization.ts): omits a redundant empty default namespace declaration | Retains the child's `xmlns=""` when the inherited namespace is already empty. | [XML serialization](https://w3c.github.io/DOM-Parsing/#xml-serialization-algorithm) omits the redundant declaration; [pinned WPT](https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/domparsing/XMLSerializer-serializeToString.html). | -| [`document.xmlSerialization`](src/compatibility/document/xml-serialization.ts): omits a redundant nonempty default namespace declaration | Retains the child's declaration when it repeats the inherited namespace. | Same XML serialization rule and pinned WPT. | -| [`document.xmlSerialization`](src/compatibility/document/xml-serialization.ts): emits attribute nodes as empty strings even when their values are nonempty | Serializes the attribute's escaped value. | The [Attr serialization branch](https://w3c.github.io/DOM-Parsing/#xml-serializing-a-node) returns an empty string. The local case extends WPT's empty-valued Attr with a nonempty value. | -| [`focus.removal`](src/compatibility/focus/removal.ts): removing a focused node clears focus without firing blur | Fires `blur` during removal. | [HTML removing steps](https://html.spec.whatwg.org/multipage/infrastructure.html#html-element-removing-steps). | -| [`elements.input.radioGroups`](src/compatibility/elements/input/radio-groups.ts): groups radios by tree and nonempty name | The previous group member stays checked after a checked detached radio changes name. | [Radio group updates](https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio)); [WPT disconnected groups](https://github.com/web-platform-tests/wpt/blob/master/html/semantics/forms/the-input-element/radio-disconnected-group-owner.html). | -| [`elements.input.labels`](src/compatibility/elements/input/labels.ts): keeps retained labels live when a detached control moves under a label | Retained labels stay empty. | [Live labels in the same tree](https://html.spec.whatwg.org/multipage/forms.html#dom-lfe-labels). | -| [`elements.textarea.labels`](src/compatibility/elements/textarea/labels.ts): keeps retained labels live when a detached control moves under a label | Retained labels stay empty. | Same live-label rule. | -| [`element.selectors`](src/compatibility/element/selectors/grammar.ts): WPT `querySelector-mixed-case.html`, mixed HTML/SVG/MathML tree with various mixed-case attributes | `[viewbox]` matches four elements where WPT expects two HTML elements. | The [pinned upstream case](https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/dom/nodes/querySelector-mixed-case.html) produces the same failure in Chromium; its assertions remain unchanged. | +Browser validation keeps upstream expectations intact. Failures remain enabled +and produce a nonzero exit code. ### Coverage boundaries -The [feature catalogue](src/compatibility/features.ts) and [registry](src/compatibility/registry.ts) describe the executable coverage. Harness tests check that every feature has a module, registered cases, and specification references. The [UI feature manifest](../ui/test/dom-features.ts) declares its implementation's supported features; the [UI test README](../ui/test/README.md) explains the integration and how to update those decisions. [WPT references and licenses](docs/references.md) identify the selected upstream adaptations. Passing selected cases does not imply complete standards conformance. +The [adapted sources](src/wpt) and generated [registry](src/shared/registry.ts) describe the executable coverage. Harness tests check source identity, case selection, and reachability of every source module. The [UI configuration](../ui/test/wpt.config.ts) records the cases enforced by that integration; the [UI test README](../ui/test/README.md) explains how to select additional cases while implementing them. Passing selected cases does not imply complete standards conformance. Remaining test gaps include: diff --git a/packages/web-platform-tests/WPT_REVISION b/packages/web-platform-tests/WPT_REVISION new file mode 100644 index 0000000..53e0a11 --- /dev/null +++ b/packages/web-platform-tests/WPT_REVISION @@ -0,0 +1 @@ +b719612f963a05ece9c89a7dd7598c1c59618874 diff --git a/packages/web-platform-tests/docs/omissions.md b/packages/web-platform-tests/docs/omissions.md new file mode 100644 index 0000000..e2694aa --- /dev/null +++ b/packages/web-platform-tests/docs/omissions.md @@ -0,0 +1,43 @@ +# WPT omissions + +Known omissions at [WPT_REVISION](../WPT_REVISION). Source paths are relative to +the upstream WPT root. Consumer exclusions in `wpt.config.ts` are separate. + +## DOM and selectors + +| Upstream source | Omitted cases | Reason | +| --- | --- | --- | +| `dom/nodes/Element-webkitMatchesSelector.html` | Entire file. | Vendor-prefixed alias; coverage targets standard `matches`. | +| `dom/nodes/Element-matches-namespaced-elements.html` | The `webkitMatchesSelector` rows. | Vendor-prefixed alias. | +| `dom/nodes/ParentNode-querySelector-All.html`, `dom/nodes/ParentNode-querySelector-All-xht.xht`, `dom/nodes/Element-matches.html` | Unnamed iframe-loader cases. | The consumer's document factory replaces upstream fixture loading; behavioral subtests remain. | +| `domparsing/createContextualFragment.html` | `', - 'text/html', - ); - expect(parsed).not.toBe(document); - expect(parsed.title).toBe('A & B'); - expect(parsed.body.querySelector('p')!.textContent).toBe('x'); - expect(parsed.contentType).toBe('text/html'); - }, - ); - context.test('parses XML with namespaces and preserves element name case', (fixture) => { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const parsed = new DOMParser().parseFromString( - '', - 'application/xml', - ); - expect(parsed.documentElement.localName).toBe('Root'); - expect(parsed.documentElement.namespaceURI).toBe('urn:p'); - expect(parsed.documentElement.firstElementChild!.localName).toBe('Child'); - expect(parsed.querySelector('parsererror')).toBeNull(); - }); - context.test('returns an XML error document for malformed input', (fixture) => { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const parsed = new DOMParser().parseFromString('', 'application/xml'); - expect(parsed.getElementsByTagName('parsererror').length).toBeGreaterThan(0); - }); - context.test('serializes XML namespaces and reparses equivalent trees', (fixture) => { - const document = fixture.document as unknown as Document; - const XMLSerializer = fixture.globals('XMLSerializer') as typeof globalThis.XMLSerializer, - DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const xml = document.implementation.createDocument('urn:p', 'p:Root'); - xml.documentElement.setAttribute('title', 'a&<'); - const text = new XMLSerializer().serializeToString(xml); - expect(text).toContain('xmlns:p="urn:p"'); - expect(text).toContain('title="a&<"'); - const parsed = new DOMParser().parseFromString(text, 'application/xml'); - expect(parsed.documentElement.namespaceURI).toBe('urn:p'); - expect(parsed.documentElement.getAttribute('title')).toBe('a&<'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/document/xml-serialization.ts b/packages/web-platform-tests/src/compatibility/document/xml-serialization.ts deleted file mode 100644 index 294bb73..0000000 --- a/packages/web-platform-tests/src/compatibility/document/xml-serialization.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { CompatibilityContext, DOMFixture } from '../context.ts'; - -export const spec = 'https://w3c.github.io/DOM-Parsing/#dfn-xml-serialization'; -export const requires = ['document.xml', 'document.parsing'] as const; -const requiredGlobals = ['DOMParser', 'XMLSerializer'] as const; -export { requiredGlobals as globals }; -// Adapted from WPT domparsing/XMLSerializer-serializeToString.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/domparsing/XMLSerializer-serializeToString.html'; - -const xmlns = 'http://www.w3.org/2000/xmlns/'; -function setup(fixture: DOMFixture): { - parse(source: string): Element; - serialize(node: Node): string; -} { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const XMLSerializer = fixture.globals('XMLSerializer') as typeof globalThis.XMLSerializer; - const parser = new DOMParser(), - serializer = new XMLSerializer(); - return { - parse: (source: string) => parser.parseFromString(source, 'application/xml').documentElement, - serialize: (node: Node) => serializer.serializeToString(node), - }; -} - -/** Registers XML namespace fixup, escaping, and serialization identity cases. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'resets the default namespace when a no-namespace child is moved below it', - (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse('value'); - const child = root.firstChild!; - const parent = root.ownerDocument.createElementNS('urn:outer', 'parent'); - root.replaceChild(parent, child); - parent.appendChild(child); - expect(serialize(root)).toBe( - 'value', - ); - expect((child as Element).hasAttribute('xmlns')).toBe(false); - expect(child.parentNode).toBe(parent); - }, - ); - for (const { name, source, expected } of [ - { - name: 'omits a redundant empty default namespace declaration', - source: '', - expected: '', - }, - { - name: 'omits a redundant nonempty default namespace declaration', - source: '', - expected: '', - }, - { - name: 'preserves a necessary default namespace reset without repeating it on descendants', - source: '', - expected: '', - }, - ]) { - context.test(name, (fixture) => { - const { parse, serialize } = setup(fixture); - expect(serialize(parse(source))).toBe(expected); - }); - } - context.test( - 'uses the nearest declaration for an attribute with a namespace but no prefix', - (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse(''); - const child = root.firstElementChild!; - child.setAttributeNS('urn:a', 'name', 'value'); - expect(serialize(root)).toBe( - '', - ); - expect(child.getAttributeNodeNS('urn:a', 'name')!.prefix).toBeNull(); - }, - ); - context.test('reuses an existing attribute prefix for the same namespace', (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse(''); - root.firstElementChild!.setAttributeNS('urn:a', 'different:name', 'value'); - expect(serialize(root)).toBe( - '', - ); - expect(root.firstElementChild!.getAttributeNodeNS('urn:a', 'name')!.prefix).toBe('different'); - }); - context.test( - 'generates a prefix when an attribute prefix conflicts with an inherited declaration', - (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse(''); - root.firstElementChild!.setAttributeNS('urn:second', 'p:name', 'value'); - expect(serialize(root)).toBe( - '', - ); - const reparsed = parse(serialize(root)); - expect(reparsed.firstElementChild!.getAttributeNS('urn:second', 'name')).toBe('value'); - }, - ); - context.test( - 'accounts for conflicting xmlns attributes on the element being serialized', - (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse(''); - root.setAttributeNS('urn:first', 'p:name', 'value'); - root.setAttributeNS(xmlns, 'xmlns:p', 'urn:second'); - expect(serialize(root)).toBe( - '', - ); - expect(root.getAttributeNodeNS('urn:first', 'name')!.prefix).toBe('p'); - }, - ); - context.test('fixes element prefixes consistently in opening and closing tags', (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse(''); - const node = root.ownerDocument.createElementNS('urn:a', 'nested'); - node.appendChild(root.ownerDocument.createElementNS('urn:a', 'inner')); - root.firstElementChild!.appendChild(node); - expect(serialize(root)).toBe( - '', - ); - }); - context.test( - 'serializes document fragments and preserves children of HTML void elements in XML', - (fixture) => { - const document = fixture.document as unknown as Document; - const { serialize } = setup(fixture); - const fragment = document.createDocumentFragment(); - const image = document.createElement('img'); - image.append(document.createElement('style')); - fragment.append(image, document.createElement('span')); - expect(serialize(fragment)).toBe( - '', - ); - expect(fragment.firstChild).toBe(image); - expect(fragment.childNodes.length).toBe(2); - }, - ); - context.test( - 'escapes XML attribute characters and round trips their original values', - (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse(''); - root.setAttribute('value', '<>&"\''); - expect(serialize(root)).toBe(''); - expect(parse(serialize(root)).getAttribute('value')).toBe('<>&"\''); - }, - ); - context.test( - 'emits attribute nodes as empty strings even when their values are nonempty', - (fixture) => { - const { parse, serialize } = setup(fixture); - const root = parse(''); - root.setAttribute('value', '<>&"\''); - expect(serialize(root.getAttributeNode('value')!)).toBe(''); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/document/xml.ts b/packages/web-platform-tests/src/compatibility/document/xml.ts deleted file mode 100644 index 018f8e5..0000000 --- a/packages/web-platform-tests/src/compatibility/document/xml.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://dom.spec.whatwg.org/#interface-domimplementation'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('creates XML documents with namespaced roots and doctypes', (fixture) => { - const document = fixture.document as unknown as Document; - const impl = document.implementation; - const dt = impl.createDocumentType('p:root', 'public', 'system'); - const xml = impl.createDocument('urn:test', 'p:root', dt); - expect(xml.contentType).toBe('application/xml'); - expect(xml.documentElement.namespaceURI).toBe('urn:test'); - expect(xml.documentElement.prefix).toBe('p'); - expect(xml.doctype).toBe(dt); - expect(dt.ownerDocument).toBe(xml); - expect(dt.publicId).toBe('public'); - expect(xml.createElement('MiXeD').tagName).toBe('MiXeD'); - }); - context.test('validates qualified names and namespace constraints', (fixture) => { - const document = fixture.document as unknown as Document; - for (const operation of [ - () => document.createElement('a b'), - () => document.createAttribute('a b'), - ]) - expect(operation).toThrow(expect.objectContaining({ name: 'InvalidCharacterError' })); - expect(() => document.createElementNS(null, 'p:x')).toThrow( - expect.objectContaining({ name: 'NamespaceError' }), - ); - expect(() => document.createElementNS('urn:x', 'xml:x')).toThrow( - expect.objectContaining({ name: 'NamespaceError' }), - ); - }); - context.test('creates independent HTML skeletons with title text', (fixture) => { - const document = fixture.document as unknown as Document; - const other = document.implementation.createHTMLDocument(''); - expect(other).not.toBe(document); - expect(other.doctype!.name).toBe('html'); - expect(other.head).not.toBeNull(); - expect(other.body).not.toBeNull(); - expect(other.title).toBe('<title>'); - expect(other.head.querySelector('title')!.textContent).toBe('<title>'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/attribute-nodes.ts b/packages/web-platform-tests/src/compatibility/element/attribute-nodes.ts deleted file mode 100644 index f8413f1..0000000 --- a/packages/web-platform-tests/src/compatibility/element/attribute-nodes.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#interface-attr'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'moves ownership only after validating attribute node attachment', - ({ document }) => { - const first = document.createElement('div'); - const second = document.createElement('div'); - const attr = document.createAttribute('title'); - attr.value = 'one'; - first.setAttributeNode(attr); - expect(() => second.setAttributeNode(attr)).toThrow( - expect.objectContaining({ name: 'InUseAttributeError' }), - ); - expect(attr.ownerElement).toBe(first); - expect(second.attributes.length).toBe(0); - const replacement = document.createAttribute('title'); - replacement.value = 'two'; - expect(first.setAttributeNode(replacement)).toBe(attr); - expect(attr.ownerElement).toBeNull(); - expect(replacement.ownerElement).toBe(first); - expect(first.removeAttributeNode(replacement)).toBe(replacement); - expect(replacement.ownerElement).toBeNull(); - }, - ); - - context.test('retains Attr identity on value changes and detaches replaced Attr', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - e.setAttribute('title', 'a'); - const a = e.getAttributeNode('title')!; - e.setAttribute('title', 'b'); - expect(e.getAttributeNode('title')).toBe(a); - expect(a.value).toBe('b'); - const replacement = document.createAttribute('title'); - replacement.value = 'c'; - expect(e.setAttributeNode(replacement)).toBe(a); - expect(a.ownerElement).toBeNull(); - expect(replacement.ownerElement).toBe(e); - }); - - context.test('rejects attributes in use and removing absent attributes', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'), - other = document.createElement('div'), - a = document.createAttribute('x'); - e.setAttributeNode(a); - expect(() => other.setAttributeNode(a)).toThrow( - expect.objectContaining({ name: 'InUseAttributeError' }), - ); - expect(() => other.removeAttributeNode(a)).toThrow( - expect.objectContaining({ name: 'NotFoundError' }), - ); - expect(a.ownerElement).toBe(e); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/attributes.ts b/packages/web-platform-tests/src/compatibility/element/attributes.ts deleted file mode 100644 index 2d984aa..0000000 --- a/packages/web-platform-tests/src/compatibility/element/attributes.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#interface-element'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'updates attributes coherently with presence and string coercion', - ({ document }) => { - const element = document.createElement('div'); - expect(element.getAttribute('title')).toBeNull(); - element.setAttribute('title', 'one'); - const attr = element.getAttributeNode('title'); - element.setAttribute('title', 'two'); - expect(element.getAttributeNode('title')).toBe(attr); - expect(attr?.value).toBe('two'); - const node = document.createAttribute('count'); - // @ts-expect-error -- Verify the standard DOMString conversion at runtime. - node.value = 42; - element.setAttributeNode(node); - expect(element.getAttribute('count')).toBe('42'); - expect(element.hasAttributes()).toBe(true); - element.toggleAttribute('hidden', true); - expect(element.hasAttribute('hidden')).toBe(true); - element.toggleAttribute('hidden', false); - expect(element.hasAttribute('hidden')).toBe(false); - element.removeAttribute('title'); - expect(attr?.ownerElement).toBeNull(); - }, - ); - - context.test( - 'distinguishes absent attributes from empty values and preserves order', - (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - expect(e.getAttribute('title')).toBeNull(); - e.setAttribute('A', '1'); - e.setAttribute('b', '2'); - e.setAttribute('a', '3'); - expect(e.getAttributeNames()).toEqual(['a', 'b']); - expect(e.getAttribute('A')).toBe('3'); - e.removeAttribute('a'); - e.setAttribute('a', ''); - expect(e.getAttributeNames()).toEqual(['b', 'a']); - expect(e.hasAttribute('a')).toBe(true); - }, - ); - - context.test('toggles presence with force and validates names', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - expect(e.toggleAttribute('x')).toBe(true); - expect(e.getAttribute('x')).toBe(''); - expect(e.toggleAttribute('x', true)).toBe(true); - expect(e.toggleAttribute('x', false)).toBe(false); - expect(e.toggleAttribute('x', false)).toBe(false); - expect(() => e.setAttribute('bad name', 'x')).toThrow( - expect.objectContaining({ name: 'InvalidCharacterError' }), - ); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/child-elements.ts b/packages/web-platform-tests/src/compatibility/element/child-elements.ts deleted file mode 100644 index 8c2c5cd..0000000 --- a/packages/web-platform-tests/src/compatibility/element/child-elements.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://dom.spec.whatwg.org/#dom-parentnode-children'; -export const requires = ['node.basicAppend'] as const; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('filters text nodes from children in tree order', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('span'), - b = document.createElement('b'); - expect(p.children.length).toBe(0); - p.appendChild(document.createTextNode('before')); - p.appendChild(a); - p.appendChild(document.createTextNode('between')); - p.appendChild(b); - expect(Array.from(p.children)).toEqual([a, b]); - }); - context.test('includes only immediate element children', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('span'), - b = document.createElement('b'); - a.appendChild(b); - p.appendChild(a); - expect(Array.from(p.children)).toEqual([a]); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/class-list.ts b/packages/web-platform-tests/src/compatibility/element/class-list.ts deleted file mode 100644 index 2980a70..0000000 --- a/packages/web-platform-tests/src/compatibility/element/class-list.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#interface-domtokenlist'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'maintains an ordered unique token set reflected in the class attribute', - ({ document }) => { - const element = document.createElement('div'); - element.className = 'one two one'; - const list = element.classList; - expect(Array.from(list)).toEqual(['one', 'two']); - list.add('three', 'two'); - expect(element.getAttribute('class')).toBe('one two three'); - list.replace('one', 'three'); - expect(Array.from(list)).toEqual(['three', 'two']); - expect(list.toggle('two', false)).toBe(false); - expect(list.contains('two')).toBe(false); - element.setAttribute('class', 'fresh'); - expect(list.item(0)).toBe('fresh'); - expect(() => list.add('bad token')).toThrow( - expect.objectContaining({ name: 'InvalidCharacterError' }), - ); - }, - ); - - context.test('validates every token before multi-token mutation', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - e.className = 'a'; - for (const [token, name] of [ - ['', 'SyntaxError'], - ['a b', 'InvalidCharacterError'], - ]) { - expect(() => e.classList.add('b', token!)).toThrow(expect.objectContaining({ name })); - expect(e.className).toBe('a'); - } - expect(e.classList.item(99)).toBeNull(); - }); - - context.test('preserves ordered uniqueness and replace semantics', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - e.className = 'a b a'; - const list = e.classList; - expect(Array.from(list)).toEqual(['a', 'b']); - expect(list.replace('missing', 'c')).toBe(false); - expect(list.replace('a', 'b')).toBe(true); - expect(Array.from(list)).toEqual(['b']); - expect(list.toggle('b', true)).toBe(true); - expect(list.toggle('b', false)).toBe(false); - expect(list.length).toBe(0); - expect(e.classList).toBe(list); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/dataset.ts b/packages/web-platform-tests/src/compatibility/element/dataset.ts deleted file mode 100644 index a58a4e9..0000000 --- a/packages/web-platform-tests/src/compatibility/element/dataset.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://html.spec.whatwg.org/multipage/dom.html#dom-dataset'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('maps names and synchronizes attributes in both directions', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const data = e.dataset; - e.setAttribute('data-long-name', 'one'); - expect(data.longName).toBe('one'); - data.longName = 'two'; - expect(e.getAttribute('data-long-name')).toBe('two'); - delete data.longName; - expect(e.hasAttribute('data-long-name')).toBe(false); - expect(e.dataset).toBe(data); - }); - context.test('coerces values and rejects ambiguous dashed property names', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - Reflect.set(e.dataset, 'x', null); - expect(e.dataset.x).toBe('null'); - expect(() => { - e.dataset['bad-name'] = 'x'; - }).toThrow(expect.objectContaining({ name: 'SyntaxError' })); - e.setAttribute('data-a-1', 'n'); - expect(e.dataset['a-1']).toBe('n'); - expect(Object.keys(e.dataset)).toEqual(['x', 'a-1']); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/namespaces.ts b/packages/web-platform-tests/src/compatibility/element/namespaces.ts deleted file mode 100644 index c8de3b1..0000000 --- a/packages/web-platform-tests/src/compatibility/element/namespaces.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#namespaces'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('preserves namespace local names prefixes and attribute lookup', ({ document }) => { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg:linearGradient'); - expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg'); - expect(svg.prefix).toBe('svg'); - expect(svg.localName).toBe('linearGradient'); - svg.setAttributeNS('urn:test', 'p:name', 'value'); - expect(svg.getAttributeNS('urn:test', 'name')).toBe('value'); - const attr = svg.getAttributeNodeNS('urn:test', 'name'); - expect(attr?.prefix).toBe('p'); - expect(attr?.localName).toBe('name'); - expect(() => document.createElementNS(null, 'p:name')).toThrow( - expect.objectContaining({ name: 'NamespaceError' }), - ); - }); - - context.test('distinguishes equal local attribute names in different namespaces', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElementNS('urn:root', 'p:root'); - e.setAttributeNS('urn:a', 'a:id', 'first'); - e.setAttributeNS('urn:b', 'b:id', 'second'); - expect(e.getAttributeNS('urn:a', 'id')).toBe('first'); - expect(e.getAttributeNS('urn:b', 'id')).toBe('second'); - e.removeAttributeNS('urn:a', 'id'); - expect(e.hasAttributeNS('urn:a', 'id')).toBe(false); - expect(e.getAttributeNS('urn:b', 'id')).toBe('second'); - }); - - context.test('resolves inherited namespace declarations', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElementNS('urn:root', 'p:root'), - child = document.createElementNS('urn:child', 'c:child'); - e.appendChild(child); - expect(child.lookupNamespaceURI('p')).toBe('urn:root'); - expect(e.lookupPrefix('urn:root')).toBe('p'); - expect(e.isDefaultNamespace('urn:root')).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/reflection.ts b/packages/web-platform-tests/src/compatibility/element/reflection.ts deleted file mode 100644 index 5c58ddd..0000000 --- a/packages/web-platform-tests/src/compatibility/element/reflection.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflect'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('reflects string properties and attribute removal', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - expect(e.id).toBe(''); - expect(e.title).toBe(''); - e.id = 'a'; - expect(e.getAttribute('id')).toBe('a'); - e.setAttribute('title', 'hello'); - expect(e.title).toBe('hello'); - e.removeAttribute('title'); - expect(e.title).toBe(''); - e.lang = 'en'; - expect(e.getAttribute('lang')).toBe('en'); - }); - context.test('reflects boolean presence independently of attribute spelling', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - expect(e.hidden).toBe(false); - e.setAttribute('hidden', 'false'); - expect(e.hidden).toBe(true); - e.hidden = false; - expect(e.hasAttribute('hidden')).toBe(false); - e.hidden = true; - expect(e.getAttribute('hidden')).toBe(''); - }); - context.test('reflects tabIndex integer defaults and invalid input', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - expect(e.tabIndex).toBe(-1); - e.tabIndex = 3; - expect(e.getAttribute('tabindex')).toBe('3'); - e.setAttribute('tabindex', 'bad'); - expect(e.tabIndex).toBe(-1); - expect(document.createElement('button').tabIndex).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/element/selectors.ts b/packages/web-platform-tests/src/compatibility/element/selectors.ts deleted file mode 100644 index 9ed8649..0000000 --- a/packages/web-platform-tests/src/compatibility/element/selectors.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -import { register as registerGrammar } from './selectors/grammar.ts'; -import { register as registerMatching } from './selectors/matching.ts'; -import { register as registerNamespaces } from './selectors/namespaces.ts'; -import { register as registerRoots } from './selectors/roots.ts'; -import { register as registerMatrix } from './selectors/matrix.ts'; -export const spec = 'https://dom.spec.whatwg.org/#dom-parentnode-queryselector'; -export const requires = ['parsing.fragments', 'element.attributes'] as const; -export const globals = ['DOMException', 'NodeList', 'TypeError'] as const; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('returns tree-ordered static snapshots and first match', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'); - p.innerHTML = '<i class=x></i><b class=x></b>'; - const found = p.querySelectorAll('.x'); - expect(found.length).toBe(2); - expect(found[0]).toBe(p.firstChild); - expect(found[1]).toBe(p.lastChild); - expect(p.querySelector('.x')).toBe(found[0]); - p.appendChild(document.createElement('i')).className = 'x'; - expect(found.length).toBe(2); - expect(p.querySelectorAll('.x').length).toBe(3); - expect(p.querySelector('.missing')).toBeNull(); - }); - context.test('handles combinators, attributes, scope, matches and closest', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'); - p.innerHTML = '<section><i data-x="A" class=x></i><b></b></section>'; - const i = p.querySelector('i')!; - expect(i.matches('section > i[data-x="A"]:first-child')).toBe(true); - expect(i.closest('section')).toBe(p.firstChild); - expect(i.closest('.x')).toBe(i); - expect(p.querySelector(':scope > section > i + b')).toBe(i.nextSibling); - expect(i.closest('aside')).toBeNull(); - }); - context.test('rejects invalid selectors consistently', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'); - for (const operation of [ - () => p.querySelector('['), - () => p.querySelectorAll('['), - () => p.matches('['), - () => p.closest('['), - ]) - expect(operation).toThrow(expect.objectContaining({ name: 'SyntaxError' })); - }); - registerMatrix(context); - registerRoots(context); - registerMatching(context); - registerNamespaces(context); - registerGrammar(context); -} diff --git a/packages/web-platform-tests/src/compatibility/element/selectors/fixtures.ts b/packages/web-platform-tests/src/compatibility/element/selectors/fixtures.ts deleted file mode 100644 index 8196500..0000000 --- a/packages/web-platform-tests/src/compatibility/element/selectors/fixtures.ts +++ /dev/null @@ -1,754 +0,0 @@ -// WPT dom/nodes/ParentNode-querySelector-All-content.{html,xht} at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright © web-platform-tests contributors. BSD-3-Clause; see ../../../../test/wpt/LICENSE. -// Unmodified upstream fixture markup. Each case receives a fresh document. -export const html = `<!DOCTYPE html> -<html id="html" lang="en"> -<head id="head"> - <meta id="meta" charset="UTF-8"> - <title id="title">Selectors-API Test Suite: HTML with Selectors Level 2 using TestHarness: Test Document - - - - - - - - -
-
- -
-

Universal selector tests inside element with id="universal".

-
-
Some preformatted text with some embedded code
-

This is a normal link: W3C

-
Some more nested elements code hyperlink
-
- -
-
-
-
-
-

-

-    
-
    - - - - -
    - -
    -
    -
    -
    -
    - -
    - - - - - - - - - -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - - - - - - - - - -

    -
    - -
    -
    -
    -
    -
    -
    - -
    - - - - -
    -
    -
    -
    -
    - -

    -
    - -
    - - - - -
    -
    -
    -
    - -

    -
    - -
    - - - - -
    -
    -
    -
    -
    -
    - -

    -
    - -
    - - - - -
    - -
      -
    1. -
    2. -
    3. -
    4. -
    5. -
    6. -
    7. -
    8. -
    9. -
    10. -
    11. -
    12. -
    - -

    - span1 - em1 - - em2 - span2 - strong1 - em3 - span3 - span4 - strong2 - em4 -

    -
    - -
    -
    -
    -
    - -

    -

    -

    -
    - -
    -

    -

    -

    - -
    -
    -
    -
    - -
    -

    - -

    -

    - - -

    -

    - - - -

    -
    > - -
    -

    -

    -

    -

    Text node

    -

    -
    - - - -
    -
    -
    -
    -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    -
    -
    -
    - -

    -

    -

    -
    - -
    All pseudo-element tests
    - -
    -

    -

    -

    - - -
    -
    -

    -
    -

    -
    -
    -
    -
    - - - - - - -
    - -
    -
    -
    - -
      -
    • -
    • -
    • -
    • -
    - - - - - - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -

    -
    -
    -
    -

    -

    -
    - -
    -
    -
    -
    -
    -
    -

    -
    -
    -
    -

    -

    -
    - -
    - - -
    -
    - - -`; -export const xhtml = ` - - - Selectors-API Test Suite: HTML with Selectors Level 2 using TestHarness: Test Document - - - - - - - -
    -
    - -
    -

    Universal selector tests inside element with id="universal".

    -
    -
    Some preformatted text with some embedded code
    -

    This is a normal link: W3C

    -
    Some more nested elements code hyperlink
    -
    - -
    -
    -
    -
    -
    -

    -
    
    -    
    -
      - - - - -
      - -
      -
      -
      -
      -
      - -
      - - - - - - - - - -
      - -
      -
      - -
      -
      -
      -
      - -
      -
      - - - - - - - - - -

      -
      - -
      -
      -
      -
      -
      -
      - -
      - - - - -
      -
      -
      -
      -
      - -

      -
      - -
      - - - - -
      -
      -
      -
      - -

      -
      - -
      - - - - -
      -
      -
      -
      -
      -
      - -

      -
      - -
      - - - - -
      - -
        -
      1. -
      2. -
      3. -
      4. -
      5. -
      6. -
      7. -
      8. -
      9. -
      10. -
      11. -
      12. -
      - -

      - span1 - em1 - - em2 - span2 - strong1 - em3 - span3 - span4 - strong2 - em4 -

      -
      - -
      -
      -
      -
      - -

      -

      -

      -
      - -
      -

      -

      -

      - -
      -
      -
      -
      - -
      -

      - -

      -

      - - -

      -

      - - - -

      -
      > - -
      -

      -

      -

      -

      Text node

      -

      -
      - - - -
      -
      -
      -
      -
      -
      - -
      - - - - - - - - - - - - - - - - - - - - - - - -
      - -
      -
      -
      -
      - -

      -

      -

      -
      - -
      All pseudo-element tests
      - -
      -

      -

      -

      - - -
      -
      -

      -
      -

      -
      -
      -
      -
      - - - - - - -
      - -
      -
      -
      - -
        -
      • -
      • -
      • -
      • -
      - - - - - - -
      - -
      -
      -
      -
      -
      -
      -
      -
      -
      - -
      -
      -
      -
      -
      -
      -
      -
      -
      - -
      -
      -
      -
      -
      -
      -

      -
      -
      -
      -

      -

      -
      - -
      -
      -
      -
      -
      -
      -

      -
      -
      -
      -

      -

      -
      - -
      - - -
      -
      - - -`; diff --git a/packages/web-platform-tests/src/compatibility/element/selectors/grammar.ts b/packages/web-platform-tests/src/compatibility/element/selectors/grammar.ts deleted file mode 100644 index ba512d8..0000000 --- a/packages/web-platform-tests/src/compatibility/element/selectors/grammar.ts +++ /dev/null @@ -1,1874 +0,0 @@ -// Adapted from web-platform-tests at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright © web-platform-tests contributors. CSS escape cases by Domenic -// Denicola and bellbind; :has cases by Byungwoo Lee. BSD-3-Clause; -// see ../../../../test/wpt/LICENSE. Original source paths are listed below. -import type { CompatibilityContext, DOMFixture } from '../../context.ts'; -import type { DOMElement } from '../../contracts/nodes.ts'; - -function prepare(fixture: DOMFixture, markup: string): Document { - const document = fixture.document as unknown as Document; - const root = document.createElement('div'); - root.innerHTML = markup; - fixture.connect(root as unknown as DOMElement); - return document; -} - -export const sourceFiles = [ - 'dom/nodes/ParentNode-querySelector-All-multiple-not.html', - 'dom/nodes/ParentNode-querySelector-basic-has.html', - 'dom/nodes/ParentNode-querySelector-empty-attribute-value.html', - 'dom/nodes/ParentNode-querySelector-hyphen-prefix.html', - 'dom/nodes/ParentNode-querySelector-invalid-comment-like.html', - 'dom/nodes/ParentNode-querySelector-is-nested-pseudo-class.html', - 'dom/nodes/ParentNode-querySelector-is-not.html', - 'dom/nodes/ParentNode-querySelector-last-child-combinator.html', - 'dom/nodes/ParentNode-querySelector-matches.html', - 'dom/nodes/ParentNode-querySelector-nested-nth-child.html', - 'dom/nodes/ParentNode-querySelector-nested.html', - 'dom/nodes/ParentNode-querySelector-nth-of-type-complex.html', - 'dom/nodes/ParentNode-querySelectorAll-case-insensitive-attribute-flag.html', - 'dom/nodes/ParentNode-querySelectorAll-nested-not-has.html', - 'dom/nodes/ParentNode-querySelectorAll-not-descendant.html', - 'dom/nodes/ParentNode-querySelectors-space-and-dash-attribute-value.html', - 'dom/nodes/querySelector-empty-id.html', - 'dom/nodes/querySelector-id-nth-child.html', - 'dom/nodes/ParentNode-querySelector-escapes.html', - 'dom/nodes/querySelector-mixed-case.html', - 'css/selectors/is-where-basic.html', - 'css/selectors/has-basic.html', -] as const; - -/** Registers every assertion from the listed pinned WPT selector regression files. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - function arrayEquals( - actual: ArrayLike, - options: { expected: ArrayLike; message?: string }, - ): void { - expect(actual.length, options.message).toBe(options.expected.length); - for (let i = 0; i < actual.length; i++) - expect(actual[i], options.message).toBe(options.expected[i]); - } - function syntaxError(fixture: DOMFixture, operation: () => unknown): void { - let caught: unknown; - try { - operation(); - } catch (error) { - caught = error; - } - expect(caught).toBeInstanceOf(fixture.globals('DOMException')); - expect(caught).toMatchObject({ name: 'SyntaxError', code: 12 }); - } - - context.test( - 'WPT ParentNode-querySelector-All-multiple-not.html: :not() with multiple selectors in the list', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n
      \n
      \n

      Foo

      \n \n
      \n
      ', - ); - - const container = document.getElementById('container')!; - expect(container.querySelectorAll(':not(svg, svg *)').length).toBe(4); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-basic-has.html: Basic :has() support in querySelector', - (fixture) => { - const document = prepare( - fixture, - '\n\n

      \n 123\n

      \n

      ', - ); - - const expectedP = document.getElementById('p-with-span')!; - expect(document.querySelector('p:has(span)')!).toBe(expectedP); - }, - ); - - context.test( - "WPT ParentNode-querySelector-empty-attribute-value.html: [title=''] matches element with empty title attribute", - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n
      \n
      has empty title
      \n
      has non-empty title
      \n
      ', - ); - - const container = document.getElementById('container')!; - expect(container.querySelectorAll("div[title='']").length).toBe(1); - }, - ); - - context.test( - "WPT ParentNode-querySelector-empty-attribute-value.html: Compound [title][title=''] matches element with empty title attribute", - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n
      \n
      has empty title
      \n
      has non-empty title
      \n
      ', - ); - - const container = document.getElementById('container')!; - expect(container.querySelectorAll("div[title][title='']").length).toBe(1); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-hyphen-prefix.html: ID selector with hyphen-prefix followed by letters', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      ', - ); - - const element = document.querySelector('#-abc')!; - expect(element).not.toBe(null); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-hyphen-prefix.html: Class selector with hyphen-prefix followed by letters', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      ', - ); - - const element = document.querySelector('.-xyz')!; - expect(element).not.toBe(null); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-hyphen-prefix.html: ID selector with hyphen-prefix followed by numbers', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      ', - ); - - syntaxError(fixture, () => { - document.querySelector('#-123')!; - }); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-hyphen-prefix.html: Class selector with hyphen-prefix followed by numbers', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      ', - ); - - syntaxError(fixture, () => { - document.querySelector('.-123')!; - }); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-invalid-comment-like.html: querySelector throws SyntaxError for comment-like selector', - (fixture) => { - const document = prepare( - fixture, - '', - ); - - const selector = - ' //MAIN MENU - (used to keep mobile menu options hidden) // #tncms-region-nav-main-nav-right-nav'; - - syntaxError(fixture, () => document.querySelector(selector)!); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-invalid-comment-like.html: querySelectorAll throws SyntaxError for comment-like selector', - (fixture) => { - const document = prepare( - fixture, - '', - ); - - const selector = - ' //MAIN MENU - (used to keep mobile menu options hidden) // #tncms-region-nav-main-nav-right-nav'; - - syntaxError(fixture, () => document.querySelectorAll(selector)); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-invalid-comment-like.html: matches throws SyntaxError for comment-like selector', - (fixture) => { - const document = prepare( - fixture, - '', - ); - - const selector = - ' //MAIN MENU - (used to keep mobile menu options hidden) // #tncms-region-nav-main-nav-right-nav'; - - syntaxError(fixture, () => document.body.matches(selector)); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-is-nested-pseudo-class.html: Simple nested :is selector should match all inputs and buttons', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n \n \n \n \n \n
      ', - ); - - const elements = document.querySelectorAll(':is(:is(input), button)'); - const elementIds = Array.from(elements).map((el) => el.id); - - const expectedIds = ['button1', 'input1', 'input2', 'button2', 'button3']; - arrayEquals(elementIds, { expected: expectedIds }); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-is-nested-pseudo-class.html: Complex nested :is selector with attribute and :not selectors should work', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n \n \n \n \n \n
      ', - ); - - const selector = ':is(:is(button, input)[type=submit], button:not([type])):not([disabled])'; - const elements = document.querySelectorAll(selector); - const elementIds = Array.from(elements).map((el) => el.id); - - const expectedIds = ['button1', 'input2', 'button2']; - arrayEquals(elementIds, { expected: expectedIds }); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-is-not.html: querySelector with :is():not() combined', - (fixture) => { - const document = prepare( - fixture, - '', - ); - - expect(document.querySelector(':is([a],b):not(.c)')!, 'does not throw').toBe(null); - expect(document.querySelector(':is(a,[b]):not(.c)')!, 'does not throw').toBe(null); - expect(document.querySelector(':is(a,b):not(.c)')!, 'does not throw').toBe(null); - expect(document.querySelector(':is(a,b):not([c])')!, 'does not throw').toBe(null); - expect(document.querySelector(':is(a):not([b],.c)')!, 'does not throw').toBe(null); - expect(document.querySelector(':is(a,[b])')!, 'does not throw').toBe(null); - expect(document.querySelector(':is(a,[b]):first-child')!, 'does not throw').toBe(null); - expect(document.querySelector(':is([b]):not(.c)')!, 'does not throw').toBe(null); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-last-child-combinator.html: div:last-child > span[title] should match', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n
      text
      \n
      ', - ); - - const container = document.getElementById('container')!; - const span = container.querySelector('div:last-child > span[title]')!; - expect(span).not.toBe(null); - expect(span.textContent).toBe('text'); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-matches.html: matches with ancestor ID and descendant type selector', - (fixture) => { - const document = prepare( - fixture, - '
      \n

      Foo

      \n

      Bar

      \n
      ', - ); - - const el = document.querySelector('#main .foo')!; - expect(el.matches('#main p')).toBe(true); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-matches.html: matches returns false for non-matching ID selector', - (fixture) => { - const document = prepare( - fixture, - '
      \n

      Foo

      \n

      Bar

      \n
      ', - ); - - const el = document.querySelector('#main .foo')!; - expect(el.matches('#nonexistent')).toBe(false); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-matches.html: matches with :not() pseudo-class', - (fixture) => { - const document = prepare( - fixture, - '
      \n

      Foo

      \n

      Bar

      \n
      ', - ); - - const el = document.querySelector('#main p:not(.foo)')!; - expect(el.matches('#main p:not(.foo)')).toBe(true); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-matches.html: matches with simple type selector', - (fixture) => { - const document = prepare( - fixture, - '
      \n

      Foo

      \n

      Bar

      \n
      ', - ); - - const el = document.querySelector('#main .foo')!; - expect(el.matches('p')).toBe(true); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-nested-nth-child.html: querySelector with nested :nth-child() without tag names', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n
      \n
      \n

      \n link\n

      \n
      \n
      \n
      ', - ); - - const selector = 'main > :nth-child(1) > :nth-child(1) a'; - const expectedLink = document.getElementById('targetLink')!; - expect(document.querySelector(selector)!).toBe(expectedLink); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-nested.html: querySelector on multi-level query selecting', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n Actions\n \n
      ', - ); - - const target = document.getElementById('span2')!; - const anchor = document.querySelector('a')!; - const span = anchor.querySelector('span')!; - expect(span).toBe(target); - }, - ); - - context.test( - 'WPT ParentNode-querySelector-nth-of-type-complex.html: querySelector with complex :nth-of-type selector', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n

      Sibling to ensure div.clearfix is :nth-of-type(1) among divs

      \n
      \n \n \n \n \n \n \n \n
      Row 1 Data
      \n Target Text\n
      \n
      \n
      \n \n
      \n
      ', - ); - - const selector = '.team-wrap .clearfix:nth-of-type(1) tr:nth-of-type(2) .u-hyphenate'; - const expectedElement = document.getElementById('targetElement')!; - expect(document.querySelector(selector)!).toBe(expectedElement); - }, - ); - - context.test( - "WPT ParentNode-querySelectorAll-case-insensitive-attribute-flag.html: Case-insensitive attribute selector with lowercase flag 'i' should work", - (fixture) => { - const document = prepare( - fixture, - '
      \n
      Test div with CasE class
      \n
      ', - ); - - const elements = document.querySelectorAll('div[class=CasE i]'); - expect(elements.length, "Selector with lowercase 'i' flag should match the element").toBe(1); - }, - ); - - context.test( - "WPT ParentNode-querySelectorAll-case-insensitive-attribute-flag.html: Case-insensitive attribute selector with uppercase flag 'I' should work", - (fixture) => { - const document = prepare( - fixture, - '
      \n
      Test div with CasE class
      \n
      ', - ); - - const elements = document.querySelectorAll('div[class=CasE I]'); - expect(elements.length, "Selector with uppercase 'I' flag should match the element").toBe(1); - }, - ); - - context.test( - 'WPT ParentNode-querySelectorAll-nested-not-has.html: querySelectorAll with :not(:has(..))', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n
      A:
      \n
      A
      \n
      B:
      \n
      \n B\n
      \n
      C:
      \n
      C
      \n
      D:
      \n
      D
      \n
      \n
      \n
      E:
      \n
      E
      \n
      ', - ); - - const selector1 = 'dd:not(:has(dd))'; - const selector2 = 'dd:not(dd:has(dd))'; - const expectedElements = [ - document.getElementById('dd-a')!, - document.getElementById('dd-c')!, - document.getElementById('dd-d')!, - document.getElementById('dd-e')!, - ]; - - const result1 = document.querySelectorAll(selector1); - arrayEquals(result1, { expected: expectedElements, message: selector1 }); - - const result2 = document.querySelectorAll(selector2); - arrayEquals(result2, { expected: expectedElements, message: selector2 }); - }, - ); - - context.test( - 'WPT ParentNode-querySelectorAll-not-descendant.html: querySelectorAll with :not() pseudo-class and descendant selector', - (fixture) => { - const document = prepare( - fixture, - '\n\n
      \n

      \n \n \n \n \n \n \n
      \n

      \n
      \n

      \n

      \n
      ', - ); - - const container = document.getElementById('container')!; - const result = container.querySelectorAll('p:not(table p)'); - - // Browsers parse the invalid HTML `

      ...

      ` - // such that the first

      is closed before the , - // and the closing

      tag creates a *new* empty

      element after the table. - // Thus, the query should match p#p1, the empty p after the table, and p#p3. - const resultIDs = Array.from(result, (el) => el.id); - arrayEquals(resultIDs, { expected: ['p1', '', 'p3'] }); - }, - ); - - context.test( - 'WPT ParentNode-querySelectors-space-and-dash-attribute-value.html: querySelector', - (fixture) => { - const document = prepare( - fixture, - '\n\nTest One', - ); - - const el = document.getElementById('testme')!; - - expect(document.querySelector("a[title='test with - dash and space']")!).toBe(el); - }, - ); - - context.test( - 'WPT ParentNode-querySelectors-space-and-dash-attribute-value.html: querySelectorAll', - (fixture) => { - const document = prepare( - fixture, - '\n\nTest One', - ); - - const el = document.getElementById('testme')!; - - arrayEquals(document.querySelectorAll("a[title='test with - dash and space']"), { - expected: [el], - }); - }, - ); - - context.test( - 'WPT querySelector-empty-id.html: querySelector attribute matching empty id', - (fixture) => { - const document = prepare( - fixture, - '

      \n
      \n
      \n
      \n
      ', - ); - const root = document.getElementById('root')!; - - expect(root.querySelectorAll('[id]').length).toBe(2); - expect(root.querySelectorAll("[id='']").length).toBe(2); - }, - ); - - context.test( - 'WPT querySelector-id-nth-child.html: querySelector matching #id + constant nth-child()', - (fixture) => { - const document = prepare(fixture, '
      \n
      \n
      '); - const target = document.getElementById('target')!; - - expect(document.querySelector('#target:nth-child(1)')!).toBe(target); - expect(document.querySelector('#target:nth-child(4)')!).toBe(null); - }, - ); - - context.test( - 'WPT querySelector-id-nth-child.html: querySelectorAll matching #id + constant nth-child()', - (fixture) => { - const document = prepare(fixture, '
      \n
      \n
      '); - - expect(document.querySelectorAll('#target:nth-child(1)').length).toBe(1); - expect(document.querySelectorAll('#target:nth-child(4)').length).toBe(0); - }, - ); - - function escapeCase(id: string, options: { selector: string; matched: boolean }): void { - const { selector, matched } = options; - context.test( - 'WPT ParentNode-querySelector-escapes.html: ' + - JSON.stringify(id) + - (matched ? ' should match with ' : ' should never match with ') + - JSON.stringify(selector), - (fixture) => { - const document = fixture.document as unknown as Document; - const container = document.createElement('div'); - const child = document.createElement('span'); - child.id = id; - container.appendChild(child); - expect(container.querySelector(selector)).toBe(matched ? child : null); - }, - ); - } - // 4.3.7 from https://drafts.csswg.org/css-syntax/#consume-escaped-code-point - escapeCase('nonescaped', { selector: '#nonescaped', matched: true }); - - // - escape hex digit - escapeCase('0nextIsWhiteSpace', { selector: '#\\30 nextIsWhiteSpace', matched: true }); - escapeCase('0nextIsNotHexLetters', { selector: '#\\30nextIsNotHexLetters', matched: true }); - escapeCase('0connectHexMoreThan6Hex', { - selector: '#\\000030connectHexMoreThan6Hex', - matched: true, - }); - escapeCase('0spaceMoreThan6Hex', { selector: '#\\000030 spaceMoreThan6Hex', matched: true }); - - // - hex digit special replacement - // 1. zero points - escapeCase('zero\u{fffd}', { selector: '#zero\\0', matched: true }); - escapeCase('zero\u{0}', { selector: '#zero\\0', matched: false }); - escapeCase('zero\u{fffd}', { selector: '#zero\\000000', matched: true }); - escapeCase('zero\u{0}', { selector: '#zero\\000000', matched: false }); - // 2. surrogate points - escapeCase('\u{fffd}surrogateFirst', { selector: '#\\d83d surrogateFirst', matched: true }); - escapeCase('\ud83dsurrogateFirst', { selector: '#\\d83d surrogateFirst', matched: false }); - escapeCase('surrogateSecond\u{fffd}', { selector: '#surrogateSecond\\dd11', matched: true }); - escapeCase('surrogateSecond\udd11', { selector: '#surrogateSecond\\dd11', matched: false }); - escapeCase('surrogatePair\u{fffd}\u{fffd}', { - selector: '#surrogatePair\\d83d\\dd11', - matched: true, - }); - escapeCase('surrogatePair\u{1f511}', { selector: '#surrogatePair\\d83d\\dd11', matched: false }); - // 3. out of range points - escapeCase('outOfRange\u{fffd}', { selector: '#outOfRange\\110000', matched: true }); - escapeCase('outOfRange\u{fffd}', { selector: '#outOfRange\\110030', matched: true }); - escapeCase('outOfRange\u{30}', { selector: '#outOfRange\\110030', matched: false }); - escapeCase('outOfRange\u{fffd}', { selector: '#outOfRange\\555555', matched: true }); - escapeCase('outOfRange\u{fffd}', { selector: '#outOfRange\\ffffff', matched: true }); - - // - escape EOF - escapeCase('eof\\', { selector: '#eof\\', matched: false }); - - // - escape anythong else - escapeCase('.comma', { selector: '#\\.comma', matched: true }); - escapeCase('-minus', { selector: '#\\-minus', matched: true }); - escapeCase('g', { selector: '#\\g', matched: true }); - - // non edge cases - escapeCase('aBMPRegular', { selector: '#\\61 BMPRegular', matched: true }); - escapeCase('\u{1f511}nonBMP', { selector: '#\\1f511 nonBMP', matched: true }); - escapeCase('00continueEscapes', { selector: '#\\30\\30 continueEscapes', matched: true }); - escapeCase('00continueEscapes', { selector: '#\\30 \\30 continueEscapes', matched: true }); - escapeCase('continueEscapes00', { selector: '#continueEscapes\\30 \\30 ', matched: true }); - escapeCase('continueEscapes00', { selector: '#continueEscapes\\30 \\30', matched: true }); - escapeCase('continueEscapes00', { selector: '#continueEscapes\\30\\30 ', matched: true }); - escapeCase('continueEscapes00', { selector: '#continueEscapes\\30\\30', matched: true }); - - // ident tests case from CSS tests of chromium source: https://goo.gl/3Cxdov - escapeCase('hello', { selector: '#hel\\6Co', matched: true }); - escapeCase('&B', { selector: '#\\26 B', matched: true }); - escapeCase('hello', { selector: '#hel\\6C o', matched: true }); - escapeCase('spaces', { selector: '#spac\\65\r\ns', matched: true }); - escapeCase('spaces', { selector: '#sp\\61\tc\\65\fs', matched: true }); - escapeCase('test\u{D799}', { selector: '#test\\D799', matched: true }); - escapeCase('\u{E000}', { selector: '#\\E000', matched: true }); - escapeCase('test', { selector: '#te\\s\\t', matched: true }); - escapeCase('spaces in\tident', { selector: '#spaces\\ in\\\tident', matched: true }); - escapeCase('.,:!', { selector: '#\\.\\,\\:\\!', matched: true }); - escapeCase('null\u{fffd}', { selector: '#null\\0', matched: true }); - escapeCase('null\u{fffd}', { selector: '#null\\0000', matched: true }); - escapeCase('large\u{fffd}', { selector: '#large\\110000', matched: true }); - escapeCase('large\u{fffd}', { selector: '#large\\23456a', matched: true }); - escapeCase('surrogate\u{fffd}', { selector: '#surrogate\\D800', matched: true }); - escapeCase('surrogate\u{fffd}', { selector: '#surrogate\\0DBAC', matched: true }); - escapeCase('\u{fffd}surrogate', { selector: '#\\00DFFFsurrogate', matched: true }); - escapeCase('\u{10ffff}', { selector: '#\\10fFfF', matched: true }); - escapeCase('\u{10ffff}0', { selector: '#\\10fFfF0', matched: true }); - escapeCase('\u{100000}00', { selector: '#\\10000000', matched: true }); - escapeCase('eof\u{fffd}', { selector: '#eof\\', matched: true }); - - escapeCase('simple-ident', { selector: '#simple-ident', matched: true }); - escapeCase('testing123', { selector: '#testing123', matched: true }); - escapeCase('_underscore', { selector: '#_underscore', matched: true }); - escapeCase('-text', { selector: '#-text', matched: true }); - escapeCase('-m', { selector: '#-\\6d', matched: true }); - escapeCase('--abc', { selector: '#--abc', matched: true }); - escapeCase('--', { selector: '#--', matched: true }); - escapeCase('--11', { selector: '#--11', matched: true }); - escapeCase('---', { selector: '#---', matched: true }); - escapeCase('\u{2003}', { selector: '#\u{2003}', matched: true }); - escapeCase('\u{A0}', { selector: '#\u{A0}', matched: true }); - escapeCase('\u{1234}', { selector: '#\u{1234}', matched: true }); - escapeCase('\u{12345}', { selector: '#\u{12345}', matched: true }); - escapeCase('\u{fffd}', { selector: '#\u{0}', matched: true }); - escapeCase('ab\u{fffd}c', { selector: '#ab\u{0}c', matched: true }); - - context.test( - 'WPT querySelector-mixed-case.html: Mixed HTML/SVG/MathML tree with various mixed-case attributes', - (fixture) => { - const document = prepare(fixture, '
      '); - const container = document.getElementById('test-container')!; - function buildTestTree(): HTMLDivElement { - // Build entire DOM tree structure first (without attributes) - const html1 = document.createElement('div'); - html1.id = 'html1'; - - const svg1 = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg1.id = 'svg1'; - - const svg2 = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - svg2.id = 'svg2'; - - const svg3 = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - svg3.id = 'svg3'; - - const html2 = document.createElement('div'); - html2.id = 'html2'; - - const math1 = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'math'); - math1.id = 'math1'; - - const math2 = document.createElementNS('http://www.w3.org/1998/Math/MathML', 'mi'); - math2.id = 'math2'; - - const svg4 = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg4.id = 'svg4'; - - const svg5 = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - svg5.id = 'svg5'; - - const html3 = document.createElement('div'); - html3.id = 'html3'; - - // Create foreignObject with HTML inside SVG (will go in svg1) - const foreign1 = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); - foreign1.id = 'foreign1'; - - const html4 = document.createElement('div'); - html4.id = 'html4'; - - const html5 = document.createElement('span'); - html5.id = 'html5'; - - // Create nested: HTML > SVG inside html2 - const svg6 = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg6.id = 'svg6'; - - const foreign2 = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'); - foreign2.id = 'foreign2'; - - const html6 = document.createElement('div'); - html6.id = 'html6'; - - const svg7 = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg7.id = 'svg7'; - - const svg8 = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - svg8.id = 'svg8'; - - // Build tree structure - svg2.appendChild(svg3); - svg1.appendChild(svg2); - - // Add foreignObject with HTML to svg1 - html4.appendChild(html5); - foreign1.appendChild(html4); - svg1.appendChild(foreign1); - - html1.appendChild(svg1); - - math1.appendChild(math2); - html2.appendChild(math1); - - // Add deeply nested SVG > foreignObject > HTML > SVG to html2 - svg7.appendChild(svg8); - html6.appendChild(svg7); - foreign2.appendChild(html6); - svg6.appendChild(foreign2); - html2.appendChild(svg6); - - html1.appendChild(html2); - - svg4.appendChild(svg5); - - // Create wrapper container for all trees - const wrapper = document.createElement('div'); - wrapper.appendChild(html1); - wrapper.appendChild(svg4); - wrapper.appendChild(html3); - - // NOW set all attributes - html1.setAttribute('viewBox', 'html-val'); - html1.setAttribute('dataIndex', '0'); - html1.setAttribute('testAttr', 'alpha'); - - svg1.setAttribute('viewBox', 'svg-val'); - svg1.setAttribute('dataIndex', '1'); - svg1.setAttribute('testAttr', 'beta'); - - svg2.setAttribute('mixedCase', 'foo'); - svg2.setAttribute('dataValue', 'first'); - - svg3.setAttribute('innerAttr', 'found'); - - html2.setAttribute('viewBox', 'nested'); - html2.setAttribute('mathVariant', 'bold'); - - math1.setAttribute('mathVariant', 'italic'); - math1.setAttribute('displayStyle', 'true'); - - math2.setAttribute('testIndex', '5'); - math2.setAttribute('dataValue', 'second'); - - svg5.setAttribute('viewBox', 'rect-val'); - svg5.setAttribute('testIndex', '10'); - - html3.setAttribute('MixedCase', 'bar'); - html3.setAttribute('TestIndex', '20'); - - // Set attributes on foreignObject tree in svg1 - foreign1.setAttribute('foreignAttr', 'foreign-val'); - - html4.setAttribute('ForeignHTML', 'inside-foreign'); - html4.setAttribute('DataType', 'html-in-svg'); - - html5.setAttribute('NestedCase', 'span-val'); - - // Set attributes on deeply nested tree in html2 - svg6.setAttribute('DeepSVG', 'middle-svg'); - svg6.setAttribute('CaseSensitive', 'nested'); - - foreign2.setAttribute('SecondForeign', 'deep-foreign'); - - html6.setAttribute('InnerHTML', 'deep-html'); - html6.setAttribute('DataType', 'nested'); - - svg7.setAttribute('ReEnteredSVG', 'back-to-svg'); - - svg8.setAttribute('DeepestAttr', 'circle-val'); - svg8.setAttribute('CaseSensitive', 'deepest'); - - return wrapper; - } - function runTests(root: Element): void { - // DOM tree structure with attributes: - //
      - //
      - // - // - // - // - // - //
      - // - //
      - //
      - //
      - //
      - // - // - // - // - // - //
      - // - // - // - //
      - //
      - //
      - //
      - //
      - // - // - // - //
      - //
      - - // Test 1: viewBox with mixed case - HTML is case-insensitive, SVG is case-sensitive - let results = root.querySelectorAll('[viewBox]'); - expect(results.length, '[viewBox] should match 2 HTML elements + 2 SVG elements').toBe(4); - let ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html1', 'html2', 'svg1', 'svg5'], - message: '[viewBox] should match correct elements', - }); - - // Test 2: viewbox lowercase - should only match HTML elements - results = root.querySelectorAll('[viewbox]'); - expect(results.length, '[viewbox] should only match HTML elements').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html1', 'html2'], - message: '[viewbox] should only match HTML divs', - }); - - // Test 3: VIEWBOX uppercase - should only match HTML elements - results = root.querySelectorAll('[VIEWBOX]'); - expect(results.length, '[VIEWBOX] should only match HTML elements').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html1', 'html2'], - message: '[VIEWBOX] should only match HTML divs', - }); - - // Test 4: mathVariant - HTML is case-insensitive, MathML is case-sensitive - results = root.querySelectorAll('[mathVariant]'); - expect(results.length, '[mathVariant] should match HTML + MathML').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html2', 'math1'], - message: '[mathVariant] should match correct elements', - }); - - // Test 5: mathvariant lowercase - should only match HTML - results = root.querySelectorAll('[mathvariant]'); - expect(results.length, '[mathvariant] should only match HTML').toBe(1); - expect(results[0]!.id, '[mathvariant] should match html2').toBe('html2'); - - // Test 6: dataIndex exact case - results = root.querySelectorAll('[dataIndex]'); - expect(results.length, '[dataIndex] should match HTML and SVG').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html1', 'svg1'], - message: '[dataIndex] should match html1 and svg1', - }); - - // Test 7: dataindex lowercase - should only match HTML - results = root.querySelectorAll('[dataindex]'); - expect(results.length, '[dataindex] should only match HTML').toBe(1); - expect(results[0]!.id, '[dataindex] should match html1').toBe('html1'); - - // Test 8: testIndex mixed case - // HTML elements: selector lowercased → [testindex] → matches html3 - // SVG/MathML: selector used as-is → [testIndex] → matches math2, svg5 (case-sensitive) - results = root.querySelectorAll('[testIndex]'); - expect(results.length, '[testIndex] should match 3 elements').toBe(3); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html3', 'math2', 'svg5'], - message: '[testIndex] should match html3, math2, and svg5', - }); - - // Test 9: TestIndex with capital T - // HTML elements: selector lowercased → [testindex] → matches html3 - // SVG/MathML: selector used as-is → [TestIndex] → no match (testIndex != TestIndex) - results = root.querySelectorAll('[TestIndex]'); - expect(results.length, '[TestIndex] should only match HTML element').toBe(1); - expect(results[0]!.id, '[TestIndex] should match html3').toBe('html3'); - - // Test 10: testindex all lowercase - // HTML elements: selector lowercased → [testindex] → matches html3 - // SVG/MathML: selector used as-is → [testindex] → no match (testIndex != testindex) - results = root.querySelectorAll('[testindex]'); - expect(results.length, '[testindex] should only match HTML element').toBe(1); - expect(results[0]!.id, '[testindex] should match html3').toBe('html3'); - - // Test 11: mixedCase exact - // HTML elements: selector lowercased → [mixedcase] → matches html3 - // SVG: selector used as-is → [mixedCase] → matches svg2 (case-sensitive) - results = root.querySelectorAll('[mixedCase]'); - expect(results.length, '[mixedCase] should match 2 elements').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html3', 'svg2'], - message: '[mixedCase] should match html3 and svg2', - }); - - // Test 12: MixedCase capital M - // HTML elements: selector lowercased → [mixedcase] → matches html3 - // SVG: selector used as-is → [MixedCase] → no match (mixedCase != MixedCase) - results = root.querySelectorAll('[MixedCase]'); - expect(results.length, '[MixedCase] should only match HTML element').toBe(1); - expect(results[0]!.id, '[MixedCase] should match html3').toBe('html3'); - - // Test 13: mixedcase all lowercase - // HTML elements: selector lowercased → [mixedcase] → matches html3 - // SVG: selector used as-is → [mixedcase] → no match (mixedCase != mixedcase) - results = root.querySelectorAll('[mixedcase]'); - expect(results.length, '[mixedcase] should only match HTML element').toBe(1); - expect(results[0]!.id, '[mixedcase] should match html3').toBe('html3'); - - // Test 14: testAttr exact case - results = root.querySelectorAll('[testAttr]'); - expect(results.length, '[testAttr] should match HTML and SVG').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html1', 'svg1'], - message: '[testAttr] should match html1 and svg1', - }); - - // Test 15: Attribute value matching with prefix - results = root.querySelectorAll('[viewBox^="svg"]'); - expect(results.length, '[viewBox^="svg"] should match 1 SVG element').toBe(1); - expect(results[0]!.id, '[viewBox^="svg"] should match svg1').toBe('svg1'); - - // Test 16: Attribute value matching with exact match - results = root.querySelectorAll('[dataIndex="0"]'); - expect(results.length, '[dataIndex="0"] should match 1 element').toBe(1); - expect(results[0]!.id, '[dataIndex="0"] should match html1').toBe('html1'); - - // Test 17: Nested element query - results = root.querySelectorAll('[innerAttr]'); - expect(results.length, '[innerAttr] should match nested circle').toBe(1); - expect(results[0]!.id, '[innerAttr] should match svg3').toBe('svg3'); - - // Test 18: displayStyle on MathML - results = root.querySelectorAll('[displayStyle]'); - expect(results.length, '[displayStyle] should match MathML').toBe(1); - expect(results[0]!.id, '[displayStyle] should match math1').toBe('math1'); - - // Test 19: displaystyle lowercase - should not match anything (no HTML element has it) - results = root.querySelectorAll('[displaystyle]'); - expect(results.length, '[displaystyle] should not match anything').toBe(0); - - // Test 20: Case-sensitive value matching with 's' flag - results = root.querySelectorAll('[testAttr="alpha" s]'); - expect(results.length, '[testAttr="alpha" s] should match 1 element').toBe(1); - expect(results[0]!.id, '[testAttr="alpha" s] should match html1').toBe('html1'); - - // Test 21: Case-insensitive value matching with 'i' flag - results = root.querySelectorAll('[testAttr="ALPHA" i]'); - expect(results.length, '[testAttr="ALPHA" i] should match 1 element').toBe(1); - expect(results[0]!.id, '[testAttr="ALPHA" i] should match html1').toBe('html1'); - - // Test 22: ForeignHTML - HTML inside foreignObject should be case-insensitive - results = root.querySelectorAll('[ForeignHTML]'); - expect(results.length, '[ForeignHTML] should match HTML in foreignObject').toBe(1); - expect(results[0]!.id, '[ForeignHTML] should match html4').toBe('html4'); - - // Test 23: foreignhtml lowercase - should match HTML element in foreignObject - results = root.querySelectorAll('[foreignhtml]'); - expect(results.length, '[foreignhtml] should match HTML in foreignObject').toBe(1); - expect(results[0]!.id, '[foreignhtml] should match html4').toBe('html4'); - - // Test 24: DataType - should match both HTML elements (case-insensitive) - results = root.querySelectorAll('[DataType]'); - expect(results.length, '[DataType] should match 2 HTML elements').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html4', 'html6'], - message: '[DataType] should match html4 and html6', - }); - - // Test 25: datatype lowercase - should match both HTML elements - results = root.querySelectorAll('[datatype]'); - expect(results.length, '[datatype] should match 2 HTML elements').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['html4', 'html6'], - message: '[datatype] should match html4 and html6', - }); - - // Test 26: CaseSensitive on SVG elements - case-sensitive - results = root.querySelectorAll('[CaseSensitive]'); - expect(results.length, '[CaseSensitive] should match 2 SVG elements').toBe(2); - ids = Array.from(results) - .map((el) => el.id) - .toSorted(); - arrayEquals(ids, { - expected: ['svg6', 'svg8'], - message: '[CaseSensitive] should match svg6, svg8', - }); - - // Test 27: casesensitive lowercase - should NOT match SVG elements - results = root.querySelectorAll('[casesensitive]'); - expect(results.length, '[casesensitive] should not match SVG elements').toBe(0); - - // Test 28: NestedCase - HTML span inside foreignObject should be case-insensitive - results = root.querySelectorAll('[NestedCase]'); - expect(results.length, '[NestedCase] should match HTML span in foreignObject').toBe(1); - expect(results[0]!.id, '[NestedCase] should match html5').toBe('html5'); - - // Test 29: nestedcase lowercase - should match HTML span - results = root.querySelectorAll('[nestedcase]'); - expect(results.length, '[nestedcase] should match HTML span in foreignObject').toBe(1); - expect(results[0]!.id, '[nestedcase] should match html5').toBe('html5'); - - // Test 30: ReEnteredSVG - SVG inside HTML inside foreignObject should be case-sensitive - results = root.querySelectorAll('[ReEnteredSVG]'); - expect( - results.length, - '[ReEnteredSVG] should match SVG nested in HTML in foreignObject', - ).toBe(1); - expect(results[0]!.id, '[ReEnteredSVG] should match svg7').toBe('svg7'); - - // Test 31: reenteredsvg lowercase - should NOT match (SVG is case-sensitive) - results = root.querySelectorAll('[reenteredsvg]'); - expect(results.length, '[reenteredsvg] should not match SVG element').toBe(0); - - // Test 32: DeepestAttr - deeply nested SVG circle should be case-sensitive - results = root.querySelectorAll('[DeepestAttr]'); - expect(results.length, '[DeepestAttr] should match deeply nested circle').toBe(1); - expect(results[0]!.id, '[DeepestAttr] should match svg8').toBe('svg8'); - - // Test 33: deepestattr lowercase - should NOT match - results = root.querySelectorAll('[deepestattr]'); - expect(results.length, '[deepestattr] should not match SVG circle').toBe(0); - - // Test 34: foreignAttr on foreignObject element - case-sensitive - results = root.querySelectorAll('[foreignAttr]'); - expect(results.length, '[foreignAttr] should match foreignObject').toBe(1); - expect(results[0]!.id, '[foreignAttr] should match foreign1').toBe('foreign1'); - - // Test 35: foreignattr lowercase - should NOT match foreignObject (SVG namespace) - results = root.querySelectorAll('[foreignattr]'); - expect(results.length, '[foreignattr] should not match foreignObject').toBe(0); - - // Test 36: InnerHTML on HTML element inside foreignObject - case-insensitive - results = root.querySelectorAll('[InnerHTML]'); - expect(results.length, '[InnerHTML] should match HTML in foreignObject').toBe(1); - expect(results[0]!.id, '[InnerHTML] should match html6').toBe('html6'); - - // Test 37: innerhtml lowercase - should match HTML in foreignObject - results = root.querySelectorAll('[innerhtml]'); - expect(results.length, '[innerhtml] should match HTML in foreignObject').toBe(1); - expect(results[0]!.id, '[innerhtml] should match html6').toBe('html6'); - - // Test 38: DeepSVG on SVG element - case-sensitive - results = root.querySelectorAll('[DeepSVG]'); - expect(results.length, '[DeepSVG] should match SVG element').toBe(1); - expect(results[0]!.id, '[DeepSVG] should match svg6').toBe('svg6'); - - // Test 39: deepsvg lowercase - should NOT match SVG - results = root.querySelectorAll('[deepsvg]'); - expect(results.length, '[deepsvg] should not match SVG element').toBe(0); - - // Test 40: SecondForeign on foreignObject element - case-sensitive - results = root.querySelectorAll('[SecondForeign]'); - expect(results.length, '[SecondForeign] should match foreignObject').toBe(1); - expect(results[0]!.id, '[SecondForeign] should match foreign2').toBe('foreign2'); - - // Test 41: secondforeign lowercase - should NOT match foreignObject - results = root.querySelectorAll('[secondforeign]'); - expect(results.length, '[secondforeign] should not match foreignObject').toBe(0); - } - - const tree = buildTestTree(); - - // Test on disconnected tree first - runTests(tree); - - // Now append to document and test again - container.appendChild(tree); - runTests(container); - - container.innerHTML = ''; - }, - ['element.namespaces'], - ); - - 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(), - ); - }, - ); - - context.test('WPT has-basic.html: :has(#a) matches expected elements', (fixture) => { - const document = prepare( - fixture, - '
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      ', - ); - const h = document.getElementById('h')!; - expect(h.matches(':has(.target ~ .sibling .descendant)')).toBe(true); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/element/selectors/matching.ts b/packages/web-platform-tests/src/compatibility/element/selectors/matching.ts deleted file mode 100644 index 4ec0a6f..0000000 --- a/packages/web-platform-tests/src/compatibility/element/selectors/matching.ts +++ /dev/null @@ -1,268 +0,0 @@ -// Adapted from the WPT files listed below at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright © web-platform-tests contributors. BSD-3-Clause; see ../../../../test/wpt/LICENSE. -import type { CompatibilityContext, DOMFixture } from '../../context.ts'; -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; - -export const sourceFiles = [ - 'dom/nodes/Element-closest-null-selectors.html', - 'dom/nodes/Element-closest.html', - 'dom/nodes/Element-matches-complex-nth-last-of-type.html', - 'dom/nodes/Element-matches-disabled-pseudo-class.html', - 'dom/nodes/Element-matches-active-pseudo-class.html', -] as const; - -function html(fixture: DOMFixture, markup: string): Document { - const document = fixture.document as unknown as Document; - const container = document.createElement('div'); - container.innerHTML = markup; - for (const child of Array.from(container.children)) - fixture.connect(child as unknown as DOMElementContract); - 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 disabledMarkup = ` -
      - - - - -
      -
      - - - - -
      - - -
      A Legend
      `; - -/** Registers every assertion from the pinned closest and matches regressions. */ -export function register(context: CompatibilityContext): void { - 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(); - }, - ); - } - 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); - }, - selector === ':invalid' - ? ['elements.form.validation', 'elements.input.validation', 'elements.select.validation'] - : [], - ); - } - context.test( - 'WPT Element-matches-complex-nth-last-of-type.html: element.matches() with complex :nth-last-of-type selector', - (fixture) => { - const document = fixture.document as unknown as 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); - }, - ); - 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); - }, - ['elements.fieldset.disabled'], - ); - } - 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); - }, - ['elements.fieldset.disabled'], - ); - 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', - ]); - }, - ['elements.fieldset.disabled'], - ); - 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); - }, - ['elements.fieldset.disabled'], - ); - 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'); - }, - ['elements.fieldset.disabled'], - ); - 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']); - }, - ['elements.fieldset.disabled'], - ); - 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); - }, - ['elements.fieldset.disabled'], - ); - 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
      \n\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
      \n\nLink element', - ); - expect(document.querySelectorAll(':active').length).toBe(0); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/element/selectors/matrix.ts b/packages/web-platform-tests/src/compatibility/element/selectors/matrix.ts deleted file mode 100644 index d2c6458..0000000 --- a/packages/web-platform-tests/src/compatibility/element/selectors/matrix.ts +++ /dev/null @@ -1,336 +0,0 @@ -// Adapted from the WPT files in sourceFiles at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright © web-platform-tests contributors. BSD-3-Clause; see ../../../../test/wpt/LICENSE. -import type { CompatibilityContext, DOMFixture } from '../../context.ts'; -import type { DOMFeaturePath } from '../../features.ts'; -import { - invalidSelectors, - validSelectors, - scopedSelectors, - TEST_QSA, - TEST_MATCH, - type Selector, -} from './data.ts'; -import * as content from './fixtures.ts'; - -export const sourceFiles = [ - 'dom/nodes/selectors.js', - 'dom/nodes/ParentNode-querySelector-All.js', - 'dom/nodes/ParentNode-querySelector-All.html', - 'dom/nodes/ParentNode-querySelector-All-xht.xht', - 'dom/nodes/ParentNode-querySelector-All-content.html', - 'dom/nodes/ParentNode-querySelector-All-content.xht', - 'dom/nodes/Element-matches.js', - 'dom/nodes/Element-matches-init.js', - 'dom/nodes/Element-matches.html', -] as const; - -type Root = Document | Element | DocumentFragment; -type Kind = 'document' | 'detached' | 'fragment' | 'element' | 'empty'; -const roots = { - document: 'Document', - detached: 'Detached Element', - fragment: 'Fragment', - element: 'In-document Element', -} as const; -const htmlNamespace = 'http://www.w3.org/1999/xhtml'; -const exampleNamespace = 'http://www.example.org/ns'; - -/** Walk without selector queries so expected tree order is independent of the tested API. */ -function elements(root: Node): Element[] { - const result: Element[] = []; - function visit(node: Node): void { - if (node.nodeType === 1) result.push(node as Element); - for (let child = node.firstChild; child; child = child.nextSibling) visit(child); - } - visit(root); - return result; -} - -/** The original programmatic additions to both HTML and XHTML content documents. */ -function specialElements(document: Document, parent: Element): void { - parent.appendChild(document.createElement('null')); - parent.appendChild(document.createElement('undefined')); - for (const prefix of ['any-namespace', 'no-namespace']) { - const container = document.createElement('div'); - container.id = prefix; - const children = [ - document.createElement('div'), - document.createElementNS(htmlNamespace, 'div'), - document.createElementNS('', 'div'), - document.createElementNS(exampleNamespace, 'div'), - ]; - children.forEach((child, index) => { - child.setAttribute('id', `${prefix}-div${index + 1}`); - container.appendChild(child); - }); - parent.appendChild(container); - } - document.getElementById('attr-presence-i1')!.setAttributeNS(exampleNamespace, 'title', ''); -} - -function prepare(fixture: DOMFixture, kind: Kind): Root { - const document = fixture.document as unknown as Document; - 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; -} - -function insertionParent(root: Root): Node { - return root.nodeType === 9 ? (root as Document).body : root; -} - -function prepareQueryMutations(fixture: DOMFixture, root: Root): void { - const document = fixture.document as unknown as Document; - 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')); -} - -function prerequisites(kind: Kind, xml: boolean): DOMFeaturePath[] { - return [ - 'element.namespaces', - 'document.lookup', - ...(kind === 'detached' || kind === 'element' || kind === 'fragment' - ? ['node.cloning' as const] - : []), - ...(kind === 'fragment' ? ['node.fragments' as const] : []), - ...(xml ? ['document.xml' as const] : []), - ]; -} - -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') - ); -} - -/** Retains the original matrix, including API masks and HTML/XML/root exclusions. */ -export function register(context: CompatibilityContext): void { - for (const xml of [false, true]) registerQueries(context, xml); - registerMatches(context); -} - -function registerQueries(context: CompatibilityContext, xml: boolean): void { - const { expect } = context.runner; - const source = xml ? 'ParentNode-querySelector-All-xht.xht' : 'ParentNode-querySelector-All.html'; - function test( - options: { kind: Kind; name: string }, - run: (fixture: DOMFixture, root: Root) => void, - ): void { - context.test( - `WPT ${source}: ${options.name}`, - (fixture) => run(fixture, prepare(fixture, options.kind)), - { - requires: prerequisites(options.kind, xml), - document: { - contentType: xml ? 'application/xhtml+xml' : 'text/html', - content: xml ? content.xhtml : content.html, - fragment: 'target', - }, - }, - ); - } - function syntaxError(fixture: DOMFixture, operation: () => unknown): void { - try { - operation(); - expect.fail('Expected SyntaxError'); - } catch (error) { - expect(error).toBeInstanceOf(fixture.globals('DOMException')); - expect(error).toMatchObject({ name: 'SyntaxError', code: 12 }); - } - } - for (const [kind, label] of Object.entries(roots) as [Exclude, string][]) { - for (const method of ['querySelector', 'querySelectorAll'] as const) { - test({ kind, name: `${label} supports ${method}` }, (_fixture, root) => - expect(typeof root[method]).toBe('function'), - ); - } - test({ kind, name: `${label}.querySelectorAll returns NodeList instance` }, (fixture, root) => { - expect(root.querySelectorAll('div')).toBeInstanceOf(fixture.globals('NodeList')); - }); - for (const method of ['querySelectorAll', 'querySelector'] as const) { - for (const value of [null, undefined]) { - test({ kind, name: `${label}.${method} ${String(value)}` }, (_fixture, root) => { - const result = Reflect.apply(root[method], root, [value]); - if (method === 'querySelectorAll') expect(result.length).toBe(1); - else { - if (value === null) expect(result).not.toBeNull(); - else expect(result).not.toBeUndefined(); - expect(result.tagName.toUpperCase()).toBe(String(value).toUpperCase()); - } - }); - } - test({ kind, name: `${label}.${method} no parameter` }, (fixture, root) => { - expect(() => Reflect.apply(root[method], root, [])).toThrow(fixture.globals('TypeError')); - }); - } - test({ kind, name: `${label}.querySelectorAll tree order` }, (_fixture, root) => { - const result = root.querySelectorAll('*'); - elements(root) - .filter((element) => element !== root) - .forEach((element, index) => expect(result[index]).toBe(element)); - }); - for (const variant of ['static NodeList', 'new NodeList']) { - test({ kind, name: `${label}: ${variant}` }, (fixture, root) => { - const before = root.querySelectorAll('div'); - const beforeLength = before.length; - insertionParent(root).appendChild( - (fixture.document as unknown as Document).createElement('div'), - ); - if (variant === 'static NodeList') expect(before.length).toBe(beforeLength); - else expect(root.querySelectorAll('div').length).toBe(beforeLength + 1); - }); - } - for (const row of validSelectors) { - if (!permitted(row, { kind, xml, mask: TEST_QSA })) continue; - for (const method of ['querySelectorAll', 'querySelector'] as const) { - test( - { kind, name: `${label}.${method}: ${row.name}: ${row.selector}` }, - (fixture, root) => { - // verifyStaticList precedes valid queries in WPT; preserve that mutation in each fresh case. - prepareQueryMutations(fixture, root); - const all = root.querySelectorAll(row.selector); - if (method === 'querySelectorAll') { - expect(all).not.toBeNull(); - expect(all.length).toBe(row.expect.length); - row.expect.forEach((id, index) => { - expect(all[index]).not.toBeNull(); - expect(all[index]!.getAttribute('id')).toBe(id); - expect(all[index]!.hasAttribute('data-clone')).toBe(false); - }); - } else { - const found = root.querySelector(row.selector); - if (row.expect.length) { - expect(found).not.toBeNull(); - expect(found!.getAttribute('id')).toBe(row.expect[0]); - expect(found).toBe(all[0]); - expect(found!.hasAttribute('data-clone')).toBe(false); - } else expect(found).toBeNull(); - } - }, - ); - } - } - } - for (const [kind, label] of Object.entries({ ...roots, empty: 'Empty Element' }) as [ - Kind, - string, - ][]) { - for (const row of invalidSelectors) { - for (const method of ['querySelector', 'querySelectorAll'] as const) { - test({ kind, name: `${label}.${method}: ${row.name}: ${row.selector}` }, (fixture, root) => - syntaxError(fixture, () => - method === 'querySelector' - ? root.querySelector(row.selector) - : root.querySelectorAll(row.selector), - ), - ); - } - } - } -} - -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)), - { - requires: prerequisites(options.kind, false), - document: { contentType: 'text/html', content: 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}` }, (fixture, root) => { - try { - (root as Element).matches(row.selector); - expect.fail('Expected SyntaxError'); - } catch (error) { - expect(error).toBeInstanceOf(fixture.globals('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}` }, - (fixture, 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( - fixture.globals('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/compatibility/element/selectors/namespaces.ts b/packages/web-platform-tests/src/compatibility/element/selectors/namespaces.ts deleted file mode 100644 index b6064a6..0000000 --- a/packages/web-platform-tests/src/compatibility/element/selectors/namespaces.ts +++ /dev/null @@ -1,454 +0,0 @@ -// Adapted from the WPT files listed below at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright © web-platform-tests contributors. BSD-3-Clause; see ../../../../test/wpt/LICENSE. -import type { CompatibilityContext, DOMFixture } from '../../context.ts'; -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; - -export const sourceFiles = [ - 'dom/nodes/Element-matches-namespaced-elements.html', - 'dom/nodes/ParentNode-querySelector-case-insensitive.html', - 'dom/nodes/ParentNode-querySelector-multiple-attributes-xml.xhtml', - 'dom/nodes/ParentNode-querySelector-namespace-tagname-attribute.html', - 'dom/nodes/ParentNode-querySelector-scope-numeric-id.xhtml', - 'dom/nodes/ParentNode-querySelector-svg-not-root.html', - 'dom/nodes/ParentNode-querySelector-tag-case.html', - 'dom/nodes/ParentNode-querySelector-underscore-local-name.xhtml', - 'dom/nodes/ParentNode-querySelector-xml-capitalization.window.js', - 'dom/nodes/ParentNode-querySelectorAll-namespace.html', - 'dom/nodes/ParentNode-querySelectorAll-svg-append.html', - 'dom/nodes/ParentNode-querySelectorAll-svg-class.html', - 'dom/nodes/ParentNode-querySelectors-namespaces.html', - 'dom/nodes/svg-template-querySelector.html', -] as const; - -const xmlRequirements = ['document.xml', 'document.parsing'] as const; - -function html(fixture: DOMFixture, markup: string): Document { - const document = fixture.document as unknown as Document; - const container = document.createElement('div'); - container.innerHTML = markup; - for (const child of Array.from(container.children)) - fixture.connect(child as unknown as DOMElementContract); - return document; -} - -function xml( - fixture: DOMFixture, - options: { markup: string; type?: DOMParserSupportedType }, -): Document { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - return new DOMParser().parseFromString(options.markup, options.type ?? 'application/xml'); -} - -function xhtml(fixture: DOMFixture, markup: string): Document { - return xml(fixture, { - markup: `${markup}`, - type: 'application/xhtml+xml', - }); -} - -const propertiesMarkup = ` - - -`; - -const tagCaseMarkup = ` -
      -
      -

      Paragraph 1

      -

      Paragraph 2

      -
      -
      -
      `; - -const soapMarkup = ` - - - 1234554321 - ManagedCustomerService - get - 1 - 115 - - - - - - 2 - ManagedCustomerPage - - Test1 - 1234566789 - - - Test2 - 987654321 - - - - - `; - -/** Registers standard-API assertions from the pinned namespace, XML, and case regressions. */ -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}`, - (fixture) => { - const document = fixture.document as unknown as Document; - const element = document.createElementNS(namespace, name); - expect(element[method](selector)).toBe(true); - }, - ['element.namespaces'], - ); - } - } - for (const method of ['querySelector', 'querySelectorAll'] as const) { - context.test(`WPT ParentNode-querySelector-case-insensitive.html: ${method}`, (fixture) => { - const document = html(fixture, ''); - const input = document.getElementById('testInput'); - if (method === 'querySelector') - expect(document.querySelector('input[name*=user i]')).toBe(input); - else expect(Array.from(document.querySelectorAll('input[name*=user i]'))).toEqual([input]); - }); - } - context.test( - 'WPT ParentNode-querySelector-multiple-attributes-xml.xhtml: querySelector with multiple attribute selectors in XML', - (fixture) => { - const document = xhtml( - fixture, - ` - - - - - - - - - `, - ); - const target = document.getElementById('targetFilter'); - expect( - document.querySelector( - 'mlt > tractor[id="tractor0"] > filter[mlt_service="greyscale"][track="0"]', - ), - ).toBe(target); - }, - xmlRequirements, - ); - for (const { selector, title, expected } of [ - { selector: 'coreProperties', title: 'match selector without prefix', expected: 'target' }, - { - selector: '*|coreProperties', - title: 'match namespace selector with universal prefix', - expected: 'target', - }, - { - selector: 'cp|coreProperties', - title: 'throws on specified namespace selector', - expected: 'throw', - }, - { - selector: ':is(cp|coreProperties)', - title: 'does not throw on specified namespace selector if forgiven', - expected: 'null', - }, - { - selector: '[lang]', - title: 'does not match attriubte selector without prefix', - expected: 'null', - }, - { - selector: '[*|lang]', - title: 'match namespace attriubte selector with universal prefix', - expected: 'target', - }, - { - selector: '[xml|lang]', - title: 'throws on specified namespace attriubte selector', - expected: 'throw', - }, - { - selector: ':is([xml|lang])', - title: 'does not throw on specified namespace attriubte selector if forgiven', - expected: 'null', - }, - ]) { - context.test( - `WPT ParentNode-querySelector-namespace-tagname-attribute.html: ${title}`, - (fixture) => { - const document = xml(fixture, { markup: propertiesMarkup }); - if (expected === 'throw') - expect(() => document.querySelector(selector)).toThrow( - expect.objectContaining({ name: 'SyntaxError' }), - ); - else - expect(document.querySelector(selector)).toBe( - expected === 'null' ? null : document.getElementById('target'), - ); - }, - xmlRequirements, - ); - } - context.test( - 'WPT ParentNode-querySelector-scope-numeric-id.xhtml: querySelector with :scope on element with numeric ID', - (fixture) => { - const document = xhtml(fixture, '
      '); - const scope = document.getElementById('9a'); - expect(scope).not.toBeNull(); - const result = scope!.querySelector(':scope > b'); - expect(result).not.toBeNull(); - expect(result!.id).toBe('target_b'); - }, - xmlRequirements, - ); - context.test( - 'WPT ParentNode-querySelector-svg-not-root.html: svg:not(:root) in a HTML document', - (fixture) => { - const document = html( - fixture, - ` - - - -`, - ); - const svg = document.getElementById('the-svg'); - expect(svg).not.toBeNull(); - expect(document.querySelector('svg:not(:root)')).toBe(svg); - }, - ['parsing.namespaces'], - ); - context.test( - 'WPT ParentNode-querySelector-tag-case.html: The same element should be found with both lowercase and uppercase div selectors', - (fixture) => { - const document = html(fixture, tagCaseMarkup); - const box = document.getElementById('container')!.firstElementChild; - const lower = document.querySelector('div.box'), - upper = document.querySelector('DIV.box'); - expect(lower).not.toBeNull(); - expect(upper).not.toBeNull(); - expect(lower).toBe(upper); - expect(lower).toBe(box); - }, - ); - context.test( - 'WPT ParentNode-querySelector-tag-case.html: The same paragraph should be found with both lowercase and uppercase p selectors', - (fixture) => { - const document = html(fixture, tagCaseMarkup); - const lower = document.querySelector('#container p#p1'), - upper = document.querySelector('#container P#p1'); - expect(lower).not.toBeNull(); - expect(upper).not.toBeNull(); - expect(lower).toBe(upper); - expect(lower).toBe(document.getElementById('p1')); - }, - ); - context.test( - 'WPT ParentNode-querySelector-tag-case.html: The same NodeList should be returned for querySelectorAll with lowercase and uppercase p selectors', - (fixture) => { - const document = html(fixture, tagCaseMarkup); - const lower = Array.from(document.querySelectorAll('div.box p')), - upper = Array.from(document.querySelectorAll('div.box P')); - expect(lower).toEqual(upper); - expect(lower).toEqual([document.getElementById('p1'), document.getElementById('p2')]); - }, - ); - context.test( - 'WPT ParentNode-querySelector-tag-case.html: Mixed case selectors should work correctly', - (fixture) => { - const document = html(fixture, tagCaseMarkup); - expect(document.querySelectorAll('DIV.box p').length).toBe(2); - const mixed = document.querySelector('div#container DIV.box P#p2'); - expect(mixed).not.toBeNull(); - expect(mixed!.id).toBe('p2'); - }, - ); - context.test( - 'WPT ParentNode-querySelector-tag-case.html: Tag name case-insensitivity with child combinator (>) dynamically created', - (fixture) => { - const document = fixture.document as unknown as Document; - const div = document.createElement('div'), - ul = document.createElement('ul'), - li = document.createElement('li'); - ul.appendChild(li); - div.appendChild(ul); - expect(Array.from(div.querySelectorAll('ul'))).toEqual([ul]); - expect(Array.from(div.querySelectorAll('UL'))).toEqual([ul]); - expect(Array.from(div.querySelectorAll('ul > li'))).toEqual([li]); - expect(Array.from(div.querySelectorAll('UL > LI'))).toEqual([li]); - }, - ); - context.test( - 'WPT ParentNode-querySelector-tag-case.html: Case-insensitivity with non-standard tag names', - (fixture) => { - const document = html(fixture, tagCaseMarkup); - const container = document.getElementById('container-for-3750')!; - const custom = document.createElement('myElement'); - custom.textContent = 'Hello world'; - container.appendChild(custom); - expect(container.querySelector('myelement')).toBe(custom); - expect(container.querySelector('myElement')).toBe(custom); - }, - ); - context.test( - 'WPT ParentNode-querySelector-underscore-local-name.xhtml: Element local name with an underscore in querySelector', - (fixture) => { - const document = xhtml(fixture, '<_g>hey'); - expect(document.querySelector('data > _g > b')!.textContent).toBe('hey'); - }, - xmlRequirements, - ); - context.test( - 'WPT ParentNode-querySelector-xml-capitalization.window.js: Descendant combinator', - (fixture) => { - const document = xml(fixture, { markup: 'baz' }); - expect(document.querySelector('Foo')).toBe(document.documentElement); - expect(document.querySelector('bar')).toBe(document.documentElement.firstChild); - expect(document.querySelector('Foo bar')).toBe(document.documentElement.firstChild); - }, - xmlRequirements, - ); - context.test( - 'WPT ParentNode-querySelector-xml-capitalization.window.js: Descendant combinator with *', - (fixture) => { - const document = xml(fixture, { - markup: ` - - - - - - - - -`, - }); - const c = document.documentElement.children[0]!.firstElementChild, - e = document.documentElement.children[1]!.firstElementChild; - expect(document.querySelector('c')).not.toBeNull(); - expect(document.querySelector('aB *')).toBe(c); - expect(document.querySelector('e')).not.toBeNull(); - expect(document.querySelector('cd *')).toBe(e); - }, - xmlRequirements, - ); - context.test( - 'WPT ParentNode-querySelector-xml-capitalization.window.js: Child combinator', - (fixture) => { - const document = xml(fixture, { - markup: ` - - content - abc - -`, - type: 'text/xml', - }); - expect(document.querySelector('Test')).not.toBeNull(); - expect(document.querySelector('test')).toBeNull(); - expect(document.querySelector('my-tag')).not.toBeNull(); - expect(document.querySelector('Test>my-tag')).not.toBeNull(); - }, - xmlRequirements, - ); - context.test( - 'WPT ParentNode-querySelectorAll-namespace.html: querySelectorAll with namespaced XML elements', - (fixture) => { - const document = xml(fixture, { markup: soapMarkup, type: 'text/xml' }); - expect(document.querySelectorAll('rval > entries').length).toBe(2); - }, - xmlRequirements, - ); - context.test( - 'WPT ParentNode-querySelectorAll-svg-append.html: Sanity check with div', - (fixture) => { - const document = html(fixture, '
      \n

      foo

      \n
      '); - const div = document.getElementsByTagName('div')[0]!; - expect(div.querySelectorAll('div p').length).toBe(1); - }, - ['parsing.namespaces'], - ); - context.test( - 'WPT ParentNode-querySelectorAll-svg-append.html: Append child nodes to svg', - (fixture) => { - const document = html(fixture, '
      \n

      foo

      \n
      '); - const svg = document.getElementsByTagName('svg')[0]!; - const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); - svg.append(g); - const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); - g.append(circle); - expect(g.querySelectorAll('g circle').length).toBe(1); - expect(g.querySelectorAll('g>circle').length).toBe(1); - expect(g.querySelectorAll('circle').length).toBe(1); - }, - ['parsing.namespaces', 'element.namespaces'], - ); - context.test( - 'WPT ParentNode-querySelectorAll-svg-class.html: Selecting a svg element with a class selector', - (fixture) => { - const document = html(fixture, '
      \n
      \n\n'); - expect(document.querySelectorAll('svg.test').length).toBe(1); - }, - ['parsing.namespaces'], - ); - context.test( - 'WPT ParentNode-querySelectors-namespaces.html: querySelectorAll must work with namespace attribute selectors on SVG', - (fixture) => { - const document = html(fixture, ''); - const element = document.getElementById('thesvg'); - expect(document.querySelector('[*|href]')).toBe(element); - expect(Array.from(document.querySelectorAll('[*|href]'))).toEqual([element]); - }, - ['parsing.namespaces'], - ); - for (const { id, selector, nested, title } of [ - { - id: 'template1', - selector: 'div', - nested: false, - title: 'querySelector works on template contents fragments with HTML elements (sanity check)', - }, - { - id: 'template2', - selector: 'svg', - nested: false, - title: 'querySelector works on template contents fragments with SVG elements', - }, - { - id: 'template3', - selector: 'svg', - nested: true, - title: 'querySelector works on template contents fragments with nested SVG elements', - }, - ]) { - context.test( - `WPT svg-template-querySelector.html: ${title}`, - (fixture) => { - const document = html( - fixture, - '\n\n', - ); - const fragment = (document.getElementById(id) as HTMLTemplateElement).content; - const scope = nested ? (fragment.firstChild as Element) : fragment; - expect(scope.querySelector(selector)).not.toBeNull(); - }, - ['templates.parsing', 'parsing.namespaces'], - ); - } -} diff --git a/packages/web-platform-tests/src/compatibility/element/selectors/roots.ts b/packages/web-platform-tests/src/compatibility/element/selectors/roots.ts deleted file mode 100644 index d2e5523..0000000 --- a/packages/web-platform-tests/src/compatibility/element/selectors/roots.ts +++ /dev/null @@ -1,578 +0,0 @@ -// Adapted from the WPT files listed in sourceFiles at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright © web-platform-tests contributors. BSD-3-Clause; see ../../../../test/wpt/LICENSE. -// No individual authors are declared in these source files. -import type { CompatibilityContext, DOMFixture } from '../../context.ts'; -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; - -export const sourceFiles = [ - 'dom/nodes/DocumentFragment-querySelectorAll-after-modification.html', - 'dom/nodes/Element-querySelectorAll-context-element.html', - 'dom/nodes/ParentNode-querySelector-All-comma-selector.html', - 'dom/nodes/ParentNode-querySelector-All-scope.html', - 'dom/nodes/ParentNode-querySelector-argument-stringification.html', - 'dom/nodes/ParentNode-querySelector-attribute-change.html', - 'dom/nodes/ParentNode-querySelector-class-change.html', - 'dom/nodes/ParentNode-querySelector-dupe-id.html', - 'dom/nodes/ParentNode-querySelector-innerHTML.html', - 'dom/nodes/ParentNode-querySelector-orphan-scope.html', - 'dom/nodes/ParentNode-querySelector-parent-context.html', - 'dom/nodes/ParentNode-querySelector-scope-in-not.html', - 'dom/nodes/ParentNode-querySelector-scope.html', - 'dom/nodes/ParentNode-querySelector-host-pseudo-class.html', - 'dom/nodes/ParentNode-querySelector-shadowroot-nth-child.html', - 'dom/nodes/ParentNode-querySelectorAll-removed-elements.html', - 'dom/nodes/ParentNode-querySelectorAll-scope-change.html', - 'dom/nodes/ParentNode-querySelectorAll-scope-colon-id-class.html', - 'dom/nodes/ParentNode-querySelectors-exclusive.html', -] as const; - -function title(source: string, name: string): string { - return `WPT ${source}: ${name}`; -} - -function mount(fixture: DOMFixture, markup: string): HTMLElement { - const document = fixture.document as unknown as Document; - const container = document.createElement('main'); - container.innerHTML = markup; - fixture.connect(container as unknown as DOMElementContract); - return container; -} - -/** Registers every case in the listed pinned WPT selector files. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - title( - 'DocumentFragment-querySelectorAll-after-modification.html', - 'querySelectorAll should still work on DocumentFragments after they are modified', - ), - (fixture) => { - const document = fixture.document as unknown as 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([]); - }, - ['node.fragments'], - ); - - 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.document as unknown as Document; - 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, - ]); - }); - } - - context.test( - title('ParentNode-querySelector-All-comma-selector.html', 'Basic comma-separated selector'), - (fixture) => { - const document = fixture.document as unknown as Document; - mount(fixture, '

      Hello world

      Goodbye, world

      '); - expect(document.querySelectorAll('h1,h2').length).toBe(2); - }, - ); - context.test( - title( - 'ParentNode-querySelector-All-comma-selector.html', - 'Repeated identical querySelectorAll calls with repeated selectors should work on empty elements', - ), - (fixture) => { - const document = fixture.document as unknown as Document; - const el = document.createElement('div'); - const selector = '.foo, .foo, .bar'; - const results1 = el.querySelectorAll(selector); - expect(results1.length).toBe(0); - const results2 = el.querySelectorAll(selector); - expect(results2.length).toBe(0); - }, - ); - - const scopeMarkup = - '

      hello

      hey

      div
      ' + - '
      ' + - '
      • Alpha
      • Beta
        • Gamma
        • Delta
      '; - for (const name of [ - 'querySelector', - 'querySelectorAll', - 'querySelector comma separated', - 'querySelectorAll comma separated', - ]) { - context.test(title('ParentNode-querySelector-All-scope.html', name), (fixture) => { - const container = mount(fixture, scopeMarkup); - const div = container.firstElementChild!; - const [p, p2, div2] = Array.from(div.children); - if (name === 'querySelector') { - expect(div.querySelector(':scope > p')).toBe(p); - expect(div.querySelector(':scope > span')).toBeNull(); - } else if (name === 'querySelectorAll') { - expect(Array.from(div.querySelectorAll(':scope > p'))).toEqual([p, p2]); - expect(Array.from(div.querySelectorAll(':scope > span'))).toEqual([]); - } else if (name === 'querySelector comma separated') { - expect(div.querySelector(':scope > p, :scope > div')).toBe(p); - } else { - expect(Array.from(div.querySelectorAll(':scope > p, :scope > div'))).toEqual([p, p2, div2]); - } - }); - } - context.test( - title( - 'ParentNode-querySelector-All-scope.html', - 'querySelector with :scope > child should not match descendants', - ), - (fixture) => { - const scopeDiv = mount(fixture, scopeMarkup).children[1]!; - const result = scopeDiv.querySelector(':scope > span'); - expect(result, ':scope > span should not match descendant span').toBeNull(); - }, - ); - context.test( - title( - 'ParentNode-querySelector-All-scope.html', - 'querySelectorAll with :scope > child should only match direct children', - ), - (fixture) => { - const ul = mount(fixture, scopeMarkup).children[2]!; - const result = ul.querySelectorAll(':scope > li'); - const resultIDs = Array.from(result, (el) => el.id); - expect(resultIDs, ':scope > li should only match direct children').toEqual(['alpha', 'beta']); - }, - ); - context.test( - title( - 'ParentNode-querySelector-All-scope.html', - 'querySelectorAll with :scope in unattached svg document', - ), - (fixture) => { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const doc = new DOMParser().parseFromString( - '', - 'image/svg+xml', - ); - const svg = doc.documentElement; - const g = svg.children[1]!; - const result = svg.querySelectorAll('g'); - expect(Array.from(result), ':scope descend match descendants').toEqual([g]); - }, - ['document.parsing', 'document.xml'], - ); - context.test( - title( - 'ParentNode-querySelector-All-scope.html', - 'querySelectorAll with :scope in document where element moved', - ), - (fixture) => { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const xml = ''; - const doc = new DOMParser().parseFromString(xml, 'text/xml'); - const theBar = doc.documentElement.children[0]!; - const childBar = doc.documentElement.children[1]!; - const bars = Array.from(doc.querySelectorAll('bar')); - for (const bar of bars) { - if (!bar.parentElement) continue; - bar.parentElement.querySelector(':scope > child-bars')!.appendChild(bar); - } - expect(theBar.parentNode === childBar).toBe(true); - }, - ['document.parsing', 'document.xml', 'node.append'], - ); - - for (const name of [ - 'querySelector stringifies Array argument via toString()', - 'querySelectorAll stringifies Array argument via toString()', - 'querySelector stringifies object with custom toString()', - ]) { - context.test( - title('ParentNode-querySelector-argument-stringification.html', name), - (fixture) => { - const container = mount( - fixture, - '
      bolditalic 1italic 2

      paragraph

      ', - ).firstElementChild!; - if (name.startsWith('querySelectorAll')) { - const results = container.querySelectorAll(['strong', 'em'] as unknown as string); - expect( - results.length, - "Array.toString() produces 'strong,em' matching both element types", - ).toBe(3); - } else if (name.includes('Array')) { - const result = container.querySelector(['strong'] as unknown as string); - expect(result!.textContent).toBe('bold'); - } else { - const result = container.querySelector({ - toString() { - return 'p'; - }, - } as unknown as string); - expect(result!.textContent).toBe('paragraph'); - } - }, - ); - } - - const attributeMarkup = - '
      ' + - '
      ' + - '
      ' + - '
      ' + - '
      ' + - '
      ' + - '
      '; - for (const [index, attribute] of [ - 'id', - 'class', - 'hidden', - 'style', - 'readonly', - 'width', - 'height', - ].entries()) { - context.test( - title('ParentNode-querySelector-attribute-change.html', `${attribute} attribute`), - (fixture) => { - const div = mount(fixture, attributeMarkup).children[index]!; - const node1 = div.firstElementChild!, - node2 = div.lastElementChild!; - expect(div.querySelector(`[${attribute}]`), 'initial').toBe(node1); - expect(div.querySelector(`:not([${attribute}])`), 'initial not').toBe(node2); - node1.removeAttribute(attribute); - expect(div.querySelector(`[${attribute}]`), 'change attribute').toBeNull(); - expect(div.querySelector(`:not([${attribute}])`), 'change attribute not').toBe(node1); - }, - ); - } - - for (const [name, selector, isSVG, nextClass] of [ - ['svg simple class', '.a', true, 'b'], - ['svg compound class', 'g.a', true, 'b'], - ['svg complex class', 'svg g.a', true, 'b'], - ['html simple class', '.one', false, 'b'], - ['html compound class', 'span.one', false, 'two'], - ['html complex class', 'div span.one', false, 'two'], - ] as const) { - context.test( - title('ParentNode-querySelector-class-change.html', name), - (fixture) => { - const document = fixture.document as unknown as Document; - const container = mount( - fixture, - '
      ', - ); - const node = container.children[isSVG ? 0 : 1]!.firstElementChild!; - fixture.cleanup(() => { - if (isSVG) node.setAttributeNS(null, 'class', 'a'); - else node.setAttribute('class', 'one'); - }); - expect(document.querySelector(selector), 'initial').toBe(node); - if (isSVG) node.setAttributeNS(null, 'class', nextClass); - else node.setAttribute('class', nextClass); - expect( - document.querySelector(selector), - name === 'html simple class' ? 'querySelectorchange class' : 'change class', - ).toBeNull(); - }, - isSVG ? ['parsing.namespaces', 'element.namespaces'] : [], - ); - } - - for (const connected of [false, true]) { - context.test( - title( - 'ParentNode-querySelector-dupe-id.html', - connected - ? 'Returns a non-null value for connected cloned node with duplicated ID' - : 'Sanity: returns a non-null value', - ), - (fixture) => { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const html = '
      '; - const doc = new DOMParser().parseFromString(html, 'text/html'); - const root = doc.documentElement; - const test1 = doc.body.firstElementChild!; - const test1Clone = test1.cloneNode(true) as Element; - const expected = test1Clone.firstElementChild!; - if (connected) root.appendChild(test1Clone); - const test2 = test1Clone.querySelector('#test2'); - expect(test2).not.toBeNull(); - expect(test2).toBe(expected); - expect( - test1Clone.querySelector('[id=test2]'), - 'ID selector and attribute selector points to the same node', - ).toBe(expected); - if (!connected) - expect( - test1Clone.querySelectorAll('#test2')[0], - 'querySelector and querySelectorAll[0] points to the same node', - ).toBe(expected); - }, - ['document.parsing', 'node.cloning'], - ); - } - - context.test( - title( - 'ParentNode-querySelector-innerHTML.html', - 'querySelector matches nodes inserted by innerHTML', - ), - (fixture) => { - const document = fixture.document as unknown as Document; - const div = document.createElement('div'); - expect(div.innerHTML, 'initial').toBe(''); - div.innerHTML = '
      '; - const expectedDiv = div.firstElementChild!, - expectedSpan = expectedDiv.firstElementChild!; - expect(div.querySelector('div'), 'inner div').toBe(expectedDiv); - expect(div.querySelector('span'), 'inner span').toBe(expectedSpan); - }, - ); - - for (const self of [true, false]) { - context.test( - title( - 'ParentNode-querySelector-orphan-scope.html', - self - ? 'querySelectorAll on orphaned element does not return the element itself' - : 'querySelectorAll descendant selector works on orphaned elements', - ), - (fixture) => { - const document = fixture.document as unknown as Document; - const parent = document.createElement('div'); - parent.id = 'fuz'; - const child = document.createElement('span'); - child.id = 'buz'; - parent.appendChild(child); - if (self) - expect( - parent.querySelectorAll('#fuz').length, - 'should not return the base element itself', - ).toBe(0); - else { - const results = parent.querySelectorAll('#fuz #buz'); - expect(results.length).toBe(1); - expect(results[0]).toBe(child); - } - }, - ); - } - - context.test( - title( - 'ParentNode-querySelector-parent-context.html', - 'querySelector with ancestor context in selector', - ), - (fixture) => { - const container = mount(fixture, '
      ').firstElementChild!; - container.innerHTML = '
      '; - const section = container.firstElementChild!.firstElementChild!; - const expectedSpan = section.firstElementChild!; - expect(section.querySelector('div section span')).toBe(expectedSpan); - }, - ); - - context.test( - title( - 'ParentNode-querySelector-scope-in-not.html', - ':scope nested in :not should select one element per [data-main] scope', - ), - (fixture) => { - const document = fixture.document as unknown as Document; - mount( - fixture, - '
      ', - ); - const mainAttribute = 'data-main', - itemAttribute = 'data-item', - sideAttribute = 'data-side'; - const result = []; - const foundElements = document.querySelectorAll(`[${mainAttribute}]`); - for (const element of foundElements) result.push(element); - for (const element of result) { - const selector = `:scope [${sideAttribute}]:not(:scope [${itemAttribute}] [${itemAttribute}] [${sideAttribute}])`; - const results = element.querySelectorAll(selector); - expect(results.length).toBe(1); - } - }, - ); - - for (const name of [ - 'querySelector with :scope', - 'querySelector with id and sibling', - 'querySelectorAll with :scope', - 'querySelectorAll with id and sibling', - ]) { - context.test(title('ParentNode-querySelector-scope.html', name), (fixture) => { - const div = mount( - fixture, - '

      hello

      ', - ).firstElementChild!; - const p = div.children[1]!; - if (name === 'querySelector with :scope') { - expect(div.querySelector(':scope > p')).toBe(p); - expect(div.querySelector(':scope > span')).toBeNull(); - } else if (name === 'querySelector with id and sibling') { - expect(div.querySelector('#test + p')).toBe(p); - expect(p.querySelector('#test + p')).toBeNull(); - } else if (name === 'querySelectorAll with :scope') { - expect(Array.from(div.querySelectorAll(':scope > p'))).toEqual([p]); - expect(Array.from(div.querySelectorAll(':scope > span'))).toEqual([]); - } else { - expect(Array.from(div.querySelectorAll('#test + p'))).toEqual([p]); - expect(Array.from(p.querySelectorAll('#test + p'))).toEqual([]); - } - }); - } - - context.test( - title( - 'ParentNode-querySelector-host-pseudo-class.html', - 'Using :host pseudo-class selector in shadow DOM', - ), - (fixture) => { - const document = fixture.document as unknown as Document; - const section = document.createElement('section'); - section.id = 'test-container'; - fixture.connect(section as unknown as DOMElementContract); - const article = document.createElement('article'); - const shadowSection = section.attachShadow({ mode: 'open' }); - shadowSection.appendChild(article); - const normalQuery = shadowSection.querySelector('article'); - expect(normalQuery, 'Simple querySelector for article should find the element').toBe(article); - const hostQuery = shadowSection.querySelector(':host > article'); - expect(hostQuery, 'querySelector with :host pseudo-class should find the element').toBe( - article, - ); - }, - ['shadow.roots.attachment'], - ); - - context.test( - title( - 'ParentNode-querySelector-shadowroot-nth-child.html', - 'div:nth-child(2) should select #first when preceded by a style inside shadow root', - ), - (fixture) => { - const container = mount( - fixture, - '
      ', - ); - const hostElement = container.children[1]!; - const shadowRoot = hostElement.attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = - '
      '; - const el = shadowRoot.querySelector('div:nth-child(2)'); - expect(el!.id).toBe('first'); - }, - ['shadow.roots.attachment'], - ); - - context.test( - title( - 'ParentNode-querySelectorAll-removed-elements.html', - 'querySelectorAll must not return removed elements', - ), - (fixture) => { - const container = mount(fixture, '
      ').firstElementChild!; - const getIDs = (): string[] => [...container.querySelectorAll('a.test')].map((el) => el.id); - container.innerHTML = 'a link'; - expect(getIDs(), 'Sanity check: initial setup').toEqual(['link-a']); - container.innerHTML = ''; - expect(getIDs(), 'After replacement').toEqual(['link-b']); - container.innerHTML = 'a link'; - expect(getIDs(), 'After changing back to the original HTML').toEqual(['link-a']); - }, - ); - - context.test( - title('ParentNode-querySelectorAll-scope-change.html', 'Change the scope of querySelectorAll'), - (fixture) => { - const DOMParser = fixture.globals('DOMParser') as typeof globalThis.DOMParser; - const data = - 'PayloadTypeConfigurationPayloadContentFullScreen'; - const xmlDocument = new DOMParser().parseFromString(data, 'application/xml'); - const expectedRoot = xmlDocument.documentElement.firstElementChild!; - const expectedTarget1 = expectedRoot.children[0]!; - const expectedTarget2 = expectedRoot.children[3]!.children[0]!.children[0]!; - const rootCollection = xmlDocument.querySelector(':root > dict')!; - const keysInRootCollection = rootCollection.querySelectorAll(':scope > key'); - expect(keysInRootCollection.item(0), 'first item').toBe(expectedTarget1); - const subCollection = rootCollection.children[3]!.children[0]!; - const keysInSubCollection = subCollection.querySelectorAll(':scope > key'); - const clonedSubCollection = subCollection.cloneNode(true) as Element; - const keysInClonedSubCollection = clonedSubCollection.querySelectorAll(':scope > key'); - expect(subCollection.outerHTML, 'outerHTML').toBe(clonedSubCollection.outerHTML); - expect(keysInSubCollection.length, 'length').toBe(keysInClonedSubCollection.length); - expect(keysInSubCollection.item(0), 'second item').toBe(expectedTarget2); - }, - ['document.parsing', 'document.xml', 'node.cloning', 'parsing.serialization'], - ); - - context.test( - title( - 'ParentNode-querySelectorAll-scope-colon-id-class.html', - ':scope child selector on element with colon-prefixed class', - ), - (fixture) => { - const container = mount( - fixture, - '
      ', - ); - const div = container.firstElementChild!; - const expectedSpan = div.firstElementChild!; - const result = div.querySelectorAll(':scope > span'); - expect(Array.from(result)).toEqual([expectedSpan]); - }, - ); - context.test( - title( - 'ParentNode-querySelectorAll-scope-colon-id-class.html', - ':scope selector on element with colon-prefixed ID', - ), - (fixture) => { - const container = mount( - fixture, - '
      ', - ); - const div = container.children[1]!; - const result = div.querySelector(':scope'); - expect(result).toBeNull(); - }, - ); - - context.test( - title( - 'ParentNode-querySelectors-exclusive.html', - 'querySelector/querySelectorAll should not include their thisArg', - ), - (fixture) => { - const document = fixture.document as unknown as Document; - const button = document.createElement('button'); - for (const stage of ['before modification', 'after modification']) { - if (stage === 'after modification') button.innerHTML = 'text'; - for (const selector of ['*', 'button', 'button, span']) { - expect( - button.querySelector(selector), - `querySelector, '${selector}', ${stage}`, - ).toBeNull(); - } - for (const selector of ['*', 'button', 'button, span']) { - expect( - Array.from(button.querySelectorAll(selector)), - `querySelectorAll, '${selector}', ${stage}`, - ).toEqual([]); - } - } - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/button/activation.ts b/packages/web-platform-tests/src/compatibility/elements/button/activation.ts deleted file mode 100644 index 1eebfcf..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/button/activation.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element'; -export const requires = ['elements.form.submission', 'events.dispatch.cancellation'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('click submits the owning form and disabled suppresses activation', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const button = document.createElement('button'); - form.appendChild(button); - fixture.connect(form); - let submissions = 0; - form.addEventListener('submit', (event) => { - event.preventDefault(); - submissions++; - }); - button.click(); - expect(submissions).toBe(1); - button.disabled = true; - button.click(); - expect(submissions).toBe(1); - }); - - context.test('canceled click prevents submit activation', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - button = document.createElement('button'); - form.append(button); - fixture.connect(form as unknown as DOMElementContract); - let submits = 0; - form.addEventListener('submit', (event) => { - event.preventDefault(); - submits++; - }); - button.addEventListener('click', (event) => event.preventDefault()); - button.click(); - expect(submits).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/button/attributes.ts b/packages/web-platform-tests/src/compatibility/elements/button/attributes.ts deleted file mode 100644 index 24ffc28..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/button/attributes.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('reflects value, name, and disabled attributes', (fixture) => { - const document = fixture.document; - const button = document.createElement('button'); - expect(button.type).toBe('submit'); - button.type = 'reset'; - button.name = 'action'; - button.value = 'save'; - button.disabled = true; - expect(button.getAttribute('type')).toBe('reset'); - expect(button.getAttribute('name')).toBe('action'); - expect(button.getAttribute('value')).toBe('save'); - expect(button.hasAttribute('disabled')).toBe(true); - button.removeAttribute('disabled'); - expect(button.disabled).toBe(false); - }); - - context.test('normalizes invalid type and reflects disabled presence', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('button'); - expect(e.type).toBe('submit'); - e.setAttribute('type', 'invalid'); - expect(e.type).toBe('submit'); - e.type = 'reset'; - expect(e.getAttribute('type')).toBe('reset'); - e.setAttribute('disabled', 'false'); - expect(e.disabled).toBe(true); - e.disabled = false; - expect(e.hasAttribute('disabled')).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/button/form-association.ts b/packages/web-platform-tests/src/compatibility/elements/button/form-association.ts deleted file mode 100644 index 8d54169..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/button/form-association.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'resolves an explicit form owner and falls back after attribute removal', - (fixture) => { - const document = fixture.document; - const root = document.createElement('div'); - const first = document.createElement('form'); - const second = document.createElement('form'); - first.id = 'first-form'; - const button = document.createElement('button'); - button.setAttribute('form', first.id); - second.appendChild(button); - root.append(first, second); - fixture.connect(root); - expect(button.form).toBe(first); - button.removeAttribute('form'); - expect(button.form).toBe(second); - }, - ); - - context.test('updates form ownership on moving between forms', (fixture) => { - const document = fixture.document as unknown as Document; - const a = document.createElement('form'), - b = document.createElement('form'), - e = document.createElement('button'); - expect(e.form).toBeNull(); - a.append(e); - expect(e.form).toBe(a); - b.append(e); - expect(e.form).toBe(b); - e.remove(); - expect(e.form).toBeNull(); - }); - - context.test('an empty explicit form attribute prevents ancestor ownership', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('button'); - form.append(e); - fixture.connect(form as unknown as DOMElementContract); - e.setAttribute('form', ''); - expect(e.form).toBeNull(); - e.removeAttribute('form'); - expect(e.form).toBe(form); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/datalist.ts b/packages/web-platform-tests/src/compatibility/elements/datalist.ts deleted file mode 100644 index ad74de4..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/datalist.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#the-datalist-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('exposes a live collection of descendant options', (fixture) => { - const document = fixture.document; - const datalist = document.createElement('datalist'); - const options = datalist.options; - const option = document.createElement('option'); - const wrapper = document.createElement('div'); - wrapper.appendChild(option); - datalist.appendChild(wrapper); - expect(datalist.options).toBe(options); - expect(Array.from(options)).toEqual([option]); - option.remove(); - expect(options.length).toBe(0); - }); - - context.test('keeps option collection live and excludes non-option descendants', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('datalist'), - options = e.options, - a = document.createElement('option'), - div = document.createElement('div'); - div.append(a); - e.append(div); - expect(options.length).toBe(1); - expect(options.item(0)).toBe(a); - e.append(document.createElement('span')); - expect(options.length).toBe(1); - a.remove(); - expect(options.length).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/details/exclusive-groups.ts b/packages/web-platform-tests/src/compatibility/elements/details/exclusive-groups.ts deleted file mode 100644 index d24e515..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/details/exclusive-groups.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/interactive-elements.html#details-name'; -export const requires = ['elements.details.open'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'keeps at most one details element open per nonempty named tree group', - (fixture) => { - const document = fixture.document; - const root = document.createElement('div'); - const first = document.createElement('details'); - const second = document.createElement('details'); - first.name = second.name = 'group'; - root.append(first, second); - first.open = true; - second.open = true; - expect(first.open).toBe(false); - expect(second.open).toBe(true); - first.name = 'different'; - first.open = true; - expect(second.open).toBe(true); - }, - ); - - context.test('keeps differently named groups and empty names independent', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('details'), - b = document.createElement('details'); - a.name = 'a'; - b.name = 'b'; - p.append(a, b); - fixture.connect(p as unknown as DOMElementContract); - a.open = b.open = true; - expect(a.open).toBe(true); - expect(b.open).toBe(true); - a.name = b.name = ''; - expect(a.open).toBe(true); - expect(b.open).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/details/open.ts b/packages/web-platform-tests/src/compatibility/elements/details/open.ts deleted file mode 100644 index 7bc5af1..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/details/open.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#the-details-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('reflects open as a boolean attribute', (fixture) => { - const document = fixture.document; - const details = document.createElement('details'); - expect(details.open).toBe(false); - details.open = true; - expect(details.hasAttribute('open')).toBe(true); - details.setAttribute('open', 'false'); - expect(details.open).toBe(true); - details.open = false; - expect(details.hasAttribute('open')).toBe(false); - }); - - context.test('reflects open presence rather than attribute spelling', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('details'); - expect(e.open).toBe(false); - e.setAttribute('open', 'false'); - expect(e.open).toBe(true); - e.open = false; - expect(e.hasAttribute('open')).toBe(false); - e.open = true; - expect(e.getAttribute('open')).toBe(''); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/details/toggle-events.ts b/packages/web-platform-tests/src/compatibility/elements/details/toggle-events.ts deleted file mode 100644 index 76557b8..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/details/toggle-events.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { DOMEvent, DOMToggleEvent } from '../../contracts/events.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#details-toggle-task-tracker'; -export const requires = ['elements.details.open'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('queues and coalesces toggle events', async (fixture) => { - const document = fixture.document; - const details = document.createElement('details'); - let calls = 0; - const delivered = new Promise((resolve) => { - details.addEventListener('toggle', (event) => { - calls++; - resolve(event); - }); - }); - details.open = true; - details.open = false; - expect(calls).toBe(0); - const event = (await delivered) as DOMToggleEvent; - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - // The first uncoalesced event would end in "open". Checking the merged - // states catches that error without guessing how long the task queue takes. - expect(event.oldState).toBe('closed'); - expect(event.newState).toBe('closed'); - expect(calls).toBe(1); - }); - - context.test('queues and coalesces toggle events across synchronous changes', async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('details'); - fixture.connect(e as unknown as DOMElementContract); - const seen: unknown[] = []; - e.addEventListener('toggle', (event) => { - const toggle = event as ToggleEvent; - seen.push([toggle.oldState, toggle.newState, event.bubbles, event.cancelable]); - }); - e.open = true; - e.open = false; - e.open = true; - expect(seen).toEqual([]); - await new Promise((resolve) => setTimeout(resolve, 30)); - expect(seen).toEqual([['closed', 'open', false, false]]); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/dialog/close.ts b/packages/web-platform-tests/src/compatibility/elements/dialog/close.ts deleted file mode 100644 index c82ade6..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/dialog/close.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { DOMEvent } from '../../contracts/events.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element'; -export const requires = ['elements.dialog.show'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('sets the return value and queues a noncancelable close event', async (fixture) => { - const document = fixture.document; - const dialog = document.createElement('dialog'); - fixture.connect(dialog); - dialog.show(); - let closed = false; - const delivered = new Promise((resolve) => { - dialog.addEventListener('close', (event) => { - closed = true; - resolve(event); - }); - }); - dialog.close('accepted'); - expect(dialog.open).toBe(false); - expect(dialog.returnValue).toBe('accepted'); - expect(closed).toBe(false); - const event = await delivered; - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - }); - - context.test('close updates returnValue and does nothing when already closed', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('dialog'); - fixture.connect(e as unknown as DOMElementContract); - expect(e.returnValue).toBe(''); - e.show(); - e.close('result'); - expect(e.open).toBe(false); - expect(e.returnValue).toBe('result'); - e.close('ignored'); - expect(e.returnValue).toBe('result'); - }); - - context.test('close event is asynchronous and noncancelable', async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('dialog'); - fixture.connect(e as unknown as DOMElementContract); - const seen: unknown[] = []; - e.addEventListener('close', (event) => seen.push([e.open, event.bubbles, event.cancelable])); - e.show(); - e.close(); - expect(seen).toEqual([]); - await new Promise((resolve) => setTimeout(resolve, 40)); - expect(seen).toEqual([[false, false, false]]); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/dialog/focus.ts b/packages/web-platform-tests/src/compatibility/elements/dialog/focus.ts deleted file mode 100644 index 9c84579..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/dialog/focus.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-focusing-steps'; -export const requires = ['elements.dialog.modal', 'focus.transitions'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'focuses autofocus content and restores the previously focused control', - (fixture) => { - const document = fixture.document; - const root = document.createElement('div'); - const outside = document.createElement('button'); - const dialog = document.createElement('dialog'); - const preferred = document.createElement('button'); - preferred.setAttribute('autofocus', ''); - dialog.appendChild(preferred); - root.append(outside, dialog); - fixture.connect(root); - outside.focus(); - dialog.showModal(); - fixture.cleanup(() => dialog.close()); - expect(document.activeElement).toBe(preferred); - dialog.close(); - expect(document.activeElement).toBe(outside); - }, - ); - - context.test('showModal focuses an autofocus control inside the dialog', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('dialog'), - input = document.createElement('input'); - input.autofocus = true; - e.append(input); - fixture.connect(e as unknown as DOMElementContract); - e.showModal(); - expect(document.activeElement).toBe(input); - e.close(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/dialog/form-submission.ts b/packages/web-platform-tests/src/compatibility/elements/dialog/form-submission.ts deleted file mode 100644 index 8d96cc4..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/dialog/form-submission.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://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm'; -export const requires = [ - 'elements.dialog.show', - 'elements.form.submission', - 'elements.button.activation', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'dialog submission closes with the submitter value without navigating', - (fixture) => { - const document = fixture.document; - const dialog = document.createElement('dialog'); - const form = document.createElement('form'); - form.method = 'dialog'; - const button = document.createElement('button'); - button.value = 'accepted'; - form.appendChild(button); - dialog.appendChild(form); - fixture.connect(dialog); - dialog.show(); - button.click(); - expect(dialog.open).toBe(false); - expect(dialog.returnValue).toBe('accepted'); - }, - ); - - context.test('canceled dialog form submit leaves dialog open', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('dialog'), - form = document.createElement('form'), - button = document.createElement('button'); - form.method = 'dialog'; - form.append(button); - e.append(form); - fixture.connect(e as unknown as DOMElementContract); - form.addEventListener('submit', (event) => event.preventDefault()); - e.show(); - button.click(); - expect(e.open).toBe(true); - e.close(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/dialog/modal.ts b/packages/web-platform-tests/src/compatibility/elements/dialog/modal.ts deleted file mode 100644 index 45c218f..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/dialog/modal.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element'; -export const requires = ['elements.dialog.show', 'elements.dialog.close'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('requires connectivity and rejects switching an open nonmodal dialog', (fixture) => { - const document = fixture.document; - const dialog = document.createElement('dialog'); - expect(() => dialog.showModal()).toThrow( - expect.objectContaining({ name: 'InvalidStateError' }), - ); - fixture.connect(dialog); - dialog.show(); - expect(() => dialog.showModal()).toThrow( - expect.objectContaining({ name: 'InvalidStateError' }), - ); - dialog.close(); - dialog.showModal(); - fixture.cleanup(() => dialog.close()); - dialog.showModal(); - expect(dialog.open).toBe(true); - expect(() => dialog.show()).toThrow(expect.objectContaining({ name: 'InvalidStateError' })); - }); - - context.test('rejects modal display on disconnected and already nonmodal dialogs', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('dialog'); - expect(() => e.showModal()).toThrow(expect.objectContaining({ name: 'InvalidStateError' })); - fixture.connect(e as unknown as DOMElementContract); - e.show(); - expect(() => e.showModal()).toThrow(expect.objectContaining({ name: 'InvalidStateError' })); - e.close(); - e.showModal(); - expect(e.open).toBe(true); - e.close(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/dialog/request-close.ts b/packages/web-platform-tests/src/compatibility/elements/dialog/request-close.ts deleted file mode 100644 index 53b7bc1..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/dialog/request-close.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { DOMEvent } from '../../contracts/events.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element'; -export const requires = [ - 'elements.dialog.show', - 'elements.dialog.close', - 'events.dispatch.cancellation', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('allows cancel listeners to veto a close request', (fixture) => { - const document = fixture.document; - const dialog = document.createElement('dialog'); - fixture.connect(dialog); - dialog.show(); - const cancel = (event: DOMEvent): void => { - event.preventDefault(); - }; - dialog.addEventListener('cancel', cancel); - dialog.requestClose('ignored'); - expect(dialog.open).toBe(true); - expect(dialog.returnValue).toBe(''); - dialog.removeEventListener('cancel', cancel); - dialog.requestClose('accepted'); - expect(dialog.open).toBe(false); - expect(dialog.returnValue).toBe('accepted'); - }); - - context.test('requestClose fires cancel before close and accepts a return value', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('dialog'); - fixture.connect(e as unknown as DOMElementContract); - const seen: unknown[] = []; - e.addEventListener('cancel', (event) => seen.push([e.open, event.cancelable])); - e.show(); - e.requestClose('result'); - expect(seen).toEqual([[true, true]]); - expect(e.open).toBe(false); - expect(e.returnValue).toBe('result'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/dialog/show.ts b/packages/web-platform-tests/src/compatibility/elements/dialog/show.ts deleted file mode 100644 index 46e67ad..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/dialog/show.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#the-dialog-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('show is idempotent and sets open', (fixture) => { - const document = fixture.document; - const dialog = document.createElement('dialog'); - fixture.connect(dialog); - expect(dialog.open).toBe(false); - dialog.show(); - expect(dialog.open).toBe(true); - dialog.show(); - expect(dialog.open).toBe(true); - }); - - context.test('show is idempotent for an already nonmodal dialog', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('dialog'); - fixture.connect(e as unknown as DOMElementContract); - e.show(); - expect(e.open).toBe(true); - e.show(); - expect(e.open).toBe(true); - e.close(); - expect(e.open).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/fieldset/controls.ts b/packages/web-platform-tests/src/compatibility/elements/fieldset/controls.ts deleted file mode 100644 index ba9f730..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/fieldset/controls.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#dom-fieldset-elements'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('collects listed descendants in a live collection', (fixture) => { - const document = fixture.document; - const fieldset = document.createElement('fieldset'); - const controls = fieldset.elements; - const input = document.createElement('input'); - const nested = document.createElement('fieldset'); - const button = document.createElement('button'); - nested.appendChild(button); - fieldset.append(input, nested); - expect(fieldset.elements).toBe(controls); - expect(Array.from(controls)).toEqual([input, nested, button]); - input.remove(); - expect(Array.from(controls)).toEqual([nested, button]); - }); - - context.test('exposes descendant controls as a live collection', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('fieldset'), - a = document.createElement('input'), - b = document.createElement('button'), - elements = e.elements; - e.append(a); - expect(elements.item(0)).toBe(a); - e.append(b); - expect(Array.from(elements)).toEqual([a, b]); - a.remove(); - expect(elements.length).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/fieldset/disabled.ts b/packages/web-platform-tests/src/compatibility/elements/fieldset/disabled.ts deleted file mode 100644 index 19bff5e..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/fieldset/disabled.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute'; -export const requires = ['elements.input.validation'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('exempts descendants of the first legend from disabled inheritance', (fixture) => { - const document = fixture.document; - const fieldset = document.createElement('fieldset'); - const legend = document.createElement('legend'); - const exempt = document.createElement('input'); - const blocked = document.createElement('input'); - legend.appendChild(exempt); - fieldset.append(legend, blocked); - fieldset.disabled = true; - expect(exempt.willValidate).toBe(true); - expect(blocked.willValidate).toBe(false); - expect(blocked.disabled).toBe(false); - fieldset.disabled = false; - expect(blocked.willValidate).toBe(true); - }); - - context.test( - 'exempts only descendants of the first legend from disabled validation', - (fixture) => { - const document = fixture.document as unknown as Document; - const f = document.createElement('fieldset'), - a = document.createElement('legend'), - b = document.createElement('legend'), - x = document.createElement('input'), - y = document.createElement('input'), - z = document.createElement('input'); - a.append(x); - b.append(y); - f.append(a, b, z); - f.disabled = true; - expect(x.willValidate).toBe(true); - expect(y.willValidate).toBe(false); - expect(z.willValidate).toBe(false); - expect(z.disabled).toBe(false); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/form/controls.ts b/packages/web-platform-tests/src/compatibility/elements/form/controls.ts deleted file mode 100644 index 0e4455e..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/form/controls.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/forms.html#dom-form-elements'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('keeps the collection live and in tree order', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const controls = form.elements; - const first = document.createElement('input'); - const second = document.createElement('button'); - form.append(first, second); - expect(form.elements).toBe(controls); - expect(Array.from(controls)).toEqual([first, second]); - expect(form.length).toBe(2); - first.remove(); - expect(Array.from(controls)).toEqual([second]); - }); - - context.test('retains live controls across insertion and removal in tree order', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - a = document.createElement('input'), - b = document.createElement('button'), - elements = form.elements; - form.append(a, b); - expect(Array.from(elements)).toEqual([a, b]); - form.insertBefore(b, a); - expect(Array.from(elements)).toEqual([b, a]); - a.remove(); - expect(elements.length).toBe(1); - expect(elements.item(0)).toBe(b); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/form/data-events.ts b/packages/web-platform-tests/src/compatibility/elements/form/data-events.ts deleted file mode 100644 index 3cf55dc..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/form/data-events.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-entry-list'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/html/semantics/forms/form-submission-0/constructing-form-data-set.html'; -export const globals = ['FormData'] as const; -export const requires = [ - 'elements.form.formData', - 'events.dispatch.capture', - 'events.dispatch.bubbling', - 'events.dispatch.cancellation', -] 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 form entry-list construction and formdata event interactions. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - for (const connected of [true, false]) { - context.test( - `formdata bubbles synchronously through ${connected ? 'connected' : 'detached'} ancestors without cancellation`, - (fixture) => { - const document = fixture.document as unknown as Document; - const Data = fixture.globals('FormData') as unknown as typeof FormData; - const parent = document.createElement('div'), - form = document.createElement('form'); - parent.append(form); - if (connected) fixture.connect(parent as unknown as DOMElementContract); - const seen: unknown[] = []; - parent.addEventListener( - 'formdata', - (event) => { - event.preventDefault(); - seen.push([ - 'capture', - event.target, - event.eventPhase, - event.bubbles, - event.cancelable, - event.defaultPrevented, - ]); - }, - true, - ); - form.addEventListener('formdata', (event) => seen.push(['target', event.eventPhase])); - parent.addEventListener('formdata', (event) => seen.push(['bubble', event.eventPhase])); - const data = new Data(form); - seen.push('returned'); - expect([...data]).toEqual([]); - expect(seen).toEqual([ - ['capture', form, 1, true, false, false], - ['target', 2], - ['bubble', 3], - 'returned', - ]); - }, - ); - } - context.test('shares listener mutations in order with the constructed data', (fixture) => { - const document = fixture.document as unknown as Document; - const Data = fixture.globals('FormData') as unknown as typeof FormData; - const form = document.createElement('form'), - input = document.createElement('input'); - input.name = 'first'; - input.value = 'original'; - form.append(input); - const seen: unknown[] = []; - let eventData: FormData | undefined; - form.addEventListener('formdata', (event) => { - eventData = event.formData; - seen.push([...event.formData]); - event.formData.append('extra', 'one'); - event.formData.append('extra', 'two'); - }); - form.addEventListener('formdata', (event) => { - seen.push([...event.formData]); - event.formData.set('first', 'updated'); - }); - const data = new Data(form); - expect(seen).toEqual([ - [['first', 'original']], - [ - ['first', 'original'], - ['extra', 'one'], - ['extra', 'two'], - ], - ]); - expect([...data]).toEqual([ - ['first', 'updated'], - ['extra', 'one'], - ['extra', 'two'], - ]); - expect([...eventData!]).toEqual([...data]); - expect(input.value).toBe('original'); - }); - context.test('preserves newlines in event-added entry names and values', (fixture) => { - const document = fixture.document as unknown as Document; - const Data = fixture.globals('FormData') as unknown as typeof FormData; - const form = document.createElement('form'); - form.addEventListener('formdata', (event) => event.formData.append('a\nb', 'c\rd\r\ne')); - expect([...new Data(form)]).toEqual([['a\nb', 'c\rd\r\ne']]); - }); - context.test('constructs entries before event listeners mutate the controls', (fixture) => { - const document = fixture.document as unknown as Document; - const Data = fixture.globals('FormData') as unknown as typeof FormData; - const form = document.createElement('form'), - input = document.createElement('input'); - input.name = 'value'; - input.value = 'before'; - form.append(input); - form.addEventListener('formdata', () => { - input.value = 'after'; - }); - expect(new Data(form).get('value')).toBe('before'); - expect(new Data(form).get('value')).toBe('after'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/form/form-data.ts b/packages/web-platform-tests/src/compatibility/elements/form/form-data.ts deleted file mode 100644 index 233e617..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/form/form-data.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { DOMHTMLElement as DOMHTMLElementContract } from '../../contracts/nodes.ts'; -import type { DOMFormElement as DOMFormElementContract } from '../../contracts/elements.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-the-form-data-set'; -const requiredGlobals = ['FormData'] as const; -export { requiredGlobals as globals }; -export const requires = ['elements.input.value', 'elements.input.checkedness'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('collects successful named controls in tree order', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const input = document.createElement('input'); - input.name = 'answer'; - input.value = '42'; - const disabled = document.createElement('input'); - disabled.name = 'excluded'; - disabled.disabled = true; - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.name = 'agreed'; - form.append(input, disabled, checkbox); - const FormData = fixture.globals('FormData'); - const initial = new FormData(form); - expect(Array.from(initial.entries())).toEqual([['answer', '42']]); - checkbox.checked = true; - const checked = new FormData(form); - expect(checked.get('agreed')).toBe('on'); - }); - - context.test('serializes only successful named controls in tree order', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'); - form.innerHTML = - ''; - const FormData = fixture.globals('FormData'); - const data = new FormData(form as unknown as DOMFormElementContract); - expect([...data.entries()]).toEqual([ - ['a', 'one'], - ['a', 'two'], - ['d', 'ok'], - ]); - expect(data.getAll('a')).toEqual(['one', 'two']); - expect(data.get('missing')).toBeNull(); - }); - - context.test('includes only the selected submitter and rejects invalid submitters', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - button = document.createElement('button'), - foreign = document.createElement('button'); - button.name = 'go'; - button.value = 'yes'; - form.append(button); - const FormData = fixture.globals('FormData'); - expect([ - ...new FormData( - form as unknown as DOMFormElementContract, - button as unknown as DOMHTMLElementContract, - ).entries(), - ]).toEqual([['go', 'yes']]); - expect( - () => - new FormData( - form as unknown as DOMFormElementContract, - foreign as unknown as DOMHTMLElementContract, - ), - ).toThrow(expect.objectContaining({ name: 'NotFoundError' })); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/form/named-controls.ts b/packages/web-platform-tests/src/compatibility/elements/form/named-controls.ts deleted file mode 100644 index 87dc112..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/form/named-controls.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#htmlformcontrolscollection'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/tree/master/html/semantics/forms/the-form-element'; -export const requires = ['elements.form.controls'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('looks up controls through item and namedItem', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const input = document.createElement('input'); - input.id = 'control-id'; - input.name = 'control-name'; - form.appendChild(input); - const controls = form.elements; - expect(controls.item(0)).toBe(input); - expect(controls.item(1)).toBeNull(); - expect(controls.namedItem('control-id')).toBe(input); - expect(controls.namedItem('control-name')).toBe(input); - expect(controls.namedItem('missing')).toBeNull(); - }); - - context.test('resolves id and name changes in a retained controls collection', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('input'), - elements = form.elements; - form.append(e); - e.name = 'first'; - expect(elements.namedItem('first')).toBe(e); - e.name = 'second'; - expect(elements.namedItem('first')).toBeNull(); - expect(elements.namedItem('second')).toBe(e); - e.id = 'identifier'; - expect(elements.namedItem('identifier')).toBe(e); - }); - - context.test('returns a RadioNodeList for repeated named controls', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'); - form.innerHTML = ''; - const named = form.elements.namedItem('x') as RadioNodeList; - expect(named.length).toBe(2); - expect(named.value).toBe('b'); - named.value = 'a'; - expect((form.elements[0] as HTMLInputElement).checked).toBe(true); - expect((form.elements[1] as HTMLInputElement).checked).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/form/reset.ts b/packages/web-platform-tests/src/compatibility/elements/form/reset.ts deleted file mode 100644 index f85e718..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/form/reset.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { DOMEvent } from '../../contracts/events.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#resetting-a-form'; -export const requires = ['elements.input.value', 'events.dispatch.cancellation'] as const; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/html/semantics/forms/resetting-a-form/reset-form.html'; - -// Select/reset-activation cases adapted from WPT at -// b719612f963a05ece9c89a7dd7598c1c59618874. Copyright © web-platform-tests -// contributors. Original author: Intel. BSD-3-Clause; -// see ../../../../test/wpt/LICENSE and ../../../../docs/wpt-css-forms.md. - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('restores control defaults unless the reset event is canceled', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const input = document.createElement('input'); - input.defaultValue = 'original'; - input.value = 'edited'; - form.appendChild(input); - const cancel = (event: DOMEvent): void => { - event.preventDefault(); - }; - form.addEventListener('reset', cancel); - form.reset(); - expect(input.value).toBe('edited'); - form.removeEventListener('reset', cancel); - form.reset(); - expect(input.value).toBe('original'); - input.defaultValue = 'new default'; - expect(input.value).toBe('new default'); - }); - - context.test('cancellation leaves dirty control state untouched', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('input'); - e.defaultValue = 'default'; - e.value = 'dirty'; - form.append(e); - let target: unknown; - form.addEventListener('reset', (event) => { - target = event.target; - event.preventDefault(); - }); - form.reset(); - expect(target).toBe(form); - expect(e.value).toBe('dirty'); - }); - - context.test('reset restores text, checkbox and textarea defaults together', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('input'), - c = document.createElement('input'), - t = document.createElement('textarea'); - c.type = 'checkbox'; - e.defaultValue = 'a'; - c.defaultChecked = true; - t.defaultValue = 'b'; - form.append(e, c, t); - e.value = 'dirty'; - c.checked = false; - t.value = 'dirty'; - form.reset(); - expect(e.value).toBe('a'); - expect(c.checked).toBe(true); - expect(t.value).toBe('b'); - }); - - for (const activation of ['method', 'button', 'input'] as const) { - context.test( - `restores single and multiple select defaults through reset ${activation}`, - (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'); - const selects = [false, false, true].map((multiple, index) => { - const select = document.createElement('select'); - select.multiple = multiple; - for (let i = 0; i < 3; i++) { - const option = document.createElement('option'); - option.value = String(i); - option.defaultSelected = index === 1 ? i === 1 : index === 2 && i > 0; - select.append(option); - } - form.append(select); - select.value = index === 0 ? '2' : '0'; - return select; - }); - const dirty = [['2'], ['0'], ['0']]; - const selected = (): string[][] => - selects.map((select) => Array.from(select.selectedOptions, (option) => option.value)); - expect(selected()).toEqual(dirty); - const statesDuringReset: string[][][] = []; - form.addEventListener('reset', () => statesDuringReset.push(selected())); - if (activation === 'method') form.reset(); - else { - const trigger = document.createElement(activation); - trigger.type = 'reset'; - form.append(trigger); - trigger.disabled = true; - trigger.click(); - expect(statesDuringReset).toEqual([]); - expect(selected()).toEqual(dirty); - trigger.disabled = false; - trigger.addEventListener('click', (event) => event.preventDefault(), { once: true }); - trigger.click(); - expect(statesDuringReset).toEqual([]); - expect(selected()).toEqual(dirty); - trigger.click(); - } - expect(statesDuringReset).toEqual([dirty]); - expect(selected()).toEqual([['0'], ['1'], ['1', '2']]); - selects[2]!.options[0]!.defaultSelected = true; - expect(selected()[2]).toEqual(['0', '1', '2']); - }, - activation === 'method' - ? ['elements.select.selection', 'elements.option.selectedness'] - : [ - 'elements.select.selection', - 'elements.option.selectedness', - 'events.listeners.once', - activation === 'button' ? 'elements.button.activation' : 'elements.input.activation', - ], - ); - } -} diff --git a/packages/web-platform-tests/src/compatibility/elements/form/submission.ts b/packages/web-platform-tests/src/compatibility/elements/form/submission.ts deleted file mode 100644 index dd1e1b1..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/form/submission.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { DOMEvent } from '../../contracts/events.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-form-requestsubmit'; -export const requires = ['events.dispatch.cancellation'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('requestSubmit emits a cancelable submit event', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const button = document.createElement('button'); - form.appendChild(button); - fixture.connect(form); - const submitted: DOMEvent[] = []; - form.addEventListener('submit', (event) => { - event.preventDefault(); - submitted.push(event); - }); - form.requestSubmit(button); - expect(submitted.length).toBe(1); - const event = submitted[0]; - if (!event) return expect.fail('Expected a submit event'); - expect(event.target).toBe(form); - expect(event.cancelable).toBe(true); - expect(event.bubbles).toBe(true); - const outsider = document.createElement('button'); - expect(() => form.requestSubmit(outsider)).toThrow( - expect.objectContaining({ name: 'NotFoundError' }), - ); - }); - - context.test('requestSubmit validates first and reports submitter after success', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('input'), - button = document.createElement('button'); - e.required = true; - form.append(e, button); - fixture.connect(form as unknown as DOMElementContract); - const seen: unknown[] = []; - e.addEventListener('invalid', () => seen.push('invalid')); - form.addEventListener('submit', (event) => { - event.preventDefault(); - seen.push((event as SubmitEvent).submitter); - }); - form.requestSubmit(button); - expect(seen).toEqual(['invalid']); - e.value = 'ok'; - form.requestSubmit(button); - expect(seen).toEqual(['invalid', button]); - }); - - context.test('rejects invalid requestSubmit arguments before firing events', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - foreign = document.createElement('button'), - button = document.createElement('button'); - button.type = 'button'; - form.append(button); - expect(() => form.requestSubmit(button)).toThrow( - expect.objectContaining({ name: 'TypeError' }), - ); - expect(() => form.requestSubmit(foreign)).toThrow( - expect.objectContaining({ name: 'NotFoundError' }), - ); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/form/validation.ts b/packages/web-platform-tests/src/compatibility/elements/form/validation.ts deleted file mode 100644 index 8032aa4..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/form/validation.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-constraint-validation-api'; -export const requires = ['elements.input.validation'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('checks each invalid candidate without stopping at the first', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const first = document.createElement('input'); - const second = document.createElement('input'); - first.required = second.required = true; - form.append(first, second); - const invalid: string[] = []; - first.addEventListener('invalid', () => invalid.push('first')); - second.addEventListener('invalid', () => invalid.push('second')); - expect(form.checkValidity()).toBe(false); - expect(invalid).toEqual(['first', 'second']); - first.value = second.value = 'ready'; - expect(form.checkValidity()).toBe(true); - }); - - context.test('dispatches nonbubbling invalid events on every invalid control', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - a = document.createElement('input'), - b = document.createElement('input'); - a.required = b.required = true; - form.append(a, b); - const seen: unknown[] = []; - form.addEventListener('invalid', () => seen.push('bubble')); - form.addEventListener( - 'invalid', - (event) => { - seen.push(event.target); - event.preventDefault(); - }, - true, - ); - expect(form.checkValidity()).toBe(false); - expect(seen).toEqual([a, b]); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/activation.ts b/packages/web-platform-tests/src/compatibility/elements/input/activation.ts deleted file mode 100644 index 0b65855..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/activation.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { DOMEvent } from '../../contracts/events.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/input.html#checkbox-state-(type=checkbox)'; -export const requires = ['elements.input.checkedness', 'events.dispatch.cancellation'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('runs checkbox preactivation and restores canceled checkedness', (fixture) => { - const document = fixture.document; - const input = document.createElement('input'); - input.type = 'checkbox'; - input.indeterminate = true; - fixture.connect(input); - const preactivation: boolean[][] = []; - const cancel = (event: DOMEvent): void => { - preactivation.push([input.checked, input.indeterminate]); - event.preventDefault(); - }; - input.addEventListener('click', cancel); - input.click(); - expect(preactivation).toEqual([[true, false]]); - expect(input.checked).toBe(false); - expect(input.indeterminate).toBe(true); - input.removeEventListener('click', cancel); - input.click(); - expect(input.checked).toBe(true); - expect(input.indeterminate).toBe(false); - }); - - context.test('checkbox canceled click rolls back checked and indeterminate state', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'checkbox'; - e.indeterminate = true; - let during: unknown; - let changes = 0; - e.addEventListener('click', (event) => { - during = [e.checked, e.indeterminate]; - event.preventDefault(); - }); - e.addEventListener('change', () => changes++); - e.click(); - expect(during).toEqual([true, false]); - expect(e.checked).toBe(false); - expect(e.indeterminate).toBe(true); - expect(changes).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/checkedness.ts b/packages/web-platform-tests/src/compatibility/elements/input/checkedness.ts deleted file mode 100644 index b0f892b..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/checkedness.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#dom-input-checked'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('keeps checkedness separate from the default after a property write', (fixture) => { - const document = fixture.document; - const input = document.createElement('input'); - input.type = 'checkbox'; - expect(input.checked).toBe(false); - input.defaultChecked = true; - expect(input.checked).toBe(true); - input.checked = false; - input.defaultChecked = false; - input.defaultChecked = true; - expect(input.hasAttribute('checked')).toBe(true); - expect(input.checked).toBe(false); - input.indeterminate = true; - expect(input.checked).toBe(false); - }); - - context.test('resets dirty checkedness from the checked attribute', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('input'); - e.type = 'checkbox'; - form.append(e); - e.defaultChecked = true; - expect(e.checked).toBe(true); - e.checked = false; - e.defaultChecked = false; - e.defaultChecked = true; - expect(e.checked).toBe(false); - form.reset(); - expect(e.checked).toBe(true); - expect(e.defaultChecked).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/cloning.ts b/packages/web-platform-tests/src/compatibility/elements/input/cloning.ts deleted file mode 100644 index c14dbdf..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/cloning.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#the-input-element'; -const requiredGlobals = ['HTMLInputElement'] as const; -export { requiredGlobals as globals }; -export const requires = ['elements.input.value', 'node.cloning'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('copies dirty value independently of the source', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.defaultValue = 'original'; - element.value = 'edited'; - const clone = element.cloneNode(true); - const Constructor = fixture.globals('HTMLInputElement'); - if (!(clone instanceof Constructor)) return expect.unreachable('Unexpected clone interface'); - expect(clone.value).toBe('edited'); - expect(clone.defaultValue).toBe('original'); - clone.defaultValue = 'new default'; - expect(clone.value).toBe('edited'); - clone.value = 'independent'; - expect(element.value).toBe('edited'); - }); - - context.test('clones dirty value without sharing subsequent edits', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.defaultValue = 'default'; - e.value = 'dirty'; - const copy = e.cloneNode(true) as HTMLInputElement; - expect(copy.value).toBe('dirty'); - expect(copy.defaultValue).toBe('default'); - copy.value = 'changed'; - expect(e.value).toBe('dirty'); - }); - - context.test('clones checkedness separately from defaultChecked', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'checkbox'; - e.defaultChecked = true; - e.checked = false; - const copy = e.cloneNode() as HTMLInputElement; - expect(copy.defaultChecked).toBe(true); - expect(copy.checked).toBe(false); - copy.checked = true; - expect(e.checked).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/color-value.ts b/packages/web-platform-tests/src/compatibility/elements/input/color-value.ts deleted file mode 100644 index 6c46b3a..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/color-value.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#color-state-(type=color)'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/tree/master/html/semantics/forms/the-input-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('accepts CSS color values and serializes opaque limited-sRGB colors', (fixture) => { - const document = fixture.document; - const input = document.createElement('input'); - input.type = 'color'; - input.value = 'red'; - expect(input.value).toBe('#ff0000'); - input.value = '#AABBCC'; - expect(input.value).toBe('#aabbcc'); - input.value = 'not a color'; - expect(input.value).toBe('#000000'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/date-value.ts b/packages/web-platform-tests/src/compatibility/elements/input/date-value.ts deleted file mode 100644 index b9ac90f..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/date-value.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasdate'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('reads and writes UTC dates', (fixture) => { - const document = fixture.document; - const input = document.createElement('input'); - input.type = 'date'; - expect(input.valueAsDate).toBeNull(); - input.value = '2024-02-29'; - const date = input.valueAsDate; - if (!date) return expect.fail('Expected a date for a valid date value'); - expect(date.getTime()).toBe(Date.UTC(2024, 1, 29)); - input.valueAsDate = new Date(Date.UTC(2025, 5, 3)); - expect(input.value).toBe('2025-06-03'); - input.valueAsDate = null; - expect(input.value).toBe(''); - }); - - context.test('sanitizes invalid dates and represents empty dates as null and NaN', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'date'; - e.value = '2023-02-29'; - expect(e.value).toBe(''); - expect(e.valueAsDate).toBeNull(); - expect(Number.isNaN(e.valueAsNumber)).toBe(true); - e.value = '2024-02-29'; - expect(e.valueAsNumber).toBe(Date.UTC(2024, 1, 29)); - }); - - context.test('writes date values using UTC calendar components', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'date'; - e.valueAsDate = new Date(Date.UTC(2024, 0, 2, 23)); - expect(e.value).toBe('2024-01-02'); - e.valueAsDate = null; - expect(e.value).toBe(''); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/form-association.ts b/packages/web-platform-tests/src/compatibility/elements/input/form-association.ts deleted file mode 100644 index ab357e0..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/form-association.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('follows the nearest ancestor form when moved', (fixture) => { - const document = fixture.document; - const first = document.createElement('form'); - const second = document.createElement('form'); - const element = document.createElement('input'); - first.appendChild(element); - expect(element.form).toBe(first); - second.appendChild(element); - expect(element.form).toBe(second); - element.remove(); - expect(element.form).toBeNull(); - }); - - context.test('explicit form id overrides ancestors and tracks id changes', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('form'), - b = document.createElement('form'), - e = document.createElement('input'); - a.id = 'a'; - b.id = 'b'; - a.append(e); - p.append(a, b); - fixture.connect(p as unknown as DOMElementContract); - expect(e.form).toBe(a); - e.setAttribute('form', 'b'); - expect(e.form).toBe(b); - b.id = 'changed'; - expect(e.form).toBeNull(); - e.removeAttribute('form'); - expect(e.form).toBe(a); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/labels.ts b/packages/web-platform-tests/src/compatibility/elements/input/labels.ts deleted file mode 100644 index b251f42..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/labels.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/forms.html#dom-lfe-labels'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/html/semantics/forms/the-label-element/labels-live-attribute-changes.html'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'keeps retained labels live when a detached control moves under a label', - (fixture) => { - const document = fixture.document; - const root = document.createElement('div'); - const label = document.createElement('label'); - const element = document.createElement('input'); - root.append(label, element); - const labels = element.labels; - if (!labels) return expect.fail('Expected labels for a text input'); - expect(labels.length).toBe(0); - expect(element.labels).toBe(labels); - label.appendChild(element); - expect(labels.length).toBe(1); - expect(labels[0]).toBe(label); - root.appendChild(element); - expect(labels.length).toBe(0); - }, - ); - - context.test('keeps connected explicit labels in tree order', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('input'), - a = document.createElement('label'), - b = document.createElement('label'); - e.id = 'control'; - a.htmlFor = b.htmlFor = 'control'; - p.append(a, e, b); - fixture.connect(p as unknown as DOMElementContract); - const labels = e.labels!; - expect(Array.from(labels)).toEqual([a, b]); - p.insertBefore(b, a); - expect(Array.from(labels)).toEqual([b, a]); - e.id = 'other'; - expect(labels.length).toBe(0); - }); - - context.test('does not associate labels with a different tree', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'), - label = document.createElement('label'); - e.id = 'control'; - label.htmlFor = 'control'; - fixture.connect(e as unknown as DOMElementContract); - expect(e.labels!.length).toBe(0); - expect(label.control).toBeNull(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/numeric-value.ts b/packages/web-platform-tests/src/compatibility/elements/input/numeric-value.ts deleted file mode 100644 index bf3ffea..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/numeric-value.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#dom-input-valueasnumber'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('converts numeric values and clears NaN', (fixture) => { - const document = fixture.document; - const input = document.createElement('input'); - input.type = 'number'; - expect(Number.isNaN(input.valueAsNumber)).toBe(true); - input.value = '1.5'; - expect(input.valueAsNumber).toBe(1.5); - input.valueAsNumber = 42; - expect(input.value).toBe('42'); - input.valueAsNumber = Number.NaN; - expect(input.value).toBe(''); - input.type = 'text'; - expect(Number.isNaN(input.valueAsNumber)).toBe(true); - expect(() => { - input.valueAsNumber = 1; - }).toThrow(expect.objectContaining({ name: 'InvalidStateError' })); - }); - - context.test('represents empty numeric values as NaN and rejects infinity', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'number'; - expect(Number.isNaN(e.valueAsNumber)).toBe(true); - e.valueAsNumber = 3.5; - expect(e.value).toBe('3.5'); - e.valueAsNumber = NaN; - expect(e.value).toBe(''); - expect(() => { - e.valueAsNumber = Infinity; - }).toThrow(expect.objectContaining({ name: 'TypeError' })); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/radio-groups.ts b/packages/web-platform-tests/src/compatibility/elements/input/radio-groups.ts deleted file mode 100644 index b81dee1..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/radio-groups.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio)'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/html/semantics/forms/the-input-element/radio-disconnected-group-owner.html'; -export const requires = ['elements.input.checkedness'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('groups radios by tree and nonempty name', (fixture) => { - const document = fixture.document; - const root = document.createElement('div'); - const first = document.createElement('input'); - const second = document.createElement('input'); - first.type = second.type = 'radio'; - first.name = second.name = 'choice'; - root.append(first, second); - first.checked = true; - second.checked = true; - expect(first.checked, 'setting checked in a detached group deselects its peer').toBe(false); - expect(second.checked).toBe(true); - first.name = 'other'; - first.checked = true; - expect(second.checked).toBe(true); - first.name = 'choice'; - expect(second.checked, 'changing a checked radio name updates its detached group').toBe(false); - }); - - context.test('keeps empty names independent and scopes named groups to forms', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - a = document.createElement('input'), - b = document.createElement('input'); - a.type = b.type = 'radio'; - form.append(a, b); - a.checked = b.checked = true; - expect(a.checked).toBe(true); - expect(b.checked).toBe(true); - a.name = b.name = 'group'; - a.checked = true; - b.checked = true; - expect(a.checked).toBe(false); - expect(b.checked).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/range-text.ts b/packages/web-platform-tests/src/compatibility/elements/input/range-text.ts deleted file mode 100644 index 2a5b15e..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/range-text.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection'; -export const requires = ['elements.input.selection'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('replaces explicit offsets and selects the replacement', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.value = 'abcdef'; - element.setRangeText('XYZ', 2, 4, 'select'); - expect(element.value).toBe('abXYZef'); - expect(element.selectionStart).toBe(2); - expect(element.selectionEnd).toBe(5); - expect(() => element.setRangeText('invalid', 5, 1)).toThrow( - expect.objectContaining({ name: 'IndexSizeError' }), - ); - expect(element.value).toBe('abXYZef'); - }); - context.test('uses the current selection when offsets are omitted', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.value = 'abcdef'; - element.setSelectionRange(2, 4); - element.setRangeText('X'); - expect(element.value).toBe('abXef'); - expect(element.selectionStart).toBe(2); - expect(element.selectionEnd).toBe(3); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/sanitization.ts b/packages/web-platform-tests/src/compatibility/elements/input/sanitization.ts deleted file mode 100644 index d0af00b..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/sanitization.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#the-input-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('sanitizes invalid numeric and calendar values', (fixture) => { - const document = fixture.document; - const input = document.createElement('input'); - for (const [type, value, expected] of [ - ['number', ' 3 ', ''], - ['number', 'Infinity', ''], - ['number', '1e2', '1e2'], - ['date', '2025-02-29', ''], - ['date', '2024-02-29', '2024-02-29'], - ['time', '24:00', ''], - ['datetime-local', '2024-03-02 09:30:00', '2024-03-02T09:30'], - ] as const) { - input.type = type; - input.value = value; - expect(input.value).toBe(expected); - } - }); - - context.test('sanitizes line endings for text and whitespace for URL values', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.value = 'a\r\nb\nc'; - expect(e.value).toBe('abc'); - e.type = 'url'; - e.value = ' https://example.test/a '; - expect(e.value).toBe('https://example.test/a'); - }); - - context.test('normalizes invalid types and invalid numeric values', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'unknown'; - expect(e.type).toBe('text'); - e.type = 'number'; - e.value = 'not a number'; - expect(e.value).toBe(''); - e.value = '2.5'; - expect(e.valueAsNumber).toBe(2.5); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/selection.ts b/packages/web-platform-tests/src/compatibility/elements/input/selection.ts deleted file mode 100644 index abb3e0b..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/selection.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection'; -export const requires = ['elements.input.value'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('uses UTF-16 offsets and clamps selection boundaries', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.value = 'a😀b'; - element.setSelectionRange(1, 3, 'backward'); - expect(element.selectionStart).toBe(1); - expect(element.selectionEnd).toBe(3); - expect(element.selectionDirection).toBe('backward'); - element.setSelectionRange(99, 99); - expect(element.selectionStart).toBe(4); - expect(element.selectionEnd).toBe(4); - element.setSelectionRange(3, 1); - expect(element.selectionStart).toBe(1); - expect(element.selectionEnd).toBe(1); - element.select(); - expect(element.selectionStart).toBe(0); - expect(element.selectionEnd).toBe(4); - }); - context.test('preserves the selection when assigning the same value', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.value = 'hello'; - element.setSelectionRange(1, 3); - element.value = 'hello'; - expect(element.selectionStart).toBe(1); - expect(element.selectionEnd).toBe(3); - element.value = 'new'; - expect(element.selectionStart).toBe(3); - expect(element.selectionEnd).toBe(3); - }); - - context.test('clamps endpoints and collapses reversed selection', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.value = 'abc'; - e.setSelectionRange(1, 99, 'backward'); - expect(e.selectionStart).toBe(1); - expect(e.selectionEnd).toBe(3); - expect(e.selectionDirection).toBe('backward'); - e.setSelectionRange(3, 1); - expect(e.selectionStart).toBe(1); - expect(e.selectionEnd).toBe(1); - }); - - context.test('rejects selection methods on unsupported input types', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'number'; - expect(e.selectionStart).toBeNull(); - expect(e.selectionEnd).toBeNull(); - expect(() => e.setSelectionRange(0, 1)).toThrow( - expect.objectContaining({ name: 'InvalidStateError' }), - ); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/stepping.ts b/packages/web-platform-tests/src/compatibility/elements/input/stepping.ts deleted file mode 100644 index 8215964..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/stepping.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#dom-input-stepup'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('steps within numeric constraints and rejects any', (fixture) => { - const document = fixture.document; - const input = document.createElement('input'); - input.type = 'number'; - input.min = '1'; - input.max = '9'; - input.step = '2'; - input.value = '3'; - input.stepUp(2); - expect(input.value).toBe('7'); - input.stepDown(); - expect(input.value).toBe('5'); - input.stepUp(99); - expect(input.value).toBe('9'); - input.step = 'any'; - expect(() => input.stepUp()).toThrow(expect.objectContaining({ name: 'InvalidStateError' })); - }); - - context.test('steps relative to min and rejects step any', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'number'; - e.min = '1'; - e.step = '2'; - e.value = '3'; - e.stepUp(2); - expect(e.value).toBe('7'); - e.stepDown(); - expect(e.value).toBe('5'); - e.step = 'any'; - expect(() => e.stepUp()).toThrow(expect.objectContaining({ name: 'InvalidStateError' })); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/validation.ts b/packages/web-platform-tests/src/compatibility/elements/input/validation.ts deleted file mode 100644 index 52802fa..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/validation.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-constraint-validation-api'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('exposes required and custom validity independently', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.required = true; - expect(element.willValidate).toBe(true); - expect(element.validity.valueMissing).toBe(true); - expect(element.checkValidity()).toBe(false); - element.value = 'present'; - expect(element.validity.valueMissing).toBe(false); - element.setCustomValidity('Try again'); - expect(element.validity.customError).toBe(true); - expect(element.validationMessage).toBe('Try again'); - element.setCustomValidity(''); - expect(element.validity.valid).toBe(true); - element.disabled = true; - expect(element.willValidate).toBe(false); - }); - - context.test('updates required validity and disabled validation candidacy', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.required = true; - expect(e.validity.valueMissing).toBe(true); - expect(e.checkValidity()).toBe(false); - e.value = 'x'; - expect(e.validity.valueMissing).toBe(false); - expect(e.checkValidity()).toBe(true); - e.disabled = true; - e.value = ''; - expect(e.willValidate).toBe(false); - expect(e.checkValidity()).toBe(true); - }); - - context.test('retains custom errors until explicitly cleared', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.setCustomValidity('problem'); - expect(e.validity.customError).toBe(true); - expect(e.validationMessage).toBe('problem'); - e.value = 'changed'; - expect(e.checkValidity()).toBe(false); - e.setCustomValidity(''); - expect(e.validity.customError).toBe(false); - expect(e.checkValidity()).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/input/value.ts b/packages/web-platform-tests/src/compatibility/elements/input/value.ts deleted file mode 100644 index 665ac8f..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/input/value.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/input.html#the-input-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('keeps default and dirty values separate', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.defaultValue = 'original'; - expect(element.value).toBe('original'); - element.value = 'edited'; - element.defaultValue = 'replacement'; - expect(element.value).toBe('edited'); - expect(element.defaultValue).toBe('replacement'); - }); - context.test('normalizes line breaks in the API value', (fixture) => { - const document = fixture.document; - const element = document.createElement('input'); - element.value = 'a\r\nb\rc'; - expect(element.value).toBe('abc'); - }); - - context.test('keeps dirty value independent from defaults until reset', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('input'); - form.append(e); - expect(e.value).toBe(''); - e.defaultValue = 'a'; - expect(e.value).toBe('a'); - e.value = 'dirty'; - e.setAttribute('value', 'b'); - expect(e.value).toBe('dirty'); - expect(e.defaultValue).toBe('b'); - form.reset(); - expect(e.value).toBe('b'); - e.defaultValue = 'c'; - expect(e.value).toBe('c'); - }); - - context.test('does not fire input or change for programmatic writes', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - const seen: string[] = []; - e.addEventListener('input', () => seen.push('input')); - e.addEventListener('change', () => seen.push('change')); - e.value = 'x'; - expect(seen).toEqual([]); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/label/activation.ts b/packages/web-platform-tests/src/compatibility/elements/label/activation.ts deleted file mode 100644 index af0e187..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/label/activation.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://html.spec.whatwg.org/multipage/forms.html#the-label-element'; -export const requires = ['elements.input.activation'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('does not forward activation from an interactive descendant', (fixture) => { - const document = fixture.document; - const label = document.createElement('label'); - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - const button = document.createElement('button'); - button.type = 'button'; - label.append(checkbox, button); - fixture.connect(label); - button.click(); - expect(checkbox.checked).toBe(false); - checkbox.click(); - expect(checkbox.checked).toBe(true); - }); - - context.test('canceling label click prevents forwarded control activation', (fixture) => { - const document = fixture.document as unknown as Document; - const label = document.createElement('label'), - e = document.createElement('input'); - e.type = 'checkbox'; - label.append(e); - fixture.connect(label as unknown as DOMElementContract); - label.addEventListener('click', (event) => event.preventDefault()); - label.click(); - expect(e.checked).toBe(false); - }); - - context.test('does not forward activation through a disabled associated control', (fixture) => { - const document = fixture.document as unknown as Document; - const label = document.createElement('label'), - e = document.createElement('input'); - e.type = 'checkbox'; - e.disabled = true; - label.append(e); - fixture.connect(label as unknown as DOMElementContract); - let n = 0; - e.addEventListener('click', () => n++); - label.click(); - expect(n).toBe(0); - expect(e.checked).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/label/association.ts b/packages/web-platform-tests/src/compatibility/elements/label/association.ts deleted file mode 100644 index b0277d4..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/label/association.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/forms.html#the-label-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('uses for before nested labelable descendants', (fixture) => { - const document = fixture.document; - const root = document.createElement('div'); - const label = document.createElement('label'); - const nested = document.createElement('input'); - const external = document.createElement('input'); - external.id = 'external-control'; - label.appendChild(nested); - root.append(label, external); - fixture.connect(root); - expect(label.control).toBe(nested); - label.htmlFor = external.id; - expect(label.control).toBe(external); - label.htmlFor = 'missing'; - expect(label.control).toBeNull(); - }); - - context.test('explicit for association overrides nested labelable descendants', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - label = document.createElement('label'), - nested = document.createElement('input'), - other = document.createElement('input'); - other.id = 'other'; - label.append(nested); - p.append(label, other); - fixture.connect(p as unknown as DOMElementContract); - expect(label.control).toBe(nested); - label.htmlFor = 'other'; - expect(label.control).toBe(other); - label.htmlFor = 'missing'; - expect(label.control).toBeNull(); - }); - - context.test('hidden input is not labelable', (fixture) => { - const document = fixture.document as unknown as Document; - const label = document.createElement('label'), - e = document.createElement('input'); - e.type = 'hidden'; - label.append(e); - expect(label.control).toBeNull(); - expect(e.labels).toBeNull(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/legend.ts b/packages/web-platform-tests/src/compatibility/elements/legend.ts deleted file mode 100644 index 4b43a16..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/legend.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-legend-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('gets its form through its parent fieldset', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const fieldset = document.createElement('fieldset'); - const legend = document.createElement('legend'); - form.appendChild(fieldset); - fieldset.appendChild(legend); - expect(legend.form).toBe(form); - form.appendChild(legend); - expect(legend.form).toBeNull(); - }); - - context.test('form ownership comes from its parent fieldset', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - fieldset = document.createElement('fieldset'), - legend = document.createElement('legend'); - expect(legend.form).toBeNull(); - fieldset.append(legend); - form.append(fieldset); - expect(legend.form).toBe(form); - form.append(legend); - expect(legend.form).toBeNull(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/meter.ts b/packages/web-platform-tests/src/compatibility/elements/meter.ts deleted file mode 100644 index 37110a3..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/meter.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-meter-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('derives default thresholds and clamps values to the range', (fixture) => { - const document = fixture.document; - const meter = document.createElement('meter'); - expect(meter.min).toBe(0); - expect(meter.max).toBe(1); - expect(meter.optimum).toBe(0.5); - meter.min = 10; - meter.max = 20; - meter.value = 25; - expect(meter.value).toBe(20); - expect(meter.getAttribute('value')).toBe('25'); - expect(meter.low).toBe(10); - expect(meter.high).toBe(20); - expect(meter.optimum).toBe(15); - }); - - context.test('clamps values to min/max and reflects explicit numeric attributes', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('meter'); - expect(e.min).toBe(0); - expect(e.max).toBe(1); - e.min = 2; - e.max = 10; - e.value = 20; - expect(e.value).toBe(10); - e.value = -1; - expect(e.value).toBe(2); - e.low = 4; - e.high = 8; - e.optimum = 6; - expect(e.low).toBe(4); - expect(e.high).toBe(8); - expect(e.optimum).toBe(6); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/optgroup.ts b/packages/web-platform-tests/src/compatibility/elements/optgroup.ts deleted file mode 100644 index 1fb8b4a..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/optgroup.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#the-optgroup-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('reflects label and disabled without rewriting child attributes', (fixture) => { - const document = fixture.document; - const group = document.createElement('optgroup'); - const option = document.createElement('option'); - group.appendChild(option); - group.label = 'Group'; - group.disabled = true; - expect(group.getAttribute('label')).toBe('Group'); - expect(group.hasAttribute('disabled')).toBe(true); - expect(option.disabled).toBe(false); - }); - - context.test('reflects disabled and label attributes with standard defaults', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('optgroup'); - expect(e.disabled).toBe(false); - expect(e.label).toBe(''); - e.disabled = true; - e.label = 'Group'; - expect(e.hasAttribute('disabled')).toBe(true); - expect(e.getAttribute('label')).toBe('Group'); - e.removeAttribute('disabled'); - expect(e.disabled).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/option/attributes.ts b/packages/web-platform-tests/src/compatibility/elements/option/attributes.ts deleted file mode 100644 index c9a541c..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/option/attributes.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-option-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('reflects explicit value, label, and disabled', (fixture) => { - const document = fixture.document; - const option = document.createElement('option'); - option.text = 'Visible'; - expect(option.value).toBe('Visible'); - expect(option.label).toBe('Visible'); - option.value = ''; - option.label = 'Label'; - option.disabled = true; - expect(option.value).toBe(''); - expect(option.label).toBe('Label'); - expect(option.getAttribute('value')).toBe(''); - expect(option.hasAttribute('disabled')).toBe(true); - }); - - context.test('reflects defaultSelected independently of dirty selectedness', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('option'); - expect(e.disabled).toBe(false); - expect(e.defaultSelected).toBe(false); - expect(e.label).toBe(''); - e.disabled = true; - expect(e.hasAttribute('disabled')).toBe(true); - e.label = 'label'; - expect(e.getAttribute('label')).toBe('label'); - e.defaultSelected = true; - expect(e.getAttribute('selected')).toBe(''); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/option/selectedness.ts b/packages/web-platform-tests/src/compatibility/elements/option/selectedness.ts deleted file mode 100644 index faa6c13..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/option/selectedness.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#concept-option-selectedness'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b4b303c8dca22989c43e2d76fce199c36d4b77a7/html/semantics/forms/the-option-element/option-selected.html'; - -// Adapted from WPT option-selected.html at b4b303c8dca22989c43e2d76fce199c36d4b77a7. -// Copyright © web-platform-tests contributors. Original author: Corey Farwell. -// BSD-3-Clause; see ../../../../test/wpt/LICENSE. - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('reflects selected attributes until selectedness becomes dirty', (fixture) => { - const document = fixture.document; - const option = document.createElement('option'); - expect(option.selected).toBe(false); - option.setAttribute('selected', ''); - expect(option.selected).toBe(true); - option.removeAttribute('selected'); - expect(option.selected).toBe(false); - option.defaultSelected = true; - expect(option.selected).toBe(true); - option.defaultSelected = false; - expect(option.selected).toBe(false); - }); - for (const selected of [true, false]) { - context.test(`preserves dirty ${selected} selectedness across default changes`, (fixture) => { - const document = fixture.document; - const option = document.createElement('option'); - option.selected = selected; - option.setAttribute('selected', ''); - expect(option.selected).toBe(selected); - option.removeAttribute('selected'); - expect(option.selected).toBe(selected); - option.defaultSelected = true; - expect(option.selected).toBe(selected); - option.defaultSelected = false; - expect(option.selected).toBe(selected); - }); - } - context.test('separates dirty selectedness from the selected attribute', (fixture) => { - const document = fixture.document; - const option = document.createElement('option'); - option.defaultSelected = true; - expect(option.selected).toBe(true); - option.selected = false; - option.defaultSelected = false; - option.defaultSelected = true; - expect(option.hasAttribute('selected')).toBe(true); - expect(option.selected).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/option/text.ts b/packages/web-platform-tests/src/compatibility/elements/option/text.ts deleted file mode 100644 index fa3c6fe..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/option/text.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#dom-option-text'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('collapses ASCII whitespace and excludes script descendants', (fixture) => { - const document = fixture.document; - const option = document.createElement('option'); - option.append(' one\n\t'); - const span = document.createElement('span'); - span.textContent = ' two\u00a0three '; - const script = document.createElement('script'); - script.textContent = 'ignored'; - option.append(span, script); - expect(option.text).toBe('one two\u00a0three'); - option.text = 'replacement'; - expect(option.textContent).toBe('replacement'); - expect(option.childNodes.length).toBe(1); - }); - - context.test('collapses ASCII whitespace and uses text as the value fallback', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('option'); - e.textContent = ' a\n b '; - expect(e.text).toBe('a b'); - expect(e.value).toBe('a b'); - e.value = 'explicit'; - expect(e.text).toBe('a b'); - expect(e.value).toBe('explicit'); - e.removeAttribute('value'); - expect(e.value).toBe('a b'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/output/html-for.ts b/packages/web-platform-tests/src/compatibility/elements/output/html-for.ts deleted file mode 100644 index 18b08cb..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/output/html-for.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#dom-output-htmlfor'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('keeps the token list and for attribute synchronized', (fixture) => { - const document = fixture.document; - const output = document.createElement('output'); - const tokens = output.htmlFor; - output.setAttribute('for', 'first second'); - expect(tokens.contains('first')).toBe(true); - expect(tokens.contains('second')).toBe(true); - tokens.add('third'); - expect(output.getAttribute('for')).toBe('first second third'); - tokens.remove('second'); - expect(output.getAttribute('for')).toBe('first third'); - expect(output.htmlFor).toBe(tokens); - }); - - context.test('htmlFor token list synchronizes for attribute and stays stable', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('output'), - list = e.htmlFor; - e.setAttribute('for', 'a b a'); - expect(Array.from(list)).toEqual(['a', 'b']); - list.add('c'); - expect(e.getAttribute('for')).toBe('a b c'); - e.removeAttribute('for'); - expect(list.length).toBe(0); - expect(e.htmlFor).toBe(list); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/output/reset.ts b/packages/web-platform-tests/src/compatibility/elements/output/reset.ts deleted file mode 100644 index 47e83a7..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/output/reset.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-output-element'; -export const requires = ['elements.form.reset', 'elements.output.value'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('form reset restores the default and default-value mode', (fixture) => { - const document = fixture.document; - const form = document.createElement('form'); - const output = document.createElement('output'); - form.appendChild(output); - output.defaultValue = 'zero'; - output.value = 'one'; - form.reset(); - expect(output.value).toBe('zero'); - output.defaultValue = 'two'; - expect(output.value).toBe('two'); - }); - - context.test('reset applies latest default after dirty output changes', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('output'); - form.append(e); - e.defaultValue = 'a'; - e.value = 'dirty'; - e.defaultValue = 'b'; - form.reset(); - expect(e.value).toBe('b'); - expect(e.textContent).toBe('b'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/output/value.ts b/packages/web-platform-tests/src/compatibility/elements/output/value.ts deleted file mode 100644 index 14ca27d..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/output/value.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-output-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('keeps a default value separately after entering value mode', (fixture) => { - const document = fixture.document; - const output = document.createElement('output'); - output.defaultValue = 'original'; - expect(output.value).toBe('original'); - expect(output.textContent).toBe('original'); - output.value = 'calculated'; - expect(output.textContent).toBe('calculated'); - expect(output.defaultValue).toBe('original'); - output.defaultValue = 'replacement'; - expect(output.value).toBe('calculated'); - expect(output.type).toBe('output'); - expect(output.willValidate).toBe(false); - }); - - context.test('value writes replace text while defaultValue remains independent', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('output'); - e.defaultValue = 'default'; - expect(e.value).toBe('default'); - e.value = 'current'; - expect(e.textContent).toBe('current'); - expect(e.defaultValue).toBe('default'); - e.defaultValue = 'next'; - expect(e.value).toBe('current'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/progress.ts b/packages/web-platform-tests/src/compatibility/elements/progress.ts deleted file mode 100644 index 393cc6f..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/progress.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#the-progress-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('clamps exposed values while retaining reflected attributes', (fixture) => { - const document = fixture.document; - const progress = document.createElement('progress'); - expect(progress.max).toBe(1); - expect(progress.value).toBe(0); - expect(progress.position).toBe(-1); - progress.max = 10; - progress.value = 4; - expect(progress.position).toBe(0.4); - progress.value = 20; - expect(progress.getAttribute('value')).toBe('20'); - expect(progress.value).toBe(10); - expect(progress.position).toBe(1); - progress.removeAttribute('value'); - expect(progress.position).toBe(-1); - }); - - context.test('clamps value and computes determinate position without layout', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('progress'); - expect(e.max).toBe(1); - expect(e.value).toBe(0); - expect(e.position).toBe(-1); - e.max = 10; - e.value = 20; - expect(e.value).toBe(10); - expect(e.position).toBe(1); - e.value = -1; - expect(e.value).toBe(0); - e.removeAttribute('value'); - expect(e.position).toBe(-1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/select/cloning.ts b/packages/web-platform-tests/src/compatibility/elements/select/cloning.ts deleted file mode 100644 index d0ad61b..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/select/cloning.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#the-option-element'; -const requiredGlobals = ['HTMLSelectElement'] as const; -export { requiredGlobals as globals }; -export const requires = ['elements.select.selection', 'node.cloning'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'reconstructs selection from cloned attributes rather than dirty selectedness', - (fixture) => { - const document = fixture.document; - const select = document.createElement('select'); - const first = document.createElement('option'); - first.value = 'first'; - const second = document.createElement('option'); - second.value = 'second'; - select.append(first, second); - select.value = 'second'; - const copy = select.cloneNode(true); - const Constructor = fixture.globals('HTMLSelectElement'); - if (!(copy instanceof Constructor)) return expect.unreachable('Unexpected clone interface'); - // HTML has special value-copying steps for input and textarea, not option. - expect(copy.value).toBe('first'); - expect(copy.options[0]).not.toBe(first); - expect(copy.options[1]).not.toBe(second); - copy.value = 'first'; - expect(select.value).toBe('second'); - }, - ); - - context.test('deep clone owns independent options and shallow clone has none', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('select'); - e.innerHTML = ''; - const copy = e.cloneNode(true) as HTMLSelectElement; - expect(copy.value).toBe('a'); - expect(copy.options[0]).not.toBe(e.options[0]); - copy.value = 'b'; - expect(e.value).toBe('a'); - expect((e.cloneNode(false) as HTMLSelectElement).options.length).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/select/indexed-options.ts b/packages/web-platform-tests/src/compatibility/elements/select/indexed-options.ts deleted file mode 100644 index 57ebcd0..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/select/indexed-options.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#dom-htmloptionscollection-setter'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/tree/master/html/semantics/forms/the-select-element'; -export const requires = ['elements.select.options'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('supports the standard indexed options setter', (fixture) => { - const document = fixture.document; - const select = document.createElement('select'); - const original = document.createElement('option'); - const replacement = document.createElement('option'); - select.appendChild(original); - select.options[0] = replacement; - expect(select.options[0]).toBe(replacement); - expect(replacement.parentNode).toBe(select); - expect(original.parentNode).toBeNull(); - expect(select.length).toBe(1); - select.options[0] = null; - expect(select.length).toBe(0); - expect(replacement.parentNode).toBeNull(); - }); - - context.test( - 'replaces and deletes indexed options while retaining collection identity', - (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('select'), - a = document.createElement('option'), - b = document.createElement('option'), - options = e.options; - e.add(a); - e[0] = b; - expect(e.options).toBe(options); - expect(options[0]).toBe(b); - expect(a.parentNode).toBeNull(); - Reflect.set(e, 0, null); - expect(options.length).toBe(0); - }, - ); - - context.test('setting length grows with empty options and truncates children', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('select'); - e.options.length = 3; - expect(e.options.length).toBe(3); - expect(e.options[2]!.value).toBe(''); - const last = e.options[2]!; - e.options.length = 1; - expect(last.parentNode).toBeNull(); - expect(e.options.length).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/select/mutation.ts b/packages/web-platform-tests/src/compatibility/elements/select/mutation.ts deleted file mode 100644 index 9d7fad3..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/select/mutation.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-add'; -export const requires = ['elements.select.options'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('adds options at indexed anchors and adjusts length', (fixture) => { - const document = fixture.document; - const select = document.createElement('select'); - const first = document.createElement('option'); - const second = document.createElement('option'); - select.add(second, null); - select.add(first, 0); - expect(Array.from(select.options)).toEqual([first, second]); - select.remove(0); - expect(Array.from(select.options)).toEqual([second]); - select.length = 3; - expect(select.options.length).toBe(3); - const generated = select.options[2]; - if (!generated) return expect.fail('Expected a generated option'); - expect(generated.localName).toBe('option'); - select.length = 0; - expect(select.options.length).toBe(0); - expect(second.parentNode).toBeNull(); - }); - - context.test( - 'updates selectedIndex after insertion and removal before the selected option', - (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('select'); - e.innerHTML = ''; - e.value = 'b'; - const selected = e.options[1]!; - e.insertBefore(document.createElement('option'), e.firstChild); - expect(e.selectedIndex).toBe(2); - expect(e.options[2]).toBe(selected); - e.remove(0); - expect(e.selectedIndex).toBe(1); - expect(e.value).toBe('b'); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/select/options.ts b/packages/web-platform-tests/src/compatibility/elements/select/options.ts deleted file mode 100644 index dd7692d..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/select/options.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-options'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('keeps options live and excludes nested selects', (fixture) => { - const document = fixture.document; - const select = document.createElement('select'); - const options = select.options; - const first = document.createElement('option'); - first.id = 'first-option'; - const group = document.createElement('optgroup'); - const second = document.createElement('option'); - group.appendChild(second); - select.append(first, group); - expect(select.options).toBe(options); - expect(Array.from(options)).toEqual([first, second]); - expect(select.item(0)).toBe(first); - expect(select.item(99)).toBeNull(); - expect(select.namedItem('first-option')).toBe(first); - expect(second.index).toBe(1); - first.remove(); - expect(Array.from(options)).toEqual([second]); - expect(second.index).toBe(0); - }); - - context.test('adds before a reference and removes by index', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('select'), - a = document.createElement('option'), - b = document.createElement('option'); - e.add(a); - e.add(b, a); - expect(Array.from(e.options)).toEqual([b, a]); - e.remove(0); - expect(Array.from(e.options)).toEqual([a]); - e.remove(99); - expect(e.options.length).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/select/selection.ts b/packages/web-platform-tests/src/compatibility/elements/select/selection.ts deleted file mode 100644 index 4264700..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/select/selection.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-selectedindex'; -export const requires = ['elements.select.options'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('selects by value and permits an explicit empty selection', (fixture) => { - const document = fixture.document; - const select = document.createElement('select'); - const first = document.createElement('option'); - const second = document.createElement('option'); - first.value = 'first'; - second.value = 'second'; - select.append(first, second); - expect(select.selectedIndex).toBe(0); - const selected = select.selectedOptions; - select.value = 'second'; - expect(select.selectedIndex).toBe(1); - expect(Array.from(selected)).toEqual([second]); - select.value = 'missing'; - expect(select.selectedIndex).toBe(-1); - expect(selected.length).toBe(0); - expect(select.value).toBe(''); - select.multiple = true; - first.selected = second.selected = true; - expect(select.type).toBe('select-multiple'); - expect(Array.from(selected)).toEqual([first, second]); - }); - - context.test('sets selectedIndex and clears selection for missing values', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('select'); - e.innerHTML = ''; - expect(e.selectedIndex).toBe(0); - e.value = 'b'; - expect(e.selectedIndex).toBe(1); - expect(e.options[1]!.selected).toBe(true); - e.value = 'missing'; - expect(e.selectedIndex).toBe(-1); - expect(e.value).toBe(''); - e.selectedIndex = 0; - expect(e.value).toBe('a'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/select/validation.ts b/packages/web-platform-tests/src/compatibility/elements/select/validation.ts deleted file mode 100644 index 9dcf1bb..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/select/validation.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#placeholder-label-option'; -export const requires = ['elements.select.selection'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('treats the first empty option as a required placeholder', (fixture) => { - const document = fixture.document; - const select = document.createElement('select'); - select.required = true; - const placeholder = document.createElement('option'); - placeholder.value = ''; - const choice = document.createElement('option'); - choice.value = 'chosen'; - select.append(placeholder, choice); - expect(select.validity.valueMissing).toBe(true); - select.value = 'chosen'; - expect(select.validity.valueMissing).toBe(false); - select.setCustomValidity('Choose again'); - expect(select.checkValidity()).toBe(false); - expect(select.validationMessage).toBe('Choose again'); - select.disabled = true; - expect(select.willValidate).toBe(false); - }); - - context.test('required single select rejects the placeholder option', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('select'); - e.required = true; - e.innerHTML = ''; - expect(e.validity.valueMissing).toBe(true); - e.value = 'x'; - expect(e.checkValidity()).toBe(true); - e.disabled = true; - e.value = ''; - expect(e.willValidate).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/selectedcontent.ts b/packages/web-platform-tests/src/compatibility/elements/selectedcontent.ts deleted file mode 100644 index 2859082..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/selectedcontent.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#the-selectedcontent-element'; -export const requires = ['elements.select.selection'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('copies selected option content without sharing node identity', (fixture) => { - const document = fixture.document; - const select = document.createElement('select'); - const button = document.createElement('button'); - const selected = document.createElement('selectedcontent'); - button.appendChild(selected); - const first = document.createElement('option'); - const label = document.createElement('span'); - label.textContent = 'First'; - first.appendChild(label); - const second = document.createElement('option'); - second.text = 'Second'; - select.append(button, first, second); - fixture.connect(select); - expect(selected.textContent).toBe('First'); - expect(selected.firstChild).not.toBe(label); - select.selectedIndex = 1; - expect(selected.textContent).toBe('Second'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/summary/activation.ts b/packages/web-platform-tests/src/compatibility/elements/summary/activation.ts deleted file mode 100644 index e07b2de..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/summary/activation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interactive-elements.html#the-summary-element'; -export const requires = ['elements.details.open', 'events.dispatch.cancellation'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'toggles details only through the first summary and honors cancellation', - (fixture) => { - const document = fixture.document; - const details = document.createElement('details'); - const first = document.createElement('summary'); - const second = document.createElement('summary'); - details.append(first, second); - second.click(); - expect(details.open).toBe(false); - first.click(); - expect(details.open).toBe(true); - first.addEventListener('click', (event) => { - event.preventDefault(); - }); - first.click(); - expect(details.open).toBe(true); - }, - ); - - context.test('only the first summary has details activation behavior', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('details'), - a = document.createElement('summary'), - b = document.createElement('summary'); - e.append(a, b); - fixture.connect(e as unknown as DOMElementContract); - b.click(); - expect(e.open).toBe(false); - a.click(); - expect(e.open).toBe(true); - a.click(); - expect(e.open).toBe(false); - }); - - context.test('canceled summary click leaves details state unchanged', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('details'), - s = document.createElement('summary'); - e.append(s); - fixture.connect(e as unknown as DOMElementContract); - s.addEventListener('click', (event) => event.preventDefault()); - s.click(); - expect(e.open).toBe(false); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/textarea/cloning.ts b/packages/web-platform-tests/src/compatibility/elements/textarea/cloning.ts deleted file mode 100644 index 86dfb1c..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/textarea/cloning.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element'; -const requiredGlobals = ['HTMLTextAreaElement'] as const; -export { requiredGlobals as globals }; -export const requires = ['elements.textarea.value', 'node.cloning'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('copies dirty value independently of the source', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.defaultValue = 'original'; - element.value = 'edited'; - const clone = element.cloneNode(true); - const Constructor = fixture.globals('HTMLTextAreaElement'); - if (!(clone instanceof Constructor)) return expect.unreachable('Unexpected clone interface'); - expect(clone.value).toBe('edited'); - expect(clone.defaultValue).toBe('original'); - clone.defaultValue = 'new default'; - expect(clone.value).toBe('edited'); - clone.value = 'independent'; - expect(element.value).toBe('edited'); - }); - - context.test('clones dirty value and text defaults independently', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('textarea'); - e.defaultValue = 'default'; - e.value = 'dirty'; - const copy = e.cloneNode(true) as HTMLTextAreaElement; - expect(copy.value).toBe('dirty'); - expect(copy.defaultValue).toBe('default'); - copy.value = 'changed'; - expect(e.value).toBe('dirty'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/textarea/form-association.ts b/packages/web-platform-tests/src/compatibility/elements/textarea/form-association.ts deleted file mode 100644 index 358bfd0..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/textarea/form-association.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#association-of-controls-and-forms'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('follows the nearest ancestor form when moved', (fixture) => { - const document = fixture.document; - const first = document.createElement('form'); - const second = document.createElement('form'); - const element = document.createElement('textarea'); - first.appendChild(element); - expect(element.form).toBe(first); - second.appendChild(element); - expect(element.form).toBe(second); - element.remove(); - expect(element.form).toBeNull(); - }); - - context.test('updates form ownership on moving between forms', (fixture) => { - const document = fixture.document as unknown as Document; - const a = document.createElement('form'), - b = document.createElement('form'), - e = document.createElement('textarea'); - expect(e.form).toBeNull(); - a.append(e); - expect(e.form).toBe(a); - b.append(e); - expect(e.form).toBe(b); - e.remove(); - expect(e.form).toBeNull(); - }); - - context.test('an empty explicit form attribute prevents ancestor ownership', (fixture) => { - const document = fixture.document as unknown as Document; - const form = document.createElement('form'), - e = document.createElement('textarea'); - form.append(e); - fixture.connect(form as unknown as DOMElementContract); - e.setAttribute('form', ''); - expect(e.form).toBeNull(); - e.removeAttribute('form'); - expect(e.form).toBe(form); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/textarea/labels.ts b/packages/web-platform-tests/src/compatibility/elements/textarea/labels.ts deleted file mode 100644 index 69ec30c..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/textarea/labels.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/forms.html#dom-lfe-labels'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/html/semantics/forms/the-label-element/labels-live-attribute-changes.html'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'keeps retained labels live when a detached control moves under a label', - (fixture) => { - const document = fixture.document; - const root = document.createElement('div'); - const label = document.createElement('label'); - const element = document.createElement('textarea'); - root.append(label, element); - const labels = element.labels; - expect(labels.length).toBe(0); - expect(element.labels).toBe(labels); - label.appendChild(element); - expect(labels.length).toBe(1); - expect(labels[0]).toBe(label); - root.appendChild(element); - expect(labels.length).toBe(0); - }, - ); - - context.test('keeps connected explicit labels in tree order', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('textarea'), - a = document.createElement('label'), - b = document.createElement('label'); - e.id = 'control'; - a.htmlFor = b.htmlFor = 'control'; - p.append(a, e, b); - fixture.connect(p as unknown as DOMElementContract); - const labels = e.labels!; - expect(Array.from(labels)).toEqual([a, b]); - p.insertBefore(b, a); - expect(Array.from(labels)).toEqual([b, a]); - e.id = 'other'; - expect(labels.length).toBe(0); - }); - - context.test('does not associate labels with a different tree', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('textarea'), - label = document.createElement('label'); - e.id = 'control'; - label.htmlFor = 'control'; - fixture.connect(e as unknown as DOMElementContract); - expect(e.labels!.length).toBe(0); - expect(label.control).toBeNull(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/textarea/range-text.ts b/packages/web-platform-tests/src/compatibility/elements/textarea/range-text.ts deleted file mode 100644 index d106ec1..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/textarea/range-text.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection'; -export const requires = ['elements.textarea.selection'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('replaces explicit offsets and selects the replacement', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.value = 'abcdef'; - element.setRangeText('XYZ', 2, 4, 'select'); - expect(element.value).toBe('abXYZef'); - expect(element.selectionStart).toBe(2); - expect(element.selectionEnd).toBe(5); - expect(() => element.setRangeText('invalid', 5, 1)).toThrow( - expect.objectContaining({ name: 'IndexSizeError' }), - ); - expect(element.value).toBe('abXYZef'); - }); - context.test('uses the current selection when offsets are omitted', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.value = 'abcdef'; - element.setSelectionRange(2, 4); - element.setRangeText('X'); - expect(element.value).toBe('abXef'); - expect(element.selectionStart).toBe(2); - expect(element.selectionEnd).toBe(3); - }); - - context.test('supports selection replacement modes and rejects reversed offsets', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('textarea'); - e.value = 'abcdef'; - e.setSelectionRange(1, 3); - e.setRangeText('XY', 1, 3, 'select'); - expect(e.value).toBe('aXYdef'); - expect(e.selectionStart).toBe(1); - expect(e.selectionEnd).toBe(3); - e.setRangeText('Q', 1, 3, 'end'); - expect(e.selectionStart).toBe(2); - expect(e.selectionEnd).toBe(2); - expect(() => e.setRangeText('x', 3, 1)).toThrow( - expect.objectContaining({ name: 'IndexSizeError' }), - ); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/textarea/selection.ts b/packages/web-platform-tests/src/compatibility/elements/textarea/selection.ts deleted file mode 100644 index 3eb1d8c..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/textarea/selection.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#textFieldSelection'; -export const requires = ['elements.textarea.value'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('uses UTF-16 offsets and clamps selection boundaries', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.value = 'a😀b'; - element.setSelectionRange(1, 3, 'backward'); - expect(element.selectionStart).toBe(1); - expect(element.selectionEnd).toBe(3); - expect(element.selectionDirection).toBe('backward'); - element.setSelectionRange(99, 99); - expect(element.selectionStart).toBe(4); - expect(element.selectionEnd).toBe(4); - element.setSelectionRange(3, 1); - expect(element.selectionStart).toBe(1); - expect(element.selectionEnd).toBe(1); - element.select(); - expect(element.selectionStart).toBe(0); - expect(element.selectionEnd).toBe(4); - }); - context.test('preserves the selection when assigning the same value', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.value = 'hello'; - element.setSelectionRange(1, 3); - element.value = 'hello'; - expect(element.selectionStart).toBe(1); - expect(element.selectionEnd).toBe(3); - element.value = 'new'; - expect(element.selectionStart).toBe(3); - expect(element.selectionEnd).toBe(3); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/textarea/validation.ts b/packages/web-platform-tests/src/compatibility/elements/textarea/validation.ts deleted file mode 100644 index 706d8cc..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/textarea/validation.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-constraint-validation-api'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('exposes required and custom validity independently', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.required = true; - expect(element.willValidate).toBe(true); - expect(element.validity.valueMissing).toBe(true); - expect(element.checkValidity()).toBe(false); - element.value = 'present'; - expect(element.validity.valueMissing).toBe(false); - element.setCustomValidity('Try again'); - expect(element.validity.customError).toBe(true); - expect(element.validationMessage).toBe('Try again'); - element.setCustomValidity(''); - expect(element.validity.valid).toBe(true); - element.disabled = true; - expect(element.willValidate).toBe(false); - }); - - context.test('required textarea validity tracks its current value', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('textarea'); - e.required = true; - expect(e.validity.valueMissing).toBe(true); - e.value = 'x'; - expect(e.checkValidity()).toBe(true); - e.value = ''; - e.readOnly = true; - expect(e.willValidate).toBe(false); - expect(e.checkValidity()).toBe(true); - }); - - context.test('reports and clears custom validity messages', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('textarea'); - e.setCustomValidity('invalid'); - expect(e.validity.customError).toBe(true); - expect(e.validationMessage).toBe('invalid'); - e.setCustomValidity(''); - expect(e.validity.valid).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/elements/textarea/value.ts b/packages/web-platform-tests/src/compatibility/elements/textarea/value.ts deleted file mode 100644 index 994b68a..0000000 --- a/packages/web-platform-tests/src/compatibility/elements/textarea/value.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-elements.html#the-textarea-element'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('keeps default and dirty values separate', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.defaultValue = 'original'; - expect(element.value).toBe('original'); - element.value = 'edited'; - element.defaultValue = 'replacement'; - expect(element.value).toBe('edited'); - expect(element.defaultValue).toBe('replacement'); - }); - context.test('normalizes line breaks in the API value', (fixture) => { - const document = fixture.document; - const element = document.createElement('textarea'); - element.value = 'a\r\nb\rc'; - expect(element.value).toBe('a\nb\nc'); - }); - - context.test('normalizes newlines and separates dirty value from text defaults', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('textarea'); - e.textContent = 'initial'; - expect(e.defaultValue).toBe('initial'); - expect(e.value).toBe('initial'); - e.value = 'a\r\nb\rc'; - expect(e.value).toBe('a\nb\nc'); - e.textContent = 'new default'; - expect(e.value).toBe('a\nb\nc'); - expect(e.defaultValue).toBe('new default'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/data/custom.ts b/packages/web-platform-tests/src/compatibility/events/data/custom.ts deleted file mode 100644 index a6ca4af..0000000 --- a/packages/web-platform-tests/src/compatibility/events/data/custom.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['CustomEvent'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#interface-customevent'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test('retains detail identity and defaults omitted detail to null', ({ globals }) => { - const CustomEvent = globals('CustomEvent'); - const detail = { answer: 42 }; - const event = new CustomEvent('MixedCase', { - detail, - bubbles: true, - cancelable: true, - composed: true, - }); - expect(event.type).toBe('MixedCase'); - expect(event.detail).toBe(detail); - expect(event.bubbles).toBe(true); - expect(event.cancelable).toBe(true); - expect(event.composed).toBe(true); - const defaults = new CustomEvent('probe'); - expect(defaults.detail).toBeNull(); - }); - - context.test('uses standard defaults for CustomEvent', (fixture) => { - const Constructor = fixture.globals('CustomEvent') as unknown as typeof CustomEvent; - const event = new Constructor('x'); - expect(event.type).toBe('x'); - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - expect(event.composed).toBe(false); - expect(event.isTrusted).toBe(false); - expect(event.detail).toBeNull(); - }); - - context.test('preserves event constructor flags without implicit dispatch', (fixture) => { - const Constructor = fixture.globals('CustomEvent') as unknown as typeof CustomEvent; - const event = new Constructor('x', { bubbles: true, cancelable: true, composed: true }); - expect(event.target).toBeNull(); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.bubbles).toBe(true); - expect(event.cancelable).toBe(true); - expect(event.composed).toBe(true); - event.preventDefault(); - expect(event.defaultPrevented).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/data/focus.ts b/packages/web-platform-tests/src/compatibility/events/data/focus.ts deleted file mode 100644 index ec28da9..0000000 --- a/packages/web-platform-tests/src/compatibility/events/data/focus.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['FocusEvent'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://w3c.github.io/uievents/#interface-focusevent'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test('initializes relatedTarget as synthetic focus data', ({ document, globals }) => { - const FocusEvent = globals('FocusEvent'); - const target = document.createElement('div'); - const event = new FocusEvent('focus', { relatedTarget: target }); - expect(event.relatedTarget).toBe(target); - const defaults = new FocusEvent('blur'); - expect(defaults.relatedTarget).toBeNull(); - expect(event.isTrusted).toBe(false); - }); - - context.test('uses standard defaults for FocusEvent', (fixture) => { - const Constructor = fixture.globals('FocusEvent') as unknown as typeof FocusEvent; - const event = new Constructor('x'); - expect(event.type).toBe('x'); - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - expect(event.composed).toBe(false); - expect(event.isTrusted).toBe(false); - expect(event.relatedTarget).toBeNull(); - }); - - context.test('preserves event constructor flags without implicit dispatch', (fixture) => { - const Constructor = fixture.globals('FocusEvent') as unknown as typeof FocusEvent; - const event = new Constructor('x', { bubbles: true, cancelable: true, composed: true }); - expect(event.target).toBeNull(); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.bubbles).toBe(true); - expect(event.cancelable).toBe(true); - expect(event.composed).toBe(true); - event.preventDefault(); - expect(event.defaultPrevented).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/data/input.ts b/packages/web-platform-tests/src/compatibility/events/data/input.ts deleted file mode 100644 index af264e1..0000000 --- a/packages/web-platform-tests/src/compatibility/events/data/input.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['InputEvent'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://w3c.github.io/uievents/#interface-inputevent'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test('initializes edit data independently of editing behavior', ({ globals }) => { - const InputEvent = globals('InputEvent'); - const event = new InputEvent('beforeinput', { - data: 'abc', - inputType: 'insertText', - isComposing: true, - cancelable: true, - }); - expect([event.data, event.inputType, event.isComposing, event.cancelable]).toEqual([ - 'abc', - 'insertText', - true, - true, - ]); - const defaults = new InputEvent('input'); - expect([defaults.data, defaults.inputType, defaults.isComposing]).toEqual([null, '', false]); - expect(defaults.isTrusted).toBe(false); - }); - - context.test('uses standard defaults for InputEvent', (fixture) => { - const Constructor = fixture.globals('InputEvent') as unknown as typeof InputEvent; - const event = new Constructor('x'); - expect(event.type).toBe('x'); - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - expect(event.composed).toBe(false); - expect(event.isTrusted).toBe(false); - expect(event.data).toBeNull(); - expect(event.inputType).toBe(''); - expect(event.isComposing).toBe(false); - }); - - context.test('preserves event constructor flags without implicit dispatch', (fixture) => { - const Constructor = fixture.globals('InputEvent') as unknown as typeof InputEvent; - const event = new Constructor('x', { bubbles: true, cancelable: true, composed: true }); - expect(event.target).toBeNull(); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.bubbles).toBe(true); - expect(event.cancelable).toBe(true); - expect(event.composed).toBe(true); - event.preventDefault(); - expect(event.defaultPrevented).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/data/keyboard.ts b/packages/web-platform-tests/src/compatibility/events/data/keyboard.ts deleted file mode 100644 index 9bdb6e6..0000000 --- a/packages/web-platform-tests/src/compatibility/events/data/keyboard.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['KeyboardEvent'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://w3c.github.io/uievents/#interface-keyboardevent'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'initializes keyboard data and modifier queries without trusted input', - ({ globals }) => { - const KeyboardEvent = globals('KeyboardEvent'); - const event = new KeyboardEvent('keydown', { - key: 'A', - code: 'KeyA', - location: 1, - repeat: true, - isComposing: true, - ctrlKey: true, - altKey: false, - shiftKey: true, - metaKey: false, - }); - expect([event.key, event.code, event.location, event.repeat, event.isComposing]).toEqual([ - 'A', - 'KeyA', - 1, - true, - true, - ]); - expect([event.ctrlKey, event.altKey, event.shiftKey, event.metaKey]).toEqual([ - true, - false, - true, - false, - ]); - expect( - ['Control', 'Alt', 'Shift', 'Meta', 'Unrecognized'].map((name) => - event.getModifierState(name), - ), - ).toEqual([true, false, true, false, false]); - expect(event.isTrusted).toBe(false); - const defaults = new KeyboardEvent('keyup'); - expect([ - defaults.key, - defaults.code, - defaults.location, - defaults.repeat, - defaults.isComposing, - ]).toEqual(['', '', 0, false, false]); - }, - ); - - context.test('uses standard defaults for KeyboardEvent', (fixture) => { - const Constructor = fixture.globals('KeyboardEvent') as unknown as typeof KeyboardEvent; - const event = new Constructor('x'); - expect(event.type).toBe('x'); - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - expect(event.composed).toBe(false); - expect(event.isTrusted).toBe(false); - expect(event.key).toBe(''); - expect(event.code).toBe(''); - expect(event.location).toBe(0); - expect(event.repeat).toBe(false); - expect(event.ctrlKey).toBe(false); - expect(event.getModifierState('Shift')).toBe(false); - }); - - context.test('preserves event constructor flags without implicit dispatch', (fixture) => { - const Constructor = fixture.globals('KeyboardEvent') as unknown as typeof KeyboardEvent; - const event = new Constructor('x', { bubbles: true, cancelable: true, composed: true }); - expect(event.target).toBeNull(); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.bubbles).toBe(true); - expect(event.cancelable).toBe(true); - expect(event.composed).toBe(true); - event.preventDefault(); - expect(event.defaultPrevented).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/data/mouse.ts b/packages/web-platform-tests/src/compatibility/events/data/mouse.ts deleted file mode 100644 index 648d126..0000000 --- a/packages/web-platform-tests/src/compatibility/events/data/mouse.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['MouseEvent'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://w3c.github.io/uievents/#interface-mouseevent'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'initializes mouse buttons, coordinates, modifiers, and relatedTarget', - ({ document, globals }) => { - const MouseEvent = globals('MouseEvent'); - const relatedTarget = document.createElement('div'); - const event = new MouseEvent('mousedown', { - button: 2, - buttons: 3, - clientX: 12, - clientY: 13, - screenX: 20, - screenY: 21, - ctrlKey: true, - shiftKey: true, - relatedTarget, - }); - expect([ - event.button, - event.buttons, - event.clientX, - event.clientY, - event.screenX, - event.screenY, - ]).toEqual([2, 3, 12, 13, 20, 21]); - expect([event.ctrlKey, event.altKey, event.shiftKey, event.metaKey]).toEqual([ - true, - false, - true, - false, - ]); - expect(event.relatedTarget).toBe(relatedTarget); - expect(event.isTrusted).toBe(false); - const defaults = new MouseEvent('mousemove'); - expect(defaults.relatedTarget).toBeNull(); - }, - ); - - context.test('uses standard defaults for MouseEvent', (fixture) => { - const Constructor = fixture.globals('MouseEvent') as unknown as typeof MouseEvent; - const event = new Constructor('x'); - expect(event.type).toBe('x'); - expect(event.bubbles).toBe(false); - expect(event.cancelable).toBe(false); - expect(event.composed).toBe(false); - expect(event.isTrusted).toBe(false); - expect(event.button).toBe(0); - expect(event.buttons).toBe(0); - expect(event.clientX).toBe(0); - expect(event.ctrlKey).toBe(false); - expect(event.relatedTarget).toBeNull(); - }); - - context.test('preserves event constructor flags without implicit dispatch', (fixture) => { - const Constructor = fixture.globals('MouseEvent') as unknown as typeof MouseEvent; - const event = new Constructor('x', { bubbles: true, cancelable: true, composed: true }); - expect(event.target).toBeNull(); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.bubbles).toBe(true); - expect(event.cancelable).toBe(true); - expect(event.composed).toBe(true); - event.preventDefault(); - expect(event.defaultPrevented).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/bubbling.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/bubbling.ts deleted file mode 100644 index 147ee0c..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/bubbling.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'bubbles from the target through ancestors only when requested', - ({ document, globals }) => { - const Event = globals('Event'); - const root = document.createElement('div'); - const parent = document.createElement('div'); - const target = document.createElement('div'); - root.appendChild(parent); - parent.appendChild(target); - const calls: unknown[] = []; - for (const [node, label] of [ - [root, 'root'], - [parent, 'parent'], - [target, 'target'], - ] as const) { - node.addEventListener('probe', (event) => { - calls.push([ - label, - event.eventPhase, - event.target === target, - event.currentTarget === node, - ]); - }); - } - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual([['target', 2, true, true]]); - calls.length = 0; - target.dispatchEvent(new Event('probe', { bubbles: true })); - expect(calls).toEqual([ - ['target', 2, true, true], - ['parent', 3, true, true], - ['root', 3, true, true], - ]); - }, - ); - - context.test('nonbubbling events reach target but not bubble ancestors', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('i'); - p.append(e); - const seen: string[] = []; - p.addEventListener('x', () => seen.push('parent')); - e.addEventListener('x', () => seen.push('target')); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(seen).toEqual(['target']); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/cancellation.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/cancellation.ts deleted file mode 100644 index 2937938..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/cancellation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'returns false exactly when a cancelable event has been canceled', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - target.addEventListener('probe', (event) => { - event.preventDefault(); - }); - const cancelable = new Event('probe', { cancelable: true }); - expect(target.dispatchEvent(cancelable)).toBe(false); - expect(cancelable.defaultPrevented).toBe(true); - const nonCancelable = new Event('probe'); - expect(target.dispatchEvent(nonCancelable)).toBe(true); - expect(nonCancelable.defaultPrevented).toBe(false); - }, - ); - context.test('retains cancellation when a canceled event is reused', ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const event = new Event('probe', { cancelable: true }); - event.preventDefault(); - expect(target.dispatchEvent(event)).toBe(false); - expect(event.defaultPrevented).toBe(true); - }); - - context.test('noncancelable events ignore preventDefault and return true', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const Event = fixture.globals('Event'), - event = new Event('x'); - e.addEventListener('x', (ev) => ev.preventDefault()); - expect(e.dispatchEvent(event as unknown as Event)).toBe(true); - expect(event.defaultPrevented).toBe(false); - }); - - context.test('preserves canceled state on redispatch', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const Event = fixture.globals('Event'), - event = new Event('x', { cancelable: true }); - event.preventDefault(); - expect(e.dispatchEvent(event as unknown as Event)).toBe(false); - expect(event.defaultPrevented).toBe(true); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/capture.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/capture.ts deleted file mode 100644 index 67a71b6..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/capture.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'captures non-bubbling events down the path, then delivers both target phases', - ({ document, globals }) => { - const Event = globals('Event'); - const root = document.createElement('div'); - const parent = document.createElement('div'); - const target = document.createElement('div'); - root.appendChild(parent); - parent.appendChild(target); - const calls: unknown[] = []; - for (const [node, label] of [ - [root, 'root'], - [parent, 'parent'], - [target, 'target'], - ] as const) { - node.addEventListener( - 'probe', - (event) => { - calls.push([ - label, - 'capture', - event.eventPhase, - event.target === target, - event.currentTarget === node, - ]); - }, - true, - ); - node.addEventListener('probe', (event) => { - calls.push([ - label, - 'bubble', - event.eventPhase, - event.target === target, - event.currentTarget === node, - ]); - }); - } - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual([ - ['root', 'capture', 1, true, true], - ['parent', 'capture', 1, true, true], - ['target', 'capture', 2, true, true], - ['target', 'bubble', 2, true, true], - ]); - }, - ); - context.test( - 'matches removal by capture, whether options are boolean or an object', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - let count = 0; - const listener = (): void => { - count++; - }; - target.addEventListener('probe', listener, true); - target.addEventListener('probe', listener, false); - target.removeEventListener('probe', listener, { capture: true }); - target.dispatchEvent(new Event('probe')); - expect(count).toBe(1); - target.removeEventListener('probe', listener, false); - target.dispatchEvent(new Event('probe')); - expect(count).toBe(1); - }, - ); - - context.test('orders ancestor capture before target capture and target bubble', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('i'); - p.append(e); - const seen: string[] = []; - p.addEventListener('x', () => seen.push('ancestor'), true); - e.addEventListener('x', () => seen.push('target-bubble')); - e.addEventListener('x', () => seen.push('target-capture'), true); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(seen).toEqual(['ancestor', 'target-capture', 'target-bubble']); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/path-snapshot.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/path-snapshot.ts deleted file mode 100644 index 17a861a..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/path-snapshot.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.bubbling'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'keeps the original path when the target is moved during dispatch', - ({ document, globals }) => { - const Event = globals('Event'); - const root = document.createElement('div'); - const oldParent = document.createElement('div'); - const newParent = document.createElement('div'); - const target = document.createElement('div'); - root.appendChild(oldParent); - root.appendChild(newParent); - oldParent.appendChild(target); - const calls: string[] = []; - const path: unknown[] = []; - target.addEventListener('probe', (event) => { - newParent.appendChild(target); - path.push(...event.composedPath()); - }); - oldParent.addEventListener('probe', () => { - calls.push('old'); - }); - newParent.addEventListener('probe', () => { - calls.push('new'); - }); - root.addEventListener('probe', () => { - calls.push('root'); - }); - target.dispatchEvent(new Event('probe', { bubbles: true })); - expect(calls).toEqual(['old', 'root']); - expect(path.length).toBe(3); - expect(path[0]).toBe(target); - expect(path[1]).toBe(oldParent); - expect(path[2]).toBe(root); - expect(target.parentNode).toBe(newParent); - }, - ); - - context.test( - 'keeps original ancestor targets after the target moves during dispatch', - (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - q = document.createElement('div'), - e = document.createElement('i'); - p.append(e); - const seen: unknown[] = []; - e.addEventListener('x', () => q.append(e)); - p.addEventListener('x', (event) => seen.push(event.target)); - q.addEventListener('x', () => seen.push('new-parent')); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x', { bubbles: true }) as unknown as Event); - expect(seen).toEqual([e]); - expect(e.parentNode).toBe(q); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/reentrancy.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/reentrancy.ts deleted file mode 100644 index 5b9349d..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/reentrancy.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'rejects redispatch of the active event but permits a distinct nested event', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const other = document.createElement('div'); - const outer = new Event('outer'); - const nested = new Event('nested'); - const calls: string[] = []; - target.addEventListener('outer', (event) => { - expect(() => other.dispatchEvent(event)).toThrow( - expect.objectContaining({ name: 'InvalidStateError' }), - ); - other.dispatchEvent(nested); - expect(event.currentTarget).toBe(target); - expect(event.eventPhase).toBe(2); - calls.push('outer'); - }); - other.addEventListener('nested', () => { - calls.push('nested'); - }); - target.dispatchEvent(outer); - expect(calls).toEqual(['nested', 'outer']); - expect(other.dispatchEvent(outer)).toBe(true); - }, - ); - - context.test('permits nested dispatch of a different event with independent state', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const Event = fixture.globals('Event'); - const seen: string[] = []; - e.addEventListener('inner', () => seen.push('inner')); - e.addEventListener('outer', () => { - seen.push('before'); - e.dispatchEvent(new Event('inner') as unknown as Event); - seen.push('after'); - }); - e.dispatchEvent(new Event('outer') as unknown as Event); - expect(seen).toEqual(['before', 'inner', 'after']); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/reuse.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/reuse.ts deleted file mode 100644 index 96ee830..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/reuse.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'updates target when an event is reused on another element', - ({ document, globals }) => { - const Event = globals('Event'); - const first = document.createElement('div'); - const second = document.createElement('div'); - const event = new Event('probe'); - first.dispatchEvent(event); - expect(event.target).toBe(first); - second.dispatchEvent(event); - expect(event.target).toBe(second); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.composedPath()).toEqual([]); - }, - ); - - context.test('clears stopPropagation state for the next dispatch', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('i'); - p.append(e); - let count = 0; - const stop = (event: Event): void => event.stopPropagation(); - e.addEventListener('x', stop); - p.addEventListener('x', () => count++); - const Event = fixture.globals('Event'), - event = new Event('x', { bubbles: true }); - e.dispatchEvent(event as unknown as Event); - expect(count).toBe(0); - e.removeEventListener('x', stop); - e.dispatchEvent(event as unknown as Event); - expect(count).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/stop-immediate-propagation.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/stop-immediate-propagation.ts deleted file mode 100644 index 76d1e8f..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/stop-immediate-propagation.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'stops remaining listeners on the current invocation target', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const calls: string[] = []; - target.addEventListener('probe', (event) => { - calls.push('first'); - event.stopImmediatePropagation(); - }); - target.addEventListener('probe', () => { - calls.push('second'); - }); - const event = new Event('probe'); - target.dispatchEvent(event); - target.dispatchEvent(event); - expect(calls).toEqual(['first', 'first']); - }, - ); - context.test( - 'stops both the remaining target capture listeners and the target non-capture invocation', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const calls: string[] = []; - target.addEventListener( - 'probe', - (event) => { - calls.push('first'); - event.stopImmediatePropagation(); - }, - true, - ); - target.addEventListener( - 'probe', - () => { - calls.push('second'); - }, - true, - ); - target.addEventListener('probe', () => { - calls.push('non-capture'); - }); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['first']); - }, - ['events.dispatch.capture'], - ); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/stop-propagation.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/stop-propagation.ts deleted file mode 100644 index bffe56e..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/stop-propagation.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'stops subsequent ancestors while preserving remaining listeners on the same invocation target', - ({ document, globals }) => { - const Event = globals('Event'); - const parent = document.createElement('div'); - const target = document.createElement('div'); - parent.appendChild(target); - const calls: string[] = []; - target.addEventListener('probe', (event) => { - calls.push('first'); - event.stopPropagation(); - }); - target.addEventListener('probe', () => { - calls.push('second'); - }); - parent.addEventListener('probe', () => { - calls.push('parent'); - }); - expect(target.dispatchEvent(new Event('probe', { bubbles: true, cancelable: true }))).toBe( - true, - ); - expect(calls).toEqual(['first', 'second']); - }, - ['events.dispatch.bubbling'], - ); - context.test('resets a stop flag at the end of dispatch', ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const event = new Event('probe'); - let calls = 0; - target.addEventListener('probe', (received) => { - calls++; - received.stopPropagation(); - }); - target.dispatchEvent(event); - target.dispatchEvent(event); - expect(calls).toBe(2); - }); - context.test( - 'honors a stop flag set before dispatch and clears it afterward', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const event = new Event('probe'); - let calls = 0; - target.addEventListener('probe', () => { - calls++; - }); - event.stopPropagation(); - target.dispatchEvent(event); - expect(calls).toBe(0); - target.dispatchEvent(event); - expect(calls).toBe(1); - }, - ); - context.test( - 'stops between target capture and target non-capture invocations', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const calls: string[] = []; - target.addEventListener( - 'probe', - (event) => { - calls.push('capture first'); - event.stopPropagation(); - }, - true, - ); - target.addEventListener( - 'probe', - () => { - calls.push('capture second'); - }, - true, - ); - target.addEventListener('probe', () => { - calls.push('non-capture'); - }); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['capture first', 'capture second']); - }, - ['events.dispatch.capture'], - ); - context.test( - 'stops before reaching the target when an ancestor capture listener requests it', - ({ document, globals }) => { - const Event = globals('Event'); - const parent = document.createElement('div'); - const target = document.createElement('div'); - parent.appendChild(target); - const calls: string[] = []; - parent.addEventListener( - 'probe', - (event) => { - calls.push('parent'); - event.stopPropagation(); - }, - true, - ); - target.addEventListener( - 'probe', - () => { - calls.push('target capture'); - }, - true, - ); - target.addEventListener('probe', () => { - calls.push('target non-capture'); - }); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['parent']); - }, - ['events.dispatch.capture'], - ); -} diff --git a/packages/web-platform-tests/src/compatibility/events/dispatch/target.ts b/packages/web-platform-tests/src/compatibility/events/dispatch/target.ts deleted file mode 100644 index 742c8bc..0000000 --- a/packages/web-platform-tests/src/compatibility/events/dispatch/target.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-dispatch'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'delivers a synthetic event to the target with the correct receiver and cleans up state', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const event = new Event('MixedCase'); - const seen: unknown[] = []; - target.addEventListener('MixedCase', function (received) { - seen.push(received, this, received.target, received.currentTarget, received.eventPhase); - }); - expect(event.target).toBeNull(); - expect(target.dispatchEvent(event)).toBe(true); - expect(seen.length).toBe(5); - expect(seen[0]).toBe(event); - expect(seen[1]).toBe(target); - expect(seen[2]).toBe(target); - expect(seen[3]).toBe(target); - expect(seen[4]).toBe(2); - expect(event.target).toBe(target); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.composedPath()).toEqual([]); - expect(event.isTrusted).toBe(false); - }, - ); - context.test('does not lowercase event names', ({ document, globals }) => { - const target = document.createElement('div'); - let count = 0; - target.addEventListener('MixedCase', () => { - count++; - }); - const Event = globals('Event'); - target.dispatchEvent(new Event('mixedcase')); - expect(count).toBe(0); - target.dispatchEvent(new Event('MixedCase')); - expect(count).toBe(1); - }); - - context.test( - 'clears currentTarget and phase after dispatch while retaining target', - (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const Event = fixture.globals('Event'), - event = new Event('x'); - let phase = 0; - const receivers: unknown[] = []; - e.addEventListener('x', function (this: HTMLElement, ev) { - phase = ev.eventPhase; - receivers.push(this); - }); - e.dispatchEvent(event as unknown as Event); - expect(phase).toBe(2); - expect(receivers).toEqual([e]); - expect(event.target).toBe(e); - expect(event.currentTarget).toBeNull(); - expect(event.eventPhase).toBe(0); - expect(event.composedPath()).toEqual([]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/events/listeners/abort-signal.ts b/packages/web-platform-tests/src/compatibility/events/listeners/abort-signal.ts deleted file mode 100644 index e170010..0000000 --- a/packages/web-platform-tests/src/compatibility/events/listeners/abort-signal.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event', 'AbortController'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#add-an-event-listener'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/dom/events/AddEventListenerOptions-signal.any.js'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'removes a listener when its signal aborts and ignores already-aborted signals', - ({ document, globals }) => { - const Event = globals('Event'); - const AbortController = globals('AbortController'); - const target = document.createElement('div'); - const controller = new AbortController(); - let calls = 0; - const listener = (): void => { - calls++; - }; - target.addEventListener('probe', listener, { signal: controller.signal }); - target.dispatchEvent(new Event('probe')); - expect(calls).toBe(1); - controller.abort(); - target.dispatchEvent(new Event('probe')); - expect(calls).toBe(1); - target.addEventListener('probe', listener, { signal: controller.signal }); - target.dispatchEvent(new Event('probe')); - expect(calls).toBe(1); - }, - ); - context.test( - 'aborting a signal removes listeners still waiting in the current invocation', - ({ document, globals }) => { - const Event = globals('Event'); - const AbortController = globals('AbortController'); - const target = document.createElement('div'); - const controller = new AbortController(); - const calls: string[] = []; - target.addEventListener('probe', () => { - calls.push('first'); - controller.abort(); - }); - target.addEventListener( - 'probe', - () => { - calls.push('aborted'); - }, - { signal: controller.signal }, - ); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['first']); - }, - ); - context.test('validates a signal even when the callback is null', ({ document }) => { - const target = document.createElement('div'); - expect(() => { - // @ts-expect-error -- Verify Web IDL rejects null instead of an AbortSignal. - target.addEventListener('probe', null, { signal: null }); - }).toThrow(expect.objectContaining({ name: 'TypeError' })); - }); - - context.test( - 'ignores already aborted signals and removes multiple listeners synchronously', - (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const AbortController = fixture.globals('AbortController'), - Event = fixture.globals('Event'); - const controller = new AbortController(); - let n = 0; - e.addEventListener('x', () => n++, { signal: controller.signal }); - e.addEventListener('y', () => n++, { signal: controller.signal }); - controller.abort(); - e.addEventListener('x', () => n++, { signal: controller.signal }); - e.dispatchEvent(new Event('x') as unknown as Event); - e.dispatchEvent(new Event('y') as unknown as Event); - expect(n).toBe(0); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/events/listeners/exception-reporting.ts b/packages/web-platform-tests/src/compatibility/events/listeners/exception-reporting.ts deleted file mode 100644 index 4f70b39..0000000 --- a/packages/web-platform-tests/src/compatibility/events/listeners/exception-reporting.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMEvent } from '../../contracts/events.ts'; - -const requiredGlobals = ['Event', 'ErrorEvent', 'window'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://html.spec.whatwg.org/multipage/webappapis.html#report-an-exception'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'reports listener exceptions as global error events without interrupting dispatch', - ({ document, globals, cleanup }) => { - const Event = globals('Event'); - const ErrorEvent = globals('ErrorEvent'); - const window = globals('window'); - const target = document.createElement('div'); - const failure = new TypeError('compatibility listener failure'); - const errors: unknown[] = []; - const listener = (event: DOMEvent): void => { - if (event instanceof ErrorEvent && event.error === failure) { - errors.push(event.error); - event.preventDefault(); - } - }; - window.addEventListener('error', listener); - cleanup(() => { - window.removeEventListener('error', listener); - }); - let following = 0; - target.addEventListener('probe', () => { - throw failure; - }); - target.addEventListener('probe', () => { - following++; - }); - expect(target.dispatchEvent(new Event('probe'))).toBe(true); - expect(following).toBe(1); - expect(errors.length).toBe(1); - expect(errors[0]).toBe(failure); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/events/listeners/objects.ts b/packages/web-platform-tests/src/compatibility/events/listeners/objects.ts deleted file mode 100644 index c354c24..0000000 --- a/packages/web-platform-tests/src/compatibility/events/listeners/objects.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'calls handleEvent with the listener object as its receiver', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const receivers: unknown[] = []; - const calls: string[] = []; - const listener = { - handleEvent() { - receivers.push(this); - calls.push('initial'); - }, - }; - target.addEventListener('probe', listener); - target.dispatchEvent(new Event('probe')); - listener.handleEvent = function () { - receivers.push(this); - calls.push('replacement'); - }; - target.dispatchEvent(new Event('probe')); - expect(receivers.length).toBe(2); - expect(receivers[0]).toBe(listener); - expect(receivers[1]).toBe(listener); - expect(calls).toEqual(['initial', 'replacement']); - target.removeEventListener('probe', listener); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['initial', 'replacement']); - }, - ); - - context.test( - 'looks up handleEvent at invocation and preserves the object receiver', - (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'), - seen: unknown[] = []; - const listener = { - handleEvent() { - seen.push(this); - }, - }; - e.addEventListener('x', listener); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x') as unknown as Event); - listener.handleEvent = function () { - seen.push('changed', this); - }; - e.dispatchEvent(new Event('x') as unknown as Event); - expect(seen).toEqual([listener, 'changed', listener]); - e.removeEventListener('x', listener); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(seen.length).toBe(3); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/events/listeners/once.ts b/packages/web-platform-tests/src/compatibility/events/listeners/once.ts deleted file mode 100644 index 76148d9..0000000 --- a/packages/web-platform-tests/src/compatibility/events/listeners/once.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target', 'events.listeners.registration'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/dom/events/AddEventListenerOptions-once.any.js'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'removes a once listener before calling it, including nested dispatch', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - let calls = 0; - target.addEventListener( - 'probe', - () => { - calls++; - target.dispatchEvent(new Event('probe')); - }, - { once: true }, - ); - target.dispatchEvent(new Event('probe')); - target.dispatchEvent(new Event('probe')); - expect(calls).toBe(1); - }, - ); - context.test( - 'does not replace an existing registration when once changes', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - let calls = 0; - const listener = (): void => { - calls++; - }; - target.addEventListener('probe', listener); - target.addEventListener('probe', listener, { once: true }); - target.dispatchEvent(new Event('probe')); - target.dispatchEvent(new Event('probe')); - expect(calls).toBe(2); - }, - ); - - context.test('removes once listeners before nested dispatch', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - let n = 0; - const Event = fixture.globals('Event'); - e.addEventListener( - 'x', - () => { - n++; - e.dispatchEvent(new Event('x') as unknown as Event); - }, - { once: true }, - ); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(n).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/listeners/passive.ts b/packages/web-platform-tests/src/compatibility/events/listeners/passive.ts deleted file mode 100644 index 219d035..0000000 --- a/packages/web-platform-tests/src/compatibility/events/listeners/passive.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.cancellation'] as const; -export const spec = 'https://dom.spec.whatwg.org/#set-the-canceled-flag'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/dom/events/AddEventListenerOptions-passive.any.js'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'prevents cancellation only while invoking a passive listener', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const prevented: boolean[] = []; - target.addEventListener( - 'probe', - (event) => { - event.preventDefault(); - prevented.push(event.defaultPrevented); - }, - { passive: true }, - ); - target.addEventListener( - 'probe', - (event) => { - event.preventDefault(); - prevented.push(event.defaultPrevented); - }, - { passive: false }, - ); - expect(target.dispatchEvent(new Event('probe', { cancelable: true }))).toBe(false); - expect(prevented).toEqual([false, true]); - }, - ); - - context.test('passive listeners cannot cancel but ordinary listeners still can', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const Event = fixture.globals('Event'), - seen: boolean[] = []; - e.addEventListener( - 'x', - (ev) => { - ev.preventDefault(); - seen.push(ev.defaultPrevented); - }, - { passive: true }, - ); - e.addEventListener('x', (ev) => { - ev.preventDefault(); - seen.push(ev.defaultPrevented); - }); - const event = new Event('x', { cancelable: true }); - expect(e.dispatchEvent(event as unknown as Event)).toBe(false); - expect(seen).toEqual([false, true]); - }); - - context.test('passive listeners can still stop propagation', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('i'); - p.append(e); - let count = 0; - e.addEventListener('x', (ev) => ev.stopPropagation(), { passive: true }); - p.addEventListener('x', () => count++); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x', { bubbles: true }) as unknown as Event); - expect(count).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/listeners/registration.ts b/packages/web-platform-tests/src/compatibility/events/listeners/registration.ts deleted file mode 100644 index 69edbc3..0000000 --- a/packages/web-platform-tests/src/compatibility/events/listeners/registration.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-listener'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test('deduplicates by callback identity and event type', ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - let calls = 0; - const listener = (): void => { - calls++; - }; - target.addEventListener('probe', listener); - target.addEventListener('probe', listener); - target.addEventListener('other', listener); - target.addEventListener('probe', null); - target.dispatchEvent(new Event('probe')); - expect(calls).toBe(1); - target.dispatchEvent(new Event('other')); - expect(calls).toBe(2); - target.removeEventListener('probe', listener); - target.removeEventListener('probe', listener); - target.removeEventListener('probe', null); - target.dispatchEvent(new Event('probe')); - expect(calls).toBe(2); - }); - - context.test('deduplicates by type, callback and capture independently of once', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const calls: string[] = []; - const callback = (): number => calls.push('x'); - e.addEventListener('x', callback); - e.addEventListener('x', callback, { once: true }); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x') as unknown as Event); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(calls).toEqual(['x', 'x']); - e.removeEventListener('x', callback, { capture: true }); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(calls.length).toBe(3); - e.removeEventListener('x', callback); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(calls.length).toBe(3); - }); - - context.test('ignores null listeners and matches event type case sensitively', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - let n = 0; - Reflect.apply(e.addEventListener, e, ['x', null]); - e.addEventListener('X', () => n++); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(n).toBe(0); - e.dispatchEvent(new Event('X') as unknown as Event); - expect(n).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/events/listeners/snapshots.ts b/packages/web-platform-tests/src/compatibility/events/listeners/snapshots.ts deleted file mode 100644 index 3fa47b0..0000000 --- a/packages/web-platform-tests/src/compatibility/events/listeners/snapshots.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target', 'events.listeners.registration'] as const; -export const spec = 'https://dom.spec.whatwg.org/#concept-event-listener-invoke'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'removals take effect immediately and additions do not enter an active invocation', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const calls: string[] = []; - let first = true; - const removed = (): void => { - calls.push('removed'); - }; - const added = (): void => { - calls.push('added'); - }; - target.addEventListener('probe', () => { - calls.push('first'); - if (first) { - first = false; - target.removeEventListener('probe', removed); - target.addEventListener('probe', removed); - target.addEventListener('probe', added); - } - }); - target.addEventListener('probe', removed); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['first']); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['first', 'first', 'removed', 'added']); - }, - ); - context.test( - 'takes a fresh snapshot for the target non-capture invocation', - ({ document, globals }) => { - const Event = globals('Event'); - const target = document.createElement('div'); - const calls: string[] = []; - target.addEventListener( - 'probe', - () => { - calls.push('capture'); - target.addEventListener('probe', () => { - calls.push('added non-capture'); - }); - target.addEventListener( - 'probe', - () => { - calls.push('added capture'); - }, - true, - ); - }, - true, - ); - target.dispatchEvent(new Event('probe')); - expect(calls).toEqual(['capture', 'added non-capture']); - }, - ['events.dispatch.capture'], - ); - context.test( - 'permits listeners added before a later ancestor invocation', - ({ document, globals }) => { - const Event = globals('Event'); - const parent = document.createElement('div'); - const target = document.createElement('div'); - parent.appendChild(target); - const calls: string[] = []; - target.addEventListener('probe', () => { - parent.addEventListener('probe', () => { - calls.push('new ancestor listener'); - }); - }); - target.dispatchEvent(new Event('probe', { bubbles: true })); - expect(calls).toEqual(['new ancestor listener']); - }, - ['events.dispatch.bubbling'], - ); - - context.test('removal before a listener turn prevents its invocation', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - const seen: string[] = []; - const second = (): number => seen.push('second'); - e.addEventListener('x', () => { - seen.push('first'); - e.removeEventListener('x', second); - }); - e.addEventListener('x', second); - const Event = fixture.globals('Event'); - e.dispatchEvent(new Event('x') as unknown as Event); - expect(seen).toEqual(['first']); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/features.ts b/packages/web-platform-tests/src/compatibility/features.ts deleted file mode 100644 index dd42c4d..0000000 --- a/packages/web-platform-tests/src/compatibility/features.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { Type } from 'typebox'; -import { Check, Errors } from 'typebox/value'; -import type { StandardSchemaV1 } from '@standard-schema/spec'; -import type { TLocalizedValidationError } from 'typebox/error'; - -/** Core DOM first, then higher-level APIs and host interactions. Values describe the schema. */ -export const featureShape = { - document: { - basicCreation: true, - creation: true, - lookup: true, - importNode: true, - xml: true, - parsing: true, - xmlSerialization: true, - }, - node: { - basicAppend: true, - relationships: true, - connectivity: true, - liveCollections: true, - textContent: true, - append: true, - insertBefore: true, - remove: true, - replace: true, - fragments: true, - variadicMutation: true, - cloning: true, - comparison: true, - normalization: true, - adoption: true, - }, - characterData: { - stringStorage: true, - data: true, - editing: true, - splitText: true, - }, - element: { - childElements: true, - attributes: true, - attributeNodes: true, - namespaces: true, - classList: true, - dataset: true, - reflection: true, - selectors: true, - }, - traversal: { - nodeIterator: true, - treeWalker: true, - }, - parsing: { - fragments: true, - namespaces: true, - serialization: true, - contextual: true, - markupMutation: true, - recovery: true, - }, - templates: { - content: true, - cloning: true, - parsing: true, - }, - events: { - data: { - custom: true, - focus: true, - keyboard: true, - mouse: true, - input: true, - }, - dispatch: { - target: true, - capture: true, - bubbling: true, - cancellation: true, - stopPropagation: true, - stopImmediatePropagation: true, - pathSnapshot: true, - reuse: true, - reentrancy: true, - }, - listeners: { - registration: true, - objects: true, - once: true, - passive: true, - abortSignal: true, - snapshots: true, - exceptionReporting: true, - }, - }, - mutationObserver: { - registration: true, - records: { - childList: true, - attributes: true, - characterData: true, - }, - filtering: true, - oldValue: true, - subtree: true, - takeRecords: true, - disconnect: true, - microtaskDelivery: true, - detached: true, - reentrancy: true, - }, - focus: { - eligibility: true, - transitions: true, - removal: true, - inert: true, - reentrancy: true, - }, - selection: { - range: { - boundaries: true, - comparison: true, - stringification: true, - cloneContents: true, - deleteContents: true, - extractContents: true, - liveBoundaries: true, - mutation: true, - }, - document: { - rangeManagement: true, - direction: true, - connectivity: true, - liveUpdates: true, - mutation: true, - changeEvents: true, - }, - }, - elements: { - progress: true, - meter: true, - option: { - attributes: true, - text: true, - selectedness: true, - }, - optgroup: true, - datalist: true, - input: { - value: true, - sanitization: true, - colorValue: true, - numericValue: true, - dateValue: true, - stepping: true, - checkedness: true, - radioGroups: true, - selection: true, - rangeText: true, - validation: true, - formAssociation: true, - labels: true, - activation: true, - cloning: true, - }, - textarea: { - value: true, - selection: true, - rangeText: true, - validation: true, - formAssociation: true, - labels: true, - cloning: true, - }, - select: { - options: true, - indexedOptions: true, - selection: true, - mutation: true, - validation: true, - cloning: true, - }, - selectedcontent: true, - form: { - controls: true, - namedControls: true, - reset: true, - validation: true, - submission: true, - formData: true, - dataEvents: true, - }, - button: { - attributes: true, - formAssociation: true, - activation: true, - }, - label: { - association: true, - activation: true, - }, - fieldset: { - controls: true, - disabled: true, - }, - legend: true, - output: { - htmlFor: true, - value: true, - reset: true, - }, - details: { - open: true, - toggleEvents: true, - exclusiveGroups: true, - }, - summary: { - activation: true, - }, - dialog: { - show: true, - close: true, - modal: true, - requestClose: true, - focus: true, - formSubmission: true, - }, - }, - customElements: { - registration: true, - construction: true, - upgrading: true, - observedAttributes: true, - connection: true, - adoption: true, - reactionOrdering: true, - reentrancy: true, - exceptionReporting: true, - formAssociation: true, - }, - shadow: { - roots: { - attachment: true, - hostEligibility: true, - connectivity: true, - cloning: true, - adoption: true, - }, - slots: { - named: true, - fallback: true, - manual: true, - slotchange: true, - observerOrdering: true, - }, - events: { - retargeting: true, - composedPath: true, - relatedTarget: true, - }, - focus: { - retargeting: true, - delegation: true, - }, - declarative: true, - serialization: true, - }, - style: { - inline: true, - parsing: true, - attributeSynchronization: true, - customProperties: true, - stylesheets: true, - computed: true, - cascade: true, - layers: true, - constructedStylesheets: true, - focusState: true, - controlSelectors: true, - relationalSelectors: true, - mutationObservation: true, - }, - input: { - keyboard: { - delivery: true, - modifiers: true, - navigation: true, - activation: true, - editing: true, - multilineEditing: true, - numberStepping: true, - focusStyles: true, - focusVisible: true, - shadow: true, - implicitSubmission: true, - trustedEvents: true, - }, - mouse: { - delivery: true, - buttons: true, - hover: true, - focus: true, - activation: true, - stateStyles: true, - focusVisible: true, - shadow: true, - trustedEvents: true, - }, - }, -} as const; - -export type FeatureFlags = { - readonly [K in keyof T]: T[K] extends boolean ? boolean : FeatureFlags; -}; -export type DOMFeatures = FeatureFlags; -type Paths = { - [K in keyof T & string]: T[K] extends boolean ? K : `${K}.${Paths}`; -}[keyof T & string]; -export type DOMFeaturePath = Paths; - -interface FeatureBranch { - readonly [key: string]: boolean | FeatureBranch; -} - -function featureSchema(shape: FeatureBranch): Type.TSchema { - return Type.Object( - Object.fromEntries( - Object.entries(shape).map(([key, value]) => [ - key, - typeof value === 'boolean' ? Type.Boolean() : featureSchema(value), - ]), - ), - { additionalProperties: false }, - ); -} - -// Both the schema and DOMFeatures derive from featureShape. Object.entries loses its literal keys. -const featureType = Type.Unsafe(featureSchema(featureShape)); - -function featureIssues(error: TLocalizedValidationError): StandardSchemaV1.Issue[] { - const path = error.instancePath - .split('/') - .slice(1) - .map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~')); - const keys = - error.keyword === 'required' - ? error.params.requiredProperties - : error.keyword === 'additionalProperties' - ? error.params.additionalProperties - : []; - return keys.length - ? keys.map((key) => ({ message: error.message, path: [...path, key] })) - : [{ message: error.message, path }]; -} - -/** Validate configuration only; DOM behavior is verified directly by enabled cases. */ -export const domFeaturesSchema: StandardSchemaV1 = { - '~standard': { - version: 1, - vendor: 'typebox', - validate(value) { - if (Check(featureType, value)) return { value }; - return { issues: Errors(featureType, value).flatMap(featureIssues) }; - }, - }, -}; - -/** Compatibility enabled support. */ -export function enabled(features: DOMFeatures, path: DOMFeaturePath): boolean { - let value: unknown = features; - for (const key of path.split('.')) { - if (typeof value !== 'object' || value === null) return false; - value = Reflect.get(value, key); - } - return value === true; -} - -/** File paths are a mechanical spelling of the exhaustive schema paths. */ -export function featureFile(path: string): string { - return ( - path - .split('.') - .map((part) => part.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)) - .join('/') + '.ts' - ); -} - -/** Compatibility featurePaths support. */ -export function featurePaths(shape: FeatureBranch = featureShape, prefix = ''): string[] { - return Object.entries(shape).flatMap(([key, value]) => { - const path = prefix ? `${prefix}.${key}` : key; - return typeof value === 'boolean' ? [path] : featurePaths(value, path); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/focus/eligibility.ts b/packages/web-platform-tests/src/compatibility/focus/eligibility.ts deleted file mode 100644 index ed8cd3f..0000000 --- a/packages/web-platform-tests/src/compatibility/focus/eligibility.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://html.spec.whatwg.org/multipage/interaction.html#focusable-area'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'focuses enabled controls and explicitly focusable HTML elements', - ({ document, connect }) => { - const root = document.createElement('div'); - const button = document.createElement('button'); - const custom = document.createElement('div'); - custom.tabIndex = -1; - root.append(button, custom); - connect(root); - button.focus(); - expect(document.activeElement).toBe(button); - custom.focus(); - expect(document.activeElement).toBe(custom); - button.setAttribute('disabled', ''); - button.focus(); - expect(document.activeElement).toBe(custom); - const detached = document.createElement('button'); - detached.focus(); - expect(document.activeElement).toBe(custom); - }, - ); - - context.test( - 'rejects disabled and hidden controls but accepts negative tabindex focus', - (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - disabled = document.createElement('input'), - hidden = document.createElement('input'), - target = document.createElement('div'); - disabled.disabled = true; - hidden.type = 'hidden'; - target.tabIndex = -1; - p.append(disabled, hidden, target); - fixture.connect(p as unknown as DOMElementContract); - target.focus(); - expect(document.activeElement).toBe(target); - disabled.focus(); - expect(document.activeElement).toBe(target); - hidden.focus(); - expect(document.activeElement).toBe(target); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/focus/inert.ts b/packages/web-platform-tests/src/compatibility/focus/inert.ts deleted file mode 100644 index 3d49b5e..0000000 --- a/packages/web-platform-tests/src/compatibility/focus/inert.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Adapted from WPT inert-node-is-unfocusable.html and inert-in-shadow-dom.html at -// b719612f963a05ece9c89a7dd7598c1c59618874. Copyright web-platform-tests contributors; -// Alice Boxhall. BSD-3-Clause; see ../../../test/wpt/LICENSE and ../../../docs/wpt-state-styles.md. -import type { CompatibilityContext } from '../context.ts'; -import type { DOMElement } from '../contracts/nodes.ts'; -export const spec = 'https://html.spec.whatwg.org/multipage/interaction.html#inert-subtrees'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/inert/inert-node-is-unfocusable.html'; -export const requires = [ - 'focus.eligibility', - 'focus.transitions', - 'events.dispatch.capture', - 'element.selectors', - 'element.attributes', - 'parsing.fragments', -] as const; -/** Registers inertness reflection and focus restrictions across trees. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'reflects explicit inertness without reflecting ancestor inertness on descendants', - (fixture) => { - const document = fixture.document as unknown as Document; - const parent = document.createElement('div'), - child = document.createElement('button'); - parent.append(child); - expect(parent.inert).toBe(false); - parent.inert = true; - expect(parent.hasAttribute('inert')).toBe(true); - expect(child.inert).toBe(false); - parent.removeAttribute('inert'); - expect(parent.inert).toBe(false); - parent.setAttribute('inert', 'false'); - expect(parent.inert).toBe(true); - parent.inert = false; - expect(parent.hasAttribute('inert')).toBe(false); - }, - ); - context.test( - 'blocks focus and focus events throughout an inert subtree and restores eligibility', - (fixture) => { - const document = fixture.document as unknown as Document; - const root = document.createElement('div'); - root.innerHTML = - '
      LinkSpan
      '; - fixture.connect(root as unknown as DOMElement); - const outside = root.querySelector('#outside')!, - parent = root.querySelector('section')!; - outside.focus(); - let events = 0; - parent.addEventListener('focus', () => events++, true); - const children = Array.from(parent.children) as HTMLElement[]; - for (const child of children) { - child.focus(); - expect(document.activeElement).toBe(outside); - } - expect(events).toBe(0); - parent.inert = false; - for (const child of children) { - child.focus(); - expect(document.activeElement).toBe(child); - } - expect(events).toBe(children.length); - }, - ); - context.test( - 'a nested inert attribute continues blocking focus when its ancestor is cleared', - (fixture) => { - const document = fixture.document as unknown as Document; - const parent = document.createElement('div'), - child = document.createElement('button'), - outside = document.createElement('button'); - parent.inert = true; - child.inert = true; - parent.append(child); - fixture.connect(parent as unknown as DOMElement); - fixture.connect(outside as unknown as DOMElement); - outside.focus(); - parent.inert = false; - child.focus(); - expect(document.activeElement).toBe(outside); - child.inert = false; - child.focus(); - expect(document.activeElement).toBe(child); - }, - ); - context.test( - 'inertness does not make a control match disabled', - (fixture) => { - const document = fixture.document as unknown as Document; - const button = document.createElement('button'); - button.inert = true; - expect(button.matches(':disabled')).toBe(false); - expect(button.matches(':enabled')).toBe(true); - button.disabled = true; - expect(button.matches(':disabled')).toBe(true); - }, - ['element.selectors'], - ); - for (const mode of ['open', 'closed'] as const) { - context.test( - `inert host blocks both slotted and ${mode} shadow descendants`, - (fixture) => { - const document = fixture.document as unknown as Document; - const host = document.createElement('div'), - outside = document.createElement('button'), - slotted = document.createElement('button'), - internal = document.createElement('button'); - const shadow = host.attachShadow({ mode }); - shadow.append(document.createElement('slot'), internal); - host.append(slotted); - host.inert = true; - fixture.connect(host as unknown as DOMElement); - fixture.connect(outside as unknown as DOMElement); - outside.focus(); - for (const target of [slotted, internal]) { - target.focus(); - expect(document.activeElement).toBe(outside); - expect(shadow.activeElement).toBeNull(); - } - host.inert = false; - slotted.focus(); - expect(document.activeElement).toBe(slotted); - internal.focus(); - expect(document.activeElement).toBe(host); - expect(shadow.activeElement).toBe(internal); - }, - ['shadow.slots.named', 'shadow.focus.retargeting'], - ); - } -} diff --git a/packages/web-platform-tests/src/compatibility/focus/reentrancy.ts b/packages/web-platform-tests/src/compatibility/focus/reentrancy.ts deleted file mode 100644 index 94d715e..0000000 --- a/packages/web-platform-tests/src/compatibility/focus/reentrancy.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://html.spec.whatwg.org/multipage/interaction.html#focusing-steps'; -export const requires = ['focus.eligibility', 'events.dispatch.target'] as const; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'allows a focus listener to move focus to another element', - ({ document, connect }) => { - const root = document.createElement('div'); - const first = document.createElement('button'); - const second = document.createElement('button'); - root.append(first, second); - connect(root); - const calls: string[] = []; - first.addEventListener('focus', () => { - calls.push('first'); - second.focus(); - }); - second.addEventListener('focus', () => calls.push('second')); - first.focus(); - expect(document.activeElement).toBe(second); - expect(calls).toEqual(['first', 'second']); - }, - ); - - context.test('focus handlers can synchronously redirect focus to another control', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('input'), - b = document.createElement('input'); - p.append(a, b); - fixture.connect(p as unknown as DOMElementContract); - const seen: string[] = []; - a.addEventListener('focus', () => { - seen.push('a'); - b.focus(); - }); - b.addEventListener('focus', () => seen.push('b')); - a.focus(); - expect(document.activeElement).toBe(b); - expect(seen).toEqual(['a', 'b']); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/focus/removal.ts b/packages/web-platform-tests/src/compatibility/focus/removal.ts deleted file mode 100644 index da00a9e..0000000 --- a/packages/web-platform-tests/src/compatibility/focus/removal.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://html.spec.whatwg.org/multipage/infrastructure.html#dom-trees'; -export const requires = ['focus.eligibility'] as const; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'removing a focused node clears focus without firing blur', - ({ document, connect }) => { - const root = document.createElement('div'); - const button = document.createElement('button'); - root.append(button); - connect(root); - button.focus(); - let blurred = 0; - button.addEventListener('blur', () => blurred++); - button.remove(); - expect(document.activeElement).not.toBe(button); - expect(blurred).toBe(0); - root.append(button); - expect(document.activeElement).not.toBe(button); - }, - ); - - context.test('removing an unrelated subtree preserves the focused element', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('input'), - other = document.createElement('div'); - p.append(e, other); - fixture.connect(p as unknown as DOMElementContract); - e.focus(); - other.remove(); - expect(document.activeElement).toBe(e); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/focus/transitions.ts b/packages/web-platform-tests/src/compatibility/focus/transitions.ts deleted file mode 100644 index 6297b3d..0000000 --- a/packages/web-platform-tests/src/compatibility/focus/transitions.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; -import type { DOMEvent } from '../contracts/events.ts'; -export const spec = 'https://html.spec.whatwg.org/multipage/interaction.html#focus-update-steps'; -const requiredGlobals = ['FocusEvent'] as const; -export { requiredGlobals as globals }; -export const requires = ['focus.eligibility', 'events.dispatch.target'] as const; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'delivers focus transitions with related targets and ignores repeated focus', - ({ document, connect, globals, cleanup }) => { - const FocusEvent = globals('FocusEvent'); - const root = document.createElement('div'); - const first = document.createElement('button'); - const second = document.createElement('button'); - root.append(first, second); - connect(root); - first.focus(); - const calls: string[] = []; - for (const node of [first, second]) - for (const type of ['blur', 'focusout', 'focus', 'focusin']) { - const listener = (event: DOMEvent): void => { - if (!(event instanceof FocusEvent)) return expect.fail('Expected FocusEvent'); - expect(event.relatedTarget).toBe(node === first ? second : first); - calls.push(type); - }; - node.addEventListener(type, listener); - cleanup(() => node.removeEventListener(type, listener)); - } - second.focus(); - expect(document.activeElement).toBe(second); - expect(calls).toEqual(['blur', 'focusout', 'focus', 'focusin']); - second.focus(); - first.blur(); - expect(calls).toHaveLength(4); - }, - ); - - context.test( - 'repeated focus does not refire focus or blur and blur releases activeElement', - (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - fixture.connect(e as unknown as DOMElementContract); - const seen: string[] = []; - e.addEventListener('focus', () => seen.push('focus')); - e.addEventListener('blur', () => seen.push('blur')); - e.focus(); - e.focus(); - expect(seen).toEqual(['focus']); - e.blur(); - expect(seen).toEqual(['focus', 'blur']); - expect(document.activeElement).not.toBe(e); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/globals.ts b/packages/web-platform-tests/src/compatibility/globals.ts deleted file mode 100644 index 4eb6225..0000000 --- a/packages/web-platform-tests/src/compatibility/globals.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { DOMDocument, DOMElement, DOMHTMLElement } from './contracts/nodes.ts'; -import type { - DOMEvent, - DOMEventTarget, - DOMEventConstructor, - DOMEventTargetConstructor, - DOMCustomEventConstructor, - DOMKeyboardEventConstructor, - DOMMouseEventConstructor, - DOMFocusEventConstructor, - DOMInputEventConstructor, -} from './contracts/events.ts'; -import type { DOMMutationObserverConstructor } from './contracts/observation.ts'; -import type { DOMTemplateElement, DOMCustomElementRegistry } from './contracts/shadow.ts'; -import type { - DOMFormElement, - DOMInputElement, - DOMTextAreaElement, - DOMSelectElement, -} from './contracts/elements.ts'; - -/** Implementations supply only the bindings needed by their selected features. */ -export type DOMGlobalBindings = Record; - -interface DOMFormData { - get(name: string): string | File | null; - getAll(name: string): (string | File)[]; - entries(): IterableIterator<[string, string | File]>; -} - -/** Idealized test contracts, not a claim about an implementation's supported APIs. */ -export interface DOMGlobals extends DOMGlobalBindings { - Document: new () => DOMDocument; - Element: abstract new (...args: never[]) => DOMElement; - HTMLElement: new () => DOMHTMLElement; - HTMLTemplateElement: abstract new (...args: never[]) => DOMTemplateElement; - HTMLInputElement: abstract new (...args: never[]) => DOMInputElement; - HTMLTextAreaElement: abstract new (...args: never[]) => DOMTextAreaElement; - HTMLSelectElement: abstract new (...args: never[]) => DOMSelectElement; - Event: DOMEventConstructor; - EventTarget: DOMEventTargetConstructor; - CustomEvent: DOMCustomEventConstructor; - KeyboardEvent: DOMKeyboardEventConstructor; - MouseEvent: DOMMouseEventConstructor; - FocusEvent: DOMFocusEventConstructor; - InputEvent: DOMInputEventConstructor; - ErrorEvent: abstract new (...args: never[]) => DOMEvent & { readonly error: unknown }; - MutationObserver: DOMMutationObserverConstructor; - AbortController: typeof AbortController; - FormData: new (form?: DOMFormElement, submitter?: DOMHTMLElement | null) => DOMFormData; - customElements: DOMCustomElementRegistry; - window: DOMEventTarget; -} - -/** Compatibility MissingGlobalError support. */ -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/keyboard/activation.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/activation.ts deleted file mode 100644 index aac4e03..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/activation.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://w3c.github.io/uievents/#event-type-keydown'; -export const input = ['keyboard'] as const; -export const requires = ['input.keyboard.delivery', 'elements.button.activation'] as const; - -// This capability selects the desktop Enter-on-down / Space-on-up interaction policy. -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'activates a focused button on Enter keydown exactly once per press', - async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - const received: string[] = []; - for (const type of ['keydown', 'click', 'keyup']) { - button.addEventListener(type, () => received.push(type)); - } - button.focus(); - await fixture.input.keyDown('Enter'); - expect(received).toEqual(['keydown', 'click']); - await fixture.input.keyUp('Enter'); - expect(received).toEqual(['keydown', 'click', 'keyup']); - }, - ); - - context.test('activates a focused button on Space release', async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - const received: string[] = []; - for (const type of ['keydown', 'click', 'keyup']) { - button.addEventListener(type, () => received.push(type)); - } - button.focus(); - await fixture.input.keyDown(' '); - expect(received).toEqual(['keydown']); - await fixture.input.keyUp(' '); - expect(received).toEqual(['keydown', 'keyup', 'click']); - }); - - for (const key of ['Enter', ' ']) { - context.test( - `canceling ${key === ' ' ? 'Space' : key} keydown suppresses button activation`, - async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - let clicks = 0; - button.addEventListener('keydown', (event) => event.preventDefault()); - button.addEventListener('click', () => clicks++); - button.focus(); - await fixture.input.press(key); - expect(clicks).toBe(0); - }, - ); - } - - context.test( - 'canceling Space keyup suppresses the pending button activation', - async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - let clicks = 0; - button.addEventListener('keyup', (event) => event.preventDefault()); - button.addEventListener('click', () => clicks++); - button.focus(); - await fixture.input.press(' '); - expect(clicks).toBe(0); - }, - ); - - context.test( - 'Space toggles a checkbox and canceled click restores its checkedness', - async (fixture) => { - const checkbox = fixture.connect(fixture.document.createElement('input')); - checkbox.type = 'checkbox'; - const changes: string[] = []; - checkbox.addEventListener('input', () => changes.push('input')); - checkbox.addEventListener('change', () => changes.push('change')); - checkbox.focus(); - await fixture.input.press(' '); - expect(checkbox.checked).toBe(true); - expect(changes).toEqual(['input', 'change']); - checkbox.addEventListener('click', (event) => event.preventDefault()); - await fixture.input.press(' '); - expect(checkbox.checked).toBe(true); - expect(changes).toEqual(['input', 'change']); - }, - ['elements.input.activation'], - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/delivery.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/delivery.ts deleted file mode 100644 index 5f8549d..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/delivery.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMKeyboardEvent } from '../../contracts/events.ts'; - -export const spec = 'https://w3c.github.io/uievents/#events-keyboard-event-order'; -export const input = ['keyboard'] as const; -export const requires = [ - 'focus.transitions', - 'events.dispatch.bubbling', - 'events.dispatch.cancellation', - 'events.data.keyboard', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'delivers keydown and keyup to the focused element and bubbles both', - async (fixture) => { - const { document } = fixture; - const parent = fixture.connect(document.createElement('div')); - const target = document.createElement('input'); - parent.appendChild(target); - const received: unknown[][] = []; - for (const type of ['keydown', 'keyup']) { - target.addEventListener(type, (event) => { - const key = event as DOMKeyboardEvent; - received.push([ - type, - 'target', - key.key, - key.target, - key.bubbles, - key.cancelable, - key.composed, - ]); - }); - parent.addEventListener(type, (event) => received.push([type, 'parent', event.target])); - } - target.focus(); - await fixture.input.press('ArrowLeft'); - expect(received).toEqual([ - ['keydown', 'target', 'ArrowLeft', target, true, true, true], - ['keydown', 'parent', target], - ['keyup', 'target', 'ArrowLeft', target, true, true, true], - ['keyup', 'parent', target], - ]); - }, - ); - - context.test('targets keyup at the element focused after keydown', async (fixture) => { - const { document } = fixture; - const root = fixture.connect(document.createElement('div')); - const first = document.createElement('input'); - const second = document.createElement('input'); - root.append(first, second); - const received: unknown[][] = []; - first.addEventListener('keydown', (event) => received.push([event.type, event.target])); - first.addEventListener('keyup', (event) => received.push([event.type, event.target])); - second.addEventListener('keyup', (event) => received.push([event.type, event.target])); - first.focus(); - await fixture.input.keyDown('ArrowRight'); - second.focus(); - await fixture.input.keyUp('ArrowRight'); - expect(received).toEqual([ - ['keydown', first], - ['keyup', second], - ]); - }); - - context.test('still delivers keyup after a canceled keydown', async (fixture) => { - const { document } = fixture; - const target = fixture.connect(document.createElement('input')); - const received: unknown[][] = []; - target.addEventListener('keydown', (event) => { - event.preventDefault(); - received.push([event.type, event.defaultPrevented]); - }); - target.addEventListener('keyup', (event) => - received.push([event.type, event.defaultPrevented]), - ); - target.focus(); - await fixture.input.press('ArrowLeft'); - expect(received).toEqual([ - ['keydown', true], - ['keyup', false], - ]); - }); - - context.test( - 'keyup reaches the newly focused target after an awaited keydown', - async (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('input'), - b = document.createElement('input'); - p.append(a, b); - fixture.connect(p as unknown as DOMElementContract); - const seen: unknown[] = []; - a.addEventListener('keydown', (e) => seen.push(['down', e.target])); - b.addEventListener('keyup', (e) => seen.push(['up', e.target])); - a.focus(); - await fixture.input.keyDown('Shift'); - b.focus(); - await fixture.input.keyUp('Shift'); - expect(seen).toEqual([ - ['down', a], - ['up', b], - ]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/editing.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/editing.ts deleted file mode 100644 index 7f2972a..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/editing.ts +++ /dev/null @@ -1,217 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMInputEvent } from '../../contracts/events.ts'; - -export const spec = 'https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/input-events/input-events-typing.html'; -export const input = ['keyboard', 'text'] as const; -export const requires = [ - 'input.keyboard.delivery', - 'events.data.input', - 'elements.input.value', - 'elements.input.selection', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'text input replaces the selection between beforeinput and input', - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - target.value = 'abcd'; - target.focus(); - target.setSelectionRange(1, 3); - const received: unknown[][] = []; - for (const type of ['beforeinput', 'input']) { - target.addEventListener(type, (event) => { - const edit = event as DOMInputEvent; - received.push([ - edit.type, - edit.inputType, - edit.data, - edit.cancelable, - edit.bubbles, - target.value, - target.selectionStart, - target.selectionEnd, - ]); - }); - } - await fixture.input.insertText('X'); - expect(received).toEqual([ - ['beforeinput', 'insertText', 'X', true, true, 'abcd', 1, 3], - ['input', 'insertText', 'X', false, true, 'aXd', 2, 2], - ]); - expect(target.value).toBe('aXd'); - }, - ); - - context.test( - 'canceling beforeinput preserves the value and selection and suppresses input', - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - target.value = 'abcd'; - target.focus(); - target.setSelectionRange(1, 3); - const received: string[] = []; - target.addEventListener('beforeinput', (event) => { - received.push(event.type); - event.preventDefault(); - }); - target.addEventListener('input', (event) => received.push(event.type)); - await fixture.input.insertText('X'); - expect(received).toEqual(['beforeinput']); - expect([target.value, target.selectionStart, target.selectionEnd]).toEqual(['abcd', 1, 3]); - }, - ); - - for (const [key, inputType, value, caret] of [ - ['Backspace', 'deleteContentBackward', 'acd', 1], - ['Delete', 'deleteContentForward', 'abd', 2], - ] as const) { - context.test( - `${key} deletes beside the caret with the appropriate input type`, - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - target.value = 'abcd'; - target.focus(); - target.setSelectionRange(2, 2); - const received: unknown[][] = []; - for (const type of ['beforeinput', 'input']) { - target.addEventListener(type, (event) => { - const edit = event as DOMInputEvent; - received.push([edit.type, edit.inputType, edit.data]); - }); - } - await fixture.input.press(key); - expect(received).toEqual([ - ['beforeinput', inputType, null], - ['input', inputType, null], - ]); - expect([target.value, target.selectionStart, target.selectionEnd]).toEqual([ - value, - caret, - caret, - ]); - }, - ); - } - - context.test( - 'Backspace deletes a selected range instead of the preceding character', - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - target.value = 'abcd'; - target.focus(); - target.setSelectionRange(1, 3); - await fixture.input.press('Backspace'); - expect([target.value, target.selectionStart, target.selectionEnd]).toEqual(['ad', 1, 1]); - }, - ); - - context.test( - 'canceling a deletion keydown prevents editing events and mutation', - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - target.value = 'abcd'; - target.focus(); - target.setSelectionRange(2, 2); - const received: string[] = []; - target.addEventListener('keydown', (event) => event.preventDefault()); - for (const type of ['beforeinput', 'input']) { - target.addEventListener(type, () => received.push(type)); - } - await fixture.input.press('Backspace'); - expect(received).toEqual([]); - expect([target.value, target.selectionStart, target.selectionEnd]).toEqual(['abcd', 2, 2]); - }, - ); - - context.test( - 'read-only controls accept focus without allowing text insertion or deletion', - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - target.value = 'abcd'; - target.readOnly = true; - target.focus(); - target.setSelectionRange(1, 3); - let changes = 0; - target.addEventListener('input', () => changes++); - await fixture.input.insertText('X'); - await fixture.input.press('Backspace'); - expect(fixture.document.activeElement).toBe(target); - expect(target.value).toBe('abcd'); - expect(changes).toBe(0); - }, - ); - - context.test( - 'Delete removes a noncollapsed selection and leaves a collapsed caret', - async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - fixture.connect(e as unknown as DOMElementContract); - e.value = 'abcdef'; - e.focus(); - e.setSelectionRange(2, 4, 'backward'); - await fixture.input.press('Delete'); - expect(e.value).toBe('abef'); - expect(e.selectionStart).toBe(2); - expect(e.selectionEnd).toBe(2); - }, - ); - - context.test( - 'Backspace at the start and Delete at the end leave text unchanged', - async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - fixture.connect(e as unknown as DOMElementContract); - e.value = 'abc'; - e.focus(); - e.setSelectionRange(0, 0); - await fixture.input.press('Backspace'); - expect(e.value).toBe('abc'); - expect(e.selectionStart).toBe(0); - e.setSelectionRange(3, 3); - await fixture.input.press('Delete'); - expect(e.value).toBe('abc'); - expect(e.selectionStart).toBe(3); - }, - ); - - context.test('keydown cancellation blocks printable key default editing', async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - fixture.connect(e as unknown as DOMElementContract); - e.value = 'abc'; - e.focus(); - e.setSelectionRange(1, 1); - let edits = 0; - e.addEventListener('keydown', (event) => event.preventDefault()); - e.addEventListener('input', () => edits++); - await fixture.input.press('x'); - expect(e.value).toBe('abc'); - expect(e.selectionStart).toBe(1); - expect(edits).toBe(0); - }); - - context.test( - 'successive host text insertions preserve Unicode and replace selection', - async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - fixture.connect(e as unknown as DOMElementContract); - e.focus(); - await fixture.input.insertText('A😀B'); - expect(e.value).toBe('A😀B'); - expect(e.selectionStart).toBe(4); - e.setSelectionRange(1, 3); - await fixture.input.insertText('é'); - expect(e.value).toBe('AéB'); - expect(e.selectionStart).toBe(2); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/focus-styles.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/focus-styles.ts deleted file mode 100644 index 2a2977c..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/focus-styles.ts +++ /dev/null @@ -1,70 +0,0 @@ -// WPT focus-within-011.html and user-action-pseudo-classes-in-has.html adaptations. -// Revision b719612f963a05ece9c89a7dd7598c1c59618874; copyright WPT contributors, -// Ethan Lin, Mozilla, Byungwoo Lee. BSD-3-Clause; ../../../../test/wpt/LICENSE. -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/invalidation/user-action-pseudo-classes-in-has.html'; -export const input = ['keyboard'] as const; -export const requires = ['input.keyboard.navigation', 'style.focusState'] as const; -export const globals = ['getComputedStyle'] as const; -/** Registers native navigation and CSS focus state interactions. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'Tab and Shift Tab move focus selectors and styles between subtrees', - async (fixture) => { - const { root, get } = createStyleFixture(fixture, { - markup: - '
      ', - css: 'section {color:rgb(1,2,3)} section:focus-within {color:rgb(4,5,6)} button:focus {color:rgb(7,8,9)}', - }); - const [first, second] = Array.from(root.querySelectorAll('button')); - const [parent, other] = Array.from(root.querySelectorAll('section')); - first!.focus(); - await fixture.input.press('Tab'); - expect(first!.matches(':focus')).toBe(false); - expect(second!.matches(':focus')).toBe(true); - expect(get(parent!).color).toBe('rgb(1, 2, 3)'); - expect(get(other!).color).toBe('rgb(4, 5, 6)'); - expect(get(second!).color).toBe('rgb(7, 8, 9)'); - await fixture.input.keyDown('Shift'); - await fixture.input.press('Tab'); - await fixture.input.keyUp('Shift'); - expect(first!.matches(':focus')).toBe(true); - expect(get(parent!).color).toBe('rgb(4, 5, 6)'); - expect(get(other!).color).toBe('rgb(1, 2, 3)'); - }, - ); - context.test('canceling Tab retains focus selectors and styles', async (fixture) => { - const { root, get } = createStyleFixture(fixture, { - markup: '', - css: 'button {color:rgb(1,2,3)} button:focus {color:rgb(4,5,6)}', - }); - const [first, second] = Array.from(root.querySelectorAll('button')); - first!.focus(); - first!.addEventListener('keydown', (event) => event.preventDefault()); - await fixture.input.press('Tab'); - expect(first!.matches(':focus')).toBe(true); - expect(second!.matches(':focus')).toBe(false); - expect(get(first!).color).toBe('rgb(4, 5, 6)'); - expect(get(second!).color).toBe('rgb(1, 2, 3)'); - }); - context.test( - 'Tab skips inert descendants and never gives their ancestors focus-within', - async (fixture) => { - const { root, get } = createStyleFixture(fixture, { - markup: - '
      ', - css: 'section {color:rgb(1,2,3)} section:focus-within {color:rgb(4,5,6)}', - }); - root.querySelector('#first')!.focus(); - await fixture.input.press('Tab'); - expect(root.querySelector('#last')!.matches(':focus')).toBe(true); - expect(root.querySelector('section')!.matches(':focus-within')).toBe(false); - expect(get(root.querySelector('section')!).color).toBe('rgb(1, 2, 3)'); - }, - ['focus.inert'], - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/focus-visible.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/focus-visible.ts deleted file mode 100644 index bcfc279..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/focus-visible.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Adapted from WPT focus-visible-001.html at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright web-platform-tests contributors; Rob Dodson. 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/#the-focus-visible-pseudo'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/selectors/focus-visible-001.html'; -export const input = ['keyboard'] as const; -export const requires = ['input.keyboard.navigation', 'style.focusState'] as const; -export const globals = ['getComputedStyle'] as const; -/** Registers the explicit keyboard-focus-visible policy independently of event trust. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - for (const markup of [ - '', - 'Target', - '', - ]) { - context.test( - `Tab makes ${markup} focus-visible and clears matching after blur`, - async (fixture) => { - const { document, root, get } = createStyleFixture(fixture, { - markup: `${markup}`, - css: ':focus-visible {color:rgb(4,5,6)}', - }); - const first = root.firstElementChild as HTMLElement, - target = root.lastElementChild as HTMLElement; - first.focus(); - await fixture.input.press('Tab'); - expect(document.activeElement).toBe(target); - expect(target.matches(':focus-visible')).toBe(true); - expect(Array.from(root.querySelectorAll(':focus-visible'))).toEqual([target]); - expect(get(target).color).toBe('rgb(4, 5, 6)'); - target.blur(); - expect(target.matches(':focus-visible')).toBe(false); - }, - ); - } -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/implicit-submission.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/implicit-submission.ts deleted file mode 100644 index 75f8140..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/implicit-submission.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMSubmitEvent } from '../../contracts/events.ts'; -import type { DOMElement } from '../../contracts/nodes.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/master/html/semantics/forms/form-submission-0/implicit-submission.optional.html'; -export const input = ['keyboard'] as const; -export const requires = [ - 'input.keyboard.delivery', - 'elements.input.formAssociation', - 'elements.button.activation', - 'elements.form.submission', -] as const; - -// Enter as the implicit-submission trigger is an explicitly selected interaction policy. -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('Enter in a text control activates the default submit button', async (fixture) => { - const form = fixture.connect(fixture.document.createElement('form')); - const text = fixture.document.createElement('input'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = 'submit'; - second.type = 'submit'; - form.append(text, first, second); - const clicks: DOMElement[] = []; - first.addEventListener('click', () => clicks.push(first)); - second.addEventListener('click', () => clicks.push(second)); - const submissions: DOMSubmitEvent[] = []; - form.addEventListener('submit', (event) => { - event.preventDefault(); - submissions.push(event as DOMSubmitEvent); - }); - text.focus(); - await fixture.input.press('Enter'); - expect(clicks).toEqual([first]); - expect(submissions).toHaveLength(1); - const submitted = submissions[0]; - if (!submitted) return expect.fail('Expected a submit event'); - expect(submitted.submitter).toBe(first); - expect(submitted.target).toBe(form); - expect([submitted.bubbles, submitted.cancelable]).toEqual([true, true]); - }); - - context.test('canceling Enter keydown suppresses implicit submission', async (fixture) => { - const form = fixture.connect(fixture.document.createElement('form')); - const text = fixture.document.createElement('input'); - const button = fixture.document.createElement('button'); - button.type = 'submit'; - form.append(text, button); - const received: string[] = []; - text.addEventListener('keydown', (event) => event.preventDefault()); - button.addEventListener('click', () => received.push('click')); - form.addEventListener('submit', (event) => { - event.preventDefault(); - received.push('submit'); - }); - text.focus(); - await fixture.input.press('Enter'); - expect(received).toEqual([]); - }); - - context.test( - 'canceling the default button click suppresses implicit submission', - async (fixture) => { - const form = fixture.connect(fixture.document.createElement('form')); - const text = fixture.document.createElement('input'); - const button = fixture.document.createElement('button'); - button.type = 'submit'; - form.append(text, button); - const received: string[] = []; - button.addEventListener('click', (event) => { - event.preventDefault(); - received.push('click'); - }); - form.addEventListener('submit', (event) => { - event.preventDefault(); - received.push('submit'); - }); - text.focus(); - await fixture.input.press('Enter'); - expect(received).toEqual(['click']); - }, - ); - - context.test( - 'a disabled default submit button blocks submission rather than selecting the next button', - async (fixture) => { - const form = fixture.connect(fixture.document.createElement('form')); - const text = fixture.document.createElement('input'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = 'submit'; - second.type = 'submit'; - first.disabled = true; - form.append(text, first, second); - const received: string[] = []; - first.addEventListener('click', () => received.push('first')); - second.addEventListener('click', () => received.push('second')); - form.addEventListener('submit', (event) => { - event.preventDefault(); - received.push('submit'); - }); - text.focus(); - await fixture.input.press('Enter'); - expect(received).toEqual([]); - }, - ); - - context.test( - 'a single text control submits without a submit button and reports a null submitter', - async (fixture) => { - const form = fixture.connect(fixture.document.createElement('form')); - const text = fixture.document.createElement('input'); - form.appendChild(text); - const submissions: DOMSubmitEvent[] = []; - form.addEventListener('submit', (event) => { - event.preventDefault(); - submissions.push(event as DOMSubmitEvent); - }); - text.focus(); - await fixture.input.press('Enter'); - expect(submissions).toHaveLength(1); - expect(submissions[0]?.submitter).toBe(null); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/modifiers.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/modifiers.ts deleted file mode 100644 index a7acb48..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/modifiers.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMKeyboardEvent } from '../../contracts/events.ts'; - -export const spec = 'https://w3c.github.io/uievents/#keys-modifiers'; -export const input = ['keyboard'] as const; -export const requires = ['input.keyboard.delivery'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('holds Shift across another key press and clears it on release', async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - const received: unknown[][] = []; - for (const type of ['keydown', 'keyup']) { - target.addEventListener(type, (event) => { - const key = event as DOMKeyboardEvent; - received.push([key.type, key.key, key.shiftKey, key.getModifierState('Shift')]); - }); - } - target.focus(); - await fixture.input.keyDown('Shift'); - await fixture.input.press('ArrowRight'); - await fixture.input.keyUp('Shift'); - await fixture.input.press('ArrowRight'); - expect(received).toEqual([ - ['keydown', 'Shift', true, true], - ['keydown', 'ArrowRight', true, true], - ['keyup', 'ArrowRight', true, true], - ['keyup', 'Shift', false, false], - ['keydown', 'ArrowRight', false, false], - ['keyup', 'ArrowRight', false, false], - ]); - }); - - context.test('preserves held modifiers when focus moves between controls', async (fixture) => { - const root = fixture.connect(fixture.document.createElement('div')); - const first = fixture.document.createElement('input'); - const second = fixture.document.createElement('input'); - root.append(first, second); - const received: boolean[][] = []; - second.addEventListener('keydown', (event) => { - const key = event as DOMKeyboardEvent; - received.push([key.shiftKey, key.ctrlKey, key.altKey, key.metaKey]); - }); - first.focus(); - await fixture.input.keyDown('Shift'); - second.focus(); - await fixture.input.press('ArrowLeft'); - await fixture.input.keyUp('Shift'); - expect(received).toEqual([[true, false, false, false]]); - }); - - context.test( - 'Control remains held across independent key presses until release', - async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - fixture.connect(e as unknown as DOMElementContract); - e.focus(); - const seen: unknown[] = []; - e.addEventListener('keydown', (event) => { - if (event.key === 'x' || event.key === 'y') { - event.preventDefault(); - seen.push([event.key, event.ctrlKey, event.shiftKey]); - } - }); - await fixture.input.keyDown('Control'); - await fixture.input.press('x'); - await fixture.input.press('y'); - await fixture.input.keyUp('Control'); - await fixture.input.press('x'); - expect(seen).toEqual([ - ['x', true, false], - ['y', true, false], - ['x', false, false], - ]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/multiline-editing.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/multiline-editing.ts deleted file mode 100644 index f80e726..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/multiline-editing.ts +++ /dev/null @@ -1,173 +0,0 @@ -// Adapted from WPT input-events-typing.html at b719612f963a05ece9c89a7dd7598c1c59618874. -// Copyright © web-platform-tests contributors. 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://www.w3.org/TR/input-events-2/#input-event-types'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/input-events/input-events-typing.html'; -export const input = ['keyboard', 'text'] as const; -export const requires = [ - 'input.keyboard.delivery', - 'input.keyboard.modifiers', - 'events.data.input', - 'elements.textarea.value', - 'elements.textarea.selection', - 'selection.document.direction', -] as const; -// Selects the policy Enter = paragraph in contenteditable, Enter = line break in -// textarea, and Shift+Enter = line break in contenteditable. No platform detection. - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - for (const kind of ['textarea', 'contenteditable'] as const) { - context.test(`orders typing events around the ${kind} edit`, async (fixture) => { - const document = fixture.document as unknown as Document; - const target = document.createElement(kind === 'textarea' ? 'textarea' : 'div'); - if (kind === 'contenteditable') target.contentEditable = 'true'; - fixture.connect(target as unknown as DOMElementContract); - const value = (): string => - kind === 'textarea' ? (target as HTMLTextAreaElement).value : (target.textContent ?? ''); - const calls: unknown[] = []; - for (const type of ['keydown', 'beforeinput', 'input', 'keyup']) - target.addEventListener(type, (event) => { - const edit = event as InputEvent; - calls.push([ - type, - event.target, - value(), - type === 'beforeinput' || type === 'input' - ? [edit.inputType, edit.data, edit.isComposing, event.cancelable] - : (event as KeyboardEvent).key, - ]); - }); - target.focus(); - await fixture.input.press('a'); - expect(value()).toBe('a'); - expect(calls).toEqual([ - ['keydown', target, '', 'a'], - ['beforeinput', target, '', ['insertText', 'a', false, true]], - ['input', target, 'a', ['insertText', 'a', false, false]], - ['keyup', target, 'a', 'a'], - ]); - }); - - context.test( - `canceling beforeinput preserves ${kind} text and selected range`, - async (fixture) => { - const document = fixture.document as unknown as Document; - const target = document.createElement(kind === 'textarea' ? 'textarea' : 'div'); - const textarea = target as HTMLTextAreaElement; - if (kind === 'textarea') textarea.value = 'abcd'; - else { - target.contentEditable = 'true'; - target.textContent = 'abcd'; - } - fixture.connect(target as unknown as DOMElementContract); - target.focus(); - const text = target.firstChild; - const selection = document.getSelection(); - if (kind === 'textarea') textarea.setSelectionRange(1, 3); - else { - if (!selection || !text) - return expect.fail('Connected editable must have a selection and text node'); - fixture.cleanup(() => selection.removeAllRanges()); - selection.setBaseAndExtent(text, 1, text, 3); - } - const calls: string[] = []; - target.addEventListener('beforeinput', (event) => { - calls.push('beforeinput'); - event.preventDefault(); - }); - target.addEventListener('input', () => calls.push('input')); - await fixture.input.insertText('X'); - expect(calls).toEqual(['beforeinput']); - if (kind === 'textarea') { - expect(textarea.value).toBe('abcd'); - expect([textarea.selectionStart, textarea.selectionEnd]).toEqual([1, 3]); - } else { - expect(target.textContent).toBe('abcd'); - expect(target.firstChild).toBe(text); - expect(selection?.anchorNode).toBe(text); - expect(selection?.focusNode).toBe(text); - expect([selection?.anchorOffset, selection?.focusOffset]).toEqual([1, 3]); - } - }, - ); - } - - context.test('Enter replaces a textarea selection with a line break', async (fixture) => { - const document = fixture.document as unknown as Document; - const target = document.createElement('textarea'); - fixture.connect(target as unknown as DOMElementContract); - target.value = 'abcd'; - target.focus(); - target.setSelectionRange(1, 3); - const calls: unknown[] = []; - for (const type of ['beforeinput', 'input']) - target.addEventListener(type, (event) => { - const edit = event as InputEvent; - calls.push([type, edit.inputType, target.value]); - }); - await fixture.input.press('Enter'); - expect(calls).toEqual([ - ['beforeinput', 'insertLineBreak', 'abcd'], - ['input', 'insertLineBreak', 'a\nd'], - ]); - expect([target.selectionStart, target.selectionEnd]).toEqual([2, 2]); - }); - - for (const shift of [false, true]) { - context.test( - `${shift ? 'Shift+Enter' : 'Enter'} reports ${shift ? 'line-break' : 'paragraph'} insertion in contenteditable`, - async (fixture) => { - const document = fixture.document as unknown as Document; - const target = document.createElement('div'); - target.contentEditable = 'true'; - target.textContent = 'abcd'; - fixture.connect(target as unknown as DOMElementContract); - target.focus(); - const selection = document.getSelection(); - if (!selection || !target.firstChild) - return expect.fail('Connected editable must have a selection and text node'); - fixture.cleanup(() => selection.removeAllRanges()); - selection.collapse(target.firstChild, 2); - const originalMarkup = target.innerHTML; - const calls: unknown[] = []; - for (const type of ['beforeinput', 'input']) - target.addEventListener(type, (event) => { - const edit = event as InputEvent; - calls.push([type, event.target, edit.inputType, edit.data, event.cancelable]); - }); - if (shift) await fixture.input.keyDown('Shift'); - await fixture.input.press('Enter'); - if (shift) await fixture.input.keyUp('Shift'); - const inputType = shift ? 'insertLineBreak' : 'insertParagraph'; - expect(calls).toEqual([ - ['beforeinput', target, inputType, null, true], - ['input', target, inputType, null, false], - ]); - expect(target.innerHTML).not.toBe(originalMarkup); - expect(target.textContent).toBe('abcd'); - expect(selection.isCollapsed).toBe(true); - expect(target.contains(selection.anchorNode)).toBe(true); - }, - ); - } - - context.test( - 'text insertion replaces a multiline textarea selection with Unicode text', - async (fixture) => { - const document = fixture.document as unknown as Document; - const target = document.createElement('textarea'); - fixture.connect(target as unknown as DOMElementContract); - target.value = 'one\ntwo\nthree'; - target.focus(); - target.setSelectionRange(2, 9); - await fixture.input.insertText('😀é'); - expect(target.value).toBe('on😀éhree'); - expect([target.selectionStart, target.selectionEnd]).toEqual([5, 5]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/navigation.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/navigation.ts deleted file mode 100644 index 189ac54..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/navigation.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMKeyboardEvent } from '../../contracts/events.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation'; -export const input = ['keyboard'] as const; -export const requires = [ - 'input.keyboard.delivery', - 'input.keyboard.modifiers', - 'focus.eligibility', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'Tab advances through controls and delivers keyup at the new focus', - async (fixture) => { - const root = fixture.connect(fixture.document.createElement('div')); - const first = fixture.document.createElement('input'); - const second = fixture.document.createElement('input'); - const third = fixture.document.createElement('input'); - root.append(first, second, third); - const received: unknown[][] = []; - first.addEventListener('keydown', (event) => received.push([event.type, event.target])); - second.addEventListener('keyup', (event) => received.push([event.type, event.target])); - first.focus(); - await fixture.input.press('Tab'); - expect(fixture.document.activeElement).toBe(second); - expect(received).toEqual([ - ['keydown', first], - ['keyup', second], - ]); - await fixture.input.press('Tab'); - expect(fixture.document.activeElement).toBe(third); - }, - ); - - context.test('Shift+Tab traverses controls in reverse order', async (fixture) => { - const root = fixture.connect(fixture.document.createElement('div')); - const first = fixture.document.createElement('input'); - const second = fixture.document.createElement('input'); - const third = fixture.document.createElement('input'); - root.append(first, second, third); - third.focus(); - await fixture.input.keyDown('Shift'); - await fixture.input.press('Tab'); - expect(fixture.document.activeElement).toBe(second); - await fixture.input.press('Tab'); - expect(fixture.document.activeElement).toBe(first); - await fixture.input.keyUp('Shift'); - }); - - context.test('canceled Tab leaves focus on the current control', async (fixture) => { - const root = fixture.connect(fixture.document.createElement('div')); - const first = fixture.document.createElement('input'); - const second = fixture.document.createElement('input'); - root.append(first, second); - first.addEventListener('keydown', (event) => { - if ((event as DOMKeyboardEvent).key === 'Tab') event.preventDefault(); - }); - first.focus(); - await fixture.input.press('Tab'); - expect(fixture.document.activeElement).toBe(first); - }); - - context.test( - 'skips disabled controls and negative tabindex during sequential navigation', - async (fixture) => { - const root = fixture.connect(fixture.document.createElement('div')); - const first = fixture.document.createElement('input'); - const disabled = fixture.document.createElement('input'); - disabled.disabled = true; - const excluded = fixture.document.createElement('input'); - excluded.tabIndex = -1; - const last = fixture.document.createElement('input'); - root.append(first, disabled, excluded, last); - first.focus(); - await fixture.input.press('Tab'); - expect(fixture.document.activeElement).toBe(last); - excluded.focus(); - expect(fixture.document.activeElement).toBe(excluded); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/number-stepping.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/number-stepping.ts deleted file mode 100644 index 6debce7..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/number-stepping.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Adapted from WPT input-events-arrow-key-on-number-input{,-prevent-default}.html -// at b719612f963a05ece9c89a7dd7598c1c59618874. Copyright © web-platform-tests contributors. -// 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://html.spec.whatwg.org/multipage/input.html#number-state-(type=number)'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/input-events/input-events-arrow-key-on-number-input-prevent-default.html'; -export const input = ['keyboard'] as const; -export const requires = [ - 'input.keyboard.delivery', - 'events.data.input', - 'elements.input.numericValue', - 'elements.input.stepping', -] as const; -// Selects ArrowUp/ArrowDown numeric stepping, including immediate change events. - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - for (const [key, expected] of [ - ['ArrowUp', '6'], - ['ArrowDown', '2'], - ] as const) { - context.test( - `${key} applies the numeric step between beforeinput and input/change`, - async (fixture) => { - const document = fixture.document as unknown as Document; - const target = document.createElement('input'); - target.type = 'number'; - target.min = '0'; - target.max = '10'; - target.step = '2'; - target.value = '4'; - fixture.connect(target as unknown as DOMElementContract); - target.focus(); - const calls: unknown[] = []; - for (const type of ['keydown', 'beforeinput', 'input', 'change', 'keyup']) - target.addEventListener(type, (event) => { - calls.push([type, event.target, target.value]); - }); - await fixture.input.press(key); - expect(target.value).toBe(expected); - expect(calls).toEqual([ - ['keydown', target, '4'], - ['beforeinput', target, '4'], - ['input', target, expected], - ['change', target, expected], - ['keyup', target, expected], - ]); - }, - ); - } - for (const canceled of ['keydown', 'beforeinput']) { - context.test( - `canceling ${canceled} prevents numeric arrow stepping and input/change`, - async (fixture) => { - const document = fixture.document as unknown as Document; - const target = document.createElement('input'); - target.type = 'number'; - target.value = '4'; - fixture.connect(target as unknown as DOMElementContract); - target.focus(); - const calls: string[] = []; - for (const type of ['keydown', 'beforeinput', 'input', 'change', 'keyup']) - target.addEventListener(type, (event) => { - calls.push(type); - if (type === canceled) event.preventDefault(); - }); - await fixture.input.press('ArrowUp'); - expect(target.value).toBe('4'); - expect(calls).toEqual( - canceled === 'keydown' ? ['keydown', 'keyup'] : ['keydown', 'beforeinput', 'keyup'], - ); - }, - ); - } -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/shadow.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/shadow.ts deleted file mode 100644 index 192985d..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/shadow.ts +++ /dev/null @@ -1,169 +0,0 @@ -// Adapted from WPT event-composed-path.html and focus-navigation-slot-fallback-default-tabindex.html -// at b719612f963a05ece9c89a7dd7598c1c59618874. Copyright © web-platform-tests contributors. -// Event-path author: 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://html.spec.whatwg.org/multipage/interaction.html#sequential-focus-navigation'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/shadow-dom/focus-navigation/focus-navigation-slot-fallback-default-tabindex.html'; -export const input = ['keyboard'] as const; -export const requires = [ - 'input.keyboard.delivery', - 'shadow.focus.retargeting', - 'shadow.events.composedPath', -] 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) { - context.test( - `retargets native keys and filters paths outside a ${mode} shadow root`, - async (fixture) => { - const document = fixture.document as unknown as Document; - const host = document.createElement('div'); - const root = host.attachShadow({ mode }); - const target = document.createElement('input'); - root.append(target); - fixture.connect(host as unknown as DOMElementContract); - target.focus(); - expect(document.activeElement).toBe(host); - expect(root.activeElement).toBe(target); - const calls: unknown[] = []; - for (const type of ['keydown', 'keyup']) { - target.addEventListener(type, (event) => - calls.push([ - type, - 'inside', - event.target, - event.composed, - event.composedPath().slice(0, 3), - ]), - ); - host.addEventListener(type, (event) => - calls.push([ - type, - 'outside', - event.target, - event.composed, - event - .composedPath() - .filter((node) => node === target || node === root || node === host), - ]), - ); - } - await fixture.input.press('ArrowLeft'); - expect(calls).toEqual([ - ['keydown', 'inside', target, true, [target, root, host]], - ['keydown', 'outside', host, true, mode === 'open' ? [target, root, host] : [host]], - ['keyup', 'inside', target, true, [target, root, host]], - ['keyup', 'outside', host, true, mode === 'open' ? [target, root, host] : [host]], - ]); - }, - ); - } - - context.test( - 'canceling a retargeted host keydown prevents editing inside the shadow root', - async (fixture) => { - const document = fixture.document as unknown as Document; - const host = document.createElement('div'); - const root = host.attachShadow({ mode: 'open' }); - const target = document.createElement('input'); - target.value = 'abcd'; - root.append(target); - fixture.connect(host as unknown as DOMElementContract); - target.focus(); - target.setSelectionRange(2, 2); - const calls: unknown[] = []; - host.addEventListener('keydown', (event) => { - calls.push(['keydown', event.target]); - event.preventDefault(); - }); - host.addEventListener('keyup', (event) => calls.push(['keyup', event.target])); - for (const type of ['beforeinput', 'input']) - target.addEventListener(type, () => calls.push([type])); - await fixture.input.press('Backspace'); - expect(calls).toEqual([ - ['keydown', host], - ['keyup', host], - ]); - expect(target.value).toBe('abcd'); - expect([target.selectionStart, target.selectionEnd]).toEqual([2, 2]); - }, - ['input.keyboard.editing'], - ); - - context.test( - 'Tab follows slot scopes and skips fallback content replaced by assigned nodes', - async (fixture) => { - const document = fixture.document as unknown as Document; - const before = document.createElement('input'); - const host = document.createElement('div'); - const after = document.createElement('input'); - const root = host.attachShadow({ mode: 'open' }); - const firstSlot = document.createElement('slot'), - secondSlot = document.createElement('slot'); - firstSlot.name = 'assigned'; - secondSlot.name = 'fallback'; - const assigned = document.createElement('input'); - assigned.slot = 'assigned'; - const hiddenFallback = document.createElement('input'); - const fallback = document.createElement('input'); - firstSlot.append(hiddenFallback); - secondSlot.append(fallback); - root.append(firstSlot, secondSlot); - host.append(assigned); - const container = document.createElement('div'); - container.append(before, host, after); - fixture.connect(container as unknown as DOMElementContract); - before.focus(); - await fixture.input.press('Tab'); - expect(document.activeElement).toBe(assigned); - await fixture.input.press('Tab'); - expect(document.activeElement).toBe(host); - expect(root.activeElement).toBe(fallback); - await fixture.input.press('Tab'); - expect(document.activeElement).toBe(after); - await fixture.input.keyDown('Shift'); - await fixture.input.press('Tab'); - expect(root.activeElement).toBe(fallback); - await fixture.input.press('Tab'); - expect(document.activeElement).toBe(assigned); - await fixture.input.press('Tab'); - expect(document.activeElement).toBe(before); - await fixture.input.keyUp('Shift'); - }, - ['input.keyboard.navigation', 'shadow.slots.fallback'], - ); - - context.test( - 'keyup follows focus moved from a shadow descendant into the light tree', - async (fixture) => { - const document = fixture.document as unknown as Document; - const container = document.createElement('div'); - const host = document.createElement('div'); - const root = host.attachShadow({ mode: 'closed' }); - const inside = document.createElement('input'), - outside = document.createElement('input'); - root.append(inside); - container.append(host, outside); - fixture.connect(container as unknown as DOMElementContract); - const calls: unknown[] = []; - container.addEventListener('keydown', (event) => calls.push(['keydown', event.target])); - container.addEventListener('keyup', (event) => calls.push(['keyup', event.target])); - inside.focus(); - await fixture.input.keyDown('Shift'); - outside.focus(); - await fixture.input.keyUp('Shift'); - expect(calls).toEqual([ - ['keydown', host], - ['keyup', outside], - ]); - expect(root.activeElement).toBeNull(); - expect(document.activeElement).toBe(outside); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/keyboard/trusted-events.ts b/packages/web-platform-tests/src/compatibility/input/keyboard/trusted-events.ts deleted file mode 100644 index 015e7a5..0000000 --- a/packages/web-platform-tests/src/compatibility/input/keyboard/trusted-events.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; - -export const spec = 'https://w3c.github.io/webdriver/#dispatching-a-keydown-action'; -export const input = ['keyboard'] as const; -export const requires = ['input.keyboard.delivery'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('input-driver keyboard events are trusted', async (fixture) => { - const target = fixture.connect(fixture.document.createElement('input')); - const received: unknown[][] = []; - for (const type of ['keydown', 'keyup']) { - target.addEventListener(type, (event) => received.push([event.type, event.isTrusted])); - } - target.focus(); - await fixture.input.press('ArrowLeft'); - expect(received).toEqual([ - ['keydown', true], - ['keyup', true], - ]); - }); - - context.test( - 'keyboard-generated button activation emits a trusted click', - async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - const trusted: boolean[] = []; - button.addEventListener('click', (event) => trusted.push(event.isTrusted)); - button.focus(); - await fixture.input.press('Enter'); - expect(trusted).toEqual([true]); - }, - ['input.keyboard.activation'], - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/activation.ts b/packages/web-platform-tests/src/compatibility/input/mouse/activation.ts deleted file mode 100644 index 6f73070..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/activation.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMSubmitEvent } from '../../contracts/events.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/interaction.html#activation-behavior'; -export const input = ['mouse'] as const; -export const requires = [ - 'input.mouse.delivery', - 'elements.input.activation', - 'elements.button.activation', - 'elements.form.submission', - 'events.dispatch.cancellation', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'clicking a checkbox toggles checkedness before click and emits input then change', - async (fixture) => { - const checkbox = fixture.document.createElement('input'); - checkbox.type = 'checkbox'; - fixture.connect(checkbox); - const calls: [string, boolean][] = []; - for (const type of ['click', 'input', 'change']) - checkbox.addEventListener(type, (event) => calls.push([event.type, checkbox.checked])); - await fixture.input.click(checkbox); - expect(checkbox.checked).toBe(true); - expect(calls).toEqual([ - ['click', true], - ['input', true], - ['change', true], - ]); - calls.length = 0; - await fixture.input.click(checkbox); - expect(checkbox.checked).toBe(false); - expect(calls).toEqual([ - ['click', false], - ['input', false], - ['change', false], - ]); - }, - ); - - context.test( - 'canceling checkbox click restores checkedness and indeterminate without input or change', - async (fixture) => { - const checkbox = fixture.document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.indeterminate = true; - fixture.connect(checkbox); - const preactivation: boolean[][] = []; - const changes: string[] = []; - checkbox.addEventListener('click', (event) => { - preactivation.push([checkbox.checked, checkbox.indeterminate]); - event.preventDefault(); - }); - for (const type of ['input', 'change']) - checkbox.addEventListener(type, (event) => changes.push(event.type)); - await fixture.input.click(checkbox); - expect(preactivation).toEqual([[true, false]]); - expect([checkbox.checked, checkbox.indeterminate]).toEqual([false, true]); - expect(changes).toEqual([]); - }, - ); - - context.test( - 'clicking a submit button submits its form with that button as submitter', - async (fixture) => { - const form = fixture.document.createElement('form'); - const button = fixture.document.createElement('button'); - button.type = 'submit'; - button.textContent = 'Submit'; - form.appendChild(button); - fixture.connect(form); - const calls: string[] = []; - const submissions: DOMSubmitEvent[] = []; - button.addEventListener('click', () => calls.push('click')); - form.addEventListener('submit', (event) => { - event.preventDefault(); - calls.push('submit'); - submissions.push(event as DOMSubmitEvent); - }); - await fixture.input.click(button); - expect(calls).toEqual(['click', 'submit']); - expect(submissions).toHaveLength(1); - expect(submissions[0]?.target).toBe(form); - expect(submissions[0]?.submitter).toBe(button); - expect(submissions[0]?.bubbles).toBe(true); - expect(submissions[0]?.cancelable).toBe(true); - }, - ); - - context.test( - 'canceling submit-button click prevents the form submission default action', - async (fixture) => { - const form = fixture.document.createElement('form'); - const button = fixture.document.createElement('button'); - button.type = 'submit'; - button.textContent = 'Canceled submit'; - form.appendChild(button); - fixture.connect(form); - let clicks = 0; - let submissions = 0; - button.addEventListener('click', (event) => { - event.preventDefault(); - clicks++; - }); - form.addEventListener('submit', (event) => { - event.preventDefault(); - submissions++; - }); - await fixture.input.click(button); - expect(clicks).toBe(1); - expect(submissions).toBe(0); - }, - ); - - context.test('disabled controls suppress click activation from mouse input', async (fixture) => { - const form = fixture.document.createElement('form'); - const button = fixture.document.createElement('button'); - const checkbox = fixture.document.createElement('input'); - button.type = 'submit'; - button.textContent = 'Disabled submit'; - button.disabled = true; - checkbox.type = 'checkbox'; - checkbox.disabled = true; - form.append(button, checkbox); - fixture.connect(form); - const calls: string[] = []; - button.addEventListener('click', () => calls.push('button click')); - checkbox.addEventListener('click', () => calls.push('checkbox click')); - form.addEventListener('submit', (event) => { - event.preventDefault(); - calls.push('submit'); - }); - await fixture.input.click(button); - await fixture.input.click(checkbox); - expect(checkbox.checked).toBe(false); - expect(calls).toEqual([]); - }); - - context.test( - 'repeated native checkbox clicks preserve click input change order', - async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('input'); - e.type = 'checkbox'; - fixture.connect(e as unknown as DOMElementContract); - const seen: string[] = []; - for (const type of ['click', 'input', 'change']) - e.addEventListener(type, () => seen.push(type)); - await fixture.input.click(e as unknown as DOMElementContract); - await fixture.input.click(e as unknown as DOMElementContract); - expect(e.checked).toBe(false); - expect(seen).toEqual(['click', 'input', 'change', 'click', 'input', 'change']); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/buttons.ts b/packages/web-platform-tests/src/compatibility/input/mouse/buttons.ts deleted file mode 100644 index aa4093d..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/buttons.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { DOMElement as DOMElementContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMEvent, DOMMouseEvent } from '../../contracts/events.ts'; - -export const spec = 'https://w3c.github.io/uievents/#dom-mouseevent-buttons'; -export const input = ['mouse'] as const; -export const requires = ['input.mouse.delivery', 'events.data.mouse'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - for (const [button, mask] of [ - [0, 1], - [1, 4], - [2, 2], - ] as const) { - context.test( - `reports button ${button} with buttons mask ${mask} on press and zero on release`, - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('button')); - target.type = 'button'; - target.textContent = 'Buttons'; - target.addEventListener('contextmenu', (event) => event.preventDefault()); - target.addEventListener('auxclick', (event) => event.preventDefault()); - const calls: [string, number, number][] = []; - for (const type of ['mousedown', 'mouseup']) { - target.addEventListener(type, (event) => { - const mouse = event as DOMMouseEvent; - calls.push([mouse.type, mouse.button, mouse.buttons]); - }); - } - await fixture.input.move(target); - await fixture.input.down(button); - await fixture.input.up(button); - expect(calls).toEqual([ - ['mousedown', button, mask], - ['mouseup', button, 0], - ]); - }, - ); - } - - context.test( - 'preserves other held buttons when a button is pressed or released', - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('button')); - target.type = 'button'; - target.textContent = 'Chord'; - target.addEventListener('contextmenu', (event) => event.preventDefault()); - target.addEventListener('auxclick', (event) => event.preventDefault()); - const states: [string, number, number][] = []; - for (const type of ['mousedown', 'mouseup']) { - target.addEventListener(type, (event) => { - const mouse = event as DOMMouseEvent; - states.push([mouse.type, mouse.button, mouse.buttons]); - }); - } - await fixture.input.move(target); - await fixture.input.down(0); - await fixture.input.down(2); - await fixture.input.up(0); - await fixture.input.up(2); - expect(states).toEqual([ - ['mousedown', 0, 1], - ['mousedown', 2, 3], - ['mouseup', 0, 2], - ['mouseup', 2, 0], - ]); - }, - ); - - context.test( - 'reports held and released buttons during movement to another element', - async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'Start'; - second.textContent = 'Destination'; - root.append(first, second); - fixture.connect(root); - // Suppress selection/drag defaults while observing the mouse button state. - first.addEventListener('mousedown', (event) => event.preventDefault()); - const moves: DOMMouseEvent[] = []; - const record = (event: DOMEvent): void => { - moves.push(event as DOMMouseEvent); - }; - first.addEventListener('mousemove', record); - second.addEventListener('mousemove', record); - await fixture.input.move(first); - moves.length = 0; - await fixture.input.down(); - await fixture.input.move(second); - expect(moves.length).toBeGreaterThan(0); - expect(moves.at(-1)?.target).toBe(second); - expect(moves.at(-1)?.buttons).toBe(1); - await fixture.input.up(); - moves.length = 0; - await fixture.input.move(first); - expect(moves.length).toBeGreaterThan(0); - expect(moves.at(-1)?.target).toBe(first); - expect(moves.at(-1)?.buttons).toBe(0); - }, - ); - - context.test( - 'does not turn auxiliary or secondary button clicks into primary click events', - async (fixture) => { - const target = fixture.connect(fixture.document.createElement('button')); - target.type = 'button'; - target.textContent = 'Auxiliary'; - target.addEventListener('contextmenu', (event) => event.preventDefault()); - target.addEventListener('auxclick', (event) => event.preventDefault()); - let clicks = 0; - target.addEventListener('click', () => clicks++); - await fixture.input.click(target, 1); - await fixture.input.click(target, 2); - expect(clicks).toBe(0); - await fixture.input.click(target, 0); - expect(clicks).toBe(1); - }, - ); - - context.test( - 'canceling auxiliary activation does not produce a primary click', - async (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('button'); - fixture.connect(e as unknown as DOMElementContract); - const clicks: number[] = [], - aux: number[] = []; - e.addEventListener('click', (event) => clicks.push(event.button)); - e.addEventListener('auxclick', (event) => { - aux.push(event.button); - event.preventDefault(); - }); - await fixture.input.click(e as unknown as DOMElementContract, 1); - expect(clicks).toEqual([]); - expect(aux).toEqual([1]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/delivery.ts b/packages/web-platform-tests/src/compatibility/input/mouse/delivery.ts deleted file mode 100644 index e032cad..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/delivery.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMEvent, DOMEventTarget } from '../../contracts/events.ts'; - -export const spec = 'https://w3c.github.io/uievents/#events-mouseevent-event-order'; -export const input = ['mouse'] as const; -export const requires = [ - 'events.dispatch.target', - 'events.dispatch.capture', - 'events.dispatch.bubbling', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'delivers down, up, and click through capture, target, and bubble', - async (fixture) => { - const { document } = fixture; - const root = document.createElement('div'); - const button = document.createElement('button'); - button.type = 'button'; - button.textContent = 'Target'; - root.appendChild(button); - fixture.connect(root); - const calls: { - type: string; - target: DOMEventTarget | null; - currentTarget: DOMEventTarget | null; - phase: number; - bubbles: boolean; - cancelable: boolean; - }[] = []; - const record = (event: DOMEvent): void => { - calls.push({ - type: event.type, - target: event.target, - currentTarget: event.currentTarget, - phase: event.eventPhase, - bubbles: event.bubbles, - cancelable: event.cancelable, - }); - }; - for (const type of ['mousedown', 'mouseup', 'click']) { - root.addEventListener(type, record, true); - button.addEventListener(type, record); - root.addEventListener(type, record); - } - await fixture.input.click(button); - expect(calls).toEqual( - ['mousedown', 'mouseup', 'click'].flatMap((type) => - [root, button, root].map((currentTarget, index) => ({ - type, - target: button, - currentTarget, - phase: index + 1, - bubbles: true, - cancelable: true, - })), - ), - ); - }, - ); - - context.test( - 'defers mouseup and click until the held primary button is released', - async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - button.textContent = 'Hold'; - const calls: string[] = []; - for (const type of ['mousedown', 'mouseup', 'click']) - button.addEventListener(type, (event) => calls.push(event.type)); - await fixture.input.move(button); - await fixture.input.down(); - expect(calls).toEqual(['mousedown']); - await fixture.input.up(); - expect(calls).toEqual(['mousedown', 'mouseup', 'click']); - }, - ); - - context.test( - 'retargets mouse delivery when the pointer moves between sibling controls', - async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'First'; - second.textContent = 'Second'; - root.append(first, second); - fixture.connect(root); - const targets: (DOMEventTarget | null)[] = []; - root.addEventListener('click', (event) => targets.push(event.target)); - await fixture.input.click(first); - await fixture.input.click(second); - expect(targets).toEqual([first, second]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/focus-visible.ts b/packages/web-platform-tests/src/compatibility/input/mouse/focus-visible.ts deleted file mode 100644 index aaa7fa0..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/focus-visible.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Adapted from WPT focus-visible-002.html and focus-visible-003.html at -// b719612f963a05ece9c89a7dd7598c1c59618874. Copyright WPT contributors; Alice Boxhall. -// BSD-3-Clause; ../../../../test/wpt/LICENSE and ../../../../docs/wpt-state-styles.md. -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMElement } from '../../contracts/nodes.ts'; -import { createStyleFixture } from '../../style-fixture.ts'; -export const spec = 'https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/selectors/focus-visible-002.html'; -export const input = ['mouse'] as const; -export const requires = ['input.mouse.delivery', 'style.focusState'] as const; -export const globals = ['getComputedStyle'] as const; -/** Registers the explicit text-entry versus non-text mouse focus indication policy. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - for (const markup of [ - '', - '', - '', - '', - ]) { - context.test(`mouse focus on ${markup} matches focus-visible and styles`, async (fixture) => { - const { document, root, get } = createStyleFixture(fixture, { - markup: markup, - css: ':focus-visible {color:rgb(4,5,6)}', - }); - const target = root.firstElementChild as HTMLElement; - await fixture.input.click(target as unknown as DOMElement); - expect(document.activeElement).toBe(target); - expect(target.matches(':focus-visible')).toBe(true); - expect(get(target).color).toBe('rgb(4, 5, 6)'); - target.blur(); - expect(target.matches(':focus-visible')).toBe(false); - }); - } - for (const markup of [ - '', - 'Span', - '', - ]) { - context.test( - `mouse focus on ${markup} matches focus without focus-visible`, - async (fixture) => { - const { document, root, get } = createStyleFixture(fixture, { - markup: markup, - css: ':focus {color:rgb(1,2,3)} :focus-visible {color:rgb(4,5,6)}', - }); - const target = root.firstElementChild as HTMLElement; - await fixture.input.click(target as unknown as DOMElement); - expect(document.activeElement).toBe(target); - expect(target.matches(':focus')).toBe(true); - expect(target.matches(':focus-visible')).toBe(false); - expect(get(target).color).toBe('rgb(1, 2, 3)'); - }, - ['input.mouse.focus'], - ); - } -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/focus.ts b/packages/web-platform-tests/src/compatibility/input/mouse/focus.ts deleted file mode 100644 index cff189e..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/focus.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMElement } from '../../contracts/nodes.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/interaction.html#click-focusable'; -export const input = ['mouse'] as const; -// Click focusability is a host/platform policy. Enable this leaf for hosts that -// focus enabled buttons on primary mousedown. -export const requires = [ - 'input.mouse.delivery', - 'focus.eligibility', - 'focus.transitions', - 'events.dispatch.cancellation', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test('primary mousedown transfers focus before mouseup and click', async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'Previously focused'; - second.textContent = 'Mouse target'; - root.append(first, second); - fixture.connect(root); - first.focus(); - const calls: string[] = []; - const activeDuringRelease: (DOMElement | null)[] = []; - first.addEventListener('blur', () => calls.push('blur')); - second.addEventListener('focus', () => calls.push('focus')); - for (const type of ['mouseup', 'click']) - second.addEventListener(type, () => activeDuringRelease.push(fixture.document.activeElement)); - await fixture.input.move(second); - await fixture.input.down(); - expect(fixture.document.activeElement).toBe(second); - expect(calls).toEqual(['blur', 'focus']); - await fixture.input.up(); - expect(activeDuringRelease).toEqual([second, second]); - }); - - context.test( - 'canceling mousedown preserves focus while still allowing click delivery', - async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'Keep focus'; - second.textContent = 'Cancel mousedown'; - root.append(first, second); - fixture.connect(root); - first.focus(); - const transitions: string[] = []; - let clicks = 0; - first.addEventListener('blur', () => transitions.push('blur')); - second.addEventListener('focus', () => transitions.push('focus')); - second.addEventListener('mousedown', (event) => event.preventDefault()); - second.addEventListener('click', () => clicks++); - await fixture.input.click(second); - expect(fixture.document.activeElement).toBe(first); - expect(transitions).toEqual([]); - expect(clicks).toBe(1); - }, - ); - - context.test( - 'clicking the focused control does not repeat focus transitions', - async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - button.textContent = 'Already focused'; - button.focus(); - const transitions: string[] = []; - let clicks = 0; - for (const type of ['blur', 'focusout', 'focus', 'focusin']) - button.addEventListener(type, (event) => transitions.push(event.type)); - button.addEventListener('click', () => clicks++); - await fixture.input.click(button); - expect(fixture.document.activeElement).toBe(button); - expect(transitions).toEqual([]); - expect(clicks).toBe(1); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/hover.ts b/packages/web-platform-tests/src/compatibility/input/mouse/hover.ts deleted file mode 100644 index b21e28f..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/hover.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMEvent, DOMEventTarget, DOMMouseEvent } from '../../contracts/events.ts'; - -export const spec = 'https://w3c.github.io/uievents/#events-mouseevent-event-order'; -export const input = ['mouse'] as const; -export const requires = [ - 'input.mouse.delivery', - 'events.data.mouse', - 'events.dispatch.capture', - 'events.dispatch.bubbling', -] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'moving between siblings leaves the old target before entering the new one', - async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'First'; - second.textContent = 'Second'; - root.append(first, second); - fixture.connect(root); - await fixture.input.move(first); - const calls: [string, DOMEventTarget | null, DOMEventTarget | null][] = []; - const record = (event: DOMEvent): void => { - const mouse = event as DOMMouseEvent; - calls.push([mouse.type, mouse.target, mouse.relatedTarget]); - }; - for (const target of [first, second]) - for (const type of ['mouseout', 'mouseleave', 'mouseover', 'mouseenter']) - target.addEventListener(type, record); - await fixture.input.move(second); - expect(calls).toEqual([ - ['mouseout', first, second], - ['mouseleave', first, second], - ['mouseover', second, first], - ['mouseenter', second, first], - ]); - }, - ); - - context.test( - 'over and out bubble while enter and leave are captured without bubbling', - async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'First'; - second.textContent = 'Second'; - root.append(first, second); - fixture.connect(root); - await fixture.input.move(first); - const calls: [string, string, boolean, DOMEventTarget | null][] = []; - for (const type of ['mouseout', 'mouseleave', 'mouseover', 'mouseenter']) { - root.addEventListener( - type, - (event) => { - calls.push(['capture', event.type, event.bubbles, event.target]); - }, - true, - ); - root.addEventListener(type, (event) => { - calls.push(['bubble', event.type, event.bubbles, event.target]); - }); - } - await fixture.input.move(second); - expect(calls).toEqual([ - ['capture', 'mouseout', true, first], - ['bubble', 'mouseout', true, first], - ['capture', 'mouseleave', false, first], - ['capture', 'mouseover', true, second], - ['bubble', 'mouseover', true, second], - ['capture', 'mouseenter', false, second], - ]); - }, - ); - - context.test( - 'movement within a common ancestor does not leave or reenter that ancestor', - async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'First'; - second.textContent = 'Second'; - root.append(first, second); - fixture.connect(root); - await fixture.input.move(first); - const rootTransitions: string[] = []; - const childTransitions: string[] = []; - for (const type of ['mouseenter', 'mouseleave']) { - root.addEventListener(type, (event) => rootTransitions.push(event.type)); - first.addEventListener(type, (event) => childTransitions.push(`first:${event.type}`)); - second.addEventListener(type, (event) => childTransitions.push(`second:${event.type}`)); - } - await fixture.input.move(second); - await fixture.input.move(first); - expect(rootTransitions).toEqual([]); - expect(childTransitions).toEqual([ - 'first:mouseleave', - 'second:mouseenter', - 'second:mouseleave', - 'first:mouseenter', - ]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/shadow.ts b/packages/web-platform-tests/src/compatibility/input/mouse/shadow.ts deleted file mode 100644 index 8849a8c..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/shadow.ts +++ /dev/null @@ -1,147 +0,0 @@ -// Adapted from WPT shadow event paths, related-target tests, and native mouseover/out tests -// at b719612f963a05ece9c89a7dd7598c1c59618874. Copyright © web-platform-tests contributors. -// Shadow test authors: Hayato Ito and Ryosuke Niwa. 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/#concept-event-dispatch'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/shadow-dom/event-with-related-target.html'; -export const input = ['mouse'] as const; -export const requires = [ - 'input.mouse.delivery', - 'shadow.events.composedPath', - 'shadow.events.relatedTarget', -] 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) { - context.test( - `retargets native mouse down/up/click outside a ${mode} shadow root`, - async (fixture) => { - const document = fixture.document as unknown as Document; - const host = document.createElement('div'); - const root = host.attachShadow({ mode }); - const target = document.createElement('button'); - target.type = 'button'; - target.textContent = 'Shadow target'; - root.append(target); - fixture.connect(host as unknown as DOMElementContract); - const calls: unknown[] = []; - for (const type of ['mousedown', 'mouseup', 'click']) { - target.addEventListener(type, (event) => - calls.push([ - type, - 'inside', - event.target, - event.composed, - event.composedPath().slice(0, 3), - ]), - ); - host.addEventListener(type, (event) => - calls.push([ - type, - 'outside', - event.target, - event.composed, - event - .composedPath() - .filter((node) => node === target || node === root || node === host), - ]), - ); - } - await fixture.input.click(target as unknown as DOMElementContract); - const outsidePath = mode === 'open' ? [target, root, host] : [host]; - expect(calls).toEqual([ - ['mousedown', 'inside', target, true, [target, root, host]], - ['mousedown', 'outside', host, true, outsidePath], - ['mouseup', 'inside', target, true, [target, root, host]], - ['mouseup', 'outside', host, true, outsidePath], - ['click', 'inside', target, true, [target, root, host]], - ['click', 'outside', host, true, outsidePath], - ]); - }, - ); - } - - context.test( - 'canceling a retargeted host click rolls back shadow checkbox activation', - async (fixture) => { - const document = fixture.document as unknown as Document; - const host = document.createElement('div'); - const root = host.attachShadow({ mode: 'closed' }); - const target = document.createElement('input'); - target.type = 'checkbox'; - target.indeterminate = true; - root.append(target); - fixture.connect(host as unknown as DOMElementContract); - const calls: unknown[] = []; - target.addEventListener('click', (event) => - calls.push(['inside', event.target, target.checked, target.indeterminate]), - ); - host.addEventListener('click', (event) => { - calls.push(['outside', event.target, target.checked, target.indeterminate]); - event.preventDefault(); - }); - for (const type of ['input', 'change']) - target.addEventListener(type, () => calls.push([type])); - await fixture.input.click(target as unknown as DOMElementContract); - expect(calls).toEqual([ - ['inside', target, true, false], - ['outside', host, true, false], - ]); - expect([target.checked, target.indeterminate]).toEqual([false, true]); - }, - ['elements.input.activation', 'events.dispatch.cancellation'], - ); - - context.test( - 'trims internal hover transitions and retargets relatedTarget when leaving shadow', - async (fixture) => { - const document = fixture.document as unknown as Document; - const container = document.createElement('div'); - const host = document.createElement('div'); - const root = host.attachShadow({ mode: 'closed' }); - const first = document.createElement('button'), - second = document.createElement('button'), - outside = document.createElement('button'); - first.type = second.type = outside.type = 'button'; - first.textContent = 'First'; - second.textContent = 'Second'; - outside.textContent = 'Outside'; - root.append(first, second); - container.append(host, outside); - fixture.connect(container as unknown as DOMElementContract); - await fixture.input.move(first as unknown as DOMElementContract); - const insideCalls: unknown[] = [], - outsideCalls: unknown[] = []; - for (const type of ['mouseover', 'mouseout']) { - root.addEventListener(type, (event) => - insideCalls.push([type, event.target, (event as MouseEvent).relatedTarget]), - ); - host.addEventListener(type, (event) => - outsideCalls.push([type, event.target, (event as MouseEvent).relatedTarget]), - ); - outside.addEventListener(type, (event) => - outsideCalls.push([type, event.target, (event as MouseEvent).relatedTarget]), - ); - } - await fixture.input.move(second as unknown as DOMElementContract); - expect(insideCalls).toEqual([ - ['mouseout', first, second], - ['mouseover', second, first], - ]); - expect(outsideCalls).toEqual([]); - insideCalls.length = 0; - await fixture.input.move(outside as unknown as DOMElementContract); - expect(insideCalls).toEqual([['mouseout', second, outside]]); - expect(outsideCalls).toEqual([ - ['mouseout', host, outside], - ['mouseover', outside, host], - ]); - }, - ['input.mouse.hover'], - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/state-styles.ts b/packages/web-platform-tests/src/compatibility/input/mouse/state-styles.ts deleted file mode 100644 index e2a8105..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/state-styles.ts +++ /dev/null @@ -1,131 +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 type { DOMElement } from '../../contracts/nodes.ts'; -import { createStyleFixture } from '../../style-fixture.ts'; -export const spec = 'https://drafts.csswg.org/selectors-4/#useraction-pseudos'; -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/css/selectors/invalidation/user-action-pseudo-classes-in-has.html'; -export const input = ['mouse'] as const; -export const requires = [ - 'input.mouse.delivery', - 'input.mouse.hover', - 'element.selectors', - 'parsing.fragments', - 'style.computed', -] as const; -export const globals = ['getComputedStyle'] as const; -/** Registers pointer state and CSS interactions through the host input pipeline. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'hover follows pointer movement and updates both targets and ancestors', - async (fixture) => { - const { root, get } = createStyleFixture(fixture, { - markup: '
      ', - css: 'section, button {color:rgb(1,2,3)} section:hover {color:rgb(4,5,6)} button:hover {color:rgb(7,8,9)}', - }); - const first = root.querySelector('button')!, - outside = root.querySelector('#outside')!, - section = root.querySelector('section')!; - await fixture.input.move(first as unknown as DOMElement); - expect(first.matches(':hover'), 'target hover after move').toBe(true); - expect(section.matches(':hover'), 'ancestor hover after move').toBe(true); - expect(get(first).color).toBe('rgb(7, 8, 9)'); - expect(get(section).color).toBe('rgb(4, 5, 6)'); - await fixture.input.move(outside as unknown as DOMElement); - expect(first.matches(':hover')).toBe(false); - expect(section.matches(':hover')).toBe(false); - expect(get(first).color).toBe('rgb(1, 2, 3)'); - expect(get(outside).color).toBe('rgb(7, 8, 9)'); - }, - ); - context.test( - 'mouse down and up enter and leave active while retaining hover', - async (fixture) => { - const { root, get } = createStyleFixture(fixture, { - markup: '', - css: 'button:hover {color:rgb(1,2,3)} button:hover:active {color:rgb(4,5,6)}', - }); - const target = root.querySelector('button')!; - await fixture.input.move(target as unknown as DOMElement); - expect(target.matches(':active')).toBe(false); - await fixture.input.down(); - expect(target.matches(':hover:active'), 'hover and active after down').toBe(true); - expect(get(target).color).toBe('rgb(4, 5, 6)'); - await fixture.input.up(); - expect(target.matches(':active')).toBe(false); - expect(target.matches(':hover'), 'hover after release').toBe(true); - expect(get(target).color).toBe('rgb(1, 2, 3)'); - }, - ); - context.test( - 'relational hover and active update an ancestor and a sibling descendant', - async (fixture) => { - const { root, get } = createStyleFixture(fixture, { - markup: - '
      Other
      ', - css: 'section, i {color:rgb(1,2,3)} section:has(button:hover) {color:rgb(4,5,6)} section:has(button:hover) i {color:rgb(7,8,9)} section:has(button:active) i {color:rgb(10,11,12)}', - }); - const target = root.querySelector('button')!, - section = root.querySelector('section')!, - sibling = root.querySelector('i')!; - await fixture.input.move(target as unknown as DOMElement); - expect(section.matches(':has(button:hover)')).toBe(true); - expect(get(section).color).toBe('rgb(4, 5, 6)'); - expect(get(sibling).color).toBe('rgb(7, 8, 9)'); - await fixture.input.down(); - expect(get(sibling).color).toBe('rgb(10, 11, 12)'); - await fixture.input.up(); - expect(get(sibling).color).toBe('rgb(7, 8, 9)'); - await fixture.input.move(root.querySelector('#outside')! as unknown as DOMElement); - expect(get(sibling).color).toBe('rgb(1, 2, 3)'); - }, - ['style.relationalSelectors'], - ); - context.test( - 'canceling mousedown preserves prior focus and focus styles', - async (fixture) => { - const { document, root, get } = createStyleFixture(fixture, { - markup: '', - css: 'button {color:rgb(1,2,3)} button:focus {color:rgb(4,5,6)}', - }); - const [first, second] = Array.from(root.querySelectorAll('button')); - first!.focus(); - second!.addEventListener('mousedown', (event) => event.preventDefault()); - await fixture.input.click(second! as unknown as DOMElement); - expect(document.activeElement).toBe(first); - expect(get(first!).color).toBe('rgb(4, 5, 6)'); - expect(get(second!).color).toBe('rgb(1, 2, 3)'); - }, - ['input.mouse.focus', 'style.focusState'], - ); - context.test( - 'native checkbox activation and cancellation update checked styles with rollback', - async (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')!; - await fixture.input.click(target as unknown as DOMElement); - expect(target.matches(':checked')).toBe(true); - expect(get(target).color).toBe('rgb(4, 5, 6)'); - const seen: unknown[] = []; - target.addEventListener( - 'click', - (event) => { - seen.push(target.matches(':checked'), get(target).color); - event.preventDefault(); - }, - { once: true }, - ); - await fixture.input.click(target as unknown as DOMElement); - expect(seen).toEqual([false, 'rgb(1, 2, 3)']); - expect(target.matches(':checked')).toBe(true); - expect(get(target).color).toBe('rgb(4, 5, 6)'); - }, - ['input.mouse.activation', 'style.controlSelectors', 'events.listeners.once'], - ); -} diff --git a/packages/web-platform-tests/src/compatibility/input/mouse/trusted-events.ts b/packages/web-platform-tests/src/compatibility/input/mouse/trusted-events.ts deleted file mode 100644 index 4b3288e..0000000 --- a/packages/web-platform-tests/src/compatibility/input/mouse/trusted-events.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { CompatibilityContext } from '../../context.ts'; -import type { DOMEvent } from '../../contracts/events.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#dom-event-istrusted'; -export const input = ['mouse'] as const; -export const requires = ['input.mouse.delivery', 'input.mouse.hover'] as const; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test('input-generated down, up, and click events are trusted', async (fixture) => { - const button = fixture.connect(fixture.document.createElement('button')); - button.type = 'button'; - button.textContent = 'Trusted click'; - const events: DOMEvent[] = []; - for (const type of ['mousedown', 'mouseup', 'click']) - button.addEventListener(type, (event) => events.push(event)); - await fixture.input.click(button); - expect(events.map((event) => [event.type, event.isTrusted])).toEqual([ - ['mousedown', true], - ['mouseup', true], - ['click', true], - ]); - }); - - context.test('input-generated hover and movement events are trusted', async (fixture) => { - const root = fixture.document.createElement('div'); - const first = fixture.document.createElement('button'); - const second = fixture.document.createElement('button'); - first.type = second.type = 'button'; - first.textContent = 'Start'; - second.textContent = 'Destination'; - root.append(first, second); - fixture.connect(root); - await fixture.input.move(first); - const events: DOMEvent[] = []; - for (const target of [first, second]) - for (const type of ['mouseout', 'mouseleave', 'mouseover', 'mouseenter', 'mousemove']) - target.addEventListener(type, (event) => events.push(event)); - await fixture.input.move(second); - for (const type of ['mouseout', 'mouseleave', 'mouseover', 'mouseenter', 'mousemove']) { - const matching = events.filter((event) => event.type === type); - expect(matching.length).toBeGreaterThan(0); - expect(matching.every((event) => event.isTrusted)).toBe(true); - } - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/detached.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/detached.ts deleted file mode 100644 index 2259ffe..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/detached.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const requires = ['mutationObserver.subtree', 'mutationObserver.microtaskDelivery'] as const; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'observes an explicitly targeted detached node', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributes: true }); - target.setAttribute('title', 'detached'); - const [record] = observer.takeRecords(); - expect(record?.target).toBe(target); - }, - ); - context.test( - 'tracks a removed descendant until the removal notification microtask', - async ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const root = document.createElement('div'); - const child = document.createElement('span'); - root.appendChild(child); - const delivered: string[][] = []; - const observer = new MutationObserver((records) => { - delivered.push( - records.map((record) => { - return record.type; - }), - ); - }); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(root, { childList: true, attributes: true, subtree: true }); - root.removeChild(child); - child.setAttribute('title', 'still observed'); - await Promise.resolve(); - expect(delivered).toEqual([['childList', 'attributes']]); - child.setAttribute('title', 'no longer observed'); - await Promise.resolve(); - expect(delivered).toEqual([['childList', 'attributes']]); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/disconnect.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/disconnect.ts deleted file mode 100644 index 63dbc93..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/disconnect.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'discards pending records and allows later re-observation', - async ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - let callbacks = 0; - const observer = new MutationObserver(() => { - callbacks++; - }); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributes: true }); - target.setAttribute('id', 'pending'); - observer.disconnect(); - expect(observer.takeRecords()).toEqual([]); - target.setAttribute('id', 'unobserved'); - await Promise.resolve(); - expect(callbacks).toBe(0); - observer.observe(target, { attributes: true }); - target.setAttribute('id', 'observed again'); - expect(observer.takeRecords().length).toBe(1); - }, - ); - - context.test('discards queued records and permits subsequent observation', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const o = new MutationObserver(() => {}); - fixture.cleanup(() => o.disconnect()); - const e = document.createElement('div'); - o.observe(e as unknown as DOMNodeContract, { attributes: true }); - e.setAttribute('x', '1'); - o.disconnect(); - expect(o.takeRecords()).toEqual([]); - e.setAttribute('x', '2'); - expect(o.takeRecords()).toEqual([]); - o.observe(e as unknown as DOMNodeContract, { attributes: true }); - e.setAttribute('x', '3'); - expect(o.takeRecords().length).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/filtering.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/filtering.ts deleted file mode 100644 index b21e2fa..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/filtering.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'filters by exact local name and excludes namespaced attributes', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributeFilter: ['title'] }); - target.setAttribute('id', 'ignored'); - target.setAttribute('title', 'observed'); - target.setAttributeNS('urn:compatibility', 'x:title', 'ignored'); - expect( - observer.takeRecords().map((record) => { - return record.attributeName; - }), - ).toEqual(['title']); - observer.observe(target, { attributeFilter: [] }); - target.setAttribute('title', 'ignored'); - expect(observer.takeRecords()).toEqual([]); - }, - ); - - context.test( - 'matches attribute filters exactly and excludes namespaced attributes', - (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const observer = new MutationObserver(() => {}); - fixture.cleanup(() => observer.disconnect()); - const e = document.createElement('div'); - observer.observe(e as unknown as DOMNodeContract, { - attributes: true, - attributeFilter: ['x'], - }); - e.setAttribute('y', 'a'); - e.setAttributeNS('urn:test', 'p:x', 'b'); - e.setAttribute('x', 'c'); - const records = observer.takeRecords(); - expect(records.length).toBe(1); - expect(records[0]!.attributeName).toBe('x'); - expect(records[0]!.attributeNamespace).toBeNull(); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/microtask-delivery.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/microtask-delivery.ts deleted file mode 100644 index db46003..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/microtask-delivery.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'batches records into a microtask and supplies the observer as receiver and argument', - async ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const deliveries: (string | null)[][] = []; - const receivers: unknown[] = []; - const arguments_: unknown[] = []; - const observer = new MutationObserver(function (records, suppliedObserver) { - receivers.push(this); - arguments_.push(suppliedObserver); - deliveries.push( - records.map((record) => { - return record.attributeName; - }), - ); - }); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributes: true }); - target.setAttribute('id', 'one'); - target.setAttribute('title', 'two'); - expect(deliveries).toEqual([]); - await Promise.resolve(); - expect(deliveries).toEqual([['id', 'title']]); - expect(receivers[0]).toBe(observer); - expect(arguments_[0]).toBe(observer); - expect(observer.takeRecords()).toEqual([]); - }, - ); - - context.test( - 'batches synchronous mutations and delivers before a later queued microtask', - async (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const seen: string[] = []; - const o = new MutationObserver((records) => seen.push('observer:' + records.length)); - fixture.cleanup(() => o.disconnect()); - const e = document.createElement('div'); - o.observe(e as unknown as DOMNodeContract, { attributes: true }); - e.setAttribute('x', '1'); - e.setAttribute('y', '2'); - queueMicrotask(() => seen.push('microtask')); - expect(seen).toEqual([]); - await new Promise((resolve) => queueMicrotask(resolve)); - expect(seen).toEqual(['observer:2', 'microtask']); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/old-value.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/old-value.ts deleted file mode 100644 index 7e6eb54..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/old-value.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'captures prior attribute and character-data values in mutation order', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const text = document.createTextNode('a'); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributeOldValue: true }); - observer.observe(text, { characterDataOldValue: true }); - target.setAttribute('title', 'one'); - target.setAttribute('title', 'two'); - target.removeAttribute('title'); - text.data = 'b'; - text.data = 'c'; - expect( - observer.takeRecords().map((record) => { - return record.oldValue; - }), - ).toEqual([null, 'one', 'two', 'a', 'b']); - }, - ); - - context.test('records each prior value including null and repeated assignments', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const observer = new MutationObserver(() => {}); - fixture.cleanup(() => observer.disconnect()); - const e = document.createElement('div'); - observer.observe(e as unknown as DOMNodeContract, { - attributes: true, - attributeOldValue: true, - }); - e.setAttribute('x', 'a'); - e.setAttribute('x', 'a'); - e.removeAttribute('x'); - expect(observer.takeRecords().map((r) => r.oldValue)).toEqual([null, 'a', 'a']); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/records/attributes.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/records/attributes.ts deleted file mode 100644 index 59d2bf0..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/records/attributes.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'records attribute names, namespaces, and mutations that repeat a value', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributes: true }); - target.setAttribute('title', 'one'); - target.setAttribute('title', 'one'); - target.removeAttribute('title'); - target.setAttributeNS('urn:compatibility', 'x:flag', 'yes'); - const records = observer.takeRecords(); - expect(records.length).toBe(4); - expect( - records.map((record) => { - return record.attributeName; - }), - ).toEqual(['title', 'title', 'title', 'flag']); - expect( - records.map((record) => { - return record.attributeNamespace; - }), - ).toEqual([null, null, null, 'urn:compatibility']); - for (const record of records) { - expect(record.type).toBe('attributes'); - expect(record.target).toBe(target); - expect(record.addedNodes.length).toBe(0); - expect(record.removedNodes.length).toBe(0); - expect(record.previousSibling).toBeNull(); - expect(record.nextSibling).toBeNull(); - expect(record.oldValue).toBeNull(); - } - }, - ); - - context.test('records namespace and local name independently of prefix', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'), - o = new MutationObserver(() => {}); - fixture.cleanup(() => o.disconnect()); - const e = document.createElement('div'); - o.observe(e as unknown as DOMNodeContract, { attributes: true, attributeOldValue: true }); - e.setAttributeNS('urn:x', 'p:name', 'a'); - e.setAttributeNS('urn:x', 'q:name', 'b'); - const records = o.takeRecords(); - expect(records.map((r) => [r.type, r.attributeName, r.attributeNamespace, r.oldValue])).toEqual( - [ - ['attributes', 'name', 'urn:x', null], - ['attributes', 'name', 'urn:x', 'a'], - ], - ); - expect(records[0]!.addedNodes.length).toBe(0); - expect(records[0]!.removedNodes.length).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/records/character-data.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/records/character-data.ts deleted file mode 100644 index 96a6b51..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/records/character-data.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'records text and comment data mutations without old values by default', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const text = document.createTextNode('before'); - const comment = document.createComment('before'); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(text, { characterData: true }); - observer.observe(comment, { characterData: true }); - text.data = 'after'; - comment.data = 'after'; - const records = observer.takeRecords(); - expect(records.length).toBe(2); - expect(records[0]?.target).toBe(text); - expect(records[1]?.target).toBe(comment); - for (const record of records) { - expect(record.type).toBe('characterData'); - expect(record.oldValue).toBeNull(); - expect(record.addedNodes.length).toBe(0); - expect(record.removedNodes.length).toBe(0); - expect(record.attributeName).toBeNull(); - expect(record.attributeNamespace).toBeNull(); - } - }, - ); - - context.test('reports each edit with previous data and null attribute fields', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'), - o = new MutationObserver(() => {}); - fixture.cleanup(() => o.disconnect()); - const t = document.createTextNode('abc'); - o.observe(t as unknown as DOMNodeContract, { - characterData: true, - characterDataOldValue: true, - }); - t.appendData('d'); - t.deleteData(0, 1); - const records = o.takeRecords(); - expect(records.map((r) => r.oldValue)).toEqual(['abc', 'abcd']); - expect(records[0]!.attributeName).toBeNull(); - expect(records[0]!.attributeNamespace).toBeNull(); - expect(records[0]!.previousSibling).toBeNull(); - expect(records[0]!.target).toBe(t); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/records/child-list.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/records/child-list.ts deleted file mode 100644 index 5d0e42c..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/records/child-list.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../../contracts/nodes.ts'; -import type { CompatibilityContext } from '../../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'records inserted and removed node identities with sibling positions', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const parent = document.createElement('div'); - const first = document.createElement('span'); - const last = document.createElement('span'); - parent.appendChild(first); - parent.appendChild(last); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(parent, { childList: true }); - const inserted = document.createTextNode('inserted'); - parent.insertBefore(inserted, last); - parent.removeChild(inserted); - const records = observer.takeRecords(); - expect(records.length).toBe(2); - const addition = records[0]; - const removal = records[1]; - expect(addition?.type).toBe('childList'); - expect(addition?.target).toBe(parent); - expect(addition?.addedNodes.length).toBe(1); - expect(addition?.addedNodes[0]).toBe(inserted); - expect(addition?.removedNodes.length).toBe(0); - expect(addition?.previousSibling).toBe(first); - expect(addition?.nextSibling).toBe(last); - expect(addition?.attributeName).toBeNull(); - expect(addition?.attributeNamespace).toBeNull(); - expect(addition?.oldValue).toBeNull(); - expect(removal?.addedNodes.length).toBe(0); - expect(removal?.removedNodes.length).toBe(1); - expect(removal?.removedNodes[0]).toBe(inserted); - expect(removal?.previousSibling).toBe(first); - expect(removal?.nextSibling).toBe(last); - }, - ); - - context.test('records sibling anchors and groups fragment insertion', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const o = new MutationObserver(() => {}); - fixture.cleanup(() => o.disconnect()); - const p = document.createElement('div'), - a = document.createElement('i'), - b = document.createElement('b'), - f = document.createDocumentFragment(); - p.append(a, b); - o.observe(p as unknown as DOMNodeContract, { childList: true }); - const x = document.createElement('u'), - y = document.createTextNode('y'); - f.append(x, y); - p.insertBefore(f, b); - const records = o.takeRecords(); - expect(records.length).toBe(1); - expect(Array.from(records[0]!.addedNodes)).toEqual([x, y]); - expect(records[0]!.previousSibling).toBe(a); - expect(records[0]!.nextSibling).toBe(b); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/reentrancy.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/reentrancy.ts deleted file mode 100644 index f2505c5..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/reentrancy.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const requires = ['mutationObserver.microtaskDelivery'] as const; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'queues callback mutations for another notification', - async ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const deliveries: string[][] = []; - let inCallback = false; - let reentered = false; - const observer = new MutationObserver((records) => { - if (inCallback) reentered = true; - inCallback = true; - deliveries.push( - records.map((record) => { - return record.attributeName ?? ''; - }), - ); - if (deliveries.length === 1) target.setAttribute('title', 'second'); - inCallback = false; - }); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributes: true }); - target.setAttribute('id', 'first'); - await Promise.resolve(); - await Promise.resolve(); - expect(deliveries).toEqual([['id'], ['title']]); - expect(reentered).toBe(false); - }, - ); - - context.test('delivers callback-generated mutations in a later notification', async (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'), - e = document.createElement('div'), - seen: number[] = []; - const o = new MutationObserver((records) => { - seen.push(records.length); - if (seen.length === 1) e.setAttribute('y', '2'); - }); - fixture.cleanup(() => o.disconnect()); - o.observe(e as unknown as DOMNodeContract, { attributes: true }); - e.setAttribute('x', '1'); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(seen).toEqual([1, 1]); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/registration.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/registration.ts deleted file mode 100644 index acdcdd5..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/registration.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'rejects empty or contradictory observation options', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - expect(() => observer.observe(target, {})).toThrow( - expect.objectContaining({ name: 'TypeError' }), - ); - expect(() => - observer.observe(target, { attributes: false, attributeOldValue: true }), - ).toThrow(expect.objectContaining({ name: 'TypeError' })); - expect(() => observer.observe(target, { attributes: false, attributeFilter: [] })).toThrow( - expect.objectContaining({ name: 'TypeError' }), - ); - expect(() => - observer.observe(target, { characterData: false, characterDataOldValue: true }), - ).toThrow(expect.objectContaining({ name: 'TypeError' })); - expect(() => observer.observe(target, { attributeOldValue: true })).not.toThrow(); - expect(() => observer.observe(target, { characterDataOldValue: true })).not.toThrow(); - }, - ); - context.test( - 'replaces options when the same observer observes a target again', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributes: true }); - observer.observe(target, { childList: true }); - target.setAttribute('title', 'unobserved'); - const child = document.createElement('span'); - target.appendChild(child); - const records = observer.takeRecords(); - expect(records.length).toBe(1); - expect(records[0]?.type).toBe('childList'); - expect(records[0]?.addedNodes[0]).toBe(child); - }, - ); - - context.test( - 'requires a mutation type and rejects contradictory old-value options', - (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const observer = new MutationObserver(() => {}); - fixture.cleanup(() => observer.disconnect()); - const e = document.createElement('div'); - for (const options of [ - {}, - { attributes: false, attributeOldValue: true }, - { characterData: false, characterDataOldValue: true }, - { attributes: false, attributeFilter: ['x'] }, - ]) - expect(() => observer.observe(e as unknown as DOMNodeContract, options)).toThrow( - expect.objectContaining({ name: 'TypeError' }), - ); - }, - ); - - context.test('implicitly enables attributes when only a filter is supplied', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const observer = new MutationObserver(() => {}); - fixture.cleanup(() => observer.disconnect()); - const e = document.createElement('div'); - observer.observe(e as unknown as DOMNodeContract, { attributeFilter: ['x'] }); - e.setAttribute('x', '1'); - expect(observer.takeRecords().length).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/subtree.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/subtree.ts deleted file mode 100644 index b9af97a..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/subtree.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'observes descendants only when subtree is enabled and deduplicates overlapping registrations', - ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const root = document.createElement('div'); - const child = document.createElement('span'); - root.appendChild(child); - const observer = new MutationObserver(() => {}); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(root, { attributes: true }); - child.setAttribute('title', 'unobserved'); - expect(observer.takeRecords()).toEqual([]); - observer.observe(root, { attributes: true, subtree: true }); - observer.observe(child, { attributes: true, attributeOldValue: true }); - child.setAttribute('title', 'observed'); - const records = observer.takeRecords(); - expect(records.length).toBe(1); - expect(records[0]?.target).toBe(child); - expect(records[0]?.oldValue).toBe('unobserved'); - }, - ); - - context.test('does not include descendant mutations unless subtree is requested', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'), - o = new MutationObserver(() => {}); - fixture.cleanup(() => o.disconnect()); - const p = document.createElement('div'), - child = document.createElement('i'); - p.append(child); - o.observe(p as unknown as DOMNodeContract, { attributes: true }); - child.setAttribute('x', '1'); - expect(o.takeRecords()).toEqual([]); - o.observe(p as unknown as DOMNodeContract, { attributes: true, subtree: true }); - child.setAttribute('x', '2'); - expect(o.takeRecords()[0]!.target).toBe(child); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/mutation-observer/take-records.ts b/packages/web-platform-tests/src/compatibility/mutation-observer/take-records.ts deleted file mode 100644 index f942f14..0000000 --- a/packages/web-platform-tests/src/compatibility/mutation-observer/take-records.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { DOMNode as DOMNodeContract } from '../contracts/nodes.ts'; -import type { CompatibilityContext } from '../context.ts'; - -const requiredGlobals = ['MutationObserver'] as const; -export { requiredGlobals as globals }; -export const spec = 'https://dom.spec.whatwg.org/#mutation-observers'; - -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - - context.test( - 'drains pending records without invoking the callback', - async ({ document, globals, cleanup }) => { - const MutationObserver = globals('MutationObserver'); - const target = document.createElement('div'); - let callbacks = 0; - const observer = new MutationObserver(() => { - callbacks++; - }); - cleanup(() => { - observer.disconnect(); - }); - observer.observe(target, { attributes: true }); - target.setAttribute('id', 'one'); - const records = observer.takeRecords(); - expect(records.length).toBe(1); - expect(records[0]?.attributeName).toBe('id'); - expect(observer.takeRecords()).toEqual([]); - await Promise.resolve(); - expect(callbacks).toBe(0); - }, - ); - - context.test('drains only the calling observer and preserves record identity', (fixture) => { - const document = fixture.document as unknown as Document; - const MutationObserver = fixture.globals('MutationObserver'); - const a = new MutationObserver(() => {}), - b = new MutationObserver(() => {}); - fixture.cleanup(() => { - a.disconnect(); - b.disconnect(); - }); - const e = document.createElement('div'); - a.observe(e as unknown as DOMNodeContract, { attributes: true }); - b.observe(e as unknown as DOMNodeContract, { attributes: true }); - e.setAttribute('x', '1'); - const first = a.takeRecords(); - expect(first.length).toBe(1); - expect(a.takeRecords()).toEqual([]); - expect(b.takeRecords().length).toBe(1); - expect(first[0]!.target).toBe(e); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/adoption.ts b/packages/web-platform-tests/src/compatibility/node/adoption.ts deleted file mode 100644 index d71bf1c..0000000 --- a/packages/web-platform-tests/src/compatibility/node/adoption.ts +++ /dev/null @@ -1,51 +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 }; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'adopts descendants and attributes while preserving their identity', - ({ document, globals }) => { - const other = new (globals('Document'))(); - const parent = document.createElement('div'); - const child = document.createElement('span'); - const text = document.createTextNode('kept'); - child.setAttribute('title', 'value'); - const attr = child.getAttributeNode('title'); - child.appendChild(text); - parent.appendChild(child); - expect(other.adoptNode(child)).toBe(child); - expect(child.parentNode).toBeNull(); - expect(child.ownerDocument).toBe(other); - expect(text.ownerDocument).toBe(other); - expect(attr?.ownerDocument).toBe(other); - expect(attr?.ownerElement).toBe(child); - expect(child.firstChild).toBe(text); - }, - ); - - context.test('adopts descendants and attributes while preserving identity', (fixture) => { - const document = fixture.document as unknown as Document; - const other = document.implementation.createHTMLDocument(''); - const p = other.createElement('div'), - child = other.createElement('i'); - p.setAttribute('title', 'x'); - p.append(child); - other.body.append(p); - expect(document.adoptNode(p)).toBe(p); - expect(p.parentNode).toBeNull(); - expect(child.ownerDocument).toBe(document); - expect(p.getAttributeNode('title')!.ownerDocument).toBe(document); - }); - - context.test('rejects Document adoption without changing ownership', (fixture) => { - const document = fixture.document as unknown as Document; - expect(() => document.adoptNode(document)).toThrow( - expect.objectContaining({ name: 'NotSupportedError' }), - ); - expect(document.ownerDocument).toBeNull(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/append.ts b/packages/web-platform-tests/src/compatibility/node/append.ts deleted file mode 100644 index 1bf8784..0000000 --- a/packages/web-platform-tests/src/compatibility/node/append.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#concept-node-pre-insert'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('moves existing nodes and rejects cycles before changing a tree', ({ document }) => { - const parent = document.createElement('div'); - const other = document.createElement('div'); - const child = document.createElement('span'); - const text = document.createTextNode('state'); - child.appendChild(text); - parent.appendChild(child); - expect(other.appendChild(child)).toBe(child); - expect(parent.childNodes.length).toBe(0); - expect(child.firstChild).toBe(text); - expect(other.appendChild(child)).toBe(child); - expect(other.childNodes.length).toBe(1); - expect(() => child.appendChild(other)).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(() => child.appendChild(child)).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(child.parentNode).toBe(other); - expect(child.firstChild).toBe(text); - }); - - context.test('moves a same-parent node to the end without duplication', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('i'), - b = document.createElement('b'); - p.appendChild(a); - p.appendChild(b); - p.appendChild(a); - expect(Array.from(p.childNodes)).toEqual([b, a]); - expect(a.previousSibling).toBe(b); - expect(b.nextSibling).toBe(a); - }); - - context.test( - 'rejects a second document element and text under Document atomically', - (fixture) => { - const document = fixture.document as unknown as Document; - const other = document.implementation.createHTMLDocument(''); - const before = Array.from(other.childNodes); - expect(() => other.appendChild(other.createElement('i'))).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(() => other.appendChild(other.createTextNode('x'))).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(Array.from(other.childNodes)).toEqual(before); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/node/basic-append.ts b/packages/web-platform-tests/src/compatibility/node/basic-append.ts deleted file mode 100644 index 8776dcb..0000000 --- a/packages/web-platform-tests/src/compatibility/node/basic-append.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://dom.spec.whatwg.org/#dom-node-appendchild'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('returns the appended node and preserves insertion order', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createTextNode('a'), - b = document.createElement('span'); - expect(p.appendChild(a)).toBe(a); - expect(p.appendChild(b)).toBe(b); - expect(Array.from(p.childNodes)).toEqual([a, b]); - }); - context.test('keeps nested child identity and permits detached tree construction', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - child = document.createElement('span'), - t = document.createTextNode('x'); - child.appendChild(t); - p.appendChild(child); - expect(p.childNodes[0]).toBe(child); - expect(child.childNodes[0]).toBe(t); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/cloning.ts b/packages/web-platform-tests/src/compatibility/node/cloning.ts deleted file mode 100644 index 908524d..0000000 --- a/packages/web-platform-tests/src/compatibility/node/cloning.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#concept-node-clone'; -const requiredGlobals = ['Event'] as const; -export { requiredGlobals as globals }; -export const requires = ['events.dispatch.target'] as const; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'copies descendants without sharing identity or registered listeners', - ({ document, globals }) => { - const Event = globals('Event'); - const source = document.createElement('div'); - const text = document.createTextNode('one'); - source.appendChild(text); - let called = 0; - source.addEventListener('probe', () => { - called++; - }); - const shallow = source.cloneNode(); - const deep = source.cloneNode(true); - expect(shallow.childNodes.length).toBe(0); - expect(deep.firstChild).not.toBe(text); - expect(deep.textContent).toBe('one'); - expect(deep.parentNode).toBeNull(); - expect(deep.ownerDocument).toBe(document); - deep.dispatchEvent(new Event('probe')); - expect(called).toBe(0); - source.dispatchEvent(new Event('probe')); - expect(called).toBe(1); - }, - ); - - context.test('shallow clones copy attributes independently without children', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'); - p.setAttribute('title', 'before'); - p.append('text'); - const copy = p.cloneNode() as Element; - expect(copy).not.toBe(p); - expect(copy.ownerDocument).toBe(document); - expect(copy.parentNode).toBeNull(); - expect(copy.childNodes.length).toBe(0); - expect(copy.getAttribute('title')).toBe('before'); - copy.setAttribute('title', 'after'); - expect(p.getAttribute('title')).toBe('before'); - }); - - context.test('does not copy registered event listeners', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'); - let count = 0; - p.addEventListener('x', () => count++); - const copy = p.cloneNode(true); - const Event = fixture.globals('Event'); - copy.dispatchEvent(new Event('x') as unknown as Event); - expect(count).toBe(0); - p.dispatchEvent(new Event('x') as unknown as Event); - expect(count).toBe(1); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/comparison.ts b/packages/web-platform-tests/src/compatibility/node/comparison.ts deleted file mode 100644 index 15de413..0000000 --- a/packages/web-platform-tests/src/compatibility/node/comparison.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://dom.spec.whatwg.org/#dom-node-comparedocumentposition'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('compares identity, ancestors, and ordered siblings', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('a'), - b = document.createElement('b'); - p.append(a, b); - expect(a.compareDocumentPosition(a)).toBe(0); - expect(a.compareDocumentPosition(b)).toBe(4); - expect(b.compareDocumentPosition(a)).toBe(2); - expect(p.compareDocumentPosition(a)).toBe(20); - expect(a.compareDocumentPosition(p)).toBe(10); - expect(p.contains(a)).toBe(true); - expect(a.contains(null)).toBe(false); - }); - context.test('distinguishes structural equality from identity', (fixture) => { - const document = fixture.document as unknown as Document; - const a = document.createElement('div'); - a.setAttribute('title', 'x'); - a.append('text'); - const b = a.cloneNode(true); - expect(a.isEqualNode(b)).toBe(true); - expect(a.isSameNode(b)).toBe(false); - (b as Element).setAttribute('title', 'y'); - expect(a.isEqualNode(b)).toBe(false); - }); - context.test( - 'reports disconnected nodes symmetrically with implementation-specific order', - (fixture) => { - const document = fixture.document as unknown as Document; - const a = document.createElement('i'), - b = document.createElement('i'); - const ab = a.compareDocumentPosition(b), - ba = b.compareDocumentPosition(a); - expect(ab & 1).toBe(1); - expect(ab & 32).toBe(32); - expect(Boolean(ab & 2)).toBe(Boolean(ba & 4)); - }, - ); -} diff --git a/packages/web-platform-tests/src/compatibility/node/connectivity.ts b/packages/web-platform-tests/src/compatibility/node/connectivity.ts deleted file mode 100644 index 5f1b9da..0000000 --- a/packages/web-platform-tests/src/compatibility/node/connectivity.ts +++ /dev/null @@ -1,35 +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/#dom-node-isconnected'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('updates connectivity on attachment and removal', ({ document, connect }) => { - const parent = document.createElement('div'); - const child = document.createTextNode('child'); - parent.appendChild(child); - expect(parent.isConnected).toBe(false); - expect(child.getRootNode()).toBe(parent); - connect(parent); - expect(child.isConnected).toBe(true); - expect(child.getRootNode()).toBe(document); - parent.remove(); - expect(child.isConnected).toBe(false); - expect(child.getRootNode()).toBe(parent); - }); - - context.test('updates connected state recursively on insertion and removal', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - child = document.createElement('i'); - p.append(child); - expect(child.isConnected).toBe(false); - fixture.connect(p as unknown as DOMElementContract); - expect(child.isConnected).toBe(true); - expect(child.getRootNode()).toBe(document); - p.remove(); - expect(child.isConnected).toBe(false); - expect(child.getRootNode()).toBe(p); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/fragments.ts b/packages/web-platform-tests/src/compatibility/node/fragments.ts deleted file mode 100644 index 8e37cad..0000000 --- a/packages/web-platform-tests/src/compatibility/node/fragments.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity'; -const requiredGlobals = ['Document'] as const; -export { requiredGlobals as globals }; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'transfers fragment children and validates document insertion before moving them', - ({ document, globals }) => { - const fragment = document.createDocumentFragment(); - const a = document.createElement('div'); - const b = document.createElement('span'); - fragment.append(a, b); - const target = document.createElement('section'); - expect(target.appendChild(fragment)).toBe(fragment); - expect(fragment.childNodes.length).toBe(0); - expect(target.childNodes[0]).toBe(a); - expect(target.childNodes[1]).toBe(b); - fragment.append(a, b); - const empty = new (globals('Document'))(); - expect(() => empty.appendChild(fragment)).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(fragment.childNodes[0]).toBe(a); - expect(fragment.childNodes[1]).toBe(b); - expect(empty.childNodes.length).toBe(0); - for (const leaf of [ - document.createTextNode('text'), - document.createComment('comment'), - document.createAttribute('title'), - ]) { - expect(() => leaf.appendChild(a)).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(a.parentNode).toBe(fragment); - } - }, - ); - - context.test('empties inserted fragments and preserves their child order', (fixture) => { - const document = fixture.document as unknown as Document; - const f = document.createDocumentFragment(), - p = document.createElement('div'), - a = document.createElement('i'), - b = document.createTextNode('b'); - f.append(a, b); - expect(p.appendChild(f)).toBe(f); - expect(f.childNodes.length).toBe(0); - expect(Array.from(p.childNodes)).toEqual([a, b]); - expect(a.parentNode).toBe(p); - }); - - context.test('rejects invalid document fragments before removing any child', (fixture) => { - const document = fixture.document as unknown as Document; - const other = document.implementation.createDocument(null, 'root'), - f = document.createDocumentFragment(); - f.append(document.createElement('a'), document.createElement('b')); - expect(() => other.replaceChild(f, other.documentElement)).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(f.childNodes.length).toBe(2); - expect(other.documentElement.localName).toBe('root'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/insert-before.ts b/packages/web-platform-tests/src/compatibility/node/insert-before.ts deleted file mode 100644 index 20f98d3..0000000 --- a/packages/web-platform-tests/src/compatibility/node/insert-before.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#dom-node-insertbefore'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'supports null anchors and reorders in both directions atomically', - ({ document }) => { - const parent = document.createElement('div'); - const a = document.createElement('a'); - const b = document.createElement('b'); - const c = document.createElement('i'); - parent.appendChild(a); - parent.appendChild(b); - parent.appendChild(c); - parent.insertBefore(c, a); - expect(parent.firstChild).toBe(c); - expect(c.nextSibling).toBe(a); - parent.insertBefore(c, null); - expect(parent.lastChild).toBe(c); - parent.insertBefore(a, a); - expect(parent.firstChild).toBe(a); - const outside = document.createElement('span'); - expect(() => outside.insertBefore(a, b)).toThrow( - expect.objectContaining({ name: 'NotFoundError' }), - ); - expect(a.parentNode).toBe(parent); - expect(outside.childNodes.length).toBe(0); - }, - ); - - context.test('treats self insertion as a no-op and null as append', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('i'), - b = document.createElement('b'); - p.appendChild(a); - expect(p.insertBefore(a, a)).toBe(a); - expect(Array.from(p.childNodes)).toEqual([a]); - p.insertBefore(b, null); - expect(Array.from(p.childNodes)).toEqual([a, b]); - }); - - context.test('rejects a foreign anchor without detaching the candidate', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - other = document.createElement('div'), - a = document.createElement('i'), - foreign = document.createElement('b'); - other.appendChild(a); - expect(() => p.insertBefore(a, foreign)).toThrow( - expect.objectContaining({ name: 'NotFoundError' }), - ); - expect(a.parentNode).toBe(other); - expect(p.childNodes.length).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/live-collections.ts b/packages/web-platform-tests/src/compatibility/node/live-collections.ts deleted file mode 100644 index abf0586..0000000 --- a/packages/web-platform-tests/src/compatibility/node/live-collections.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#old-style-collections'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('retains live childNodes and children views across mutations', ({ document }) => { - const parent = document.createElement('div'); - const nodes = parent.childNodes; - const elements = parent.children; - const child = document.createElement('span'); - const text = document.createTextNode('text'); - parent.append(text, child); - expect(nodes.length).toBe(2); - expect(nodes[0]).toBe(text); - expect(elements.length).toBe(1); - expect(elements[0]).toBe(child); - child.remove(); - expect(nodes.length).toBe(1); - expect(elements.length).toBe(0); - }); - - context.test( - 'keeps childNodes live and supplies standard item and iteration methods', - (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - nodes = p.childNodes, - a = document.createTextNode('a'); - expect(nodes.item(0)).toBeNull(); - p.append(a); - expect(nodes.item(0)).toBe(a); - expect([...nodes.values()]).toEqual([a]); - expect([...nodes.keys()]).toEqual([0]); - expect([...nodes.entries()]).toEqual([[0, a]]); - const seen: unknown[] = []; - nodes.forEach((n, i, list) => seen.push([n, i, list])); - expect(seen).toEqual([[a, 0, nodes]]); - a.remove(); - expect(nodes.length).toBe(0); - }, - ); - - context.test('updates HTMLCollection named lookups after name and id changes', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('input'); - const children = p.children; - p.append(e); - e.id = 'first'; - expect(children.namedItem('first')).toBe(e); - e.id = 'second'; - expect(children.namedItem('first')).toBeNull(); - expect(children.namedItem('second')).toBe(e); - e.remove(); - expect(children.item(0)).toBeNull(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/normalization.ts b/packages/web-platform-tests/src/compatibility/node/normalization.ts deleted file mode 100644 index 2e77cf6..0000000 --- a/packages/web-platform-tests/src/compatibility/node/normalization.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = 'https://dom.spec.whatwg.org/#dom-node-normalize'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('merges adjacent text and removes empties without crossing comments', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createTextNode('a'), - empty = document.createTextNode(''), - b = document.createTextNode('b'), - c = document.createComment('c'); - p.append(empty, a, b, c, 'd', 'e'); - p.normalize(); - expect(Array.from(p.childNodes).map((n) => n.nodeValue)).toEqual(['ab', 'c', 'de']); - expect(p.firstChild).toBe(a); - expect(empty.parentNode).toBeNull(); - expect(b.parentNode).toBeNull(); - p.normalize(); - expect(p.childNodes.length).toBe(3); - }); - context.test('normalizes descendants while retaining element boundaries', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - child = document.createElement('i'); - child.append('a', '', 'b'); - p.append('x', child, 'y'); - p.normalize(); - expect(child.childNodes.length).toBe(1); - expect(child.textContent).toBe('ab'); - expect(p.childNodes.length).toBe(3); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/relationships.ts b/packages/web-platform-tests/src/compatibility/node/relationships.ts deleted file mode 100644 index 43b5b09..0000000 --- a/packages/web-platform-tests/src/compatibility/node/relationships.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#interface-node'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('maintains parent child and sibling identity', ({ document }) => { - const parent = document.createElement('div'); - const a = document.createElement('span'); - const b = document.createTextNode('b'); - parent.appendChild(a); - parent.appendChild(b); - expect(a.parentNode).toBe(parent); - expect(b.parentElement).toBe(parent); - expect(parent.firstChild).toBe(a); - expect(parent.lastChild).toBe(b); - expect(a.nextSibling).toBe(b); - expect(b.previousSibling).toBe(a); - expect(a.previousSibling).toBeNull(); - expect(b.nextSibling).toBeNull(); - expect(parent.contains(b)).toBe(true); - expect(a.contains(b)).toBe(false); - expect(parent.contains(parent)).toBe(true); - expect(parent.hasChildNodes()).toBe(true); - }); - - context.test('tracks first, last and sibling links after reordering', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('i'), - b = document.createTextNode('b'), - c = document.createElement('u'); - p.append(a, b, c); - expect(p.firstChild).toBe(a); - expect(p.lastChild).toBe(c); - expect(b.previousSibling).toBe(a); - expect(b.nextSibling).toBe(c); - p.insertBefore(c, a); - expect(c.previousSibling).toBeNull(); - expect(a.previousSibling).toBe(c); - expect(b.nextSibling).toBeNull(); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/remove.ts b/packages/web-platform-tests/src/compatibility/node/remove.ts deleted file mode 100644 index f63dcb6..0000000 --- a/packages/web-platform-tests/src/compatibility/node/remove.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#dom-childnode-remove'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'distinguishes harmless ChildNode.remove from wrong-parent removeChild', - ({ document }) => { - const parent = document.createElement('div'); - const other = document.createElement('div'); - const child = document.createElement('span'); - parent.appendChild(child); - expect(() => other.removeChild(child)).toThrow( - expect.objectContaining({ name: 'NotFoundError' }), - ); - expect(child.parentNode).toBe(parent); - expect(parent.removeChild(child)).toBe(child); - expect(child.parentNode).toBeNull(); - child.remove(); - child.remove(); - expect(parent.childNodes.length).toBe(0); - }, - ); - - context.test('throws on removing an absent child and detaches sibling links', (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); - expect(p.removeChild(a)).toBe(a); - expect(a.parentNode).toBeNull(); - expect(a.previousSibling).toBeNull(); - expect(a.nextSibling).toBeNull(); - expect(b.previousSibling).toBeNull(); - expect(() => p.removeChild(a)).toThrow(expect.objectContaining({ name: 'NotFoundError' })); - expect(p.firstChild).toBe(b); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/replace.ts b/packages/web-platform-tests/src/compatibility/node/replace.ts deleted file mode 100644 index d173a9b..0000000 --- a/packages/web-platform-tests/src/compatibility/node/replace.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#concept-node-replace'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'returns the removed node and preserves invariants on invalid replacements', - ({ document }) => { - const parent = document.createElement('div'); - const a = document.createElement('span'); - const b = document.createElement('b'); - parent.appendChild(a); - parent.appendChild(b); - expect(parent.replaceChild(b, a)).toBe(a); - expect(parent.childNodes.length).toBe(1); - expect(parent.firstChild).toBe(b); - expect(a.parentNode).toBeNull(); - expect(parent.replaceChild(b, b)).toBe(b); - expect(() => parent.replaceChild(parent, b)).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(parent.firstChild).toBe(b); - }, - ); - - context.test('replacing a child with itself preserves identity and order', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('i'); - p.appendChild(a); - expect(p.replaceChild(a, a)).toBe(a); - expect(p.firstChild).toBe(a); - expect(a.parentNode).toBe(p); - }); - - context.test('replaces using an existing sibling without duplicate references', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('i'), - b = document.createElement('b'), - c = document.createElement('u'); - p.append(a, b, c); - expect(p.replaceChild(c, a)).toBe(a); - expect(Array.from(p.childNodes)).toEqual([c, b]); - expect(a.parentNode).toBeNull(); - }); - - context.test('rejects missing children and cycles before changing either tree', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - a = document.createElement('i'), - other = document.createElement('b'); - p.appendChild(a); - expect(() => p.replaceChild(other, other)).toThrow( - expect.objectContaining({ name: 'NotFoundError' }), - ); - expect(() => p.replaceChild(p, a)).toThrow( - expect.objectContaining({ name: 'HierarchyRequestError' }), - ); - expect(p.firstChild).toBe(a); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/text-content.ts b/packages/web-platform-tests/src/compatibility/node/text-content.ts deleted file mode 100644 index 2c3d1c8..0000000 --- a/packages/web-platform-tests/src/compatibility/node/text-content.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#dom-node-textcontent'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'excludes comments and replaces children while document textContent stays null', - ({ document }) => { - const parent = document.createElement('div'); - const text = document.createTextNode('one'); - const span = document.createElement('span'); - span.textContent = 'two'; - parent.append(text, document.createComment('ignored'), span); - expect(parent.textContent).toBe('onetwo'); - parent.textContent = 'replacement'; - expect(parent.childNodes.length).toBe(1); - expect(parent.firstChild?.nodeType).toBe(3); - expect(text.parentNode).toBeNull(); - parent.textContent = null; - expect(parent.childNodes.length).toBe(0); - expect(parent.textContent).toBe(''); - expect(document.textContent).toBeNull(); - }, - ); - - context.test('excludes comments and replaces descendants with one text node', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'); - p.append('a', document.createComment('ignored'), document.createElement('i')); - p.lastChild!.textContent = 'b'; - expect(p.textContent).toBe('ab'); - const old = p.firstChild!; - p.textContent = 'next'; - expect(p.childNodes.length).toBe(1); - expect(p.textContent).toBe('next'); - expect(old.parentNode).toBeNull(); - p.textContent = null; - expect(p.childNodes.length).toBe(0); - }); - - context.test('keeps Document textContent null with no setter mutation', (fixture) => { - const document = fixture.document as unknown as Document; - const before = Array.from(document.childNodes); - expect(document.textContent).toBeNull(); - Reflect.set(document, 'textContent', 'ignored'); - expect(document.textContent).toBeNull(); - expect(Array.from(document.childNodes)).toEqual(before); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/node/variadic-mutation.ts b/packages/web-platform-tests/src/compatibility/node/variadic-mutation.ts deleted file mode 100644 index 57cb334..0000000 --- a/packages/web-platform-tests/src/compatibility/node/variadic-mutation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://dom.spec.whatwg.org/#interface-parentnode'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test( - 'converts strings and preserves argument order when reusing siblings', - ({ document }) => { - const parent = document.createElement('div'); - const a = document.createElement('span'); - const b = document.createElement('b'); - const c = document.createElement('i'); - parent.append(a, b, c); - parent.prepend(a, c); - expect(parent.childNodes[0]).toBe(a); - expect(parent.childNodes[1]).toBe(c); - expect(parent.childNodes[2]).toBe(b); - c.before('before'); - c.after('after'); - expect(c.previousSibling?.nodeValue).toBe('before'); - expect(c.nextSibling?.nodeValue).toBe('after'); - c.replaceWith(c, 'tail'); - expect(c.parentNode).toBe(parent); - expect(c.nextSibling?.nodeValue).toBe('tail'); - parent.replaceChildren('one', a, 'two'); - expect(parent.textContent).toBe('onetwo'); - expect(parent.childNodes[1]).toBe(a); - }, - ); - - context.test('supports no-argument operations and string conversion', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'); - p.append(); - expect(p.childNodes.length).toBe(0); - p.append('a', 'b'); - expect(p.childNodes.length).toBe(2); - p.prepend('first'); - expect(p.textContent).toBe('firstab'); - p.replaceChildren(); - expect(p.childNodes.length).toBe(0); - }); - - context.test('handles self references in before and after operations', (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); - a.after(a, 'x'); - expect(Array.from(p.childNodes).map((n) => n.nodeName)).toEqual(['I', '#text', 'B']); - b.before(b); - expect(p.lastChild).toBe(b); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/parsing/contextual.ts b/packages/web-platform-tests/src/compatibility/parsing/contextual.ts deleted file mode 100644 index be20354..0000000 --- a/packages/web-platform-tests/src/compatibility/parsing/contextual.ts +++ /dev/null @@ -1,33 +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/#dom-range-createcontextualfragment'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('uses table context for contextual fragments', (fixture) => { - const document = fixture.document as unknown as Document; - const table = document.createElement('table'); - const range = document.createRange(); - range.selectNodeContents(table); - const fragment = range.createContextualFragment('x'); - expect((fragment.firstChild as Element).localName).toBe('tbody'); - expect(fragment.querySelector('td')!.textContent).toBe('x'); - expect(table.childNodes.length).toBe(0); - }); - context.test('parses raw text and entities according to context', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('textarea'); - e.innerHTML = '<b>&'; - expect(e.textContent).toBe('&'); - const script = document.createElement('script'); - script.innerHTML = 'if (a < b) x="&";'; - expect(script.textContent).toBe('if (a < b) x="&";'); - }); - context.test('keeps scripts inert during markup replacement', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - fixture.connect(e as unknown as DOMElementContract); - e.innerHTML = 'ok'; - expect(e.lastChild!.textContent).toBe('ok'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/parsing/fragments.ts b/packages/web-platform-tests/src/compatibility/parsing/fragments.ts deleted file mode 100644 index cf553e0..0000000 --- a/packages/web-platform-tests/src/compatibility/parsing/fragments.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = - 'https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('parses markup and references into real fragment children', ({ document }) => { - const container = document.createElement('div'); - container.innerHTML = 'one&two'; - expect(container.childNodes.length).toBe(2); - expect(container.firstElementChild?.localName).toBe('span'); - expect(container.firstElementChild?.getAttribute('title')).toBe('a&b'); - expect(container.firstElementChild?.textContent).toBe('one&two'); - expect(container.lastChild?.nodeType).toBe(8); - }); - - context.test('repairs table structure and decodes numeric character references', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('table'); - e.innerHTML = 'A😀'; - expect(e.firstElementChild!.localName).toBe('tbody'); - expect(e.querySelector('td')!.textContent).toBe('A😀'); - }); - - context.test('innerHTML replacement detaches previous descendants', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - e.innerHTML = 'a'; - const old = e.firstChild!; - e.innerHTML = 'b'; - expect(old.parentNode).toBeNull(); - expect(e.firstElementChild!.localName).toBe('b'); - e.innerHTML = ''; - expect(e.childNodes.length).toBe(0); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/parsing/markup-mutation.ts b/packages/web-platform-tests/src/compatibility/parsing/markup-mutation.ts deleted file mode 100644 index eb73ded..0000000 --- a/packages/web-platform-tests/src/compatibility/parsing/markup-mutation.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; -export const spec = - 'https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-element-insertadjacenthtml'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('inserts markup in all four adjacent positions', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('i'); - p.append(e); - e.insertAdjacentHTML('beforebegin', ''); - e.insertAdjacentHTML('afterbegin', ''); - e.insertAdjacentHTML('beforeend', ''); - e.insertAdjacentHTML('afterend', ''); - expect(Array.from(p.children).map((n) => n.localName)).toEqual(['a', 'i', 'u']); - expect(Array.from(e.children).map((n) => n.localName)).toEqual(['b', 'em']); - }); - context.test('replaces outerHTML without changing the removed node', (fixture) => { - const document = fixture.document as unknown as Document; - const p = document.createElement('div'), - e = document.createElement('i'); - e.textContent = 'old'; - p.append(e); - e.outerHTML = 'new'; - expect(e.parentNode).toBeNull(); - expect(e.textContent).toBe('old'); - expect(p.children.length).toBe(2); - expect(p.firstChild!.textContent).toBe('new'); - }); - context.test('rejects invalid insertion positions without mutating', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - e.innerHTML = ''; - expect(() => e.insertAdjacentHTML('invalid' as InsertPosition, 'x')).toThrow( - expect.objectContaining({ name: 'SyntaxError' }), - ); - expect(e.innerHTML).toBe(''); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/parsing/namespaces.ts b/packages/web-platform-tests/src/compatibility/parsing/namespaces.ts deleted file mode 100644 index 421f206..0000000 --- a/packages/web-platform-tests/src/compatibility/parsing/namespaces.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign'; -/** Registers behavioral cases with the consumer runner. */ -export function register(context: CompatibilityContext): void { - const { expect } = context.runner; - context.test('uses HTML SVG and MathML namespaces during fragment parsing', ({ document }) => { - const container = document.createElement('div'); - container.innerHTML = 'x'; - expect(container.children[0]?.namespaceURI).toBe('http://www.w3.org/2000/svg'); - expect(container.children[0]?.firstElementChild?.localName).toBe('linearGradient'); - expect(container.children[1]?.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML'); - }); - - context.test('switches into HTML at SVG foreignObject integration points', (fixture) => { - const document = fixture.document as unknown as Document; - const e = document.createElement('div'); - e.innerHTML = '
      '; - const svg = e.firstElementChild!, - foreign = svg.firstElementChild!, - div = foreign.firstElementChild!; - expect(svg.namespaceURI).toBe('http://www.w3.org/2000/svg'); - expect(foreign.localName).toBe('foreignObject'); - expect(div.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); - }); -} diff --git a/packages/web-platform-tests/src/compatibility/parsing/recovery.ts b/packages/web-platform-tests/src/compatibility/parsing/recovery.ts deleted file mode 100644 index 6b225be..0000000 --- a/packages/web-platform-tests/src/compatibility/parsing/recovery.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { CompatibilityContext } from '../context.ts'; - -export const spec = 'https://html.spec.whatwg.org/multipage/parsing.html#tree-construction'; -export const requires = ['parsing.fragments', 'document.parsing', 'templates.parsing'] as const; -const requiredGlobals = ['DOMParser'] as const; -export { requiredGlobals as globals }; -// Adapted from WPT html/syntax/parsing/resources/{adoption01,tables01}.dat. -// Copyright © web-platform-tests contributors (BSD-3-Clause). -// Copyright (c) 2006-2013 James Graham, Geoffrey Sneddon, and other contributors (MIT). -// See ../../../test/wpt/LICENSE, ../../../test/wpt/HTML5LIB-LICENSE.txt, -// and docs/wpt-ranges-parsing.md. -export const wpt = - 'https://github.com/web-platform-tests/wpt/blob/b719612f963a05ece9c89a7dd7598c1c59618874/html/syntax/parsing/resources/adoption01.dat'; - -const recoveryCases = [ - { - name: 'reconstructs formatting across misnested closing tags', - markup: '123', - expected: '123', - }, - { - name: 'splits formatting around a paragraph without losing reconstructed ancestry', - markup: '

      ', - expected: '

      ', - }, - { - name: 'preserves attributes when reconstructing multiple formatting elements', - markup: '

      123

      45', - expected: '

      123

      45', - }, - { - name: 'foster parents table text in order and inserts missing row containers', - markup: 'AC
      B
      ', - expected: 'AC
      B
      ', - }, - { - name: 'foster parents a select and retains its option contents', - markup: '
      ', - expected: '
      ', - }, - { - name: 'closes a button and its table cell when a new cell begins', - markup: '
      ', - 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
      B
      '); - 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 = 'outerend'; - const nested = template.content.querySelector('template')!; - expect(nested.innerHTML).toBe('123'); - 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('abcd'); - 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 = 'abcd'; - 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('abcd'); - }); - 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 = 'abcde'; - 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( - '
      light
      ', - ); - 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('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}>`, - 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 = - '
      '; - 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: '
      Other
      ', - 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'; - 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'); - }, - ); - - 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 = ''; - const nested = e.content.firstChild as HTMLTemplateElement; - expect(nested.childNodes.length).toBe(0); - expect(nested.content.firstChild!.textContent).toBe('x'); - expect(e.innerHTML).toBe(''); - }); -} 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 = 'ab'; - 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 + + + + +
      +
      +\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. +
        +
      1. If the user-agent does not claim to support the :focus-visible pseudo-class then SKIP this test.
      2. +
      3. Use the TAB key on the keyboard to focus the element below that says "Focus me."
      4. +
      5. 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.
      6. +
      +
      +
      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. +
        +
      1. If the user-agent does not claim to support the :focus-visible pseudo-class then SKIP this test.
      2. +
      3. Click each form element below to focus it.
      4. +
      5. 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.
      6. +
      +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +\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. +
        +
      1. If the user-agent does not claim to support the :focus-visible pseudo-class then SKIP this test.
      2. +
      3. Click each element element below to focus it.
      4. +
      5. 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.
      6. +
      +
      +
      + 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: + +
      + + + +`; + +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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \n
      \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
      +
      +
      Focus me
      +
      +
      +
      subject
      +
      +
      +
      Focus me
      +
      subject
      +
      + +`, + }, + }, + ); +} 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