From a2a503994957bdba1efe4862d5c01b2f887d4921 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Sat, 22 Jun 2024 12:25:20 -0400 Subject: [PATCH] refactor(types): build types from JS source --- .eslintignore | 1 + .eslintrc.cjs | 1 + .github/workflows/release.yml | 32 ++++++++- .gitignore | 3 + package.json | 4 +- {types => src/__tests__}/types.test-d.ts | 36 +++++++++- src/index.js | 12 ++-- src/pure.js | 91 ++++++++++++++++++++---- tsconfig.build.json | 12 ++++ tsconfig.json | 3 +- types/index.d.ts | 82 --------------------- types/vite.d.ts | 12 ---- 12 files changed, 169 insertions(+), 120 deletions(-) rename {types => src/__tests__}/types.test-d.ts (67%) create mode 100644 tsconfig.build.json delete mode 100644 types/index.d.ts delete mode 100644 types/vite.d.ts diff --git a/.eslintignore b/.eslintignore index 111e490..39f9d9e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ scripts/* .prettierignore .github/workflows/* *.md +types diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 326785a..f403dca 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -26,6 +26,7 @@ module.exports = { rules: { 'no-undef-init': 'off', 'prefer-const': 'off', + 'svelte/no-unused-svelte-ignore': 'off', }, }, { diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d63311e..c47c84b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,12 +59,36 @@ jobs: run: npm run test:${{ matrix.test-runner }} - name: ▶️ Run type-checks - if: ${{ matrix.node == '20' && matrix.svelte == '4' && matrix.test-runner == 'vitest:jsdom' }} + # NOTE: `SvelteComponent` is not generic in Svelte v3, so type-checking will not pass + 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 @@ -80,6 +104,12 @@ jobs: with: node-version: 20 + - name: 📥 Downloads types build + uses: actions/download-artifact@v4 + with: + name: types + path: types + - name: 🚀 Release uses: cycjimmy/semantic-release-action@v4 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 a79dc52..556f090 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "files": [ "src", "types", - "!*.test-d.ts", "!__tests__" ], "scripts": { @@ -69,7 +68,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:* test:jest types", + "validate": "npm-run-all test:vitest:* test:jest types build", + "build": "tsc -p tsconfig.build.json", "contributors:add": "all-contributors add", "contributors:generate": "all-contributors generate", "preview-release": "./scripts/preview-release" 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/index.js b/src/index.js index 3d8f18f..2704824 100644 --- a/src/index.js +++ b/src/index.js @@ -16,11 +16,7 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) { export * from '@testing-library/dom' // export svelte-specific functions and custom `fireEvent` -// `fireEvent` must be a named export to take priority over wildcard export above -export { - act, - cleanup, - fireEvent, - render, - UnknownSvelteOptionsError, -} from './pure.js' +export { UnknownSvelteOptionsError } from './core/index.js' +export * from './pure.js' +// `fireEvent` must be named to take priority over wildcard from @testing-library/dom +export { fireEvent } from './pure.js' diff --git a/src/pure.js b/src/pure.js index 71dff1e..edb94b3 100644 --- a/src/pure.js +++ b/src/pure.js @@ -1,21 +1,61 @@ import { - fireEvent as dtlFireEvent, + fireEvent as baseFireEvent, getQueriesForElement, prettyDOM, } from '@testing-library/dom' import { tick } from 'svelte' -import { - mount, - UnknownSvelteOptionsError, - unmount, - updateProps, - validateOptions, -} from './core/index.js' +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('svelte').SvelteComponent} C + * @typedef {import('svelte').ComponentProps | 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('svelte').SvelteComponent} C + * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] + * + * @typedef {{ + * container: HTMLElement + * baseElement: HTMLElement + * component: C + * 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('svelte').SvelteComponent} C + * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] + * + * @param {import('svelte').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) @@ -62,6 +102,7 @@ const render = (Component, options = {}, renderOptions = {}) => { } } +/** Remove a component from the component cache. */ const cleanupComponent = (component) => { const inCache = componentCache.delete(component) @@ -70,6 +111,7 @@ const cleanupComponent = (component) => { } } +/** Remove a target element from the target cache. */ const cleanupTarget = (target) => { const inCache = targetCache.delete(target) @@ -78,11 +120,18 @@ const cleanupTarget = (target) => { } } +/** Unmount all components and remove elements added to ``. */ const cleanup = () => { componentCache.forEach(cleanupComponent) targetCache.forEach(cleanupTarget) } +/** + * 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} + */ const act = async (fn) => { if (fn) { await fn() @@ -90,18 +139,36 @@ const act = async (fn) => { return tick() } +/** + * @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) => { - const event = dtlFireEvent(...args) + const event = baseFireEvent(...args) await tick() return event } -Object.keys(dtlFireEvent).forEach((key) => { +Object.keys(baseFireEvent).forEach((key) => { fireEvent[key] = async (...args) => { - const event = dtlFireEvent[key](...args) + const event = baseFireEvent[key](...args) await tick() return event } }) -export { act, cleanup, fireEvent, render, UnknownSvelteOptionsError } +export { act, cleanup, fireEvent, render } 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