diff --git a/README.md b/README.md index d49bbae..0a39429 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,21 @@ This library has `peerDependencies` listings for `svelte >= 3`. You may also be interested in installing `@testing-library/jest-dom` so you can use [the custom jest matchers](https://github.com/testing-library/jest-dom). +### Svelte 5 support + +If you are riding the bleeding edge of Svelte 5, you'll need to either +import from `@testing-library/svelte/svelte5` instead of `@testing-library/svelte`, or have your `vite.config.js` contains the following alias: + +``` +export default defineConfig(({ }) => ({ + test: { + alias: { + '@testing-library/svelte': '@testing-library/svelte/svelte5' + } + }, +})) +``` + ## Docs See the [**docs**](https://testing-library.com/docs/svelte-testing-library/intro) over at the Testing Library website. diff --git a/package.json b/package.json index 938f563..4e8c6f1 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ "types": "./types/index.d.ts", "default": "./src/index.js" }, + "./svelte5": { + "types": "./types/index.d.ts", + "default": "./src/svelte5-index.js" + }, "./vitest": { "default": "./src/vitest.js" } diff --git a/src/__tests__/act.test.js b/src/__tests__/act.test.js index 6eafc0d..57fa5b9 100644 --- a/src/__tests__/act.test.js +++ b/src/__tests__/act.test.js @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { act, fireEvent, render as stlRender } from '..' +import { act, fireEvent, render as stlRender } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' describe('act', () => { diff --git a/src/__tests__/auto-cleanup-skip.test.js b/src/__tests__/auto-cleanup-skip.test.js index 265f55b..db65447 100644 --- a/src/__tests__/auto-cleanup-skip.test.js +++ b/src/__tests__/auto-cleanup-skip.test.js @@ -7,7 +7,7 @@ describe('auto-cleanup-skip', () => { beforeAll(async () => { process.env.STL_SKIP_AUTO_CLEANUP = 'true' - const stl = await import('..') + const stl = await import('@testing-library/svelte') render = stl.render }) diff --git a/src/__tests__/auto-cleanup.test.js b/src/__tests__/auto-cleanup.test.js index 349ee39..4d38d35 100644 --- a/src/__tests__/auto-cleanup.test.js +++ b/src/__tests__/auto-cleanup.test.js @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' -import { render } from '..' +import { render } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' describe('auto-cleanup', () => { diff --git a/src/__tests__/cleanup.test.js b/src/__tests__/cleanup.test.js index 789338d..3e49548 100644 --- a/src/__tests__/cleanup.test.js +++ b/src/__tests__/cleanup.test.js @@ -1,6 +1,7 @@ import { describe, expect, test, vi } from 'vitest' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { act, cleanup, render } from '..' +import { act, cleanup, render } from '@testing-library/svelte' import Mounter from './fixtures/Mounter.svelte' const onExecuted = vi.fn() @@ -15,7 +16,7 @@ describe('cleanup', () => { expect(document.body).toBeEmptyDOMElement() }) - test('cleanup unmounts component', async () => { + test.runIf(SVELTE_VERSION < '5')('cleanup unmounts component', async () => { await act(renderSubject) cleanup() diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js index effdef4..e7f2fd5 100644 --- a/src/__tests__/context.test.js +++ b/src/__tests__/context.test.js @@ -1,9 +1,10 @@ import { expect, test } from 'vitest' -import { render } from '..' +import { render } from '@testing-library/svelte' import Comp from './fixtures/Context.svelte' +import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js' -test('can set a context', () => { +test.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('can set a context', () => { const message = 'Got it' const { getByText } = render(Comp, { diff --git a/src/__tests__/debug.test.js b/src/__tests__/debug.test.js index 1072a0f..2a9c6e3 100644 --- a/src/__tests__/debug.test.js +++ b/src/__tests__/debug.test.js @@ -1,7 +1,7 @@ import { prettyDOM } from '@testing-library/dom' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { render } from '..' +import { render } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' describe('debug', () => { diff --git a/src/__tests__/events.test.js b/src/__tests__/events.test.js index fccf990..16f688c 100644 --- a/src/__tests__/events.test.js +++ b/src/__tests__/events.test.js @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' -import { fireEvent, render } from '..' +import { fireEvent, render } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' describe('events', () => { diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js index 898aa6a..df0792b 100644 --- a/src/__tests__/mount.test.js +++ b/src/__tests__/mount.test.js @@ -1,13 +1,14 @@ import { describe, expect, test, vi } from 'vitest' -import { act, render, screen } from '..' +import { act, render, screen } from '@testing-library/svelte' import Mounter from './fixtures/Mounter.svelte' +import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js' const onMounted = vi.fn() const onDestroyed = vi.fn() const renderSubject = () => render(Mounter, { onMounted, onDestroyed }) -describe('mount and destroy', () => { +describe.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('mount and destroy', () => { test('component is mounted', async () => { renderSubject() diff --git a/src/__tests__/multi-base.test.js b/src/__tests__/multi-base.test.js index 39f28d1..bad628e 100644 --- a/src/__tests__/multi-base.test.js +++ b/src/__tests__/multi-base.test.js @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest' -import { render } from '..' +import { render } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' describe('multi-base', () => { diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js index 262e062..cb30b77 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -1,7 +1,7 @@ import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { beforeEach, describe, expect, test } from 'vitest' -import { act, render as stlRender } from '..' +import { act, render as stlRender } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' import CompDefault from './fixtures/Comp2.svelte' @@ -107,7 +107,7 @@ describe('render', () => { }) test('correctly find component constructor on the default property', () => { - const { getByText } = render(CompDefault, { props: { name: 'World' } }) + const { getByText } = stlRender(CompDefault, { props: { name: 'World' } }) expect(getByText('Hello World!')).toBeInTheDocument() }) diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js index 922ea63..6fabf36 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -1,10 +1,10 @@ /** * @jest-environment jsdom */ -import { describe, expect, test, vi } from 'vitest' -import { writable } from 'svelte/store' +import { expect, test, vi } from 'vitest' + +import { render, waitFor } from '@testing-library/svelte' -import { act, render, waitFor } from '..' import Comp from './fixtures/Rerender.svelte' test('mounts new component successfully', async () => { diff --git a/src/__tests__/transition.test.js b/src/__tests__/transition.test.js index e8191f5..7e7dd3d 100644 --- a/src/__tests__/transition.test.js +++ b/src/__tests__/transition.test.js @@ -1,16 +1,17 @@ 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 { IS_JSDOM, IS_SVELTE_5 } from './utils.js' + +import { render, screen, waitFor } from '@testing-library/svelte' import Transitioner from './fixtures/Transitioner.svelte' -describe.runIf(SVELTE_VERSION < '5')('transitions', () => { +describe.runIf(!IS_SVELTE_5)('transitions', () => { beforeEach(() => { - if (window.navigator.userAgent.includes('jsdom')) { - const raf = (fn) => setTimeout(() => fn(new Date()), 16) - vi.stubGlobal('requestAnimationFrame', raf) - } + if (!IS_JSDOM) return + + const raf = (fn) => setTimeout(() => fn(new Date()), 16) + vi.stubGlobal('requestAnimationFrame', raf) }) test('on:introend', async () => { diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js new file mode 100644 index 0000000..69be184 --- /dev/null +++ b/src/__tests__/utils.js @@ -0,0 +1,7 @@ +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' + +export const IS_JSDOM = window.navigator.userAgent.includes('jsdom') + +export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js + +export const IS_SVELTE_5 = SVELTE_VERSION >= '5' diff --git a/src/index.js b/src/index.js index e94d814..4181419 100644 --- a/src/index.js +++ b/src/index.js @@ -13,3 +13,4 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) { } export * from './pure.js' +export * from '@testing-library/dom' diff --git a/src/pure.js b/src/pure.js index a672b64..3c412da 100644 --- a/src/pure.js +++ b/src/pure.js @@ -3,60 +3,60 @@ import { getQueriesForElement, prettyDOM, } from '@testing-library/dom' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import * as Svelte from 'svelte' -const IS_SVELTE_5 = typeof Svelte.createRoot === 'function' -const targetCache = new Set() -const componentCache = new Set() - -const svelteComponentOptions = IS_SVELTE_5 - ? ['target', 'props', 'events', 'context', 'intro', 'recover'] - : ['accessors', 'anchor', 'props', 'hydrate', 'intro', 'context'] - -const render = ( - Component, - { target, ...options } = {}, - { container, queries } = {} -) => { - container = container || document.body - target = target || container.appendChild(document.createElement('div')) - targetCache.add(target) - - const ComponentConstructor = Component.default || Component - - const checkProps = (options) => { - const isProps = !Object.keys(options).some((option) => - svelteComponentOptions.includes(option) +const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) +export const targetCache = new Set() +export const componentCache = new Set() + +const svelteComponentOptions = [ + 'accessors', + 'anchor', + 'props', + 'hydrate', + 'intro', + 'context', +] + +export const buildCheckProps = (svelteComponentOptions) => (options) => { + const isProps = !Object.keys(options).some((option) => + svelteComponentOptions.includes(option) + ) + + // Check if any props and Svelte options were accidentally mixed. + if (!isProps) { + const unrecognizedOptions = Object.keys(options).filter( + (option) => !svelteComponentOptions.includes(option) ) - // Check if any props and Svelte options were accidentally mixed. - if (!isProps) { - const unrecognizedOptions = Object.keys(options).filter( - (option) => !svelteComponentOptions.includes(option) - ) - - if (unrecognizedOptions.length > 0) { - throw Error(` + if (unrecognizedOptions.length > 0) { + throw Error(` Unknown options were found [${unrecognizedOptions}]. This might happen if you've mixed passing in props with Svelte options into the render function. Valid Svelte options are [${svelteComponentOptions}]. You can either change the prop names, or pass in your props for that component via the \`props\` option.\n\n Eg: const { /** Results **/ } = render(MyComponent, { props: { /** props here **/ } })\n\n `) - } - - return options } - return { props: options } + return options } - const renderComponent = (options) => { + return { props: options } +} + +const checkProps = buildCheckProps(svelteComponentOptions) + +const buildRenderComponent = + ({ target, ComponentConstructor }) => + (options) => { options = { target, ...checkProps(options) } - const component = IS_SVELTE_5 - ? Svelte.createRoot(ComponentConstructor, options) - : new ComponentConstructor(options) + if (IS_SVELTE_5) + throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') + + const component = new ComponentConstructor(options) componentCache.add(component) @@ -71,30 +71,46 @@ const render = ( return component } - let component = renderComponent(options) - - return { - container, - component, - debug: (el = container) => console.log(prettyDOM(el)), - rerender: async (props) => { - if (props.props) { - console.warn( - 'rerender({ props: {...} }) deprecated, use rerender({...}) instead' - ) - props = props.props - } - component.$set(props) - await Svelte.tick() - }, - unmount: () => { - cleanupComponent(component) - }, - ...getQueriesForElement(container, queries), +export const buildRender = + (buildRenderComponent) => + (Component, { target, ...options } = {}, { container, queries } = {}) => { + container = container || document.body + target = target || container.appendChild(document.createElement('div')) + targetCache.add(target) + + const ComponentConstructor = Component.default || Component + + const renderComponent = buildRenderComponent({ + target, + ComponentConstructor, + }) + + let component = renderComponent(options) + + return { + container, + component, + debug: (el = container) => console.log(prettyDOM(el)), + rerender: async (props) => { + if (props.props) { + console.warn( + 'rerender({ props: {...} }) deprecated, use rerender({...}) instead' + ) + props = props.props + } + component.$set(props) + await Svelte.tick() + }, + unmount: () => { + cleanupComponent(component) + }, + ...getQueriesForElement(container, queries), + } } -} -const cleanupComponent = (component) => { +export const render = buildRender(buildRenderComponent) + +export const cleanupComponent = (component) => { const inCache = componentCache.delete(component) if (inCache) { @@ -110,19 +126,19 @@ const cleanupTarget = (target) => { } } -const cleanup = () => { +export const cleanup = () => { componentCache.forEach(cleanupComponent) targetCache.forEach(cleanupTarget) } -const act = async (fn) => { +export const act = async (fn) => { if (fn) { await fn() } return Svelte.tick() } -const fireEvent = async (...args) => { +export const fireEvent = async (...args) => { const event = dtlFireEvent(...args) await Svelte.tick() return event @@ -135,9 +151,3 @@ Object.keys(dtlFireEvent).forEach((key) => { return event } }) - -/* eslint-disable import/export */ - -export * from '@testing-library/dom' - -export { render, cleanup, fireEvent, act } diff --git a/src/svelte5-index.js b/src/svelte5-index.js new file mode 100644 index 0000000..1770eac --- /dev/null +++ b/src/svelte5-index.js @@ -0,0 +1,17 @@ +import { act, cleanup } from './svelte5.js' + +// If we're running in a test runner that supports afterEach +// then we'll automatically run cleanup afterEach test +// this ensures that tests run in isolation from each other +// if you don't like this then either import the `pure` module +// or set the STL_SKIP_AUTO_CLEANUP env variable to 'true'. +if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) { + afterEach(async () => { + await act() + cleanup() + }) +} + +export * from './svelte5.js' +export * from '@testing-library/dom' +export { act, fireEvent } from './pure.js' diff --git a/src/svelte5.js b/src/svelte5.js new file mode 100644 index 0000000..6f30784 --- /dev/null +++ b/src/svelte5.js @@ -0,0 +1,41 @@ +import { createClassComponent } from 'svelte/legacy' +import { + componentCache, + cleanup, + buildCheckProps, + buildRender, +} from './pure.js' + +const svelteComponentOptions = [ + 'target', + 'props', + 'events', + 'context', + 'intro', + 'recover', +] + +const checkProps = buildCheckProps(svelteComponentOptions) + +const buildRenderComponent = + ({ target, ComponentConstructor }) => + (options) => { + options = { target, ...checkProps(options) } + + const component = createClassComponent({ + component: ComponentConstructor, + ...options, + }) + + componentCache.add(component) + + return component + } + +const render = buildRender(buildRenderComponent) + +/* eslint-disable import/export */ + +import { act, fireEvent } from './pure.js' + +export { render, cleanup, fireEvent, act } diff --git a/src/vitest.js b/src/vitest.js index 135ddbe..2f9930c 100644 --- a/src/vitest.js +++ b/src/vitest.js @@ -1,6 +1,6 @@ import { afterEach } from 'vitest' -import { act, cleanup } from './pure.js' +import { act, cleanup } from '@testing-library/svelte' afterEach(async () => { await act() diff --git a/vite.config.js b/vite.config.js index 0ad04cc..3a8aaf2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,19 @@ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vite' +import path from 'path' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' + +const IS_SVELTE_5 = SVELTE_VERSION >= '5' + +const alias = [ + { + find: '@testing-library/svelte', + replacement: path.resolve( + __dirname, + IS_SVELTE_5 ? 'src/svelte5-index.js' : 'src/index.js' + ), + }, +] // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ @@ -12,6 +26,7 @@ export default defineConfig(({ mode }) => ({ conditions: mode === 'test' ? ['browser'] : [], }, test: { + alias, environment: 'jsdom', setupFiles: ['./src/__tests__/_vitest-setup.js'], mockReset: true,