Skip to content

Commit e43f3c7

Browse files
authored
feat(core): create standalone core module (#460)
1 parent dc415ae commit e43f3c7

File tree

22 files changed

+525
-160
lines changed

22 files changed

+525
-160
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

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ jobs:
138138
- name: 🚀 Release
139139
run: |
140140
pnpm \
141-
--package="@anolilab/multi-semantic-release@2" \
142-
--package="@anolilab/semantic-release-pnpm@2" \
141+
--package="@anolilab/multi-semantic-release@3" \
142+
--package="@anolilab/semantic-release-pnpm@3" \
143143
--package="semantic-release@25" \
144144
--package="conventional-changelog-conventionalcommits@9" \
145145
dlx \

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: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
- [`render`](#render)
13+
- [`setup`](#setup)
14+
- [`mount`](#mount)
15+
- [`cleanup`](#cleanup)
16+
- [`addCleanupTask`](#addcleanuptask)
17+
- [`removeCleanupTask`](#removecleanuptask)
18+
- [Utility types](#utility-types)
19+
20+
## Example Usage
21+
22+
```ts
23+
import { beforeEach } from 'vitest'
24+
import * as SvelteCore from '@testing-library/svelte-core'
25+
26+
import { bindQueries, type Screen } from './bring-your-own-queries.js'
27+
28+
beforeEach(() => {
29+
SvelteCore.cleanup()
30+
})
31+
32+
export interface RenderResult<
33+
C extends SvelteCore.Component,
34+
> extends SvelteCore.RenderResult<C> {
35+
screen: Screen
36+
}
37+
38+
export const render = <C extends SvelteCore.Component>(
39+
Component: SvelteCore.ComponentImport<C>,
40+
options: SvelteCore.ComponentOptions<C>
41+
): RenderResult<C> => {
42+
const renderResult = SvelteCore.render(Component, options)
43+
const screen = bindQueries(baseElement)
44+
45+
return { screen, ...renderResult }
46+
}
47+
```
48+
49+
## API
50+
51+
### `render`
52+
53+
Set up the document and mount a component into that document.
54+
55+
```ts
56+
const { baseElement, container, component, unmount, rerender } = render(
57+
Component,
58+
componentOptions,
59+
setupOptions
60+
)
61+
```
62+
63+
| Argument | Type | Description |
64+
| ------------------ | ------------------------------------------------------- | --------------------------------------------- |
65+
| `Component` | [Svelte component][svelte-component-docs] | An imported Svelte component |
66+
| `componentOptions` | `Props` or partial [`mount` options][svelte-mount-docs] | Options for how the component will be mounted |
67+
| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` |
68+
69+
| Result | Type | Description | Default |
70+
| ------------- | ------------------------------------------ | ---------------------------------------- | ----------------------------------- |
71+
| `baseElement` | `HTMLElement` | The base element | `document.body` |
72+
| `container` | `HTMLElement` | The component's immediate parent element | `<div>` appended to `document.body` |
73+
| `component` | [component exports][svelte-mount-docs] | The component's exports from `mount` | N/A |
74+
| `rerender` | `(props: Partial<Props>) => Promise<void>` | Update the component's props | N/A |
75+
| `unmount` | `() => void` | Unmount the component from the document | N/A |
76+
77+
> \[!TIP]
78+
> Calling `render` is equivalent to calling `setup` followed by `mount`
79+
>
80+
> ```ts
81+
> const { baseElement, container, mountOptions } = setup(
82+
> componentOptions,
83+
> setupOptions
84+
> )
85+
> const { component, rerender, unmount } = mount(Component, mountOptions)
86+
> ```
87+
88+
[svelte-component-docs]: https://svelte.dev/docs/svelte-components
89+
[svelte-mount-docs]: https://svelte.dev/docs/svelte/imperative-component-api#mount
90+
91+
### `setup`
92+
93+
Validate options and prepare document elements for rendering.
94+
95+
```ts
96+
const { baseElement, target, mountOptions } = setup(options, renderOptions)
97+
```
98+
99+
| Argument | Type | Description |
100+
| ------------------ | ------------------------------------------------------- | --------------------------------------------- |
101+
| `componentOptions` | `Props` or partial [`mount` options][svelte-mount-docs] | Options for how the component will be mounted |
102+
| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` |
103+
104+
| Result | Type | Description | Default |
105+
| -------------- | ------------------------------------ | ---------------------------------------- | ----------------------------------- |
106+
| `baseElement` | `HTMLElement` | The base element | `document.body` |
107+
| `container` | `HTMLElement` | The component's immediate parent element | `<div>` appended to `document.body` |
108+
| `mountOptions` | [`mount` options][svelte-mount-docs] | Validated options to pass to `mount` | `{ target, props: {} }` |
109+
110+
### `mount`
111+
112+
Mount a Svelte component into the document.
113+
114+
```ts
115+
const { component, unmount, rerender } = mount(Component, options)
116+
```
117+
118+
| Argument | Type | Description |
119+
| -------------- | ----------------------------------------- | -------------------------------------------- |
120+
| `Component` | [Svelte component][svelte-component-docs] | An imported Svelte component |
121+
| `mountOptions` | [component options][svelte-mount-docs] | Options to pass to Svelte's `mount` function |
122+
123+
| Result | Type | Description |
124+
| ----------- | ------------------------------------------ | --------------------------------------- |
125+
| `component` | [component exports][svelte-mount-docs] | The component's exports from `mount` |
126+
| `unmount` | `() => void` | Unmount the component from the document |
127+
| `rerender` | `(props: Partial<Props>) => Promise<void>` | Update the component's props |
128+
129+
### `cleanup`
130+
131+
Cleanup rendered components and added elements. Call this when your tests are
132+
over.
133+
134+
```ts
135+
cleanup()
136+
```
137+
138+
### `addCleanupTask`
139+
140+
Add a custom cleanup task to be called with `cleanup()`
141+
142+
```ts
143+
addCleanupTask(() => {
144+
// ...reset something
145+
})
146+
```
147+
148+
### `removeCleanupTask`
149+
150+
Remove a cleanup task from `cleanup()`. Useful if a cleanup task can only be run
151+
once and may be run outside of `cleanup`
152+
153+
```ts
154+
const customCleanup = () => {
155+
// ...reset something
156+
}
157+
158+
addCleanupTask(customCleanup)
159+
160+
const manuallyCleanupEarly = () => {
161+
customCleanup()
162+
removeCleanupTask(customCleanup)
163+
}
164+
```
165+
166+
### Utility types
167+
168+
This module exports various utility types from
169+
`@testing-library/svelte-core/types`. They adapt to whatever Svelte version is
170+
installed, and can be used to get type signatures for imported components,
171+
props, exports, etc.
172+
173+
See [`./types.d.ts`](./types.d.ts) for the full list of available types.

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.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
*/
88
export * from './cleanup.js'
99
export * from './mount.js'
10+
export * from './render.js'
1011
export * from './setup.js'

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ 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 {import('../types.js').MountResult<C>}
17+
*/
1118
const mountModern = (Component, options) => {
1219
const [props, updateProps] = createProps(options.props)
1320
const component = Svelte.mount(Component, { ...options, props })
@@ -29,7 +36,14 @@ const mountModern = (Component, options) => {
2936
return { component, unmount, rerender }
3037
}
3138

32-
/** Mount a legacy Svelte 3 or 4 component into the DOM. */
39+
/**
40+
* Mount a legacy Svelte 3 or 4 component into the DOM.
41+
*
42+
* @template {import('../types.js').LegacyComponent} C
43+
* @param {import('../types.js').ComponentType<C>} Component
44+
* @param {import('../types.js').MountOptions<C>} options
45+
* @returns {import('../types.js').MountResult<C>}
46+
*/
3347
const mountLegacy = (Component, options) => {
3448
const component = new Component(options)
3549

@@ -62,24 +76,30 @@ const mountComponent = IS_MODERN_SVELTE ? mountModern : mountLegacy
6276
/**
6377
* Render a Svelte component into the document.
6478
*
65-
* @template {import('./types.js').Component} C
66-
* @param {import('./types.js').ComponentType<C>} Component
67-
* @param {import('./types.js').MountOptions<C>} options
68-
* @returns {{
69-
* component: C
70-
* unmount: () => void
71-
* rerender: (props: Partial<import('./types.js').Props<C>>) => Promise<void>
72-
* }}
79+
* @template {import('../types.js').Component} C
80+
* @param {import('../types.js').ComponentImport<C>} Component
81+
* @param {import('../types.js').MountOptions<C>} options
82+
* @returns {import('../types.js').MountResult<C>}
7383
*/
7484
const mount = (Component, options) => {
75-
const { component, unmount, rerender } = mountComponent(Component, options)
85+
const { component, unmount, rerender } = mountComponent(
86+
'default' in Component ? Component.default : Component,
87+
options
88+
)
7689

7790
return {
7891
component,
7992
unmount,
8093
rerender: async (props) => {
94+
if ('props' in props) {
95+
console.warn(
96+
'rerender({ props: { ... } }) deprecated, use rerender({ ... }) instead'
97+
)
98+
props = props.props
99+
}
100+
81101
rerender(props)
82-
// Await the next tick for Svelte 4, which cannot flush changes synchronously
102+
// Await the next tick for Svelte 3/4, which cannot flush changes synchronously
83103
await Svelte.tick()
84104
},
85105
}

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-core/src/render.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { mount } from './mount.js'
2+
import { setup } from './setup.js'
3+
4+
/**
5+
* Render a component into the document.
6+
*
7+
* @template {import('../types.js').Component} C
8+
*
9+
* @param {import('../types.js').ComponentImport<C>} Component - The component to render.
10+
* @param {import('../types.js').ComponentOptions<C>} componentOptions - Customize how Svelte renders the component.
11+
* @param {import('../types.js').SetupOptions<C>} setupOptions - Customize how the document is set up.
12+
* @returns {import('../types.js').RenderResult<C>} The rendered component.
13+
*/
14+
const render = (Component, componentOptions, setupOptions = {}) => {
15+
const { mountOptions, ...setupResult } = setup(componentOptions, setupOptions)
16+
const mountResult = mount(Component, mountOptions)
17+
18+
return { ...setupResult, ...mountResult }
19+
}
20+
21+
export { render }

0 commit comments

Comments
 (0)