import { BlurOption, BoundingRect, ClickOption, CssProperty, EnterTextOption, FocusOption, HoverOption, ITestEngineOption, Interactor, MouseDownOption, MouseEnterOption, MouseLeaveOption, MouseMoveOption, MouseOutOption, MouseUpOption, Optional, PartLocator, Point, PressKeyOption, ScenePart, TestEngine, WaitForOption, WaitUntilOption } from "@atomic-testing/core"; //#region src/types.d.ts /** * @deprecated Use {@link ITestEngineOption} from `@atomic-testing/core`. */ type IDomTestEngineOption = ITestEngineOption; /** * The subset of the `@testing-library/user-event` API that `DOMInteractor` * dispatches interactions through. Structural on purpose: the library's default * export and a configured instance from `userEvent.setup()` — such as Storybook's * instrumented `userEvent` from `storybook/test` — are both assignable without * coupling to either package's concrete types. */ interface UserEventDispatcher { clear(element: Element): Promise; click(element: Element): Promise; dblClick(element: Element): Promise; hover(element: Element): Promise; keyboard(text: string): Promise; selectOptions(element: Element, values: string[]): Promise; type(element: Element, text: string): Promise; upload(element: HTMLElement, files: File | File[]): Promise; } /** * Construction options for `DOMInteractor`. */ interface DOMInteractorOption { /** * The user-event API interactions are dispatched through. * @defaultValue the `@testing-library/user-event` default export */ readonly userEvent?: UserEventDispatcher; } //#endregion //#region src/DOMInteractor.d.ts /** * The jsdom-backed {@link Interactor} implementation — dispatches events and * reads the DOM via `@testing-library/dom`'s `fireEvent` and `user-event`. The * blessed base for every framework adapter that runs against jsdom (ADR-002, * ADR-007): `ReactInteractor` and `VueInteractor` extend it and layer their * reactivity flush onto the {@link runInteraction} seam rather than * reimplementing the primitives. `PlaywrightInteractor` does not extend this * class — its browser backing shares no implementation with jsdom. */ declare class DOMInteractor implements Interactor { protected readonly rootEl: HTMLElement; protected readonly userEvent: UserEventDispatcher; constructor(rootEl?: HTMLElement, option?: DOMInteractorOption); /** * Template-method seam every mutating primitive (and both wait conditions) * routes through. The base runs the interaction verbatim; framework adapters * override it to flush their reactivity around the whole interaction — * `ReactInteractor` wraps it in `act(...)`, `VueInteractor` awaits * `nextTick()`. Because every mutation funnels through this one method, a new * mutating primitive added to this base is flushed by every adapter * automatically — closing the silent gap that the previous per-method * overrides left open, where an un-mirrored primitive was inherited unwrapped * (#1052). * * Reads (`getText`, `getAttribute`, `exists`, …) deliberately do NOT route * through here: they observe state without mutating it, so there is nothing to * flush. */ protected runInteraction(fn: () => Promise): Promise; getAttribute(locator: PartLocator, name: string, isMultiple: true): Promise; getAttribute(locator: PartLocator, name: string, isMultiple: false): Promise>; getAttribute(locator: PartLocator, name: string): Promise>; getStyleValue(locator: PartLocator, propertyName: CssProperty): Promise>; protected calculateMousePosition(el: Element, preferredPoint?: Point): Point; /** * Dispatch a click event on the element that matches the locator. * * @param locator - Locator used to find the target element * @param option - Optional click configuration such as the click position * @returns A promise that resolves after the event is triggered * @throws {ElementNotFoundError} If the element is not found */ click(locator: PartLocator, option?: ClickOption): Promise; /** * Move the mouse over the element. * * @param locator - Locator used to find the target element * @param _option - Reserved for future use * @returns A promise that resolves after the hover event * @throws {ElementNotFoundError} If the element is not found */ hover(locator: PartLocator, _option?: HoverOption): Promise; /** * Dispatch a `mousemove` event on the target element. * * @param locator - Locator used to find the target element * @param option - Allows specifying the mouse position relative to the element * @returns A promise that resolves once the event has been dispatched * @throws {ElementNotFoundError} If the element is not found */ mouseMove(locator: PartLocator, option?: Partial): Promise; /** * Dispatch a `mousedown` event on the target element. * * @param locator - Locator used to find the target element * @param option - Allows specifying the mouse position relative to the element * @returns Promise resolved when the event is dispatched * @throws {ElementNotFoundError} If the element is not found */ mouseDown(locator: PartLocator, option?: Partial): Promise; /** * Dispatch a `mouseup` event on the target element. * * @param locator - Locator used to find the target element * @param option - Allows specifying the mouse position relative to the element * @returns Promise resolved when the event is dispatched * @throws {ElementNotFoundError} If the element is not found */ mouseUp(locator: PartLocator, option?: Partial): Promise; /** * Dispatch a `mouseover` event on the target element. * * @param locator - Locator used to find the target element * @param option - Optional mouse position relative to the element * @returns Promise resolved once the event is dispatched * @throws {ElementNotFoundError} If the element is not found */ mouseOver(locator: PartLocator, option?: Partial): Promise; /** * Dispatch a `mouseout` event on the target element. * * @param locator - Locator used to find the target element * @param _option - Reserved for future use * @returns Promise resolved once the event is dispatched * @throws {ElementNotFoundError} If the element is not found */ mouseOut(locator: PartLocator, _option?: Partial): Promise; /** * Dispatch a `mouseenter` event on the target element. * * @param locator - Locator used to find the target element * @param _option - Reserved for future use * @returns Promise resolved after the event dispatches * @throws {ElementNotFoundError} If the element is not found */ mouseEnter(locator: PartLocator, _option?: Partial): Promise; /** * Dispatch a `mouseleave` event on the target element. * * @param locator - Locator used to find the target element * @param _option - Reserved for future use * @returns Promise resolved once the event is dispatched * @throws {ElementNotFoundError} If the element is not found */ mouseLeave(locator: PartLocator, _option?: Partial): Promise; /** * Move focus to the element found by the locator. * * @param locator - Locator used to find the target element * @param _option - Reserved for future use * @returns Promise resolved when focus has been applied * @throws {ElementNotFoundError} If the element is not found */ focus(locator: PartLocator, _option?: Partial): Promise; /** * Remove focus from the element found by the locator. * * @param locator - Locator used to find the target element * @param _option - Reserved for future use * @returns Promise resolved when blur has been applied * @throws {ElementNotFoundError} If the element is not found */ blur(locator: PartLocator, _option?: Partial): Promise; /** * Legacy numeric key codes for the named keys drivers press. Synthetic * `KeyboardEvent`s carry `keyCode: 0` unless told otherwise, and several * component libraries (Angular Material/CDK among them) still dispatch on * `event.keyCode` rather than `event.key` — without this a synthetic * `Escape`/`Enter` is silently ignored. Real browser input (Playwright) * carries the code natively; this map restores parity for the DOM path. */ private static readonly legacyKeyCodes; private static legacyKeyCodeOf; /** * Dispatch a key press (`keydown` + `keyup`) on the element matched by the locator. * * The element is focused first so the key originates from the active element, * matching a real key press. `fireEvent` is used over `userEvent.keyboard` for * determinism and because MUI handlers read `event.key` directly. The physical * `code` is derived from `key` (see {@link deriveKeyCode}) so handlers that * switch on `event.code` — e.g. PrimeVue's Slider — behave as they do under a * real browser event, where `code` is always populated. * * `key` is dispatched verbatim — a `shift` modifier is NOT used to derive a * shifted character (`{ key: 'a', shift: true }` stays `key: 'a'`, never * folded to `'A'`), matching `PlaywrightInteractor`'s behavior (see * {@link KeyboardActions.pressKey} for the cross-engine verification, #924). * * @param locator - Locator used to find the target element * @param key - A `KeyboardEvent.key` value, e.g. `'Escape'`, `'Backspace'` * @param option - Modifier flags folded into the event init as * `ctrlKey`/`shiftKey`/`altKey`/`metaKey`, so a handler reading * `event.ctrlKey` (etc.) sees the chord — see {@link PressKeyOption} * @returns Promise resolved once the events have been dispatched * @throws {ElementNotFoundError} If the element is not found */ pressKey(locator: PartLocator, key: string, option?: Partial): Promise; /** * Dispatch a `contextmenu` (right-click) event on the element matched by the locator. * * The element is focused first if focusable, mirroring {@link pressKey}, so the * event originates from the active element as a real right-click would. A * context menu has no `aria-expanded`/controlled-open path, so this dispatched * event is the only way to exercise the menu-opening behavior. * * @param locator - Locator used to find the target element * @returns Promise resolved once the event has been dispatched * @throws {ElementNotFoundError} If the element is not found */ contextMenu(locator: PartLocator): Promise; /** * Activate the element matched by the locator without pointer geometry. * * Uses `userEvent.click`, which ignores layout and coordinates, so it reaches a * visually-hidden or covered input that a positional click would miss (e.g. MUI * Rating's hidden ``). * * @param locator - Locator used to find the target element * @returns Promise resolved once the element has been activated * @throws {ElementNotFoundError} If the element is not found */ activate(locator: PartLocator): Promise; /** * Type text into the element matched by the locator. * * @param locator - Locator used to find the target element * @param text - The string to type * @param option - Options such as appending or replacing existing value * @returns Promise resolved when typing has completed * @throws {ElementNotFoundError} If the element is not found */ enterText(locator: PartLocator, text: string, option?: Partial | undefined): Promise; /** * Type text into the element as real per-character keystrokes. * * Focuses the element, then dispatches the characters through * `userEvent.keyboard`, which fires the full key event sequence * (keydown → beforeinput → input → keyup) against the active element — * matching `PlaywrightInteractor`'s `pressSequentially` (focus + keys, no * pointer event, no clearing). `{`/`[` are doubled so user-event's * descriptor syntax never engages and the text is typed literally. * * @param locator - Locator used to find the target element * @param text - The literal text to type, one keystroke per character * @returns Promise resolved once every keystroke has been dispatched * @throws {ElementNotFoundError} If the element is not found */ typeText(locator: PartLocator, text: string): Promise; /** * Set the value of a range input and fire its change event. * * `fireEvent.change` assigns the value through the element's native value * setter (which both sanitizes it to the input's step and lets React's value * tracker observe the change) and dispatches the event so a controlled * component re-renders. Typing (`enterText`) does not apply to a range input. * * @param locator - Locator used to find the range input element * @param value - The numeric value to set * @returns Promise resolved once the change event has fired * @throws {ElementNotFoundError} If the element is not found */ setRangeValue(locator: PartLocator, value: number): Promise; /** * Select one or more option values in a `` element. * * The interactor contract passes filesystem paths, but a file input's value * cannot be assigned programmatically — the browser blocks it — so the * `FileList` must be populated through `userEvent.upload`, which also fires the * `change` event. jsdom has no filesystem and never reads file bytes; only * `File.name` is observable, so each path is wrapped in an empty `File` named * by its basename. The real bytes matter only to the Playwright layer, which * reads the paths natively — keeping `dom-core` free of any `node` dependency. * * @param locator - Locator used to find the file input element * @param files - One or more filesystem paths to upload * @returns Promise resolved once the upload change event has fired * @throws {ElementNotFoundError} If the element is not found */ setInputFiles(locator: PartLocator, files: string | string[]): Promise; /** * Scroll the located element into view. * * jsdom has no layout engine, so this never produces an observable scroll — * geometry stays zeroed and nothing becomes "visible". Worse, jsdom does not * implement `Element.prototype.scrollIntoView` as a function in every version, * so calling it unguarded would throw a `TypeError`. The `typeof` guard keeps * this a safe no-op that resolves; real scrolling behavior is E2E-only. * * @param locator - Locator used to find the element * @throws {ElementNotFoundError} If the element is not found */ scrollIntoView(locator: PartLocator): Promise; /** * Scroll the located element by the given pixel delta. * * jsdom has no layout engine, so the scroll offset never changes — this is a * no-op behaviorally. As with {@link scrollIntoView}, jsdom may not implement * `Element.prototype.scrollBy` as a function, so the `typeof` guard prevents a * `TypeError` and keeps the call a safe no-op that resolves; real scroll * behavior is E2E-only. * * @param locator - Locator used to find the scrollable element * @param delta - Pixel offset to scroll by * @throws {ElementNotFoundError} If the element is not found */ scrollBy(locator: PartLocator, delta: Point): Promise; /** * Dispatch a single bubbling mouse event at `point` using the shared * {@link FakeMouseEvent}. Centralizes the drag gesture's event shape so * {@link drag} and {@link dragTo} cannot drift apart. */ private dispatchMouse; /** * Fire the native HTML5 drag-and-drop event sequence — `dragstart` on * `sourceEl` → `dragenter`/`dragover`/`drop` on `targetEl` → `dragend` on * `sourceEl` — sharing one {@link FakeDataTransfer} across every event, so a * `dragstart` handler's `setData` is readable from `drop`'s `getData`. * `sourceEl === targetEl` is valid: {@link drag}'s single-element delta * gesture has no separate drop target, so it drags and drops onto itself. * * Fired unconditionally: unlike a real browser — which fires `drop` only * when the `dragover` listener calls `preventDefault()` to accept the drop — * jsdom has no native drag-recognition machinery to gate on, so this always * runs the full sequence. This is NOT identical to {@link * PlaywrightInteractor}, whose `drag`/`dragTo` drive a REAL pointer gesture * that the real browser's own native DnD recognition processes — including * the real `preventDefault()` gate — see #922. A target that opts in the * way any real HTML5-DnD target must (calling `preventDefault()` in its * `dragover`/`dragenter` handler, as the shared `.suite.ts` fixture does) * sees `drop` fire in both engines regardless of this difference; a target * that never opts in would still see `drop` here but not in a real browser * — an accepted simplification for a synthetic environment with no native * gesture recognition to simulate faithfully. * * jsdom implements neither `DragEvent` nor `DataTransfer` (only * `MouseEvent`), so `@testing-library/dom`'s `fireEvent.drag*` dispatch a * plain `Event` with `dataTransfer` attached as a property rather than a * real `DragEvent` — its own documented recipe for jsdom HTML5 DnD (see * https://github.com/jsdom/jsdom/issues/1568). Coordinates are not carried on * these events for the same reason {@link dispatchMouse}'s are E2E-only: * jsdom has no layout. */ private dispatchHtml5DragSequence; /** * Drag the source element and drop it onto the target element. * * The pointer sequence (`mousedown` on source → `mousemove` on target → * `mouseup` on target) is synthesized with the shared {@link dispatchMouse} + * {@link calculateMousePosition} pattern. jsdom has no layout, so those * coordinates are all zeros — the event wiring (and any drop handler the * sequence triggers) is exercised, but the positional outcome is E2E-only. * * The native HTML5 drag-and-drop sequence * (`dragstart`/`dragenter`/`dragover`/`drop`/`dragend` + `dataTransfer`) is * ALSO synthesized via {@link dispatchHtml5DragSequence}, so both DnD * models — pointer/mouse-based (dnd-kit, react-beautiful-dnd) and native * HTML5 (`draggable` + `ondragstart`/`ondragover`/`ondrop`) — are driven by * this one primitive (#922). * * @param source - Locator used to find the element to drag * @param target - Locator used to find the drop target * @throws {ElementNotFoundError} If either element is not found */ dragTo(source: PartLocator, target: PartLocator): Promise; /** * Drag the located element by the given pixel delta from its center. * * The sequence (`mousedown` at center → `mousemove` at center + delta → * `mouseup` at center + delta) is synthesized with the shared * {@link dispatchMouse} + {@link calculateMousePosition} pattern, using the * caller-supplied delta for the move/up coordinates. jsdom has no layout, so the * center resolves to zeros and only the event wiring is exercised — the * behavioral outcome of the drag is E2E-only. * * The native HTML5 drag-and-drop sequence is ALSO synthesized (on the same * element, as its own drop target — see {@link dispatchHtml5DragSequence}), * so a `draggable` element driven by this primitive sees `dragstart` and * `dragend` regardless of DnD model (#922). * * @param locator - Locator used to find the element to drag * @param delta - Pixel offset to drag by * @throws {ElementNotFoundError} If the element is not found */ drag(locator: PartLocator, delta: Point): Promise; waitUntilComponentState(locator: PartLocator, option?: Partial>): Promise; waitUntil(option: WaitUntilOption): Promise; exists(locator: PartLocator): Promise; /** * Count every element matching the locator — the length of the multi-match * query. Reuses the {@link getElement} multiple-overload rather than a second * `querySelectorAll` path, so the document-root (`:root`) escape is honored in * exactly one place. A read: it does NOT route through {@link runInteraction}. */ getElementCount(locator: PartLocator): Promise; getElement(locator: PartLocator, isMultiple: true): Promise; getElement(locator: PartLocator, isMultiple: false): Promise>; getElement(locator: PartLocator): Promise>; /** * Resolve a locator chain split at its {@link AccessibleRoleLocator} * segment (`findByRole`) — the second resolution channel, bypassing CSS * entirely (#923). Three cases for the scope the accname search runs * within, mirroring how an ordinary CSS chain resolves: * * - `roleLocator.relative === 'Root'` — escapes to the document root * (via {@link getElement}'s existing `[]` → `:root` handling), mirroring * how a trailing `'Root'` locator in an ordinary chain slices away * everything before it (see `escapesToDocumentRoot`). * - `before` is non-empty — resolves normally (recursing back into * {@link getElement}, so it fully supports nested `LinkedCssLocator`s * etc.) to a single scope element. * - `before` is empty and NOT a `'Root'` escape — scopes to `this.rootEl` * directly, NOT the document root. An unscoped `findByRole(...)` must * still respect the interactor's own scoping (e.g. a Storybook canvas), * exactly like a bare CSS locator does; routing this case through * `getElement([])` would incorrectly escape to the document regardless * of `rootEl`, since an empty chain always reduces to `:root`. * * Within that scope, `@testing-library/dom`'s `queryAllByRole` resolves by * the accname algorithm — the same engine `dom-core` already depends on for * this exact purpose (see the `findByRole` design in ADR 0001, Decision B). * `hidden: true` matches this codebase's other locators, which resolve * structurally regardless of visibility (`isVisible` is the dedicated * visibility check, not baked into resolution). */ private getElementByAccessibleRole; /** * A `'Root'`-relative locator (the portal escape — see the portals guide) is * documented to search from the document, not from this interactor's root, so * portalled content (dialogs, dropdowns rendered at ``) stays reachable * even when the interactor is scoped to a sub-tree such as a Storybook canvas. * Mirrors `locatorUtil.getEffectiveLocator`'s slicing rule: the last `'Root'` * locator wins unless it is `'linked'`, whose CSS still needs the scoped * context. */ private escapesToDocumentRoot; getInputValue(locator: PartLocator): Promise>; getSelectValues(locator: PartLocator): Promise>; getSelectLabels(locator: PartLocator): Promise>; getText(locator: PartLocator): Promise>; /** * Get the located element's bounding rectangle. * * jsdom has no layout engine, so `getBoundingClientRect` returns all zeros: the * rect is structurally valid but behaviorally meaningless. Real geometry is * E2E-only. * * @param locator - Locator used to find the element to measure * @returns The element's bounding rectangle (a zero-rect under jsdom) * @throws {ElementNotFoundError} If the element is not found */ getBoundingRect(locator: PartLocator): Promise; isChecked(locator: PartLocator): Promise; isDisabled(locator: PartLocator): Promise; isReadonly(locator: PartLocator): Promise; isRequired(locator: PartLocator): Promise; isError(locator: PartLocator): Promise; isVisible(locator: PartLocator): Promise; hasCssClass(locator: PartLocator, className: string): Promise; hasAttribute(locator: PartLocator, name: string): Promise; innerHTML(locator: PartLocator): Promise; } //#endregion //#region src/createTestEngine.d.ts /** * Create test engine for DOM testing * @param element The element to test, if not sure, use document.body * @param partDefinitions The scene part definitions * @returns The test engine */ declare function createTestEngine(element: HTMLElement, partDefinitions: T): TestEngine; /** * @deprecated Use {@link createTestEngine}. Kept as an alias for backward * compatibility; every adapter now exports `createTestEngine`. */ declare const createDomTestEngine: typeof createTestEngine; //#endregion //#region src/fakeEvents/FakeDataTransfer.d.ts /** * Minimal `DataTransfer` implementation for synthesizing HTML5 drag-and-drop * events under jsdom, which implements neither `DragEvent` nor `DataTransfer` * (only `MouseEvent` — see {@link FakeMouseEvent}). `@testing-library/dom`'s * `fireEvent.drag*` helpers special-case a `dataTransfer` init value and attach * it to the dispatched event verbatim when `window.DataTransfer` is absent — * this is that value, shared across one gesture's `dragstart` → `dragenter` → * `dragover` → `drop` → `dragend` sequence so a `dragstart` handler's * `setData` is readable from `drop`'s `getData`. * * `files`/`items` are not supported — the drag primitives synthesize element * drag-and-drop, not OS file drops, which {@link DOMInteractor.setInputFiles} * already covers. * * @see https://github.com/jsdom/jsdom/issues/1568 * @internal */ declare class FakeDataTransfer implements DataTransfer { private readonly store; dropEffect: DataTransfer['dropEffect']; effectAllowed: DataTransfer['effectAllowed']; readonly files: FileList; readonly items: DataTransferItemList; get types(): readonly string[]; clearData(format?: string): void; getData(format: string): string; setData(format: string, data: string): void; /** * No-op: jsdom has no layout/paint, so a custom drag image has nothing to * render. (Overrides the inherited `DataTransfer.setDragImage` doc comment, * which embeds raw ``/`` markup that breaks MDX rendering on * the generated API reference page.) */ setDragImage(_image: Element, _x: number, _y: number): void; } //#endregion //#region src/fakeEvents/FakeMouseEvent.d.ts /** * Fake mouse event used internally by `DOMInteractor` to synthesize positioned * mouse events. Exported for cross-package reuse within the monorepo, not part * of the stable 1.0 consumer API. * * `pageX`/`pageY` are getter-only accessors on the `MouseEvent` prototype in * real browsers (the Angular fixtures run DOM tests in Chromium, ADR-013), so * they are shadowed with own properties rather than assigned — plain * assignment throws where jsdom happened to tolerate it. * * @see https://github.com/testing-library/react-testing-library/issues/268 * @internal */ declare class FakeMouseEvent extends MouseEvent { constructor(type: string, overrides?: Partial); } //#endregion export { DOMInteractor, DOMInteractorOption, FakeDataTransfer, FakeMouseEvent, IDomTestEngineOption, UserEventDispatcher, createDomTestEngine, createTestEngine }; //# sourceMappingURL=index.d.mts.map