@testing-library/svelte 4.2.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,6 +80,21 @@ This library has `peerDependencies` listings for `svelte >= 3`.
80
80
  You may also be interested in installing `@testing-library/jest-dom` so you can use
81
81
  [the custom jest matchers](https://github.com/testing-library/jest-dom).
82
82
 
83
+ ### Svelte 5 support
84
+
85
+ If you are riding the bleeding edge of Svelte 5, you'll need to either
86
+ import from `@testing-library/svelte/svelte5` instead of `@testing-library/svelte`, or have your `vite.config.js` contains the following alias:
87
+
88
+ ```
89
+ export default defineConfig(({ }) => ({
90
+ test: {
91
+ alias: {
92
+ '@testing-library/svelte': '@testing-library/svelte/svelte5'
93
+ }
94
+ },
95
+ }))
96
+ ```
97
+
83
98
  ## Docs
84
99
 
85
100
  See the [**docs**](https://testing-library.com/docs/svelte-testing-library/intro) over at the Testing Library website.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testing-library/svelte",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "Simple and complete Svelte testing utilities that encourage good testing practices.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -8,6 +8,10 @@
8
8
  "types": "./types/index.d.ts",
9
9
  "default": "./src/index.js"
10
10
  },
11
+ "./svelte5": {
12
+ "types": "./types/index.d.ts",
13
+ "default": "./src/svelte5-index.js"
14
+ },
11
15
  "./vitest": {
12
16
  "default": "./src/vitest.js"
13
17
  }
@@ -94,7 +98,7 @@
94
98
  "npm-run-all": "^4.1.5",
95
99
  "prettier": "3.2.4",
96
100
  "prettier-plugin-svelte": "3.1.2",
97
- "svelte": "^4.2.10",
101
+ "svelte": "^3 || ^4 || ^5",
98
102
  "svelte-check": "^3.6.3",
99
103
  "svelte-jester": "^3.0.0",
100
104
  "typescript": "^5.3.3",
@@ -1,25 +1,13 @@
1
- import { beforeEach, describe, expect, test } from 'vitest'
1
+ import { setTimeout } from 'node:timers/promises'
2
+
3
+ import { act, render } from '@testing-library/svelte'
4
+ import { describe, expect, test } from 'vitest'
2
5
 
3
- import { act, fireEvent, render as stlRender } from '..'
4
6
  import Comp from './fixtures/Comp.svelte'
5
7
 
6
8
  describe('act', () => {
7
- let props
8
-
9
- const render = () => {
10
- return stlRender(Comp, {
11
- props
12
- })
13
- }
14
-
15
- beforeEach(() => {
16
- props = {
17
- name: 'World'
18
- }
19
- })
20
-
21
9
  test('state updates are flushed', async () => {
22
- const { getByText } = render()
10
+ const { getByText } = render(Comp)
23
11
  const button = getByText('Button')
24
12
 
25
13
  expect(button).toHaveTextContent('Button')
@@ -31,24 +19,13 @@ describe('act', () => {
31
19
  expect(button).toHaveTextContent('Button Clicked')
32
20
  })
33
21
 
34
- test('findByTestId returns the element', async () => {
35
- const { findByTestId } = render()
36
-
37
- expect(await findByTestId('test')).toHaveTextContent(`Hello ${props.name}!`)
38
- })
39
-
40
22
  test('accepts async functions', async () => {
41
- const sleep = (ms) =>
42
- new Promise((resolve) => {
43
- setTimeout(() => resolve(), ms)
44
- })
45
-
46
- const { getByText } = render()
23
+ const { getByText } = render(Comp)
47
24
  const button = getByText('Button')
48
25
 
49
26
  await act(async () => {
50
- await sleep(100)
51
- await fireEvent.click(button)
27
+ await setTimeout(100)
28
+ button.click()
52
29
  })
53
30
 
54
31
  expect(button).toHaveTextContent('Button Clicked')
@@ -7,7 +7,7 @@ describe('auto-cleanup-skip', () => {
7
7
 
8
8
  beforeAll(async () => {
9
9
  process.env.STL_SKIP_AUTO_CLEANUP = 'true'
10
- const stl = await import('..')
10
+ const stl = await import('@testing-library/svelte')
11
11
  render = stl.render
12
12
  })
13
13
 
@@ -1,6 +1,6 @@
1
+ import { render } from '@testing-library/svelte'
1
2
  import { describe, expect, test } from 'vitest'
2
3
 
3
- import { render } from '..'
4
4
  import Comp from './fixtures/Comp.svelte'
5
5
 
6
6
  describe('auto-cleanup', () => {
@@ -1,6 +1,6 @@
1
+ import { cleanup, render } from '@testing-library/svelte'
1
2
  import { describe, expect, test, vi } from 'vitest'
2
3
 
3
- import { act, cleanup, render } from '..'
4
4
  import Mounter from './fixtures/Mounter.svelte'
5
5
 
6
6
  const onExecuted = vi.fn()
@@ -15,8 +15,8 @@ describe('cleanup', () => {
15
15
  expect(document.body).toBeEmptyDOMElement()
16
16
  })
17
17
 
18
- test('cleanup unmounts component', async () => {
19
- await act(renderSubject)
18
+ test('cleanup unmounts component', () => {
19
+ renderSubject()
20
20
  cleanup()
21
21
 
22
22
  expect(onDestroyed).toHaveBeenCalledOnce()
@@ -1,9 +1,10 @@
1
+ import { render } from '@testing-library/svelte'
1
2
  import { expect, test } from 'vitest'
2
3
 
3
- import { render } from '..'
4
4
  import Comp from './fixtures/Context.svelte'
5
+ import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js'
5
6
 
6
- test('can set a context', () => {
7
+ test.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('can set a context', () => {
7
8
  const message = 'Got it'
8
9
 
9
10
  const { getByText } = render(Comp, {
@@ -1,24 +1,18 @@
1
1
  import { prettyDOM } from '@testing-library/dom'
2
- import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
2
+ import { render } from '@testing-library/svelte'
3
+ import { describe, expect, test, vi } from 'vitest'
3
4
 
4
- import { render } from '..'
5
5
  import Comp from './fixtures/Comp.svelte'
6
6
 
7
7
  describe('debug', () => {
8
- beforeEach(() => {
9
- vi.spyOn(console, 'log').mockImplementation(() => { })
10
- })
11
-
12
- afterEach(() => {
13
- console.log.mockRestore()
14
- })
8
+ test('pretty prints the base element', () => {
9
+ vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() })
15
10
 
16
- test('pretty prints the container', () => {
17
- const { container, debug } = render(Comp, { props: { name: 'world' } })
11
+ const { baseElement, debug } = render(Comp, { props: { name: 'world' } })
18
12
 
19
13
  debug()
20
14
 
21
15
  expect(console.log).toHaveBeenCalledTimes(1)
22
- expect(console.log).toHaveBeenCalledWith(prettyDOM(container))
16
+ expect(console.log).toHaveBeenCalledWith(prettyDOM(baseElement))
23
17
  })
24
18
  })
@@ -1,6 +1,6 @@
1
+ import { fireEvent, render } from '@testing-library/svelte'
1
2
  import { describe, expect, test } from 'vitest'
2
3
 
3
- import { fireEvent, render } from '..'
4
4
  import Comp from './fixtures/Comp.svelte'
5
5
 
6
6
  describe('events', () => {
@@ -8,8 +8,9 @@ describe('events', () => {
8
8
  const { getByText } = render(Comp, { props: { name: 'World' } })
9
9
  const button = getByText('Button')
10
10
 
11
- await fireEvent.click(button)
11
+ const result = fireEvent.click(button)
12
12
 
13
+ await expect(result).resolves.toBe(true)
13
14
  expect(button).toHaveTextContent('Button Clicked')
14
15
  })
15
16
 
@@ -17,14 +18,15 @@ describe('events', () => {
17
18
  const { getByText } = render(Comp, { props: { name: 'World' } })
18
19
  const button = getByText('Button')
19
20
 
20
- await fireEvent(
21
+ const result = fireEvent(
21
22
  button,
22
23
  new MouseEvent('click', {
23
24
  bubbles: true,
24
- cancelable: true
25
+ cancelable: true,
25
26
  })
26
27
  )
27
28
 
29
+ await expect(result).resolves.toBe(true)
28
30
  expect(button).toHaveTextContent('Button Clicked')
29
31
  })
30
32
  })
@@ -1,23 +1,17 @@
1
1
  <svelte:options accessors />
2
2
 
3
3
  <script>
4
- import { getContext } from 'svelte'
5
-
6
- export let name
4
+ export let name = 'World'
7
5
 
8
6
  let buttonText = 'Button'
9
7
 
10
- const contextName = getContext('name')
11
-
12
- function handleClick () {
8
+ function handleClick() {
13
9
  buttonText = 'Button Clicked'
14
10
  }
15
11
  </script>
16
12
 
17
13
  <h1 data-testid="test">Hello {name}!</h1>
18
14
 
19
- <div>we have {contextName}</div>
20
-
21
15
  <button on:click={handleClick}>{buttonText}</button>
22
16
 
23
17
  <style></style>
@@ -1,7 +1,7 @@
1
1
  <script>
2
- import { getContext } from 'svelte';
2
+ import { getContext } from 'svelte'
3
3
 
4
- const ctx = getContext('foo');
4
+ const ctx = getContext('foo')
5
5
  </script>
6
6
 
7
7
  <div>{ctx.message}</div>
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
2
  export let name: string
3
+ export let count: number
3
4
  </script>
4
5
 
5
6
  <h1>hello {name}</h1>
7
+ <p>count: {count}</p>
@@ -1,13 +1,14 @@
1
+ import { act, render, screen } from '@testing-library/svelte'
1
2
  import { describe, expect, test, vi } from 'vitest'
2
3
 
3
- import { act, render, screen } from '..'
4
4
  import Mounter from './fixtures/Mounter.svelte'
5
+ import { IS_HAPPYDOM, IS_SVELTE_5 } from './utils.js'
5
6
 
6
7
  const onMounted = vi.fn()
7
8
  const onDestroyed = vi.fn()
8
9
  const renderSubject = () => render(Mounter, { onMounted, onDestroyed })
9
10
 
10
- describe('mount and destroy', () => {
11
+ describe.skipIf(IS_SVELTE_5 && IS_HAPPYDOM)('mount and destroy', () => {
11
12
  test('component is mounted', async () => {
12
13
  renderSubject()
13
14
 
@@ -1,6 +1,6 @@
1
+ import { render } from '@testing-library/svelte'
1
2
  import { describe, expect, test } from 'vitest'
2
3
 
3
- import { render } from '..'
4
4
  import Comp from './fixtures/Comp.svelte'
5
5
 
6
6
  describe('multi-base', () => {
@@ -13,11 +13,11 @@ describe('multi-base', () => {
13
13
  {
14
14
  target: treeA,
15
15
  props: {
16
- name: 'Tree A'
17
- }
16
+ name: 'Tree A',
17
+ },
18
18
  },
19
19
  {
20
- container: treeA
20
+ baseElement: treeA,
21
21
  }
22
22
  )
23
23
 
@@ -26,11 +26,11 @@ describe('multi-base', () => {
26
26
  {
27
27
  target: treeB,
28
28
  props: {
29
- name: 'Tree B'
30
- }
29
+ name: 'Tree B',
30
+ },
31
31
  },
32
32
  {
33
- container: treeB
33
+ baseElement: treeB,
34
34
  }
35
35
  )
36
36
 
@@ -1,123 +1,85 @@
1
- import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
2
- import { beforeEach, describe, expect, test } from 'vitest'
1
+ import { render } from '@testing-library/svelte'
2
+ import { describe, expect, test } from 'vitest'
3
3
 
4
- import { act, render as stlRender } from '..'
5
4
  import Comp from './fixtures/Comp.svelte'
6
- import CompDefault from './fixtures/Comp2.svelte'
5
+ import { IS_SVELTE_5 } from './utils.js'
7
6
 
8
7
  describe('render', () => {
9
- let props
10
-
11
- const render = (additional = {}) => {
12
- return stlRender(Comp, {
13
- target: document.body,
14
- props,
15
- ...additional,
16
- })
17
- }
18
-
19
- beforeEach(() => {
20
- props = {
21
- name: 'World',
22
- }
23
- })
8
+ const props = { name: 'World' }
24
9
 
25
10
  test('renders component into the document', () => {
26
- const { getByText } = render()
11
+ const { getByText } = render(Comp, { props })
27
12
 
28
13
  expect(getByText('Hello World!')).toBeInTheDocument()
29
14
  })
30
15
 
31
- // Dear reader, this is not something you generally want to do in your tests.
32
- test('programmatically change props', async () => {
33
- const { component, getByText } = render()
34
-
16
+ test('accepts props directly', () => {
17
+ const { getByText } = render(Comp, props)
35
18
  expect(getByText('Hello World!')).toBeInTheDocument()
36
-
37
- await act(() => {
38
- component.$set({ name: 'Worlds' })
39
- })
40
-
41
- expect(getByText('Hello Worlds!')).toBeInTheDocument()
42
19
  })
43
20
 
44
- test('change props with accessors', async () => {
45
- const { component, getByText } = render(
46
- SVELTE_VERSION < '5' ? { accessors: true } : {}
47
- )
48
-
49
- expect(getByText('Hello World!')).toBeInTheDocument()
21
+ test('throws error when mixing svelte component options and props', () => {
22
+ expect(() => {
23
+ render(Comp, { props, name: 'World' })
24
+ }).toThrow(/Unknown options/)
25
+ })
50
26
 
51
- expect(component.name).toBe('World')
27
+ test('throws error when mixing target option and props', () => {
28
+ expect(() => {
29
+ render(Comp, { target: document.createElement('div'), name: 'World' })
30
+ }).toThrow(/Unknown options/)
31
+ })
52
32
 
53
- await act(() => {
54
- component.value = 'Planet'
55
- })
33
+ test('should return a container object wrapping the DOM of the rendered component', () => {
34
+ const { container, getByTestId } = render(Comp, props)
35
+ const firstElement = getByTestId('test')
56
36
 
57
- expect(getByText('Hello World!')).toBeInTheDocument()
37
+ expect(container.firstChild).toBe(firstElement)
58
38
  })
59
39
 
60
- test('should accept props directly', () => {
61
- const { getByText } = stlRender(Comp, { name: 'World' })
62
- expect(getByText('Hello World!')).toBeInTheDocument()
63
- })
40
+ test('should return a baseElement object, which holds the container', () => {
41
+ const { baseElement, container } = render(Comp, props)
64
42
 
65
- test.runIf(SVELTE_VERSION < '5')(
66
- 'should accept svelte v4 component options',
67
- () => {
68
- const target = document.createElement('div')
69
- const div = document.createElement('div')
70
- document.body.appendChild(target)
71
- target.appendChild(div)
72
- const { container } = stlRender(Comp, {
73
- target,
74
- anchor: div,
75
- props: { name: 'World' },
76
- context: new Map([['name', 'context']]),
77
- })
78
- expect(container).toMatchSnapshot()
79
- }
80
- )
81
-
82
- test.runIf(SVELTE_VERSION >= '5')(
83
- 'should accept svelte v5 component options',
84
- () => {
85
- const target = document.createElement('section')
86
- document.body.appendChild(target)
87
-
88
- const { container } = stlRender(Comp, {
89
- target,
90
- props: { name: 'World' },
91
- context: new Map([['name', 'context']]),
92
- })
93
- expect(container).toMatchSnapshot()
94
- }
95
- )
96
-
97
- test('should throw error when mixing svelte component options and props', () => {
98
- expect(() => {
99
- stlRender(Comp, { props: {}, name: 'World' })
100
- }).toThrow(/Unknown options were found/)
43
+ expect(baseElement).toBe(document.body)
44
+ expect(baseElement.firstChild).toBe(container)
101
45
  })
102
46
 
103
- test('should return a container object, which contains the DOM of the rendered component', () => {
104
- const { container } = render()
47
+ test('if target is provided, use it as container and baseElement', () => {
48
+ const target = document.createElement('div')
49
+ const { baseElement, container } = render(Comp, { props, target })
105
50
 
106
- expect(container.innerHTML).toBe(document.body.innerHTML)
51
+ expect(container).toBe(target)
52
+ expect(baseElement).toBe(target)
107
53
  })
108
54
 
109
- test('correctly find component constructor on the default property', () => {
110
- const { getByText } = render(CompDefault, { props: { name: 'World' } })
55
+ test('allow baseElement to be specified', () => {
56
+ const customBaseElement = document.createElement('div')
111
57
 
112
- expect(getByText('Hello World!')).toBeInTheDocument()
58
+ const { baseElement, container } = render(
59
+ Comp,
60
+ { props },
61
+ { baseElement: customBaseElement }
62
+ )
63
+
64
+ expect(baseElement).toBe(customBaseElement)
65
+ expect(baseElement.firstChild).toBe(container)
113
66
  })
114
67
 
115
- test("accept the 'context' option", () => {
116
- const { getByText } = stlRender(Comp, {
117
- props: { name: 'Universe' },
118
- context: new Map([['name', 'context']]),
119
- })
68
+ test.skipIf(IS_SVELTE_5)('should accept anchor option in Svelte v4', () => {
69
+ const baseElement = document.body
70
+ const target = document.createElement('section')
71
+ const anchor = document.createElement('div')
72
+ baseElement.appendChild(target)
73
+ target.appendChild(anchor)
74
+
75
+ const { getByTestId } = render(
76
+ Comp,
77
+ { props, target, anchor },
78
+ { baseElement }
79
+ )
80
+ const firstElement = getByTestId('test')
120
81
 
121
- expect(getByText('we have context')).toBeInTheDocument()
82
+ expect(target.firstChild).toBe(firstElement)
83
+ expect(target.lastChild).toBe(anchor)
122
84
  })
123
85
  })
@@ -1,41 +1,50 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
1
+ import { act, render, screen } from '@testing-library/svelte'
2
+ import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
4
3
  import { describe, expect, test, vi } from 'vitest'
5
- import { writable } from 'svelte/store'
6
4
 
7
- import { act, render, waitFor } from '..'
8
- import Comp from './fixtures/Rerender.svelte'
5
+ import Comp from './fixtures/Comp.svelte'
9
6
 
10
- test('mounts new component successfully', async () => {
11
- const onMounted = vi.fn()
12
- const onDestroyed = vi.fn()
7
+ describe('rerender', () => {
8
+ test('updates props', async () => {
9
+ const { rerender } = render(Comp, { name: 'World' })
10
+ const element = screen.getByText('Hello World!')
13
11
 
14
- const { getByTestId, rerender } = render(Comp, {
15
- props: { name: 'World 1', onMounted, onDestroyed },
12
+ await rerender({ name: 'Dolly' })
13
+
14
+ expect(element).toHaveTextContent('Hello Dolly!')
16
15
  })
17
16
 
18
- const expectToRender = (content) =>
19
- waitFor(() => {
20
- expect(getByTestId('test')).toHaveTextContent(content)
21
- expect(onMounted).toHaveBeenCalledOnce()
22
- })
17
+ test('warns if incorrect arguments shape used', async () => {
18
+ vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() })
19
+
20
+ const { rerender } = render(Comp, { name: 'World' })
21
+ const element = screen.getByText('Hello World!')
23
22
 
24
- await expectToRender('Hello World 1!')
23
+ await rerender({ props: { name: 'Dolly' } })
25
24
 
26
- console.warn = vi.fn()
25
+ expect(element).toHaveTextContent('Hello Dolly!')
26
+ expect(console.warn).toHaveBeenCalledOnce()
27
+ expect(console.warn).toHaveBeenCalledWith(
28
+ expect.stringMatching(/deprecated/iu)
29
+ )
30
+ })
27
31
 
28
- rerender({ props: { name: 'World 2' } })
29
- await expectToRender('Hello World 2!')
30
- expect(onDestroyed).not.toHaveBeenCalled()
32
+ test('change props with accessors', async () => {
33
+ const { component, getByText } = render(
34
+ Comp,
35
+ SVELTE_VERSION < '5'
36
+ ? { accessors: true, props: { name: 'World' } }
37
+ : { name: 'World' }
38
+ )
39
+ const element = getByText('Hello World!')
31
40
 
32
- expect(console.warn).toHaveBeenCalledOnce()
41
+ expect(element).toBeInTheDocument()
42
+ expect(component.name).toBe('World')
33
43
 
34
- console.warn.mockClear()
35
- onDestroyed.mockReset()
36
- rerender({ name: 'World 3' })
37
- await expectToRender('Hello World 3!')
38
- expect(onDestroyed).not.toHaveBeenCalled()
44
+ await act(() => {
45
+ component.name = 'Planet'
46
+ })
39
47
 
40
- expect(console.warn).not.toHaveBeenCalled()
48
+ expect(element).toHaveTextContent('Hello Planet!')
49
+ })
41
50
  })
@@ -1,16 +1,16 @@
1
+ import { render, screen, waitFor } from '@testing-library/svelte'
1
2
  import { userEvent } from '@testing-library/user-event'
2
- import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
3
3
  import { beforeEach, describe, expect, test, vi } from 'vitest'
4
4
 
5
- import { render, screen, waitFor } from '..'
6
5
  import Transitioner from './fixtures/Transitioner.svelte'
6
+ import { IS_JSDOM, IS_SVELTE_5 } from './utils.js'
7
7
 
8
- describe.runIf(SVELTE_VERSION < '5')('transitions', () => {
8
+ describe.skipIf(IS_SVELTE_5)('transitions', () => {
9
9
  beforeEach(() => {
10
- if (window.navigator.userAgent.includes('jsdom')) {
11
- const raf = (fn) => setTimeout(() => fn(new Date()), 16)
12
- vi.stubGlobal('requestAnimationFrame', raf)
13
- }
10
+ if (!IS_JSDOM) return
11
+
12
+ const raf = (fn) => setTimeout(() => fn(new Date()), 16)
13
+ vi.stubGlobal('requestAnimationFrame', raf)
14
14
  })
15
15
 
16
16
  test('on:introend', async () => {
@@ -0,0 +1,7 @@
1
+ import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
2
+
3
+ export const IS_JSDOM = window.navigator.userAgent.includes('jsdom')
4
+
5
+ export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js
6
+
7
+ export const IS_SVELTE_5 = SVELTE_VERSION >= '5'
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable import/export */
1
2
  import { act, cleanup } from './pure.js'
2
3
 
3
4
  // If we're running in a test runner that supports afterEach
@@ -12,4 +13,9 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
12
13
  })
13
14
  }
14
15
 
15
- export * from './pure.js'
16
+ // export all base queries, screen, etc.
17
+ export * from '@testing-library/dom'
18
+
19
+ // export svelte-specific functions and custom `fireEvent`
20
+ // `fireEvent` must be a named export to take priority over wildcard export above
21
+ export { act, cleanup, fireEvent, render } from './pure.js'
package/src/pure.js CHANGED
@@ -4,42 +4,40 @@ import {
4
4
  prettyDOM,
5
5
  } from '@testing-library/dom'
6
6
  import * as Svelte from 'svelte'
7
+ import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
7
8
 
8
- const IS_SVELTE_5 = typeof Svelte.createRoot === 'function'
9
- const targetCache = new Set()
10
- const componentCache = new Set()
9
+ const IS_SVELTE_5 = /^5\./.test(SVELTE_VERSION)
11
10
 
12
- const svelteComponentOptions = IS_SVELTE_5
13
- ? ['target', 'props', 'events', 'context', 'intro', 'recover']
14
- : ['accessors', 'anchor', 'props', 'hydrate', 'intro', 'context']
11
+ export class SvelteTestingLibrary {
12
+ svelteComponentOptions = [
13
+ 'target',
14
+ 'accessors',
15
+ 'anchor',
16
+ 'props',
17
+ 'hydrate',
18
+ 'intro',
19
+ 'context',
20
+ ]
15
21
 
16
- const render = (
17
- Component,
18
- { target, ...options } = {},
19
- { container, queries } = {}
20
- ) => {
21
- container = container || document.body
22
- target = target || container.appendChild(document.createElement('div'))
23
- targetCache.add(target)
22
+ targetCache = new Set()
23
+ componentCache = new Set()
24
24
 
25
- const ComponentConstructor = Component.default || Component
26
-
27
- const checkProps = (options) => {
25
+ checkProps(options) {
28
26
  const isProps = !Object.keys(options).some((option) =>
29
- svelteComponentOptions.includes(option)
27
+ this.svelteComponentOptions.includes(option)
30
28
  )
31
29
 
32
30
  // Check if any props and Svelte options were accidentally mixed.
33
31
  if (!isProps) {
34
32
  const unrecognizedOptions = Object.keys(options).filter(
35
- (option) => !svelteComponentOptions.includes(option)
33
+ (option) => !this.svelteComponentOptions.includes(option)
36
34
  )
37
35
 
38
36
  if (unrecognizedOptions.length > 0) {
39
37
  throw Error(`
40
38
  Unknown options were found [${unrecognizedOptions}]. This might happen if you've mixed
41
39
  passing in props with Svelte options into the render function. Valid Svelte options
42
- are [${svelteComponentOptions}]. You can either change the prop names, or pass in your
40
+ are [${this.svelteComponentOptions}]. You can either change the prop names, or pass in your
43
41
  props for that component via the \`props\` option.\n\n
44
42
  Eg: const { /** Results **/ } = render(MyComponent, { props: { /** props here **/ } })\n\n
45
43
  `)
@@ -51,78 +49,100 @@ const render = (
51
49
  return { props: options }
52
50
  }
53
51
 
54
- const renderComponent = (options) => {
55
- options = { target, ...checkProps(options) }
52
+ render(Component, componentOptions = {}, renderOptions = {}) {
53
+ componentOptions = this.checkProps(componentOptions)
54
+
55
+ const baseElement =
56
+ renderOptions.baseElement ?? componentOptions.target ?? document.body
57
+
58
+ const target =
59
+ componentOptions.target ??
60
+ baseElement.appendChild(document.createElement('div'))
61
+
62
+ this.targetCache.add(target)
63
+
64
+ const ComponentConstructor = Component.default || Component
65
+
66
+ const component = this.renderComponent(ComponentConstructor, {
67
+ ...componentOptions,
68
+ target,
69
+ })
70
+
71
+ return {
72
+ baseElement,
73
+ component,
74
+ container: target,
75
+ debug: (el = baseElement) => console.log(prettyDOM(el)),
76
+ rerender: async (props) => {
77
+ if (props.props) {
78
+ console.warn(
79
+ 'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
80
+ )
81
+ props = props.props
82
+ }
83
+ component.$set(props)
84
+ await Svelte.tick()
85
+ },
86
+ unmount: () => {
87
+ this.cleanupComponent(component)
88
+ },
89
+ ...getQueriesForElement(baseElement, renderOptions.queries),
90
+ }
91
+ }
92
+
93
+ renderComponent(ComponentConstructor, componentOptions) {
94
+ if (IS_SVELTE_5) {
95
+ throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`')
96
+ }
56
97
 
57
- const component = IS_SVELTE_5
58
- ? Svelte.createRoot(ComponentConstructor, options)
59
- : new ComponentConstructor(options)
98
+ const component = new ComponentConstructor(componentOptions)
60
99
 
61
- componentCache.add(component)
100
+ this.componentCache.add(component)
62
101
 
63
102
  // TODO(mcous, 2024-02-11): remove this behavior in the next major version
64
- // It is unnecessary has no path to implementation in Svelte v5
65
- if (!IS_SVELTE_5) {
66
- component.$$.on_destroy.push(() => {
67
- componentCache.delete(component)
68
- })
69
- }
103
+ component.$$.on_destroy.push(() => {
104
+ this.componentCache.delete(component)
105
+ })
70
106
 
71
107
  return component
72
108
  }
73
109
 
74
- let component = renderComponent(options)
75
-
76
- return {
77
- container,
78
- component,
79
- debug: (el = container) => console.log(prettyDOM(el)),
80
- rerender: async (props) => {
81
- if (props.props) {
82
- console.warn(
83
- 'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
84
- )
85
- props = props.props
86
- }
87
- component.$set(props)
88
- await Svelte.tick()
89
- },
90
- unmount: () => {
91
- cleanupComponent(component)
92
- },
93
- ...getQueriesForElement(container, queries),
110
+ cleanupComponent(component) {
111
+ const inCache = this.componentCache.delete(component)
112
+
113
+ if (inCache) {
114
+ component.$destroy()
115
+ }
94
116
  }
95
- }
96
117
 
97
- const cleanupComponent = (component) => {
98
- const inCache = componentCache.delete(component)
118
+ cleanupTarget(target) {
119
+ const inCache = this.targetCache.delete(target)
99
120
 
100
- if (inCache) {
101
- component.$destroy()
121
+ if (inCache && target.parentNode === document.body) {
122
+ document.body.removeChild(target)
123
+ }
102
124
  }
103
- }
104
-
105
- const cleanupTarget = (target) => {
106
- const inCache = targetCache.delete(target)
107
125
 
108
- if (inCache && target.parentNode === document.body) {
109
- document.body.removeChild(target)
126
+ cleanup() {
127
+ this.componentCache.forEach(this.cleanupComponent.bind(this))
128
+ this.targetCache.forEach(this.cleanupTarget.bind(this))
110
129
  }
111
130
  }
112
131
 
113
- const cleanup = () => {
114
- componentCache.forEach(cleanupComponent)
115
- targetCache.forEach(cleanupTarget)
116
- }
132
+ const instance = new SvelteTestingLibrary()
133
+
134
+ export const render = instance.render.bind(instance)
117
135
 
118
- const act = async (fn) => {
136
+ export const cleanup = instance.cleanup.bind(instance)
137
+
138
+ export const act = async (fn) => {
119
139
  if (fn) {
120
140
  await fn()
121
141
  }
122
142
  return Svelte.tick()
123
143
  }
124
144
 
125
- const fireEvent = async (...args) => {
145
+ export const fireEvent = async (...args) => {
126
146
  const event = dtlFireEvent(...args)
127
147
  await Svelte.tick()
128
148
  return event
@@ -135,9 +155,3 @@ Object.keys(dtlFireEvent).forEach((key) => {
135
155
  return event
136
156
  }
137
157
  })
138
-
139
- /* eslint-disable import/export */
140
-
141
- export * from '@testing-library/dom'
142
-
143
- export { render, cleanup, fireEvent, act }
@@ -0,0 +1,22 @@
1
+ /* eslint-disable import/export */
2
+ import { act, cleanup } from './svelte5.js'
3
+
4
+ // If we're running in a test runner that supports afterEach
5
+ // then we'll automatically run cleanup afterEach test
6
+ // this ensures that tests run in isolation from each other
7
+ // if you don't like this then either import the `pure` module
8
+ // or set the STL_SKIP_AUTO_CLEANUP env variable to 'true'.
9
+ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
10
+ afterEach(async () => {
11
+ await act()
12
+ cleanup()
13
+ })
14
+ }
15
+
16
+ // export all base queries, screen, etc.
17
+ export * from '@testing-library/dom'
18
+
19
+ // export svelte-specific functions and custom `fireEvent`
20
+ // `fireEvent` must be a named export to take priority over wildcard export above
21
+ export { act, fireEvent } from './pure.js'
22
+ export { cleanup, render } from './svelte5.js'
package/src/svelte5.js ADDED
@@ -0,0 +1,30 @@
1
+ import { createClassComponent } from 'svelte/legacy'
2
+
3
+ import { SvelteTestingLibrary } from './pure.js'
4
+
5
+ class Svelte5TestingLibrary extends SvelteTestingLibrary {
6
+ svelteComponentOptions = [
7
+ 'target',
8
+ 'props',
9
+ 'events',
10
+ 'context',
11
+ 'intro',
12
+ 'recover',
13
+ ]
14
+
15
+ renderComponent(ComponentConstructor, componentOptions) {
16
+ const component = createClassComponent({
17
+ ...componentOptions,
18
+ component: ComponentConstructor,
19
+ })
20
+
21
+ this.componentCache.add(component)
22
+
23
+ return component
24
+ }
25
+ }
26
+
27
+ const instance = new Svelte5TestingLibrary()
28
+
29
+ export const render = instance.render.bind(instance)
30
+ export const cleanup = instance.cleanup.bind(instance)
package/src/vitest.js CHANGED
@@ -1,7 +1,6 @@
1
+ import { act, cleanup } from '@testing-library/svelte'
1
2
  import { afterEach } from 'vitest'
2
3
 
3
- import { act, cleanup } from './pure.js'
4
-
5
4
  afterEach(async () => {
6
5
  await act()
7
6
  cleanup()
package/types/index.d.ts CHANGED
@@ -18,12 +18,7 @@ export * from '@testing-library/dom'
18
18
 
19
19
  type SvelteComponentOptions<C extends SvelteComponent> =
20
20
  | ComponentProps<C>
21
- | Pick<
22
- ComponentConstructorOptions<ComponentProps<C>>,
23
- 'anchor' | 'props' | 'hydrate' | 'intro' | 'context'
24
- >
25
-
26
- type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
21
+ | Partial<ComponentConstructorOptions<ComponentProps<C>>>
27
22
 
28
23
  type Constructor<T> = new (...args: any[]) => T
29
24
 
@@ -35,24 +30,22 @@ export type RenderResult<
35
30
  Q extends Queries = typeof queries,
36
31
  > = {
37
32
  container: HTMLElement
33
+ baseElement: HTMLElement
38
34
  component: C
39
35
  debug: (el?: HTMLElement | DocumentFragment) => void
40
- rerender: (props: ComponentProps<C>) => Promise<void>
36
+ rerender: (props: Partial<ComponentProps<C>>) => Promise<void>
41
37
  unmount: () => void
42
38
  } & { [P in keyof Q]: BoundFunction<Q[P]> }
43
39
 
44
40
  export interface RenderOptions<Q extends Queries = typeof queries> {
45
- container?: HTMLElement
41
+ baseElement?: HTMLElement
46
42
  queries?: Q
47
43
  }
48
44
 
49
- export function render<C extends SvelteComponent>(
50
- component: Constructor<C>,
51
- componentOptions?: SvelteComponentOptions<C>,
52
- renderOptions?: Omit<RenderOptions, 'queries'>
53
- ): RenderResult<C>
54
-
55
- export function render<C extends SvelteComponent, Q extends Queries>(
45
+ export function render<
46
+ C extends SvelteComponent,
47
+ Q extends Queries = typeof queries,
48
+ >(
56
49
  component: Constructor<C>,
57
50
  componentOptions?: SvelteComponentOptions<C>,
58
51
  renderOptions?: RenderOptions<Q>
@@ -1,48 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`render > should accept svelte v4 component options 1`] = `
4
- <body>
5
- <div>
6
- <h1
7
- data-testid="test"
8
- >
9
- Hello
10
- World
11
- !
12
- </h1>
13
-
14
- <div>
15
- we have context
16
- </div>
17
-
18
- <button>
19
- Button
20
- </button>
21
- <div />
22
- </div>
23
- </body>
24
- `;
25
-
26
- exports[`render > should accept svelte v5 component options 1`] = `
27
- <body>
28
-
29
-
30
-
31
- <section>
32
- <h1
33
- data-testid="test"
34
- >
35
- Hello World!
36
- </h1>
37
-
38
- <div>
39
- we have context
40
- </div>
41
-
42
- <button>
43
- Button
44
- </button>
45
-
46
- </section>
47
- </body>
48
- `;
@@ -1,15 +0,0 @@
1
- <script>
2
- export let name
3
-
4
- let buttonText = 'Button Text'
5
-
6
- function handleClick () {
7
- buttonText = 'Button Clicked'
8
- }
9
- </script>
10
-
11
- <style></style>
12
-
13
- <h1>Hello {name}!</h1>
14
-
15
- <button on:click={handleClick}>{buttonText}</button>
@@ -1,17 +0,0 @@
1
- <script>
2
- import { onDestroy, onMount } from 'svelte'
3
-
4
- export let onExecuted = undefined
5
- export let onMounted = undefined
6
- export let onDestroyed = undefined
7
-
8
- export let name = ''
9
-
10
- onExecuted?.()
11
-
12
- onMount(() => onMounted?.())
13
-
14
- onDestroy(() => onDestroyed?.())
15
- </script>
16
-
17
- <div data-testid="test">Hello {name}!</div>