Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
max_line_length = off
trim_trailing_whitespace = false
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ jobs:
- name: 🚀 Release
run: |
pnpm \
--package="@anolilab/multi-semantic-release@2" \
--package="@anolilab/semantic-release-pnpm@2" \
--package="@anolilab/multi-semantic-release@3" \
--package="@anolilab/semantic-release-pnpm@3" \
--package="semantic-release@25" \
--package="conventional-changelog-conventionalcommits@9" \
dlx \
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"test:all": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest && pnpm test:examples",
"test:all:legacy": "pnpm test:vitest:jsdom && pnpm test:vitest:happy-dom && pnpm test:jest",
"build": "pnpm build:types && pnpm build:docs",
"build:types": "tsc --build && cp packages/svelte/src/core/types.d.ts packages/svelte/dist/core",
"build:docs": "remark --output --use remark-toc --use remark-code-import --use unified-prettier README.md examples && cp -f README.md packages/svelte",
"build:types": "tsc --build",
"build:docs": "remark --output --use remark-toc --use remark-code-import --use unified-prettier README.md packages/*/README.md examples && cp -f README.md packages/svelte",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"install:3": "./scripts/install-dependencies 3",
Expand Down
173 changes: 173 additions & 0 deletions packages/svelte-core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# @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

## Table of Contents

- [Example Usage](#example-usage)
- [API](#api)
- [`render`](#render)
- [`setup`](#setup)
- [`mount`](#mount)
- [`cleanup`](#cleanup)
- [`addCleanupTask`](#addcleanuptask)
- [`removeCleanupTask`](#removecleanuptask)
- [Utility types](#utility-types)

## Example Usage

```ts
import { beforeEach } from 'vitest'
import * as SvelteCore from '@testing-library/svelte-core'

import { bindQueries, type Screen } from './bring-your-own-queries.js'

beforeEach(() => {
SvelteCore.cleanup()
})

export interface RenderResult<
C extends SvelteCore.Component,
> extends SvelteCore.RenderResult<C> {
screen: Screen
}

export const render = <C extends SvelteCore.Component>(
Component: SvelteCore.ComponentImport<C>,
options: SvelteCore.ComponentOptions<C>
): RenderResult<C> => {
const renderResult = SvelteCore.render(Component, options)
const screen = bindQueries(baseElement)

return { screen, ...renderResult }
}
```

## API

### `render`

Set up the document and mount a component into that document.

```ts
const { baseElement, container, component, unmount, rerender } = render(
Component,
componentOptions,
setupOptions
)
```

| Argument | Type | Description |
| ------------------ | ------------------------------------------------------- | --------------------------------------------- |
| `Component` | [Svelte component][svelte-component-docs] | An imported Svelte component |
| `componentOptions` | `Props` or partial [`mount` options][svelte-mount-docs] | Options for how the component will be mounted |
| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` |

| Result | Type | Description | Default |
| ------------- | ------------------------------------------ | ---------------------------------------- | ----------------------------------- |
| `baseElement` | `HTMLElement` | The base element | `document.body` |
| `container` | `HTMLElement` | The component's immediate parent element | `<div>` appended to `document.body` |
| `component` | [component exports][svelte-mount-docs] | The component's exports from `mount` | N/A |
| `rerender` | `(props: Partial<Props>) => Promise<void>` | Update the component's props | N/A |
| `unmount` | `() => void` | Unmount the component from the document | N/A |

> \[!TIP]
> Calling `render` is equivalent to calling `setup` followed by `mount`
>
> ```ts
> const { baseElement, container, mountOptions } = setup(
> componentOptions,
> setupOptions
> )
> const { component, rerender, unmount } = mount(Component, mountOptions)
> ```

[svelte-component-docs]: https://svelte.dev/docs/svelte-components
[svelte-mount-docs]: https://svelte.dev/docs/svelte/imperative-component-api#mount

### `setup`

Validate options and prepare document elements for rendering.

```ts
const { baseElement, target, mountOptions } = setup(options, renderOptions)
```

| Argument | Type | Description |
| ------------------ | ------------------------------------------------------- | --------------------------------------------- |
| `componentOptions` | `Props` or partial [`mount` options][svelte-mount-docs] | Options for how the component will be mounted |
| `setupOptions` | `{ baseElement?: HTMLElement }` | Optionally override `baseElement` |

| Result | Type | Description | Default |
| -------------- | ------------------------------------ | ---------------------------------------- | ----------------------------------- |
| `baseElement` | `HTMLElement` | The base element | `document.body` |
| `container` | `HTMLElement` | The component's immediate parent element | `<div>` appended to `document.body` |
| `mountOptions` | [`mount` options][svelte-mount-docs] | Validated options to pass to `mount` | `{ target, props: {} }` |

### `mount`

Mount a Svelte component into the document.

```ts
const { component, unmount, rerender } = mount(Component, options)
```

| Argument | Type | Description |
| -------------- | ----------------------------------------- | -------------------------------------------- |
| `Component` | [Svelte component][svelte-component-docs] | An imported Svelte component |
| `mountOptions` | [component options][svelte-mount-docs] | Options to pass to Svelte's `mount` function |

| Result | Type | Description |
| ----------- | ------------------------------------------ | --------------------------------------- |
| `component` | [component exports][svelte-mount-docs] | The component's exports from `mount` |
| `unmount` | `() => void` | Unmount the component from the document |
| `rerender` | `(props: Partial<Props>) => Promise<void>` | Update the component's props |

### `cleanup`

Cleanup rendered components and added elements. Call this when your tests are
over.

```ts
cleanup()
```

### `addCleanupTask`

Add a custom cleanup task to be called with `cleanup()`

```ts
addCleanupTask(() => {
// ...reset something
})
```

### `removeCleanupTask`

Remove a cleanup task from `cleanup()`. Useful if a cleanup task can only be run
once and may be run outside of `cleanup`

```ts
const customCleanup = () => {
// ...reset something
}

addCleanupTask(customCleanup)

const manuallyCleanupEarly = () => {
customCleanup()
removeCleanupTask(customCleanup)
}
```

### Utility types

This module exports various utility types from
`@testing-library/svelte-core/types`. They adapt to whatever Svelte version is
installed, and can be used to get type signatures for imported components,
props, exports, etc.

See [`./types.d.ts`](./types.d.ts) for the full list of available types.
53 changes: 53 additions & 0 deletions packages/svelte-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@testing-library/svelte-core",
"version": "0.0.0-semantically-released",
"description": "Core rendering and cleanup logic for Svelte testing utilities.",
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./src/index.js",
"default": "./src/index.js"
},
"./types": {
"types": "./types.d.ts"
}
},
"type": "module",
"license": "MIT",
"homepage": "https://github.com/testing-library/svelte-testing-library#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/testing-library/svelte-testing-library.git",
"directory": "packages/svelte-core"
},
"bugs": {
"url": "https://github.com/testing-library/svelte-testing-library/issues"
},
"engines": {
"node": ">=16"
},
"keywords": [
"testing",
"svelte",
"ui",
"dom",
"jsdom",
"unit",
"integration",
"functional",
"end-to-end",
"e2e"
],
"files": [
"dist",
"src",
"types.d.ts"
],
"peerDependencies": {
"svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0"
},
"publishConfig": {
"access": "public",
"provenance": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
*/
export * from './cleanup.js'
export * from './mount.js'
export * from './render.js'
export * from './setup.js'
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import { addCleanupTask, removeCleanupTask } from './cleanup.js'
import { createProps } from './props.svelte.js'
import { IS_MODERN_SVELTE } from './svelte-version.js'

/** Mount a modern Svelte 5 component into the DOM. */
/**
* Mount a modern Svelte 5 component into the DOM.
*
* @template {import('../types.js').Component} C
* @param {import('../types.js').ComponentType<C>} Component
* @param {import('../types.js').MountOptions<C>} options
* @returns {import('../types.js').MountResult<C>}
*/
const mountModern = (Component, options) => {
const [props, updateProps] = createProps(options.props)
const component = Svelte.mount(Component, { ...options, props })
Expand All @@ -29,7 +36,14 @@ const mountModern = (Component, options) => {
return { component, unmount, rerender }
}

/** Mount a legacy Svelte 3 or 4 component into the DOM. */
/**
* Mount a legacy Svelte 3 or 4 component into the DOM.
*
* @template {import('../types.js').LegacyComponent} C
* @param {import('../types.js').ComponentType<C>} Component
* @param {import('../types.js').MountOptions<C>} options
* @returns {import('../types.js').MountResult<C>}
*/
const mountLegacy = (Component, options) => {
const component = new Component(options)

Expand Down Expand Up @@ -62,24 +76,30 @@ const mountComponent = IS_MODERN_SVELTE ? mountModern : mountLegacy
/**
* Render a Svelte component into the document.
*
* @template {import('./types.js').Component} C
* @param {import('./types.js').ComponentType<C>} Component
* @param {import('./types.js').MountOptions<C>} options
* @returns {{
* component: C
* unmount: () => void
* rerender: (props: Partial<import('./types.js').Props<C>>) => Promise<void>
* }}
* @template {import('../types.js').Component} C
* @param {import('../types.js').ComponentImport<C>} Component
* @param {import('../types.js').MountOptions<C>} options
* @returns {import('../types.js').MountResult<C>}
*/
const mount = (Component, options) => {
const { component, unmount, rerender } = mountComponent(Component, options)
const { component, unmount, rerender } = mountComponent(
'default' in Component ? Component.default : Component,
options
)

return {
component,
unmount,
rerender: async (props) => {
if ('props' in props) {
console.warn(
'rerender({ props: { ... } }) deprecated, use rerender({ ... }) instead'
)
props = props.props
}

rerender(props)
// Await the next tick for Svelte 4, which cannot flush changes synchronously
// Await the next tick for Svelte 3/4, which cannot flush changes synchronously
await Svelte.tick()
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
* @param {Props} initialProps
* @returns {[Props, (nextProps: Partial<Props>) => void]}
*/
const createProps = (initialProps) => {
const targetProps = initialProps ?? {}
let currentProps = $state.raw(targetProps)
const createProps = (initialProps = {}) => {
let currentProps = $state.raw(initialProps)

const props = new Proxy(targetProps, {
const props = new Proxy(initialProps, {
get(_, key) {
return currentProps[key]
},
Expand Down
21 changes: 21 additions & 0 deletions packages/svelte-core/src/render.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { mount } from './mount.js'
import { setup } from './setup.js'

/**
* Render a component into the document.
*
* @template {import('../types.js').Component} C
*
* @param {import('../types.js').ComponentImport<C>} Component - The component to render.
* @param {import('../types.js').ComponentOptions<C>} componentOptions - Customize how Svelte renders the component.
* @param {import('../types.js').SetupOptions<C>} setupOptions - Customize how the document is set up.
* @returns {import('../types.js').RenderResult<C>} The rendered component.
*/
const render = (Component, componentOptions, setupOptions = {}) => {
const { mountOptions, ...setupResult } = setup(componentOptions, setupOptions)
const mountResult = mount(Component, mountOptions)

return { ...setupResult, ...mountResult }
}

export { render }
Loading