diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 27af7a8..2737c1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,12 +59,35 @@ jobs: run: npm run test:${{ matrix.test-runner }} - name: ▶️ Run type-checks - if: ${{ matrix.node == '20' && matrix.svelte == '4' && matrix.test-runner == 'vitest:jsdom' }} + if: ${{ matrix.node == '20' && matrix.svelte != '3' && matrix.test-runner == 'vitest:jsdom' }} run: npm run types - name: ⬆️ Upload coverage report uses: codecov/codecov-action@v3 + build: + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v4 + + - name: ⎔ Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: 📥 Download deps + run: npm install --no-package-lock + + - name: 🏗️ Build types + run: npm run build + + - name: ⬆️ Upload types build + uses: actions/upload-artifact@v4 + with: + name: types + path: types + release: needs: main runs-on: ubuntu-latest @@ -83,6 +106,12 @@ jobs: - name: 📥 Download deps run: npm install --no-package-lock + - name: 📥 Downloads types build + uses: actions/download-artifact@v4 + with: + name: types + path: types + - name: 🚀 Release uses: cycjimmy/semantic-release-action@v2 with: diff --git a/.gitignore b/.gitignore index c09be87..151e826 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ dist yarn-error.log package-lock.json yarn.lock + +# generated typing output +types diff --git a/package.json b/package.json index b67db89..6d415d6 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "test:vitest:happy-dom": "vitest run --coverage --environment happy-dom", "test:jest": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --coverage", "types": "svelte-check", - "validate": "npm-run-all test:vitest:* types", + "build": "tsc -p tsconfig.build.json", + "validate": "npm-run-all test:vitest:* types build", "contributors:add": "all-contributors add", "contributors:generate": "all-contributors generate" }, diff --git a/types/types.test-d.ts b/src/__tests__/types.test-d.ts similarity index 67% rename from types/types.test-d.ts rename to src/__tests__/types.test-d.ts index 4a42bb1..9927a05 100644 --- a/types/types.test-d.ts +++ b/src/__tests__/types.test-d.ts @@ -2,8 +2,8 @@ import { expectTypeOf } from 'expect-type' import type { ComponentProps, SvelteComponent } from 'svelte' import { describe, test } from 'vitest' -import Simple from '../src/__tests__/fixtures/Simple.svelte' -import * as subject from './index.js' +import * as subject from '../index.js' +import Simple from './fixtures/Simple.svelte' describe('types', () => { test('render is a function that accepts a Svelte component', () => { @@ -62,4 +62,36 @@ describe('types', () => { expectTypeOf(result.getByVibes).parameters.toMatchTypeOf<[vibes: string]>() }) + + test('act is an async function', () => { + expectTypeOf(subject.act).toMatchTypeOf<() => Promise>() + }) + + test('act accepts a sync function', () => { + expectTypeOf(subject.act).toMatchTypeOf<(fn: () => void) => Promise>() + }) + + test('act accepts an async function', () => { + expectTypeOf(subject.act).toMatchTypeOf< + (fn: () => Promise) => Promise + >() + }) + + test('fireEvent is an async function', () => { + expectTypeOf(subject.fireEvent).toMatchTypeOf< + ( + element: Element | Node | Document | Window, + event: Event + ) => Promise + >() + }) + + test('fireEvent[eventName] is an async function', () => { + expectTypeOf(subject.fireEvent.click).toMatchTypeOf< + ( + element: Element | Node | Document | Window, + options?: {} + ) => Promise + >() + }) }) diff --git a/src/pure.js b/src/pure.js index 364c225..10063a7 100644 --- a/src/pure.js +++ b/src/pure.js @@ -1,13 +1,44 @@ -import { - fireEvent as dtlFireEvent, - getQueriesForElement, - prettyDOM, -} from '@testing-library/dom' +import * as DOMTestingLibrary from '@testing-library/dom' import * as Svelte from 'svelte' import { VERSION as SVELTE_VERSION } from 'svelte/compiler' const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) +/** + * Customize how Svelte renders the component. + * + * @template {Svelte.SvelteComponent} C + * @typedef {Svelte.ComponentProps | Partial>>} SvelteComponentOptions + */ + +/** + * Customize how Testing Library sets up the document and binds queries. + * + * @template {DOMTestingLibrary.Queries} [Q=typeof DOMTestingLibrary.queries] + * @typedef {{ + * baseElement?: HTMLElement + * queries?: Q + * }} RenderOptions + */ + +/** + * The rendered component and bound testing functions. + * + * @template {Svelte.SvelteComponent} C + * @template {DOMTestingLibrary.Queries} [Q=typeof DOMTestingLibrary.queries] + * + * @typedef {{ + * container: HTMLElement + * baseElement: HTMLElement + * component: C + * debug: (el?: HTMLElement | DocumentFragment) => void + * rerender: (props: Partial>) => Promise + * unmount: () => void + * } & { + * [P in keyof Q]: DOMTestingLibrary.BoundFunction + * }} RenderResult + */ + export class SvelteTestingLibrary { svelteComponentOptions = [ 'target', @@ -49,6 +80,17 @@ export class SvelteTestingLibrary { return { props: options } } + /** + * Render a component into the document. + * + * @template {Svelte.SvelteComponent} C + * @template {DOMTestingLibrary.Queries} [Q=typeof DOMTestingLibrary.queries] + * + * @param {Svelte.ComponentType} Component - The component to render. + * @param {SvelteComponentOptions} componentOptions - 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. + */ render(Component, componentOptions = {}, renderOptions = {}) { componentOptions = this.checkProps(componentOptions) @@ -72,7 +114,7 @@ export class SvelteTestingLibrary { baseElement, component, container: target, - debug: (el = baseElement) => console.log(prettyDOM(el)), + debug: (el = baseElement) => console.log(DOMTestingLibrary.prettyDOM(el)), rerender: async (props) => { if (props.props) { console.warn( @@ -86,7 +128,10 @@ export class SvelteTestingLibrary { unmount: () => { this.cleanupComponent(component) }, - ...getQueriesForElement(baseElement, renderOptions.queries), + ...DOMTestingLibrary.getQueriesForElement( + baseElement, + renderOptions.queries + ), } } @@ -123,6 +168,9 @@ export class SvelteTestingLibrary { } } + /** + * Unmount all components and remove elements added to ``. + */ cleanup() { this.componentCache.forEach(this.cleanupComponent.bind(this)) this.targetCache.forEach(this.cleanupTarget.bind(this)) @@ -135,6 +183,12 @@ export const render = instance.render.bind(instance) export const cleanup = instance.cleanup.bind(instance) +/** + * Call a function and wait for Svelte to flush pending changes. + * + * @param {() => unknown} [fn] - A function, which may be `async`, to call before flushing updates. + * @returns {Promise} + */ export const act = async (fn) => { if (fn) { await fn() @@ -142,15 +196,33 @@ export const act = async (fn) => { return Svelte.tick() } +/** + * @typedef {(...args: Parameters) => Promise>} FireFunction + */ + +/** + * @typedef {{ + * [K in DOMTestingLibrary.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} + */ export const fireEvent = async (...args) => { - const event = dtlFireEvent(...args) + const event = DOMTestingLibrary.fireEvent(...args) await Svelte.tick() return event } -Object.keys(dtlFireEvent).forEach((key) => { +Object.keys(DOMTestingLibrary.fireEvent).forEach((key) => { fireEvent[key] = async (...args) => { - const event = dtlFireEvent[key](...args) + const event = DOMTestingLibrary.fireEvent[key](...args) await Svelte.tick() return event } diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..0baa218 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": ["./tsconfig.json"], + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "noEmit": false, + "rootDir": "src", + "outDir": "types" + }, + "exclude": ["src/**/__tests__/**"] +} diff --git a/tsconfig.json b/tsconfig.json index 2b353f3..f79cace 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,11 @@ { "compilerOptions": { "module": "node16", + "allowJs": true, "noEmit": true, "skipLibCheck": true, "strict": true, "types": ["svelte", "vite/client", "vitest", "vitest/globals"] }, - "include": ["src", "types"] + "include": ["src"] } diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index a206467..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Type definitions for Svelte Testing Library -// Project: https://github.com/testing-library/svelte-testing-library -// Definitions by: Rahim Alwer - -import { - BoundFunction, - EventType, - Queries, - queries, -} from '@testing-library/dom' -import { - ComponentConstructorOptions, - ComponentProps, - SvelteComponent, -} from 'svelte' - -export * from '@testing-library/dom' - -type SvelteComponentOptions = - | ComponentProps - | Partial>> - -type Constructor = new (...args: any[]) => T - -/** - * Render a Component into the Document. - */ -export type RenderResult< - C extends SvelteComponent, - Q extends Queries = typeof queries, -> = { - container: HTMLElement - baseElement: HTMLElement - component: C - debug: (el?: HTMLElement | DocumentFragment) => void - rerender: (props: Partial>) => Promise - unmount: () => void -} & { [P in keyof Q]: BoundFunction } - -export interface RenderOptions { - baseElement?: HTMLElement - queries?: Q -} - -export function render< - C extends SvelteComponent, - Q extends Queries = typeof queries, ->( - component: Constructor, - componentOptions?: SvelteComponentOptions, - renderOptions?: RenderOptions -): RenderResult - -/** - * Unmounts trees that were mounted with render. - */ -export function cleanup(): void - -/** - * Fires DOM events on an element provided by @testing-library/dom. Since Svelte needs to flush - * pending state changes via `tick`, these methods have been override and now return a promise. - */ -export type FireFunction = ( - element: Document | Element | Window, - event: Event -) => Promise - -export type FireObject = { - [K in EventType]: ( - element: Document | Element | Window, - options?: {} - ) => Promise -} - -export const fireEvent: FireFunction & FireObject - -/** - * Calls a function and notifies Svelte to flush any pending state changes. - * - * If the function returns a Promise, that Promise will be resolved first. - */ -export function act(fn?: () => unknown): Promise diff --git a/types/vite.d.ts b/types/vite.d.ts deleted file mode 100644 index 470e487..0000000 --- a/types/vite.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Plugin } from 'vite' - -/** - * Vite plugin to configure @testing-library/svelte. - * - * Ensures Svelte is imported correctly in tests - * and that the DOM is cleaned up after each test. - */ -export function svelteTesting(options?: { - resolveBrowser?: boolean - autoCleanup?: boolean -}): Plugin