From 178b2dee9c9f3719e88e5e513a562e541532c1e5 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Tue, 13 Feb 2024 13:11:23 -0500 Subject: [PATCH 01/10] feat: add Svelte v5-next support (#321) * fix: remove DOM elements even if component creation fails Fixes #190 * feat: add Svelte v5-next support --- .github/workflows/release.yml | 12 +++ package.json | 8 +- .../__snapshots__/render.test.js.snap | 27 ++++++- src/__tests__/cleanup.test.js | 35 +++++++++ src/__tests__/fixtures/Mounter.svelte | 18 +++-- src/__tests__/fixtures/Rerender.svelte | 20 ++--- src/__tests__/mount.test.js | 26 +++---- src/__tests__/render.test.js | 61 +++++++++------ src/__tests__/rerender.test.js | 19 +++-- src/__tests__/transition.test.js | 3 +- src/pure.js | 74 +++++++++++-------- vite.config.js | 2 +- 12 files changed, 210 insertions(+), 95 deletions(-) create mode 100644 src/__tests__/cleanup.test.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6e83fa..ab433cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,11 +16,23 @@ jobs: if: ${{ !contains(github.head_ref, 'all-contributors') }} name: Node ${{ matrix.node }}, Svelte ${{ matrix.svelte }}, ${{ matrix.test-runner }} runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} strategy: + fail-fast: false matrix: node: ['16', '18', '20'] svelte: ['3', '4'] test-runner: ['vitest:jsdom', 'vitest:happy-dom'] + experimental: [false] + include: + - node: '20' + svelte: 'next' + test-runner: 'vitest:jsdom' + experimental: true + - node: '20' + svelte: 'next' + test-runner: 'vitest:happy-dom' + experimental: true steps: - name: ⬇️ Checkout repo diff --git a/package.json b/package.json index 20bc1c2..938f563 100644 --- a/package.json +++ b/package.json @@ -64,13 +64,13 @@ "contributors:generate": "all-contributors generate" }, "peerDependencies": { - "svelte": "^3 || ^4" + "svelte": "^3 || ^4 || ^5" }, "dependencies": { "@testing-library/dom": "^9.3.1" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.2", + "@sveltejs/vite-plugin-svelte": "^3.0.2", "@testing-library/jest-dom": "^6.3.0", "@testing-library/user-event": "^14.5.2", "@typescript-eslint/eslint-plugin": "6.19.1", @@ -94,11 +94,11 @@ "npm-run-all": "^4.1.5", "prettier": "3.2.4", "prettier-plugin-svelte": "3.1.2", - "svelte": "^3 || ^4", + "svelte": "^4.2.10", "svelte-check": "^3.6.3", "svelte-jester": "^3.0.0", "typescript": "^5.3.3", - "vite": "^4.3.9", + "vite": "^5.1.1", "vitest": "^0.33.0" } } diff --git a/src/__tests__/__snapshots__/render.test.js.snap b/src/__tests__/__snapshots__/render.test.js.snap index a31cf2e..b9eb849 100644 --- a/src/__tests__/__snapshots__/render.test.js.snap +++ b/src/__tests__/__snapshots__/render.test.js.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`render > should accept svelte component options 1`] = ` +exports[`render > should accept svelte v4 component options 1`] = `

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

+ Hello World! +

+ +
+ we have context +
+ + + +
+ +`; diff --git a/src/__tests__/cleanup.test.js b/src/__tests__/cleanup.test.js new file mode 100644 index 0000000..789338d --- /dev/null +++ b/src/__tests__/cleanup.test.js @@ -0,0 +1,35 @@ +import { describe, expect, test, vi } from 'vitest' + +import { act, cleanup, render } from '..' +import Mounter from './fixtures/Mounter.svelte' + +const onExecuted = vi.fn() +const onDestroyed = vi.fn() +const renderSubject = () => render(Mounter, { onExecuted, onDestroyed }) + +describe('cleanup', () => { + test('cleanup deletes element', async () => { + renderSubject() + cleanup() + + expect(document.body).toBeEmptyDOMElement() + }) + + test('cleanup unmounts component', async () => { + await act(renderSubject) + cleanup() + + expect(onDestroyed).toHaveBeenCalledOnce() + }) + + test('cleanup handles unexpected errors during mount', () => { + onExecuted.mockImplementation(() => { + throw new Error('oh no!') + }) + + expect(renderSubject).toThrowError() + cleanup() + + expect(document.body).toBeEmptyDOMElement() + }) +}) diff --git a/src/__tests__/fixtures/Mounter.svelte b/src/__tests__/fixtures/Mounter.svelte index 477bb34..51ebcd8 100644 --- a/src/__tests__/fixtures/Mounter.svelte +++ b/src/__tests__/fixtures/Mounter.svelte @@ -1,11 +1,19 @@ -
-
- -`; - -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' From 16f6b404b7d929109d0534f6a1e51d7a3b968f02 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Mon, 25 Mar 2024 17:20:29 -0400 Subject: [PATCH 05/10] test: remove unnecessary tests and skips (#338) --- .eslintrc.cjs | 3 +++ .prettierignore | 1 + .prettierrc.yaml | 2 +- src/__tests__/act.test.js | 39 ++++++--------------------- src/__tests__/auto-cleanup.test.js | 2 +- src/__tests__/cleanup.test.js | 7 +++-- src/__tests__/context.test.js | 2 +- src/__tests__/debug.test.js | 14 +++------- src/__tests__/events.test.js | 4 +-- src/__tests__/fixtures/Comp.svelte | 2 +- src/__tests__/fixtures/Context.svelte | 4 +-- src/__tests__/mount.test.js | 2 +- src/__tests__/render.test.js | 39 +++++++++++++-------------- src/__tests__/rerender.test.js | 2 +- src/__tests__/transition.test.js | 7 +++-- src/vitest.js | 3 +-- svelte.config.js | 6 ++--- 17 files changed, 54 insertions(+), 85 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6021776..778d507 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -23,6 +23,9 @@ module.exports = { parserOptions: { parser: '@typescript-eslint/parser', }, + rules: { + 'no-undef-init': 'off', + }, }, { files: ['*.ts'], diff --git a/.prettierignore b/.prettierignore index 113f29b..5e50c20 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ scripts/* .eslintignore .prettierignore +.all-contributorsrc diff --git a/.prettierrc.yaml b/.prettierrc.yaml index e1bf864..0a2ace3 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -4,6 +4,6 @@ trailingComma: es5 plugins: - prettier-plugin-svelte overrides: - - files: "*.svelte" + - files: '*.svelte' options: parser: svelte diff --git a/src/__tests__/act.test.js b/src/__tests__/act.test.js index 57fa5b9..75c9ded 100644 --- a/src/__tests__/act.test.js +++ b/src/__tests__/act.test.js @@ -1,25 +1,13 @@ -import { beforeEach, describe, expect, test } from 'vitest' +import { setTimeout } from 'node:timers/promises' + +import { act, render } from '@testing-library/svelte' +import { describe, expect, test } from 'vitest' -import { act, fireEvent, render as stlRender } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' describe('act', () => { - let props - - const render = () => { - return stlRender(Comp, { - props - }) - } - - beforeEach(() => { - props = { - name: 'World' - } - }) - test('state updates are flushed', async () => { - const { getByText } = render() + const { getByText } = render(Comp) const button = getByText('Button') expect(button).toHaveTextContent('Button') @@ -31,24 +19,13 @@ describe('act', () => { expect(button).toHaveTextContent('Button Clicked') }) - test('findByTestId returns the element', async () => { - const { findByTestId } = render() - - expect(await findByTestId('test')).toHaveTextContent(`Hello ${props.name}!`) - }) - test('accepts async functions', async () => { - const sleep = (ms) => - new Promise((resolve) => { - setTimeout(() => resolve(), ms) - }) - - const { getByText } = render() + const { getByText } = render(Comp) const button = getByText('Button') await act(async () => { - await sleep(100) - await fireEvent.click(button) + await setTimeout(100) + button.click() }) expect(button).toHaveTextContent('Button Clicked') diff --git a/src/__tests__/auto-cleanup.test.js b/src/__tests__/auto-cleanup.test.js index 4d38d35..206d101 100644 --- a/src/__tests__/auto-cleanup.test.js +++ b/src/__tests__/auto-cleanup.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('auto-cleanup', () => { diff --git a/src/__tests__/cleanup.test.js b/src/__tests__/cleanup.test.js index 3e49548..7131624 100644 --- a/src/__tests__/cleanup.test.js +++ b/src/__tests__/cleanup.test.js @@ -1,7 +1,6 @@ +import { cleanup, render } from '@testing-library/svelte' import { describe, expect, test, vi } from 'vitest' -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' -import { act, cleanup, render } from '@testing-library/svelte' import Mounter from './fixtures/Mounter.svelte' const onExecuted = vi.fn() @@ -16,8 +15,8 @@ describe('cleanup', () => { expect(document.body).toBeEmptyDOMElement() }) - test.runIf(SVELTE_VERSION < '5')('cleanup unmounts component', async () => { - await act(renderSubject) + test('cleanup unmounts component', () => { + renderSubject() cleanup() expect(onDestroyed).toHaveBeenCalledOnce() diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js index e7f2fd5..0fd672d 100644 --- a/src/__tests__/context.test.js +++ b/src/__tests__/context.test.js @@ -1,6 +1,6 @@ +import { render } from '@testing-library/svelte' import { expect, test } from 'vitest' -import { render } from '@testing-library/svelte' import Comp from './fixtures/Context.svelte' import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js' diff --git a/src/__tests__/debug.test.js b/src/__tests__/debug.test.js index 769c0c7..e2a9eda 100644 --- a/src/__tests__/debug.test.js +++ b/src/__tests__/debug.test.js @@ -1,19 +1,13 @@ import { prettyDOM } from '@testing-library/dom' -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' - import { render } from '@testing-library/svelte' +import { describe, expect, test, vi } from 'vitest' + import Comp from './fixtures/Comp.svelte' describe('debug', () => { - beforeEach(() => { - vi.spyOn(console, 'log').mockImplementation(() => {}) - }) - - afterEach(() => { - console.log.mockRestore() - }) - test('pretty prints the base element', () => { + vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() }) + const { baseElement, debug } = render(Comp, { props: { name: 'world' } }) debug() diff --git a/src/__tests__/events.test.js b/src/__tests__/events.test.js index 16f688c..9074db9 100644 --- a/src/__tests__/events.test.js +++ b/src/__tests__/events.test.js @@ -1,6 +1,6 @@ +import { fireEvent, render } from '@testing-library/svelte' import { describe, expect, test } from 'vitest' -import { fireEvent, render } from '@testing-library/svelte' import Comp from './fixtures/Comp.svelte' describe('events', () => { @@ -21,7 +21,7 @@ describe('events', () => { button, new MouseEvent('click', { bubbles: true, - cancelable: true + cancelable: true, }) ) diff --git a/src/__tests__/fixtures/Comp.svelte b/src/__tests__/fixtures/Comp.svelte index c739725..ba23d88 100644 --- a/src/__tests__/fixtures/Comp.svelte +++ b/src/__tests__/fixtures/Comp.svelte @@ -1,7 +1,7 @@
{ctx.message}
diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js index df0792b..d650368 100644 --- a/src/__tests__/mount.test.js +++ b/src/__tests__/mount.test.js @@ -1,6 +1,6 @@ +import { act, render, screen } from '@testing-library/svelte' import { describe, expect, test, vi } from 'vitest' -import { act, render, screen } from '@testing-library/svelte' import Mounter from './fixtures/Mounter.svelte' import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js' diff --git a/src/__tests__/render.test.js b/src/__tests__/render.test.js index 9221012..ea445d5 100644 --- a/src/__tests__/render.test.js +++ b/src/__tests__/render.test.js @@ -1,8 +1,8 @@ import { render } from '@testing-library/svelte' -import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { describe, expect, test } from 'vitest' import Comp from './fixtures/Comp.svelte' +import { IS_SVELTE_5 } from './utils.js' describe('render', () => { const props = { name: 'World' } @@ -65,24 +65,21 @@ describe('render', () => { 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) - } - ) + test.skipIf(IS_SVELTE_5)('should accept anchor option in Svelte v4', () => { + const baseElement = document.body + const target = document.createElement('section') + const anchor = document.createElement('div') + baseElement.appendChild(target) + target.appendChild(anchor) + + const { getByTestId } = render( + Comp, + { props, target, anchor }, + { baseElement } + ) + const firstElement = getByTestId('test') + + expect(target.firstChild).toBe(firstElement) + expect(target.lastChild).toBe(anchor) + }) }) diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js index 6efda86..ca4b8e8 100644 --- a/src/__tests__/rerender.test.js +++ b/src/__tests__/rerender.test.js @@ -15,7 +15,7 @@ describe('rerender', () => { }) test('warns if incorrect arguments shape used', async () => { - vi.stubGlobal('console', { warn: vi.fn() }) + vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() }) const { rerender } = render(Comp, { name: 'World' }) const element = screen.getByText('Hello World!') diff --git a/src/__tests__/transition.test.js b/src/__tests__/transition.test.js index 7e7dd3d..a9c8e02 100644 --- a/src/__tests__/transition.test.js +++ b/src/__tests__/transition.test.js @@ -1,12 +1,11 @@ +import { render, screen, waitFor } from '@testing-library/svelte' import { userEvent } from '@testing-library/user-event' import { beforeEach, describe, expect, test, vi } from 'vitest' -import { IS_JSDOM, IS_SVELTE_5 } from './utils.js' - -import { render, screen, waitFor } from '@testing-library/svelte' import Transitioner from './fixtures/Transitioner.svelte' +import { IS_JSDOM, IS_SVELTE_5 } from './utils.js' -describe.runIf(!IS_SVELTE_5)('transitions', () => { +describe.skipIf(IS_SVELTE_5)('transitions', () => { beforeEach(() => { if (!IS_JSDOM) return diff --git a/src/vitest.js b/src/vitest.js index 2f9930c..71977e6 100644 --- a/src/vitest.js +++ b/src/vitest.js @@ -1,6 +1,5 @@ -import { afterEach } from 'vitest' - import { act, cleanup } from '@testing-library/svelte' +import { afterEach } from 'vitest' afterEach(async () => { await act() diff --git a/svelte.config.js b/svelte.config.js index 61eb947..b0683fd 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,7 +1,7 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { - // Consult https://svelte.dev/docs#compile-time-svelte-preprocess - // for more information about preprocessors - preprocess: vitePreprocess(), + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), } From 40feeb49fd8219a7f9484fd6bdbf2f60fc615cc8 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Mon, 25 Mar 2024 17:21:22 -0400 Subject: [PATCH 06/10] fix: ensure fireEvent is exported (#339) --- src/__tests__/events.test.js | 6 ++++-- src/index.js | 6 +++++- src/svelte5-index.js | 8 ++++++-- vite.config.js | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/__tests__/events.test.js b/src/__tests__/events.test.js index 9074db9..e0aba4d 100644 --- a/src/__tests__/events.test.js +++ b/src/__tests__/events.test.js @@ -8,8 +8,9 @@ describe('events', () => { const { getByText } = render(Comp, { props: { name: 'World' } }) const button = getByText('Button') - await fireEvent.click(button) + const result = fireEvent.click(button) + await expect(result).resolves.toBe(true) expect(button).toHaveTextContent('Button Clicked') }) @@ -17,7 +18,7 @@ describe('events', () => { const { getByText } = render(Comp, { props: { name: 'World' } }) const button = getByText('Button') - await fireEvent( + const result = fireEvent( button, new MouseEvent('click', { bubbles: true, @@ -25,6 +26,7 @@ describe('events', () => { }) ) + await expect(result).resolves.toBe(true) expect(button).toHaveTextContent('Button Clicked') }) }) diff --git a/src/index.js b/src/index.js index 3e58608..46fd662 100644 --- a/src/index.js +++ b/src/index.js @@ -13,5 +13,9 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) { }) } -export * from './pure.js' +// export all base queries, screen, etc. export * from '@testing-library/dom' + +// export svelte-specific functions and custom `fireEvent` +// `fireEvent` must be a named export to take priority over wildcard export above +export { act, cleanup, fireEvent, render } from './pure.js' diff --git a/src/svelte5-index.js b/src/svelte5-index.js index 8dc11e8..86ff742 100644 --- a/src/svelte5-index.js +++ b/src/svelte5-index.js @@ -13,6 +13,10 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) { }) } -export { act, fireEvent } from './pure.js' -export * from './svelte5.js' +// export all base queries, screen, etc. export * from '@testing-library/dom' + +// export svelte-specific functions and custom `fireEvent` +// `fireEvent` must be a named export to take priority over wildcard export above +export { act, fireEvent } from './pure.js' +export { cleanup, render } from './svelte5.js' diff --git a/vite.config.js b/vite.config.js index 1ef160e..d9085ef 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,6 @@ +import path from 'node:path' + import { svelte } from '@sveltejs/vite-plugin-svelte' -import path from 'path' import { VERSION as SVELTE_VERSION } from 'svelte/compiler' import { defineConfig } from 'vite' From d4e60f740f9a193b7043907dc9eb09f83520217b Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Tue, 2 Apr 2024 09:02:01 -0400 Subject: [PATCH 07/10] ci: run weekly check with latest svelte versions (#344) --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab433cc..be47107 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: branches: [main, next] pull_request: branches: [main, next] + schedule: + # Tuesdays at 14:45 UTC (10:45 EST) + - cron: 45 14 * * 1 concurrency: group: ${{ github.workflow }}-${{ github.ref }} From e951b9a7f1e233f2204bf2be96924d3047ff31dd Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Thu, 18 Apr 2024 11:11:09 -0400 Subject: [PATCH 08/10] test: remove Svelte 5 + HappyDOM skips after bugfix (#348) --- package.json | 2 +- src/__tests__/context.test.js | 3 +-- src/__tests__/mount.test.js | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6ee42a8..1948cfc 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "eslint-plugin-svelte": "2.35.1", "eslint-plugin-vitest-globals": "1.4.0", "expect-type": "^0.17.3", - "happy-dom": "^13.3.1", + "happy-dom": "^14.7.1", "jsdom": "^22.1.0", "npm-run-all": "^4.1.5", "prettier": "3.2.4", diff --git a/src/__tests__/context.test.js b/src/__tests__/context.test.js index 0fd672d..da54b9d 100644 --- a/src/__tests__/context.test.js +++ b/src/__tests__/context.test.js @@ -2,9 +2,8 @@ import { render } from '@testing-library/svelte' import { expect, test } from 'vitest' import Comp from './fixtures/Context.svelte' -import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js' -test.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('can set a context', () => { +test('can set a context', () => { const message = 'Got it' const { getByText } = render(Comp, { diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js index d650368..48d985f 100644 --- a/src/__tests__/mount.test.js +++ b/src/__tests__/mount.test.js @@ -2,13 +2,12 @@ import { act, render, screen } from '@testing-library/svelte' import { describe, expect, test, vi } from 'vitest' import Mounter from './fixtures/Mounter.svelte' -import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js' const onMounted = vi.fn() const onDestroyed = vi.fn() const renderSubject = () => render(Mounter, { onMounted, onDestroyed }) -describe.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('mount and destroy', () => { +describe('mount and destroy', () => { test('component is mounted', async () => { renderSubject() From 3eaa04ee5be2793f1b49057186a8716cddd322d9 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Thu, 18 Apr 2024 11:14:25 -0400 Subject: [PATCH 09/10] fix(svelte5): ensure proper `act` used in cleanup (#346) --- src/svelte5-index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/svelte5-index.js b/src/svelte5-index.js index 86ff742..ab49641 100644 --- a/src/svelte5-index.js +++ b/src/svelte5-index.js @@ -1,5 +1,6 @@ /* eslint-disable import/export */ -import { act, cleanup } from './svelte5.js' +import { act } from './pure.js' +import { cleanup } from './svelte5.js' // If we're running in a test runner that supports afterEach // then we'll automatically run cleanup afterEach test From 496c4559c09c2d31115b797ad6ba36b0b3a4aa41 Mon Sep 17 00:00:00 2001 From: Michael Cousins Date: Thu, 18 Apr 2024 11:29:10 -0400 Subject: [PATCH 10/10] chore(release): note breaking changes for v5 (#347) BREAKING CHANGE: `render` is now more strict about the `target` option, and the `container` result is now the direct parent of your component instance.