Repository: testing-library/svelte-testing-library
Pull request #210 (2022-11-20 21:54:27)

fix!: Rerender to only update props of component without destroying it
When rerendering the component, it shouldn't be destroyed and re mounted. Browser behavior is not working like that, and current stl implementation can lead false positive test results. Possible fail case is reproduced [in this repo](https://github.com/basarbk/stl-rerender-issue)
In that scenario UserPage is visible for paths `/user/:id` . Corresponding routing setup would be like this (either with svelte-routing or svelte-navigator). We can pass the id as param

```html
<Route path="/user/:id" let:params>
   <UserPage id={params.id} />
</Route>
```

When we test this on browser and we navigate from `/user/1` to `/user/2`, UserPage component is not destroyed. Only the prop, id is changed and same component is used. And If we want to load corresponding user data from backend, we must be making another api call with this new id.

But in test, since rerender is destroying and mounting the component, even an incorrect implementation [like this one](https://github.com/basarbk/stl-rerender-issue/blob/main/src/UserPage.svelte#L11-L14) is working. So it ends up with false positive result.

With this change, since rerender can be called only for a rendered component, when it is called, just updating the component's props.
I'm not sure how many other use cases are there other than updating the props, but with the fix, just updating the props of component.
And since this [prop change is async](https://svelte.dev/docs#run-time-client-side-component-api-$set), calling tick as well which makes rerender to wait for tick.

If this impl approved then we need to update the rerender part here [in this doc](https://testing-library.com/docs/svelte-testing-library/api/#results)

mcous: @yanick how are you feeling about the breaking change? We could implement this with a `{preserve: true}` option and keep it as a minor bump to reduce churn, if that felt useful
yanick: > @yanick how are you feeling about the breaking change? 

I'm leaning toward doing it. It's kinda too bad that it'll make our major version out of sync with Svelte's, but that's something that will happen sooner or later anyway.

> We could implement this with a `{preserve: true}` option and keep it as a minor bump to reduce churn, if that felt useful

In this case I don't think it's necessary as I don't see that many people actually realized the components were destroyed on re-rendering and _actually_ wanted that. 

Sounds sane?
yanick: Documentation branch: https://github.com/yanick/testing-library-docs/tree/rerender


Patch:
diff --git a/src/pure.js b/src/pure.js
index 6d49434..59c62ff 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -1,7 +1,7 @@
 import {
   fireEvent as dtlFireEvent,
   getQueriesForElement,
-  prettyDOM
+  prettyDOM,
 } from '@testing-library/dom'
 import { tick } from 'svelte'
 
@@ -14,7 +14,7 @@ const svelteComponentOptions = [
   'props',
   'hydrate',
   'intro',
-  'context'
+  'context',
 ]
 
 const render = (
@@ -56,7 +56,7 @@ const render = (
 
   let component = new ComponentConstructor({
     target,
-    ...checkProps(options)
+    ...checkProps(options),
   })
 
   containerCache.add({ container, target, component })
@@ -70,26 +70,20 @@ const render = (
     container,
     component,
     debug: (el = container) => console.log(prettyDOM(el)),
-    rerender: (options) => {
-      if (componentCache.has(component)) component.$destroy()
-
-      // eslint-disable-next-line no-new
-      component = new ComponentConstructor({
-        target,
-        ...checkProps(options)
-      })
-
-      containerCache.add({ container, target, component })
-      componentCache.add(component)
-
-      component.$$.on_destroy.push(() => {
-        componentCache.delete(component)
-      })
+    rerender: async (props) => {
+      if (props.props) {
+        console.warn(
+          'rerender({ props: {...} }) deprecated, use rerender({...}) instead'
+        )
+        props = props.props
+      }
+      component.$set(props)
+      await tick()
     },
     unmount: () => {
       if (componentCache.has(component)) component.$destroy()
     },
-    ...getQueriesForElement(container, queries)
+    ...getQueriesForElement(container, queries),
   }
 }
 
diff --git a/types/index.d.ts b/types/index.d.ts
index 26d85d7..d60d779 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -2,25 +2,42 @@
 // Project: https://github.com/testing-library/svelte-testing-library
 // Definitions by: Rahim Alwer <https://github.com/mihar-22>
 
-import {BoundFunction, EventType,Queries, queries} from '@testing-library/dom'
-import { ComponentConstructorOptions,ComponentProps, SvelteComponent  } from 'svelte'
+import {
+  BoundFunction,
+  EventType,
+  Queries,
+  queries,
+} from '@testing-library/dom'
+import {
+  ComponentConstructorOptions,
+  ComponentProps,
+  SvelteComponent,
+} from 'svelte'
 
 export * from '@testing-library/dom'
 
-type SvelteComponentOptions<C extends SvelteComponent> = ComponentProps<C> | Pick<ComponentConstructorOptions<ComponentProps<C>>, "anchor" | "props" | "hydrate" | "intro" | "context">
+type SvelteComponentOptions<C extends SvelteComponent> =
+  | ComponentProps<C>
+  | Pick<
+      ComponentConstructorOptions<ComponentProps<C>>,
+      'anchor' | 'props' | 'hydrate' | 'intro' | 'context'
+    >
 
 type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
 
-type Constructor<T> = new (...args: any[]) => T;
+type Constructor<T> = new (...args: any[]) => T
 
 /**
  * Render a Component into the Document.
  */
-export type RenderResult<C extends SvelteComponent, Q extends Queries = typeof queries> = {
+export type RenderResult<
+  C extends SvelteComponent,
+  Q extends Queries = typeof queries,
+> = {
   container: HTMLElement
   component: C
   debug: (el?: HTMLElement | DocumentFragment) => void
-  rerender: (options: SvelteComponentOptions<C>) => void
+  rerender: (props: ComponentProps<C>) => Promise<void>
   unmount: () => void
 } & { [P in keyof Q]: BoundFunction<Q[P]> }
 
@@ -38,7 +55,7 @@ export function render<C extends SvelteComponent>(
 export function render<C extends SvelteComponent, Q extends Queries>(
   component: Constructor<C>,
   componentOptions?: SvelteComponentOptions<C>,
-  renderOptions?: RenderOptions<Q>,
+  renderOptions?: RenderOptions<Q>
 ): RenderResult<C, Q>
 
 /**
@@ -50,13 +67,19 @@ export function cleanup(): void
  * Fires DOM events on an element provided by @testing-library/dom. Since Svelte needs to flush
  * pending state changes via `tick`, these methods have been override and now return a promise.
  */
-export type FireFunction = (element: Document | Element | Window, event: Event) => Promise<boolean>;
+export type FireFunction = (
+  element: Document | Element | Window,
+  event: Event
+) => Promise<boolean>
 
 export type FireObject = {
-  [K in EventType]: (element: Document | Element | Window, options?: {}) => Promise<boolean>;
-};
+  [K in EventType]: (
+    element: Document | Element | Window,
+    options?: {}
+  ) => Promise<boolean>
+}
 
-export const fireEvent: FireFunction & FireObject;
+export const fireEvent: FireFunction & FireObject
 
 /**
  * Calls a function and notifies Svelte to flush any pending state changes.


Test patch:
diff --git a/src/__tests__/fixtures/Rerender.svelte b/src/__tests__/fixtures/Rerender.svelte
new file mode 100644
index 0000000..aac1600
--- /dev/null
+++ b/src/__tests__/fixtures/Rerender.svelte
@@ -0,0 +1,15 @@
+<script>
+  import { getContext, onMount } from 'svelte'
+
+  export let name
+
+  const mountCounter = getContext('mountCounter')
+
+  onMount(() => {
+    mountCounter.update((i) => i + 1)
+  })
+</script>
+
+<h1 data-testid="test">Hello {name}!</h1>
+
+<div data-testid="mount-counter">{$mountCounter}</div>
diff --git a/src/__tests__/rerender.test.js b/src/__tests__/rerender.test.js
index e3eddb2..d6cbc21 100644
--- a/src/__tests__/rerender.test.js
+++ b/src/__tests__/rerender.test.js
@@ -1,38 +1,38 @@
 /**
  * @jest-environment jsdom
  */
-import { describe, expect, test } from 'vitest'
+import { expect, test, vi } from 'vitest'
+import { writable } from 'svelte/store'
 
-import { render } from '..'
-import Comp from './fixtures/Comp.svelte'
+import { render, waitFor } from '..'
+import Comp from './fixtures/Rerender.svelte'
 
-describe('rerender', () => {
-  test('mounts new component successfully', () => {
-    const { container, rerender } = render(Comp, { props: { name: 'World 1' } })
+const mountCounter = writable(0)
 
-    expect(container.firstChild).toHaveTextContent('Hello World 1!')
-    rerender({ props: { name: 'World 2' } })
-    expect(container.firstChild).toHaveTextContent('Hello World 2!')
+test('mounts new component successfully', async () => {
+  const { getByTestId, rerender } = render(Comp, {
+    props: { name: 'World 1' },
+    context: new Map(Object.entries({ mountCounter })),
   })
 
-  test('destroys old component', () => {
-    let isDestroyed
+  const expectToRender = (content) =>
+    waitFor(() => {
+      expect(getByTestId('test')).toHaveTextContent(content)
+      expect(getByTestId('mount-counter')).toHaveTextContent('1')
+    })
 
-    const { rerender, component } = render(Comp, { props: { name: '' } })
+  await expectToRender('Hello World 1!')
 
-    component.$$.on_destroy.push(() => {
-      isDestroyed = true
-    })
-    rerender({ props: { name: '' } })
-    expect(isDestroyed).toBeTruthy()
-  })
+  console.warn = vi.fn()
 
-  test('destroys old components on multiple rerenders', () => {
-    const { rerender, queryByText } = render(Comp, { props: { name: 'Neil' } })
+  rerender({ props: { name: 'World 2' } })
+  await expectToRender('Hello World 2!')
 
-    rerender({ props: { name: 'Alex' } })
-    expect(queryByText('Hello Neil!')).not.toBeInTheDocument()
-    rerender({ props: { name: 'Geddy' } })
-    expect(queryByText('Hello Alex!')).not.toBeInTheDocument()
-  })
+  expect(console.warn).toHaveBeenCalled()
+
+  console.warn.mockClear()
+  rerender({ name: 'World 3' })
+  await expectToRender('Hello World 3!')
+
+  expect(console.warn).not.toHaveBeenCalled()
 })