From 2ce3309359df380f88f01c1a7543ba5dde861773 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Thu, 15 Feb 2024 13:52:04 -0500 Subject: [PATCH 01/11] fix!: align `target` and `baseElement` options with testing-library Fixes #312, fixes #313 BREAKING CHANGES: The `container` option has been renamed to `baseElement`, `result.container` is now set to `target` rather than `baseElement`, and `render` will now throw if you mix props with the `target` option. --- package.json | 2 +- .../__snapshots__/render.test.js.snap | 48 ------ src/__tests__/debug.test.js | 8 +- src/__tests__/fixtures/Comp.svelte | 8 +- src/__tests__/fixtures/Comp2.svelte | 15 -- src/__tests__/fixtures/Rerender.svelte | 17 -- src/__tests__/render.test.js | 147 +++++++----------- src/__tests__/rerender.test.js | 65 ++++---- src/pure.js | 86 +++++----- types/index.d.ts | 21 +-- 10 files changed, 150 insertions(+), 267 deletions(-) delete mode 100644 src/__tests__/__snapshots__/render.test.js.snap delete mode 100644 src/__tests__/fixtures/Comp2.svelte delete mode 100644 src/__tests__/fixtures/Rerender.svelte diff --git a/package.json b/package.json index 938f563..5d060e4 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "npm-run-all": "^4.1.5", "prettier": "3.2.4", "prettier-plugin-svelte": "3.1.2", - "svelte": "^4.2.10", + "svelte": "^3 || ^4 || ^5", "svelte-check": "^3.6.3", "svelte-jester": "^3.0.0", "typescript": "^5.3.3", 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 1072a0f..324c5d5 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__/render.test.js b/src/__tests__/render.test.js index 262e062..e09075b 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -1,123 +1,84 @@ 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 '..' +import { render } from '..' 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 component 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 component 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 } = render(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') + + const { baseElement, container } = render( + Comp, + { props }, + { baseElement: customBaseElement } + ) - expect(getByText('we have context')).toBeInTheDocument() + 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 922ea63..a6162a8 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -1,41 +1,50 @@ -/** - * @jest-environment jsdom - */ +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { describe, expect, test, vi } from 'vitest' -import { writable } from 'svelte/store' -import { act, render, waitFor } from '..' -import Comp from './fixtures/Rerender.svelte' +import { act, render, screen } from '..' +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 a672b64..da286c8 100644 --- a/src/pure.js +++ b/src/pure.js @@ -11,45 +11,21 @@ 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) + : ['target', 'accessors', 'anchor', 'props', 'hydrate', 'intro', 'context'] - const ComponentConstructor = Component.default || Component +const render = (Component, componentOptions = {}, renderOptions = {}) => { + componentOptions = checkProps(componentOptions) - const checkProps = (options) => { - const isProps = !Object.keys(options).some((option) => - svelteComponentOptions.includes(option) - ) + const baseElement = + renderOptions.baseElement ?? componentOptions.target ?? document.body - // 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 - `) - } + const target = + componentOptions.target ?? + baseElement.appendChild(document.createElement('div')) - return options - } + targetCache.add(target) - return { props: options } - } + const ComponentConstructor = Component.default || Component const renderComponent = (options) => { options = { target, ...checkProps(options) } @@ -71,12 +47,13 @@ const render = ( return component } - let component = renderComponent(options) + const component = renderComponent({ ...componentOptions, target }) return { - container, component, - debug: (el = container) => console.log(prettyDOM(el)), + baseElement, + container: target, + debug: (el = baseElement) => console.log(prettyDOM(el)), rerender: async (props) => { if (props.props) { console.warn( @@ -84,16 +61,45 @@ const render = ( ) props = props.props } - component.$set(props) - await Svelte.tick() + await act(() => component.$set(props)) }, unmount: () => { cleanupComponent(component) }, - ...getQueriesForElement(container, queries), + ...getQueriesForElement(baseElement, renderOptions.queries), } } +const checkProps = (options) => { + const isOptions = Object.keys(options).some((option) => + svelteComponentOptions.includes(option) + ) + + // Check if any props and Svelte options were accidentally mixed. + if (isOptions) { + const unrecognizedOptions = Object.keys(options).filter( + (option) => !svelteComponentOptions.includes(option) + ) + + if (unrecognizedOptions.length > 0) { + throw Error(` + Unknown component options: [${unrecognizedOptions.join(', ')}] + Valid Svelte component options: [${svelteComponentOptions.join(', ')}] + + This error occurs if props are mixed with Svelte component options, + or any props use the same name as a Svelte component option. + Either rename the props, or place props under the \`props\` option. + + Eg: const { /** results **/ } = render(MyComponent, { props: { /** props here **/ } }) +`) + } + + return options + } + + return { props: options } +} + const cleanupComponent = (component) => { const inCache = componentCache.delete(component) diff --git a/types/index.d.ts b/types/index.d.ts index d60d779..443308e 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,6 +30,7 @@ export type RenderResult< Q extends Queries = typeof queries, > = { container: HTMLElement + baseElement: HTMLElement component: C debug: (el?: HTMLElement | DocumentFragment) => void rerender: (props: ComponentProps) => Promise @@ -42,17 +38,14 @@ export type RenderResult< } & { [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 From 450809d60aa2e4e3dea1e0eff55a4391e4d59404 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Thu, 15 Feb 2024 14:29:22 -0500 Subject: [PATCH 02/11] fixup: simplify --- src/pure.js | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/pure.js b/src/pure.js index da286c8..c5cb9ca 100644 --- a/src/pure.js +++ b/src/pure.js @@ -25,30 +25,21 @@ const render = (Component, componentOptions = {}, renderOptions = {}) => { targetCache.add(target) - const ComponentConstructor = Component.default || Component - - const renderComponent = (options) => { - options = { target, ...checkProps(options) } - - const component = IS_SVELTE_5 - ? Svelte.createRoot(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 + const ComponentConstructor = Component.default ?? Component + const component = IS_SVELTE_5 + ? Svelte.createRoot(ComponentConstructor, { ...componentOptions, target }) + : new ComponentConstructor({ ...componentOptions, target }) + + 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) + }) } - const component = renderComponent({ ...componentOptions, target }) - return { component, baseElement, From 2c7f46025d544a12484cdda26116d06479c13b5a Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Sat, 17 Feb 2024 14:00:56 -0500 Subject: [PATCH 03/11] fixup: formatting --- src/pure.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pure.js b/src/pure.js index c5cb9ca..79fbcca 100644 --- a/src/pure.js +++ b/src/pure.js @@ -27,8 +27,8 @@ const render = (Component, componentOptions = {}, renderOptions = {}) => { const ComponentConstructor = Component.default ?? Component const component = IS_SVELTE_5 - ? Svelte.createRoot(ComponentConstructor, { ...componentOptions, target }) - : new ComponentConstructor({ ...componentOptions, target }) + ? Svelte.createRoot(ComponentConstructor, { ...componentOptions, target }) + : new ComponentConstructor({ ...componentOptions, target }) componentCache.add(component) From 90535154c39f36a205e711349d2b2da2295c5e14 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Sat, 17 Feb 2024 14:01:28 -0500 Subject: [PATCH 04/11] fixup: formatting --- src/__tests__/render.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js index e09075b..4523d47 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -74,7 +74,11 @@ describe('render', () => { baseElement.appendChild(target) target.appendChild(anchor) - const {getByTestId} = render(Comp, { props, target, anchor }, { baseElement }) + const { getByTestId } = render( + Comp, + { props, target, anchor }, + { baseElement } + ) const firstElement = getByTestId('test') expect(target.firstChild).toBe(firstElement) From f4ae93da17f2a896ce09fe3df3acf19b9c8b5085 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Sat, 17 Feb 2024 14:26:53 -0500 Subject: [PATCH 05/11] fixup: fix rerender types --- src/__tests__/fixtures/Simple.svelte | 2 ++ types/index.d.ts | 2 +- types/types.test-d.ts | 19 +++++++++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) 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/types/index.d.ts b/types/index.d.ts index 443308e..a206467 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -33,7 +33,7 @@ export type RenderResult< baseElement: HTMLElement component: C debug: (el?: HTMLElement | DocumentFragment) => void - rerender: (props: ComponentProps) => Promise + rerender: (props: Partial>) => Promise unmount: () => void } & { [P in keyof Q]: BoundFunction } 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 } } ) From 3aa48e954f0803f57b259b71d076169df980e223 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Mon, 19 Feb 2024 16:32:14 -0500 Subject: [PATCH 06/11] fixup: replace some removed Svelte v5 APIs --- src/pure.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pure.js b/src/pure.js index 79fbcca..95d6726 100644 --- a/src/pure.js +++ b/src/pure.js @@ -4,8 +4,10 @@ import { prettyDOM, } from '@testing-library/dom' import * as Svelte from 'svelte' +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' + +const IS_SVELTE_5 = SVELTE_VERSION >= '5' -const IS_SVELTE_5 = typeof Svelte.createRoot === 'function' const targetCache = new Set() const componentCache = new Set() @@ -27,7 +29,7 @@ const render = (Component, componentOptions = {}, renderOptions = {}) => { const ComponentConstructor = Component.default ?? Component const component = IS_SVELTE_5 - ? Svelte.createRoot(ComponentConstructor, { ...componentOptions, target }) + ? Svelte.mount(ComponentConstructor, { ...componentOptions, target }) : new ComponentConstructor({ ...componentOptions, target }) componentCache.add(component) @@ -95,7 +97,11 @@ const cleanupComponent = (component) => { const inCache = componentCache.delete(component) if (inCache) { - component.$destroy() + if (IS_SVELTE_5) { + Svelte.unmount(component) + } else { + component.$destroy() + } } } From 9172cdf051f8df7ce7bbc59d1edee71de4492644 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Wed, 28 Feb 2024 11:18:07 -0500 Subject: [PATCH 07/11] wip --- src/__tests__/render.test.js.orig | 122 ++++++++++++++ src/__tests__/rerender.test.js.orig | 61 +++++++ src/pure.js | 43 ++--- src/pure.js.orig | 249 ++++++++++++++++++++++++++++ src/svelte5.js | 2 +- 5 files changed, 455 insertions(+), 22 deletions(-) create mode 100644 src/__tests__/render.test.js.orig create mode 100644 src/__tests__/rerender.test.js.orig create mode 100644 src/pure.js.orig diff --git a/src/__tests__/render.test.js.orig b/src/__tests__/render.test.js.orig new file mode 100644 index 0000000..fb33453 --- /dev/null +++ b/src/__tests__/render.test.js.orig @@ -0,0 +1,122 @@ +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' +import { describe, expect, test } from 'vitest' + +<<<<<<< HEAD +import { render } from '..' +======= +import { act, render as stlRender } from '@testing-library/svelte' +>>>>>>> origin/next +import Comp from './fixtures/Comp.svelte' + +describe('render', () => { + const props = { name: 'World' } + + test('renders component into the document', () => { + const { getByText } = render(Comp, { props }) + + expect(getByText('Hello World!')).toBeInTheDocument() + }) + + test('accepts props directly', () => { + const { getByText } = render(Comp, props) + expect(getByText('Hello World!')).toBeInTheDocument() + }) + + test('throws error when mixing svelte component options and props', () => { + expect(() => { + render(Comp, { props, name: 'World' }) + }).toThrow(/Unknown component options/) + }) + + test('throws error when mixing target option and props', () => { + expect(() => { + render(Comp, { target: document.createElement('div'), name: 'World' }) + }).toThrow(/Unknown component options/) + }) + + test('should return a container object wrapping the DOM of the rendered component', () => { + const { container, getByTestId } = render(Comp, props) + const firstElement = getByTestId('test') + + expect(container.firstChild).toBe(firstElement) + }) + + 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('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('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 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) + } + ) +<<<<<<< HEAD +======= + + test('should throw error when mixing svelte component options and props', () => { + expect(() => { + stlRender(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() + + expect(container.innerHTML).toBe(document.body.innerHTML) + }) + + test('correctly find component constructor on the default property', () => { + const { getByText } = stlRender(CompDefault, { props: { name: 'World' } }) + + expect(getByText('Hello World!')).toBeInTheDocument() + }) + + test("accept the 'context' option", () => { + const { getByText } = stlRender(Comp, { + props: { name: 'Universe' }, + context: new Map([['name', 'context']]), + }) + + expect(getByText('we have context')).toBeInTheDocument() + }) +>>>>>>> origin/next +}) diff --git a/src/__tests__/rerender.test.js.orig b/src/__tests__/rerender.test.js.orig new file mode 100644 index 0000000..ec0fe2f --- /dev/null +++ b/src/__tests__/rerender.test.js.orig @@ -0,0 +1,61 @@ +<<<<<<< HEAD +import { VERSION as SVELTE_VERSION } from 'svelte/compiler' +import { describe, expect, test, vi } from 'vitest' + +import { act, render, screen } from '..' +import Comp from './fixtures/Comp.svelte' +======= +/** + * @jest-environment jsdom + */ +import { expect, test, vi } from 'vitest' + +import { render, waitFor } from '@testing-library/svelte' + +import Comp from './fixtures/Rerender.svelte' +>>>>>>> origin/next + +describe('rerender', () => { + test('updates props', async () => { + const { rerender } = render(Comp, { name: 'World' }) + const element = screen.getByText('Hello World!') + + await rerender({ name: 'Dolly' }) + + expect(element).toHaveTextContent('Hello Dolly!') + }) + + 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 rerender({ props: { name: 'Dolly' } }) + + expect(element).toHaveTextContent('Hello Dolly!') + expect(console.warn).toHaveBeenCalledOnce() + expect(console.warn).toHaveBeenCalledWith( + expect.stringMatching(/deprecated/iu) + ) + }) + + 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(element).toBeInTheDocument() + expect(component.name).toBe('World') + + await act(() => { + component.name = 'Planet' + }) + + expect(element).toHaveTextContent('Hello Planet!') + }) +}) diff --git a/src/pure.js b/src/pure.js index 2b2ae81..75053dc 100644 --- a/src/pure.js +++ b/src/pure.js @@ -5,10 +5,8 @@ import { } 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 = SVELTE_VERSION >= '5' +const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) export const targetCache = new Set() export const componentCache = new Set() @@ -22,24 +20,27 @@ const svelteComponentOptions = [ ] export const buildCheckProps = (svelteComponentOptions) => (options) => { - const isProps = !Object.keys(options).some((option) => + const isOptions = Object.keys(options).some((option) => svelteComponentOptions.includes(option) ) // Check if any props and Svelte options were accidentally mixed. - if (!isProps) { + if (isOptions) { 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 - `) + Unknown component options: [${unrecognizedOptions.join(', ')}] + Valid Svelte component options: [${svelteComponentOptions.join(', ')}] + + This error occurs if props are mixed with Svelte component options, + or any props use the same name as a Svelte component option. + Either rename the props, or place props under the \`props\` option. + + Eg: const { /** results **/ } = render(MyComponent, { props: { /** props here **/ } }) +`) } return options @@ -53,7 +54,7 @@ const checkProps = buildCheckProps(svelteComponentOptions) const buildRenderComponent = ({ target, ComponentConstructor }) => (options) => { - options = { target, ...checkProps(options) } + options = checkProps(options) if (IS_SVELTE_5) throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') @@ -75,9 +76,13 @@ const buildRenderComponent = export const buildRender = (buildRenderComponent) => - (Component, { target, ...options } = {}, { container, queries } = {}) => { - container = container || document.body - target = target || container.appendChild(document.createElement('div')) + (Component, options = {}, renderOptions = {}) => { + const baseElement = + renderOptions.baseElement ?? options.target ?? document.body + + const target = + options.target ?? baseElement.appendChild(document.createElement('div')) + targetCache.add(target) const ComponentConstructor = Component.default || Component @@ -87,7 +92,7 @@ export const buildRender = ComponentConstructor, }) - let component = renderComponent(options) + let component = renderComponent({ target, ...options }) return { container, @@ -116,11 +121,7 @@ export const cleanupComponent = (component) => { const inCache = componentCache.delete(component) if (inCache) { - if (IS_SVELTE_5) { - Svelte.unmount(component) - } else { - component.$destroy() - } + component.$destroy() } } diff --git a/src/pure.js.orig b/src/pure.js.orig new file mode 100644 index 0000000..86dcf99 --- /dev/null +++ b/src/pure.js.orig @@ -0,0 +1,249 @@ +import { + fireEvent as dtlFireEvent, + 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 = SVELTE_VERSION >= '5' + +<<<<<<< HEAD +const targetCache = new Set() +const componentCache = new Set() + +const svelteComponentOptions = IS_SVELTE_5 + ? ['target', 'props', 'events', 'context', 'intro', 'recover'] + : ['target', 'accessors', 'anchor', 'props', 'hydrate', 'intro', 'context'] + +const render = (Component, componentOptions = {}, renderOptions = {}) => { + componentOptions = checkProps(componentOptions) + + const baseElement = + renderOptions.baseElement ?? componentOptions.target ?? document.body + + const target = + componentOptions.target ?? + baseElement.appendChild(document.createElement('div')) + + targetCache.add(target) + + const ComponentConstructor = Component.default ?? Component + const component = IS_SVELTE_5 + ? Svelte.mount(ComponentConstructor, { ...componentOptions, target }) + : new ComponentConstructor({ ...componentOptions, target }) + + 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, + baseElement, + 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 + } + await act(() => component.$set(props)) + }, + unmount: () => { + cleanupComponent(component) + }, + ...getQueriesForElement(baseElement, renderOptions.queries), +======= +const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) +export const targetCache = new Set() +export const componentCache = new Set() + +const svelteComponentOptions = [ + 'accessors', + 'anchor', + 'props', + 'hydrate', + 'intro', + 'context', +] + +export const buildCheckProps = (svelteComponentOptions) => (options) => { + const isProps = !Object.keys(options).some((option) => + svelteComponentOptions.includes(option) + ) + + // Check if any props and Svelte options were accidentally mixed. + if (!isProps) { + const unrecognizedOptions = Object.keys(options).filter( + (option) => !svelteComponentOptions.includes(option) + ) + + 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 checkProps = buildCheckProps(svelteComponentOptions) + +const buildRenderComponent = + ({ target, ComponentConstructor }) => + (options) => { + options = { target, ...checkProps(options) } + + if (IS_SVELTE_5) + throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') + + const component = 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 + } + +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), + } +>>>>>>> origin/next + } + +<<<<<<< HEAD +const checkProps = (options) => { + const isOptions = Object.keys(options).some((option) => + svelteComponentOptions.includes(option) + ) + + // Check if any props and Svelte options were accidentally mixed. + if (isOptions) { + const unrecognizedOptions = Object.keys(options).filter( + (option) => !svelteComponentOptions.includes(option) + ) + + if (unrecognizedOptions.length > 0) { + throw Error(` + Unknown component options: [${unrecognizedOptions.join(', ')}] + Valid Svelte component options: [${svelteComponentOptions.join(', ')}] + + This error occurs if props are mixed with Svelte component options, + or any props use the same name as a Svelte component option. + Either rename the props, or place props under the \`props\` option. + + Eg: const { /** results **/ } = render(MyComponent, { props: { /** props here **/ } }) +`) + } + + return options + } + + return { props: options } +} + +const cleanupComponent = (component) => { +======= +export const render = buildRender(buildRenderComponent) + +export const cleanupComponent = (component) => { +>>>>>>> origin/next + const inCache = componentCache.delete(component) + + if (inCache) { + if (IS_SVELTE_5) { + Svelte.unmount(component) + } else { + component.$destroy() + } + } +} + +const cleanupTarget = (target) => { + const inCache = targetCache.delete(target) + + if (inCache && target.parentNode === document.body) { + document.body.removeChild(target) + } +} + +export const cleanup = () => { + componentCache.forEach(cleanupComponent) + targetCache.forEach(cleanupTarget) +} + +export const act = async (fn) => { + if (fn) { + await fn() + } + return Svelte.tick() +} + +export 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 + } +}) diff --git a/src/svelte5.js b/src/svelte5.js index 6f30784..bf7e898 100644 --- a/src/svelte5.js +++ b/src/svelte5.js @@ -20,7 +20,7 @@ const checkProps = buildCheckProps(svelteComponentOptions) const buildRenderComponent = ({ target, ComponentConstructor }) => (options) => { - options = { target, ...checkProps(options) } + options = checkProps(options) const component = createClassComponent({ component: ComponentConstructor, From e10c11aa1a3f39b1cbe6a317855ea82a8b0e1581 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Wed, 28 Feb 2024 11:38:12 -0500 Subject: [PATCH 08/11] implement inheritance with classes --- src/pure.js | 158 +++++++++++++++++++++++++++------------------------- 1 file changed, 82 insertions(+), 76 deletions(-) diff --git a/src/pure.js b/src/pure.js index 3c412da..18dd633 100644 --- a/src/pure.js +++ b/src/pure.js @@ -7,85 +7,61 @@ import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import * as Svelte from 'svelte' const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) -export const targetCache = new Set() -export const componentCache = new Set() - -const svelteComponentOptions = [ - 'accessors', - 'anchor', - 'props', - 'hydrate', - 'intro', - 'context', -] - -export const buildCheckProps = (svelteComponentOptions) => (options) => { - const isProps = !Object.keys(options).some((option) => - svelteComponentOptions.includes(option) - ) - - // Check if any props and Svelte options were accidentally mixed. - if (!isProps) { - const unrecognizedOptions = Object.keys(options).filter( - (option) => !svelteComponentOptions.includes(option) + +class SvelteTestingLibrary { + svelteComponentOptions = [ + 'accessors', + 'anchor', + 'props', + 'hydrate', + 'intro', + 'context', + ] + + targetCache = new Set() + componentCache = new Set() + + checkProps(options) { + const isProps = !Object.keys(options).some((option) => + this.svelteComponentOptions.includes(option) ) - if (unrecognizedOptions.length > 0) { - throw Error(` + // Check if any props and Svelte options were accidentally mixed. + if (!isProps) { + const unrecognizedOptions = Object.keys(options).filter( + (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 `) - } - - return options - } - - return { props: options } -} + } -const checkProps = buildCheckProps(svelteComponentOptions) - -const buildRenderComponent = - ({ target, ComponentConstructor }) => - (options) => { - options = { target, ...checkProps(options) } - - if (IS_SVELTE_5) - throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') - - const component = 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 options } - return component + return { props: options } } -export const buildRender = - (buildRenderComponent) => - (Component, { target, ...options } = {}, { container, queries } = {}) => { + render(Component, { target, ...options } = {}, { container, queries } = {}) { container = container || document.body target = target || container.appendChild(document.createElement('div')) - targetCache.add(target) + this.targetCache.add(target) const ComponentConstructor = Component.default || Component - const renderComponent = buildRenderComponent({ - target, - ComponentConstructor, - }) - - let component = renderComponent(options) + const component = this.renderComponent( + { + target, + ComponentConstructor, + }, + options + ) return { container, @@ -108,29 +84,59 @@ export const buildRender = } } -export const render = buildRender(buildRenderComponent) + renderComponent({ target, ComponentConstructor }, options) { + options = { target, ...this.checkProps(options) } + + if (IS_SVELTE_5) + throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') + + const component = new ComponentConstructor(options) -export const cleanupComponent = (component) => { - const inCache = componentCache.delete(component) + this.componentCache.add(component) - if (inCache) { - component.$destroy() + // 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) + }) + } + + return component } -} -const cleanupTarget = (target) => { - const inCache = targetCache.delete(target) + cleanupComponent(component) { + const inCache = this.componentCache.delete(component) - if (inCache && target.parentNode === document.body) { - document.body.removeChild(target) + if (inCache) { + component.$destroy() + } + } + + cleanupTarget(target) { + const inCache = this.targetCache.delete(target) + + if (inCache && target.parentNode === document.body) { + document.body.removeChild(target) + } } -} -export const cleanup = () => { - componentCache.forEach(cleanupComponent) - targetCache.forEach(cleanupTarget) + cleanup() { + this.componentCache.forEach(cleanupComponent) + this.targetCache.forEach(cleanupTarget) + } } +const instance = new SvelteTestingLibrary() + +export const render = instance.render.bind(instance) + +export const cleanupComponent = instance.cleanupComponent.bind(instance) + +const cleanupTarget = instance.cleanupTarget.bind(instance) + +export const cleanup = instance.cleanup.bind(instance) + export const act = async (fn) => { if (fn) { await fn() From 1df1c7ca7a9af12f303a0f45595e587e3cbb4b96 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Wed, 28 Feb 2024 11:57:54 -0500 Subject: [PATCH 09/11] support for svelte 5 --- src/pure.js | 12 ++++-------- src/svelte5.js | 44 +++++++++++++++++--------------------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/src/pure.js b/src/pure.js index 18dd633..cb90733 100644 --- a/src/pure.js +++ b/src/pure.js @@ -8,7 +8,7 @@ import * as Svelte from 'svelte' const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) -class SvelteTestingLibrary { +export class SvelteTestingLibrary { svelteComponentOptions = [ 'accessors', 'anchor', @@ -78,7 +78,7 @@ class SvelteTestingLibrary { await Svelte.tick() }, unmount: () => { - cleanupComponent(component) + this.cleanupComponent(component) }, ...getQueriesForElement(container, queries), } @@ -122,8 +122,8 @@ class SvelteTestingLibrary { } cleanup() { - this.componentCache.forEach(cleanupComponent) - this.targetCache.forEach(cleanupTarget) + this.componentCache.forEach(this.cleanupComponent.bind(this)) + this.targetCache.forEach(this.cleanupTarget.bind(this)) } } @@ -131,10 +131,6 @@ const instance = new SvelteTestingLibrary() export const render = instance.render.bind(instance) -export const cleanupComponent = instance.cleanupComponent.bind(instance) - -const cleanupTarget = instance.cleanupTarget.bind(instance) - export const cleanup = instance.cleanup.bind(instance) export const act = async (fn) => { diff --git a/src/svelte5.js b/src/svelte5.js index 6f30784..57e9e54 100644 --- a/src/svelte5.js +++ b/src/svelte5.js @@ -1,41 +1,31 @@ import { createClassComponent } from 'svelte/legacy' -import { - componentCache, - cleanup, - buildCheckProps, - buildRender, -} from './pure.js' +import { SvelteTestingLibrary } from './pure.js' -const svelteComponentOptions = [ - 'target', - 'props', - 'events', - 'context', - 'intro', - 'recover', -] +class Svelte5TestingLibrary extends SvelteTestingLibrary { + svelteComponentOptions = [ + 'target', + 'props', + 'events', + 'context', + 'intro', + 'recover', + ] -const checkProps = buildCheckProps(svelteComponentOptions) - -const buildRenderComponent = - ({ target, ComponentConstructor }) => - (options) => { - options = { target, ...checkProps(options) } + renderComponent({ target, ComponentConstructor }, options) { + options = { target, ...this.checkProps(options) } const component = createClassComponent({ component: ComponentConstructor, ...options, }) - componentCache.add(component) + this.componentCache.add(component) return component } +} -const render = buildRender(buildRenderComponent) - -/* eslint-disable import/export */ - -import { act, fireEvent } from './pure.js' +const instance = new Svelte5TestingLibrary() -export { render, cleanup, fireEvent, act } +export const render = instance.render.bind(instance) +export const cleanup = instance.cleanup.bind(instance) From 55e8355b0e91cb46c0000583fa8eabeff4dc2681 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Wed, 28 Feb 2024 15:14:48 -0500 Subject: [PATCH 10/11] all tests pass --- src/__tests__/fixtures/Comp.svelte | 6 +- src/__tests__/render.test.js | 16 +- src/__tests__/render.test.js.orig | 122 -------------- src/__tests__/rerender.test.js | 2 +- src/__tests__/rerender.test.js.orig | 61 ------- src/pure.js | 41 ++--- src/pure.js.orig | 249 ---------------------------- src/svelte5.js | 10 +- 8 files changed, 38 insertions(+), 469 deletions(-) delete mode 100644 src/__tests__/render.test.js.orig delete mode 100644 src/__tests__/rerender.test.js.orig delete mode 100644 src/pure.js.orig diff --git a/src/__tests__/fixtures/Comp.svelte b/src/__tests__/fixtures/Comp.svelte index c739725..a0caf8b 100644 --- a/src/__tests__/fixtures/Comp.svelte +++ b/src/__tests__/fixtures/Comp.svelte @@ -1,6 +1,8 @@

Hello {name}!

- +
we have {contextName}
diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js index 524a4ef..446823e 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 { 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' describe('render', () => { @@ -21,13 +21,13 @@ describe('render', () => { test('throws error when mixing svelte component options and props', () => { expect(() => { render(Comp, { props, name: 'World' }) - }).toThrow(/Unknown component options/) + }).toThrow(/Unknown options/) }) test('throws error when mixing target option and props', () => { expect(() => { render(Comp, { target: document.createElement('div'), name: 'World' }) - }).toThrow(/Unknown component options/) + }).toThrow(/Unknown options/) }) test('should return a container object wrapping the DOM of the rendered component', () => { @@ -88,24 +88,24 @@ describe('render', () => { 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__/render.test.js.orig b/src/__tests__/render.test.js.orig deleted file mode 100644 index fb33453..0000000 --- a/src/__tests__/render.test.js.orig +++ /dev/null @@ -1,122 +0,0 @@ -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { describe, expect, test } from 'vitest' - -<<<<<<< HEAD -import { render } from '..' -======= -import { act, render as stlRender } from '@testing-library/svelte' ->>>>>>> origin/next -import Comp from './fixtures/Comp.svelte' - -describe('render', () => { - const props = { name: 'World' } - - test('renders component into the document', () => { - const { getByText } = render(Comp, { props }) - - expect(getByText('Hello World!')).toBeInTheDocument() - }) - - test('accepts props directly', () => { - const { getByText } = render(Comp, props) - expect(getByText('Hello World!')).toBeInTheDocument() - }) - - test('throws error when mixing svelte component options and props', () => { - expect(() => { - render(Comp, { props, name: 'World' }) - }).toThrow(/Unknown component options/) - }) - - test('throws error when mixing target option and props', () => { - expect(() => { - render(Comp, { target: document.createElement('div'), name: 'World' }) - }).toThrow(/Unknown component options/) - }) - - test('should return a container object wrapping the DOM of the rendered component', () => { - const { container, getByTestId } = render(Comp, props) - const firstElement = getByTestId('test') - - expect(container.firstChild).toBe(firstElement) - }) - - 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('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('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 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) - } - ) -<<<<<<< HEAD -======= - - test('should throw error when mixing svelte component options and props', () => { - expect(() => { - stlRender(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() - - expect(container.innerHTML).toBe(document.body.innerHTML) - }) - - test('correctly find component constructor on the default property', () => { - const { getByText } = stlRender(CompDefault, { props: { name: 'World' } }) - - expect(getByText('Hello World!')).toBeInTheDocument() - }) - - test("accept the 'context' option", () => { - const { getByText } = stlRender(Comp, { - props: { name: 'Universe' }, - context: new Map([['name', 'context']]), - }) - - expect(getByText('we have context')).toBeInTheDocument() - }) ->>>>>>> origin/next -}) diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js index 744a7cb..1dee6da 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -6,7 +6,7 @@ import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { act, screen, render, waitFor } from '@testing-library/svelte' -import Comp from './fixtures/Rerender.svelte' +import Comp from './fixtures/Comp.svelte' describe('rerender', () => { test('updates props', async () => { diff --git a/src/__tests__/rerender.test.js.orig b/src/__tests__/rerender.test.js.orig deleted file mode 100644 index ec0fe2f..0000000 --- a/src/__tests__/rerender.test.js.orig +++ /dev/null @@ -1,61 +0,0 @@ -<<<<<<< HEAD -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { describe, expect, test, vi } from 'vitest' - -import { act, render, screen } from '..' -import Comp from './fixtures/Comp.svelte' -======= -/** - * @jest-environment jsdom - */ -import { expect, test, vi } from 'vitest' - -import { render, waitFor } from '@testing-library/svelte' - -import Comp from './fixtures/Rerender.svelte' ->>>>>>> origin/next - -describe('rerender', () => { - test('updates props', async () => { - const { rerender } = render(Comp, { name: 'World' }) - const element = screen.getByText('Hello World!') - - await rerender({ name: 'Dolly' }) - - expect(element).toHaveTextContent('Hello Dolly!') - }) - - 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 rerender({ props: { name: 'Dolly' } }) - - expect(element).toHaveTextContent('Hello Dolly!') - expect(console.warn).toHaveBeenCalledOnce() - expect(console.warn).toHaveBeenCalledWith( - expect.stringMatching(/deprecated/iu) - ) - }) - - 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(element).toBeInTheDocument() - expect(component.name).toBe('World') - - await act(() => { - component.name = 'Planet' - }) - - expect(element).toHaveTextContent('Hello Planet!') - }) -}) diff --git a/src/pure.js b/src/pure.js index cb90733..a82e67e 100644 --- a/src/pure.js +++ b/src/pure.js @@ -48,25 +48,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 +87,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/pure.js.orig b/src/pure.js.orig deleted file mode 100644 index 86dcf99..0000000 --- a/src/pure.js.orig +++ /dev/null @@ -1,249 +0,0 @@ -import { - fireEvent as dtlFireEvent, - 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 = SVELTE_VERSION >= '5' - -<<<<<<< HEAD -const targetCache = new Set() -const componentCache = new Set() - -const svelteComponentOptions = IS_SVELTE_5 - ? ['target', 'props', 'events', 'context', 'intro', 'recover'] - : ['target', 'accessors', 'anchor', 'props', 'hydrate', 'intro', 'context'] - -const render = (Component, componentOptions = {}, renderOptions = {}) => { - componentOptions = checkProps(componentOptions) - - const baseElement = - renderOptions.baseElement ?? componentOptions.target ?? document.body - - const target = - componentOptions.target ?? - baseElement.appendChild(document.createElement('div')) - - targetCache.add(target) - - const ComponentConstructor = Component.default ?? Component - const component = IS_SVELTE_5 - ? Svelte.mount(ComponentConstructor, { ...componentOptions, target }) - : new ComponentConstructor({ ...componentOptions, target }) - - 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, - baseElement, - 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 - } - await act(() => component.$set(props)) - }, - unmount: () => { - cleanupComponent(component) - }, - ...getQueriesForElement(baseElement, renderOptions.queries), -======= -const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION) -export const targetCache = new Set() -export const componentCache = new Set() - -const svelteComponentOptions = [ - 'accessors', - 'anchor', - 'props', - 'hydrate', - 'intro', - 'context', -] - -export const buildCheckProps = (svelteComponentOptions) => (options) => { - const isProps = !Object.keys(options).some((option) => - svelteComponentOptions.includes(option) - ) - - // Check if any props and Svelte options were accidentally mixed. - if (!isProps) { - const unrecognizedOptions = Object.keys(options).filter( - (option) => !svelteComponentOptions.includes(option) - ) - - 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 checkProps = buildCheckProps(svelteComponentOptions) - -const buildRenderComponent = - ({ target, ComponentConstructor }) => - (options) => { - options = { target, ...checkProps(options) } - - if (IS_SVELTE_5) - throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`') - - const component = 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 - } - -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), - } ->>>>>>> origin/next - } - -<<<<<<< HEAD -const checkProps = (options) => { - const isOptions = Object.keys(options).some((option) => - svelteComponentOptions.includes(option) - ) - - // Check if any props and Svelte options were accidentally mixed. - if (isOptions) { - const unrecognizedOptions = Object.keys(options).filter( - (option) => !svelteComponentOptions.includes(option) - ) - - if (unrecognizedOptions.length > 0) { - throw Error(` - Unknown component options: [${unrecognizedOptions.join(', ')}] - Valid Svelte component options: [${svelteComponentOptions.join(', ')}] - - This error occurs if props are mixed with Svelte component options, - or any props use the same name as a Svelte component option. - Either rename the props, or place props under the \`props\` option. - - Eg: const { /** results **/ } = render(MyComponent, { props: { /** props here **/ } }) -`) - } - - return options - } - - return { props: options } -} - -const cleanupComponent = (component) => { -======= -export const render = buildRender(buildRenderComponent) - -export const cleanupComponent = (component) => { ->>>>>>> origin/next - const inCache = componentCache.delete(component) - - if (inCache) { - if (IS_SVELTE_5) { - Svelte.unmount(component) - } else { - component.$destroy() - } - } -} - -const cleanupTarget = (target) => { - const inCache = targetCache.delete(target) - - if (inCache && target.parentNode === document.body) { - document.body.removeChild(target) - } -} - -export const cleanup = () => { - componentCache.forEach(cleanupComponent) - targetCache.forEach(cleanupTarget) -} - -export const act = async (fn) => { - if (fn) { - await fn() - } - return Svelte.tick() -} - -export 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 - } -}) 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 } } From ad06be3b0e57ebef12879d2c5e2aa2636f1d64e8 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Wed, 28 Feb 2024 15:31:00 -0500 Subject: [PATCH 11/11] add target to the options --- src/pure.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pure.js b/src/pure.js index a82e67e..ef081cb 100644 --- a/src/pure.js +++ b/src/pure.js @@ -16,6 +16,7 @@ export class SvelteTestingLibrary { 'hydrate', 'intro', 'context', + 'target', ] targetCache = new Set()