import { createElement, useRef, useState, type ReactElement, type ReactNode, } from "react"; import { flushSync } from "react-dom"; import { createRoot, type Root } from "react-dom/client"; import { DESIGN_SYSTEM_CONTRACT_VERSION } from "../design-system/types.js"; import type { DesignSystemComponents } from "../design-system/types.js"; import type { DesignSystemConformanceCategory, DesignSystemConformanceCheckResult, DesignSystemConformanceReport, RunDesignSystemConformanceOptions, } from "./types.js"; import { assertDesignSystemContractVersion } from "./version.js"; interface MountedProbe { container: HTMLDivElement; root: Root; } interface CheckContext { components: DesignSystemComponents; document: Document; mount(element: ReactElement): MountedProbe; unmount(probe: MountedProbe): void; settle(): Promise; } interface ConformanceCheck { id: string; category: DesignSystemConformanceCategory; components: readonly (keyof DesignSystemComponents)[]; run(context: CheckContext): void | Promise; } const contractComponentNames = [ "ActionButton", "IconButton", "TextField", "TextArea", "Spinner", "Skeleton", "Status", "Surface", "Avatar", "Tooltip", "Menu", "Popover", "Dialog", "Picker", "Checkbox", "Switch", "Tabs", ] as const satisfies readonly (keyof DesignSystemComponents)[]; function invariant(value: unknown, message: string): asserts value { if (!value) throw new Error(message); } function dispatch(element: Element, event: Event): void { flushSync(() => element.dispatchEvent(event)); } function click(element: Element, document: Document): void { for (const type of ["pointerdown", "mousedown"]) { dispatch( element, new document.defaultView!.MouseEvent(type, { bubbles: true, cancelable: true, button: 0, }), ); } dispatch( element, new document.defaultView!.MouseEvent("click", { bubbles: true, cancelable: true, button: 0, }), ); } function keydown(element: Element, key: string, document: Document): void { dispatch( element, new document.defaultView!.KeyboardEvent("keydown", { key, bubbles: true, cancelable: true, }), ); } function input(element: HTMLInputElement | HTMLTextAreaElement, value: string) { const prototype = element instanceof element.ownerDocument.defaultView!.HTMLTextAreaElement ? element.ownerDocument.defaultView!.HTMLTextAreaElement.prototype : element.ownerDocument.defaultView!.HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(prototype, "value")?.set?.call( element, value, ); dispatch( element, new element.ownerDocument.defaultView!.Event("input", { bubbles: true, cancelable: true, }), ); } function elementWithText(document: Document, selector: string, text: string) { return [...document.querySelectorAll(selector)].find((element) => element.textContent?.includes(text), ); } function assertInlineLayout( container: Element, iconSelector: string, labelSelector: string, message: string, ) { const icon = container.querySelector(iconSelector); const label = container.querySelector(labelSelector); invariant(icon && label, message); const iconRect = icon.getBoundingClientRect(); const labelRect = label.getBoundingClientRect(); const hasGeometry = iconRect.width > 0 || iconRect.height > 0 || labelRect.width > 0 || labelRect.height > 0; if (hasGeometry) { const verticalOverlap = Math.min(iconRect.bottom, labelRect.bottom) - Math.max(iconRect.top, labelRect.top); invariant( verticalOverlap > 0 || Math.abs(iconRect.top - labelRect.top) <= 2, message, ); return; } // happy-dom does not calculate layout. Still reject adapters that explicitly // stack the tab contents, while browser-backed runs use the geometry check. const commonAncestor = (() => { let candidate: Element | null = icon; while (candidate && !candidate.contains(label)) { candidate = candidate.parentElement; } return candidate; })(); const styles = [ container, commonAncestor, icon.parentElement, label.parentElement, ] .filter((element): element is Element => Boolean(element)) .map((element) => element.ownerDocument.defaultView?.getComputedStyle(element), ); invariant( !styles.some( (style) => style?.display === "flex" && style.flexDirection === "column", ), message, ); } function assertContainedFooter(dialog: Element, footerSelector: string) { const footer = dialog.querySelector(footerSelector); invariant( footer, "Dialog footer controls must be rendered inside the dialog surface.", ); const dialogRect = (dialog as HTMLElement).getBoundingClientRect(); const footerRect = footer.getBoundingClientRect(); const hasGeometry = dialogRect.width > 0 || dialogRect.height > 0 || footerRect.width > 0 || footerRect.height > 0; if (!hasGeometry) return; const epsilon = 2; invariant( footerRect.top >= dialogRect.top - epsilon && footerRect.bottom <= dialogRect.bottom + epsilon && footerRect.left >= dialogRect.left - epsilon && footerRect.right <= dialogRect.right + epsilon, "Dialog footer controls must remain within the dialog surface bounds.", ); } const checks: readonly ConformanceCheck[] = [ { id: "contract.components", category: "contract", components: contractComponentNames, run: ({ components }) => { for (const name of contractComponentNames) { invariant( typeof components[name] === "function", `${name} must be supplied as a React component.`, ); } }, }, { id: "leaf.action-button", category: "leaf", components: ["ActionButton"], run: ({ components, document, mount, unmount }) => { let presses = 0; const probe = mount( presses++} > Remove , ); const button = probe.container.querySelector("button"); invariant(button, "ActionButton must render an operable button."); click(button, document); invariant(presses === 1, "ActionButton must call onPress once."); unmount(probe); }, }, { id: "leaf.icon-button", category: "leaf", components: ["IconButton"], run: ({ components, document, mount, unmount }) => { let presses = 0; const probe = mount(