@testing-library/svelte 5.0.1 → 5.2.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <p>Simple and complete Svelte testing utilities that encourage good testing practices.</p>
14
14
 
15
15
  [**Read The Docs**](https://testing-library.com/docs/svelte-testing-library/intro) |
16
- [Edit the docs](https://github.com/alexkrolick/testing-library-docs)
16
+ [Edit the docs](https://github.com/testing-library/testing-library-docs)
17
17
 
18
18
  <!-- prettier-ignore-start -->
19
19
  [![Build Status][build-badge]][build]
@@ -71,29 +71,36 @@ primary guiding principle is:
71
71
  This module is distributed via [npm][npm] which is bundled with [node][node] and
72
72
  should be installed as one of your project's `devDependencies`:
73
73
 
74
- ```
74
+ ```shell
75
75
  npm install --save-dev @testing-library/svelte
76
76
  ```
77
77
 
78
- This library has `peerDependencies` listings for `svelte >= 3`.
78
+ This library supports `svelte` versions `3`, `4`, and `5`.
79
79
 
80
80
  You may also be interested in installing `@testing-library/jest-dom` so you can use
81
81
  [the custom jest matchers](https://github.com/testing-library/jest-dom).
82
82
 
83
- ### Svelte 5 support
83
+ ## Setup
84
84
 
85
- If you are riding the bleeding edge of Svelte 5, you'll need to either
86
- import from `@testing-library/svelte/svelte5` instead of `@testing-library/svelte`, or have your `vite.config.js` contains the following alias:
85
+ We recommend using `@testing-library/svelte` with [Vitest][] as your test runner. To get started, add the `svelteTesting` plugin to your Vite or Vitest config.
87
86
 
87
+ ```diff
88
+ // vite.config.js
89
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
90
+ + import { svelteTesting } from '@testing-library/svelte/vite'
91
+
92
+ export default defineConfig({
93
+ plugins: [
94
+ svelte(),
95
+ + svelteTesting(),
96
+ ]
97
+ });
88
98
  ```
89
- export default defineConfig(({ }) => ({
90
- test: {
91
- alias: {
92
- '@testing-library/svelte': '@testing-library/svelte/svelte5'
93
- }
94
- },
95
- }))
96
- ```
99
+
100
+ See the [setup docs][] for more detailed setup instructions, including for other test runners like Jest.
101
+
102
+ [vitest]: https://vitest.dev/
103
+ [setup docs]: https://testing-library.com/docs/svelte-testing-library/setup
97
104
 
98
105
  ## Docs
99
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testing-library/svelte",
3
- "version": "5.0.1",
3
+ "version": "5.2.0-next.1",
4
4
  "description": "Simple and complete Svelte testing utilities that encourage good testing practices.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -10,10 +10,14 @@
10
10
  },
11
11
  "./svelte5": {
12
12
  "types": "./types/index.d.ts",
13
- "default": "./src/svelte5-index.js"
13
+ "default": "./src/index.js"
14
14
  },
15
15
  "./vitest": {
16
16
  "default": "./src/vitest.js"
17
+ },
18
+ "./vite": {
19
+ "types": "./types/vite.d.ts",
20
+ "default": "./src/vite.js"
17
21
  }
18
22
  },
19
23
  "type": "module",
@@ -43,8 +47,10 @@
43
47
  "e2e"
44
48
  ],
45
49
  "files": [
46
- "src/",
47
- "types/index.d.ts"
50
+ "src",
51
+ "types",
52
+ "!*.test-d.ts",
53
+ "!__tests__"
48
54
  ],
49
55
  "scripts": {
50
56
  "toc": "doctoc README.md",
@@ -59,50 +65,64 @@
59
65
  "setup": "npm install && npm run validate",
60
66
  "test": "vitest run --coverage",
61
67
  "test:watch": "vitest",
62
- "test:update": "vitest run --update",
63
68
  "test:vitest:jsdom": "vitest run --coverage --environment jsdom",
64
69
  "test:vitest:happy-dom": "vitest run --coverage --environment happy-dom",
70
+ "test:jest": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --coverage",
65
71
  "types": "svelte-check",
66
- "validate": "npm-run-all test:vitest:* types",
72
+ "validate": "npm-run-all test:vitest:* test:jest types",
67
73
  "contributors:add": "all-contributors add",
68
- "contributors:generate": "all-contributors generate"
74
+ "contributors:generate": "all-contributors generate",
75
+ "preview-release": "./scripts/preview-release"
69
76
  },
70
77
  "peerDependencies": {
71
- "svelte": "^3 || ^4 || ^5"
78
+ "svelte": "^3 || ^4 || ^5",
79
+ "vite": "*",
80
+ "vitest": "*"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "vite": {
84
+ "optional": true
85
+ },
86
+ "vitest": {
87
+ "optional": true
88
+ }
72
89
  },
73
90
  "dependencies": {
74
- "@testing-library/dom": "^9.3.1"
91
+ "@testing-library/dom": "^10.0.0"
75
92
  },
76
93
  "devDependencies": {
77
- "@sveltejs/vite-plugin-svelte": "^3.0.2",
94
+ "@jest/globals": "^29.7.0",
95
+ "@sveltejs/vite-plugin-svelte": "^3.1.1",
78
96
  "@testing-library/jest-dom": "^6.3.0",
79
97
  "@testing-library/user-event": "^14.5.2",
80
- "@typescript-eslint/eslint-plugin": "6.19.1",
81
- "@typescript-eslint/parser": "6.19.1",
82
- "@vitest/coverage-v8": "^0.33.0",
98
+ "@typescript-eslint/eslint-plugin": "7.8.0",
99
+ "@typescript-eslint/parser": "7.8.0",
100
+ "@vitest/coverage-v8": "^1.5.2",
83
101
  "all-contributors-cli": "^6.26.1",
84
102
  "doctoc": "^2.2.1",
85
- "eslint": "8.56.0",
103
+ "eslint": "8.57.0",
86
104
  "eslint-config-prettier": "9.1.0",
87
105
  "eslint-config-standard": "17.1.0",
88
106
  "eslint-plugin-import": "2.29.1",
89
107
  "eslint-plugin-json-files": "^4.1.0",
90
108
  "eslint-plugin-n": "16.6.2",
91
109
  "eslint-plugin-promise": "6.1.1",
92
- "eslint-plugin-simple-import-sort": "10.0.0",
93
- "eslint-plugin-svelte": "2.35.1",
94
- "eslint-plugin-vitest-globals": "1.4.0",
95
- "expect-type": "^0.17.3",
110
+ "eslint-plugin-simple-import-sort": "12.1.0",
111
+ "eslint-plugin-svelte": "2.38.0",
112
+ "eslint-plugin-vitest-globals": "1.5.0",
113
+ "expect-type": "^0.19.0",
96
114
  "happy-dom": "^14.7.1",
97
- "jsdom": "^22.1.0",
115
+ "jest": "^29.7.0",
116
+ "jest-environment-jsdom": "^29.7.0",
117
+ "jsdom": "^24.0.0",
98
118
  "npm-run-all": "^4.1.5",
99
- "prettier": "3.2.4",
100
- "prettier-plugin-svelte": "3.1.2",
119
+ "prettier": "3.2.5",
120
+ "prettier-plugin-svelte": "3.2.3",
101
121
  "svelte": "^3 || ^4 || ^5",
102
122
  "svelte-check": "^3.6.3",
103
- "svelte-jester": "^3.0.0",
123
+ "svelte-jester": "^5.0.0",
104
124
  "typescript": "^5.3.3",
105
125
  "vite": "^5.1.1",
106
- "vitest": "^0.33.0"
126
+ "vitest": "^1.5.2"
107
127
  }
108
128
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Rendering core for svelte-testing-library.
3
+ *
4
+ * Defines how components are added to and removed from the DOM.
5
+ * Will switch to legacy, class-based mounting logic
6
+ * if it looks like we're in a Svelte <= 4 environment.
7
+ */
8
+ import * as LegacyCore from './legacy.js'
9
+ import * as ModernCore from './modern.svelte.js'
10
+ import {
11
+ createValidateOptions,
12
+ UnknownSvelteOptionsError,
13
+ } from './validate-options.js'
14
+
15
+ const { mount, unmount, updateProps, allowedOptions } =
16
+ ModernCore.IS_MODERN_SVELTE ? ModernCore : LegacyCore
17
+
18
+ /** Validate component options. */
19
+ const validateOptions = createValidateOptions(allowedOptions)
20
+
21
+ export {
22
+ mount,
23
+ UnknownSvelteOptionsError,
24
+ unmount,
25
+ updateProps,
26
+ validateOptions,
27
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Legacy rendering core for svelte-testing-library.
3
+ *
4
+ * Supports Svelte <= 4.
5
+ */
6
+
7
+ /** Allowed options for the component constructor. */
8
+ const allowedOptions = [
9
+ 'target',
10
+ 'accessors',
11
+ 'anchor',
12
+ 'props',
13
+ 'hydrate',
14
+ 'intro',
15
+ 'context',
16
+ ]
17
+
18
+ /**
19
+ * Mount the component into the DOM.
20
+ *
21
+ * The `onDestroy` callback is included for strict backwards compatibility
22
+ * with previous versions of this library. It's mostly unnecessary logic.
23
+ */
24
+ const mount = (Component, options, onDestroy) => {
25
+ const component = new Component(options)
26
+
27
+ if (typeof onDestroy === 'function') {
28
+ component.$$.on_destroy.push(() => {
29
+ onDestroy(component)
30
+ })
31
+ }
32
+
33
+ return component
34
+ }
35
+
36
+ /** Remove the component from the DOM. */
37
+ const unmount = (component) => {
38
+ component.$destroy()
39
+ }
40
+
41
+ /** Update the component's props. */
42
+ const updateProps = (component, nextProps) => {
43
+ component.$set(nextProps)
44
+ }
45
+
46
+ export { allowedOptions, mount, unmount, updateProps }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Modern rendering core for svelte-testing-library.
3
+ *
4
+ * Supports Svelte >= 5.
5
+ */
6
+ import * as Svelte from 'svelte'
7
+
8
+ /** Props signals for each rendered component. */
9
+ const propsByComponent = new Map()
10
+
11
+ /** Whether we're using Svelte >= 5. */
12
+ const IS_MODERN_SVELTE = typeof Svelte.mount === 'function'
13
+
14
+ /** Allowed options to the `mount` call. */
15
+ const allowedOptions = [
16
+ 'target',
17
+ 'anchor',
18
+ 'props',
19
+ 'events',
20
+ 'context',
21
+ 'intro',
22
+ ]
23
+
24
+ /** Mount the component into the DOM. */
25
+ const mount = (Component, options) => {
26
+ const props = $state(options.props ?? {})
27
+ const component = Svelte.mount(Component, { ...options, props })
28
+
29
+ propsByComponent.set(component, props)
30
+
31
+ return component
32
+ }
33
+
34
+ /** Remove the component from the DOM. */
35
+ const unmount = (component) => {
36
+ propsByComponent.delete(component)
37
+ Svelte.unmount(component)
38
+ }
39
+
40
+ /**
41
+ * Update the component's props.
42
+ *
43
+ * Relies on the `$state` signal added in `mount`.
44
+ */
45
+ const updateProps = (component, nextProps) => {
46
+ const prevProps = propsByComponent.get(component)
47
+ Object.assign(prevProps, nextProps)
48
+ }
49
+
50
+ export { allowedOptions, IS_MODERN_SVELTE, mount, unmount, updateProps }
@@ -0,0 +1,39 @@
1
+ class UnknownSvelteOptionsError extends TypeError {
2
+ constructor(unknownOptions, allowedOptions) {
3
+ super(`Unknown options.
4
+
5
+ Unknown: [ ${unknownOptions.join(', ')} ]
6
+ Allowed: [ ${allowedOptions.join(', ')} ]
7
+
8
+ To pass both Svelte options and props to a component,
9
+ or to use props that share a name with a Svelte option,
10
+ you must place all your props under the \`props\` key:
11
+
12
+ render(Component, { props: { /** props here **/ } })
13
+ `)
14
+ this.name = 'UnknownSvelteOptionsError'
15
+ }
16
+ }
17
+
18
+ const createValidateOptions = (allowedOptions) => (options) => {
19
+ const isProps = !Object.keys(options).some((option) =>
20
+ allowedOptions.includes(option)
21
+ )
22
+
23
+ if (isProps) {
24
+ return { props: options }
25
+ }
26
+
27
+ // Check if any props and Svelte options were accidentally mixed.
28
+ const unknownOptions = Object.keys(options).filter(
29
+ (option) => !allowedOptions.includes(option)
30
+ )
31
+
32
+ if (unknownOptions.length > 0) {
33
+ throw new UnknownSvelteOptionsError(unknownOptions, allowedOptions)
34
+ }
35
+
36
+ return options
37
+ }
38
+
39
+ export { createValidateOptions, UnknownSvelteOptionsError }
package/src/index.js CHANGED
@@ -4,8 +4,7 @@ import { act, cleanup } from './pure.js'
4
4
  // If we're running in a test runner that supports afterEach
5
5
  // then we'll automatically run cleanup afterEach test
6
6
  // this ensures that tests run in isolation from each other
7
- // if you don't like this then either import the `pure` module
8
- // or set the STL_SKIP_AUTO_CLEANUP env variable to 'true'.
7
+ // if you don't like this then set the STL_SKIP_AUTO_CLEANUP env variable.
9
8
  if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
10
9
  afterEach(async () => {
11
10
  await act()
@@ -18,4 +17,10 @@ export * from '@testing-library/dom'
18
17
 
19
18
  // export svelte-specific functions and custom `fireEvent`
20
19
  // `fireEvent` must be a named export to take priority over wildcard export above
21
- export { act, cleanup, fireEvent, render } from './pure.js'
20
+ export {
21
+ act,
22
+ cleanup,
23
+ fireEvent,
24
+ render,
25
+ UnknownSvelteOptionsError,
26
+ } from './pure.js'
package/src/pure.js CHANGED
@@ -3,155 +3,105 @@ import {
3
3
  getQueriesForElement,
4
4
  prettyDOM,
5
5
  } from '@testing-library/dom'
6
- import * as Svelte from 'svelte'
7
- import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
8
-
9
- const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION)
10
-
11
- export class SvelteTestingLibrary {
12
- svelteComponentOptions = [
13
- 'target',
14
- 'accessors',
15
- 'anchor',
16
- 'props',
17
- 'hydrate',
18
- 'intro',
19
- 'context',
20
- ]
21
-
22
- targetCache = new Set()
23
- componentCache = new Set()
24
-
25
- checkProps(options) {
26
- const isProps = !Object.keys(options).some((option) =>
27
- this.svelteComponentOptions.includes(option)
28
- )
29
-
30
- // Check if any props and Svelte options were accidentally mixed.
31
- if (!isProps) {
32
- const unrecognizedOptions = Object.keys(options).filter(
33
- (option) => !this.svelteComponentOptions.includes(option)
34
- )
35
-
36
- if (unrecognizedOptions.length > 0) {
37
- throw Error(`
38
- Unknown options were found [${unrecognizedOptions}]. This might happen if you've mixed
39
- passing in props with Svelte options into the render function. Valid Svelte options
40
- are [${this.svelteComponentOptions}]. You can either change the prop names, or pass in your
41
- props for that component via the \`props\` option.\n\n
42
- Eg: const { /** Results **/ } = render(MyComponent, { props: { /** props here **/ } })\n\n
43
- `)
44
- }
45
-
46
- return options
47
- }
48
-
49
- return { props: options }
50
- }
51
-
52
- render(Component, componentOptions = {}, renderOptions = {}) {
53
- componentOptions = this.checkProps(componentOptions)
54
-
55
- const baseElement =
56
- renderOptions.baseElement ?? componentOptions.target ?? document.body
57
-
58
- const target =
59
- componentOptions.target ??
60
- baseElement.appendChild(document.createElement('div'))
61
-
62
- this.targetCache.add(target)
63
-
64
- const ComponentConstructor = Component.default || Component
65
-
66
- const component = this.renderComponent(ComponentConstructor, {
67
- ...componentOptions,
68
- target,
69
- })
70
-
71
- return {
72
- baseElement,
73
- component,
74
- container: target,
75
- debug: (el = baseElement) => console.log(prettyDOM(el)),
76
- rerender: async (props) => {
77
- if (props.props) {
78
- console.warn(
79
- 'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
80
- )
81
- props = props.props
82
- }
83
- component.$set(props)
84
- await Svelte.tick()
85
- },
86
- unmount: () => {
87
- this.cleanupComponent(component)
88
- },
89
- ...getQueriesForElement(baseElement, renderOptions.queries),
90
- }
91
- }
92
-
93
- renderComponent(ComponentConstructor, componentOptions) {
94
- if (IS_SVELTE_5) {
95
- throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`')
96
- }
97
-
98
- const component = new ComponentConstructor(componentOptions)
6
+ import { tick } from 'svelte'
99
7
 
100
- this.componentCache.add(component)
101
-
102
- // TODO(mcous, 2024-02-11): remove this behavior in the next major version
103
- component.$$.on_destroy.push(() => {
104
- this.componentCache.delete(component)
105
- })
8
+ import {
9
+ mount,
10
+ UnknownSvelteOptionsError,
11
+ unmount,
12
+ updateProps,
13
+ validateOptions,
14
+ } from './core/index.js'
15
+
16
+ const targetCache = new Set()
17
+ const componentCache = new Set()
18
+
19
+ const render = (Component, options = {}, renderOptions = {}) => {
20
+ options = validateOptions(options)
21
+
22
+ const baseElement =
23
+ renderOptions.baseElement ?? options.target ?? document.body
24
+
25
+ const queries = getQueriesForElement(baseElement, renderOptions.queries)
26
+
27
+ const target =
28
+ options.target ?? baseElement.appendChild(document.createElement('div'))
29
+
30
+ targetCache.add(target)
31
+
32
+ const component = mount(
33
+ Component.default ?? Component,
34
+ { ...options, target },
35
+ cleanupComponent
36
+ )
37
+
38
+ componentCache.add(component)
39
+
40
+ return {
41
+ baseElement,
42
+ component,
43
+ container: target,
44
+ debug: (el = baseElement) => {
45
+ console.log(prettyDOM(el))
46
+ },
47
+ rerender: async (props) => {
48
+ if (props.props) {
49
+ console.warn(
50
+ 'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
51
+ )
52
+ props = props.props
53
+ }
106
54
 
107
- return component
55
+ updateProps(component, props)
56
+ await tick()
57
+ },
58
+ unmount: () => {
59
+ cleanupComponent(component)
60
+ },
61
+ ...queries,
108
62
  }
63
+ }
109
64
 
110
- cleanupComponent(component) {
111
- const inCache = this.componentCache.delete(component)
65
+ const cleanupComponent = (component) => {
66
+ const inCache = componentCache.delete(component)
112
67
 
113
- if (inCache) {
114
- component.$destroy()
115
- }
68
+ if (inCache) {
69
+ unmount(component)
116
70
  }
71
+ }
117
72
 
118
- cleanupTarget(target) {
119
- const inCache = this.targetCache.delete(target)
120
-
121
- if (inCache && target.parentNode === document.body) {
122
- document.body.removeChild(target)
123
- }
124
- }
73
+ const cleanupTarget = (target) => {
74
+ const inCache = targetCache.delete(target)
125
75
 
126
- cleanup() {
127
- this.componentCache.forEach(this.cleanupComponent.bind(this))
128
- this.targetCache.forEach(this.cleanupTarget.bind(this))
76
+ if (inCache && target.parentNode === document.body) {
77
+ document.body.removeChild(target)
129
78
  }
130
79
  }
131
80
 
132
- const instance = new SvelteTestingLibrary()
133
-
134
- export const render = instance.render.bind(instance)
135
-
136
- export const cleanup = instance.cleanup.bind(instance)
81
+ const cleanup = () => {
82
+ componentCache.forEach(cleanupComponent)
83
+ targetCache.forEach(cleanupTarget)
84
+ }
137
85
 
138
- export const act = async (fn) => {
86
+ const act = async (fn) => {
139
87
  if (fn) {
140
88
  await fn()
141
89
  }
142
- return Svelte.tick()
90
+ return tick()
143
91
  }
144
92
 
145
- export const fireEvent = async (...args) => {
93
+ const fireEvent = async (...args) => {
146
94
  const event = dtlFireEvent(...args)
147
- await Svelte.tick()
95
+ await tick()
148
96
  return event
149
97
  }
150
98
 
151
99
  Object.keys(dtlFireEvent).forEach((key) => {
152
100
  fireEvent[key] = async (...args) => {
153
101
  const event = dtlFireEvent[key](...args)
154
- await Svelte.tick()
102
+ await tick()
155
103
  return event
156
104
  }
157
105
  })
106
+
107
+ export { act, cleanup, fireEvent, render, UnknownSvelteOptionsError }
package/src/vite.js ADDED
@@ -0,0 +1,75 @@
1
+ import { dirname, join } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ /**
5
+ * Vite plugin to configure @testing-library/svelte.
6
+ *
7
+ * Ensures Svelte is imported correctly in tests
8
+ * and that the DOM is cleaned up after each test.
9
+ *
10
+ * @param {{resolveBrowser?: boolean, autoCleanup?: boolean}} options
11
+ * @returns {import('vite').Plugin}
12
+ */
13
+ export const svelteTesting = ({
14
+ resolveBrowser = true,
15
+ autoCleanup = true,
16
+ } = {}) => ({
17
+ name: 'vite-plugin-svelte-testing-library',
18
+ config: (config) => {
19
+ if (!process.env.VITEST) {
20
+ return
21
+ }
22
+
23
+ if (resolveBrowser) {
24
+ addBrowserCondition(config)
25
+ }
26
+
27
+ if (autoCleanup) {
28
+ addAutoCleanup(config)
29
+ }
30
+ },
31
+ })
32
+
33
+ /**
34
+ * Add `browser` to `resolve.conditions` before `node`.
35
+ *
36
+ * This ensures that Svelte's browser code is used in tests,
37
+ * rather than its SSR code.
38
+ *
39
+ * @param {import('vitest/config').UserConfig} config
40
+ */
41
+ const addBrowserCondition = (config) => {
42
+ const resolve = config.resolve ?? {}
43
+ const conditions = resolve.conditions ?? []
44
+ const nodeConditionIndex = conditions.indexOf('node')
45
+ const browserConditionIndex = conditions.indexOf('browser')
46
+
47
+ if (
48
+ nodeConditionIndex >= 0 &&
49
+ (nodeConditionIndex < browserConditionIndex || browserConditionIndex < 0)
50
+ ) {
51
+ conditions.splice(nodeConditionIndex, 0, 'browser')
52
+ }
53
+
54
+ resolve.conditions = conditions
55
+ config.resolve = resolve
56
+ }
57
+
58
+ /**
59
+ * Add auto-cleanup file to Vitest's setup files.
60
+ *
61
+ * @param {import('vitest/config').UserConfig} config
62
+ */
63
+ const addAutoCleanup = (config) => {
64
+ const test = config.test ?? {}
65
+ let setupFiles = test.setupFiles ?? []
66
+
67
+ if (typeof setupFiles === 'string') {
68
+ setupFiles = [setupFiles]
69
+ }
70
+
71
+ setupFiles.push(join(dirname(fileURLToPath(import.meta.url)), './vitest.js'))
72
+
73
+ test.setupFiles = setupFiles
74
+ config.test = test
75
+ }
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from 'vite'
2
+
3
+ /**
4
+ * Vite plugin to configure @testing-library/svelte.
5
+ *
6
+ * Ensures Svelte is imported correctly in tests
7
+ * and that the DOM is cleaned up after each test.
8
+ */
9
+ export function svelteTesting(options?: {
10
+ resolveBrowser?: boolean
11
+ autoCleanup?: boolean
12
+ }): Plugin
@@ -1,3 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`auto-cleanup-skip > second 1`] = `""`;
@@ -1,2 +0,0 @@
1
- import '@testing-library/jest-dom/vitest'
2
- import '../vitest'
@@ -1,33 +0,0 @@
1
- import { setTimeout } from 'node:timers/promises'
2
-
3
- import { act, render } from '@testing-library/svelte'
4
- import { describe, expect, test } from 'vitest'
5
-
6
- import Comp from './fixtures/Comp.svelte'
7
-
8
- describe('act', () => {
9
- test('state updates are flushed', async () => {
10
- const { getByText } = render(Comp)
11
- const button = getByText('Button')
12
-
13
- expect(button).toHaveTextContent('Button')
14
-
15
- await act(() => {
16
- button.click()
17
- })
18
-
19
- expect(button).toHaveTextContent('Button Clicked')
20
- })
21
-
22
- test('accepts async functions', async () => {
23
- const { getByText } = render(Comp)
24
- const button = getByText('Button')
25
-
26
- await act(async () => {
27
- await setTimeout(100)
28
- button.click()
29
- })
30
-
31
- expect(button).toHaveTextContent('Button Clicked')
32
- })
33
- })
@@ -1,23 +0,0 @@
1
- import { beforeAll, describe, expect, test } from 'vitest'
2
-
3
- import Comp from './fixtures/Comp.svelte'
4
-
5
- describe('auto-cleanup-skip', () => {
6
- let render
7
-
8
- beforeAll(async () => {
9
- process.env.STL_SKIP_AUTO_CLEANUP = 'true'
10
- const stl = await import('@testing-library/svelte')
11
- render = stl.render
12
- })
13
-
14
- // This one verifies that if STL_SKIP_AUTO_CLEANUP is set
15
- // then we DON'T auto-wire up the afterEach for folks
16
- test('first', () => {
17
- render(Comp, { props: { name: 'world' } })
18
- })
19
-
20
- test('second', () => {
21
- expect(document.body.innerHTML).toMatchSnapshot()
22
- })
23
- })
@@ -1,31 +0,0 @@
1
- import { render } from '@testing-library/svelte'
2
- import { describe, expect, test } from 'vitest'
3
-
4
- import Comp from './fixtures/Comp.svelte'
5
-
6
- describe('auto-cleanup', () => {
7
- // This just verifies that by importing STL in an
8
- // environment which supports afterEach (like jest)
9
- // we'll get automatic cleanup between tests.
10
- test('first', () => {
11
- render(Comp, { props: { name: 'world' } })
12
- })
13
-
14
- test('second', () => {
15
- expect(document.body.innerHTML).toEqual('')
16
- })
17
- })
18
-
19
- describe('cleanup of two components', () => {
20
- // This just verifies that by importing STL in an
21
- // environment which supports afterEach (like jest)
22
- // we'll get automatic cleanup between tests.
23
- test('first', () => {
24
- render(Comp, { props: { name: 'world' } })
25
- render(Comp, { props: { name: 'universe' } })
26
- })
27
-
28
- test('second', () => {
29
- expect(document.body.innerHTML).toEqual('')
30
- })
31
- })
@@ -1,35 +0,0 @@
1
- import { cleanup, render } from '@testing-library/svelte'
2
- import { describe, expect, test, vi } from 'vitest'
3
-
4
- import Mounter from './fixtures/Mounter.svelte'
5
-
6
- const onExecuted = vi.fn()
7
- const onDestroyed = vi.fn()
8
- const renderSubject = () => render(Mounter, { onExecuted, onDestroyed })
9
-
10
- describe('cleanup', () => {
11
- test('cleanup deletes element', async () => {
12
- renderSubject()
13
- cleanup()
14
-
15
- expect(document.body).toBeEmptyDOMElement()
16
- })
17
-
18
- test('cleanup unmounts component', () => {
19
- renderSubject()
20
- cleanup()
21
-
22
- expect(onDestroyed).toHaveBeenCalledOnce()
23
- })
24
-
25
- test('cleanup handles unexpected errors during mount', () => {
26
- onExecuted.mockImplementation(() => {
27
- throw new Error('oh no!')
28
- })
29
-
30
- expect(renderSubject).toThrowError()
31
- cleanup()
32
-
33
- expect(document.body).toBeEmptyDOMElement()
34
- })
35
- })
@@ -1,14 +0,0 @@
1
- import { render } from '@testing-library/svelte'
2
- import { expect, test } from 'vitest'
3
-
4
- import Comp from './fixtures/Context.svelte'
5
-
6
- test('can set a context', () => {
7
- const message = 'Got it'
8
-
9
- const { getByText } = render(Comp, {
10
- context: new Map(Object.entries({ foo: { message } })),
11
- })
12
-
13
- expect(getByText(message)).toBeTruthy()
14
- })
@@ -1,18 +0,0 @@
1
- import { prettyDOM } from '@testing-library/dom'
2
- import { render } from '@testing-library/svelte'
3
- import { describe, expect, test, vi } from 'vitest'
4
-
5
- import Comp from './fixtures/Comp.svelte'
6
-
7
- describe('debug', () => {
8
- test('pretty prints the base element', () => {
9
- vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() })
10
-
11
- const { baseElement, debug } = render(Comp, { props: { name: 'world' } })
12
-
13
- debug()
14
-
15
- expect(console.log).toHaveBeenCalledTimes(1)
16
- expect(console.log).toHaveBeenCalledWith(prettyDOM(baseElement))
17
- })
18
- })
@@ -1,32 +0,0 @@
1
- import { fireEvent, render } from '@testing-library/svelte'
2
- import { describe, expect, test } from 'vitest'
3
-
4
- import Comp from './fixtures/Comp.svelte'
5
-
6
- describe('events', () => {
7
- test('state changes are flushed after firing an event', async () => {
8
- const { getByText } = render(Comp, { props: { name: 'World' } })
9
- const button = getByText('Button')
10
-
11
- const result = fireEvent.click(button)
12
-
13
- await expect(result).resolves.toBe(true)
14
- expect(button).toHaveTextContent('Button Clicked')
15
- })
16
-
17
- test('calling `fireEvent` directly works too', async () => {
18
- const { getByText } = render(Comp, { props: { name: 'World' } })
19
- const button = getByText('Button')
20
-
21
- const result = fireEvent(
22
- button,
23
- new MouseEvent('click', {
24
- bubbles: true,
25
- cancelable: true,
26
- })
27
- )
28
-
29
- await expect(result).resolves.toBe(true)
30
- expect(button).toHaveTextContent('Button Clicked')
31
- })
32
- })
@@ -1,17 +0,0 @@
1
- <svelte:options accessors />
2
-
3
- <script>
4
- export let name = 'World'
5
-
6
- let buttonText = 'Button'
7
-
8
- function handleClick() {
9
- buttonText = 'Button Clicked'
10
- }
11
- </script>
12
-
13
- <h1 data-testid="test">Hello {name}!</h1>
14
-
15
- <button on:click={handleClick}>{buttonText}</button>
16
-
17
- <style></style>
@@ -1,7 +0,0 @@
1
- <script>
2
- import { getContext } from 'svelte'
3
-
4
- const ctx = getContext('foo')
5
- </script>
6
-
7
- <div>{ctx.message}</div>
@@ -1,19 +0,0 @@
1
- <script>
2
- import { onDestroy, onMount } from 'svelte'
3
-
4
- export let onExecuted = undefined
5
- export let onMounted = undefined
6
- export let onDestroyed = undefined
7
-
8
- onExecuted?.()
9
-
10
- onMount(() => {
11
- onMounted?.()
12
- })
13
-
14
- onDestroy(() => {
15
- onDestroyed?.()
16
- })
17
- </script>
18
-
19
- <button />
@@ -1,7 +0,0 @@
1
- <script lang="ts">
2
- export let name: string
3
- export let count: number
4
- </script>
5
-
6
- <h1>hello {name}</h1>
7
- <p>count: {count}</p>
@@ -1,18 +0,0 @@
1
- <script>
2
- import { blur } from 'svelte/transition'
3
-
4
- let show = false
5
- let introDone = false
6
- </script>
7
-
8
- <button on:click={() => (show = true)}>Show</button>
9
-
10
- {#if show}
11
- <div in:blur={{ duration: 64 }} on:introend={() => (introDone = true)}>
12
- {#if introDone}
13
- <p data-testid="intro-done">Done</p>
14
- {:else}
15
- <p data-testid="intro-pending">Pending</p>
16
- {/if}
17
- </div>
18
- {/if}
@@ -1,33 +0,0 @@
1
- import { act, render, screen } from '@testing-library/svelte'
2
- import { describe, expect, test, vi } from 'vitest'
3
-
4
- import Mounter from './fixtures/Mounter.svelte'
5
-
6
- const onMounted = vi.fn()
7
- const onDestroyed = vi.fn()
8
- const renderSubject = () => render(Mounter, { onMounted, onDestroyed })
9
-
10
- describe('mount and destroy', () => {
11
- test('component is mounted', async () => {
12
- renderSubject()
13
-
14
- const content = screen.getByRole('button')
15
-
16
- expect(content).toBeInTheDocument()
17
- await act()
18
- expect(onMounted).toHaveBeenCalledOnce()
19
- })
20
-
21
- test('component is destroyed', async () => {
22
- const { unmount } = renderSubject()
23
-
24
- await act()
25
- unmount()
26
-
27
- const content = screen.queryByRole('button')
28
-
29
- expect(content).not.toBeInTheDocument()
30
- await act()
31
- expect(onDestroyed).toHaveBeenCalledOnce()
32
- })
33
- })
@@ -1,42 +0,0 @@
1
- import { render } from '@testing-library/svelte'
2
- import { describe, expect, test } from 'vitest'
3
-
4
- import Comp from './fixtures/Comp.svelte'
5
-
6
- describe('multi-base', () => {
7
- const treeA = document.createElement('div')
8
- const treeB = document.createElement('div')
9
-
10
- test('container isolates trees from one another', () => {
11
- const { getByText: getByTextInA } = render(
12
- Comp,
13
- {
14
- target: treeA,
15
- props: {
16
- name: 'Tree A',
17
- },
18
- },
19
- {
20
- baseElement: treeA,
21
- }
22
- )
23
-
24
- const { getByText: getByTextInB } = render(
25
- Comp,
26
- {
27
- target: treeB,
28
- props: {
29
- name: 'Tree B',
30
- },
31
- },
32
- {
33
- baseElement: treeB,
34
- }
35
- )
36
-
37
- expect(() => getByTextInA('Hello Tree A!')).not.toThrow()
38
- expect(() => getByTextInB('Hello Tree A!')).toThrow()
39
- expect(() => getByTextInA('Hello Tree B!')).toThrow()
40
- expect(() => getByTextInB('Hello Tree B!')).not.toThrow()
41
- })
42
- })
@@ -1,85 +0,0 @@
1
- import { render } from '@testing-library/svelte'
2
- import { describe, expect, test } from 'vitest'
3
-
4
- import Comp from './fixtures/Comp.svelte'
5
- import { IS_SVELTE_5 } from './utils.js'
6
-
7
- describe('render', () => {
8
- const props = { name: 'World' }
9
-
10
- test('renders component into the document', () => {
11
- const { getByText } = render(Comp, { props })
12
-
13
- expect(getByText('Hello World!')).toBeInTheDocument()
14
- })
15
-
16
- test('accepts props directly', () => {
17
- const { getByText } = render(Comp, props)
18
- expect(getByText('Hello World!')).toBeInTheDocument()
19
- })
20
-
21
- test('throws error when mixing svelte component options and props', () => {
22
- expect(() => {
23
- render(Comp, { props, name: 'World' })
24
- }).toThrow(/Unknown options/)
25
- })
26
-
27
- test('throws error when mixing target option and props', () => {
28
- expect(() => {
29
- render(Comp, { target: document.createElement('div'), name: 'World' })
30
- }).toThrow(/Unknown options/)
31
- })
32
-
33
- test('should return a container object wrapping the DOM of the rendered component', () => {
34
- const { container, getByTestId } = render(Comp, props)
35
- const firstElement = getByTestId('test')
36
-
37
- expect(container.firstChild).toBe(firstElement)
38
- })
39
-
40
- test('should return a baseElement object, which holds the container', () => {
41
- const { baseElement, container } = render(Comp, props)
42
-
43
- expect(baseElement).toBe(document.body)
44
- expect(baseElement.firstChild).toBe(container)
45
- })
46
-
47
- test('if target is provided, use it as container and baseElement', () => {
48
- const target = document.createElement('div')
49
- const { baseElement, container } = render(Comp, { props, target })
50
-
51
- expect(container).toBe(target)
52
- expect(baseElement).toBe(target)
53
- })
54
-
55
- test('allow baseElement to be specified', () => {
56
- const customBaseElement = document.createElement('div')
57
-
58
- const { baseElement, container } = render(
59
- Comp,
60
- { props },
61
- { baseElement: customBaseElement }
62
- )
63
-
64
- expect(baseElement).toBe(customBaseElement)
65
- expect(baseElement.firstChild).toBe(container)
66
- })
67
-
68
- test.skipIf(IS_SVELTE_5)('should accept anchor option in Svelte v4', () => {
69
- const baseElement = document.body
70
- const target = document.createElement('section')
71
- const anchor = document.createElement('div')
72
- baseElement.appendChild(target)
73
- target.appendChild(anchor)
74
-
75
- const { getByTestId } = render(
76
- Comp,
77
- { props, target, anchor },
78
- { baseElement }
79
- )
80
- const firstElement = getByTestId('test')
81
-
82
- expect(target.firstChild).toBe(firstElement)
83
- expect(target.lastChild).toBe(anchor)
84
- })
85
- })
@@ -1,50 +0,0 @@
1
- import { act, render, screen } from '@testing-library/svelte'
2
- import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
3
- import { describe, expect, test, vi } from 'vitest'
4
-
5
- import Comp from './fixtures/Comp.svelte'
6
-
7
- describe('rerender', () => {
8
- test('updates props', async () => {
9
- const { rerender } = render(Comp, { name: 'World' })
10
- const element = screen.getByText('Hello World!')
11
-
12
- await rerender({ name: 'Dolly' })
13
-
14
- expect(element).toHaveTextContent('Hello Dolly!')
15
- })
16
-
17
- test('warns if incorrect arguments shape used', async () => {
18
- vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() })
19
-
20
- const { rerender } = render(Comp, { name: 'World' })
21
- const element = screen.getByText('Hello World!')
22
-
23
- await rerender({ props: { name: 'Dolly' } })
24
-
25
- expect(element).toHaveTextContent('Hello Dolly!')
26
- expect(console.warn).toHaveBeenCalledOnce()
27
- expect(console.warn).toHaveBeenCalledWith(
28
- expect.stringMatching(/deprecated/iu)
29
- )
30
- })
31
-
32
- test('change props with accessors', async () => {
33
- const { component, getByText } = render(
34
- Comp,
35
- SVELTE_VERSION < '5'
36
- ? { accessors: true, props: { name: 'World' } }
37
- : { name: 'World' }
38
- )
39
- const element = getByText('Hello World!')
40
-
41
- expect(element).toBeInTheDocument()
42
- expect(component.name).toBe('World')
43
-
44
- await act(() => {
45
- component.name = 'Planet'
46
- })
47
-
48
- expect(element).toHaveTextContent('Hello Planet!')
49
- })
50
- })
@@ -1,31 +0,0 @@
1
- import { render, screen, waitFor } from '@testing-library/svelte'
2
- import { userEvent } from '@testing-library/user-event'
3
- import { beforeEach, describe, expect, test, vi } from 'vitest'
4
-
5
- import Transitioner from './fixtures/Transitioner.svelte'
6
- import { IS_JSDOM, IS_SVELTE_5 } from './utils.js'
7
-
8
- describe.skipIf(IS_SVELTE_5)('transitions', () => {
9
- beforeEach(() => {
10
- if (!IS_JSDOM) return
11
-
12
- const raf = (fn) => setTimeout(() => fn(new Date()), 16)
13
- vi.stubGlobal('requestAnimationFrame', raf)
14
- })
15
-
16
- test('on:introend', async () => {
17
- const user = userEvent.setup()
18
-
19
- render(Transitioner)
20
- const start = screen.getByRole('button')
21
- await user.click(start)
22
-
23
- const pending = screen.getByTestId('intro-pending')
24
- expect(pending).toBeInTheDocument()
25
-
26
- await waitFor(() => {
27
- const done = screen.queryByTestId('intro-done')
28
- expect(done).toBeInTheDocument()
29
- })
30
- })
31
- })
@@ -1,7 +0,0 @@
1
- import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
2
-
3
- export const IS_JSDOM = window.navigator.userAgent.includes('jsdom')
4
-
5
- export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js
6
-
7
- export const IS_SVELTE_5 = SVELTE_VERSION >= '5'
@@ -1,23 +0,0 @@
1
- /* eslint-disable import/export */
2
- import { act } from './pure.js'
3
- import { cleanup } from './svelte5.js'
4
-
5
- // If we're running in a test runner that supports afterEach
6
- // then we'll automatically run cleanup afterEach test
7
- // this ensures that tests run in isolation from each other
8
- // if you don't like this then either import the `pure` module
9
- // or set the STL_SKIP_AUTO_CLEANUP env variable to 'true'.
10
- if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
11
- afterEach(async () => {
12
- await act()
13
- cleanup()
14
- })
15
- }
16
-
17
- // export all base queries, screen, etc.
18
- export * from '@testing-library/dom'
19
-
20
- // export svelte-specific functions and custom `fireEvent`
21
- // `fireEvent` must be a named export to take priority over wildcard export above
22
- export { act, fireEvent } from './pure.js'
23
- export { cleanup, render } from './svelte5.js'
package/src/svelte5.js DELETED
@@ -1,30 +0,0 @@
1
- import { createClassComponent } from 'svelte/legacy'
2
-
3
- import { SvelteTestingLibrary } from './pure.js'
4
-
5
- class Svelte5TestingLibrary extends SvelteTestingLibrary {
6
- svelteComponentOptions = [
7
- 'target',
8
- 'props',
9
- 'events',
10
- 'context',
11
- 'intro',
12
- 'recover',
13
- ]
14
-
15
- renderComponent(ComponentConstructor, componentOptions) {
16
- const component = createClassComponent({
17
- ...componentOptions,
18
- component: ComponentConstructor,
19
- })
20
-
21
- this.componentCache.add(component)
22
-
23
- return component
24
- }
25
- }
26
-
27
- const instance = new Svelte5TestingLibrary()
28
-
29
- export const render = instance.render.bind(instance)
30
- export const cleanup = instance.cleanup.bind(instance)