diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c6e83fa..ab433cc 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/package.json b/package.json
index 20bc1c2..938f563 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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"
}
}
diff --git a/src/__tests__/__snapshots__/render.test.js.snap b/src/__tests__/__snapshots__/render.test.js.snap
index a31cf2e..b9eb849 100644
--- a/src/__tests__/__snapshots__/render.test.js.snap
+++ b/src/__tests__/__snapshots__/render.test.js.snap
@@ -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`] = `
should accept svelte component options 1`] = `
-
`;
+
+exports[`render > should accept svelte v5 component options 1`] = `
+
+
+
+
+
+
+ Hello World!
+
+
+
+ we have context
+
+
+
+
+
+
+`;
diff --git a/src/__tests__/cleanup.test.js b/src/__tests__/cleanup.test.js
new file mode 100644
index 0000000..789338d
--- /dev/null
+++ b/src/__tests__/cleanup.test.js
@@ -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()
+ })
+})
diff --git a/src/__tests__/fixtures/Mounter.svelte b/src/__tests__/fixtures/Mounter.svelte
index 477bb34..51ebcd8 100644
--- a/src/__tests__/fixtures/Mounter.svelte
+++ b/src/__tests__/fixtures/Mounter.svelte
@@ -1,11 +1,19 @@
diff --git a/src/__tests__/fixtures/Rerender.svelte b/src/__tests__/fixtures/Rerender.svelte
index aac1600..1a3fa24 100644
--- a/src/__tests__/fixtures/Rerender.svelte
+++ b/src/__tests__/fixtures/Rerender.svelte
@@ -1,15 +1,17 @@
+ onExecuted?.()
+
+ onMount(() => onMounted?.())
-Hello {name}!
+ onDestroy(() => onDestroyed?.())
+
-{$mountCounter}
+Hello {name}!
diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js
index 830b513..898aa6a 100644
--- a/src/__tests__/mount.test.js
+++ b/src/__tests__/mount.test.js
@@ -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()
})
})
diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js
index 6beb984..262e062 100644
--- a/src/__tests__/render.test.js
+++ b/src/__tests__/render.test.js
@@ -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 '..'
@@ -11,13 +12,13 @@ describe('render', () => {
return stlRender(Comp, {
target: document.body,
props,
- ...additional
+ ...additional,
})
}
beforeEach(() => {
props = {
- name: 'World'
+ name: 'World',
}
})
@@ -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()
@@ -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/)
})
@@ -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()
diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js
index d6cbc21..922ea63 100644
--- a/src/__tests__/rerender.test.js
+++ b/src/__tests__/rerender.test.js
@@ -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!')
@@ -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()
})
diff --git a/src/__tests__/transition.test.js b/src/__tests__/transition.test.js
index 5b1ff18..e8191f5 100644
--- a/src/__tests__/transition.test.js
+++ b/src/__tests__/transition.test.js
@@ -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)
diff --git a/src/pure.js b/src/pure.js
index 59c62ff..a672b64 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -3,19 +3,15 @@ import {
getQueriesForElement,
prettyDOM,
} from '@testing-library/dom'
-import { tick } from 'svelte'
+import * as Svelte from 'svelte'
-const containerCache = new Set()
+const IS_SVELTE_5 = typeof Svelte.createRoot === 'function'
+const targetCache = new Set()
const componentCache = new Set()
-const svelteComponentOptions = [
- 'accessors',
- 'anchor',
- 'props',
- 'hydrate',
- 'intro',
- 'context',
-]
+const svelteComponentOptions = IS_SVELTE_5
+ ? ['target', 'props', 'events', 'context', 'intro', 'recover']
+ : ['accessors', 'anchor', 'props', 'hydrate', 'intro', 'context']
const render = (
Component,
@@ -24,6 +20,7 @@ const render = (
) => {
container = container || document.body
target = target || container.appendChild(document.createElement('div'))
+ targetCache.add(target)
const ComponentConstructor = Component.default || Component
@@ -54,17 +51,27 @@ const render = (
return { props: options }
}
- let component = new ComponentConstructor({
- target,
- ...checkProps(options),
- })
+ const renderComponent = (options) => {
+ options = { target, ...checkProps(options) }
- containerCache.add({ container, target, component })
- componentCache.add(component)
+ const component = IS_SVELTE_5
+ ? Svelte.createRoot(ComponentConstructor, options)
+ : new ComponentConstructor(options)
- component.$$.on_destroy.push(() => {
- componentCache.delete(component)
- })
+ componentCache.add(component)
+
+ // TODO(mcous, 2024-02-11): remove this behavior in the next major version
+ // It is unnecessary has no path to implementation in Svelte v5
+ if (!IS_SVELTE_5) {
+ component.$$.on_destroy.push(() => {
+ componentCache.delete(component)
+ })
+ }
+
+ return component
+ }
+
+ let component = renderComponent(options)
return {
container,
@@ -78,48 +85,53 @@ const render = (
props = props.props
}
component.$set(props)
- await tick()
+ await Svelte.tick()
},
unmount: () => {
- if (componentCache.has(component)) component.$destroy()
+ cleanupComponent(component)
},
...getQueriesForElement(container, queries),
}
}
-const cleanupAtContainer = (cached) => {
- const { target, component } = cached
+const cleanupComponent = (component) => {
+ const inCache = componentCache.delete(component)
- if (componentCache.has(component)) component.$destroy()
+ if (inCache) {
+ component.$destroy()
+ }
+}
- if (target.parentNode === document.body) {
+const cleanupTarget = (target) => {
+ const inCache = targetCache.delete(target)
+
+ if (inCache && target.parentNode === document.body) {
document.body.removeChild(target)
}
-
- containerCache.delete(cached)
}
const cleanup = () => {
- Array.from(containerCache.keys()).forEach(cleanupAtContainer)
+ componentCache.forEach(cleanupComponent)
+ targetCache.forEach(cleanupTarget)
}
const act = async (fn) => {
if (fn) {
await fn()
}
- return tick()
+ return Svelte.tick()
}
const fireEvent = async (...args) => {
const event = dtlFireEvent(...args)
- await tick()
+ await Svelte.tick()
return event
}
Object.keys(dtlFireEvent).forEach((key) => {
fireEvent[key] = async (...args) => {
const event = dtlFireEvent[key](...args)
- await tick()
+ await Svelte.tick()
return event
}
})
diff --git a/vite.config.js b/vite.config.js
index 4baf76f..0ad04cc 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -3,7 +3,7 @@ import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
- plugins: [svelte()],
+ plugins: [svelte({ hot: false })],
resolve: {
// Ensure `browser` exports are used in tests
// Vitest prefers modules' `node` export by default