From 44b20b466217c3f643b486b97976bc5114427901 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 22 Feb 2024 11:04:32 -0500 Subject: [PATCH 1/9] fix: make the latest Svelte 5 pass all tests For now I've resorted to use the legacy API, as the use of runes don't seem to work in the test environment (which, mind you, could be a problem on this side of the keyboard) and the important part is to have the package work with Svelte 5. --- src/__tests__/cleanup.test.js | 3 ++- src/__tests__/render.test.js | 2 +- src/pure.js | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/__tests__/cleanup.test.js b/src/__tests__/cleanup.test.js index 789338d..ceedbea 100644 --- a/src/__tests__/cleanup.test.js +++ b/src/__tests__/cleanup.test.js @@ -1,4 +1,5 @@ import { describe, expect, test, vi } from 'vitest' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { act, cleanup, render } from '..' import Mounter from './fixtures/Mounter.svelte' @@ -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__/render.test.js b/src/__tests__/render.test.js index 262e062..b1d96f3 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -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/pure.js b/src/pure.js index a672b64..8614e17 100644 --- a/src/pure.js +++ b/src/pure.js @@ -3,9 +3,11 @@ import { getQueriesForElement, prettyDOM, } from '@testing-library/dom' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' +import { createClassComponent as createComponentSvelte5 } from 'svelte/legacy' import * as Svelte from 'svelte' -const IS_SVELTE_5 = typeof Svelte.createRoot === 'function' +const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) const targetCache = new Set() const componentCache = new Set() @@ -55,7 +57,7 @@ const render = ( options = { target, ...checkProps(options) } const component = IS_SVELTE_5 - ? Svelte.createRoot(ComponentConstructor, options) + ? createComponentSvelte5({ component: ComponentConstructor, ...options }) : new ComponentConstructor(options) componentCache.add(component) From d55fbd7d30faf28ee34ea4b99ff80c080ac45ee0 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 22 Feb 2024 11:36:44 -0500 Subject: [PATCH 2/9] alias pure.js to svelte5.js --- src/pure.js | 11 ++-- src/svelte5.js | 145 +++++++++++++++++++++++++++++++++++++++++++++++++ vite.config.js | 6 ++ 3 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 src/svelte5.js diff --git a/src/pure.js b/src/pure.js index 8614e17..28427f3 100644 --- a/src/pure.js +++ b/src/pure.js @@ -4,13 +4,15 @@ import { prettyDOM, } from '@testing-library/dom' import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { createClassComponent as createComponentSvelte5 } from 'svelte/legacy' import * as Svelte from 'svelte' const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) const targetCache = new Set() const componentCache = new Set() +if (IS_SVELTE_5) + console.warn('for Svelte 5, use `@testing-library/svelte/svelte5`') + const svelteComponentOptions = IS_SVELTE_5 ? ['target', 'props', 'events', 'context', 'intro', 'recover'] : ['accessors', 'anchor', 'props', 'hydrate', 'intro', 'context'] @@ -56,9 +58,10 @@ const render = ( const renderComponent = (options) => { options = { target, ...checkProps(options) } - const component = IS_SVELTE_5 - ? createComponentSvelte5({ component: 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) diff --git a/src/svelte5.js b/src/svelte5.js new file mode 100644 index 0000000..8614e17 --- /dev/null +++ b/src/svelte5.js @@ -0,0 +1,145 @@ +import { + fireEvent as dtlFireEvent, + getQueriesForElement, + prettyDOM, +} from '@testing-library/dom' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' +import { createClassComponent as createComponentSvelte5 } from 'svelte/legacy' +import * as Svelte from 'svelte' + +const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) +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) + ) + + // 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(` + 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 } + } + + const renderComponent = (options) => { + options = { target, ...checkProps(options) } + + const component = IS_SVELTE_5 + ? createComponentSvelte5({ component: ComponentConstructor, ...options }) + : new ComponentConstructor(options) + + 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, + 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) => { + const inCache = componentCache.delete(component) + + if (inCache) { + component.$destroy() + } +} + +const cleanupTarget = (target) => { + const inCache = targetCache.delete(target) + + if (inCache && target.parentNode === document.body) { + document.body.removeChild(target) + } +} + +const cleanup = () => { + componentCache.forEach(cleanupComponent) + targetCache.forEach(cleanupTarget) +} + +const act = async (fn) => { + if (fn) { + await fn() + } + return Svelte.tick() +} + +const fireEvent = async (...args) => { + const event = dtlFireEvent(...args) + await Svelte.tick() + return event +} + +Object.keys(dtlFireEvent).forEach((key) => { + fireEvent[key] = async (...args) => { + const event = dtlFireEvent[key](...args) + await Svelte.tick() + return event + } +}) + +/* eslint-disable import/export */ + +export * from '@testing-library/dom' + +export { render, cleanup, fireEvent, act } diff --git a/vite.config.js b/vite.config.js index 0ad04cc..2b2bc5d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,10 @@ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vite' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' + +const alias = {} + +if (SVELTE_VERSION >= '5') alias['./pure.js'] = './svelte5.js' // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ @@ -20,5 +25,6 @@ export default defineConfig(({ mode }) => ({ provider: 'v8', include: ['src'], }, + alias, }, })) From 6f494ada0ac80344937a8f10a1cb1a57716ae5d9 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 22 Feb 2024 11:48:49 -0500 Subject: [PATCH 3/9] fix: test fails for Svelte5 and happy-dom --- package.json | 4 ++-- src/__tests__/mount.test.js | 40 ++++++++++++++++++++----------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 938f563..b2eea3e 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,8 @@ "test": "vitest run --coverage", "test:watch": "vitest", "test:update": "vitest run --update", - "test:vitest:jsdom": "vitest run --coverage --environment jsdom", - "test:vitest:happy-dom": "vitest run --coverage --environment happy-dom", + "test:vitest:jsdom": "VITEST_ENV=jsdom vitest run --coverage --environment jsdom", + "test:vitest:happy-dom": "VITEST_ENV=happy-dom vitest run --coverage --environment happy-dom", "types": "svelte-check", "validate": "npm-run-all test:vitest:* types", "contributors:add": "all-contributors add", diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js index 898aa6a..7edf348 100644 --- a/src/__tests__/mount.test.js +++ b/src/__tests__/mount.test.js @@ -1,4 +1,5 @@ import { describe, expect, test, vi } from 'vitest' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { act, render, screen } from '..' import Mounter from './fixtures/Mounter.svelte' @@ -7,27 +8,30 @@ const onMounted = vi.fn() const onDestroyed = vi.fn() const renderSubject = () => render(Mounter, { onMounted, onDestroyed }) -describe('mount and destroy', () => { - test('component is mounted', async () => { - renderSubject() +describe.skipIf(SVELTE_VERSION >= '5' && process.env.VITEST_ENV == 'happy-dom')( + 'mount and destroy', + () => { + test('component is mounted', async () => { + renderSubject() - const content = screen.getByRole('button') + const content = screen.getByRole('button') - expect(content).toBeInTheDocument() - await act() - expect(onMounted).toHaveBeenCalledOnce() - }) + expect(content).toBeInTheDocument() + await act() + expect(onMounted).toHaveBeenCalledOnce() + }) - test('component is destroyed', async () => { - const { unmount } = renderSubject() + test('component is destroyed', async () => { + const { unmount } = renderSubject() - await act() - unmount() + await act() + unmount() - const content = screen.queryByRole('button') + const content = screen.queryByRole('button') - expect(content).not.toBeInTheDocument() - await act() - expect(onDestroyed).toHaveBeenCalledOnce() - }) -}) + expect(content).not.toBeInTheDocument() + await act() + expect(onDestroyed).toHaveBeenCalledOnce() + }) + } +) From 914a5bb40cb433bdbaac3524eeee38f4e41a1bc9 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 22 Feb 2024 12:02:03 -0500 Subject: [PATCH 4/9] context.test.js fails too --- src/__tests__/context.test.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js index effdef4..cff0475 100644 --- a/src/__tests__/context.test.js +++ b/src/__tests__/context.test.js @@ -1,14 +1,18 @@ import { expect, test } from 'vitest' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { render } from '..' import Comp from './fixtures/Context.svelte' -test('can set a context', () => { - const message = 'Got it' +test.skipIf(SVELTE_VERSION >= '5' && process.env.VITEST_ENV == 'happy-dom')( + 'can set a context', + () => { + const message = 'Got it' - const { getByText } = render(Comp, { - context: new Map(Object.entries({ foo: { message } })), - }) + const { getByText } = render(Comp, { + context: new Map(Object.entries({ foo: { message } })), + }) - expect(getByText(message)).toBeTruthy() -}) + expect(getByText(message)).toBeTruthy() + } +) From 7ff2e510248fad2176b15d0c5c2af3145951edb7 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 22 Feb 2024 12:34:58 -0500 Subject: [PATCH 5/9] add documentation --- README.md | 15 +++++++++++++++ package.json | 6 ++---- src/svelte5-index.js | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/svelte5-index.js diff --git a/README.md b/README.md index d49bbae..78177b6 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: { + './pure.js': './svelte5.js' + } + }, +})) +``` + ## 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 b2eea3e..80c208b 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,8 @@ "description": "Simple and complete Svelte testing utilities that encourage good testing practices.", "main": "src/index.js", "exports": { - ".": { - "types": "./types/index.d.ts", - "default": "./src/index.js" - }, + ".": "./src/index.js", + "./svelte5": "./src/svelte5-index.js", "./vitest": { "default": "./src/vitest.js" } diff --git a/src/svelte5-index.js b/src/svelte5-index.js new file mode 100644 index 0000000..4b27bde --- /dev/null +++ b/src/svelte5-index.js @@ -0,0 +1,15 @@ +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' From 9d86208d568a71e56fba88c6ed572570120a0205 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Fri, 23 Feb 2024 10:50:36 -0500 Subject: [PATCH 6/9] de-duplicate code --- src/__tests__/act.test.js | 2 +- src/__tests__/auto-cleanup-skip.test.js | 2 +- src/__tests__/auto-cleanup.test.js | 2 +- src/__tests__/cleanup.test.js | 2 +- src/__tests__/context.test.js | 2 +- src/__tests__/debug.test.js | 2 +- src/__tests__/events.test.js | 2 +- src/__tests__/mount.test.js | 2 +- src/__tests__/multi-base.test.js | 2 +- src/__tests__/render.test.js | 2 +- src/__tests__/rerender.test.js | 2 +- src/__tests__/transition.test.js | 2 +- src/index.js | 1 + src/pure.js | 136 ++++++++++---------- src/svelte5-index.js | 2 + src/svelte5.js | 158 ++++-------------------- src/vitest.js | 2 +- vite.config.js | 15 ++- 18 files changed, 127 insertions(+), 211 deletions(-) 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 ceedbea..3e49548 100644 --- a/src/__tests__/cleanup.test.js +++ b/src/__tests__/cleanup.test.js @@ -1,7 +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() diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js index cff0475..0e5fc69 100644 --- a/src/__tests__/context.test.js +++ b/src/__tests__/context.test.js @@ -1,7 +1,7 @@ import { expect, test } from 'vitest' import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { render } from '..' +import { render } from '@testing-library/svelte' import Comp from './fixtures/Context.svelte' test.skipIf(SVELTE_VERSION >= '5' && process.env.VITEST_ENV == 'happy-dom')( 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 7edf348..bdc200b 100644 --- a/src/__tests__/mount.test.js +++ b/src/__tests__/mount.test.js @@ -1,7 +1,7 @@ import { describe, expect, test, vi } from 'vitest' import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { act, render, screen } from '..' +import { act, render, screen } from '@testing-library/svelte' import Mounter from './fixtures/Mounter.svelte' const onMounted = vi.fn() 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 b1d96f3..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' diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js index 922ea63..aeb5990 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -4,7 +4,7 @@ import { describe, expect, test, vi } from 'vitest' import { writable } from 'svelte/store' -import { act, render, waitFor } from '..' +import { act, render, waitFor } from '@testing-library/svelte' 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..60758c1 100644 --- a/src/__tests__/transition.test.js +++ b/src/__tests__/transition.test.js @@ -2,7 +2,7 @@ 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 { render, screen, waitFor } from '@testing-library/svelte' import Transitioner from './fixtures/Transitioner.svelte' describe.runIf(SVELTE_VERSION < '5')('transitions', () => { 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 28427f3..5e477e4 100644 --- a/src/pure.js +++ b/src/pure.js @@ -7,55 +7,53 @@ import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import * as Svelte from 'svelte' const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) -const targetCache = new Set() -const componentCache = new Set() +export const targetCache = new Set() +export const componentCache = new Set() if (IS_SVELTE_5) console.warn('for Svelte 5, use `@testing-library/svelte/svelte5`') -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 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) } if (IS_SVELTE_5) @@ -76,30 +74,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) { @@ -115,19 +129,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 @@ -140,9 +154,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 index 4b27bde..1770eac 100644 --- a/src/svelte5-index.js +++ b/src/svelte5-index.js @@ -13,3 +13,5 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_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 index 8614e17..6f30784 100644 --- a/src/svelte5.js +++ b/src/svelte5.js @@ -1,145 +1,41 @@ +import { createClassComponent } from 'svelte/legacy' import { - fireEvent as dtlFireEvent, - getQueriesForElement, - prettyDOM, -} from '@testing-library/dom' -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { createClassComponent as createComponentSvelte5 } from 'svelte/legacy' -import * as Svelte from 'svelte' - -const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) -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) - ) - - // 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(` - 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 } - } - - const renderComponent = (options) => { + 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 = IS_SVELTE_5 - ? createComponentSvelte5({ component: ComponentConstructor, ...options }) - : new ComponentConstructor(options) + const component = createClassComponent({ + component: ComponentConstructor, + ...options, + }) 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, - 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) => { - const inCache = componentCache.delete(component) - - if (inCache) { - component.$destroy() - } -} - -const cleanupTarget = (target) => { - const inCache = targetCache.delete(target) - - if (inCache && target.parentNode === document.body) { - document.body.removeChild(target) - } -} - -const cleanup = () => { - componentCache.forEach(cleanupComponent) - targetCache.forEach(cleanupTarget) -} - -const act = async (fn) => { - if (fn) { - await fn() - } - return Svelte.tick() -} - -const fireEvent = async (...args) => { - const event = dtlFireEvent(...args) - await Svelte.tick() - return event -} - -Object.keys(dtlFireEvent).forEach((key) => { - fireEvent[key] = async (...args) => { - const event = dtlFireEvent[key](...args) - await Svelte.tick() - return event - } -}) +const render = buildRender(buildRenderComponent) /* eslint-disable import/export */ -export * from '@testing-library/dom' +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 2b2bc5d..3a8aaf2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,10 +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 alias = {} +const IS_SVELTE_5 = SVELTE_VERSION >= '5' -if (SVELTE_VERSION >= '5') alias['./pure.js'] = './svelte5.js' +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 }) => ({ @@ -17,6 +26,7 @@ export default defineConfig(({ mode }) => ({ conditions: mode === 'test' ? ['browser'] : [], }, test: { + alias, environment: 'jsdom', setupFiles: ['./src/__tests__/_vitest-setup.js'], mockReset: true, @@ -25,6 +35,5 @@ export default defineConfig(({ mode }) => ({ provider: 'v8', include: ['src'], }, - alias, }, })) From 92b651fb8dc833a70eec0f4033f1ba891756626a Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Fri, 23 Feb 2024 13:09:42 -0500 Subject: [PATCH 7/9] tweak the documentation --- README.md | 2 +- src/pure.js | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 78177b6..0a39429 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ import from `@testing-library/svelte/svelte5` instead of `@testing-library/svelt export default defineConfig(({ }) => ({ test: { alias: { - './pure.js': './svelte5.js' + '@testing-library/svelte': '@testing-library/svelte/svelte5' } }, })) diff --git a/src/pure.js b/src/pure.js index 5e477e4..3c412da 100644 --- a/src/pure.js +++ b/src/pure.js @@ -10,9 +10,6 @@ const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) export const targetCache = new Set() export const componentCache = new Set() -if (IS_SVELTE_5) - console.warn('for Svelte 5, use `@testing-library/svelte/svelte5`') - const svelteComponentOptions = [ 'accessors', 'anchor', From 8f233abe2ce0f1360d9ec3c56062232c1bb7b5c6 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Fri, 23 Feb 2024 13:11:39 -0500 Subject: [PATCH 8/9] put back the types in export --- package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 80c208b..ad85df6 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,14 @@ "description": "Simple and complete Svelte testing utilities that encourage good testing practices.", "main": "src/index.js", "exports": { - ".": "./src/index.js", - "./svelte5": "./src/svelte5-index.js", + ".": { + "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" } From a3d1c962219d7ed8a8762b0b07d27f53e9b121b8 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Fri, 23 Feb 2024 13:30:35 -0500 Subject: [PATCH 9/9] test util file --- package.json | 4 ++-- src/__tests__/context.test.js | 19 +++++++-------- src/__tests__/mount.test.js | 41 +++++++++++++++----------------- src/__tests__/rerender.test.js | 6 ++--- src/__tests__/transition.test.js | 13 +++++----- src/__tests__/utils.js | 7 ++++++ 6 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 src/__tests__/utils.js diff --git a/package.json b/package.json index ad85df6..4e8c6f1 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "test": "vitest run --coverage", "test:watch": "vitest", "test:update": "vitest run --update", - "test:vitest:jsdom": "VITEST_ENV=jsdom vitest run --coverage --environment jsdom", - "test:vitest:happy-dom": "VITEST_ENV=happy-dom vitest run --coverage --environment happy-dom", + "test:vitest:jsdom": "vitest run --coverage --environment jsdom", + "test:vitest:happy-dom": "vitest run --coverage --environment happy-dom", "types": "svelte-check", "validate": "npm-run-all test:vitest:* types", "contributors:add": "all-contributors add", diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js index 0e5fc69..e7f2fd5 100644 --- a/src/__tests__/context.test.js +++ b/src/__tests__/context.test.js @@ -1,18 +1,15 @@ import { expect, test } from 'vitest' -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { render } from '@testing-library/svelte' import Comp from './fixtures/Context.svelte' +import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js' -test.skipIf(SVELTE_VERSION >= '5' && process.env.VITEST_ENV == 'happy-dom')( - 'can set a context', - () => { - const message = 'Got it' +test.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('can set a context', () => { + const message = 'Got it' - const { getByText } = render(Comp, { - context: new Map(Object.entries({ foo: { message } })), - }) + const { getByText } = render(Comp, { + context: new Map(Object.entries({ foo: { message } })), + }) - expect(getByText(message)).toBeTruthy() - } -) + expect(getByText(message)).toBeTruthy() +}) diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js index bdc200b..df0792b 100644 --- a/src/__tests__/mount.test.js +++ b/src/__tests__/mount.test.js @@ -1,37 +1,34 @@ import { describe, expect, test, vi } from 'vitest' -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' 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.skipIf(SVELTE_VERSION >= '5' && process.env.VITEST_ENV == 'happy-dom')( - 'mount and destroy', - () => { - test('component is mounted', async () => { - renderSubject() +describe.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('mount and destroy', () => { + test('component is mounted', async () => { + renderSubject() - const content = screen.getByRole('button') + const content = screen.getByRole('button') - expect(content).toBeInTheDocument() - await act() - expect(onMounted).toHaveBeenCalledOnce() - }) + expect(content).toBeInTheDocument() + await act() + expect(onMounted).toHaveBeenCalledOnce() + }) - test('component is destroyed', async () => { - const { unmount } = renderSubject() + test('component is destroyed', async () => { + const { unmount } = renderSubject() - await act() - unmount() + await act() + unmount() - const content = screen.queryByRole('button') + const content = screen.queryByRole('button') - expect(content).not.toBeInTheDocument() - await act() - expect(onDestroyed).toHaveBeenCalledOnce() - }) - } -) + expect(content).not.toBeInTheDocument() + await act() + expect(onDestroyed).toHaveBeenCalledOnce() + }) +}) diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js index aeb5990..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 '@testing-library/svelte' 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 60758c1..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 { 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'