Repository: testing-library/svelte-testing-library
Pull request #352 (2024-04-18 15:41:16)

chore(release): promote v5 as an main consumption release


Patch:
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 19c1908..778d507 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -14,6 +14,7 @@ module.exports = {
   plugins: ['svelte', 'simple-import-sort', 'json-files'],
   rules: {
     'simple-import-sort/imports': 'error',
+    'simple-import-sort/exports': 'error',
   },
   overrides: [
     {
@@ -22,6 +23,9 @@ module.exports = {
       parserOptions: {
         parser: '@typescript-eslint/parser',
       },
+      rules: {
+        'no-undef-init': 'off',
+      },
     },
     {
       files: ['*.ts'],
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c6e83fa..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 }}
@@ -16,11 +19,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/.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/README.md b/README.md
index d49bbae..0a39429 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,21 @@ This library has `peerDependencies` listings for `svelte >= 3`.
 You may also be interested in installing `@testing-library/jest-dom` so you can use
 [the custom jest matchers](https://github.com/testing-library/jest-dom).
 
+### Svelte 5 support
+
+If you are riding the bleeding edge of Svelte 5, you'll need to either
+import from `@testing-library/svelte/svelte5` instead of `@testing-library/svelte`, or have your `vite.config.js` contains the following alias:
+
+```
+export default defineConfig(({ }) => ({
+  test: {
+    alias: {
+      '@testing-library/svelte': '@testing-library/svelte/svelte5'
+    }
+  },
+}))
+```
+
 ## Docs
 
 See the [**docs**](https://testing-library.com/docs/svelte-testing-library/intro) over at the Testing Library website.
diff --git a/package.json b/package.json
index 20bc1c2..1948cfc 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,10 @@
       "types": "./types/index.d.ts",
       "default": "./src/index.js"
     },
+    "./svelte5": {
+      "types": "./types/index.d.ts",
+      "default": "./src/svelte5-index.js"
+    },
     "./vitest": {
       "default": "./src/vitest.js"
     }
@@ -64,13 +68,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",
@@ -89,16 +93,16 @@
     "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",
     "prettier-plugin-svelte": "3.1.2",
-    "svelte": "^3 || ^4",
+    "svelte": "^3 || ^4 || ^5",
     "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/index.js b/src/index.js
index e94d814..46fd662 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
@@ -12,4 +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/pure.js b/src/pure.js
index 59c62ff..364c225 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -3,46 +3,41 @@ import {
   getQueriesForElement,
   prettyDOM,
 } from '@testing-library/dom'
-import { tick } from 'svelte'
-
-const containerCache = new Set()
-const componentCache = new Set()
-
-const svelteComponentOptions = [
-  'accessors',
-  'anchor',
-  'props',
-  'hydrate',
-  'intro',
-  'context',
-]
-
-const render = (
-  Component,
-  { target, ...options } = {},
-  { container, queries } = {}
-) => {
-  container = container || document.body
-  target = target || container.appendChild(document.createElement('div'))
-
-  const ComponentConstructor = Component.default || Component
-
-  const checkProps = (options) => {
+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',
+    'hydrate',
+    'intro',
+    'context',
+  ]
+
+  targetCache = new Set()
+  componentCache = new Set()
+
+  checkProps(options) {
     const isProps = !Object.keys(options).some((option) =>
-      svelteComponentOptions.includes(option)
+      this.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)
+        (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
         `)
@@ -54,78 +49,109 @@ const render = (
     return { props: options }
   }
 
-  let component = new ComponentConstructor({
-    target,
-    ...checkProps(options),
-  })
-
-  containerCache.add({ container, target, component })
-  componentCache.add(component)
-
-  component.$$.on_destroy.push(() => {
-    componentCache.delete(component)
-  })
-
-  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 tick()
-    },
-    unmount: () => {
-      if (componentCache.has(component)) component.$destroy()
-    },
-    ...getQueriesForElement(container, queries),
+  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(ComponentConstructor, {
+      ...componentOptions,
+      target,
+    })
+
+    return {
+      baseElement,
+      component,
+      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
+        }
+        component.$set(props)
+        await Svelte.tick()
+      },
+      unmount: () => {
+        this.cleanupComponent(component)
+      },
+      ...getQueriesForElement(baseElement, renderOptions.queries),
+    }
   }
-}
 
-const cleanupAtContainer = (cached) => {
-  const { target, component } = cached
+  renderComponent(ComponentConstructor, componentOptions) {
+    if (IS_SVELTE_5) {
+      throw new Error('for Svelte 5, use `@testing-library/svelte/svelte5`')
+    }
+
+    const component = new ComponentConstructor(componentOptions)
+
+    this.componentCache.add(component)
 
-  if (componentCache.has(component)) component.$destroy()
+    // TODO(mcous, 2024-02-11): remove this behavior in the next major version
+    component.$$.on_destroy.push(() => {
+      this.componentCache.delete(component)
+    })
 
-  if (target.parentNode === document.body) {
-    document.body.removeChild(target)
+    return component
   }
 
-  containerCache.delete(cached)
-}
+  cleanupComponent(component) {
+    const inCache = this.componentCache.delete(component)
+
+    if (inCache) {
+      component.$destroy()
+    }
+  }
+
+  cleanupTarget(target) {
+    const inCache = this.targetCache.delete(target)
 
-const cleanup = () => {
-  Array.from(containerCache.keys()).forEach(cleanupAtContainer)
+    if (inCache && target.parentNode === document.body) {
+      document.body.removeChild(target)
+    }
+  }
+
+  cleanup() {
+    this.componentCache.forEach(this.cleanupComponent.bind(this))
+    this.targetCache.forEach(this.cleanupTarget.bind(this))
+  }
 }
 
-const act = async (fn) => {
+const instance = new SvelteTestingLibrary()
+
+export const render = instance.render.bind(instance)
+
+export const cleanup = instance.cleanup.bind(instance)
+
+export const act = async (fn) => {
   if (fn) {
     await fn()
   }
-  return tick()
+  return Svelte.tick()
 }
 
-const fireEvent = async (...args) => {
+export const fireEvent = async (...args) => {
   const event = dtlFireEvent(...args)
-  await tick()
+  await Svelte.tick()
   return event
 }
 
 Object.keys(dtlFireEvent).forEach((key) => {
   fireEvent[key] = async (...args) => {
     const event = dtlFireEvent[key](...args)
-    await tick()
+    await Svelte.tick()
     return event
   }
 })
-
-/* eslint-disable import/export */
-
-export * from '@testing-library/dom'
-
-export { render, cleanup, fireEvent, act }
diff --git a/src/svelte5-index.js b/src/svelte5-index.js
new file mode 100644
index 0000000..ab49641
--- /dev/null
+++ b/src/svelte5-index.js
@@ -0,0 +1,23 @@
+/* eslint-disable import/export */
+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
+// this ensures that tests run in isolation from each other
+// if you don't like this then either import the `pure` module
+// or set the STL_SKIP_AUTO_CLEANUP env variable to 'true'.
+if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
+  afterEach(async () => {
+    await act()
+    cleanup()
+  })
+}
+
+// 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/src/svelte5.js b/src/svelte5.js
new file mode 100644
index 0000000..a8dd494
--- /dev/null
+++ b/src/svelte5.js
@@ -0,0 +1,30 @@
+import { createClassComponent } from 'svelte/legacy'
+
+import { SvelteTestingLibrary } from './pure.js'
+
+class Svelte5TestingLibrary extends SvelteTestingLibrary {
+  svelteComponentOptions = [
+    'target',
+    'props',
+    'events',
+    'context',
+    'intro',
+    'recover',
+  ]
+
+  renderComponent(ComponentConstructor, componentOptions) {
+    const component = createClassComponent({
+      ...componentOptions,
+      component: ComponentConstructor,
+    })
+
+    this.componentCache.add(component)
+
+    return component
+  }
+}
+
+const instance = new Svelte5TestingLibrary()
+
+export const render = instance.render.bind(instance)
+export const cleanup = instance.cleanup.bind(instance)
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(),
 }
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<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>>
+  | Partial<ComponentConstructorOptions<ComponentProps<C>>>
 
 type Constructor<T> = 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<C>) => Promise<void>
+  rerender: (props: Partial<ComponentProps<C>>) => Promise<void>
   unmount: () => void
 } & { [P in keyof Q]: BoundFunction<Q[P]> }
 
 export interface RenderOptions<Q extends Queries = typeof queries> {
-  container?: HTMLElement
+  baseElement?: HTMLElement
   queries?: Q
 }
 
-export function render<C extends SvelteComponent>(
-  component: Constructor<C>,
-  componentOptions?: SvelteComponentOptions<C>,
-  renderOptions?: Omit<RenderOptions, 'queries'>
-): RenderResult<C>
-
-export function render<C extends SvelteComponent, Q extends Queries>(
+export function render<
+  C extends SvelteComponent,
+  Q extends Queries = typeof queries,
+>(
   component: Constructor<C>,
   componentOptions?: SvelteComponentOptions<C>,
   renderOptions?: RenderOptions<Q>
diff --git a/vite.config.js b/vite.config.js
index 4baf76f..d9085ef 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,9 +1,24 @@
+import path from 'node:path'
+
 import { svelte } from '@sveltejs/vite-plugin-svelte'
+import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
 import { defineConfig } from 'vite'
 
+const IS_SVELTE_5 = SVELTE_VERSION >= '5'
+
+const alias = [
+  {
+    find: '@testing-library/svelte',
+    replacement: path.resolve(
+      __dirname,
+      IS_SVELTE_5 ? 'src/svelte5-index.js' : 'src/index.js'
+    ),
+  },
+]
+
 // https://vitejs.dev/config/
 export default defineConfig(({ mode }) => ({
-  plugins: [svelte()],
+  plugins: [svelte({ hot: false })],
   resolve: {
     // Ensure `browser` exports are used in tests
     // Vitest prefers modules' `node` export by default
@@ -12,6 +27,7 @@ export default defineConfig(({ mode }) => ({
     conditions: mode === 'test' ? ['browser'] : [],
   },
   test: {
+    alias,
     environment: 'jsdom',
     setupFiles: ['./src/__tests__/_vitest-setup.js'],
     mockReset: true,


Test patch:
diff --git a/src/__tests__/__snapshots__/render.test.js.snap b/src/__tests__/__snapshots__/render.test.js.snap
deleted file mode 100644
index a31cf2e..0000000
--- a/src/__tests__/__snapshots__/render.test.js.snap
+++ /dev/null
@@ -1,25 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`render > should accept svelte component options 1`] = `
-<body>
-  <div>
-    <h1
-      data-testid="test"
-    >
-      Hello 
-      World
-      !
-    </h1>
-     
-    <div>
-      we have context
-    </div>
-     
-    <button>
-      Button
-    </button>
-    <!--&lt;Comp&gt;-->
-    <div />
-  </div>
-</body>
-`;
diff --git a/src/__tests__/act.test.js b/src/__tests__/act.test.js
index 6eafc0d..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 '..'
 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-skip.test.js b/src/__tests__/auto-cleanup-skip.test.js
index 265f55b..db65447 100644
--- a/src/__tests__/auto-cleanup-skip.test.js
+++ b/src/__tests__/auto-cleanup-skip.test.js
@@ -7,7 +7,7 @@ describe('auto-cleanup-skip', () => {
 
   beforeAll(async () => {
     process.env.STL_SKIP_AUTO_CLEANUP = 'true'
-    const stl = await import('..')
+    const stl = await import('@testing-library/svelte')
     render = stl.render
   })
 
diff --git a/src/__tests__/auto-cleanup.test.js b/src/__tests__/auto-cleanup.test.js
index 349ee39..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 '..'
 import Comp from './fixtures/Comp.svelte'
 
 describe('auto-cleanup', () => {
diff --git a/src/__tests__/cleanup.test.js b/src/__tests__/cleanup.test.js
new file mode 100644
index 0000000..7131624
--- /dev/null
+++ b/src/__tests__/cleanup.test.js
@@ -0,0 +1,35 @@
+import { cleanup, render } from '@testing-library/svelte'
+import { describe, expect, test, vi } from 'vitest'
+
+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', () => {
+    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__/context.test.js b/src/__tests__/context.test.js
index effdef4..da54b9d 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 '..'
 import Comp from './fixtures/Context.svelte'
 
 test('can set a context', () => {
diff --git a/src/__tests__/debug.test.js b/src/__tests__/debug.test.js
index 1072a0f..e2a9eda 100644
--- a/src/__tests__/debug.test.js
+++ b/src/__tests__/debug.test.js
@@ -1,24 +1,18 @@
 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 { render } from '..'
 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() })
 
-  test('pretty prints the container', () => {
-    const { container, debug } = render(Comp, { props: { name: 'world' } })
+    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__/events.test.js b/src/__tests__/events.test.js
index fccf990..e0aba4d 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 '..'
 import Comp from './fixtures/Comp.svelte'
 
 describe('events', () => {
@@ -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,14 +18,15 @@ describe('events', () => {
     const { getByText } = render(Comp, { props: { name: 'World' } })
     const button = getByText('Button')
 
-    await fireEvent(
+    const result = fireEvent(
       button,
       new MouseEvent('click', {
         bubbles: true,
-        cancelable: true
+        cancelable: true,
       })
     )
 
+    await expect(result).resolves.toBe(true)
     expect(button).toHaveTextContent('Button Clicked')
   })
 })
diff --git a/src/__tests__/fixtures/Comp.svelte b/src/__tests__/fixtures/Comp.svelte
index ec04c05..ba23d88 100644
--- a/src/__tests__/fixtures/Comp.svelte
+++ b/src/__tests__/fixtures/Comp.svelte
@@ -1,23 +1,17 @@
 <svelte:options accessors />
 
 <script>
-  import { getContext } from 'svelte'
-
-  export let name
+  export let name = 'World'
 
   let buttonText = 'Button'
 
-  const contextName = getContext('name')
-
-  function handleClick () {
+  function handleClick() {
     buttonText = 'Button Clicked'
   }
 </script>
 
 <h1 data-testid="test">Hello {name}!</h1>
 
-<div>we have {contextName}</div>
-
 <button on:click={handleClick}>{buttonText}</button>
 
 <style></style>
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 @@
-<script>
-  export let name
-
-  let buttonText = 'Button Text'
-
-  function handleClick () {
-    buttonText = 'Button Clicked'
-  }
-</script>
-
-<style></style>
-
-<h1>Hello {name}!</h1>
-
-<button on:click={handleClick}>{buttonText}</button>
diff --git a/src/__tests__/fixtures/Context.svelte b/src/__tests__/fixtures/Context.svelte
index be8253b..d6515d5 100644
--- a/src/__tests__/fixtures/Context.svelte
+++ b/src/__tests__/fixtures/Context.svelte
@@ -1,7 +1,7 @@
 <script>
-  import { getContext } from 'svelte';
+  import { getContext } from 'svelte'
 
-  const ctx = getContext('foo');
+  const ctx = getContext('foo')
 </script>
 
 <div>{ctx.message}</div>
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 @@
 <script>
-  import { onDestroy,onMount } from 'svelte'
+  import { onDestroy, onMount } from 'svelte'
 
-  export let onMounted
-  export let onDestroyed
+  export let onExecuted = undefined
+  export let onMounted = undefined
+  export let onDestroyed = undefined
 
-  onMount(() => onMounted())
-  onDestroy(() => onDestroyed())
+  onExecuted?.()
+
+  onMount(() => {
+    onMounted?.()
+  })
+
+  onDestroy(() => {
+    onDestroyed?.()
+  })
 </script>
 
 <button />
diff --git a/src/__tests__/fixtures/Rerender.svelte b/src/__tests__/fixtures/Rerender.svelte
deleted file mode 100644
index aac1600..0000000
--- a/src/__tests__/fixtures/Rerender.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-<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__/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 @@
 <script lang="ts">
   export let name: string
+  export let count: number
 </script>
 
 <h1>hello {name}</h1>
+<p>count: {count}</p>
diff --git a/src/__tests__/mount.test.js b/src/__tests__/mount.test.js
index 830b513..48d985f 100644
--- a/src/__tests__/mount.test.js
+++ b/src/__tests__/mount.test.js
@@ -1,33 +1,33 @@
+import { act, render, screen } from '@testing-library/svelte'
 import { describe, expect, test, vi } from 'vitest'
 
-import { act, render, screen } from '..'
 import Mounter from './fixtures/Mounter.svelte'
 
-describe('mount and destroy', () => {
-  const handleMount = vi.fn()
-  const handleDestroy = vi.fn()
+const onMounted = vi.fn()
+const onDestroyed = vi.fn()
+const renderSubject = () => render(Mounter, { onMounted, onDestroyed })
 
+describe('mount and destroy', () => {
   test('component is mounted', async () => {
-    await act(() => {
-      render(Mounter, { onMounted: handleMount, onDestroyed: handleDestroy })
-    })
+    renderSubject()
 
     const content = screen.getByRole('button')
 
-    expect(handleMount).toHaveBeenCalledOnce()
     expect(content).toBeInTheDocument()
+    await act()
+    expect(onMounted).toHaveBeenCalledOnce()
   })
 
   test('component is destroyed', async () => {
-    const { unmount } = render(Mounter, {
-      onMounted: handleMount,
-      onDestroyed: handleDestroy,
-    })
+    const { unmount } = renderSubject()
+
+    await act()
+    unmount()
 
-    await act(() => unmount())
     const content = screen.queryByRole('button')
 
-    expect(handleDestroy).toHaveBeenCalledOnce()
     expect(content).not.toBeInTheDocument()
+    await act()
+    expect(onDestroyed).toHaveBeenCalledOnce()
   })
 })
diff --git a/src/__tests__/multi-base.test.js b/src/__tests__/multi-base.test.js
index 39f28d1..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 '..'
 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 6beb984..ea445d5 100644
--- a/src/__tests__/render.test.js
+++ b/src/__tests__/render.test.js
@@ -1,104 +1,85 @@
-import { beforeEach, describe, expect, test } from 'vitest'
+import { render } from '@testing-library/svelte'
+import { describe, expect, test } from 'vitest'
 
-import { act, render as stlRender } from '..'
 import Comp from './fixtures/Comp.svelte'
-import CompDefault from './fixtures/Comp2.svelte'
+import { IS_SVELTE_5 } from './utils.js'
 
 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({ accessors: true })
-
-    expect(getByText('Hello World!')).toBeInTheDocument()
+  test('throws error when mixing svelte component options and props', () => {
+    expect(() => {
+      render(Comp, { props, name: 'World' })
+    }).toThrow(/Unknown options/)
+  })
 
-    expect(component.name).toBe('World')
+  test('throws error when mixing target option and props', () => {
+    expect(() => {
+      render(Comp, { target: document.createElement('div'), name: 'World' })
+    }).toThrow(/Unknown options/)
+  })
 
-    await act(() => {
-      component.value = 'Planet'
-    })
+  test('should return a container object wrapping the DOM of the rendered component', () => {
+    const { container, getByTestId } = render(Comp, props)
+    const firstElement = getByTestId('test')
 
-    expect(getByText('Hello World!')).toBeInTheDocument()
+    expect(container.firstChild).toBe(firstElement)
   })
 
-  test('should accept props directly', () => {
-    const { getByText } = stlRender(Comp, { name: 'World' })
-    expect(getByText('Hello World!')).toBeInTheDocument()
+  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('should accept svelte component options', () => {
+  test('if target is provided, use it as container and baseElement', () => {
     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()
-  })
+    const { baseElement, container } = render(Comp, { props, target })
 
-  test('should throw error when mixing svelte component options and props', () => {
-    expect(() => {
-      stlRender(Comp, { anchor: '', name: 'World' })
-    }).toThrow(/Unknown options were found/)
+    expect(container).toBe(target)
+    expect(baseElement).toBe(target)
   })
 
-  test('should return a container object, which contains the DOM of the rendered component', () => {
-    const { container } = render()
+  test('allow baseElement to be specified', () => {
+    const customBaseElement = document.createElement('div')
 
-    expect(container.innerHTML).toBe(document.body.innerHTML)
-  })
-
-  test('correctly find component constructor on the default property', () => {
-    const { getByText } = render(CompDefault, { props: { name: 'World' } })
+    const { baseElement, container } = render(
+      Comp,
+      { props },
+      { baseElement: customBaseElement }
+    )
 
-    expect(getByText('Hello World!')).toBeInTheDocument()
+    expect(baseElement).toBe(customBaseElement)
+    expect(baseElement.firstChild).toBe(container)
   })
 
-  test("accept the 'context' option", () => {
-    const { getByText } = stlRender(Comp, {
-      props: {
-        name: 'Universe'
-      },
-      context: new Map([['name', 'context']])
-    })
-
-    expect(getByText('we have context')).toBeInTheDocument()
+  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 d6cbc21..ca4b8e8 100644
--- a/src/__tests__/rerender.test.js
+++ b/src/__tests__/rerender.test.js
@@ -1,38 +1,50 @@
-/**
- * @jest-environment jsdom
- */
-import { expect, test, vi } from 'vitest'
-import { writable } from 'svelte/store'
+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 '..'
-import Comp from './fixtures/Rerender.svelte'
+import Comp from './fixtures/Comp.svelte'
 
-const mountCounter = writable(0)
+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 { getByTestId, rerender } = render(Comp, {
-    props: { name: 'World 1' },
-    context: new Map(Object.entries({ mountCounter })),
+    await rerender({ name: 'Dolly' })
+
+    expect(element).toHaveTextContent('Hello Dolly!')
   })
 
-  const expectToRender = (content) =>
-    waitFor(() => {
-      expect(getByTestId('test')).toHaveTextContent(content)
-      expect(getByTestId('mount-counter')).toHaveTextContent('1')
-    })
+  test('warns if incorrect arguments shape used', async () => {
+    vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: 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!')
+  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).toHaveBeenCalled()
+    expect(element).toBeInTheDocument()
+    expect(component.name).toBe('World')
 
-  console.warn.mockClear()
-  rerender({ name: 'World 3' })
-  await expectToRender('Hello World 3!')
+    await act(() => {
+      component.name = 'Planet'
+    })
 
-  expect(console.warn).not.toHaveBeenCalled()
+    expect(element).toHaveTextContent('Hello Planet!')
+  })
 })
diff --git a/src/__tests__/transition.test.js b/src/__tests__/transition.test.js
index 5b1ff18..a9c8e02 100644
--- a/src/__tests__/transition.test.js
+++ b/src/__tests__/transition.test.js
@@ -1,15 +1,16 @@
+import { render, screen, waitFor } from '@testing-library/svelte'
 import { userEvent } from '@testing-library/user-event'
 import { beforeEach, describe, expect, test, vi } from 'vitest'
 
-import { render, screen, waitFor } from '..'
 import Transitioner from './fixtures/Transitioner.svelte'
+import { IS_JSDOM, IS_SVELTE_5 } from './utils.js'
 
-describe('transitions', () => {
+describe.skipIf(IS_SVELTE_5)('transitions', () => {
   beforeEach(() => {
-    if (window.navigator.userAgent.includes('jsdom')) {
-      const raf = (fn) => setTimeout(() => fn(new Date()), 16)
-      vi.stubGlobal('requestAnimationFrame', raf)
-    }
+    if (!IS_JSDOM) return
+
+    const raf = (fn) => setTimeout(() => fn(new Date()), 16)
+    vi.stubGlobal('requestAnimationFrame', raf)
   })
 
   test('on:introend', async () => {
diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js
new file mode 100644
index 0000000..69be184
--- /dev/null
+++ b/src/__tests__/utils.js
@@ -0,0 +1,7 @@
+import { VERSION as SVELTE_VERSION } from 'svelte/compiler'
+
+export const IS_JSDOM = window.navigator.userAgent.includes('jsdom')
+
+export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js
+
+export const IS_SVELTE_5 = SVELTE_VERSION >= '5'
diff --git a/src/vitest.js b/src/vitest.js
index 135ddbe..71977e6 100644
--- a/src/vitest.js
+++ b/src/vitest.js
@@ -1,7 +1,6 @@
+import { act, cleanup } from '@testing-library/svelte'
 import { afterEach } from 'vitest'
 
-import { act, cleanup } from './pure.js'
-
 afterEach(async () => {
   await act()
   cleanup()
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<Simple>) => void
+      rerender: (props: Partial<ComponentProps<Simple>>) => Promise<void>
       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 } }
     )