diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 19c1908..778d507 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -14,6 +14,7 @@ module.exports = { plugins: ['svelte', 'simple-import-sort', 'json-files'], rules: { 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', }, overrides: [ { @@ -22,6 +23,9 @@ module.exports = { parserOptions: { parser: '@typescript-eslint/parser', }, + rules: { + 'no-undef-init': 'off', + }, }, { files: ['*.ts'], diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6e83fa..be47107 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: branches: [main, next] pull_request: branches: [main, next] + schedule: + # Tuesdays at 14:45 UTC (10:45 EST) + - cron: 45 14 * * 1 concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -16,11 +19,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/.prettierignore b/.prettierignore index 113f29b..5e50c20 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ scripts/* .eslintignore .prettierignore +.all-contributorsrc diff --git a/.prettierrc.yaml b/.prettierrc.yaml index e1bf864..0a2ace3 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -4,6 +4,6 @@ trailingComma: es5 plugins: - prettier-plugin-svelte overrides: - - files: "*.svelte" + - files: '*.svelte' options: parser: svelte 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 20bc1c2..1948cfc 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" } @@ -64,13 +68,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", @@ -89,16 +93,16 @@ "eslint-plugin-svelte": "2.35.1", "eslint-plugin-vitest-globals": "1.4.0", "expect-type": "^0.17.3", - "happy-dom": "^13.3.1", + "happy-dom": "^14.7.1", "jsdom": "^22.1.0", "npm-run-all": "^4.1.5", "prettier": "3.2.4", "prettier-plugin-svelte": "3.1.2", - "svelte": "^3 || ^4", + "svelte": "^3 || ^4 || ^5", "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 deleted file mode 100644 index a31cf2e..0000000 --- a/src/__tests__/__snapshots__/render.test.js.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`render > should accept svelte component options 1`] = ` -
-count: {count}
diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js index 830b513..48d985f 100644 --- a/src/__tests__/mount.test.js +++ b/src/__tests__/mount.test.js @@ -1,33 +1,33 @@ +import { act, render, screen } from '@testing-library/svelte' 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__/multi-base.test.js b/src/__tests__/multi-base.test.js index 39f28d1..bf5fd4e 100644 --- a/src/__tests__/multi-base.test.js +++ b/src/__tests__/multi-base.test.js @@ -1,6 +1,6 @@ +import { render } from '@testing-library/svelte' import { describe, expect, test } from 'vitest' -import { render } from '..' import Comp from './fixtures/Comp.svelte' describe('multi-base', () => { @@ -13,11 +13,11 @@ describe('multi-base', () => { { target: treeA, props: { - name: 'Tree A' - } + name: 'Tree A', + }, }, { - container: treeA + baseElement: treeA, } ) @@ -26,11 +26,11 @@ describe('multi-base', () => { { target: treeB, props: { - name: 'Tree B' - } + name: 'Tree B', + }, }, { - container: treeB + baseElement: treeB, } ) diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js index 6beb984..ea445d5 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -1,104 +1,85 @@ -import { beforeEach, describe, expect, test } from 'vitest' +import { render } from '@testing-library/svelte' +import { describe, expect, test } from 'vitest' -import { act, render as stlRender } from '..' import Comp from './fixtures/Comp.svelte' -import CompDefault from './fixtures/Comp2.svelte' +import { IS_SVELTE_5 } from './utils.js' describe('render', () => { - let props - - const render = (additional = {}) => { - return stlRender(Comp, { - target: document.body, - props, - ...additional - }) - } - - beforeEach(() => { - props = { - name: 'World' - } - }) + const props = { name: 'World' } test('renders component into the document', () => { - const { getByText } = render() + const { getByText } = render(Comp, { props }) expect(getByText('Hello World!')).toBeInTheDocument() }) - // Dear reader, this is not something you generally want to do in your tests. - test('programmatically change props', async () => { - const { component, getByText } = render() - + test('accepts props directly', () => { + const { getByText } = render(Comp, props) expect(getByText('Hello World!')).toBeInTheDocument() - - await act(() => { - component.$set({ name: 'Worlds' }) - }) - - expect(getByText('Hello Worlds!')).toBeInTheDocument() }) - test('change props with accessors', async () => { - const { component, getByText } = render({ accessors: true }) - - expect(getByText('Hello World!')).toBeInTheDocument() + test('throws error when mixing svelte component options and props', () => { + expect(() => { + render(Comp, { props, name: 'World' }) + }).toThrow(/Unknown options/) + }) - expect(component.name).toBe('World') + test('throws error when mixing target option and props', () => { + expect(() => { + render(Comp, { target: document.createElement('div'), name: 'World' }) + }).toThrow(/Unknown options/) + }) - await act(() => { - component.value = 'Planet' - }) + test('should return a container object wrapping the DOM of the rendered component', () => { + const { container, getByTestId } = render(Comp, props) + const firstElement = getByTestId('test') - expect(getByText('Hello World!')).toBeInTheDocument() + expect(container.firstChild).toBe(firstElement) }) - test('should accept props directly', () => { - const { getByText } = stlRender(Comp, { name: 'World' }) - expect(getByText('Hello World!')).toBeInTheDocument() + test('should return a baseElement object, which holds the container', () => { + const { baseElement, container } = render(Comp, props) + + expect(baseElement).toBe(document.body) + expect(baseElement.firstChild).toBe(container) }) - test('should accept svelte component options', () => { + test('if target is provided, use it as container and baseElement', () => { 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() - }) + const { baseElement, container } = render(Comp, { props, target }) - test('should throw error when mixing svelte component options and props', () => { - expect(() => { - stlRender(Comp, { anchor: '', name: 'World' }) - }).toThrow(/Unknown options were found/) + expect(container).toBe(target) + expect(baseElement).toBe(target) }) - test('should return a container object, which contains the DOM of the rendered component', () => { - const { container } = render() + test('allow baseElement to be specified', () => { + const customBaseElement = document.createElement('div') - expect(container.innerHTML).toBe(document.body.innerHTML) - }) - - test('correctly find component constructor on the default property', () => { - const { getByText } = render(CompDefault, { props: { name: 'World' } }) + const { baseElement, container } = render( + Comp, + { props }, + { baseElement: customBaseElement } + ) - expect(getByText('Hello World!')).toBeInTheDocument() + expect(baseElement).toBe(customBaseElement) + expect(baseElement.firstChild).toBe(container) }) - test("accept the 'context' option", () => { - const { getByText } = stlRender(Comp, { - props: { - name: 'Universe' - }, - context: new Map([['name', 'context']]) - }) - - expect(getByText('we have context')).toBeInTheDocument() + test.skipIf(IS_SVELTE_5)('should accept anchor option in Svelte v4', () => { + const baseElement = document.body + const target = document.createElement('section') + const anchor = document.createElement('div') + baseElement.appendChild(target) + target.appendChild(anchor) + + const { getByTestId } = render( + Comp, + { props, target, anchor }, + { baseElement } + ) + const firstElement = getByTestId('test') + + expect(target.firstChild).toBe(firstElement) + expect(target.lastChild).toBe(anchor) }) }) diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js index d6cbc21..ca4b8e8 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -1,38 +1,50 @@ -/** - * @jest-environment jsdom - */ -import { expect, test, vi } from 'vitest' -import { writable } from 'svelte/store' +import { act, render, screen } from '@testing-library/svelte' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' +import { describe, expect, test, vi } from 'vitest' -import { render, waitFor } from '..' -import Comp from './fixtures/Rerender.svelte' +import Comp from './fixtures/Comp.svelte' -const mountCounter = writable(0) +describe('rerender', () => { + test('updates props', async () => { + const { rerender } = render(Comp, { name: 'World' }) + const element = screen.getByText('Hello World!') -test('mounts new component successfully', async () => { - const { getByTestId, rerender } = render(Comp, { - props: { name: 'World 1' }, - context: new Map(Object.entries({ mountCounter })), + await rerender({ name: 'Dolly' }) + + expect(element).toHaveTextContent('Hello Dolly!') }) - const expectToRender = (content) => - waitFor(() => { - expect(getByTestId('test')).toHaveTextContent(content) - expect(getByTestId('mount-counter')).toHaveTextContent('1') - }) + test('warns if incorrect arguments shape used', async () => { + vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() }) + + const { rerender } = render(Comp, { name: 'World' }) + const element = screen.getByText('Hello World!') - await expectToRender('Hello World 1!') + await rerender({ props: { name: 'Dolly' } }) - console.warn = vi.fn() + expect(element).toHaveTextContent('Hello Dolly!') + expect(console.warn).toHaveBeenCalledOnce() + expect(console.warn).toHaveBeenCalledWith( + expect.stringMatching(/deprecated/iu) + ) + }) - rerender({ props: { name: 'World 2' } }) - await expectToRender('Hello World 2!') + test('change props with accessors', async () => { + const { component, getByText } = render( + Comp, + SVELTE_VERSION < '5' + ? { accessors: true, props: { name: 'World' } } + : { name: 'World' } + ) + const element = getByText('Hello World!') - expect(console.warn).toHaveBeenCalled() + expect(element).toBeInTheDocument() + expect(component.name).toBe('World') - console.warn.mockClear() - rerender({ name: 'World 3' }) - await expectToRender('Hello World 3!') + await act(() => { + component.name = 'Planet' + }) - expect(console.warn).not.toHaveBeenCalled() + expect(element).toHaveTextContent('Hello Planet!') + }) }) diff --git a/src/__tests__/transition.test.js b/src/__tests__/transition.test.js index 5b1ff18..a9c8e02 100644 --- a/src/__tests__/transition.test.js +++ b/src/__tests__/transition.test.js @@ -1,15 +1,16 @@ +import { render, screen, waitFor } from '@testing-library/svelte' import { userEvent } from '@testing-library/user-event' import { beforeEach, describe, expect, test, vi } from 'vitest' -import { render, screen, waitFor } from '..' import Transitioner from './fixtures/Transitioner.svelte' +import { IS_JSDOM, IS_SVELTE_5 } from './utils.js' -describe('transitions', () => { +describe.skipIf(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..46fd662 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +/* eslint-disable import/export */ import { act, cleanup } from './pure.js' // If we're running in a test runner that supports afterEach @@ -12,4 +13,9 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) { }) } -export * from './pure.js' +// export all base queries, screen, etc. +export * from '@testing-library/dom' + +// export svelte-specific functions and custom `fireEvent` +// `fireEvent` must be a named export to take priority over wildcard export above +export { act, cleanup, fireEvent, render } from './pure.js' diff --git a/src/pure.js b/src/pure.js index 59c62ff..364c225 100644 --- a/src/pure.js +++ b/src/pure.js @@ -3,46 +3,41 @@ import { getQueriesForElement, prettyDOM, } from '@testing-library/dom' -import { tick } from 'svelte' - -const containerCache = new Set() -const componentCache = new Set() - -const svelteComponentOptions = [ - 'accessors', - 'anchor', - 'props', - 'hydrate', - 'intro', - 'context', -] - -const render = ( - Component, - { target, ...options } = {}, - { container, queries } = {} -) => { - container = container || document.body - target = target || container.appendChild(document.createElement('div')) - - const ComponentConstructor = Component.default || Component - - const checkProps = (options) => { +import * as Svelte from 'svelte' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' + +const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) + +export class SvelteTestingLibrary { + svelteComponentOptions = [ + 'target', + 'accessors', + 'anchor', + 'props', + 'hydrate', + 'intro', + 'context', + ] + + targetCache = new Set() + componentCache = new Set() + + checkProps(options) { const isProps = !Object.keys(options).some((option) => - svelteComponentOptions.includes(option) + this.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) + (option) => !this.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 + are [${this.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 `) @@ -54,78 +49,109 @@ const render = ( return { props: options } } - let component = new ComponentConstructor({ - target, - ...checkProps(options), - }) - - containerCache.add({ container, target, component }) - componentCache.add(component) - - component.$$.on_destroy.push(() => { - componentCache.delete(component) - }) - - 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 tick() - }, - unmount: () => { - if (componentCache.has(component)) component.$destroy() - }, - ...getQueriesForElement(container, queries), + render(Component, componentOptions = {}, renderOptions = {}) { + componentOptions = this.checkProps(componentOptions) + + const baseElement = + renderOptions.baseElement ?? componentOptions.target ?? document.body + + const target = + componentOptions.target ?? + baseElement.appendChild(document.createElement('div')) + + this.targetCache.add(target) + + const ComponentConstructor = Component.default || Component + + const component = this.renderComponent(ComponentConstructor, { + ...componentOptions, + target, + }) + + return { + baseElement, + component, + container: target, + debug: (el = baseElement) => 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: () => { + this.cleanupComponent(component) + }, + ...getQueriesForElement(baseElement, renderOptions.queries), + } } -} -const cleanupAtContainer = (cached) => { - const { target, component } = cached + renderComponent(ComponentConstructor, componentOptions) { + if (IS_SVELTE_5) { + throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') + } + + const component = new ComponentConstructor(componentOptions) + + this.componentCache.add(component) - if (componentCache.has(component)) component.$destroy() + // TODO(mcous, 2024-02-11): remove this behavior in the next major version + component.$$.on_destroy.push(() => { + this.componentCache.delete(component) + }) - if (target.parentNode === document.body) { - document.body.removeChild(target) + return component } - containerCache.delete(cached) -} + cleanupComponent(component) { + const inCache = this.componentCache.delete(component) + + if (inCache) { + component.$destroy() + } + } + + cleanupTarget(target) { + const inCache = this.targetCache.delete(target) -const cleanup = () => { - Array.from(containerCache.keys()).forEach(cleanupAtContainer) + if (inCache && target.parentNode === document.body) { + document.body.removeChild(target) + } + } + + cleanup() { + this.componentCache.forEach(this.cleanupComponent.bind(this)) + this.targetCache.forEach(this.cleanupTarget.bind(this)) + } } -const act = async (fn) => { +const instance = new SvelteTestingLibrary() + +export const render = instance.render.bind(instance) + +export const cleanup = instance.cleanup.bind(instance) + +export const act = async (fn) => { if (fn) { await fn() } - return tick() + return Svelte.tick() } -const fireEvent = async (...args) => { +export 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 } }) - -/* 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..ab49641 --- /dev/null +++ b/src/svelte5-index.js @@ -0,0 +1,23 @@ +/* eslint-disable import/export */ +import { act } from './pure.js' +import { 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 all base queries, screen, etc. +export * from '@testing-library/dom' + +// export svelte-specific functions and custom `fireEvent` +// `fireEvent` must be a named export to take priority over wildcard export above +export { act, fireEvent } from './pure.js' +export { cleanup, render } from './svelte5.js' diff --git a/src/svelte5.js b/src/svelte5.js new file mode 100644 index 0000000..a8dd494 --- /dev/null +++ b/src/svelte5.js @@ -0,0 +1,30 @@ +import { createClassComponent } from 'svelte/legacy' + +import { SvelteTestingLibrary } from './pure.js' + +class Svelte5TestingLibrary extends SvelteTestingLibrary { + svelteComponentOptions = [ + 'target', + 'props', + 'events', + 'context', + 'intro', + 'recover', + ] + + renderComponent(ComponentConstructor, componentOptions) { + const component = createClassComponent({ + ...componentOptions, + component: ComponentConstructor, + }) + + this.componentCache.add(component) + + return component + } +} + +const instance = new Svelte5TestingLibrary() + +export const render = instance.render.bind(instance) +export const cleanup = instance.cleanup.bind(instance) diff --git a/src/vitest.js b/src/vitest.js index 135ddbe..71977e6 100644 --- a/src/vitest.js +++ b/src/vitest.js @@ -1,7 +1,6 @@ +import { act, cleanup } from '@testing-library/svelte' import { afterEach } from 'vitest' -import { act, cleanup } from './pure.js' - afterEach(async () => { await act() cleanup() diff --git a/svelte.config.js b/svelte.config.js index 61eb947..b0683fd 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,7 +1,7 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { - // Consult https://svelte.dev/docs#compile-time-svelte-preprocess - // for more information about preprocessors - preprocess: vitePreprocess(), + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), } diff --git a/types/index.d.ts b/types/index.d.ts index d60d779..a206467 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -18,12 +18,7 @@ export * from '@testing-library/dom' type SvelteComponentOptions} export interface RenderOptions{ - container?: HTMLElement + baseElement?: HTMLElement queries?: Q } -export function render( - component: Constructor , - componentOptions?: SvelteComponentOptions , - renderOptions?: Omit -): RenderResult - -export function render ( +export function render< + C extends SvelteComponent, + Q extends Queries = typeof queries, +>( component: Constructor , componentOptions?: SvelteComponentOptions , renderOptions?: RenderOptions diff --git a/types/types.test-d.ts b/types/types.test-d.ts index bfb8f85..4a42bb1 100644 --- a/types/types.test-d.ts +++ b/types/types.test-d.ts @@ -7,8 +7,15 @@ import * as subject from './index.js' describe('types', () => { test('render is a function that accepts a Svelte component', () => { - subject.render(Simple, { name: 'Alice' }) - subject.render(Simple, { props: { name: 'Alice' } }) + subject.render(Simple, { name: 'Alice', count: 42 }) + subject.render(Simple, { props: { name: 'Alice', count: 42 } }) + }) + + test('rerender is a function that accepts partial props', async () => { + const { rerender } = subject.render(Simple, { name: 'Alice', count: 42 }) + + await rerender({ name: 'Bob' }) + await rerender({ count: 0 }) }) test('invalid prop types are rejected', () => { @@ -20,19 +27,19 @@ describe('types', () => { }) test('render result has container and component', () => { - const result = subject.render(Simple, { name: 'Alice' }) + const result = subject.render(Simple, { name: 'Alice', count: 42 }) expectTypeOf(result).toMatchTypeOf<{ container: HTMLElement component: SvelteComponent<{ name: string }> debug: (el?: HTMLElement) => void - rerender: (options: ComponentProps) => void + rerender: (props: Partial >) => Promise unmount: () => void }>() }) test('render result has default queries', () => { - const result = subject.render(Simple, { name: 'Alice' }) + const result = subject.render(Simple, { name: 'Alice', count: 42 }) expectTypeOf(result.getByRole).parameters.toMatchTypeOf< [role: subject.ByRoleMatcher, options?: subject.ByRoleOptions] @@ -49,7 +56,7 @@ describe('types', () => { ) const result = subject.render( Simple, - { name: 'Alice' }, + { name: 'Alice', count: 42 }, { queries: { getByVibes } } ) diff --git a/vite.config.js b/vite.config.js index 4baf76f..d9085ef 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,24 @@ +import path from 'node:path' + import { svelte } from '@sveltejs/vite-plugin-svelte' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { defineConfig } from 'vite' +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 }) => ({ - plugins: [svelte()], + plugins: [svelte({ hot: false })], resolve: { // Ensure `browser` exports are used in tests // Vitest prefers modules' `node` export by default @@ -12,6 +27,7 @@ export default defineConfig(({ mode }) => ({ conditions: mode === 'test' ? ['browser'] : [], }, test: { + alias, environment: 'jsdom', setupFiles: ['./src/__tests__/_vitest-setup.js'], mockReset: true,