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..a0caf8b 100644 --- a/src/__tests__/fixtures/Comp.svelte +++ b/src/__tests__/fixtures/Comp.svelte @@ -7,17 +7,15 @@ let buttonText = 'Button' - const contextName = getContext('name') - - function handleClick () { + function handleClick() { buttonText = 'Button Clicked' } + + const contextName = getContext('name')

Hello {name}!

-
we have {contextName}
- - +
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__/render.test.js b/src/__tests__/render.test.js index cb30b77..446823e 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -1,119 +1,111 @@ 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 { act, render } 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' }) - }) + test('throws error when mixing svelte component options and props', () => { + expect(() => { + render(Comp, { props, name: 'World' }) + }).toThrow(/Unknown options/) + }) - expect(getByText('Hello Worlds!')).toBeInTheDocument() + test('throws error when mixing target option and props', () => { + expect(() => { + render(Comp, { target: document.createElement('div'), name: 'World' }) + }).toThrow(/Unknown options/) }) - test('change props with accessors', async () => { - const { component, getByText } = render( - SVELTE_VERSION < '5' ? { accessors: true } : {} - ) + 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) + }) - expect(component.name).toBe('World') + test('should return a baseElement object, which holds the container', () => { + const { baseElement, container } = render(Comp, props) - await act(() => { - component.value = 'Planet' - }) + expect(baseElement).toBe(document.body) + expect(baseElement.firstChild).toBe(container) + }) - expect(getByText('Hello World!')).toBeInTheDocument() + test('if target is provided, use it as container and baseElement', () => { + const target = document.createElement('div') + const { baseElement, container } = render(Comp, { props, target }) + + expect(container).toBe(target) + expect(baseElement).toBe(target) }) - test('should accept props directly', () => { - const { getByText } = stlRender(Comp, { name: 'World' }) - expect(getByText('Hello World!')).toBeInTheDocument() + test('allow baseElement to be specified', () => { + const customBaseElement = document.createElement('div') + + const { baseElement, container } = render( + Comp, + { props }, + { baseElement: customBaseElement } + ) + + expect(baseElement).toBe(customBaseElement) + expect(baseElement.firstChild).toBe(container) }) 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', + 'should accept anchor option in Svelte v4', () => { + const baseElement = document.body 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() + 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) } ) test('should throw error when mixing svelte component options and props', () => { expect(() => { - stlRender(Comp, { props: {}, name: 'World' }) + render(Comp, { props: {}, name: 'World' }) }).toThrow(/Unknown options were found/) }) test('should return a container object, which contains the DOM of the rendered component', () => { - const { container } = render() + const { baseElement } = render(Comp) - expect(container.innerHTML).toBe(document.body.innerHTML) + expect(baseElement.innerHTML).toBe(document.body.innerHTML) }) test('correctly find component constructor on the default property', () => { - const { getByText } = stlRender(CompDefault, { props: { name: 'World' } }) + const { getByText } = render(Comp, { props: { name: 'World' } }) expect(getByText('Hello World!')).toBeInTheDocument() }) test("accept the 'context' option", () => { - const { getByText } = stlRender(Comp, { + const { getByText } = render(Comp, { props: { name: 'Universe' }, context: new Map([['name', 'context']]), }) diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js index 6fabf36..1dee6da 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -1,41 +1,54 @@ /** * @jest-environment jsdom */ -import { expect, test, vi } from 'vitest' +import { describe, expect, test, vi } from 'vitest' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { render, waitFor } from '@testing-library/svelte' +import { act, screen, render, waitFor } from '@testing-library/svelte' -import Comp from './fixtures/Rerender.svelte' +import Comp from './fixtures/Comp.svelte' -test('mounts new component successfully', async () => { - const onMounted = vi.fn() - const onDestroyed = vi.fn() +describe('rerender', () => { + test('updates props', async () => { + const { rerender } = render(Comp, { name: 'World' }) + const element = screen.getByText('Hello World!') - const { getByTestId, rerender } = render(Comp, { - props: { name: 'World 1', onMounted, onDestroyed }, + await rerender({ name: 'Dolly' }) + + 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() }) + + 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!') - expect(onDestroyed).not.toHaveBeenCalled() + 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).toHaveBeenCalledOnce() + expect(element).toBeInTheDocument() + expect(component.name).toBe('World') - console.warn.mockClear() - onDestroyed.mockReset() - rerender({ name: 'World 3' }) - await expectToRender('Hello World 3!') - expect(onDestroyed).not.toHaveBeenCalled() + await act(() => { + component.name = 'Planet' + }) - expect(console.warn).not.toHaveBeenCalled() + expect(element).toHaveTextContent('Hello Planet!') + }) }) diff --git a/src/pure.js b/src/pure.js index cb90733..ef081cb 100644 --- a/src/pure.js +++ b/src/pure.js @@ -16,6 +16,7 @@ export class SvelteTestingLibrary { 'hydrate', 'intro', 'context', + 'target', ] targetCache = new Set() @@ -48,25 +49,32 @@ 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 ComponentConstructor = Component?.default ?? Component - const component = this.renderComponent( - { - target, - ComponentConstructor, - }, - options - ) + const component = this.renderComponent(ComponentConstructor, { + ...componentOptions, + target, + }) + + this.componentCache.add(component) return { - container, + baseElement, + container: target, component, - debug: (el = container) => console.log(prettyDOM(el)), + debug: (el = baseElement) => console.log(prettyDOM(el)), rerender: async (props) => { if (props.props) { console.warn( @@ -80,20 +88,16 @@ export class SvelteTestingLibrary { unmount: () => { this.cleanupComponent(component) }, - ...getQueriesForElement(container, queries), + ...getQueriesForElement(baseElement, renderOptions.queries), } } - renderComponent({ target, ComponentConstructor }, options) { - options = { target, ...this.checkProps(options) } - + renderComponent(ComponentConstructor, options) { if (IS_SVELTE_5) throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') const component = new ComponentConstructor(options) - 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) { diff --git a/src/svelte5.js b/src/svelte5.js index 57e9e54..66e1756 100644 --- a/src/svelte5.js +++ b/src/svelte5.js @@ -11,17 +11,11 @@ class Svelte5TestingLibrary extends SvelteTestingLibrary { 'recover', ] - renderComponent({ target, ComponentConstructor }, options) { - options = { target, ...this.checkProps(options) } - - const component = createClassComponent({ + renderComponent(ComponentConstructor, options) { + return createClassComponent({ component: ComponentConstructor, ...options, }) - - this.componentCache.add(component) - - return 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 } } )