import { configure as configureDTL, fireEvent as baseFireEvent, getConfig as getDTLConfig, getQueriesForElement, prettyDOM, } from '@testing-library/dom' import * as Svelte from 'svelte' import { mount, unmount, updateProps, validateOptions } from './core/index.js' const targetCache = new Set() const componentCache = new Set() /** * Customize how Svelte renders the component. * * @template {import('./component-types.js').Component} C * @typedef {import('./component-types.js').Props | Partial>} SvelteComponentOptions */ /** * Customize how Testing Library sets up the document and binds queries. * * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] * @typedef {{ * baseElement?: HTMLElement * queries?: Q * }} RenderOptions */ /** * The rendered component and bound testing functions. * * @template {import('./component-types.js').Component} C * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] * * @typedef {{ * container: HTMLElement * baseElement: HTMLElement * component: import('./component-types.js').Exports * debug: (el?: HTMLElement | DocumentFragment) => void * rerender: (props: Partial>) => Promise * unmount: () => void * } & { * [P in keyof Q]: import('@testing-library/dom').BoundFunction * }} RenderResult */ /** * Render a component into the document. * * @template {import('./component-types.js').Component} C * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] * * @param {import('./component-types.js').ComponentType} Component - The component to render. * @param {SvelteComponentOptions} options - Customize how Svelte renders the component. * @param {RenderOptions} renderOptions - Customize how Testing Library sets up the document and binds queries. * @returns {RenderResult} The rendered component and bound testing functions. */ const render = (Component, options = {}, renderOptions = {}) => { options = validateOptions(options) const baseElement = renderOptions.baseElement ?? options.target ?? document.body const queries = getQueriesForElement(baseElement, renderOptions.queries) const target = // eslint-disable-next-line unicorn/prefer-dom-node-append options.target ?? baseElement.appendChild(document.createElement('div')) targetCache.add(target) const component = mount( Component.default ?? Component, { ...options, target }, cleanupComponent ) componentCache.add(component) return { baseElement, component, container: target, debug: (el = baseElement) => { console.log(prettyDOM(el)) }, rerender: async (props) => { if (props.props) { console.warn( 'rerender({ props: {...} }) deprecated, use rerender({...}) instead' ) props = props.props } updateProps(component, props) await Svelte.tick() }, unmount: () => { cleanupComponent(component) }, ...queries, } } /** @type {import('@testing-library/dom'.Config | undefined} */ let originalDTLConfig /** * Configure `@testing-library/dom` for usage with Svelte. * * Ensures events fired from `@testing-library/dom` * and `@testing-library/user-event` wait for Svelte * to flush changes to the DOM before proceeding. */ const setup = () => { originalDTLConfig = getDTLConfig() configureDTL({ asyncWrapper: act, eventWrapper: Svelte.flushSync ?? ((cb) => cb()), }) } /** Reset dom-testing-library config. */ const cleanupDTL = () => { if (originalDTLConfig) { configureDTL(originalDTLConfig) originalDTLConfig = undefined } } /** Remove a component from the component cache. */ const cleanupComponent = (component) => { const inCache = componentCache.delete(component) if (inCache) { unmount(component) } } /** Remove a target element from the target cache. */ const cleanupTarget = (target) => { const inCache = targetCache.delete(target) if (inCache && target.parentNode === document.body) { target.remove() } } /** Unmount components, remove elements added to ``, and reset `@testing-library/dom`. */ const cleanup = () => { for (const component of componentCache) { cleanupComponent(component) } for (const target of targetCache) { cleanupTarget(target) } cleanupDTL() } /** * Call a function and wait for Svelte to flush pending changes. * * @template T * @param {(() => Promise) | () => T} [fn] - A function, which may be `async`, to call before flushing updates. * @returns {Promise} */ const act = async (fn) => { let result if (fn) { result = await fn() } await Svelte.tick() return result } /** * @typedef {(...args: Parameters) => Promise>} FireFunction */ /** * @typedef {{ * [K in import('@testing-library/dom').EventType]: (...args: Parameters) => Promise> * }} FireObject */ /** * Fire an event on an element. * * Consider using `@testing-library/user-event` instead, if possible. * @see https://testing-library.com/docs/user-event/intro/ * * @type {FireFunction & FireObject} */ const fireEvent = async (...args) => act(() => baseFireEvent(...args)) for (const [key, baseEvent] of Object.entries(baseFireEvent)) { fireEvent[key] = async (...args) => act(() => baseEvent(...args)) } export { act, cleanup, fireEvent, render, setup }