Skip to content

Commit 74b39c1

Browse files
committed
feat(core): create standalone core module
1 parent dc415ae commit 74b39c1

File tree

19 files changed

+360
-71
lines changed

19 files changed

+360
-71
lines changed

.editorconfig

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,4 @@ insert_final_newline = true
88
trim_trailing_whitespace = true
99

1010
[*.md]
11-
max_line_length = off
1211
trim_trailing_whitespace = false

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
"test:all": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest && pnpm test:examples",
2121
"test:all:legacy": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest",
2222
"build": "pnpm build:types && pnpm build:docs",
23-
"build:types": "tsc --build && cp packages/svelte/src/core/types.d.ts packages/svelte/dist/core",
24-
"build:docs": "remark --output --use remark-toc --use remark-code-import --use unified-prettier README.md examples && cp -f README.md packages/svelte",
23+
"build:types": "tsc --build",
24+
"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",
2525
"contributors:add": "all-contributors add",
2626
"contributors:generate": "all-contributors generate",
2727
"install:3": "./scripts/install-dependencies 3",

packages/svelte-core/README.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# @testing-library/svelte-core
2+
3+
Do you want to build your own Svelte testing library? You may want to use our
4+
rendering core, which abstracts away differences in Svelte versions to provide a
5+
simple API to render Svelte components into the document and clean them up
6+
afterwards
7+
8+
## Table of Contents
9+
10+
- [Example Usage](#example-usage)
11+
- [API](#api)
12+
- [`setup`](#setup)
13+
- [`mount`](#mount)
14+
- [`cleanup`](#cleanup)
15+
- [`addCleanupTask`](#addcleanuptask)
16+
- [`removeCleanupTask`](#removecleanuptask)
17+
- [Utility types](#utility-types)
18+
19+
## Example Usage
20+
21+
```ts
22+
import { beforeEach } from 'vitest'
23+
import { cleanup, mount, setup } from '@testing-library/svelte-core'
24+
import type {
25+
Component,
26+
ComponentType,
27+
ComponentOptions,
28+
Props,
29+
Exports,
30+
} from '@testing-library/svelte-core/types'
31+
32+
import { bindQueries, type Screen } from './bring-your-own-queries.js'
33+
34+
beforeEach(() => {
35+
cleanup()
36+
})
37+
38+
export interface RenderResult<C extends Component> {
39+
screen: Screen
40+
component: Exports<C>
41+
target: HTMLElement
42+
rerender: (props: Partial<Props<C>>) => Promise<void>
43+
unmount: () => void
44+
}
45+
46+
export const render = <C extends Component>(
47+
component: ComponentType<C>,
48+
options: ComponentOptions<C>
49+
): RenderResult<C> => {
50+
const { baseElement, target, mountOptions } = setup(options)
51+
const { component, unmount, rerender } = mount(component, mountOptions)
52+
const screen = bindQueries(baseElement)
53+
54+
return { screen, component, target, rerender, unmount }
55+
}
56+
```
57+
58+
## API
59+
60+
### `setup`
61+
62+
Validate options and prepare document elements for rendering.
63+
64+
```ts
65+
const { baseElement, target, mountOptions } = setup(options, renderOptions)
66+
```
67+
68+
| Argument | Type | Description |
69+
| ------------------ | ---------------------------------------- | ---------------------------------------------- |
70+
| `componentOptions` | `Props` or partial [component options][] | Options for how the component will be rendered |
71+
| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` |
72+
73+
| Result | Type | Description | Default |
74+
| -------------- | --------------------- | ------------------------------------------- | ----------------------------------- |
75+
| `baseElement` | `HTMLElement` | The base element | `document.body` |
76+
| `target` | `HTMLElement` | The component's `target` element | `<div>` appended to `document.body` |
77+
| `mountOptions` | [component options][] | Validated Svelte options to pass to `mount` | `{ target, props: {} }` |
78+
79+
[component options]: https://svelte.dev/docs/client-side-component-api
80+
81+
### `mount`
82+
83+
Mount a Svelte component into the document.
84+
85+
```ts
86+
const { component, unmount, rerender } = mount(Component, options)
87+
```
88+
89+
| Argument | Type | Description |
90+
| ----------- | --------------------- | ---------------------------- |
91+
| `Component` | [Svelte component][] | An imported Svelte component |
92+
| `options` | [component options][] | Svelte component options |
93+
94+
| Result | Type | Description |
95+
| ----------- | ------------------------------------------ | -------------------------------------------------- |
96+
| `component` | [component instance][] | The component instance |
97+
| `unmount` | `() => void` | Unmount the component from the document |
98+
| `rerender` | `(props: Partial<Props>) => Promise<void>` | Update the component's props and wait for rerender |
99+
100+
[Svelte component]: https://svelte.dev/docs/svelte-components
101+
[component instance]: https://svelte.dev/docs/client-side-component-api
102+
103+
### `cleanup`
104+
105+
Cleanup rendered components and added elements. Call this when your tests are
106+
over.
107+
108+
```ts
109+
cleanup()
110+
```
111+
112+
### `addCleanupTask`
113+
114+
Add a custom cleanup task to be called with `cleanup()`
115+
116+
```ts
117+
addCleanupTask(() => {
118+
// ...reset something
119+
})
120+
```
121+
122+
### `removeCleanupTask`
123+
124+
Remove a cleanup task from `cleanup()`. Useful if a cleanup task can only be run
125+
once and may be run outside of `cleanup`
126+
127+
```ts
128+
const customCleanup = () => {
129+
// ...reset something
130+
}
131+
132+
addCleanupTask(customCleanup)
133+
134+
const manuallyCleanupEarly = () => {
135+
customCleanup()
136+
removeCleanupTask(customCleanup)
137+
}
138+
```
139+
140+
### Utility types
141+
142+
This module exports various utility types from
143+
`@testing-library/svelte-core/types`. They adapt to whatever Svelte version is
144+
installed, and can be used to get type signatures for imported components,
145+
props, events, etc.
146+
147+
See [`./types.d.ts`](./types.d.ts) for the full list of types available.

packages/svelte-core/package.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@testing-library/svelte-core",
3+
"version": "0.0.0-semantically-released",
4+
"description": "Core rendering and cleanup logic for Svelte testing utilities.",
5+
"exports": {
6+
".": {
7+
"types": "./dist/index.d.ts",
8+
"svelte": "./src/index.js",
9+
"default": "./src/index.js"
10+
},
11+
"./types": {
12+
"types": "./types.d.ts"
13+
}
14+
},
15+
"type": "module",
16+
"license": "MIT",
17+
"homepage": "https://github.com/testing-library/svelte-testing-library#readme",
18+
"repository": {
19+
"type": "git",
20+
"url": "git+https://github.com/testing-library/svelte-testing-library.git",
21+
"directory": "packages/svelte-core"
22+
},
23+
"bugs": {
24+
"url": "https://github.com/testing-library/svelte-testing-library/issues"
25+
},
26+
"engines": {
27+
"node": ">=16"
28+
},
29+
"keywords": [
30+
"testing",
31+
"svelte",
32+
"ui",
33+
"dom",
34+
"jsdom",
35+
"unit",
36+
"integration",
37+
"functional",
38+
"end-to-end",
39+
"e2e"
40+
],
41+
"files": [
42+
"dist",
43+
"src",
44+
"types.d.ts"
45+
],
46+
"peerDependencies": {
47+
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
48+
},
49+
"publishConfig": {
50+
"access": "public",
51+
"provenance": true
52+
}
53+
}
File renamed without changes.
File renamed without changes.

packages/svelte/src/core/mount.js renamed to packages/svelte-core/src/mount.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,18 @@ import { addCleanupTask, removeCleanupTask } from './cleanup.js'
77
import { createProps } from './props.svelte.js'
88
import { IS_MODERN_SVELTE } from './svelte-version.js'
99

10-
/** Mount a modern Svelte 5 component into the DOM. */
10+
/**
11+
* Mount a modern Svelte 5 component into the DOM.
12+
*
13+
* @template {import('../types.js').Component} C
14+
* @param {import('../types.js').ComponentType<C>} Component
15+
* @param {import('../types.js').MountOptions<C>} options
16+
* @returns {{
17+
* component: C
18+
* unmount: () => void
19+
* rerender: (props: Partial<import('../types.js').Props<C>>) => Promise<void>
20+
* }}
21+
*/
1122
const mountModern = (Component, options) => {
1223
const [props, updateProps] = createProps(options.props)
1324
const component = Svelte.mount(Component, { ...options, props })
@@ -29,7 +40,18 @@ const mountModern = (Component, options) => {
2940
return { component, unmount, rerender }
3041
}
3142

32-
/** Mount a legacy Svelte 3 or 4 component into the DOM. */
43+
/**
44+
* Mount a legacy Svelte 3 or 4 component into the DOM.
45+
*
46+
* @template {import('../types.js').LegacyComponent} C
47+
* @param {import('../types.js').ComponentType<C>} Component
48+
* @param {import('../types.js').MountOptions<C>} options
49+
* @returns {{
50+
* component: C
51+
* unmount: () => void
52+
* rerender: (props: Partial<import('../types.js').Props<C>>) => Promise<void>
53+
* }}
54+
*/
3355
const mountLegacy = (Component, options) => {
3456
const component = new Component(options)
3557

@@ -62,22 +84,32 @@ const mountComponent = IS_MODERN_SVELTE ? mountModern : mountLegacy
6284
/**
6385
* Render a Svelte component into the document.
6486
*
65-
* @template {import('./types.js').Component} C
66-
* @param {import('./types.js').ComponentType<C>} Component
67-
* @param {import('./types.js').MountOptions<C>} options
87+
* @template {import('../types.js').Component} C
88+
* @param {import('../types.js').ComponentImport<C>} Component
89+
* @param {import('../types.js').MountOptions<C>} options
6890
* @returns {{
6991
* component: C
7092
* unmount: () => void
71-
* rerender: (props: Partial<import('./types.js').Props<C>>) => Promise<void>
93+
* rerender: (props: Partial<import('../types.js').Props<C>>) => Promise<void>
7294
* }}
7395
*/
7496
const mount = (Component, options) => {
75-
const { component, unmount, rerender } = mountComponent(Component, options)
97+
const { component, unmount, rerender } = mountComponent(
98+
'default' in Component ? Component.default : Component,
99+
options
100+
)
76101

77102
return {
78103
component,
79104
unmount,
80105
rerender: async (props) => {
106+
if ('props' in props) {
107+
console.warn(
108+
'rerender({ props: { ... } }) deprecated, use rerender({ ... }) instead'
109+
)
110+
props = props.props
111+
}
112+
81113
rerender(props)
82114
// Await the next tick for Svelte 4, which cannot flush changes synchronously
83115
await Svelte.tick()

packages/svelte/src/core/props.svelte.js renamed to packages/svelte-core/src/props.svelte.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
* @param {Props} initialProps
99
* @returns {[Props, (nextProps: Partial<Props>) => void]}
1010
*/
11-
const createProps = (initialProps) => {
12-
const targetProps = initialProps ?? {}
13-
let currentProps = $state.raw(targetProps)
11+
const createProps = (initialProps = {}) => {
12+
let currentProps = $state.raw(initialProps)
1413

15-
const props = new Proxy(targetProps, {
14+
const props = new Proxy(initialProps, {
1615
get(_, key) {
1716
return currentProps[key]
1817
},

packages/svelte/src/core/setup.js renamed to packages/svelte-core/src/setup.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ class UnknownSvelteOptionsError extends TypeError {
2727
/**
2828
* Validate a component's mount options.
2929
*
30-
* @template {import('./types.js').Component} C
31-
* @param {import('./types.js').ComponentOptions<C>} options - props or mount options
32-
* @returns {Partial<import('./types.js').MountOptions<C>>}
30+
* @template {import('../types.js').Component} C
31+
* @param {import('../types.js').ComponentOptions<C>} options - props or mount options
32+
* @returns {Partial<import('../types.js').MountOptions<C>>}
3333
*/
3434
const validateOptions = (options) => {
3535
const isProps = !Object.keys(options).some((option) =>
@@ -55,16 +55,16 @@ const validateOptions = (options) => {
5555
/**
5656
* Set up the document to render a component.
5757
*
58-
* @template {import('./types.js').Component} C
59-
* @param {import('./types.js').ComponentOptions<C>} componentOptions - props or mount options
58+
* @template {import('../types.js').Component} C
59+
* @param {import('../types.js').ComponentOptions<C>} componentOptions - props or mount options
6060
* @param {{ baseElement?: HTMLElement | undefined }} setupOptions - base element of the document to bind any queries
6161
* @returns {{
6262
* baseElement: HTMLElement,
6363
* target: HTMLElement,
64-
* mountOptions: import('./types.js).MountOptions<C>
64+
* mountOptions: import('../types.js').MountOptions<C>
6565
* }}
6666
*/
67-
const setup = (componentOptions, setupOptions) => {
67+
const setup = (componentOptions, setupOptions = {}) => {
6868
const mountOptions = validateOptions(componentOptions)
6969

7070
const baseElement =

0 commit comments

Comments
 (0)