Skip to content

Commit 178b2de

Browse files
authored
feat: add Svelte v5-next support (#321)
* fix: remove DOM elements even if component creation fails Fixes #190 * feat: add Svelte v5-next support
1 parent e13a6b1 commit 178b2de

12 files changed

+210
-95
lines changed

.github/workflows/release.yml

+12
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,23 @@ jobs:
1616
if: ${{ !contains(github.head_ref, 'all-contributors') }}
1717
name: Node ${{ matrix.node }}, Svelte ${{ matrix.svelte }}, ${{ matrix.test-runner }}
1818
runs-on: ubuntu-latest
19+
continue-on-error: ${{ matrix.experimental }}
1920
strategy:
21+
fail-fast: false
2022
matrix:
2123
node: ['16', '18', '20']
2224
svelte: ['3', '4']
2325
test-runner: ['vitest:jsdom', 'vitest:happy-dom']
26+
experimental: [false]
27+
include:
28+
- node: '20'
29+
svelte: 'next'
30+
test-runner: 'vitest:jsdom'
31+
experimental: true
32+
- node: '20'
33+
svelte: 'next'
34+
test-runner: 'vitest:happy-dom'
35+
experimental: true
2436

2537
steps:
2638
- name: ⬇️ Checkout repo

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,13 @@
6464
"contributors:generate": "all-contributors generate"
6565
},
6666
"peerDependencies": {
67-
"svelte": "^3 || ^4"
67+
"svelte": "^3 || ^4 || ^5"
6868
},
6969
"dependencies": {
7070
"@testing-library/dom": "^9.3.1"
7171
},
7272
"devDependencies": {
73-
"@sveltejs/vite-plugin-svelte": "^2.4.2",
73+
"@sveltejs/vite-plugin-svelte": "^3.0.2",
7474
"@testing-library/jest-dom": "^6.3.0",
7575
"@testing-library/user-event": "^14.5.2",
7676
"@typescript-eslint/eslint-plugin": "6.19.1",
@@ -94,11 +94,11 @@
9494
"npm-run-all": "^4.1.5",
9595
"prettier": "3.2.4",
9696
"prettier-plugin-svelte": "3.1.2",
97-
"svelte": "^3 || ^4",
97+
"svelte": "^4.2.10",
9898
"svelte-check": "^3.6.3",
9999
"svelte-jester": "^3.0.0",
100100
"typescript": "^5.3.3",
101-
"vite": "^4.3.9",
101+
"vite": "^5.1.1",
102102
"vitest": "^0.33.0"
103103
}
104104
}

src/__tests__/__snapshots__/render.test.js.snap

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`render > should accept svelte component options 1`] = `
3+
exports[`render > should accept svelte v4 component options 1`] = `
44
<body>
55
<div>
66
<h1
@@ -18,8 +18,31 @@ exports[`render > should accept svelte component options 1`] = `
1818
<button>
1919
Button
2020
</button>
21-
<!--&lt;Comp&gt;-->
2221
<div />
2322
</div>
2423
</body>
2524
`;
25+
26+
exports[`render > should accept svelte v5 component options 1`] = `
27+
<body>
28+
29+
30+
31+
<section>
32+
<h1
33+
data-testid="test"
34+
>
35+
Hello World!
36+
</h1>
37+
38+
<div>
39+
we have context
40+
</div>
41+
42+
<button>
43+
Button
44+
</button>
45+
46+
</section>
47+
</body>
48+
`;

src/__tests__/cleanup.test.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, test, vi } from 'vitest'
2+
3+
import { act, cleanup, render } from '..'
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', async () => {
19+
await act(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+
})

src/__tests__/fixtures/Mounter.svelte

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
<script>
2-
import { onDestroy,onMount } from 'svelte'
2+
import { onDestroy, onMount } from 'svelte'
33
4-
export let onMounted
5-
export let onDestroyed
4+
export let onExecuted = undefined
5+
export let onMounted = undefined
6+
export let onDestroyed = undefined
67
7-
onMount(() => onMounted())
8-
onDestroy(() => onDestroyed())
8+
onExecuted?.()
9+
10+
onMount(() => {
11+
onMounted?.()
12+
})
13+
14+
onDestroy(() => {
15+
onDestroyed?.()
16+
})
917
</script>
1018
1119
<button />
+11-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
<script>
2-
import { getContext, onMount } from 'svelte'
2+
import { onDestroy, onMount } from 'svelte'
33
4-
export let name
4+
export let onExecuted = undefined
5+
export let onMounted = undefined
6+
export let onDestroyed = undefined
57
6-
const mountCounter = getContext('mountCounter')
8+
export let name = ''
79
8-
onMount(() => {
9-
mountCounter.update((i) => i + 1)
10-
})
11-
</script>
10+
onExecuted?.()
11+
12+
onMount(() => onMounted?.())
1213
13-
<h1 data-testid="test">Hello {name}!</h1>
14+
onDestroy(() => onDestroyed?.())
15+
</script>
1416
15-
<div data-testid="mount-counter">{$mountCounter}</div>
17+
<div data-testid="test">Hello {name}!</div>

src/__tests__/mount.test.js

+13-13
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,31 @@ import { describe, expect, test, vi } from 'vitest'
33
import { act, render, screen } from '..'
44
import Mounter from './fixtures/Mounter.svelte'
55

6-
describe('mount and destroy', () => {
7-
const handleMount = vi.fn()
8-
const handleDestroy = vi.fn()
6+
const onMounted = vi.fn()
7+
const onDestroyed = vi.fn()
8+
const renderSubject = () => render(Mounter, { onMounted, onDestroyed })
99

10+
describe('mount and destroy', () => {
1011
test('component is mounted', async () => {
11-
await act(() => {
12-
render(Mounter, { onMounted: handleMount, onDestroyed: handleDestroy })
13-
})
12+
renderSubject()
1413

1514
const content = screen.getByRole('button')
1615

17-
expect(handleMount).toHaveBeenCalledOnce()
1816
expect(content).toBeInTheDocument()
17+
await act()
18+
expect(onMounted).toHaveBeenCalledOnce()
1919
})
2020

2121
test('component is destroyed', async () => {
22-
const { unmount } = render(Mounter, {
23-
onMounted: handleMount,
24-
onDestroyed: handleDestroy,
25-
})
22+
const { unmount } = renderSubject()
23+
24+
await act()
25+
unmount()
2626

27-
await act(() => unmount())
2827
const content = screen.queryByRole('button')
2928

30-
expect(handleDestroy).toHaveBeenCalledOnce()
3129
expect(content).not.toBeInTheDocument()
30+
await act()
31+
expect(onDestroyed).toHaveBeenCalledOnce()
3232
})
3333
})

src/__tests__/render.test.js

+40-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
12
import { beforeEach, describe, expect, test } from 'vitest'
23

34
import { act, render as stlRender } from '..'
@@ -11,13 +12,13 @@ describe('render', () => {
1112
return stlRender(Comp, {
1213
target: document.body,
1314
props,
14-
...additional
15+
...additional,
1516
})
1617
}
1718

1819
beforeEach(() => {
1920
props = {
20-
name: 'World'
21+
name: 'World',
2122
}
2223
})
2324

@@ -41,7 +42,9 @@ describe('render', () => {
4142
})
4243

4344
test('change props with accessors', async () => {
44-
const { component, getByText } = render({ accessors: true })
45+
const { component, getByText } = render(
46+
SVELTE_VERSION < '5' ? { accessors: true } : {}
47+
)
4548

4649
expect(getByText('Hello World!')).toBeInTheDocument()
4750

@@ -59,23 +62,41 @@ describe('render', () => {
5962
expect(getByText('Hello World!')).toBeInTheDocument()
6063
})
6164

62-
test('should accept svelte component options', () => {
63-
const target = document.createElement('div')
64-
const div = document.createElement('div')
65-
document.body.appendChild(target)
66-
target.appendChild(div)
67-
const { container } = stlRender(Comp, {
68-
target,
69-
anchor: div,
70-
props: { name: 'World' },
71-
context: new Map([['name', 'context']])
72-
})
73-
expect(container).toMatchSnapshot()
74-
})
65+
test.runIf(SVELTE_VERSION < '5')(
66+
'should accept svelte v4 component options',
67+
() => {
68+
const target = document.createElement('div')
69+
const div = document.createElement('div')
70+
document.body.appendChild(target)
71+
target.appendChild(div)
72+
const { container } = stlRender(Comp, {
73+
target,
74+
anchor: div,
75+
props: { name: 'World' },
76+
context: new Map([['name', 'context']]),
77+
})
78+
expect(container).toMatchSnapshot()
79+
}
80+
)
81+
82+
test.runIf(SVELTE_VERSION >= '5')(
83+
'should accept svelte v5 component options',
84+
() => {
85+
const target = document.createElement('section')
86+
document.body.appendChild(target)
87+
88+
const { container } = stlRender(Comp, {
89+
target,
90+
props: { name: 'World' },
91+
context: new Map([['name', 'context']]),
92+
})
93+
expect(container).toMatchSnapshot()
94+
}
95+
)
7596

7697
test('should throw error when mixing svelte component options and props', () => {
7798
expect(() => {
78-
stlRender(Comp, { anchor: '', name: 'World' })
99+
stlRender(Comp, { props: {}, name: 'World' })
79100
}).toThrow(/Unknown options were found/)
80101
})
81102

@@ -93,10 +114,8 @@ describe('render', () => {
93114

94115
test("accept the 'context' option", () => {
95116
const { getByText } = stlRender(Comp, {
96-
props: {
97-
name: 'Universe'
98-
},
99-
context: new Map([['name', 'context']])
117+
props: { name: 'Universe' },
118+
context: new Map([['name', 'context']]),
100119
})
101120

102121
expect(getByText('we have context')).toBeInTheDocument()

src/__tests__/rerender.test.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
/**
22
* @jest-environment jsdom
33
*/
4-
import { expect, test, vi } from 'vitest'
4+
import { describe, expect, test, vi } from 'vitest'
55
import { writable } from 'svelte/store'
66

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

10-
const mountCounter = writable(0)
11-
1210
test('mounts new component successfully', async () => {
11+
const onMounted = vi.fn()
12+
const onDestroyed = vi.fn()
13+
1314
const { getByTestId, rerender } = render(Comp, {
14-
props: { name: 'World 1' },
15-
context: new Map(Object.entries({ mountCounter })),
15+
props: { name: 'World 1', onMounted, onDestroyed },
1616
})
1717

1818
const expectToRender = (content) =>
1919
waitFor(() => {
2020
expect(getByTestId('test')).toHaveTextContent(content)
21-
expect(getByTestId('mount-counter')).toHaveTextContent('1')
21+
expect(onMounted).toHaveBeenCalledOnce()
2222
})
2323

2424
await expectToRender('Hello World 1!')
@@ -27,12 +27,15 @@ test('mounts new component successfully', async () => {
2727

2828
rerender({ props: { name: 'World 2' } })
2929
await expectToRender('Hello World 2!')
30+
expect(onDestroyed).not.toHaveBeenCalled()
3031

31-
expect(console.warn).toHaveBeenCalled()
32+
expect(console.warn).toHaveBeenCalledOnce()
3233

3334
console.warn.mockClear()
35+
onDestroyed.mockReset()
3436
rerender({ name: 'World 3' })
3537
await expectToRender('Hello World 3!')
38+
expect(onDestroyed).not.toHaveBeenCalled()
3639

3740
expect(console.warn).not.toHaveBeenCalled()
3841
})

src/__tests__/transition.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { userEvent } from '@testing-library/user-event'
2+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
23
import { beforeEach, describe, expect, test, vi } from 'vitest'
34

45
import { render, screen, waitFor } from '..'
56
import Transitioner from './fixtures/Transitioner.svelte'
67

7-
describe('transitions', () => {
8+
describe.runIf(SVELTE_VERSION < '5')('transitions', () => {
89
beforeEach(() => {
910
if (window.navigator.userAgent.includes('jsdom')) {
1011
const raf = (fn) => setTimeout(() => fn(new Date()), 16)

0 commit comments

Comments
 (0)