diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 19c1908..6021776 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: [ { diff --git a/src/__tests__/__snapshots__/render.test.js.snap b/src/__tests__/__snapshots__/render.test.js.snap deleted file mode 100644 index b9eb849..0000000 --- a/src/__tests__/__snapshots__/render.test.js.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`render > should accept svelte v4 component options 1`] = ` - -
-

- Hello - World - ! -

- -
- we have context -
- - -
-
- -`; - -exports[`render > should accept svelte v5 component options 1`] = ` - - - - -
-

- Hello World! -

- -
- we have context -
- - - -
- -`; diff --git a/src/__tests__/debug.test.js b/src/__tests__/debug.test.js index 2a9c6e3..769c0c7 100644 --- a/src/__tests__/debug.test.js +++ b/src/__tests__/debug.test.js @@ -6,19 +6,19 @@ import Comp from './fixtures/Comp.svelte' describe('debug', () => { beforeEach(() => { - vi.spyOn(console, 'log').mockImplementation(() => { }) + vi.spyOn(console, 'log').mockImplementation(() => {}) }) afterEach(() => { console.log.mockRestore() }) - test('pretty prints the container', () => { - const { container, debug } = render(Comp, { props: { name: 'world' } }) + test('pretty prints the base element', () => { + const { baseElement, debug } = render(Comp, { props: { name: 'world' } }) debug() expect(console.log).toHaveBeenCalledTimes(1) - expect(console.log).toHaveBeenCalledWith(prettyDOM(container)) + expect(console.log).toHaveBeenCalledWith(prettyDOM(baseElement)) }) }) diff --git a/src/__tests__/fixtures/Comp.svelte b/src/__tests__/fixtures/Comp.svelte index ec04c05..c739725 100644 --- a/src/__tests__/fixtures/Comp.svelte +++ b/src/__tests__/fixtures/Comp.svelte @@ -1,23 +1,17 @@

Hello {name}!

-
we have {contextName}
- diff --git a/src/__tests__/fixtures/Comp2.svelte b/src/__tests__/fixtures/Comp2.svelte deleted file mode 100644 index 104e81a..0000000 --- a/src/__tests__/fixtures/Comp2.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - - -

Hello {name}!

- - diff --git a/src/__tests__/fixtures/Rerender.svelte b/src/__tests__/fixtures/Rerender.svelte deleted file mode 100644 index 1a3fa24..0000000 --- a/src/__tests__/fixtures/Rerender.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
Hello {name}!
diff --git a/src/__tests__/fixtures/Simple.svelte b/src/__tests__/fixtures/Simple.svelte index 3fa20ce..c9c2f15 100644 --- a/src/__tests__/fixtures/Simple.svelte +++ b/src/__tests__/fixtures/Simple.svelte @@ -1,5 +1,7 @@

hello {name}

+

count: {count}

diff --git a/src/__tests__/multi-base.test.js b/src/__tests__/multi-base.test.js index bad628e..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 '@testing-library/svelte' 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 cb30b77..9221012 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -1,123 +1,88 @@ +import { render } from '@testing-library/svelte' import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { beforeEach, describe, expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' -import { act, render as stlRender } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' -import CompDefault from './fixtures/Comp2.svelte' 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( - SVELTE_VERSION < '5' ? { accessors: true } : {} - ) - - expect(getByText('Hello World!')).toBeInTheDocument() - - expect(component.name).toBe('World') - - await act(() => { - component.value = 'Planet' - }) - - expect(getByText('Hello World!')).toBeInTheDocument() + test('throws error when mixing svelte component options and props', () => { + expect(() => { + render(Comp, { props, name: 'World' }) + }).toThrow(/Unknown options/) }) - test('should accept props directly', () => { - const { getByText } = stlRender(Comp, { name: 'World' }) - expect(getByText('Hello World!')).toBeInTheDocument() + test('throws error when mixing target option and props', () => { + expect(() => { + render(Comp, { target: document.createElement('div'), name: 'World' }) + }).toThrow(/Unknown options/) }) - 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 return a container object wrapping the DOM of the rendered component', () => { + const { container, getByTestId } = render(Comp, props) + const firstElement = getByTestId('test') - test('should throw error when mixing svelte component options and props', () => { - expect(() => { - stlRender(Comp, { props: {}, name: 'World' }) - }).toThrow(/Unknown options were found/) + expect(container.firstChild).toBe(firstElement) }) - test('should return a container object, which contains the DOM of the rendered component', () => { - const { container } = render() + test('should return a baseElement object, which holds the container', () => { + const { baseElement, container } = render(Comp, props) - expect(container.innerHTML).toBe(document.body.innerHTML) + expect(baseElement).toBe(document.body) + expect(baseElement.firstChild).toBe(container) }) - test('correctly find component constructor on the default property', () => { - const { getByText } = stlRender(CompDefault, { props: { name: 'World' } }) + test('if target is provided, use it as container and baseElement', () => { + const target = document.createElement('div') + const { baseElement, container } = render(Comp, { props, target }) - expect(getByText('Hello World!')).toBeInTheDocument() + expect(container).toBe(target) + expect(baseElement).toBe(target) }) - test("accept the 'context' option", () => { - const { getByText } = stlRender(Comp, { - props: { name: 'Universe' }, - context: new Map([['name', 'context']]), - }) + test('allow baseElement to be specified', () => { + const customBaseElement = document.createElement('div') - expect(getByText('we have context')).toBeInTheDocument() + const { baseElement, container } = render( + Comp, + { props }, + { baseElement: customBaseElement } + ) + + expect(baseElement).toBe(customBaseElement) + expect(baseElement.firstChild).toBe(container) }) + + test.runIf(SVELTE_VERSION < '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 6fabf36..6efda86 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -1,41 +1,50 @@ -/** - * @jest-environment jsdom - */ -import { expect, test, vi } from 'vitest' +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 '@testing-library/svelte' +import Comp from './fixtures/Comp.svelte' -import Comp from './fixtures/Rerender.svelte' +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 onMounted = vi.fn() - const onDestroyed = vi.fn() + await rerender({ name: 'Dolly' }) - const { getByTestId, rerender } = render(Comp, { - props: { name: 'World 1', onMounted, onDestroyed }, + expect(element).toHaveTextContent('Hello Dolly!') }) - const expectToRender = (content) => - waitFor(() => { - expect(getByTestId('test')).toHaveTextContent(content) - expect(onMounted).toHaveBeenCalledOnce() - }) + test('warns if incorrect arguments shape used', async () => { + vi.stubGlobal('console', { warn: vi.fn() }) - await expectToRender('Hello World 1!') + const { rerender } = render(Comp, { name: 'World' }) + const element = screen.getByText('Hello World!') - console.warn = vi.fn() + await rerender({ props: { name: 'Dolly' } }) - rerender({ props: { name: 'World 2' } }) - await expectToRender('Hello World 2!') - expect(onDestroyed).not.toHaveBeenCalled() + expect(element).toHaveTextContent('Hello Dolly!') + expect(console.warn).toHaveBeenCalledOnce() + expect(console.warn).toHaveBeenCalledWith( + expect.stringMatching(/deprecated/iu) + ) + }) - expect(console.warn).toHaveBeenCalledOnce() + 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!') - console.warn.mockClear() - onDestroyed.mockReset() - rerender({ name: 'World 3' }) - await expectToRender('Hello World 3!') - expect(onDestroyed).not.toHaveBeenCalled() + expect(element).toBeInTheDocument() + expect(component.name).toBe('World') - expect(console.warn).not.toHaveBeenCalled() + await act(() => { + component.name = 'Planet' + }) + + expect(element).toHaveTextContent('Hello Planet!') + }) }) diff --git a/src/index.js b/src/index.js index 4181419..3e58608 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 diff --git a/src/pure.js b/src/pure.js index cb90733..364c225 100644 --- a/src/pure.js +++ b/src/pure.js @@ -3,13 +3,14 @@ import { getQueriesForElement, prettyDOM, } from '@testing-library/dom' -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' 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', @@ -48,25 +49,30 @@ export class SvelteTestingLibrary { return { props: options } } - render(Component, { target, ...options } = {}, { container, queries } = {}) { - container = container || document.body - target = target || container.appendChild(document.createElement('div')) + 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( - { - target, - ComponentConstructor, - }, - options - ) + const component = this.renderComponent(ComponentConstructor, { + ...componentOptions, + target, + }) return { - container, + baseElement, component, - debug: (el = container) => console.log(prettyDOM(el)), + container: target, + debug: (el = baseElement) => console.log(prettyDOM(el)), rerender: async (props) => { if (props.props) { console.warn( @@ -80,27 +86,23 @@ export class SvelteTestingLibrary { unmount: () => { this.cleanupComponent(component) }, - ...getQueriesForElement(container, queries), + ...getQueriesForElement(baseElement, renderOptions.queries), } } - renderComponent({ target, ComponentConstructor }, options) { - options = { target, ...this.checkProps(options) } - - if (IS_SVELTE_5) + renderComponent(ComponentConstructor, componentOptions) { + if (IS_SVELTE_5) { throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') + } - const component = new ComponentConstructor(options) + const component = new ComponentConstructor(componentOptions) this.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(() => { - this.componentCache.delete(component) - }) - } + component.$$.on_destroy.push(() => { + this.componentCache.delete(component) + }) return component } diff --git a/src/svelte5-index.js b/src/svelte5-index.js index 1770eac..8dc11e8 100644 --- a/src/svelte5-index.js +++ b/src/svelte5-index.js @@ -1,3 +1,4 @@ +/* eslint-disable import/export */ import { act, cleanup } from './svelte5.js' // If we're running in a test runner that supports afterEach @@ -12,6 +13,6 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) { }) } +export { act, fireEvent } from './pure.js' 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 57e9e54..a8dd494 100644 --- a/src/svelte5.js +++ b/src/svelte5.js @@ -1,4 +1,5 @@ import { createClassComponent } from 'svelte/legacy' + import { SvelteTestingLibrary } from './pure.js' class Svelte5TestingLibrary extends SvelteTestingLibrary { @@ -11,12 +12,10 @@ class Svelte5TestingLibrary extends SvelteTestingLibrary { 'recover', ] - renderComponent({ target, ComponentConstructor }, options) { - options = { target, ...this.checkProps(options) } - + renderComponent(ComponentConstructor, componentOptions) { const component = createClassComponent({ + ...componentOptions, component: ComponentConstructor, - ...options, }) this.componentCache.add(component) 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 = | ComponentProps - | Pick< - ComponentConstructorOptions>, - 'anchor' | 'props' | 'hydrate' | 'intro' | 'context' - > - -type Omit = Pick> + | Partial>> type Constructor = new (...args: any[]) => T @@ -35,24 +30,22 @@ export type RenderResult< Q extends Queries = typeof queries, > = { container: HTMLElement + baseElement: HTMLElement component: C debug: (el?: HTMLElement | DocumentFragment) => void - rerender: (props: ComponentProps) => Promise + rerender: (props: Partial>) => Promise unmount: () => void } & { [P in keyof Q]: BoundFunction } 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 3a8aaf2..1ef160e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte' -import { defineConfig } from 'vite' import path from 'path' import { VERSION as SVELTE_VERSION } from 'svelte/compiler' +import { defineConfig } from 'vite' const IS_SVELTE_5 = SVELTE_VERSION >= '5'