/* eslint-disable @typescript-eslint/no-misused-promises */ import {type EnterTextOptions, type UniDriver} from '@wix/unidriver-core'; import {type DriverContext, eventually, unidriverConfig} from '@wix/unidriver-core/internal'; import {Simulate} from 'react-dom/test-utils'; import {act} from './actCompat'; import {contextToWaitError} from './contextToWaitError'; import {jsdomReactUniDriverList} from './jsdomReactUniDriverList'; import {enterValue} from './utils/enterValue'; import {waitForLegacy} from './waitForLegacy'; import { isMultipleElementsWithSelectorError, MultipleElementsWithSelectorError, NoElementWithSelectorError, isNoElementWithSelectorError, } from './utils/errors'; import {type KeyboardKey, keyMeta} from './keyMeta'; import {type JSX} from 'react'; import {getSelectorsPath} from './utils/getSelectorPath'; export const jsdomReactUniDriver = ( adapterParams: (() => Element) | Element | (() => Promise), ctx: DriverContext = {selector: 'Root React driver'}, ): UniDriver => { const getBaseElement = async () => { const container = typeof adapterParams === 'function' ? adapterParams() : adapterParams; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!container) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unnecessary-condition throw new NoElementWithSelectorError({selector: ((ctx as any).selector as string) ?? ''}); } return container as HTMLElement; }; const exists = async () => { try { await getBaseElement(); return true; } catch (e) { if (isMultipleElementsWithSelectorError(e)) { throw e; } else { return false; } } }; const getElementBySelector = async (selector: string): Promise => { const container = await getBaseElement(); const elements = container.querySelectorAll(selector); if (!elements.length) { throw new NoElementWithSelectorError({selector}); } else if (elements.length > 1) { throw new MultipleElementsWithSelectorError(elements.length, selector); } return elements[0]! as HTMLElement; }; const getElementsBySelector = async (selector: string) => { const base = await getBaseElement(); return Array.from(base.querySelectorAll(selector)); }; const getDefinitionForKeyType = ( keyType: KeyboardKey, ): { key: string; keyCode?: number; } => { const definition = keyMeta[keyType]; return { key: definition.key, keyCode: 'keyCode' in definition ? Number(definition.keyCode as string | number) : undefined, }; }; const api: UniDriver & { getType: () => void; } = { getType: () => { throw new Error(`Not implemented. Deprecated adapter - migrate to "@wix/unidriver-jsdom-react"`); }, awaited: (timeout) => { return jsdomReactUniDriver( async () => { try { await eventually( async () => { if (!(await api.exists())) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unnecessary-condition throw new NoElementWithSelectorError({selector: ((ctx as any).selector as string) ?? ''}); } }, {timeout}, ); // eslint-disable-next-line @typescript-eslint/return-await return getBaseElement(); } catch (e) { throw (e as Error).cause ?? e; } }, { parent: ctx.parent, selector: ctx.selector, }, ); }, // @ts-expect-error necessary type hack unwrap: () => getBaseElement(), prop: async (name: string) => { const base = await getBaseElement(); return base[name as keyof typeof base]; }, s: (selector: string) => { return jsdomReactUniDriver( async () => { try { return await getElementBySelector(selector); } catch (e) { if (isNoElementWithSelectorError(e)) { throw new NoElementWithSelectorError({ selector, failedPath: e.failedPath, fullPath: [...(await getSelectorsPath(ctx)), selector], }); } throw e; } }, { parent: ctx, selector, }, ); }, ss: (selector: string) => jsdomReactUniDriverList( async () => { const e = await getBaseElement(); return Array.from(e.querySelectorAll(selector)); }, {parent: ctx, selector}, ), $: (selector: string) => { return api.s(unidriverConfig.queryByAttribute(selector)); }, $$: (selector: string) => { return api.ss(unidriverConfig.queryByAttribute(selector)); }, text: async () => getBaseElement().then((e) => { return (e.textContent as string | null) ?? ''; }), value: async () => { const base = (await getBaseElement()) as HTMLInputElement; const value: unknown = base.value; return typeof value === 'number' ? String(value) : (((value as string | null) ?? '') satisfies string); }, click: async (options) => { const elementIsFocusable = ( el: HTMLElement, ): el is HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement => { return ( el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA' || el.tagName === 'BUTTON' || el.tagName === 'svg' ); }; const el = await getBaseElement(); const eventData = {button: 0}; // 0 - Main Button (Left) // setting button 0 is now needed in React 16+ as it's not set by react anymore // 15 - https://github.com/facebook/react/blob/v15.6.1/src/renderers/dom/client/syntheticEvents/SyntheticMouseEvent.js#L45 // 16 - https://github.com/facebook/react/blob/master/packages/react-dom/src/events/SyntheticMouseEvent.js#L33 await act(async () => { Simulate.mouseDown(el, eventData); }); if (elementIsFocusable(el)) { if (document.activeElement !== el) { if (document.activeElement) { await act(async () => { Simulate.blur(document.activeElement!); }); } if (!el.disabled) { await act(async () => { el.focus(); }); await act(async () => { Simulate.focus(el); }); } } } await act(async () => { Simulate.mouseUp(el, eventData); }); await act(async () => { if (options?.type === 'double') { Simulate.doubleClick(el, eventData); } else { Simulate.click(el, eventData); } }); const isCheckable = () => { return ( el.tagName === 'INPUT' && ((el as HTMLInputElement).type === 'checkbox' || (el as HTMLInputElement).type === 'radio') ); }; if (isCheckable()) { // copying value, because original input el get mutated and this value changes const invertedCheck = !(el as HTMLInputElement).checked; await act(async () => { Simulate.input(el, { target: { checked: invertedCheck, } as HTMLInputElement, }); }); await act(async () => { Simulate.change(el, { target: { checked: invertedCheck, } as HTMLInputElement, }); }); } }, mouse: { hover: async () => { const el = await getBaseElement(); await act(async () => { Simulate.mouseOver(el); }); await act(async () => { Simulate.mouseEnter(el); }); }, press: async () => { const el = await getBaseElement(); await act(async () => { Simulate.mouseDown(el); }); }, release: async () => { const el = await getBaseElement(); await act(async () => { Simulate.mouseUp(el); }); }, moveTo: async (to) => { const el = await getBaseElement(); const {left, top} = (await to.unwrap()).getBoundingClientRect(); await act(async () => { Simulate.mouseMove(el, {clientX: left, clientY: top}); }); }, leave: async () => { const el = await getBaseElement(); await act(async () => { Simulate.mouseLeave(el); }); }, }, pressKey: async (key) => { const el = await getBaseElement(); const def = getDefinitionForKeyType(key); await act(async () => { Simulate.keyDown(el, def); }); await act(async () => { Simulate.keyUp(el, def); }); }, hasClass: async (className: string) => (await getBaseElement()).classList.contains(className), enterValue: async (value: string, options?: EnterTextOptions) => { const base = (await getBaseElement()) as any as JSX.IntrinsicElements['input']; return enterValue(base, value, options); }, enterText: async (value: string, options?: EnterTextOptions) => { return api.enterValue(value, { ...options, shouldClear: options?.clear, }); }, attr: async (name: string) => { const el = await getBaseElement(); return el.getAttribute(name); }, exists, get: async (selector: string, options) => { await waitForLegacy( async () => { try { const element = await getElementBySelector(selector); return !!element; } catch { return false; } }, { timeout: options?.timeout, }, ); return jsdomReactUniDriver(() => getElementBySelector(selector), { parent: ctx, selector, }); }, getAll: async (selector: string, options) => { await waitForLegacy( async () => { const elements = await getElementsBySelector(selector); return !!elements.length; }, { timeout: options?.timeout, }, ); return jsdomReactUniDriverList(() => getElementsBySelector(selector), { parent: ctx, selector, }); }, hover: async () => { return api.mouse.hover(); }, wait: async (timeout?: number) => { return waitForLegacy(exists, { timeout, message: contextToWaitError(ctx), }); }, type: 'react', // eslint-disable-next-line @typescript-eslint/no-unsafe-return getNative: () => getBaseElement() as any, _prop: async (name: string) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return api.prop(name); }, }; return api; };