Skip to content

Commit 0695343

Browse files
authored
fix!: Rerender to only update props of component without destroying it (#210)
1 parent 54ea8fa commit 0695343

File tree

4 files changed

+87
-55
lines changed

4 files changed

+87
-55
lines changed
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
import { getContext, onMount } from 'svelte'
3+
4+
export let name
5+
6+
const mountCounter = getContext('mountCounter')
7+
8+
onMount(() => {
9+
mountCounter.update((i) => i + 1)
10+
})
11+
</script>
12+
13+
<h1 data-testid="test">Hello {name}!</h1>
14+
15+
<div data-testid="mount-counter">{$mountCounter}</div>

src/__tests__/rerender.test.js

+25-25
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
11
/**
22
* @jest-environment jsdom
33
*/
4-
import { describe, expect, test } from 'vitest'
4+
import { expect, test, vi } from 'vitest'
5+
import { writable } from 'svelte/store'
56

6-
import { render } from '..'
7-
import Comp from './fixtures/Comp.svelte'
7+
import { render, waitFor } from '..'
8+
import Comp from './fixtures/Rerender.svelte'
89

9-
describe('rerender', () => {
10-
test('mounts new component successfully', () => {
11-
const { container, rerender } = render(Comp, { props: { name: 'World 1' } })
10+
const mountCounter = writable(0)
1211

13-
expect(container.firstChild).toHaveTextContent('Hello World 1!')
14-
rerender({ props: { name: 'World 2' } })
15-
expect(container.firstChild).toHaveTextContent('Hello World 2!')
12+
test('mounts new component successfully', async () => {
13+
const { getByTestId, rerender } = render(Comp, {
14+
props: { name: 'World 1' },
15+
context: new Map(Object.entries({ mountCounter })),
1616
})
1717

18-
test('destroys old component', () => {
19-
let isDestroyed
18+
const expectToRender = (content) =>
19+
waitFor(() => {
20+
expect(getByTestId('test')).toHaveTextContent(content)
21+
expect(getByTestId('mount-counter')).toHaveTextContent('1')
22+
})
2023

21-
const { rerender, component } = render(Comp, { props: { name: '' } })
24+
await expectToRender('Hello World 1!')
2225

23-
component.$$.on_destroy.push(() => {
24-
isDestroyed = true
25-
})
26-
rerender({ props: { name: '' } })
27-
expect(isDestroyed).toBeTruthy()
28-
})
26+
console.warn = vi.fn()
2927

30-
test('destroys old components on multiple rerenders', () => {
31-
const { rerender, queryByText } = render(Comp, { props: { name: 'Neil' } })
28+
rerender({ props: { name: 'World 2' } })
29+
await expectToRender('Hello World 2!')
3230

33-
rerender({ props: { name: 'Alex' } })
34-
expect(queryByText('Hello Neil!')).not.toBeInTheDocument()
35-
rerender({ props: { name: 'Geddy' } })
36-
expect(queryByText('Hello Alex!')).not.toBeInTheDocument()
37-
})
31+
expect(console.warn).toHaveBeenCalled()
32+
33+
console.warn.mockClear()
34+
rerender({ name: 'World 3' })
35+
await expectToRender('Hello World 3!')
36+
37+
expect(console.warn).not.toHaveBeenCalled()
3838
})

src/pure.js

+13-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
fireEvent as dtlFireEvent,
33
getQueriesForElement,
4-
prettyDOM
4+
prettyDOM,
55
} from '@testing-library/dom'
66
import { tick } from 'svelte'
77

@@ -14,7 +14,7 @@ const svelteComponentOptions = [
1414
'props',
1515
'hydrate',
1616
'intro',
17-
'context'
17+
'context',
1818
]
1919

2020
const render = (
@@ -56,7 +56,7 @@ const render = (
5656

5757
let component = new ComponentConstructor({
5858
target,
59-
...checkProps(options)
59+
...checkProps(options),
6060
})
6161

6262
containerCache.add({ container, target, component })
@@ -70,26 +70,20 @@ const render = (
7070
container,
7171
component,
7272
debug: (el = container) => console.log(prettyDOM(el)),
73-
rerender: (options) => {
74-
if (componentCache.has(component)) component.$destroy()
75-
76-
// eslint-disable-next-line no-new
77-
component = new ComponentConstructor({
78-
target,
79-
...checkProps(options)
80-
})
81-
82-
containerCache.add({ container, target, component })
83-
componentCache.add(component)
84-
85-
component.$$.on_destroy.push(() => {
86-
componentCache.delete(component)
87-
})
73+
rerender: async (props) => {
74+
if (props.props) {
75+
console.warn(
76+
'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
77+
)
78+
props = props.props
79+
}
80+
component.$set(props)
81+
await tick()
8882
},
8983
unmount: () => {
9084
if (componentCache.has(component)) component.$destroy()
9185
},
92-
...getQueriesForElement(container, queries)
86+
...getQueriesForElement(container, queries),
9387
}
9488
}
9589

types/index.d.ts

+34-11
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,42 @@
22
// Project: https://github.com/testing-library/svelte-testing-library
33
// Definitions by: Rahim Alwer <https://github.com/mihar-22>
44

5-
import {BoundFunction, EventType,Queries, queries} from '@testing-library/dom'
6-
import { ComponentConstructorOptions,ComponentProps, SvelteComponent } from 'svelte'
5+
import {
6+
BoundFunction,
7+
EventType,
8+
Queries,
9+
queries,
10+
} from '@testing-library/dom'
11+
import {
12+
ComponentConstructorOptions,
13+
ComponentProps,
14+
SvelteComponent,
15+
} from 'svelte'
716

817
export * from '@testing-library/dom'
918

10-
type SvelteComponentOptions<C extends SvelteComponent> = ComponentProps<C> | Pick<ComponentConstructorOptions<ComponentProps<C>>, "anchor" | "props" | "hydrate" | "intro" | "context">
19+
type SvelteComponentOptions<C extends SvelteComponent> =
20+
| ComponentProps<C>
21+
| Pick<
22+
ComponentConstructorOptions<ComponentProps<C>>,
23+
'anchor' | 'props' | 'hydrate' | 'intro' | 'context'
24+
>
1125

1226
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
1327

14-
type Constructor<T> = new (...args: any[]) => T;
28+
type Constructor<T> = new (...args: any[]) => T
1529

1630
/**
1731
* Render a Component into the Document.
1832
*/
19-
export type RenderResult<C extends SvelteComponent, Q extends Queries = typeof queries> = {
33+
export type RenderResult<
34+
C extends SvelteComponent,
35+
Q extends Queries = typeof queries,
36+
> = {
2037
container: HTMLElement
2138
component: C
2239
debug: (el?: HTMLElement | DocumentFragment) => void
23-
rerender: (options: SvelteComponentOptions<C>) => void
40+
rerender: (props: ComponentProps<C>) => Promise<void>
2441
unmount: () => void
2542
} & { [P in keyof Q]: BoundFunction<Q[P]> }
2643

@@ -38,7 +55,7 @@ export function render<C extends SvelteComponent>(
3855
export function render<C extends SvelteComponent, Q extends Queries>(
3956
component: Constructor<C>,
4057
componentOptions?: SvelteComponentOptions<C>,
41-
renderOptions?: RenderOptions<Q>,
58+
renderOptions?: RenderOptions<Q>
4259
): RenderResult<C, Q>
4360

4461
/**
@@ -50,13 +67,19 @@ export function cleanup(): void
5067
* Fires DOM events on an element provided by @testing-library/dom. Since Svelte needs to flush
5168
* pending state changes via `tick`, these methods have been override and now return a promise.
5269
*/
53-
export type FireFunction = (element: Document | Element | Window, event: Event) => Promise<boolean>;
70+
export type FireFunction = (
71+
element: Document | Element | Window,
72+
event: Event
73+
) => Promise<boolean>
5474

5575
export type FireObject = {
56-
[K in EventType]: (element: Document | Element | Window, options?: {}) => Promise<boolean>;
57-
};
76+
[K in EventType]: (
77+
element: Document | Element | Window,
78+
options?: {}
79+
) => Promise<boolean>
80+
}
5881

59-
export const fireEvent: FireFunction & FireObject;
82+
export const fireEvent: FireFunction & FireObject
6083

6184
/**
6285
* Calls a function and notifies Svelte to flush any pending state changes.

0 commit comments

Comments
 (0)