Skip to content

Commit

Permalink
feat: add Svelte v5-next support (#321)
Browse files Browse the repository at this point in the history
* fix: remove DOM elements even if component creation fails

Fixes #190

* feat: add Svelte v5-next support
  • Loading branch information
mcous committed Feb 13, 2024
1 parent e13a6b1 commit 178b2de
Show file tree
Hide file tree
Showing 12 changed files with 210 additions and 95 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@ jobs:
if: ${{ !contains(github.head_ref, 'all-contributors') }}
name: Node ${{ matrix.node }}, Svelte ${{ matrix.svelte }}, ${{ matrix.test-runner }}
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
node: ['16', '18', '20']
svelte: ['3', '4']
test-runner: ['vitest:jsdom', 'vitest:happy-dom']
experimental: [false]
include:
- node: '20'
svelte: 'next'
test-runner: 'vitest:jsdom'
experimental: true
- node: '20'
svelte: 'next'
test-runner: 'vitest:happy-dom'
experimental: true

steps:
- name: ⬇️ Checkout repo
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@
"contributors:generate": "all-contributors generate"
},
"peerDependencies": {
"svelte": "^3 || ^4"
"svelte": "^3 || ^4 || ^5"
},
"dependencies": {
"@testing-library/dom": "^9.3.1"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.4.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@testing-library/jest-dom": "^6.3.0",
"@testing-library/user-event": "^14.5.2",
"@typescript-eslint/eslint-plugin": "6.19.1",
Expand All @@ -94,11 +94,11 @@
"npm-run-all": "^4.1.5",
"prettier": "3.2.4",
"prettier-plugin-svelte": "3.1.2",
"svelte": "^3 || ^4",
"svelte": "^4.2.10",
"svelte-check": "^3.6.3",
"svelte-jester": "^3.0.0",
"typescript": "^5.3.3",
"vite": "^4.3.9",
"vite": "^5.1.1",
"vitest": "^0.33.0"
}
}
27 changes: 25 additions & 2 deletions src/__tests__/__snapshots__/render.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`render > should accept svelte component options 1`] = `
exports[`render > should accept svelte v4 component options 1`] = `
<body>
<div>
<h1
Expand All @@ -18,8 +18,31 @@ exports[`render > should accept svelte component options 1`] = `
<button>
Button
</button>
<!--&lt;Comp&gt;-->
<div />
</div>
</body>
`;

exports[`render > should accept svelte v5 component options 1`] = `
<body>
<section>
<h1
data-testid="test"
>
Hello World!
</h1>
<div>
we have context
</div>
<button>
Button
</button>
</section>
</body>
`;
35 changes: 35 additions & 0 deletions src/__tests__/cleanup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test, vi } from 'vitest'

import { act, cleanup, render } from '..'
import Mounter from './fixtures/Mounter.svelte'

const onExecuted = vi.fn()
const onDestroyed = vi.fn()
const renderSubject = () => render(Mounter, { onExecuted, onDestroyed })

describe('cleanup', () => {
test('cleanup deletes element', async () => {
renderSubject()
cleanup()

expect(document.body).toBeEmptyDOMElement()
})

test('cleanup unmounts component', async () => {
await act(renderSubject)
cleanup()

expect(onDestroyed).toHaveBeenCalledOnce()
})

test('cleanup handles unexpected errors during mount', () => {
onExecuted.mockImplementation(() => {
throw new Error('oh no!')
})

expect(renderSubject).toThrowError()
cleanup()

expect(document.body).toBeEmptyDOMElement()
})
})
18 changes: 13 additions & 5 deletions src/__tests__/fixtures/Mounter.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
<script>
import { onDestroy,onMount } from 'svelte'
import { onDestroy, onMount } from 'svelte'
export let onMounted
export let onDestroyed
export let onExecuted = undefined
export let onMounted = undefined
export let onDestroyed = undefined
onMount(() => onMounted())
onDestroy(() => onDestroyed())
onExecuted?.()
onMount(() => {
onMounted?.()
})
onDestroy(() => {
onDestroyed?.()
})
</script>
<button />
20 changes: 11 additions & 9 deletions src/__tests__/fixtures/Rerender.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<script>
import { getContext, onMount } from 'svelte'
import { onDestroy, onMount } from 'svelte'
export let name
export let onExecuted = undefined
export let onMounted = undefined
export let onDestroyed = undefined
const mountCounter = getContext('mountCounter')
export let name = ''
onMount(() => {
mountCounter.update((i) => i + 1)
})
</script>
onExecuted?.()
onMount(() => onMounted?.())
<h1 data-testid="test">Hello {name}!</h1>
onDestroy(() => onDestroyed?.())
</script>
<div data-testid="mount-counter">{$mountCounter}</div>
<div data-testid="test">Hello {name}!</div>
26 changes: 13 additions & 13 deletions src/__tests__/mount.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@ import { describe, expect, test, vi } from 'vitest'
import { act, render, screen } from '..'
import Mounter from './fixtures/Mounter.svelte'

describe('mount and destroy', () => {
const handleMount = vi.fn()
const handleDestroy = vi.fn()
const onMounted = vi.fn()
const onDestroyed = vi.fn()
const renderSubject = () => render(Mounter, { onMounted, onDestroyed })

describe('mount and destroy', () => {
test('component is mounted', async () => {
await act(() => {
render(Mounter, { onMounted: handleMount, onDestroyed: handleDestroy })
})
renderSubject()

const content = screen.getByRole('button')

expect(handleMount).toHaveBeenCalledOnce()
expect(content).toBeInTheDocument()
await act()
expect(onMounted).toHaveBeenCalledOnce()
})

test('component is destroyed', async () => {
const { unmount } = render(Mounter, {
onMounted: handleMount,
onDestroyed: handleDestroy,
})
const { unmount } = renderSubject()

await act()
unmount()

await act(() => unmount())
const content = screen.queryByRole('button')

expect(handleDestroy).toHaveBeenCalledOnce()
expect(content).not.toBeInTheDocument()
await act()
expect(onDestroyed).toHaveBeenCalledOnce()
})
})
61 changes: 40 additions & 21 deletions src/__tests__/render.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
import { beforeEach, describe, expect, test } from 'vitest'

import { act, render as stlRender } from '..'
Expand All @@ -11,13 +12,13 @@ describe('render', () => {
return stlRender(Comp, {
target: document.body,
props,
...additional
...additional,
})
}

beforeEach(() => {
props = {
name: 'World'
name: 'World',
}
})

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

test('change props with accessors', async () => {
const { component, getByText } = render({ accessors: true })
const { component, getByText } = render(
SVELTE_VERSION < '5' ? { accessors: true } : {}
)

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

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

test('should accept svelte component options', () => {
const target = document.createElement('div')
const div = document.createElement('div')
document.body.appendChild(target)
target.appendChild(div)
const { container } = stlRender(Comp, {
target,
anchor: div,
props: { name: 'World' },
context: new Map([['name', 'context']])
})
expect(container).toMatchSnapshot()
})
test.runIf(SVELTE_VERSION < '5')(
'should accept svelte v4 component options',
() => {
const target = document.createElement('div')
const div = document.createElement('div')
document.body.appendChild(target)
target.appendChild(div)
const { container } = stlRender(Comp, {
target,
anchor: div,
props: { name: 'World' },
context: new Map([['name', 'context']]),
})
expect(container).toMatchSnapshot()
}
)

test.runIf(SVELTE_VERSION >= '5')(
'should accept svelte v5 component options',
() => {
const target = document.createElement('section')
document.body.appendChild(target)

const { container } = stlRender(Comp, {
target,
props: { name: 'World' },
context: new Map([['name', 'context']]),
})
expect(container).toMatchSnapshot()
}
)

test('should throw error when mixing svelte component options and props', () => {
expect(() => {
stlRender(Comp, { anchor: '', name: 'World' })
stlRender(Comp, { props: {}, name: 'World' })
}).toThrow(/Unknown options were found/)
})

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

test("accept the 'context' option", () => {
const { getByText } = stlRender(Comp, {
props: {
name: 'Universe'
},
context: new Map([['name', 'context']])
props: { name: 'Universe' },
context: new Map([['name', 'context']]),
})

expect(getByText('we have context')).toBeInTheDocument()
Expand Down
19 changes: 11 additions & 8 deletions src/__tests__/rerender.test.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
/**
* @jest-environment jsdom
*/
import { expect, test, vi } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
import { writable } from 'svelte/store'

import { render, waitFor } from '..'
import { act, render, waitFor } from '..'
import Comp from './fixtures/Rerender.svelte'

const mountCounter = writable(0)

test('mounts new component successfully', async () => {
const onMounted = vi.fn()
const onDestroyed = vi.fn()

const { getByTestId, rerender } = render(Comp, {
props: { name: 'World 1' },
context: new Map(Object.entries({ mountCounter })),
props: { name: 'World 1', onMounted, onDestroyed },
})

const expectToRender = (content) =>
waitFor(() => {
expect(getByTestId('test')).toHaveTextContent(content)
expect(getByTestId('mount-counter')).toHaveTextContent('1')
expect(onMounted).toHaveBeenCalledOnce()
})

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

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

expect(console.warn).toHaveBeenCalled()
expect(console.warn).toHaveBeenCalledOnce()

console.warn.mockClear()
onDestroyed.mockReset()
rerender({ name: 'World 3' })
await expectToRender('Hello World 3!')
expect(onDestroyed).not.toHaveBeenCalled()

expect(console.warn).not.toHaveBeenCalled()
})
3 changes: 2 additions & 1 deletion src/__tests__/transition.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { userEvent } from '@testing-library/user-event'
import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
import { beforeEach, describe, expect, test, vi } from 'vitest'

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

describe('transitions', () => {
describe.runIf(SVELTE_VERSION < '5')('transitions', () => {
beforeEach(() => {
if (window.navigator.userAgent.includes('jsdom')) {
const raf = (fn) => setTimeout(() => fn(new Date()), 16)
Expand Down

0 comments on commit 178b2de

Please sign in to comment.