Skip to content

Commit 9dce164

Browse files
authored
feat(svelte5): incorporate Svelte 5 support into main entry point (#375)
1 parent ee1c966 commit 9dce164

20 files changed

+311
-244
lines changed

.eslintrc.cjs

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = {
2525
},
2626
rules: {
2727
'no-undef-init': 'off',
28+
'prefer-const': 'off',
2829
},
2930
},
3031
{
@@ -49,5 +50,6 @@ module.exports = {
4950
ecmaVersion: 2022,
5051
sourceType: 'module',
5152
},
53+
globals: { $state: 'readonly', $props: 'readonly' },
5254
ignorePatterns: ['!/.*'],
5355
}

README.md

+2-18
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ primary guiding principle is:
7171
This module is distributed via [npm][npm] which is bundled with [node][node] and
7272
should be installed as one of your project's `devDependencies`:
7373

74-
```
74+
```shell
7575
npm install --save-dev @testing-library/svelte
7676
```
7777

78-
This library has `peerDependencies` listings for `svelte >= 3`.
78+
This library supports `svelte` versions `3`, `4`, and `5`.
7979

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).
@@ -102,22 +102,6 @@ See the [setup docs][] for more detailed setup instructions, including for other
102102
[vitest]: https://vitest.dev/
103103
[setup docs]: https://testing-library.com/docs/svelte-testing-library/setup
104104

105-
### Svelte 5 support
106-
107-
If you are riding the bleeding edge of Svelte 5, you'll need to either
108-
import from `@testing-library/svelte/svelte5` instead of `@testing-library/svelte`, or add an alias to your `vite.config.js`:
109-
110-
```js
111-
export default defineConfig({
112-
plugins: [svelte(), svelteTesting()],
113-
test: {
114-
alias: {
115-
'@testing-library/svelte': '@testing-library/svelte/svelte5',
116-
},
117-
},
118-
})
119-
```
120-
121105
## Docs
122106

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

jest.config.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
22

3-
const IS_SVELTE_5 = SVELTE_VERSION >= '5'
3+
const SVELTE_TRANSFORM_PATTERN =
4+
SVELTE_VERSION >= '5' ? '^.+\\.svelte(?:\\.js)?$' : '^.+\\.svelte$'
45

56
export default {
67
testMatch: ['<rootDir>/src/__tests__/**/*.test.js'],
78
transform: {
8-
'^.+\\.svelte$': 'svelte-jester',
9+
[SVELTE_TRANSFORM_PATTERN]: 'svelte-jester',
910
},
1011
moduleFileExtensions: ['js', 'svelte'],
1112
extensionsToTreatAsEsm: ['.svelte'],
@@ -14,9 +15,6 @@ export default {
1415
injectGlobals: false,
1516
moduleNameMapper: {
1617
'^vitest$': '<rootDir>/src/__tests__/_jest-vitest-alias.js',
17-
'^@testing-library/svelte$': IS_SVELTE_5
18-
? '<rootDir>/src/svelte5-index.js'
19-
: '<rootDir>/src/index.js',
2018
},
2119
resetMocks: true,
2220
restoreMocks: true,

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
},
1111
"./svelte5": {
1212
"types": "./types/index.d.ts",
13-
"default": "./src/svelte5-index.js"
13+
"default": "./src/index.js"
1414
},
1515
"./vitest": {
1616
"default": "./src/vitest.js"
@@ -120,7 +120,7 @@
120120
"prettier-plugin-svelte": "3.2.3",
121121
"svelte": "^3 || ^4 || ^5",
122122
"svelte-check": "^3.6.3",
123-
"svelte-jester": "^3.0.0",
123+
"svelte-jester": "^5.0.0",
124124
"typescript": "^5.3.3",
125125
"vite": "^5.1.1",
126126
"vitest": "^1.5.2"

src/__tests__/auto-cleanup.test.js

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
22

3-
import { IS_SVELTE_5 } from './utils.js'
4-
5-
const importSvelteTestingLibrary = async () =>
6-
IS_SVELTE_5 ? import('../svelte5-index.js') : import('../index.js')
7-
83
const globalAfterEach = vi.fn()
94

105
describe('auto-cleanup', () => {
@@ -19,7 +14,7 @@ describe('auto-cleanup', () => {
1914
})
2015

2116
test('calls afterEach with cleanup if globally defined', async () => {
22-
const { render } = await importSvelteTestingLibrary()
17+
const { render } = await import('../index.js')
2318

2419
expect(globalAfterEach).toHaveBeenCalledTimes(1)
2520
expect(globalAfterEach).toHaveBeenLastCalledWith(expect.any(Function))
@@ -35,7 +30,7 @@ describe('auto-cleanup', () => {
3530
test('does not call afterEach if process STL_SKIP_AUTO_CLEANUP is set', async () => {
3631
process.env.STL_SKIP_AUTO_CLEANUP = 'true'
3732

38-
await importSvelteTestingLibrary()
33+
await import('../index.js')
3934

4035
expect(globalAfterEach).toHaveBeenCalledTimes(0)
4136
})

src/__tests__/fixtures/Comp.svelte

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!-- svelte-ignore options_deprecated_accessors -->
12
<svelte:options accessors />
23

34
<script>
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let { name = 'World' } = $props()
3+
4+
let buttonText = $state('Button')
5+
6+
function handleClick() {
7+
buttonText = 'Button Clicked'
8+
}
9+
</script>
10+
11+
<h1 data-testid="test">Hello {name}!</h1>
12+
13+
<button onclick={handleClick}>{buttonText}</button>

src/__tests__/fixtures/Mounter.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
})
1717
</script>
1818
19-
<button />
19+
<button></button>

src/__tests__/render.test.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { render } from '@testing-library/svelte'
2-
import { describe, expect, test } from 'vitest'
2+
import { beforeAll, describe, expect, test } from 'vitest'
33

4-
import Comp from './fixtures/Comp.svelte'
5-
import { IS_SVELTE_5 } from './utils.js'
4+
import { COMPONENT_FIXTURES } from './utils.js'
65

7-
describe('render', () => {
6+
describe.each(COMPONENT_FIXTURES)('render ($mode)', ({ component }) => {
87
const props = { name: 'World' }
8+
let Comp
9+
10+
beforeAll(async () => {
11+
Comp = await import(component)
12+
})
913

1014
test('renders component into the document', () => {
1115
const { getByText } = render(Comp, { props })
@@ -65,7 +69,7 @@ describe('render', () => {
6569
expect(baseElement.firstChild).toBe(container)
6670
})
6771

68-
test.skipIf(IS_SVELTE_5)('should accept anchor option in Svelte v4', () => {
72+
test('should accept anchor option', () => {
6973
const baseElement = document.body
7074
const target = document.createElement('section')
7175
const anchor = document.createElement('div')

src/__tests__/rerender.test.js

+15-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
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'
2+
import { beforeAll, describe, expect, test, vi } from 'vitest'
43

5-
import Comp from './fixtures/Comp.svelte'
4+
import { COMPONENT_FIXTURES, IS_SVELTE_5, MODE_RUNES } from './utils.js'
5+
6+
describe.each(COMPONENT_FIXTURES)('rerender ($mode)', ({ mode, component }) => {
7+
let Comp
8+
9+
beforeAll(async () => {
10+
Comp = await import(component)
11+
})
612

7-
describe('rerender', () => {
813
test('updates props', async () => {
914
const { rerender } = render(Comp, { name: 'World' })
1015
const element = screen.getByText('Hello World!')
@@ -29,13 +34,12 @@ describe('rerender', () => {
2934
)
3035
})
3136

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-
)
37+
test.skipIf(mode === MODE_RUNES)('change props with accessors', async () => {
38+
const componentOptions = IS_SVELTE_5
39+
? { name: 'World' }
40+
: { accessors: true, props: { name: 'World' } }
41+
42+
const { component, getByText } = render(Comp, componentOptions)
3943
const element = getByText('Hello World!')
4044

4145
expect(element).toBeInTheDocument()

src/__tests__/utils.js

+17
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,20 @@ export const IS_JSDOM = window.navigator.userAgent.includes('jsdom')
55
export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js
66

77
export const IS_SVELTE_5 = SVELTE_VERSION >= '5'
8+
9+
export const MODE_LEGACY = 'legacy'
10+
11+
export const MODE_RUNES = 'runes'
12+
13+
export const COMPONENT_FIXTURES = [
14+
{
15+
mode: MODE_LEGACY,
16+
component: './fixtures/Comp.svelte',
17+
isEnabled: true,
18+
},
19+
{
20+
mode: MODE_RUNES,
21+
component: './fixtures/CompRunes.svelte',
22+
isEnabled: IS_SVELTE_5,
23+
},
24+
].filter(({ isEnabled }) => isEnabled)

src/core/index.js

+27
Original file line numberDiff line numberDiff line change
@@ -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+
}

src/core/legacy.js

+46
Original file line numberDiff line numberDiff line change
@@ -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 }

src/core/modern.svelte.js

+50
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)