diff --git a/.changeset/beige-colts-chew.md b/.changeset/beige-colts-chew.md new file mode 100644 index 0000000000..8de1f668e8 --- /dev/null +++ b/.changeset/beige-colts-chew.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-style": minor +--- + +**Breaking:** Function `getBoundingBox` was removed. + +Use `Element#getBoundingBox` instead. diff --git a/.changeset/brave-dolphins-smash.md b/.changeset/brave-dolphins-smash.md new file mode 100644 index 0000000000..416c718d8b --- /dev/null +++ b/.changeset/brave-dolphins-smash.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-json": minor +--- + +**Added:** `Serializable` interface now optionally accepts serialization options. + +When implementing the interface a type parameter can be supplied and `toJSON` can optionally take an object of that type that can be used for changing the serialization behavior. diff --git a/.changeset/chilled-laws-hammer.md b/.changeset/chilled-laws-hammer.md new file mode 100644 index 0000000000..e32ca9697c --- /dev/null +++ b/.changeset/chilled-laws-hammer.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-dom": minor +--- + +**Breaking:** `Element#of` now requires the device used when scraping a page in order to store a box. + +This ensures that the boxes of the elements will be stored with and only be accessible for the same device instance. If no device is provided, no box is stored with the element. diff --git a/.changeset/metal-planets-hammer.md b/.changeset/metal-planets-hammer.md new file mode 100644 index 0000000000..3537b03a06 --- /dev/null +++ b/.changeset/metal-planets-hammer.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-tree": minor +--- + +**Added:** Optional serialization options type parameter added to abstract `Node` class. + +This is passed to the `Serializable` interface to allow implementers to supply the serialization options type. diff --git a/.changeset/slow-moles-thank.md b/.changeset/slow-moles-thank.md new file mode 100644 index 0000000000..bff50c5b13 --- /dev/null +++ b/.changeset/slow-moles-thank.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-dom": minor +--- + +**Added:** `Document#toJSON` now optionally accepts serialization options containing device. + +The options will be passed down to all children of the document and used by `Element` to serialize the box corresponding to the device. diff --git a/docs/review/api/alfa-dom.api.md b/docs/review/api/alfa-dom.api.md index 2aa0a647a3..559b252e43 100644 --- a/docs/review/api/alfa-dom.api.md +++ b/docs/review/api/alfa-dom.api.md @@ -5,6 +5,7 @@ ```ts import { Array as Array_2 } from '@siteimprove/alfa-array'; +import { Device } from '@siteimprove/alfa-device'; import * as earl from '@siteimprove/alfa-earl'; import { Equatable } from '@siteimprove/alfa-equatable'; import { Flags } from '@siteimprove/alfa-flags'; @@ -227,7 +228,7 @@ export class Document extends Node<"document"> { // (undocumented) get style(): Iterable; // (undocumented) - toJSON(): Document.JSON; + toJSON(options?: Node.SerializationOptions): Document.JSON; // (undocumented) toString(): string; } @@ -235,7 +236,7 @@ export class Document extends Node<"document"> { // @public (undocumented) export namespace Document { // @internal (undocumented) - export function fromDocument(json: JSON): Trampoline; + export function fromDocument(json: JSON, device?: Device): Trampoline; // (undocumented) export function isDocument(value: unknown): value is Document; // (undocumented) @@ -262,14 +263,14 @@ export class Element extends Node<"element"> implemen // (undocumented) get attributes(): Sequence; // (undocumented) - get box(): Option; - // (undocumented) children(options?: Node.Traversal): Sequence; // (undocumented) get classes(): Sequence; // (undocumented) get content(): Option; // (undocumented) + getBoundingBox(device: Device): Option; + // (undocumented) get id(): Option; // @internal (undocumented) protected _internalPath(options?: Node.Traversal): string; @@ -280,7 +281,7 @@ export class Element extends Node<"element"> implemen // (undocumented) get namespace(): Option; // (undocumented) - static of(namespace: Option, prefix: Option, name: N, attributes?: Iterable_2, children?: Iterable_2, style?: Option, box?: Option): Element; + static of(namespace: Option, prefix: Option, name: N, attributes?: Iterable_2, children?: Iterable_2, style?: Option, box?: Option, device?: Option): Element; // (undocumented) parent(options?: Node.Traversal): Option; // (undocumented) @@ -294,7 +295,7 @@ export class Element extends Node<"element"> implemen // (undocumented) tabIndex(): Option; // (undocumented) - toJSON(): Element.JSON; + toJSON(options?: Node.SerializationOptions): Element.JSON; // (undocumented) toString(): string; } @@ -302,7 +303,7 @@ export class Element extends Node<"element"> implemen // @public (undocumented) export namespace Element { // @internal (undocumented) - export function fromElement(json: JSON): Trampoline>; + export function fromElement(json: JSON, device?: Device): Trampoline>; // (undocumented) export function isElement(value: unknown): value is Element; // (undocumented) @@ -398,7 +399,7 @@ export class Fragment extends Node<"fragment"> { // @public (undocumented) export namespace Fragment { // @internal (undocumented) - export function fromFragment(json: JSON): Trampoline; + export function fromFragment(json: JSON, device?: Device): Trampoline; // (undocumented) export function isFragment(value: unknown): value is Fragment; // (undocumented) @@ -431,7 +432,7 @@ export namespace GroupingRule { } // @public (undocumented) -export function h(name: N, attributes?: Array | Record, children?: Array, style?: Array | Record, box?: Rectangle): Element; +export function h(name: N, attributes?: Array | Record, children?: Array, style?: Array | Record, box?: Rectangle, device?: Device): Element; // @public (undocumented) export namespace h { @@ -444,7 +445,7 @@ export namespace h { // (undocumented) export function document(children: Array, style?: Array): Document; // (undocumented) - export function element(name: N, attributes?: Array | Record, children?: Array, style?: Array | Record, namespace?: Namespace, box?: Rectangle): Element; + export function element(name: N, attributes?: Array | Record, children?: Array, style?: Array | Record, namespace?: Namespace, box?: Rectangle, device?: Device): Element; // (undocumented) export function fragment(children: Array): Fragment; // (undocumented) @@ -675,7 +676,7 @@ export namespace NamespaceRule { } // @public (undocumented) -export abstract class Node extends tree.Node implements earl.Serializable, json.Serializable>, sarif.Serializable { +export abstract class Node extends tree.Node implements earl.Serializable, json.Serializable, Node.SerializationOptions>, sarif.Serializable { protected constructor(children: Array, type: T); // (undocumented) equals(value: Node): boolean; @@ -774,32 +775,37 @@ export namespace Node { }; } // (undocumented) - export function from(json: Element.JSON): Element; + export function from(json: Element.JSON, device?: Device): Element; // (undocumented) - export function from(json: Attribute.JSON): Attribute; + export function from(json: Attribute.JSON, device?: Device): Attribute; // (undocumented) - export function from(json: Text.JSON): Text; + export function from(json: Text.JSON, device?: Device): Text; // (undocumented) - export function from(json: Comment.JSON): Comment; + export function from(json: Comment.JSON, device?: Device): Comment; + // (undocumented) + export function from(json: Document.JSON, device?: Device): Document; const flatTree: Traversal; const fullTree: Traversal; const composedNested: Traversal; // (undocumented) - export function from(json: Document.JSON): Document; - // (undocumented) - export function from(json: Type.JSON): Document; + export function from(json: Type.JSON, device?: Device): Document; // (undocumented) - export function from(json: Fragment.JSON): Fragment; + export function from(json: Fragment.JSON, device?: Device): Fragment; // (undocumented) - export function from(json: JSON): Node; + export function from(json: JSON, device?: Device): Node; // @internal (undocumented) - export function fromNode(json: JSON): Trampoline; + export function fromNode(json: JSON, device?: Device): Trampoline; // (undocumented) export function isNode(value: unknown): value is Node; // (undocumented) export interface JSON extends tree.Node.JSON { } // (undocumented) + export interface SerializationOptions { + // (undocumented) + device: Device; + } + // (undocumented) export class Traversal extends Flags { // (undocumented) static of(...flags: Array): Traversal; @@ -964,7 +970,7 @@ export class Shadow extends Node<"shadow"> { // @public (undocumented) export namespace Shadow { // @internal (undocumented) - export function fromShadow(json: JSON): Trampoline; + export function fromShadow(json: JSON, device?: Device): Trampoline; // (undocumented) export function isShadow(value: unknown): value is Shadow; // (undocumented) diff --git a/docs/review/api/alfa-json.api.md b/docs/review/api/alfa-json.api.md index 88372c959e..b50f77f972 100644 --- a/docs/review/api/alfa-json.api.md +++ b/docs/review/api/alfa-json.api.md @@ -24,9 +24,9 @@ namespace JSON_2 { export { JSON_2 as JSON } // @public (undocumented) -export interface Serializable { +export interface Serializable { // (undocumented) - toJSON(): T; + toJSON(options?: OPTIONS): T; } // @public (undocumented) @@ -36,9 +36,9 @@ export namespace Serializable { // (undocumented) export type ToJSON = T extends Serializable ? U : T extends JSON_2 ? T : JSON_2; // (undocumented) - export function toJSON(value: Serializable): T; + export function toJSON(value: Serializable, options?: OPTIONS): T; // (undocumented) - export function toJSON(value: T): ToJSON; + export function toJSON(value: T, options?: OPTIONS): ToJSON; } // (No @packageDocumentation comment for this package) diff --git a/docs/review/api/alfa-style.api.md b/docs/review/api/alfa-style.api.md index 1d4bfca929..c4a4da30b9 100644 --- a/docs/review/api/alfa-style.api.md +++ b/docs/review/api/alfa-style.api.md @@ -35,7 +35,6 @@ import { Percentage } from '@siteimprove/alfa-css'; import { Position } from '@siteimprove/alfa-css'; import { Predicate } from '@siteimprove/alfa-predicate'; import { Rectangle } from '@siteimprove/alfa-css'; -import { Rectangle as Rectangle_2 } from '@siteimprove/alfa-rectangle'; import { Rotate } from '@siteimprove/alfa-css'; import { Serializable } from '@siteimprove/alfa-json'; import { Shadow } from '@siteimprove/alfa-css'; @@ -373,7 +372,6 @@ export namespace Style { const // Warning: (ae-forgotten-export) The symbol "element" needs to be exported by the entry point index.d.ts // // (undocumented) - getBoundingBox: typeof element.getBoundingBox, // (undocumented) getOffsetParent: typeof element.getOffsetParent, // (undocumented) getPositioningParent: typeof element.getPositioningParent, // (undocumented) hasBorder: typeof element.hasBorder, // (undocumented) diff --git a/docs/review/api/alfa-tree.api.md b/docs/review/api/alfa-tree.api.md index 8fc15bc7fb..de1327782c 100644 --- a/docs/review/api/alfa-tree.api.md +++ b/docs/review/api/alfa-tree.api.md @@ -15,7 +15,7 @@ import { Refinement } from '@siteimprove/alfa-refinement'; import { Sequence } from '@siteimprove/alfa-sequence'; // @public -export abstract class Node implements Iterable>, Equatable, Hashable, json.Serializable> { +export abstract class Node implements Iterable>, Equatable, Hashable, json.Serializable, S> { // (undocumented) [Symbol.iterator](): Iterator>; protected constructor(children: Array>, type: T); @@ -90,7 +90,7 @@ export abstract class Node // (undocumented) siblings(options?: Flags): Sequence>; // (undocumented) - toJSON(): Node.JSON; + toJSON(options?: S): Node.JSON; // (undocumented) get type(): T; // (undocumented) diff --git a/packages/alfa-dom/package.json b/packages/alfa-dom/package.json index 8c62cf77b1..666c63a4d5 100644 --- a/packages/alfa-dom/package.json +++ b/packages/alfa-dom/package.json @@ -40,6 +40,7 @@ "@siteimprove/alfa-array": "workspace:^0.67.0", "@siteimprove/alfa-cache": "workspace:^0.67.0", "@siteimprove/alfa-css": "workspace:^0.67.0", + "@siteimprove/alfa-device": "workspace:^0.67.0", "@siteimprove/alfa-earl": "workspace:^0.67.0", "@siteimprove/alfa-equatable": "workspace:^0.67.0", "@siteimprove/alfa-flags": "workspace:^0.67.0", diff --git a/packages/alfa-dom/src/h.ts b/packages/alfa-dom/src/h.ts index ce0213b8f7..7842bb7e8f 100644 --- a/packages/alfa-dom/src/h.ts +++ b/packages/alfa-dom/src/h.ts @@ -1,6 +1,7 @@ import { None, Option } from "@siteimprove/alfa-option"; import { Predicate } from "@siteimprove/alfa-predicate"; +import { Device } from "@siteimprove/alfa-device"; import { Rectangle } from "@siteimprove/alfa-rectangle"; import { Attribute, @@ -37,9 +38,10 @@ export function h( attributes?: Array | Record, children?: Array, style?: Array | Record, - box?: Rectangle + box?: Rectangle, + device: Device = Device.standard() ): Element { - return h.element(name, attributes, children, style, undefined, box); + return h.element(name, attributes, children, style, undefined, box, device); } /** @@ -52,7 +54,8 @@ export namespace h { children: Array = [], style: Array | Record = [], namespace?: Namespace, - box?: Rectangle + box?: Rectangle, + device?: Device ): Element { attributes = Array.isArray(attributes) ? attributes @@ -95,7 +98,8 @@ export namespace h { .filter(nor(Document.isDocument, Shadow.isShadow)) .map((child) => (typeof child === "string" ? h.text(child) : child)), style.length === 0 ? None : Option.of(block), - Option.from(box) + Option.from(box), + Option.from(device) ); if (content !== undefined) { diff --git a/packages/alfa-dom/src/jsx.ts b/packages/alfa-dom/src/jsx.ts index 73cb49aad3..d38d5c71d0 100644 --- a/packages/alfa-dom/src/jsx.ts +++ b/packages/alfa-dom/src/jsx.ts @@ -2,6 +2,7 @@ import { h } from "./h"; import { Element, Node } from "."; +import { Device } from "@siteimprove/alfa-device"; import { Rectangle } from "@siteimprove/alfa-rectangle"; import * as dom from "."; @@ -18,6 +19,7 @@ export function jsx( const attributes: Record = {}; const style: Record = {}; let box: Rectangle | undefined = undefined; + let device: Device | undefined = undefined; for (const [name, value] of entries(properties ?? {})) { if (value === null || value === undefined) { @@ -33,7 +35,11 @@ export function jsx( continue; case "box": - box = Rectangle.from(value as Rectangle.JSON); + const deviceAndBox = value as { device: Device } & Rectangle.JSON; + + box = Rectangle.from(deviceAndBox); + device = deviceAndBox.device; + continue; default: @@ -46,7 +52,8 @@ export function jsx( attributes, (children as Array).flat(Infinity), style, - box + box, + device ); } diff --git a/packages/alfa-dom/src/node.ts b/packages/alfa-dom/src/node.ts index 9935932e4c..69d8e36743 100644 --- a/packages/alfa-dom/src/node.ts +++ b/packages/alfa-dom/src/node.ts @@ -23,6 +23,7 @@ import { Type, } from "."; +import { Device } from "@siteimprove/alfa-device"; import * as predicate from "./node/predicate"; import * as traversal from "./node/traversal"; @@ -33,7 +34,7 @@ export abstract class Node extends tree.Node implements earl.Serializable, - json.Serializable>, + json.Serializable, Node.SerializationOptions>, sarif.Serializable { protected constructor(children: Array, type: T) { @@ -243,6 +244,10 @@ export interface Node { export namespace Node { export interface JSON extends tree.Node.JSON {} + export interface SerializationOptions { + device: Device + } + export interface EARL extends earl.EARL { "@context": { ptr: "http://www.w3.org/2009/pointers#"; @@ -319,33 +324,36 @@ export namespace Node { Traversal.nested ); - export function from(json: Element.JSON): Element; + export function from(json: Element.JSON, device?: Device): Element; - export function from(json: Attribute.JSON): Attribute; + export function from(json: Attribute.JSON, device?: Device): Attribute; - export function from(json: Text.JSON): Text; + export function from(json: Text.JSON, device?: Device): Text; - export function from(json: Comment.JSON): Comment; + export function from(json: Comment.JSON, device?: Device): Comment; - export function from(json: Document.JSON): Document; + export function from(json: Document.JSON, device?: Device): Document; - export function from(json: Type.JSON): Document; + export function from(json: Type.JSON, device?: Device): Document; - export function from(json: Fragment.JSON): Fragment; + export function from(json: Fragment.JSON, device?: Device): Fragment; - export function from(json: JSON): Node; + export function from(json: JSON, device?: Device): Node; - export function from(json: JSON): Node { - return fromNode(json).run(); + export function from(json: JSON, device?: Device): Node { + return fromNode(json, device).run(); } /** * @internal */ - export function fromNode(json: JSON): Trampoline { + export function fromNode( + json: JSON, + device?: Device + ): Trampoline { switch (json.type) { case "element": - return Element.fromElement(json as Element.JSON); + return Element.fromElement(json as Element.JSON, device); case "attribute": return Attribute.fromAttribute(json as Attribute.JSON); @@ -357,13 +365,13 @@ export namespace Node { return Comment.fromComment(json as Comment.JSON); case "document": - return Document.fromDocument(json as Document.JSON); + return Document.fromDocument(json as Document.JSON, device); case "type": return Type.fromType(json as Type.JSON); case "fragment": - return Fragment.fromFragment(json as Fragment.JSON); + return Fragment.fromFragment(json as Fragment.JSON, device); default: throw new Error(`Unexpected node of type: ${json.type}`); diff --git a/packages/alfa-dom/src/node/document.ts b/packages/alfa-dom/src/node/document.ts index c5f246997a..8b3ab66dee 100644 --- a/packages/alfa-dom/src/node/document.ts +++ b/packages/alfa-dom/src/node/document.ts @@ -1,3 +1,4 @@ +import { Device } from "@siteimprove/alfa-device"; import { None, Option } from "@siteimprove/alfa-option"; import { Trampoline } from "@siteimprove/alfa-trampoline"; import { Node } from "../node"; @@ -57,9 +58,9 @@ export class Document extends Node<"document"> { return "/"; } - public toJSON(): Document.JSON { + public toJSON(options?: Node.SerializationOptions): Document.JSON { return { - ...super.toJSON(), + ...super.toJSON(options), style: this._style.map((sheet) => sheet.toJSON()), }; } @@ -109,10 +110,13 @@ export namespace Document { /** * @internal */ - export function fromDocument(json: JSON): Trampoline { - return Trampoline.traverse(json.children ?? [], Node.fromNode).map( - (children) => Document.of(children, json.style.map(Sheet.from)) - ); + export function fromDocument( + json: JSON, + device?: Device + ): Trampoline { + return Trampoline.traverse(json.children ?? [], (child) => + Node.fromNode(child, device) + ).map((children) => Document.of(children, json.style.map(Sheet.from))); } } diff --git a/packages/alfa-dom/src/node/element.ts b/packages/alfa-dom/src/node/element.ts index ebd1849dbe..6b8209faad 100644 --- a/packages/alfa-dom/src/node/element.ts +++ b/packages/alfa-dom/src/node/element.ts @@ -1,3 +1,5 @@ +import { Cache } from "@siteimprove/alfa-cache"; +import { Device } from "@siteimprove/alfa-device"; import { Iterable } from "@siteimprove/alfa-iterable"; import { None, Option, Some } from "@siteimprove/alfa-option"; import { Predicate } from "@siteimprove/alfa-predicate"; @@ -34,7 +36,8 @@ export class Element attributes: Iterable = [], children: Iterable = [], style: Option = None, - box: Option = None + box: Option = None, + device: Option = None ): Element { return new Element( namespace, @@ -43,7 +46,8 @@ export class Element Array.from(attributes), Array.from(children), style, - box + box, + device ); } @@ -52,11 +56,11 @@ export class Element private readonly _name: N; private readonly _attributes: Map; private readonly _style: Option; - private readonly _box: Option; private _shadow: Option = None; private _content: Option = None; private readonly _id: Option; private readonly _classes: Array; + private readonly _boxes: Cache; private constructor( namespace: Option, @@ -65,7 +69,8 @@ export class Element attributes: Array, children: Array, style: Option, - box: Option + box: Option, + device: Option ) { super(children, "element"); @@ -89,7 +94,9 @@ export class Element .map((attr) => attr.value.trim().split(/\s+/)) .getOr([]); - this._box = box; + this._boxes = Cache.from( + device.isSome() && box.isSome() ? [[device.get(), box.get()]] : [] + ); } public get namespace(): Option { @@ -141,8 +148,8 @@ export class Element return Sequence.from(this._classes); } - public get box(): Option { - return this._box; + public getBoundingBox(device: Device): Option { + return this._boxes.get(device); } public parent(options: Node.Traversal = Node.Traversal.empty): Option { @@ -300,7 +307,7 @@ export class Element return path; } - public toJSON(): Element.JSON { + public toJSON(options?: Node.SerializationOptions): Element.JSON { return { ...super.toJSON(), namespace: this._namespace.getOr(null), @@ -312,7 +319,13 @@ export class Element style: this._style.map((style) => style.toJSON()).getOr(null), shadow: this._shadow.map((shadow) => shadow.toJSON()).getOr(null), content: this._content.map((content) => content.toJSON()).getOr(null), - box: this._box.map((box) => box.toJSON()).getOr(null), + box: + options?.device === undefined + ? null + : this._boxes + .get(options.device) + .map((box) => box.toJSON()) + .getOr(null), }; } @@ -393,35 +406,39 @@ export namespace Element { * @internal */ export function fromElement( - json: JSON + json: JSON, + device?: Device ): Trampoline> { - return Trampoline.traverse(json.children ?? [], Node.fromNode).map( - (children) => { - const element = Element.of( - Option.from(json.namespace as Namespace | null), - Option.from(json.prefix), - json.name, - json.attributes.map((attribute) => - Attribute.fromAttribute(attribute).run() - ), - children, - json.style?.length === 0 - ? None - : Option.from(json.style).map(Block.from), - Option.from(json.box).map(Rectangle.from) - ); - - if (json.shadow !== null) { - element._attachShadow(Shadow.fromShadow(json.shadow).run()); - } - - if (json.content !== null) { - element._attachContent(Document.fromDocument(json.content).run()); - } + return Trampoline.traverse(json.children ?? [], (child) => + Node.fromNode(child, device) + ).map((children) => { + const element = Element.of( + Option.from(json.namespace as Namespace | null), + Option.from(json.prefix), + json.name, + json.attributes.map((attribute) => + Attribute.fromAttribute(attribute).run() + ), + children, + json.style?.length === 0 + ? None + : Option.from(json.style).map(Block.from), + Option.from(json.box).map(Rectangle.from), + Option.from(device) + ); + + if (json.shadow !== null) { + element._attachShadow(Shadow.fromShadow(json.shadow, device).run()); + } - return element; + if (json.content !== null) { + element._attachContent( + Document.fromDocument(json.content, device).run() + ); } - ); + + return element; + }); } export const { diff --git a/packages/alfa-dom/src/node/element/predicate/has-box.ts b/packages/alfa-dom/src/node/element/predicate/has-box.ts index 6fdd43d431..aa92425982 100644 --- a/packages/alfa-dom/src/node/element/predicate/has-box.ts +++ b/packages/alfa-dom/src/node/element/predicate/has-box.ts @@ -1,13 +1,14 @@ +import { Device } from "@siteimprove/alfa-device"; import { Predicate } from "@siteimprove/alfa-predicate"; import { Rectangle } from "@siteimprove/alfa-rectangle"; - import { Element } from "../../element"; /** * @public */ export function hasBox( - predicate: Predicate = () => true + predicate: Predicate = () => true, + device: Device ): Predicate { - return (element) => element.box.some(predicate); + return (element) => element.getBoundingBox(device).some(predicate); } diff --git a/packages/alfa-dom/src/node/fragment.ts b/packages/alfa-dom/src/node/fragment.ts index 9c9fec8c87..d8a613a96f 100644 --- a/packages/alfa-dom/src/node/fragment.ts +++ b/packages/alfa-dom/src/node/fragment.ts @@ -1,3 +1,4 @@ +import { Device } from "@siteimprove/alfa-device"; import { Trampoline } from "@siteimprove/alfa-trampoline"; import { Node } from "../node"; @@ -54,10 +55,13 @@ export namespace Fragment { /** * @internal */ - export function fromFragment(json: JSON): Trampoline { - return Trampoline.traverse(json.children ?? [], Node.fromNode).map( - (children) => Fragment.of(children) - ); + export function fromFragment( + json: JSON, + device?: Device + ): Trampoline { + return Trampoline.traverse(json.children ?? [], (child) => + Node.fromNode(child, device) + ).map((children) => Fragment.of(children)); } } diff --git a/packages/alfa-dom/src/node/shadow.ts b/packages/alfa-dom/src/node/shadow.ts index 3511a7c01b..90d3fe5c0e 100644 --- a/packages/alfa-dom/src/node/shadow.ts +++ b/packages/alfa-dom/src/node/shadow.ts @@ -1,6 +1,7 @@ import { None, Option } from "@siteimprove/alfa-option"; import { Trampoline } from "@siteimprove/alfa-trampoline"; +import { Device } from "@siteimprove/alfa-device"; import { Node } from "../node"; import { Sheet } from "../style/sheet"; import { Element } from "./element"; @@ -138,10 +139,14 @@ export namespace Shadow { /** * @internal */ - export function fromShadow(json: JSON): Trampoline { - return Trampoline.traverse(json.children ?? [], Node.fromNode).map( - (children) => - Shadow.of(children, json.style.map(Sheet.from), json.mode as Mode) + export function fromShadow( + json: JSON, + device?: Device + ): Trampoline { + return Trampoline.traverse(json.children ?? [], (child) => + Node.fromNode(child, device) + ).map((children) => + Shadow.of(children, json.style.map(Sheet.from), json.mode as Mode) ); } } diff --git a/packages/alfa-dom/test/jsx.spec.tsx b/packages/alfa-dom/test/jsx.spec.tsx index 6c9fe1aff2..c10b1bba3f 100644 --- a/packages/alfa-dom/test/jsx.spec.tsx +++ b/packages/alfa-dom/test/jsx.spec.tsx @@ -1,9 +1,13 @@ +import { Device } from "@siteimprove/alfa-device"; import { test } from "@siteimprove/alfa-test"; test("Box attribute gets turned into box property", (t) => { - const element =
; + const device = Device.standard(); + const element = ( +
+ ); - t.deepEqual(element.box.getUnsafe().toJSON(), { + t.deepEqual(element.getBoundingBox(device).getUnsafe().toJSON(), { type: "rectangle", x: 12, y: 7, @@ -12,8 +16,16 @@ test("Box attribute gets turned into box property", (t) => { }); }); +test("Element with box attribute without device has box property equal to `None`", (t) => { + const device = Device.standard(); + const element =
; + + t.deepEqual(element.getBoundingBox(device).isNone(), true); +}); + test("Element without box attribute has box property equal to `None`", (t) => { + const device = Device.standard(); const element =
; - t.deepEqual(element.box.isNone(), true); + t.deepEqual(element.getBoundingBox(device).isNone(), true); }); diff --git a/packages/alfa-dom/tsconfig.json b/packages/alfa-dom/tsconfig.json index 9763659e73..5f8e598a25 100644 --- a/packages/alfa-dom/tsconfig.json +++ b/packages/alfa-dom/tsconfig.json @@ -92,6 +92,7 @@ { "path": "../alfa-array" }, { "path": "../alfa-cache" }, { "path": "../alfa-css" }, + { "path": "../alfa-device" }, { "path": "../alfa-earl" }, { "path": "../alfa-equatable" }, { "path": "../alfa-flags" }, diff --git a/packages/alfa-json/src/serializable.ts b/packages/alfa-json/src/serializable.ts index e28ef3ba26..ed38540c88 100644 --- a/packages/alfa-json/src/serializable.ts +++ b/packages/alfa-json/src/serializable.ts @@ -10,8 +10,8 @@ const { isFunction, isObject, isString, isNumber, isBoolean, isNull } = /** * @public */ -export interface Serializable { - toJSON(): T; +export interface Serializable { + toJSON(options?: OPTIONS): T; } /** @@ -30,13 +30,13 @@ export namespace Serializable { return isObject(value) && isFunction(value.toJSON); } - export function toJSON(value: Serializable): T; + export function toJSON(value: Serializable, options?: OPTIONS): T; - export function toJSON(value: T): ToJSON; + export function toJSON(value: T, options?: OPTIONS): ToJSON; - export function toJSON(value: unknown): JSON { + export function toJSON(value: unknown, options?: unknown): JSON { if (isSerializable(value)) { - return value.toJSON(); + return value.toJSON(options); } if ( @@ -57,7 +57,7 @@ export namespace Serializable { for (const key of keys(value)) { if (value[key] !== undefined) { - json[key] = toJSON(value[key]); + json[key] = toJSON(value[key], options); } } diff --git a/packages/alfa-rules/src/common/expectation/contrast.ts b/packages/alfa-rules/src/common/expectation/contrast.ts index 90124d5033..491d939604 100644 --- a/packages/alfa-rules/src/common/expectation/contrast.ts +++ b/packages/alfa-rules/src/common/expectation/contrast.ts @@ -5,7 +5,6 @@ import { Element, Node, Text } from "@siteimprove/alfa-dom"; import { Iterable } from "@siteimprove/alfa-iterable"; import { None, Option } from "@siteimprove/alfa-option"; import { Set } from "@siteimprove/alfa-set"; -import { Style } from "@siteimprove/alfa-style"; import { expectation } from "../act/expectation"; import { Group } from "../act/group"; @@ -18,7 +17,6 @@ import { isLargeText } from "../predicate"; const { isElement } = Element; const { flatMap, map, takeWhile } = Iterable; const { min, max, round } = Math; -const { getBoundingBox } = Style; /** * @deprecated This is only used in the deprecated R66v1 and R69v1. @@ -247,12 +245,12 @@ function getIntersectors( return Option.of(candidates); } - const elementBox = getBoundingBox(element, device); + const elementBox = element.getBoundingBox(device); if ( !elementBox.isSome() || Iterable.some(candidates, (candidate) => - getBoundingBox(candidate, device).isNone() + candidate.getBoundingBox(device).isNone() ) ) { return None; @@ -264,7 +262,7 @@ function getIntersectors( (canditate) => elementBox .get() - .intersects(getBoundingBox(canditate, device).getUnsafe()) // Presence of the box is guaranteed by the above check + .intersects(canditate.getBoundingBox(device).getUnsafe()) // Presence of the box is guaranteed by the above check ) ); } diff --git a/packages/alfa-rules/src/sia-r83/rule.ts b/packages/alfa-rules/src/sia-r83/rule.ts index 487420b34f..76b78bf100 100644 --- a/packages/alfa-rules/src/sia-r83/rule.ts +++ b/packages/alfa-rules/src/sia-r83/rule.ts @@ -128,28 +128,35 @@ export default Rule.Atomic.of({ // this is not likely and just ignore it. This would only create // false negatives. horizontallyClippedBy.every( - hasBox((clippingBox) => - target - .parent() - .filter(isElement) - .some( - hasBox( - (targetBox) => clippingBox.width >= 2 * targetBox.width - ) - ) + hasBox( + (clippingBox) => + target + .parent() + .filter(isElement) + .some( + hasBox( + (targetBox) => + clippingBox.width >= 2 * targetBox.width, + device + ) + ), + device ) ) && verticallyClippedBy.every( - hasBox((clippingBox) => - target - .parent() - .filter(isElement) - .some( - hasBox( - (targetBox) => - clippingBox.height >= 2 * targetBox.height - ) - ) + hasBox( + (clippingBox) => + target + .parent() + .filter(isElement) + .some( + hasBox( + (targetBox) => + clippingBox.height >= 2 * targetBox.height, + device + ) + ), + device ) ) ? Outcomes.IsContainer( diff --git a/packages/alfa-rules/test/sia-r69/rule.spec.tsx b/packages/alfa-rules/test/sia-r69/rule.spec.tsx index c1f98ffcbe..86988ffe5b 100644 --- a/packages/alfa-rules/test/sia-r69/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r69/rule.spec.tsx @@ -859,9 +859,10 @@ test("evaluate() can tell when encountering an opaque background before an absol test("evaluate() can tell when interposed descendant overlaps offset parent, but does not overlap target", async (t) => { const target = h.text("Hello World"); + const device = Device.standard(); const document = h.document([ - +
-
{target}
+
{target}



@@ -884,7 +885,7 @@ test("evaluate() can tell when interposed descendant overlaps offset parent, but , ]); - t.deepEqual(await evaluate(R69, { document }), [ + t.deepEqual(await evaluate(R69, { document, device }), [ passed(R69, target, { 1: Outcomes.HasSufficientContrast(21, 4.5, [ Diagnostic.Pairing.of( diff --git a/packages/alfa-rules/test/sia-r83/rule.spec.tsx b/packages/alfa-rules/test/sia-r83/rule.spec.tsx index cd66d98f64..310335ed51 100644 --- a/packages/alfa-rules/test/sia-r83/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r83/rule.spec.tsx @@ -4,8 +4,9 @@ import { test } from "@siteimprove/alfa-test"; import R83, { Outcomes } from "../../src/sia-r83/rule"; +import { Device } from "@siteimprove/alfa-device"; import { evaluate } from "../common/evaluate"; -import { passed, failed, inapplicable } from "../common/outcome"; +import { failed, inapplicable, passed } from "../common/outcome"; test("evaluate() passes a text node that truncates overflow using ellipsis", async (t) => { const target = h.text("Hello world"); @@ -462,9 +463,13 @@ test(`evaluate() passes a text node with fixed height and another property test(`evaluates() passes a text node horizontally overflowing its small parent and not clipped by its wide grand-parent`, async (t) => { const target = h.text("Hello world"); + const device = Device.standard(); + const clipping = ( -
- {target} +
+ + {target} +
); @@ -480,7 +485,7 @@ test(`evaluates() passes a text node horizontally overflowing its small ]), ] ); - t.deepEqual(await evaluate(R83, { document }), [ + t.deepEqual(await evaluate(R83, { document, device }), [ passed(R83, target, { 1: Outcomes.IsContainer(Option.of(clipping), None), }), @@ -490,9 +495,13 @@ test(`evaluates() passes a text node horizontally overflowing its small test(`evaluate() passes a text node vertically overflowing its small parent and not clipped by its high grand-parent`, async (t) => { const target = h.text("Hello world"); + const device = Device.standard(); + const clipping = ( -
- {target} +
+ + {target} +
); @@ -508,7 +517,7 @@ test(`evaluate() passes a text node vertically overflowing its small ] ); - t.deepEqual(await evaluate(R83, { document }), [ + t.deepEqual(await evaluate(R83, { document, device }), [ passed(R83, target, { 1: Outcomes.IsContainer(None, Option.of(clipping)), }), diff --git a/packages/alfa-style/src/element/element.ts b/packages/alfa-style/src/element/element.ts index dc46d252b8..06b891a0fe 100755 --- a/packages/alfa-style/src/element/element.ts +++ b/packages/alfa-style/src/element/element.ts @@ -1,4 +1,3 @@ -export * from "./helpers/get-bounding-box"; export * from "./helpers/get-offset-parent"; export * from "./helpers/get-positioning-parent"; export * from "./predicate/has-border"; diff --git a/packages/alfa-style/src/element/helpers/get-bounding-box.ts b/packages/alfa-style/src/element/helpers/get-bounding-box.ts deleted file mode 100644 index 1f02d091c1..0000000000 --- a/packages/alfa-style/src/element/helpers/get-bounding-box.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Device } from "@siteimprove/alfa-device"; -import { Element } from "@siteimprove/alfa-dom"; -import { None, Option } from "@siteimprove/alfa-option"; -import { Rectangle } from "@siteimprove/alfa-rectangle"; -import { Context } from "@siteimprove/alfa-selector"; - -/** - * @public - * Gets the bounding box, corresponding to a specific device, of an element - * - * @privateRemarks - * We don't use the passed in device yet, but later we should use it to ensure the device used to collect the bounding box corresponds to the current device - */ -export function getBoundingBox( - element: Element, - device: Device, - context: Context = Context.empty() -): Option { - // We assume layout is only grabbed on empty contexts, so if the context is non-empty we don't have layout - if (!context.isEmpty()) { - return None; - } - - return element.box; -} diff --git a/packages/alfa-style/src/node/predicate/is-clipped.ts b/packages/alfa-style/src/node/predicate/is-clipped.ts index 6da7f9b0bf..8ac14acbce 100644 --- a/packages/alfa-style/src/node/predicate/is-clipped.ts +++ b/packages/alfa-style/src/node/predicate/is-clipped.ts @@ -122,7 +122,7 @@ function isClippedBySize( if ( context === Context.empty() && - element.box.some((box) => box[axis] === 0) + element.getBoundingBox(device).some((box) => box[axis] === 0) ) { // The element's box is squished in this axis. return true; @@ -142,7 +142,7 @@ function isClippedBySize( if ( context === Context.empty() && - element.box.some((box) => box[axis] <= 1) + element.getBoundingBox(device).some((box) => box[axis] <= 1) ) { return true; } @@ -242,8 +242,10 @@ function isClippedByMasking( */ function isClippedByMovingAway(device: Device): Predicate { return (element) => { - return hasBox((elementBox) => - hasPositioningParent(device, isClipping(elementBox, device))(element) + return hasBox( + (elementBox) => + hasPositioningParent(device, isClipping(elementBox, device))(element), + device )(element); }; } @@ -268,26 +270,26 @@ function isClipping(elementBox: Rectangle, device: Device): Predicate { // case where the ancestor itself is clipped away is handled by the global // recurrence in isClipped. // This doesn't handle corner cases of 1×1px intersections. - not(hasBox(elementBox.intersects.bind(elementBox))), + not(hasBox(elementBox.intersects.bind(elementBox), device)), or( // The element is to the left, and clipped away. and( - hasBox((ancestorBox) => elementBox.right < ancestorBox.left), + hasBox((ancestorBox) => elementBox.right < ancestorBox.left, device), hasComputedStyle("overflow-x", isNotVisible, device) ), // The element is to the right and cannot be scrolled to. and( - hasBox((ancestorBox) => elementBox.left > ancestorBox.right), + hasBox((ancestorBox) => elementBox.left > ancestorBox.right, device), hasComputedStyle("overflow-x", isNoScroll, device) ), // The element is above, and clipped away. and( - hasBox((ancestorBox) => elementBox.bottom < ancestorBox.top), + hasBox((ancestorBox) => elementBox.bottom < ancestorBox.top, device), hasComputedStyle("overflow-y", isNotVisible, device) ), // The element is below and cannot be scrolled to. and( - hasBox((ancestorBox) => elementBox.top > ancestorBox.bottom), + hasBox((ancestorBox) => elementBox.top > ancestorBox.bottom, device), hasComputedStyle("overflow-y", isNoScroll, device) ), // The ancestor doesn't clip, let's search for the next one. diff --git a/packages/alfa-style/src/node/predicate/is-offscreen.ts b/packages/alfa-style/src/node/predicate/is-offscreen.ts index 2cd9f481b2..19700bebff 100644 --- a/packages/alfa-style/src/node/predicate/is-offscreen.ts +++ b/packages/alfa-style/src/node/predicate/is-offscreen.ts @@ -29,7 +29,10 @@ export function isOffscreen( .get(context, Cache.empty) .get(node, () => { if (isElement(node)) { - if (node.box.isSome() && context === Context.empty()) { + if ( + node.getBoundingBox(device).isSome() && + context === Context.empty() + ) { return !isOnscreenLayout(node, device); } else { const style = Style.from(node, device, context); @@ -72,12 +75,12 @@ function isOnscreenLayout(element: Element, device: Device): boolean { // rectangle extending the viewport to the bottom. // If yes, then the element can be brought into viewport by vertical scrolling // which, we assume, is not restricted. - hasBox((box) => box.intersects(extendedViewport)), + hasBox((box) => box.intersects(extendedViewport), device), // Next, we search whether the element is in the scrollable quadrant (extends // to the bottom and right), and has a positioning ancestor that creates // the needed horizontal scrolling. and( - hasBox((box) => box.intersects(scrollableQuadrant)), + hasBox((box) => box.intersects(scrollableQuadrant), device), hasPositioningParent(device, isOnscreenAndScrolling(device)) ) )(element); @@ -100,7 +103,7 @@ function isOnscreenAndScrolling(device: Device): Predicate { or( and( // Does the element intersect the extended viewport? - hasBox((box) => box.intersects(extendedViewport)), + hasBox((box) => box.intersects(extendedViewport), device), // And does it create a scrollbar which can show the content to the right? hasComputedStyle( "overflow-x", diff --git a/packages/alfa-style/src/style.ts b/packages/alfa-style/src/style.ts index 253b5aea01..72e026c475 100644 --- a/packages/alfa-style/src/style.ts +++ b/packages/alfa-style/src/style.ts @@ -336,7 +336,6 @@ export namespace Style { export type Inherited = Longhands.Inherited; export const { - getBoundingBox, getOffsetParent, getPositioningParent, hasBorder, diff --git a/packages/alfa-style/test/node/predicate/is-clipped.spec.tsx b/packages/alfa-style/test/node/predicate/is-clipped.spec.tsx index d759e515a5..561731316f 100644 --- a/packages/alfa-style/test/node/predicate/is-clipped.spec.tsx +++ b/packages/alfa-style/test/node/predicate/is-clipped.spec.tsx @@ -4,7 +4,8 @@ import { test } from "@siteimprove/alfa-test"; import * as predicate from "../../../src/node/predicate/is-clipped"; -const isClipped = predicate.isClipped(Device.standard()); +const device = Device.standard(); +const isClipped = predicate.isClipped(device); function target( style: { [prop: string]: string }, @@ -20,7 +21,7 @@ function boxed( ): Element { return ( // The actual position of the element doesn't matter. -
+
{child ?? "Hello World"}
); diff --git a/packages/alfa-style/test/node/predicate/is-offscreen.spec.tsx b/packages/alfa-style/test/node/predicate/is-offscreen.spec.tsx index 3ef4dc3adc..7412147a40 100644 --- a/packages/alfa-style/test/node/predicate/is-offscreen.spec.tsx +++ b/packages/alfa-style/test/node/predicate/is-offscreen.spec.tsx @@ -3,7 +3,8 @@ import { test } from "@siteimprove/alfa-test"; import * as predicate from "../../../src/node/predicate/is-offscreen"; -const isOffscreen = predicate.isOffscreen(Device.standard()); +const device = Device.standard(); +const isOffscreen = predicate.isOffscreen(device); /************************************************************************* * @@ -14,7 +15,9 @@ const isOffscreen = predicate.isOffscreen(Device.standard()); test(`isOffscreen() uses boxes when they are provided`, (t) => { // The element is actually on screen, but the box is lying to us. const element = ( -
Hello world
+
+ Hello world +
); t.deepEqual(isOffscreen(element), true); @@ -22,7 +25,9 @@ test(`isOffscreen() uses boxes when they are provided`, (t) => { test(`isOffscreen() returns true for elements that are on the left of the viewport`, (t) => { const element = ( -
Hello world
+
+ Hello world +
); t.deepEqual(isOffscreen(element), true); @@ -30,7 +35,7 @@ test(`isOffscreen() returns true for elements that are on the left of the viewpo test(`isOffscreen() returns true for elements that are on top of the viewport`, (t) => { const element = ( -
Hello world
+
Hello world
); t.deepEqual(isOffscreen(element), true); @@ -38,7 +43,7 @@ test(`isOffscreen() returns true for elements that are on top of the viewport`, test(`isOffscreen() returns false for elements that are fully on screen`, (t) => { const element = ( -
Hello world
+
Hello world
); t.deepEqual(isOffscreen(element), false); @@ -46,7 +51,7 @@ test(`isOffscreen() returns false for elements that are fully on screen`, (t) => test(`isOffscreen() returns false for elements that partially intersect the viewport on the left`, (t) => { const element = ( -
Hello world
+
Hello world
); t.deepEqual(isOffscreen(element), false); @@ -54,7 +59,7 @@ test(`isOffscreen() returns false for elements that partially intersect the view test(`isOffscreen() returns false for elements that partially intersect the viewport on the top`, (t) => { const element = ( -
Hello world
+
Hello world
); t.deepEqual(isOffscreen(element), false); @@ -62,7 +67,7 @@ test(`isOffscreen() returns false for elements that partially intersect the view test(`isOffscreen() returns false for elements that far away to the bottom of the page`, (t) => { const element = ( -
Hello world
+
Hello world
); t.deepEqual(isOffscreen(element), false); @@ -70,12 +75,12 @@ test(`isOffscreen() returns false for elements that far away to the bottom of th test(`isOffscreen() returns false for elements that can be brought on screen by horizontal scrolling`, (t) => { const element = ( -
Hello world
+
Hello world
); const _ = (
{element} @@ -87,12 +92,12 @@ test(`isOffscreen() returns false for elements that can be brought on screen by test(`isOffscreen() returns true for elements that cannot be brought on screen by horizontal scrolling`, (t) => { const element = ( -
Hello world
+
Hello world
); const _ = (
{element} diff --git a/packages/alfa-style/tsconfig.json b/packages/alfa-style/tsconfig.json index da36c3cf67..12eab0ae93 100644 --- a/packages/alfa-style/tsconfig.json +++ b/packages/alfa-style/tsconfig.json @@ -7,7 +7,6 @@ }, "files": [ "src/element/element.ts", - "src/element/helpers/get-bounding-box.ts", "src/element/helpers/get-offset-parent.ts", "src/element/helpers/get-positioning-parent.ts", "src/element/predicate/has-border.ts", diff --git a/packages/alfa-tree/src/tree.ts b/packages/alfa-tree/src/tree.ts index 0caa381cf3..efa68148c0 100755 --- a/packages/alfa-tree/src/tree.ts +++ b/packages/alfa-tree/src/tree.ts @@ -32,12 +32,14 @@ export abstract class Node< // The list of flags allowed to control tree traversal. F extends Flags.allFlags, // The type - T extends string = string + T extends string = string, + // The options for serialization + S extends unknown = unknown > implements Iterable>, Equatable, Hashable, - json.Serializable> + json.Serializable, S> { protected readonly _children: Array>; protected _parent: Option> = None; @@ -374,10 +376,10 @@ export abstract class Node< hash.writeObject(this); } - public toJSON(): Node.JSON { + public toJSON(options?: S): Node.JSON { return { type: this._type, - children: this._children.map((child) => child.toJSON()), + children: this._children.map((child) => child.toJSON(options)), }; } diff --git a/packages/alfa-web/src/page.ts b/packages/alfa-web/src/page.ts index e39a03eef6..0ae73b2f25 100644 --- a/packages/alfa-web/src/page.ts +++ b/packages/alfa-web/src/page.ts @@ -68,7 +68,7 @@ export class Page return { request: this._request.toJSON(), response: this._response.toJSON(), - document: this._document.toJSON(), + document: this._document.toJSON({ device: this._device }), device: this._device.toJSON(), }; } @@ -122,14 +122,10 @@ export namespace Page { } export function from(json: JSON): Result { + const device = Device.from(json.device); return Request.from(json.request).andThen((request) => Response.from(json.response).map((response) => - Page.of( - request, - response, - Document.from(json.document), - Device.from(json.device) - ) + Page.of(request, response, Document.from(json.document, device), device) ) ); } diff --git a/packages/alfa-web/test/page.spec.ts b/packages/alfa-web/test/page.spec.tsx similarity index 61% rename from packages/alfa-web/test/page.spec.ts rename to packages/alfa-web/test/page.spec.tsx index dfb5e224f0..72e3b08ed3 100644 --- a/packages/alfa-web/test/page.spec.ts +++ b/packages/alfa-web/test/page.spec.tsx @@ -1,7 +1,7 @@ import { test } from "@siteimprove/alfa-test"; import { Device } from "@siteimprove/alfa-device"; -import { Document } from "@siteimprove/alfa-dom"; +import { Document, h } from "@siteimprove/alfa-dom"; import { Request, Response } from "@siteimprove/alfa-http"; import { Page } from "../src/page"; @@ -40,3 +40,29 @@ test(".from() returns Err on invalid URL in response", (t) => { true ); }); + +test(".toJSON() serializes correct box", (t) => { + const device = Device.standard(); + + const document = h.document([ +
+ Hello world +
, + ]); + + t.deepEqual( + Page.of(Request.empty(), Response.empty(), document, device) + .toJSON() + .document.children?.filter((x) => x.type === "element") + .map((x) => x.box), + [ + { + type: "rectangle", + height: 18, + width: 1070, + x: 8, + y: 8, + }, + ] + ); +}); diff --git a/packages/alfa-web/tsconfig.json b/packages/alfa-web/tsconfig.json index 66103bfad6..c5fc56415c 100644 --- a/packages/alfa-web/tsconfig.json +++ b/packages/alfa-web/tsconfig.json @@ -1,6 +1,10 @@ { "$schema": "http://json.schemastore.org/tsconfig", "extends": "../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "@siteimprove/alfa-dom" + }, "files": [ "native.ts", "src/index.ts", @@ -8,7 +12,7 @@ "src/page.ts", "src/resource.ts", "src/site.ts", - "test/page.spec.ts" + "test/page.spec.tsx" ], "references": [ { "path": "../alfa-device" }, diff --git a/yarn.lock b/yarn.lock index c98ea6a621..6af4309fdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1010,6 +1010,7 @@ __metadata: "@siteimprove/alfa-array": "workspace:^0.67.0" "@siteimprove/alfa-cache": "workspace:^0.67.0" "@siteimprove/alfa-css": "workspace:^0.67.0" + "@siteimprove/alfa-device": "workspace:^0.67.0" "@siteimprove/alfa-earl": "workspace:^0.67.0" "@siteimprove/alfa-equatable": "workspace:^0.67.0" "@siteimprove/alfa-flags": "workspace:^0.67.0"