diff --git a/package.json b/package.json
index b6a0c37..bf1654d 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,10 @@
"types": "./types/index.d.ts",
"default": "./src/index.js"
},
+ "./core": {
+ "types": "./types/core/index.d.ts",
+ "default": "./src/core/index.js"
+ },
"./svelte5": {
"types": "./types/index.d.ts",
"default": "./src/index.js"
@@ -53,7 +57,7 @@
"!__tests__"
],
"scripts": {
- "toc": "doctoc README.md",
+ "toc": "doctoc README.md src/core/README.md",
"lint": "prettier . --check && eslint .",
"lint:delta": "npm-run-all -p prettier:delta eslint:delta",
"prettier:delta": "prettier --check `./scripts/changed-files`",
diff --git a/src/core/README.md b/src/core/README.md
new file mode 100644
index 0000000..12e5397
--- /dev/null
+++ b/src/core/README.md
@@ -0,0 +1,80 @@
+# @testing-library/svelte/core
+
+Do you want to build your own Svelte testing library? You may want to use our rendering core, which abstracts away differences in Svelte versions to provide a simple API to render Svelte components into the document and clean them up afterwards
+
+
+
+
+- [Usage](#usage)
+- [API](#api)
+ - [`prepareDocument`](#preparedocument)
+ - [`mount`](#mount)
+ - [`cleanup`](#cleanup)
+
+
+
+## Usage
+
+```ts
+import { prepareDocument, mount, cleanup } from '@testing-library/svelte/core'
+import MyCoolComponent from './my-cool-component.svelte'
+
+const { baseElement, target, options } = prepareDocument({ awesome: true })
+const { component, unmount, rerender } = mount(MyCoolComponent, options)
+
+// later
+cleanup()
+```
+
+## API
+
+### `prepareDocument`
+
+Validate options and prepare document elements for rendering.
+
+```ts
+const { baseElement, target, options } = prepareDocument(propsOrOptions, renderOptions)
+```
+
+| Argument | Type | Description |
+| ---------------- | ---------------------------------------- | --------------------------------------------------------------------- |
+| `propsOrOptions` | `Props` or partial [component options][] | The component's props, or options to pass to Svelte's client-side API |
+| `renderOptions` | `{ baseElement?: HTMLElement }` | customize `baseElement`; will be `document.body` if unspecified |
+
+| Result | Type | Description |
+| ------------- | --------------------- | -------------------------------------------------------------------- |
+| `baseElement` | `HTMLElement` | The base element, `document.body` by default |
+| `target` | `HTMLElement` | The component's `target` element, a `
` by default |
+| `options` | [component options][] | Validated and normalized Svelte options to pass to `renderComponent` |
+
+[component options]: https://svelte.dev/docs/client-side-component-api
+
+### `mount`
+
+Mount a Svelte component into the document.
+
+```ts
+const { component, unmount, rerender } = mount(Component, options)
+```
+
+| Argument | Type | Description |
+| ----------- | --------------------- | ---------------------------- |
+| `Component` | [Svelte component][] | An imported Svelte component |
+| `options` | [component options][] | Svelte component options |
+
+| Result | Type | Description |
+| ----------- | ------------------------------------------ | -------------------------------------------------- |
+| `component` | [component instance][] | The component instance |
+| `unmount` | `() => void` | Unmount the component from the document |
+| `rerender` | `(props: Partial
) => Promise` | Update the component's props and wait for rerender |
+
+[Svelte component]: https://svelte.dev/docs/svelte-components
+[component instance]: https://svelte.dev/docs/client-side-component-api
+
+### `cleanup`
+
+Cleanup rendered components and added elements. Call this when your tests are over.
+
+```ts
+cleanup()
+```
diff --git a/src/core/cleanup.js b/src/core/cleanup.js
new file mode 100644
index 0000000..83e32b9
--- /dev/null
+++ b/src/core/cleanup.js
@@ -0,0 +1,30 @@
+/** @type {Map void} */
+const itemsToClean = new Map()
+
+/** Register an item for later cleanup. */
+const addItemToCleanup = (item, onCleanup) => {
+ itemsToClean.set(item, onCleanup)
+}
+
+/** Remove an individual item from cleanup without running its cleanup handler. */
+const removeItemFromCleanup = (item) => {
+ itemsToClean.delete(item)
+}
+
+/** Clean up an individual item. */
+const cleanupItem = (item) => {
+ const handleCleanup = itemsToClean.get(item)
+ handleCleanup?.()
+ itemsToClean.delete(item)
+}
+
+/** Clean up all components and elements added to the document. */
+const cleanup = () => {
+ for (const handleCleanup of itemsToClean.values()) {
+ handleCleanup()
+ }
+
+ itemsToClean.clear()
+}
+
+export { addItemToCleanup, cleanup, cleanupItem, removeItemFromCleanup }
diff --git a/src/core/index.js b/src/core/index.js
index f4a40aa..2ef084a 100644
--- a/src/core/index.js
+++ b/src/core/index.js
@@ -5,23 +5,6 @@
* Will switch to legacy, class-based mounting logic
* if it looks like we're in a Svelte <= 4 environment.
*/
-import * as LegacyCore from './legacy.js'
-import * as ModernCore from './modern.svelte.js'
-import {
- createValidateOptions,
- UnknownSvelteOptionsError,
-} from './validate-options.js'
-
-const { mount, unmount, updateProps, allowedOptions } =
- ModernCore.IS_MODERN_SVELTE ? ModernCore : LegacyCore
-
-/** Validate component options. */
-const validateOptions = createValidateOptions(allowedOptions)
-
-export {
- mount,
- UnknownSvelteOptionsError,
- unmount,
- updateProps,
- validateOptions,
-}
+export { cleanup } from './cleanup.js'
+export { mount, prepareDocument } from './mount.js'
+export { UnknownSvelteOptionsError } from './validate-options.js'
diff --git a/src/core/legacy.js b/src/core/legacy.js
index c9e6d1c..3de08b5 100644
--- a/src/core/legacy.js
+++ b/src/core/legacy.js
@@ -4,6 +4,8 @@
* Supports Svelte <= 4.
*/
+import { removeItemFromCleanup } from './cleanup.js'
+
/** Allowed options for the component constructor. */
const allowedOptions = [
'target',
@@ -15,32 +17,27 @@ const allowedOptions = [
'context',
]
-/**
- * Mount the component into the DOM.
- *
- * The `onDestroy` callback is included for strict backwards compatibility
- * with previous versions of this library. It's mostly unnecessary logic.
- */
-const mount = (Component, options, onDestroy) => {
+/** Mount the component into the DOM. */
+const mountComponent = (Component, options) => {
const component = new Component(options)
- if (typeof onDestroy === 'function') {
- component.$$.on_destroy.push(() => {
- onDestroy(component)
- })
- }
+ // This `$$.on_destroy` handler is included for strict backwards compatibility
+ // with previous versions of this library. It's mostly unnecessary logic.
+ component.$$.on_destroy.push(() => {
+ removeItemFromCleanup(component)
+ })
- return component
-}
+ /** Remove the component from the DOM. */
+ const unmountComponent = () => {
+ component.$destroy()
+ }
-/** Remove the component from the DOM. */
-const unmount = (component) => {
- component.$destroy()
-}
+ /** Update the component's props. */
+ const updateProps = (nextProps) => {
+ component.$set(nextProps)
+ }
-/** Update the component's props. */
-const updateProps = (component, nextProps) => {
- component.$set(nextProps)
+ return { component, unmountComponent, updateProps }
}
-export { allowedOptions, mount, unmount, updateProps }
+export { allowedOptions, mountComponent }
diff --git a/src/core/modern.svelte.js b/src/core/modern.svelte.js
index 3da78b4..1642afc 100644
--- a/src/core/modern.svelte.js
+++ b/src/core/modern.svelte.js
@@ -5,9 +5,6 @@
*/
import * as Svelte from 'svelte'
-/** Props signals for each rendered component. */
-const propsByComponent = new Map()
-
/** Whether we're using Svelte >= 5. */
const IS_MODERN_SVELTE = typeof Svelte.mount === 'function'
@@ -22,29 +19,21 @@ const allowedOptions = [
]
/** Mount the component into the DOM. */
-const mount = (Component, options) => {
+const mountComponent = (Component, options) => {
const props = $state(options.props ?? {})
const component = Svelte.mount(Component, { ...options, props })
- propsByComponent.set(component, props)
-
- return component
-}
+ /** Remove the component from the DOM. */
+ const unmountComponent = () => {
+ Svelte.unmount(component)
+ }
-/** Remove the component from the DOM. */
-const unmount = (component) => {
- propsByComponent.delete(component)
- Svelte.unmount(component)
-}
+ /** Update the component's props. */
+ const updateProps = (nextProps) => {
+ Object.assign(props, nextProps)
+ }
-/**
- * Update the component's props.
- *
- * Relies on the `$state` signal added in `mount`.
- */
-const updateProps = (component, nextProps) => {
- const prevProps = propsByComponent.get(component)
- Object.assign(prevProps, nextProps)
+ return { component, unmountComponent, updateProps }
}
-export { allowedOptions, IS_MODERN_SVELTE, mount, unmount, updateProps }
+export { allowedOptions, IS_MODERN_SVELTE, mountComponent }
diff --git a/src/core/mount.js b/src/core/mount.js
new file mode 100644
index 0000000..bd43148
--- /dev/null
+++ b/src/core/mount.js
@@ -0,0 +1,75 @@
+import { tick } from 'svelte'
+
+import { addItemToCleanup, removeItemFromCleanup } from './cleanup.js'
+import * as LegacySvelte from './legacy.js'
+import * as ModernSvelte from './modern.svelte.js'
+import { validateOptions } from './validate-options.js'
+
+const { mountComponent, allowedOptions } = ModernSvelte.IS_MODERN_SVELTE
+ ? ModernSvelte
+ : LegacySvelte
+
+/**
+ * Validate options and prepare document elements for rendering.
+ *
+ * @template {import('svelte').SvelteComponent} C
+ * @param {import('svelte').ComponentProps | Partial>>} propsOrOptions
+ * @param {{ baseElement?: HTMLElement }} renderOptions
+ * @returns {{
+ * baseElement: HTMLElement
+ * target: HTMLElement
+ * options: import('svelte').ComponentConstructorOptions>
+ * }}
+ */
+const prepareDocument = (propsOrOptions = {}, renderOptions = {}) => {
+ const options = validateOptions(allowedOptions, propsOrOptions)
+
+ const baseElement =
+ renderOptions.baseElement ?? options.target ?? document.body
+
+ const target =
+ options.target ?? baseElement.appendChild(document.createElement('div'))
+
+ addItemToCleanup(target, () => {
+ if (target.parentNode === document.body) {
+ document.body.removeChild(target)
+ }
+ })
+
+ return { baseElement, target, options: { ...options, target } }
+}
+
+/**
+ * Render a Svelte component into the document.
+ *
+ * @template {import('svelte').SvelteComponent} C
+ * @param {import('svelte').ComponentType} Component
+ * @param {import('svelte').ComponentConstructorOptions>} options
+ * @returns {{
+ * component: C
+ * rerender: (props: Partial>) => Promise
+ * unmount: () => void
+ * }}
+ */
+const mount = (Component, options = {}) => {
+ const { component, unmountComponent, updateProps } = mountComponent(
+ Component,
+ options
+ )
+
+ const unmount = () => {
+ unmountComponent()
+ removeItemFromCleanup(component)
+ }
+
+ const rerender = async (props) => {
+ updateProps(props)
+ await tick()
+ }
+
+ addItemToCleanup(component, unmount)
+
+ return { component, unmount, rerender }
+}
+
+export { mount, prepareDocument }
diff --git a/src/core/validate-options.js b/src/core/validate-options.js
index c0d794b..4e2f60e 100644
--- a/src/core/validate-options.js
+++ b/src/core/validate-options.js
@@ -15,7 +15,7 @@ class UnknownSvelteOptionsError extends TypeError {
}
}
-const createValidateOptions = (allowedOptions) => (options) => {
+const validateOptions = (allowedOptions, options) => {
const isProps = !Object.keys(options).some((option) =>
allowedOptions.includes(option)
)
@@ -36,4 +36,4 @@ const createValidateOptions = (allowedOptions) => (options) => {
return options
}
-export { createValidateOptions, UnknownSvelteOptionsError }
+export { UnknownSvelteOptionsError, validateOptions }
diff --git a/src/index.js b/src/index.js
index 2704824..fbfecb9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,5 +1,6 @@
/* eslint-disable import/export */
-import { act, cleanup } from './pure.js'
+import { cleanup } from './core/index.js'
+import { act } from './pure.js'
// If we're running in a test runner that supports afterEach
// then we'll automatically run cleanup afterEach test
@@ -16,7 +17,7 @@ if (typeof afterEach === 'function' && !process.env.STL_SKIP_AUTO_CLEANUP) {
export * from '@testing-library/dom'
// export svelte-specific functions and custom `fireEvent`
-export { UnknownSvelteOptionsError } from './core/index.js'
-export * from './pure.js'
// `fireEvent` must be named to take priority over wildcard from @testing-library/dom
+export { cleanup, UnknownSvelteOptionsError } from './core/index.js'
export { fireEvent } from './pure.js'
+export * from './pure.js'
diff --git a/src/pure.js b/src/pure.js
index edb94b3..66af185 100644
--- a/src/pure.js
+++ b/src/pure.js
@@ -5,10 +5,7 @@ import {
} from '@testing-library/dom'
import { tick } from 'svelte'
-import { mount, unmount, updateProps, validateOptions } from './core/index.js'
-
-const targetCache = new Set()
-const componentCache = new Set()
+import { mount, prepareDocument } from './core/index.js'
/**
* Customize how Svelte renders the component.
@@ -20,7 +17,7 @@ const componentCache = new Set()
/**
* Customize how Testing Library sets up the document and binds queries.
*
- * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
+ * @template {import('@testing-library/dom').Queries} [Q=import('@testing-library/dom').queries]
* @typedef {{
* baseElement?: HTMLElement
* queries?: Q
@@ -31,8 +28,7 @@ const componentCache = new Set()
* The rendered component and bound testing functions.
*
* @template {import('svelte').SvelteComponent} C
- * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
- *
+ * @template {import('@testing-library/dom').Queries} [Q=import('@testing-library/dom').queries]
* @typedef {{
* container: HTMLElement
* baseElement: HTMLElement
@@ -49,41 +45,30 @@ const componentCache = new Set()
* Render a component into the document.
*
* @template {import('svelte').SvelteComponent} C
- * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries]
- *
+ * @template {import('@testing-library/dom').Queries} [Q=import('@testing-library/dom').queries]
* @param {import('svelte').ComponentType} Component - The component to render.
- * @param {SvelteComponentOptions} options - Customize how Svelte renders the component.
+ * @param {SvelteComponentOptions} propsOrOptions - Customize how Svelte renders the component.
* @param {RenderOptions} renderOptions - Customize how Testing Library sets up the document and binds queries.
* @returns {RenderResult} The rendered component and bound testing functions.
*/
-const render = (Component, options = {}, renderOptions = {}) => {
- options = validateOptions(options)
-
- const baseElement =
- renderOptions.baseElement ?? options.target ?? document.body
-
- const queries = getQueriesForElement(baseElement, renderOptions.queries)
-
- const target =
- options.target ?? baseElement.appendChild(document.createElement('div'))
-
- targetCache.add(target)
+const render = (Component, propsOrOptions = {}, renderOptions = {}) => {
+ const { baseElement, target, options } = prepareDocument(
+ propsOrOptions,
+ renderOptions
+ )
- const component = mount(
+ const { component, unmount, rerender } = mount(
Component.default ?? Component,
- { ...options, target },
- cleanupComponent
+ options
)
- componentCache.add(component)
+ const queries = getQueriesForElement(baseElement, renderOptions.queries)
return {
baseElement,
component,
container: target,
- debug: (el = baseElement) => {
- console.log(prettyDOM(el))
- },
+ debug: (el = baseElement) => console.log(prettyDOM(el)),
rerender: async (props) => {
if (props.props) {
console.warn(
@@ -92,40 +77,13 @@ const render = (Component, options = {}, renderOptions = {}) => {
props = props.props
}
- updateProps(component, props)
- await tick()
- },
- unmount: () => {
- cleanupComponent(component)
+ await rerender(props)
},
+ unmount,
...queries,
}
}
-/** Remove a component from the component cache. */
-const cleanupComponent = (component) => {
- const inCache = componentCache.delete(component)
-
- if (inCache) {
- unmount(component)
- }
-}
-
-/** Remove a target element from the target cache. */
-const cleanupTarget = (target) => {
- const inCache = targetCache.delete(target)
-
- if (inCache && target.parentNode === document.body) {
- document.body.removeChild(target)
- }
-}
-
-/** Unmount all components and remove elements added to ``. */
-const cleanup = () => {
- componentCache.forEach(cleanupComponent)
- targetCache.forEach(cleanupTarget)
-}
-
/**
* Call a function and wait for Svelte to flush pending changes.
*
@@ -171,4 +129,4 @@ Object.keys(baseFireEvent).forEach((key) => {
}
})
-export { act, cleanup, fireEvent, render }
+export { act, fireEvent, render }