Skip to content

Commit 40973c5

Browse files
authoredFeb 24, 2024
fix: $destroy and createRoot are no more (#328)
* fix: make the latest Svelte 5 pass all tests For now I've resorted to use the legacy API, as the use of runes don't seem to work in the test environment (which, mind you, could be a problem on this side of the keyboard) and the important part is to have the package work with Svelte 5.
1 parent 178b2de commit 40973c5

21 files changed

+208
-94
lines changed
 

Diff for: ‎README.md

+15
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ This library has `peerDependencies` listings for `svelte >= 3`.
8080
You may also be interested in installing `@testing-library/jest-dom` so you can use
8181
[the custom jest matchers](https://github.com/testing-library/jest-dom).
8282

83+
### Svelte 5 support
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:
87+
88+
```
89+
export default defineConfig(({ }) => ({
90+
test: {
91+
alias: {
92+
'@testing-library/svelte': '@testing-library/svelte/svelte5'
93+
}
94+
},
95+
}))
96+
```
97+
8398
## Docs
8499

85100
See the [**docs**](https://testing-library.com/docs/svelte-testing-library/intro) over at the Testing Library website.

Diff for: ‎package.json

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
"types": "./types/index.d.ts",
99
"default": "./src/index.js"
1010
},
11+
"./svelte5": {
12+
"types": "./types/index.d.ts",
13+
"default": "./src/svelte5-index.js"
14+
},
1115
"./vitest": {
1216
"default": "./src/vitest.js"
1317
}

Diff for: ‎src/__tests__/act.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, describe, expect, test } from 'vitest'
22

3-
import { act, fireEvent, render as stlRender } from '..'
3+
import { act, fireEvent, render as stlRender } from '@testing-library/svelte'
44
import Comp from './fixtures/Comp.svelte'
55

66
describe('act', () => {

Diff for: ‎src/__tests__/auto-cleanup-skip.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('auto-cleanup-skip', () => {
77

88
beforeAll(async () => {
99
process.env.STL_SKIP_AUTO_CLEANUP = 'true'
10-
const stl = await import('..')
10+
const stl = await import('@testing-library/svelte')
1111
render = stl.render
1212
})
1313

Diff for: ‎src/__tests__/auto-cleanup.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from 'vitest'
22

3-
import { render } from '..'
3+
import { render } from '@testing-library/svelte'
44
import Comp from './fixtures/Comp.svelte'
55

66
describe('auto-cleanup', () => {

Diff for: ‎src/__tests__/cleanup.test.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test, vi } from 'vitest'
2+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
23

3-
import { act, cleanup, render } from '..'
4+
import { act, cleanup, render } from '@testing-library/svelte'
45
import Mounter from './fixtures/Mounter.svelte'
56

67
const onExecuted = vi.fn()
@@ -15,7 +16,7 @@ describe('cleanup', () => {
1516
expect(document.body).toBeEmptyDOMElement()
1617
})
1718

18-
test('cleanup unmounts component', async () => {
19+
test.runIf(SVELTE_VERSION < '5')('cleanup unmounts component', async () => {
1920
await act(renderSubject)
2021
cleanup()
2122

Diff for: ‎src/__tests__/context.test.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { expect, test } from 'vitest'
22

3-
import { render } from '..'
3+
import { render } from '@testing-library/svelte'
44
import Comp from './fixtures/Context.svelte'
5+
import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js'
56

6-
test('can set a context', () => {
7+
test.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('can set a context', () => {
78
const message = 'Got it'
89

910
const { getByText } = render(Comp, {

Diff for: ‎src/__tests__/debug.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { prettyDOM } from '@testing-library/dom'
22
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
33

4-
import { render } from '..'
4+
import { render } from '@testing-library/svelte'
55
import Comp from './fixtures/Comp.svelte'
66

77
describe('debug', () => {

Diff for: ‎src/__tests__/events.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from 'vitest'
22

3-
import { fireEvent, render } from '..'
3+
import { fireEvent, render } from '@testing-library/svelte'
44
import Comp from './fixtures/Comp.svelte'
55

66
describe('events', () => {

Diff for: ‎src/__tests__/mount.test.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { describe, expect, test, vi } from 'vitest'
22

3-
import { act, render, screen } from '..'
3+
import { act, render, screen } from '@testing-library/svelte'
44
import Mounter from './fixtures/Mounter.svelte'
5+
import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js'
56

67
const onMounted = vi.fn()
78
const onDestroyed = vi.fn()
89
const renderSubject = () => render(Mounter, { onMounted, onDestroyed })
910

10-
describe('mount and destroy', () => {
11+
describe.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('mount and destroy', () => {
1112
test('component is mounted', async () => {
1213
renderSubject()
1314

Diff for: ‎src/__tests__/multi-base.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test } from 'vitest'
22

3-
import { render } from '..'
3+
import { render } from '@testing-library/svelte'
44
import Comp from './fixtures/Comp.svelte'
55

66
describe('multi-base', () => {

Diff for: ‎src/__tests__/render.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
22
import { beforeEach, describe, expect, test } from 'vitest'
33

4-
import { act, render as stlRender } from '..'
4+
import { act, render as stlRender } from '@testing-library/svelte'
55
import Comp from './fixtures/Comp.svelte'
66
import CompDefault from './fixtures/Comp2.svelte'
77

@@ -107,7 +107,7 @@ describe('render', () => {
107107
})
108108

109109
test('correctly find component constructor on the default property', () => {
110-
const { getByText } = render(CompDefault, { props: { name: 'World' } })
110+
const { getByText } = stlRender(CompDefault, { props: { name: 'World' } })
111111

112112
expect(getByText('Hello World!')).toBeInTheDocument()
113113
})

Diff for: ‎src/__tests__/rerender.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
22
* @jest-environment jsdom
33
*/
4-
import { describe, expect, test, vi } from 'vitest'
5-
import { writable } from 'svelte/store'
4+
import { expect, test, vi } from 'vitest'
5+
6+
import { render, waitFor } from '@testing-library/svelte'
67

7-
import { act, render, waitFor } from '..'
88
import Comp from './fixtures/Rerender.svelte'
99

1010
test('mounts new component successfully', async () => {

Diff for: ‎src/__tests__/transition.test.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { userEvent } from '@testing-library/user-event'
2-
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
32
import { beforeEach, describe, expect, test, vi } from 'vitest'
43

5-
import { render, screen, waitFor } from '..'
4+
import { IS_JSDOM, IS_SVELTE_5 } from './utils.js'
5+
6+
import { render, screen, waitFor } from '@testing-library/svelte'
67
import Transitioner from './fixtures/Transitioner.svelte'
78

8-
describe.runIf(SVELTE_VERSION < '5')('transitions', () => {
9+
describe.runIf(!IS_SVELTE_5)('transitions', () => {
910
beforeEach(() => {
10-
if (window.navigator.userAgent.includes('jsdom')) {
11-
const raf = (fn) => setTimeout(() => fn(new Date()), 16)
12-
vi.stubGlobal('requestAnimationFrame', raf)
13-
}
11+
if (!IS_JSDOM) return
12+
13+
const raf = (fn) => setTimeout(() => fn(new Date()), 16)
14+
vi.stubGlobal('requestAnimationFrame', raf)
1415
})
1516

1617
test('on:introend', async () => {

Diff for: ‎src/__tests__/utils.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
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'

Diff for: ‎src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
1313
}
1414

1515
export * from './pure.js'
16+
export * from '@testing-library/dom'

Diff for: ‎src/pure.js

+79-69
Original file line numberDiff line numberDiff line change
@@ -3,60 +3,60 @@ import {
33
getQueriesForElement,
44
prettyDOM,
55
} from '@testing-library/dom'
6+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
67
import * as Svelte from 'svelte'
78

8-
const IS_SVELTE_5 = typeof Svelte.createRoot === 'function'
9-
const targetCache = new Set()
10-
const componentCache = new Set()
11-
12-
const svelteComponentOptions = IS_SVELTE_5
13-
? ['target', 'props', 'events', 'context', 'intro', 'recover']
14-
: ['accessors', 'anchor', 'props', 'hydrate', 'intro', 'context']
15-
16-
const render = (
17-
Component,
18-
{ target, ...options } = {},
19-
{ container, queries } = {}
20-
) => {
21-
container = container || document.body
22-
target = target || container.appendChild(document.createElement('div'))
23-
targetCache.add(target)
24-
25-
const ComponentConstructor = Component.default || Component
26-
27-
const checkProps = (options) => {
28-
const isProps = !Object.keys(options).some((option) =>
29-
svelteComponentOptions.includes(option)
9+
const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION)
10+
export const targetCache = new Set()
11+
export const componentCache = new Set()
12+
13+
const svelteComponentOptions = [
14+
'accessors',
15+
'anchor',
16+
'props',
17+
'hydrate',
18+
'intro',
19+
'context',
20+
]
21+
22+
export const buildCheckProps = (svelteComponentOptions) => (options) => {
23+
const isProps = !Object.keys(options).some((option) =>
24+
svelteComponentOptions.includes(option)
25+
)
26+
27+
// Check if any props and Svelte options were accidentally mixed.
28+
if (!isProps) {
29+
const unrecognizedOptions = Object.keys(options).filter(
30+
(option) => !svelteComponentOptions.includes(option)
3031
)
3132

32-
// Check if any props and Svelte options were accidentally mixed.
33-
if (!isProps) {
34-
const unrecognizedOptions = Object.keys(options).filter(
35-
(option) => !svelteComponentOptions.includes(option)
36-
)
37-
38-
if (unrecognizedOptions.length > 0) {
39-
throw Error(`
33+
if (unrecognizedOptions.length > 0) {
34+
throw Error(`
4035
Unknown options were found [${unrecognizedOptions}]. This might happen if you've mixed
4136
passing in props with Svelte options into the render function. Valid Svelte options
4237
are [${svelteComponentOptions}]. You can either change the prop names, or pass in your
4338
props for that component via the \`props\` option.\n\n
4439
Eg: const { /** Results **/ } = render(MyComponent, { props: { /** props here **/ } })\n\n
4540
`)
46-
}
47-
48-
return options
4941
}
5042

51-
return { props: options }
43+
return options
5244
}
5345

54-
const renderComponent = (options) => {
46+
return { props: options }
47+
}
48+
49+
const checkProps = buildCheckProps(svelteComponentOptions)
50+
51+
const buildRenderComponent =
52+
({ target, ComponentConstructor }) =>
53+
(options) => {
5554
options = { target, ...checkProps(options) }
5655

57-
const component = IS_SVELTE_5
58-
? Svelte.createRoot(ComponentConstructor, options)
59-
: new ComponentConstructor(options)
56+
if (IS_SVELTE_5)
57+
throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`')
58+
59+
const component = new ComponentConstructor(options)
6060

6161
componentCache.add(component)
6262

@@ -71,30 +71,46 @@ const render = (
7171
return component
7272
}
7373

74-
let component = renderComponent(options)
75-
76-
return {
77-
container,
78-
component,
79-
debug: (el = container) => console.log(prettyDOM(el)),
80-
rerender: async (props) => {
81-
if (props.props) {
82-
console.warn(
83-
'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
84-
)
85-
props = props.props
86-
}
87-
component.$set(props)
88-
await Svelte.tick()
89-
},
90-
unmount: () => {
91-
cleanupComponent(component)
92-
},
93-
...getQueriesForElement(container, queries),
74+
export const buildRender =
75+
(buildRenderComponent) =>
76+
(Component, { target, ...options } = {}, { container, queries } = {}) => {
77+
container = container || document.body
78+
target = target || container.appendChild(document.createElement('div'))
79+
targetCache.add(target)
80+
81+
const ComponentConstructor = Component.default || Component
82+
83+
const renderComponent = buildRenderComponent({
84+
target,
85+
ComponentConstructor,
86+
})
87+
88+
let component = renderComponent(options)
89+
90+
return {
91+
container,
92+
component,
93+
debug: (el = container) => console.log(prettyDOM(el)),
94+
rerender: async (props) => {
95+
if (props.props) {
96+
console.warn(
97+
'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
98+
)
99+
props = props.props
100+
}
101+
component.$set(props)
102+
await Svelte.tick()
103+
},
104+
unmount: () => {
105+
cleanupComponent(component)
106+
},
107+
...getQueriesForElement(container, queries),
108+
}
94109
}
95-
}
96110

97-
const cleanupComponent = (component) => {
111+
export const render = buildRender(buildRenderComponent)
112+
113+
export const cleanupComponent = (component) => {
98114
const inCache = componentCache.delete(component)
99115

100116
if (inCache) {
@@ -110,19 +126,19 @@ const cleanupTarget = (target) => {
110126
}
111127
}
112128

113-
const cleanup = () => {
129+
export const cleanup = () => {
114130
componentCache.forEach(cleanupComponent)
115131
targetCache.forEach(cleanupTarget)
116132
}
117133

118-
const act = async (fn) => {
134+
export const act = async (fn) => {
119135
if (fn) {
120136
await fn()
121137
}
122138
return Svelte.tick()
123139
}
124140

125-
const fireEvent = async (...args) => {
141+
export const fireEvent = async (...args) => {
126142
const event = dtlFireEvent(...args)
127143
await Svelte.tick()
128144
return event
@@ -135,9 +151,3 @@ Object.keys(dtlFireEvent).forEach((key) => {
135151
return event
136152
}
137153
})
138-
139-
/* eslint-disable import/export */
140-
141-
export * from '@testing-library/dom'
142-
143-
export { render, cleanup, fireEvent, act }

Diff for: ‎src/svelte5-index.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { act, cleanup } from './svelte5.js'
2+
3+
// If we're running in a test runner that supports afterEach
4+
// then we'll automatically run cleanup afterEach test
5+
// this ensures that tests run in isolation from each other
6+
// if you don't like this then either import the `pure` module
7+
// or set the STL_SKIP_AUTO_CLEANUP env variable to 'true'.
8+
if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
9+
afterEach(async () => {
10+
await act()
11+
cleanup()
12+
})
13+
}
14+
15+
export * from './svelte5.js'
16+
export * from '@testing-library/dom'
17+
export { act, fireEvent } from './pure.js'

Diff for: ‎src/svelte5.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createClassComponent } from 'svelte/legacy'
2+
import {
3+
componentCache,
4+
cleanup,
5+
buildCheckProps,
6+
buildRender,
7+
} from './pure.js'
8+
9+
const svelteComponentOptions = [
10+
'target',
11+
'props',
12+
'events',
13+
'context',
14+
'intro',
15+
'recover',
16+
]
17+
18+
const checkProps = buildCheckProps(svelteComponentOptions)
19+
20+
const buildRenderComponent =
21+
({ target, ComponentConstructor }) =>
22+
(options) => {
23+
options = { target, ...checkProps(options) }
24+
25+
const component = createClassComponent({
26+
component: ComponentConstructor,
27+
...options,
28+
})
29+
30+
componentCache.add(component)
31+
32+
return component
33+
}
34+
35+
const render = buildRender(buildRenderComponent)
36+
37+
/* eslint-disable import/export */
38+
39+
import { act, fireEvent } from './pure.js'
40+
41+
export { render, cleanup, fireEvent, act }

Diff for: ‎src/vitest.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterEach } from 'vitest'
22

3-
import { act, cleanup } from './pure.js'
3+
import { act, cleanup } from '@testing-library/svelte'
44

55
afterEach(async () => {
66
await act()

Diff for: ‎vite.config.js

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
import { svelte } from '@sveltejs/vite-plugin-svelte'
22
import { defineConfig } from 'vite'
3+
import path from 'path'
4+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
5+
6+
const IS_SVELTE_5 = SVELTE_VERSION >= '5'
7+
8+
const alias = [
9+
{
10+
find: '@testing-library/svelte',
11+
replacement: path.resolve(
12+
__dirname,
13+
IS_SVELTE_5 ? 'src/svelte5-index.js' : 'src/index.js'
14+
),
15+
},
16+
]
317

418
// https://vitejs.dev/config/
519
export default defineConfig(({ mode }) => ({
@@ -12,6 +26,7 @@ export default defineConfig(({ mode }) => ({
1226
conditions: mode === 'test' ? ['browser'] : [],
1327
},
1428
test: {
29+
alias,
1530
environment: 'jsdom',
1631
setupFiles: ['./src/__tests__/_vitest-setup.js'],
1732
mockReset: true,

0 commit comments

Comments
 (0)
Please sign in to comment.