diff --git a/.editorconfig b/.editorconfig index ff9b6fc..03c17bf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,4 @@ insert_final_newline = true trim_trailing_whitespace = true [*.md] -max_line_length = off trim_trailing_whitespace = false diff --git a/package.json b/package.json index 020da30..307b300 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "test:all": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest && pnpm test:examples", "test:all:legacy": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest", "build": "pnpm build:types && pnpm build:docs", - "build:types": "tsc --build && cp packages/svelte/src/core/types.d.ts packages/svelte/dist/core", - "build:docs": "remark --output --use remark-toc --use remark-code-import --use unified-prettier README.md examples && cp -f README.md packages/svelte", + "build:types": "tsc --build", + "build:docs": "remark --output --use remark-toc --use remark-code-import --use unified-prettier README.md packages/*/README.md examples && cp -f README.md packages/svelte", "contributors:add": "all-contributors add", "contributors:generate": "all-contributors generate", "install:3": "./scripts/install-dependencies 3", diff --git a/packages/svelte-core/README.md b/packages/svelte-core/README.md new file mode 100644 index 0000000..98ad590 --- /dev/null +++ b/packages/svelte-core/README.md @@ -0,0 +1,147 @@ +# @testing-library/svelte-core + +Do you want to build your own Svelte testing library? You may want to use our +rendering core, which abstracts away differences in Svelte versions to provide a +simple API to render Svelte components into the document and clean them up +afterwards + +## Table of Contents + +- [Example Usage](#example-usage) +- [API](#api) + - [`setup`](#setup) + - [`mount`](#mount) + - [`cleanup`](#cleanup) + - [`addCleanupTask`](#addcleanuptask) + - [`removeCleanupTask`](#removecleanuptask) + - [Utility types](#utility-types) + +## Example Usage + +```ts +import { beforeEach } from 'vitest' +import { cleanup, mount, setup } from '@testing-library/svelte-core' +import type { + Component, + ComponentType, + ComponentOptions, + Props, + Exports, +} from '@testing-library/svelte-core/types' + +import { bindQueries, type Screen } from './bring-your-own-queries.js' + +beforeEach(() => { + cleanup() +}) + +export interface RenderResult { + screen: Screen + component: Exports + target: HTMLElement + rerender: (props: Partial>) => Promise + unmount: () => void +} + +export const render = ( + component: ComponentType, + options: ComponentOptions +): RenderResult => { + const { baseElement, target, mountOptions } = setup(options) + const { component, unmount, rerender } = mount(component, mountOptions) + const screen = bindQueries(baseElement) + + return { screen, component, target, rerender, unmount } +} +``` + +## API + +### `setup` + +Validate options and prepare document elements for rendering. + +```ts +const { baseElement, target, mountOptions } = setup(options, renderOptions) +``` + +| Argument | Type | Description | +| ------------------ | ---------------------------------------- | ---------------------------------------------- | +| `componentOptions` | `Props` or partial [component options][] | Options for how the component will be rendered | +| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` | + +| Result | Type | Description | Default | +| -------------- | --------------------- | ------------------------------------------- | ----------------------------------- | +| `baseElement` | `HTMLElement` | The base element | `document.body` | +| `target` | `HTMLElement` | The component's `target` element | `
` appended to `document.body` | +| `mountOptions` | [component options][] | Validated Svelte options to pass to `mount` | `{ target, props: {} }` | + +[component options]: https://svelte.dev/docs/client-side-component-api + +### `mount` + +Mount a Svelte component into the document. + +```ts +const { component, unmount, rerender } = mount(Component, options) +``` + +| Argument | Type | Description | +| ----------- | --------------------- | ---------------------------- | +| `Component` | [Svelte component][] | An imported Svelte component | +| `options` | [component options][] | Svelte component options | + +| Result | Type | Description | +| ----------- | ------------------------------------------ | -------------------------------------------------- | +| `component` | [component instance][] | The component instance | +| `unmount` | `() => void` | Unmount the component from the document | +| `rerender` | `(props: Partial) => Promise` | Update the component's props and wait for rerender | + +[Svelte component]: https://svelte.dev/docs/svelte-components +[component instance]: https://svelte.dev/docs/client-side-component-api + +### `cleanup` + +Cleanup rendered components and added elements. Call this when your tests are +over. + +```ts +cleanup() +``` + +### `addCleanupTask` + +Add a custom cleanup task to be called with `cleanup()` + +```ts +addCleanupTask(() => { + // ...reset something +}) +``` + +### `removeCleanupTask` + +Remove a cleanup task from `cleanup()`. Useful if a cleanup task can only be run +once and may be run outside of `cleanup` + +```ts +const customCleanup = () => { + // ...reset something +} + +addCleanupTask(customCleanup) + +const manuallyCleanupEarly = () => { + customCleanup() + removeCleanupTask(customCleanup) +} +``` + +### Utility types + +This module exports various utility types from +`@testing-library/svelte-core/types`. They adapt to whatever Svelte version is +installed, and can be used to get type signatures for imported components, +props, events, etc. + +See [`./types.d.ts`](./types.d.ts) for the full list of types available. diff --git a/packages/svelte-core/package.json b/packages/svelte-core/package.json new file mode 100644 index 0000000..1d7e4ce --- /dev/null +++ b/packages/svelte-core/package.json @@ -0,0 +1,52 @@ +{ + "name": "@testing-library/svelte-core", + "version": "0.0.0-semantically-released", + "description": "Core rendering and cleanup logic for Svelte testing utilities.", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./src/index.js" + }, + "./types": { + "types": "./types.d.ts" + } + }, + "type": "module", + "license": "MIT", + "homepage": "https://github.com/testing-library/svelte-testing-library#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/testing-library/svelte-testing-library.git", + "directory": "packages/svelte-core" + }, + "bugs": { + "url": "https://github.com/testing-library/svelte-testing-library/issues" + }, + "engines": { + "node": ">=16" + }, + "keywords": [ + "testing", + "svelte", + "ui", + "dom", + "jsdom", + "unit", + "integration", + "functional", + "end-to-end", + "e2e" + ], + "files": [ + "dist", + "src", + "types.d.ts" + ], + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/svelte/src/core/cleanup.js b/packages/svelte-core/src/cleanup.js similarity index 100% rename from packages/svelte/src/core/cleanup.js rename to packages/svelte-core/src/cleanup.js diff --git a/packages/svelte/src/core/index.js b/packages/svelte-core/src/index.js similarity index 100% rename from packages/svelte/src/core/index.js rename to packages/svelte-core/src/index.js diff --git a/packages/svelte/src/core/mount.js b/packages/svelte-core/src/mount.js similarity index 89% rename from packages/svelte/src/core/mount.js rename to packages/svelte-core/src/mount.js index 6e5d1c5..4d65f82 100644 --- a/packages/svelte/src/core/mount.js +++ b/packages/svelte-core/src/mount.js @@ -62,13 +62,13 @@ const mountComponent = IS_MODERN_SVELTE ? mountModern : mountLegacy /** * Render a Svelte component into the document. * - * @template {import('./types.js').Component} C - * @param {import('./types.js').ComponentType} Component - * @param {import('./types.js').MountOptions} options + * @template {import('../types.js').Component} C + * @param {import('../types.js').ComponentType} Component + * @param {import('../types.js').MountOptions} options * @returns {{ * component: C * unmount: () => void - * rerender: (props: Partial>) => Promise + * rerender: (props: Partial>) => Promise * }} */ const mount = (Component, options) => { diff --git a/packages/svelte/src/core/props.svelte.js b/packages/svelte-core/src/props.svelte.js similarity index 100% rename from packages/svelte/src/core/props.svelte.js rename to packages/svelte-core/src/props.svelte.js diff --git a/packages/svelte/src/core/setup.js b/packages/svelte-core/src/setup.js similarity index 82% rename from packages/svelte/src/core/setup.js rename to packages/svelte-core/src/setup.js index 25466be..4008c24 100644 --- a/packages/svelte/src/core/setup.js +++ b/packages/svelte-core/src/setup.js @@ -27,9 +27,9 @@ class UnknownSvelteOptionsError extends TypeError { /** * Validate a component's mount options. * - * @template {import('./types.js').Component} C - * @param {import('./types.js').ComponentOptions} options - props or mount options - * @returns {Partial>} + * @template {import('../types.js').Component} C + * @param {import('../types.js').ComponentOptions} options - props or mount options + * @returns {Partial>} */ const validateOptions = (options) => { const isProps = !Object.keys(options).some((option) => @@ -55,16 +55,16 @@ const validateOptions = (options) => { /** * Set up the document to render a component. * - * @template {import('./types.js').Component} C - * @param {import('./types.js').ComponentOptions} componentOptions - props or mount options + * @template {import('../types.js').Component} C + * @param {import('../types.js').ComponentOptions} componentOptions - props or mount options * @param {{ baseElement?: HTMLElement | undefined }} setupOptions - base element of the document to bind any queries * @returns {{ * baseElement: HTMLElement, * target: HTMLElement, - * mountOptions: import('./types.js).MountOptions + * mountOptions: import('../types.js').MountOptions * }} */ -const setup = (componentOptions, setupOptions) => { +const setup = (componentOptions, setupOptions = {}) => { const mountOptions = validateOptions(componentOptions) const baseElement = diff --git a/packages/svelte/src/core/svelte-version.js b/packages/svelte-core/src/svelte-version.js similarity index 100% rename from packages/svelte/src/core/svelte-version.js rename to packages/svelte-core/src/svelte-version.js diff --git a/packages/svelte-core/tsconfig.json b/packages/svelte-core/tsconfig.json new file mode 100644 index 0000000..9e5fee6 --- /dev/null +++ b/packages/svelte-core/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "node16", + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src", "types.d.ts"] +} diff --git a/packages/svelte/src/core/types.d.ts b/packages/svelte-core/types.d.ts similarity index 100% rename from packages/svelte/src/core/types.d.ts rename to packages/svelte-core/types.d.ts diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 11837b0..098d966 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -50,7 +50,7 @@ ], "files": [ "src", - "types" + "dist" ], "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", @@ -66,6 +66,11 @@ } }, "dependencies": { - "@testing-library/dom": "9.x.x || 10.x.x" + "@testing-library/dom": "9.x.x || 10.x.x", + "@testing-library/svelte-core": "workspace:*" + }, + "publishConfig": { + "access": "public", + "provenance": true } } diff --git a/packages/svelte/src/pure.js b/packages/svelte/src/pure.js index 8ae70f4..ca39998 100644 --- a/packages/svelte/src/pure.js +++ b/packages/svelte/src/pure.js @@ -5,15 +5,14 @@ import { getQueriesForElement, prettyDOM, } from '@testing-library/dom' +import * as Core from '@testing-library/svelte-core' import * as Svelte from 'svelte' -import * as Core from './core/index.js' - /** * Customize how Svelte renders the component. * - * @template {import('./core/types.js').Component} C - * @typedef {import('./core/types.js').ComponentOptions} SvelteComponentOptions + * @template {import('@testing-library/svelte-core/types').Component} C + * @typedef {import('@testing-library/svelte-core/types').ComponentOptions} SvelteComponentOptions */ /** @@ -29,15 +28,15 @@ import * as Core from './core/index.js' /** * The rendered component and bound testing functions. * - * @template {import('./core/types.js').Component} C + * @template {import('@testing-library/svelte-core/types').Component} C * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] * * @typedef {{ * container: HTMLElement * baseElement: HTMLElement - * component: import('./core/types.js').Exports + * component: import('@testing-library/svelte-core/types').Exports * debug: (el?: HTMLElement | DocumentFragment) => void - * rerender: (props: Partial>) => Promise + * rerender: (props: Partial>) => Promise * unmount: () => void * } & { * [P in keyof Q]: import('@testing-library/dom').BoundFunction @@ -47,10 +46,10 @@ import * as Core from './core/index.js' /** * Render a component into the document. * - * @template {import('./core/types.js').Component} C + * @template {import('@testing-library/svelte-core/types').Component} C * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] * - * @param {import('./core/types.js').ComponentType} Component - The component to render. + * @param {import('@testing-library/svelte-core/types').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. @@ -155,5 +154,5 @@ for (const [key, baseEvent] of Object.entries(baseFireEvent)) { fireEvent[key] = async (...args) => act(() => baseEvent(...args)) } -export { UnknownSvelteOptionsError } from './core/index.js' +export { UnknownSvelteOptionsError } from '@testing-library/svelte-core' export { act, cleanup, fireEvent, render, setup } diff --git a/packages/svelte/src/vite.js b/packages/svelte/src/vite.js index b57c886..b859b59 100644 --- a/packages/svelte/src/vite.js +++ b/packages/svelte/src/vite.js @@ -1,6 +1,8 @@ import path from 'node:path' import url from 'node:url' +const STL_PACKAGES = ['@testing-library/svelte', '@testing-library/svelte-core'] + /** * Vite plugin to configure @testing-library/svelte. * @@ -107,17 +109,25 @@ const addNoExternal = (config) => { return } - for (const rule of noExternal) { - if (typeof rule === 'string' && rule === '@testing-library/svelte') { - return - } + const missingPackages = [] - if (rule instanceof RegExp && rule.test('@testing-library/svelte')) { - return + for (const packageName of STL_PACKAGES) { + const isIncluded = noExternal.some( + (rule) => + (typeof rule === 'string' && rule === packageName) || + (rule instanceof RegExp && rule.test(packageName)) + ) + + if (!isIncluded) { + missingPackages.push(packageName) } } - noExternal.push('@testing-library/svelte') + if (missingPackages.length === 0) { + return + } + + noExternal.push(...missingPackages) ssr.noExternal = noExternal config.ssr = ssr } diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index 66fe363..2e23d68 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -1,4 +1,5 @@ { + "references": [{ "path": "../svelte-core" }], "compilerOptions": { "module": "node16", "allowJs": true, diff --git a/prettier.config.js b/prettier.config.js index 55343f2..19919d0 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -13,7 +13,6 @@ export default { { files: 'examples/**/*.md', options: { - printWidth: 80, proseWrap: 'always', }, }, diff --git a/tests/vite-plugin.test.js b/tests/vite-plugin.test.js index 92fc8f5..480a260 100644 --- a/tests/vite-plugin.test.js +++ b/tests/vite-plugin.test.js @@ -147,19 +147,47 @@ describe.skipIf(IS_JEST)('vite plugin', () => { test.each([ { config: () => ({ ssr: { noExternal: [] } }), - expectedNoExternal: ['@testing-library/svelte'], + expectedNoExternal: [ + '@testing-library/svelte', + '@testing-library/svelte-core', + ], }, { config: () => ({}), - expectedNoExternal: ['@testing-library/svelte'], + expectedNoExternal: [ + '@testing-library/svelte', + '@testing-library/svelte-core', + ], }, { config: () => ({ ssr: { noExternal: 'other-file.js' } }), - expectedNoExternal: ['other-file.js', '@testing-library/svelte'], + expectedNoExternal: [ + 'other-file.js', + '@testing-library/svelte', + '@testing-library/svelte-core', + ], }, { config: () => ({ ssr: { noExternal: /other/u } }), - expectedNoExternal: [/other/u, '@testing-library/svelte'], + expectedNoExternal: [ + /other/u, + '@testing-library/svelte', + '@testing-library/svelte-core', + ], + }, + { + config: () => ({ ssr: { noExternal: '@testing-library/svelte' } }), + expectedNoExternal: [ + '@testing-library/svelte', + '@testing-library/svelte-core', + ], + }, + { + config: () => ({ ssr: { noExternal: '@testing-library/svelte-core' } }), + expectedNoExternal: [ + '@testing-library/svelte-core', + '@testing-library/svelte', + ], }, ])('adds noExternal rule', ({ config, expectedNoExternal }) => { const subject = svelteTesting({ @@ -183,10 +211,6 @@ describe.skipIf(IS_JEST)('vite plugin', () => { config: () => ({ ssr: { noExternal: true } }), expectedNoExternal: true, }, - { - config: () => ({ ssr: { noExternal: '@testing-library/svelte' } }), - expectedNoExternal: '@testing-library/svelte', - }, { config: () => ({ ssr: { noExternal: /svelte/u } }), expectedNoExternal: /svelte/u, diff --git a/tsconfig.json b/tsconfig.json index 493e5db..a36e1bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,7 @@ { - "references": [{ "path": "packages/svelte" }], + "references": [ + { "path": "packages/svelte" }, + { "path": "packages/svelte-core" } + ], "files": [] }