├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── README.md ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── scripts ├── publish-ci.ts └── release.ts ├── src ├── core │ ├── index.js │ ├── legacy.js │ ├── modern.svelte.js │ └── validate-options.js ├── index.js └── pure.js ├── test ├── __snapshots__ │ └── render.test.tsx.snap ├── fixtures │ ├── Counter.svelte │ └── HelloWorld.svelte └── render.test.tsx ├── tsconfig.json ├── tsup.config.ts ├── types ├── index.d.ts └── pure.d.ts └── vitest.config.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: ci-${{ github.event.pull_request.number || github.ref }} 16 | cancel-in-progress: true 17 | 18 | env: 19 | PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.cache/ms-playwright 20 | 21 | jobs: 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Install pnpm 28 | uses: pnpm/action-setup@v4 29 | 30 | - name: Set node version to ${{ inputs.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ inputs.node-version }} 34 | 35 | - name: Install 36 | run: pnpm i 37 | 38 | - name: Lint 39 | run: pnpm run lint 40 | 41 | test: 42 | runs-on: ${{ matrix.os }} 43 | 44 | timeout-minutes: 30 45 | 46 | strategy: 47 | matrix: 48 | os: [ubuntu-latest] 49 | node_version: [22] 50 | include: 51 | - os: macos-latest 52 | node_version: 22 53 | - os: windows-latest 54 | node_version: 22 55 | fail-fast: false 56 | 57 | steps: 58 | - uses: actions/checkout@v4 59 | 60 | - name: Install pnpm 61 | uses: pnpm/action-setup@v4 62 | 63 | - name: Set node version to ${{ inputs.node-version }} 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: ${{ inputs.node-version }} 67 | 68 | - name: Install 69 | run: pnpm i 70 | 71 | - name: Install Playwright Dependencies 72 | run: pnpm exec playwright install chromium --with-deps 73 | 74 | - name: Test 75 | run: pnpm run test 76 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | id-token: write 11 | 12 | jobs: 13 | publish: 14 | if: github.repository == 'vitest-dev/vitest-browser-svelte' 15 | runs-on: ubuntu-latest 16 | environment: Release 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Install pnpm 23 | uses: pnpm/action-setup@v4 24 | 25 | - name: Set node version to 20 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | registry-url: https://registry.npmjs.org/ 30 | cache: pnpm 31 | 32 | - name: Install 33 | run: pnpm install --frozen-lockfile --prefer-offline 34 | env: 35 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' 36 | 37 | - name: Publish to npm 38 | run: pnpm run publish-ci ${{ github.ref_name }} 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | 42 | - name: Generate Changelog 43 | run: npx changelogithub 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | lib-cov 7 | coverage 8 | !**/integrations/coverage 9 | node_modules 10 | .env 11 | .cache 12 | dist 13 | .idea 14 | .vite-node 15 | ltex* 16 | .DS_Store 17 | bench/test/*/*/ 18 | **/bench.json 19 | **/browser/browser.json 20 | docs/public/user-avatars 21 | docs/public/sponsors 22 | .eslintcache 23 | docs/.vitepress/cache/ 24 | !test/cli/fixtures/dotted-files/**/.cache 25 | test/**/__screenshots__/**/* 26 | test/browser/fixtures/update-snapshot/basic.test.ts 27 | .vitest-reports 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vitest-browser-svelte 2 | 3 | Render Svelte components in Vitest Browser Mode. This library follows `testing-library` principles and exposes only [locators](https://vitest.dev/guide/browser/locators) and utilities that encourage you to write tests that closely resemble how your Svelte components are used. 4 | 5 | Requires `vitest` and `@vitest/browser` 2.1.0 or higher. 6 | 7 | ```tsx 8 | import { render } from 'vitest-browser-svelte' 9 | import { expect, test } from 'vitest' 10 | import Component from './Component.svelte' 11 | 12 | test('counter button increments the count', async () => { 13 | const screen = render(Component, { 14 | initialCount: 1, 15 | }) 16 | 17 | await screen.getByRole('button', { name: 'Increment' }).click() 18 | 19 | await expect.element(screen.getByText('Count is 2')).toBeVisible() 20 | }) 21 | ``` 22 | 23 | > [!NOTE] 24 | > This library doesn't expose or use `act`. Instead, you should use Vitest's locators and `expect.element` API that have [retry-ability mechanism](https://vitest.dev/guide/browser/assertion-api) baked in. 25 | 26 | `vitest-browser-svelte` also automatically injects `render` and `cleanup` methods on the `page`. Example: 27 | 28 | ```ts 29 | // vitest.config.ts 30 | import { defineConfig } from 'vitest/config' 31 | 32 | export default defineConfig({ 33 | test: { 34 | // if the types are not picked up, add `vitest-browser-svelte` to 35 | // "compilerOptions.types" in your tsconfig or 36 | // import `vitest-browser-svelte` manually so TypeScript can pick it up 37 | setupFiles: ['vitest-browser-svelte'], 38 | browser: { 39 | name: 'chromium', 40 | enabled: true, 41 | }, 42 | }, 43 | }) 44 | ``` 45 | 46 | ```tsx 47 | import { page } from '@vitest/browser/context' 48 | import Component from './Component.svelte' 49 | 50 | test('counter button increments the count', async () => { 51 | const screen = page.render(Component, { 52 | initialCount: 1, 53 | }) 54 | 55 | screen.cleanup() 56 | }) 57 | ``` 58 | 59 | Unlike `@testing-library/svelte`, `vitest-browser-svelte` cleans up the component before the test starts instead of after, so you can see the rendered result in your UI. To avoid auto-cleanup, import the `render` function from `vitest-browser-svelte/pure`. 60 | 61 | ## Special thanks 62 | 63 | - Forked from [`@testing-library/svelte`](https://github.com/testing-library/svelte-testing-library) 64 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | { 5 | vue: false, 6 | // Disable tests rules because we need to test with various setup 7 | test: false, 8 | ignores: [ 9 | 'types/**/*.d.ts', 10 | ], 11 | }, 12 | { 13 | rules: { 14 | // prefer global Buffer to not initialize the whole module 15 | 'node/prefer-global/buffer': 'off', 16 | 'node/prefer-global/process': 'off', 17 | 'no-empty-pattern': 'off', 18 | 'antfu/indent-binary-ops': 'off', 19 | 'unused-imports/no-unused-imports': 'error', 20 | 'style/member-delimiter-style': [ 21 | 'error', 22 | { 23 | multiline: { delimiter: 'none' }, 24 | singleline: { delimiter: 'semi' }, 25 | }, 26 | ], 27 | // let TypeScript handle this 28 | 'no-undef': 'off', 29 | 'ts/no-invalid-this': 'off', 30 | 'eslint-comments/no-unlimited-disable': 'off', 31 | 'curly': ['error', 'all'], 32 | 33 | // TODO: migrate and turn it back on 34 | 'ts/ban-types': 'off', 35 | 'ts/no-unsafe-function-type': 'off', 36 | 37 | 'no-restricted-imports': [ 38 | 'error', 39 | { 40 | paths: ['path'], 41 | }, 42 | ], 43 | 44 | 'import/no-named-as-default': 'off', 45 | }, 46 | }, 47 | ) 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vitest-browser-svelte", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "packageManager": "pnpm@9.6.0", 6 | "description": "Render Svelte components in Vitest Browser Mode", 7 | "author": "Vitest Team", 8 | "license": "MIT", 9 | "funding": "https://opencollective.com/vitest", 10 | "homepage": "https://github.com/vitest-dev/vitest-browser-svelte#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vitest-dev/vitest-browser-svelte.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/vitest-dev/vitest-browser-svelte/issues" 17 | }, 18 | "keywords": [ 19 | "svelte", 20 | "vitest", 21 | "browser", 22 | "testing" 23 | ], 24 | "exports": { 25 | ".": { 26 | "types": "./types/index.d.ts", 27 | "default": "./src/index.js" 28 | }, 29 | "./pure": { 30 | "types": "./types/pure.d.ts", 31 | "default": "./src/pure.js" 32 | }, 33 | "./package.json": "./package.json" 34 | }, 35 | "main": "./src/index.js", 36 | "module": "./src/index.js", 37 | "types": "./types/index.d.ts", 38 | "files": [ 39 | "src", 40 | "types" 41 | ], 42 | "engines": { 43 | "node": "^18.0.0 || >=20.0.0" 44 | }, 45 | "scripts": { 46 | "test": "vitest", 47 | "publish-ci": "tsx scripts/publish-ci.ts", 48 | "release": "tsx scripts/release.ts", 49 | "lint": "eslint --cache .", 50 | "lint:fix": "pnpm lint --fix" 51 | }, 52 | "peerDependencies": { 53 | "@vitest/browser": "^2.1.0 || ^3.0.0-0", 54 | "svelte": ">3.0.0", 55 | "vitest": "^2.1.0 || ^3.0.0-0" 56 | }, 57 | "devDependencies": { 58 | "@antfu/eslint-config": "^2.24.1", 59 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 60 | "@vitest/browser": "^3.0.0-beta.4", 61 | "bumpp": "^9.4.2", 62 | "changelogithub": "^0.13.9", 63 | "eslint": "^9.8.0", 64 | "playwright": "^1.46.0", 65 | "svelte": "^5.17.3", 66 | "tsup": "^8.2.4", 67 | "tsx": "^4.17.0", 68 | "typescript": "^5.5.4", 69 | "vite": "6", 70 | "vitest": "^3.0.0-beta.4", 71 | "zx": "^8.1.4" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scripts/publish-ci.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | import { readFileSync } from 'node:fs' 4 | import { fileURLToPath } from 'node:url' 5 | import { $ } from 'zx' 6 | 7 | let version = process.argv[2] 8 | 9 | if (!version) { 10 | throw new Error('No tag specified') 11 | } 12 | 13 | if (version.startsWith('v')) { 14 | version = version.slice(1) 15 | } 16 | 17 | const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url)) 18 | const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) 19 | 20 | if (pkg.version !== version) { 21 | throw new Error( 22 | `Package version from tag "${version}" mismatches with the current version "${pkg.version}"`, 23 | ) 24 | } 25 | 26 | const releaseTag = version.includes('beta') 27 | ? 'beta' 28 | : version.includes('alpha') 29 | ? 'alpha' 30 | : undefined 31 | 32 | console.log('Publishing version', version, 'with tag', releaseTag || 'latest') 33 | 34 | if (releaseTag) { 35 | await $`pnpm -r publish --access public --no-git-checks --tag ${releaseTag}` 36 | } 37 | else { 38 | await $`pnpm -r publish --access public --no-git-checks` 39 | } 40 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | import { versionBump } from 'bumpp' 4 | import { $ } from 'zx' 5 | 6 | try { 7 | console.log('Bumping versions in packages:', './package.json', '\n') 8 | 9 | const result = await versionBump({ 10 | files: ['./package.json'], 11 | commit: true, 12 | push: true, 13 | tag: true, 14 | }) 15 | 16 | if (!result.newVersion.includes('beta')) { 17 | console.log('Pushing to release branch') 18 | await $`git update-ref refs/heads/release refs/heads/main` 19 | await $`git push origin release` 20 | } 21 | console.log('New release is ready, waiting for conformation at https://github.com/vitest-dev/vitest-browser-svelte/actions') 22 | } 23 | catch (err) { 24 | console.error(err) 25 | } 26 | -------------------------------------------------------------------------------- /src/core/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rendering core for svelte-testing-library. 3 | * 4 | * Defines how components are added to and removed from the DOM. 5 | * Will switch to legacy, class-based mounting logic 6 | * if it looks like we're in a Svelte <= 4 environment. 7 | */ 8 | import * as LegacyCore from './legacy.js' 9 | import * as ModernCore from './modern.svelte.js' 10 | import { 11 | UnknownSvelteOptionsError, 12 | createValidateOptions, 13 | } from './validate-options.js' 14 | 15 | const { mount, unmount, updateProps, allowedOptions } 16 | = ModernCore.IS_MODERN_SVELTE ? ModernCore : LegacyCore 17 | 18 | /** Validate component options. */ 19 | const validateOptions = createValidateOptions(allowedOptions) 20 | 21 | export { 22 | mount, 23 | UnknownSvelteOptionsError, 24 | unmount, 25 | updateProps, 26 | validateOptions, 27 | } 28 | -------------------------------------------------------------------------------- /src/core/legacy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Legacy rendering core for svelte-testing-library. 3 | * 4 | * Supports Svelte <= 4. 5 | */ 6 | 7 | /** Allowed options for the component constructor. */ 8 | const allowedOptions = [ 9 | 'target', 10 | 'accessors', 11 | 'anchor', 12 | 'props', 13 | 'hydrate', 14 | 'intro', 15 | 'context', 16 | ] 17 | 18 | /** 19 | * Mount the component into the DOM. 20 | * 21 | * The `onDestroy` callback is included for strict backwards compatibility 22 | * with previous versions of this library. It's mostly unnecessary logic. 23 | */ 24 | function mount(Component, options, onDestroy) { 25 | const component = new Component(options) 26 | 27 | if (typeof onDestroy === 'function') { 28 | component.$$.on_destroy.push(() => { 29 | onDestroy(component) 30 | }) 31 | } 32 | 33 | return component 34 | } 35 | 36 | /** Remove the component from the DOM. */ 37 | function unmount(component) { 38 | component.$destroy() 39 | } 40 | 41 | /** Update the component's props. */ 42 | function updateProps(component, nextProps) { 43 | component.$set(nextProps) 44 | } 45 | 46 | export { allowedOptions, mount, unmount, updateProps } 47 | -------------------------------------------------------------------------------- /src/core/modern.svelte.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modern rendering core for svelte-testing-library. 3 | * 4 | * Supports Svelte >= 5. 5 | */ 6 | import * as Svelte from 'svelte' 7 | 8 | /** Props signals for each rendered component. */ 9 | const propsByComponent = new Map() 10 | 11 | /** Whether we're using Svelte >= 5. */ 12 | const IS_MODERN_SVELTE = typeof Svelte.mount === 'function' 13 | 14 | /** Allowed options to the `mount` call. */ 15 | const allowedOptions = [ 16 | 'target', 17 | 'anchor', 18 | 'props', 19 | 'events', 20 | 'context', 21 | 'intro', 22 | ] 23 | 24 | /** Mount the component into the DOM. */ 25 | function mount(Component, options) { 26 | const props = $state(options.props ?? {}) 27 | const component = Svelte.mount(Component, { ...options, props }) 28 | 29 | Svelte.flushSync() 30 | propsByComponent.set(component, props) 31 | 32 | return component 33 | } 34 | 35 | /** Remove the component from the DOM. */ 36 | function unmount(component) { 37 | propsByComponent.delete(component) 38 | Svelte.flushSync(() => Svelte.unmount(component)) 39 | } 40 | 41 | /** 42 | * Update the component's props. 43 | * 44 | * Relies on the `$state` signal added in `mount`. 45 | */ 46 | function updateProps(component, nextProps) { 47 | const prevProps = propsByComponent.get(component) 48 | Object.assign(prevProps, nextProps) 49 | } 50 | 51 | export { allowedOptions, IS_MODERN_SVELTE, mount, unmount, updateProps } 52 | -------------------------------------------------------------------------------- /src/core/validate-options.js: -------------------------------------------------------------------------------- 1 | class UnknownSvelteOptionsError extends TypeError { 2 | constructor(unknownOptions, allowedOptions) { 3 | super(`Unknown options. 4 | 5 | Unknown: [ ${unknownOptions.join(', ')} ] 6 | Allowed: [ ${allowedOptions.join(', ')} ] 7 | 8 | To pass both Svelte options and props to a component, 9 | or to use props that share a name with a Svelte option, 10 | you must place all your props under the \`props\` key: 11 | 12 | render(Component, { props: { /** props here **/ } }) 13 | `) 14 | this.name = 'UnknownSvelteOptionsError' 15 | } 16 | } 17 | 18 | function createValidateOptions(allowedOptions) { 19 | return (options) => { 20 | const isProps = !Object.keys(options).some(option => 21 | allowedOptions.includes(option), 22 | ) 23 | 24 | if (isProps) { 25 | return { props: options } 26 | } 27 | 28 | // Check if any props and Svelte options were accidentally mixed. 29 | const unknownOptions = Object.keys(options).filter( 30 | option => !allowedOptions.includes(option), 31 | ) 32 | 33 | if (unknownOptions.length > 0) { 34 | throw new UnknownSvelteOptionsError(unknownOptions, allowedOptions) 35 | } 36 | 37 | return options 38 | } 39 | } 40 | 41 | export { createValidateOptions, UnknownSvelteOptionsError } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { page } from '@vitest/browser/context' 2 | import { beforeEach } from 'vitest' 3 | import { cleanup, render } from './pure' 4 | 5 | export { render, cleanup } from './pure' 6 | 7 | page.extend({ 8 | render, 9 | [Symbol.for('vitest:component-cleanup')]: cleanup, 10 | }) 11 | 12 | beforeEach(() => { 13 | cleanup() 14 | }) 15 | -------------------------------------------------------------------------------- /src/pure.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { tick } from 'svelte' 4 | import { debug, getElementLocatorSelectors } from '@vitest/browser/utils' 5 | 6 | import { mount, unmount, updateProps, validateOptions } from './core/index.js' 7 | 8 | /** 9 | * @type {Set} 10 | */ 11 | const targetCache = new Set() 12 | /** 13 | * @type {Set} 14 | */ 15 | const componentCache = new Set() 16 | 17 | /** 18 | * Customize how Svelte renders the component. 19 | * 20 | * @template {import('svelte').SvelteComponent} C 21 | * @typedef {import('svelte').ComponentProps | Partial>>} SvelteComponentOptions 22 | */ 23 | 24 | /** 25 | * Customize how Testing Library sets up the document and binds queries. 26 | * 27 | * @typedef {{ 28 | * baseElement?: HTMLElement 29 | * }} RenderOptions 30 | */ 31 | 32 | /** 33 | * The rendered component and bound testing functions. 34 | * 35 | * @template {import('svelte').SvelteComponent} C 36 | * 37 | * @typedef {{ 38 | * container: HTMLElement 39 | * baseElement: HTMLElement 40 | * component: C 41 | * debug: (el?: HTMLElement | DocumentFragment) => void 42 | * rerender: (props: Partial>) => Promise 43 | * unmount: () => void 44 | * } & import('@vitest/browser/context').LocatorSelectors} RenderResult 45 | */ 46 | 47 | /** 48 | * Render a component into the document. 49 | * 50 | * @template {import('svelte').SvelteComponent} C 51 | * 52 | * @param {import('svelte').ComponentType} Component - The component to render. 53 | * @param {SvelteComponentOptions} options - Customize how Svelte renders the component. 54 | * @param {RenderOptions} renderOptions - Customize how Testing Library sets up the document and binds queries. 55 | * @returns {RenderResult} The rendered component and bound testing functions. 56 | */ 57 | function render(Component, options = {}, renderOptions = {}) { 58 | options = validateOptions(options) 59 | 60 | const baseElement 61 | = renderOptions.baseElement ?? options.target ?? document.body 62 | 63 | const queries = getElementLocatorSelectors(baseElement) 64 | 65 | const target 66 | = options.target ?? baseElement.appendChild(document.createElement('div')) 67 | 68 | targetCache.add(target) 69 | 70 | const component = mount( 71 | 'default' in Component ? Component.default : Component, 72 | { ...options, target }, 73 | cleanupComponent, 74 | ) 75 | 76 | componentCache.add(component) 77 | 78 | return { 79 | baseElement, 80 | component, 81 | container: target, 82 | debug: (el = baseElement) => { 83 | debug(el) 84 | }, 85 | rerender: async (props) => { 86 | if (props.props) { 87 | console.warn( 88 | 'rerender({ props: {...} }) deprecated, use rerender({...}) instead', 89 | ) 90 | props = props.props 91 | } 92 | 93 | updateProps(component, props) 94 | await tick() 95 | }, 96 | unmount: () => { 97 | cleanupComponent(component) 98 | }, 99 | ...queries, 100 | } 101 | } 102 | 103 | /** 104 | * Remove a component from the component cache. 105 | * @param {import('svelte').SvelteComponent} component 106 | */ 107 | function cleanupComponent(component) { 108 | const inCache = componentCache.delete(component) 109 | 110 | if (inCache) { 111 | unmount(component) 112 | } 113 | } 114 | 115 | /** 116 | * Remove a target element from the target cache 117 | * @param {Element} target 118 | */ 119 | function cleanupTarget(target) { 120 | const inCache = targetCache.delete(target) 121 | 122 | if (inCache && target.parentNode === document.body) { 123 | document.body.removeChild(target) 124 | } 125 | } 126 | 127 | /** Unmount all components and remove elements added to ``. */ 128 | function cleanup() { 129 | componentCache.forEach(cleanupComponent) 130 | targetCache.forEach(cleanupTarget) 131 | } 132 | 133 | export { cleanup, render } 134 | -------------------------------------------------------------------------------- /test/__snapshots__/render.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`renders simple component 1`] = ` 4 |
5 |
6 | Hello World 7 |
8 | 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /test/fixtures/Counter.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | Count is {count} 13 |
14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/HelloWorld.svelte: -------------------------------------------------------------------------------- 1 |
Hello World
2 | -------------------------------------------------------------------------------- /test/render.test.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { page } from '@vitest/browser/context' 3 | import { render } from 'vitest-browser-svelte' 4 | import HelloWorld from './fixtures/HelloWorld.svelte' 5 | import Counter from './fixtures/Counter.svelte' 6 | 7 | test('renders simple component', async () => { 8 | const screen = render(HelloWorld) 9 | await expect.element(page.getByText('Hello World')).toBeVisible() 10 | expect(screen.container).toMatchSnapshot() 11 | }) 12 | 13 | test('renders counter', async () => { 14 | const screen = render(Counter, { 15 | initialCount: 1, 16 | }) 17 | 18 | await expect.element(screen.getByText('Count is 1')).toBeVisible() 19 | await screen.getByRole('button', { name: 'Increment' }).click() 20 | await expect.element(screen.getByText('Count is 2')).toBeVisible() 21 | }) 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "types": ["svelte", "@vitest/browser/providers/playwright"], 5 | "allowJs": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "skipLibCheck": true 9 | }, 10 | "include": ["src", "test"] 11 | } 12 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['./src/index.ts', './src/pure.ts'], 5 | format: ['esm'], 6 | dts: true, 7 | }) 8 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './pure.js'; -------------------------------------------------------------------------------- /types/pure.d.ts: -------------------------------------------------------------------------------- 1 | import { LocatorSelectors } from '@vitest/browser/context' 2 | 3 | import type { 4 | Component as ModernComponent, 5 | ComponentConstructorOptions as LegacyConstructorOptions, 6 | ComponentProps, 7 | EventDispatcher, 8 | mount, 9 | SvelteComponent as LegacyComponent, 10 | SvelteComponentTyped as Svelte3LegacyComponent, 11 | } from 'svelte' 12 | 13 | type IS_MODERN_SVELTE = ModernComponent extends (...args: any[]) => any 14 | ? true 15 | : false; 16 | 17 | type IS_LEGACY_SVELTE_4 = 18 | EventDispatcher extends (...args: any[]) => any ? true : false; 19 | 20 | /** A compiled, imported Svelte component. */ 21 | export type Component< 22 | P extends Record = any, 23 | E extends Record = any, 24 | > = IS_MODERN_SVELTE extends true 25 | ? ModernComponent | LegacyComponent

26 | : IS_LEGACY_SVELTE_4 extends true 27 | ? LegacyComponent

28 | : Svelte3LegacyComponent

; 29 | 30 | /** 31 | * The type of an imported, compiled Svelte component. 32 | * 33 | * In Svelte 5, this distinction no longer matters. 34 | * In Svelte 4, this is the Svelte component class constructor. 35 | */ 36 | export type ComponentType = C extends LegacyComponent 37 | ? new (...args: any[]) => C 38 | : C; 39 | 40 | /** The props of a component. */ 41 | export type Props = ComponentProps; 42 | 43 | /** 44 | * The exported fields of a component. 45 | * 46 | * In Svelte 5, this is the set of variables marked as `export`'d. 47 | * In Svelte 4, this is simply the instance of the component class. 48 | */ 49 | export type Exports = C extends LegacyComponent 50 | ? C 51 | : C extends ModernComponent 52 | ? E 53 | : never; 54 | 55 | /** 56 | * Options that may be passed to `mount` when rendering the component. 57 | * 58 | * In Svelte 4, these are the options passed to the component constructor. 59 | */ 60 | export type MountOptions = C extends LegacyComponent 61 | ? LegacyConstructorOptions> 62 | : Parameters, Exports>>[1]; 63 | 64 | /** 65 | * Customize how Svelte renders the component. 66 | */ 67 | export type SvelteComponentOptions = Props | MountOptions; 68 | /** 69 | * Customize how Testing Library sets up the document and binds queries. 70 | */ 71 | export type RenderOptions = { 72 | baseElement?: HTMLElement; 73 | }; 74 | /** 75 | * The rendered component and bound testing functions. 76 | */ 77 | export interface RenderResult extends LocatorSelectors { 78 | container: HTMLElement; 79 | baseElement: HTMLElement; 80 | component: C; 81 | debug: (el?: HTMLElement | DocumentFragment) => void; 82 | rerender: (props: Partial>) => Promise; 83 | unmount: () => void; 84 | } 85 | /** Unmount all components and remove elements added to ``. */ 86 | export function cleanup(): void; 87 | /** 88 | * Render a component into the document. 89 | */ 90 | export function render(Component: ComponentType, options?: SvelteComponentOptions, renderOptions?: RenderOptions): RenderResult; 91 | 92 | declare module '@vitest/browser/context' { 93 | interface BrowserPage { 94 | render: typeof render 95 | } 96 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | 4 | export default defineConfig({ 5 | plugins: [svelte()], 6 | test: { 7 | name: 'react', 8 | browser: { 9 | enabled: true, 10 | headless: true, 11 | name: 'chromium', 12 | provider: 'playwright', 13 | }, 14 | }, 15 | }) 16 | --------------------------------------------------------------------------------