Skip to content

Commit 2230452

Browse files
committed
fix: configure dom-testing-library to flush Svelte changes
1 parent 1aa3701 commit 2230452

10 files changed

+133
-42
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
- [This Solution](#this-solution)
7171
- [Installation](#installation)
7272
- [Setup](#setup)
73+
- [Auto-cleanup](#auto-cleanup)
7374
- [Docs](#docs)
7475
- [Issues](#issues)
7576
- [🐛 Bugs](#-bugs)
@@ -140,6 +141,39 @@ test runners like Jest.
140141
[vitest]: https://vitest.dev/
141142
[setup docs]: https://testing-library.com/docs/svelte-testing-library/setup
142143

144+
### Auto-cleanup
145+
146+
In Vitest (via the `svelteTesting` plugin) and Jest (via the `beforeEach` and `afterEach` globals),
147+
this library will automatically setup and cleanup the test environment before and after each test.
148+
149+
To do your own cleanup, or if you're using another framework, call the `setup` and `cleanup` functions yourself:
150+
151+
```js
152+
import { cleanup, render, setup } from '@testing-library/svelte'
153+
154+
// before
155+
setup()
156+
157+
// test
158+
render(/* ... */)
159+
160+
// after
161+
cleanup()
162+
```
163+
164+
To disable auto-cleanup in Vitest, set the `autoCleanup` option of the plugin to false:
165+
166+
```js
167+
svelteTesting({ autoCleanup: false })
168+
```
169+
170+
To disable auto-cleanup in Jest and other frameworks with global test hooks,
171+
set the `STL_SKIP_AUTO_CLEANUP` environment variable:
172+
173+
```shell
174+
STL_SKIP_AUTO_CLEANUP=1 jest
175+
```
176+
143177
## Docs
144178

145179
See the [**docs**][stl-docs] over at the Testing Library website.

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default {
1414
extensionsToTreatAsEsm: ['.svelte'],
1515
testEnvironment: 'jsdom',
1616
setupFilesAfterEnv: ['<rootDir>/tests/_jest-setup.js'],
17-
injectGlobals: false,
17+
injectGlobals: true,
1818
moduleNameMapper: {
1919
'^vitest$': '<rootDir>/tests/_jest-vitest-alias.js',
2020
[String.raw`^@testing-library\/svelte$`]: '<rootDir>/src/index.js',

src/index.js

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { act, cleanup } from './pure.js'
1+
import { act, cleanup, setup } from './pure.js'
22

3-
// If we're running in a test runner that supports afterEach
4-
// then we'll automatically run cleanup afterEach test
3+
// If we're running in a test runner that supports beforeEach/afterEach
4+
// we'll automatically run setup and cleanup before and after each test
55
// this ensures that tests run in isolation from each other
66
// if you don't like this then set the STL_SKIP_AUTO_CLEANUP env variable.
7-
if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
8-
afterEach(async () => {
9-
await act()
10-
cleanup()
11-
})
7+
if (!process.env.STL_SKIP_AUTO_CLEANUP) {
8+
if (typeof beforeEach === 'function') {
9+
beforeEach(() => {
10+
setup()
11+
})
12+
}
13+
14+
if (typeof afterEach === 'function') {
15+
afterEach(async () => {
16+
await act()
17+
cleanup()
18+
})
19+
}
1220
}
1321

1422
// export all base queries, screen, etc.

src/pure.js

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
2+
configure as configureDTL,
23
fireEvent as baseFireEvent,
4+
getConfig as getDTLConfig,
35
getQueriesForElement,
46
prettyDOM,
57
} from '@testing-library/dom'
6-
import { tick } from 'svelte'
8+
import * as Svelte from 'svelte'
79

810
import { mount, unmount, updateProps, validateOptions } from './core/index.js'
911

@@ -94,7 +96,7 @@ const render = (Component, options = {}, renderOptions = {}) => {
9496
}
9597

9698
updateProps(component, props)
97-
await tick()
99+
await Svelte.tick()
98100
},
99101
unmount: () => {
100102
cleanupComponent(component)
@@ -103,6 +105,33 @@ const render = (Component, options = {}, renderOptions = {}) => {
103105
}
104106
}
105107

108+
/** @type {import('@testing-library/dom'.Config | undefined} */
109+
let originalDTLConfig
110+
111+
/**
112+
* Configure `@testing-library/dom` for usage with Svelte.
113+
*
114+
* Ensures events fired from `@testing-library/dom`
115+
* and `@testing-library/user-event` wait for Svelte
116+
* to flush changes to the DOM before proceeding.
117+
*/
118+
const setup = () => {
119+
originalDTLConfig = getDTLConfig()
120+
121+
configureDTL({
122+
asyncWrapper: act,
123+
eventWrapper: Svelte.flushSync ?? ((cb) => cb()),
124+
})
125+
}
126+
127+
/** Reset dom-testing-library config. */
128+
const cleanupDTL = () => {
129+
if (originalDTLConfig) {
130+
configureDTL(originalDTLConfig)
131+
originalDTLConfig = undefined
132+
}
133+
}
134+
106135
/** Remove a component from the component cache. */
107136
const cleanupComponent = (component) => {
108137
const inCache = componentCache.delete(component)
@@ -121,27 +150,31 @@ const cleanupTarget = (target) => {
121150
}
122151
}
123152

124-
/** Unmount all components and remove elements added to `<body>`. */
153+
/** Unmount components, remove elements added to `<body>`, and reset `@testing-library/dom`. */
125154
const cleanup = () => {
126155
for (const component of componentCache) {
127156
cleanupComponent(component)
128157
}
129158
for (const target of targetCache) {
130159
cleanupTarget(target)
131160
}
161+
cleanupDTL()
132162
}
133163

134164
/**
135165
* Call a function and wait for Svelte to flush pending changes.
136166
*
137-
* @param {() => unknown} [fn] - A function, which may be `async`, to call before flushing updates.
138-
* @returns {Promise<void>}
167+
* @template T
168+
* @param {(() => Promise<T>) | () => T} [fn] - A function, which may be `async`, to call before flushing updates.
169+
* @returns {Promise<T>}
139170
*/
140171
const act = async (fn) => {
172+
let result
141173
if (fn) {
142-
await fn()
174+
result = await fn()
143175
}
144-
return tick()
176+
await Svelte.tick()
177+
return result
145178
}
146179

147180
/**
@@ -162,18 +195,10 @@ const act = async (fn) => {
162195
*
163196
* @type {FireFunction & FireObject}
164197
*/
165-
const fireEvent = async (...args) => {
166-
const event = baseFireEvent(...args)
167-
await tick()
168-
return event
169-
}
198+
const fireEvent = async (...args) => act(() => baseFireEvent(...args))
170199

171200
for (const [key, baseEvent] of Object.entries(baseFireEvent)) {
172-
fireEvent[key] = async (...args) => {
173-
const event = baseEvent(...args)
174-
await tick()
175-
return event
176-
}
201+
fireEvent[key] = async (...args) => act(() => baseEvent(...args))
177202
}
178203

179-
export { act, cleanup, fireEvent, render }
204+
export { act, cleanup, fireEvent, render, setup }

src/vitest.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { act, cleanup } from '@testing-library/svelte'
2-
import { afterEach } from 'vitest'
1+
import { act, cleanup, setup } from '@testing-library/svelte'
2+
import { beforeEach } from 'vitest'
33

4-
afterEach(async () => {
5-
await act()
6-
cleanup()
4+
beforeEach(() => {
5+
setup()
6+
7+
return async () => {
8+
await act()
9+
cleanup()
10+
}
711
})

tests/_jest-setup.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1 @@
11
import '@testing-library/jest-dom/jest-globals'
2-
3-
import { afterEach } from '@jest/globals'
4-
import { act, cleanup } from '@testing-library/svelte'
5-
6-
afterEach(async () => {
7-
await act()
8-
cleanup()
9-
})

tests/_jest-vitest-alias.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ export {
1111
jest as vi,
1212
} from '@jest/globals'
1313

14-
// Add support for describe.skipIf and test.skipIf
14+
// Add support for describe.skipIf, test.skipIf, and test.runIf
1515
describe.skipIf = (condition) => (condition ? describe.skip : describe)
1616
test.skipIf = (condition) => (condition ? test.skip : test)
17+
test.runIf = (condition) => (condition ? test : test.skip)
1718

1819
// Add support for `stubGlobal`
1920
jest.stubGlobal = (property, stub) => {

tests/act.test.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { setTimeout } from 'node:timers/promises'
22

33
import { act, render, screen } from '@testing-library/svelte'
4+
import { userEvent } from '@testing-library/user-event'
45
import { describe, expect, test } from 'vitest'
56

67
import Comp from './fixtures/Comp.svelte'
@@ -24,10 +25,20 @@ describe('act', () => {
2425
const button = screen.getByText('Button')
2526

2627
await act(async () => {
27-
await setTimeout(100)
28+
await setTimeout(10)
2829
button.click()
2930
})
3031

3132
expect(button).toHaveTextContent('Button Clicked')
3233
})
34+
35+
test('wires act into user-event', async () => {
36+
const user = userEvent.setup()
37+
render(Comp)
38+
const button = screen.getByText('Button')
39+
40+
await user.click(button)
41+
42+
expect(button).toHaveTextContent('Button Clicked')
43+
})
3344
})

tests/auto-cleanup.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import { IS_JEST } from './_env.js'
66
// in Jest breaks Svelte's environment checking heuristics.
77
// Re-implement this test in a more accurate environment, without mocks.
88
describe.skipIf(IS_JEST)('auto-cleanup', () => {
9+
const globalBeforeEach = vi.fn()
910
const globalAfterEach = vi.fn()
1011

1112
beforeEach(() => {
1213
vi.resetModules()
14+
globalThis.beforeEach = globalBeforeEach
1315
globalThis.afterEach = globalAfterEach
1416
})
1517

1618
afterEach(() => {
1719
delete process.env.STL_SKIP_AUTO_CLEANUP
20+
delete globalThis.beforeEach
1821
delete globalThis.afterEach
1922
})
2023

@@ -37,6 +40,7 @@ describe.skipIf(IS_JEST)('auto-cleanup', () => {
3740

3841
await import('@testing-library/svelte')
3942

43+
expect(globalBeforeEach).toHaveBeenCalledTimes(0)
4044
expect(globalAfterEach).toHaveBeenCalledTimes(0)
4145
})
4246
})

tests/events.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { fireEvent as fireEventDTL } from '@testing-library/dom'
12
import { fireEvent, render, screen } from '@testing-library/svelte'
23
import { describe, expect, test } from 'vitest'
34

5+
import { IS_SVELTE_5 } from './_env.js'
46
import Comp from './fixtures/Comp.svelte'
57

68
describe('events', () => {
@@ -29,4 +31,14 @@ describe('events', () => {
2931
await expect(result).resolves.toBe(true)
3032
expect(button).toHaveTextContent('Button Clicked')
3133
})
34+
35+
test.runIf(IS_SVELTE_5)('state changes are flushed synchronously', () => {
36+
render(Comp, { props: { name: 'World' } })
37+
const button = screen.getByText('Button')
38+
39+
const result = fireEventDTL.click(button)
40+
41+
expect(result).toBe(true)
42+
expect(button).toHaveTextContent('Button Clicked')
43+
})
3244
})

0 commit comments

Comments
 (0)