├── .all-contributorsrc ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── lint-pr.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.js ├── jest.config.js ├── other ├── chipmunk.png └── versions.txt ├── package.json ├── prettier.config.js ├── release.config.js ├── scripts ├── install-dependencies └── preview-release ├── src ├── component-types.d.ts ├── core │ ├── index.js │ ├── legacy.js │ ├── modern.svelte.js │ └── validate-options.js ├── index.js ├── pure.js ├── vite.js └── vitest.js ├── svelte.config.js ├── tests ├── _env.js ├── _jest-setup.js ├── _jest-vitest-alias.js ├── _vitest-setup.js ├── act.test.js ├── auto-cleanup.test.js ├── cleanup.test.js ├── context.test.js ├── debug.test.js ├── envs │ ├── svelte3 │ │ ├── node16 │ │ │ └── package.json │ │ └── package.json │ └── svelte4 │ │ ├── node16 │ │ └── package.json │ │ └── package.json ├── events.test.js ├── fixtures │ ├── Comp.svelte │ ├── CompRunes.svelte │ ├── Context.svelte │ ├── Mounter.svelte │ ├── Transitioner.svelte │ ├── Typed.svelte │ └── TypedRunes.svelte ├── mount.test.js ├── multi-base.test.js ├── render-runes.test-d.ts ├── render-utilities.test-d.ts ├── render.test-d.ts ├── render.test.js ├── rerender.test.js ├── transition.test.js └── vite-plugin.test.js ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.legacy.json └── vite.config.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "svelte-testing-library", 3 | "projectOwner": "testing-library", 4 | "repoType": "github", 5 | "files": ["README.md"], 6 | "imageSize": 100, 7 | "commit": false, 8 | "contributors": [ 9 | { 10 | "login": "benmonro", 11 | "name": "Ben Monro", 12 | "avatar_url": "https://avatars3.githubusercontent.com/u/399236?v=4", 13 | "profile": "https://github.com/benmonro", 14 | "contributions": ["code", "test", "ideas", "doc"] 15 | }, 16 | { 17 | "login": "EmilTholin", 18 | "name": "Emil Tholin", 19 | "avatar_url": "https://avatars0.githubusercontent.com/u/11573167?v=4", 20 | "profile": "https://twitter.com/EmilTholin", 21 | "contributions": ["code", "test", "ideas"] 22 | }, 23 | { 24 | "login": "oieduardorabelo", 25 | "name": "Eduardo Rabelo", 26 | "avatar_url": "https://avatars1.githubusercontent.com/u/829902?v=4", 27 | "profile": "https://medium.com/@oieduardorabelo", 28 | "contributions": ["test", "code", "doc", "example"] 29 | }, 30 | { 31 | "login": "timdeschryver", 32 | "name": "Tim Deschryver", 33 | "avatar_url": "https://avatars1.githubusercontent.com/u/28659384?v=4", 34 | "profile": "http://timdeschryver.dev", 35 | "contributions": ["doc"] 36 | }, 37 | { 38 | "login": "ematipico", 39 | "name": "Emanuele", 40 | "avatar_url": "https://avatars3.githubusercontent.com/u/602478?v=4", 41 | "profile": "http://www.ematipico.com", 42 | "contributions": ["code", "test", "doc"] 43 | }, 44 | { 45 | "login": "pngwn", 46 | "name": "pngwn", 47 | "avatar_url": "https://avatars1.githubusercontent.com/u/12937446?v=4", 48 | "profile": "https://github.com/pngwn", 49 | "contributions": ["code", "test"] 50 | }, 51 | { 52 | "login": "eps1lon", 53 | "name": "Sebastian Silbermann", 54 | "avatar_url": "https://avatars3.githubusercontent.com/u/12292047?v=4", 55 | "profile": "https://twitter.com/sebsilbermann", 56 | "contributions": ["code"] 57 | }, 58 | { 59 | "login": "mihar-22", 60 | "name": "Rahim Alwer", 61 | "avatar_url": "https://avatars3.githubusercontent.com/u/14304599?s=460&v=4", 62 | "profile": "https://github.com/mihar-22", 63 | "contributions": ["code", "doc", "test", "review"] 64 | }, 65 | { 66 | "login": "MirrorBytes", 67 | "name": "Bob", 68 | "avatar_url": "https://avatars3.githubusercontent.com/u/22119469?v=4", 69 | "profile": "https://github.com/MirrorBytes", 70 | "contributions": ["bug", "code"] 71 | }, 72 | { 73 | "login": "ronmerkin", 74 | "name": "Ron Merkin", 75 | "avatar_url": "https://avatars.githubusercontent.com/u/17492527?v=4", 76 | "profile": "https://github.com/ronmerkin", 77 | "contributions": ["code"] 78 | }, 79 | { 80 | "login": "benmccann", 81 | "name": "Ben McCann", 82 | "avatar_url": "https://avatars.githubusercontent.com/u/322311?v=4", 83 | "profile": "http://www.benmccann.com", 84 | "contributions": ["test"] 85 | }, 86 | { 87 | "login": "jgbowser", 88 | "name": "John Bowser", 89 | "avatar_url": "https://avatars.githubusercontent.com/u/66637570?v=4", 90 | "profile": "https://johnbowser.dev/", 91 | "contributions": ["code", "test"] 92 | }, 93 | { 94 | "login": "ysitbon", 95 | "name": "Yoann", 96 | "avatar_url": "https://avatars.githubusercontent.com/u/1370679?v=4", 97 | "profile": "https://github.com/ysitbon", 98 | "contributions": ["code"] 99 | }, 100 | { 101 | "login": "yanick", 102 | "name": "Yanick Champoux", 103 | "avatar_url": "https://avatars.githubusercontent.com/u/19954?v=4", 104 | "profile": "https://techblog.babyl.ca/", 105 | "contributions": ["code"] 106 | }, 107 | { 108 | "login": "mcous", 109 | "name": "Michael Cousins", 110 | "avatar_url": "https://avatars.githubusercontent.com/u/2963448?v=4", 111 | "profile": "https://michael.cousins.io/", 112 | "contributions": ["code"] 113 | } 114 | ], 115 | "contributorsPerLine": 7, 116 | "repoHost": "https://github.com", 117 | "commitConvention": "none", 118 | "skipCi": true 119 | } 120 | 121 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | max_line_length = off 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Update npm dependencies 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'monthly' 8 | groups: 9 | lint: 10 | patterns: 11 | - '*eslint*' 12 | - '*prettier*' 13 | - '*typescript*' 14 | test: 15 | patterns: 16 | - '*svelte*' 17 | - '*testing-library*' 18 | - '*vite*' 19 | - '*vitest*' 20 | - '*jsdom*' 21 | - '*happy-dom*' 22 | - 'expect-type' 23 | development: 24 | dependency-type: 'development' 25 | 26 | # Update GitHub Actions dependencies 27 | - package-ecosystem: 'github-actions' 28 | directory: '/' 29 | schedule: 30 | interval: 'monthly' 31 | groups: 32 | actions: 33 | patterns: 34 | - '*' 35 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr.yml: -------------------------------------------------------------------------------- 1 | name: 'Lint PR' 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, next] 6 | pull_request: 7 | branches: [main, next] 8 | schedule: 9 | # Tuesdays at 14:45 UTC (10:45 EST) 10 | - cron: 45 14 * * 1 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | main: 18 | # ignore all-contributors PRs 19 | if: ${{ !contains(github.head_ref, 'all-contributors') }} 20 | name: Svelte ${{ matrix.svelte }}, Node ${{ matrix.node }}, ${{ matrix.check }} 21 | runs-on: ubuntu-latest 22 | 23 | # enable OIDC for codecov uploads 24 | permissions: 25 | id-token: write 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | node: ['16', '18', '20', '22'] 31 | svelte: ['3', '4', '5'] 32 | check: ['test:vitest:jsdom', 'test:vitest:happy-dom', 'test:jest'] 33 | exclude: 34 | # Don't run Svelte 3 on Node versions greater than 20 35 | - { svelte: '3', node: '22' } 36 | # Only run Svelte 5 on Node versions greater than or equal to 20 37 | - { svelte: '5', node: '16' } 38 | - { svelte: '5', node: '18' } 39 | include: 40 | # We only need to lint once, so do it on latest Node and Svelte 41 | - { svelte: '5', node: '22', check: 'lint' } 42 | # Run type checks in latest applicable Node 43 | - { svelte: '3', node: '20', check: 'types:legacy' } 44 | - { svelte: '4', node: '22', check: 'types:legacy' } 45 | - { svelte: '5', node: '22', check: 'types' } 46 | 47 | steps: 48 | - name: ⬇️ Checkout repo 49 | uses: actions/checkout@v4 50 | 51 | - name: ⎔ Setup node 52 | uses: actions/setup-node@v4 53 | with: 54 | node-version: ${{ matrix.node }} 55 | 56 | - name: 📥 Download deps 57 | run: npm run install:${{ matrix.svelte }} 58 | 59 | - name: ▶️ Run ${{ matrix.check }} 60 | run: npm run ${{ matrix.check }} 61 | 62 | - name: ⬆️ Upload coverage report 63 | if: ${{ startsWith(matrix.check, 'test:') }} 64 | uses: codecov/codecov-action@v5 65 | with: 66 | use_oidc: true 67 | fail_ci_if_error: true 68 | 69 | build: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: ⬇️ Checkout repo 73 | uses: actions/checkout@v4 74 | 75 | - name: ⎔ Setup node 76 | uses: actions/setup-node@v4 77 | with: 78 | node-version: 22 79 | 80 | - name: 📥 Download deps 81 | run: npm install 82 | 83 | - name: 🏗️ Build types 84 | run: npm run build 85 | 86 | - name: ⬆️ Upload types build 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: types 90 | path: types 91 | 92 | release: 93 | needs: [main, build] 94 | runs-on: ubuntu-latest 95 | if: ${{ github.repository == 'testing-library/svelte-testing-library' && 96 | contains('refs/heads/main,refs/heads/next', github.ref) && 97 | github.event_name == 'push' }} 98 | steps: 99 | - name: ⬇️ Checkout repo 100 | uses: actions/checkout@v4 101 | 102 | - name: ⎔ Setup node 103 | uses: actions/setup-node@v4 104 | with: 105 | node-version: 22 106 | 107 | - name: 📥 Downloads types build 108 | uses: actions/download-artifact@v4 109 | with: 110 | name: types 111 | path: types 112 | 113 | - name: 🚀 Release 114 | uses: cycjimmy/semantic-release-action@v4 115 | with: 116 | semantic_version: 24 117 | extra_plugins: | 118 | conventional-changelog-conventionalcommits@8 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | public/bundle.* 4 | coverage 5 | dist 6 | .idea 7 | *.tgz 8 | 9 | # These cause more harm than good when working with contributors 10 | yarn-error.log 11 | package-lock.json 12 | yarn.lock 13 | 14 | # generated typing output 15 | types 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | engine-strict=true 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .all-contributorsrc 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kent+coc@doddsfamily.us. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | ## Pull requests 4 | 5 | - Consider opening an issue before submitting a pull-request to avoid unnecessary work 6 | - Ensure pull request titles adhere to the [Conventional Commits][] specification 7 | 8 | [conventional commits]: https://www.conventionalcommits.org/ 9 | 10 | ## Release 11 | 12 | The module is released automatically from the `main` and `next` branches using [semantic-release-action][]. Version bumps and change logs are generated from the commit messages. 13 | 14 | [semantic-release-action]: https://github.com/cycjimmy/semantic-release-action 15 | 16 | ### Preview release 17 | 18 | If you would like to preview the release from a given branch, and... 19 | 20 | - You have push access to the repository 21 | - The branch exists in GitHub 22 | 23 | ...you can preview the next release version and changelog using: 24 | 25 | ```shell 26 | npm run preview-release 27 | ``` 28 | 29 | ## Development setup 30 | 31 | After cloning the repository, use the `setup` script to install dependencies and run all checks: 32 | 33 | ```shell 34 | npm run setup 35 | ``` 36 | 37 | ### Lint and format 38 | 39 | Run auto-formatting to ensure any changes adhere to the code style of the repository: 40 | 41 | ```shell 42 | npm run format 43 | ``` 44 | 45 | To run lint and format checks without making any changes: 46 | 47 | ```shell 48 | npm run lint 49 | ``` 50 | 51 | ### Test 52 | 53 | Run unit tests once or in watch mode: 54 | 55 | ```shell 56 | npm test 57 | npm run test:watch 58 | ``` 59 | 60 | ### Using different versions of Svelte 61 | 62 | Use the provided script to set up your environment for different versions of Svelte: 63 | 64 | ```shell 65 | # Svelte 5 66 | npm run install:5 67 | npm run all 68 | 69 | # Svelte 4 70 | npm run install:4 71 | npm run all:legacy 72 | 73 | # Svelte 3 74 | npm run install:3 75 | npm run all:legacy 76 | ``` 77 | 78 | ### Docs 79 | 80 | Use the `toc` script to ensure the README's table of contents is up to date: 81 | 82 | ```shell 83 | npm run toc 84 | ``` 85 | 86 | Use `contributors:add` to add a contributor to the README: 87 | 88 | ```shell 89 | npm run contributors:add 90 | ``` 91 | 92 | Use `contributors:generate` to ensure the README's contributor list is up to date: 93 | 94 | ```shell 95 | npm run contributors:generate 96 | ``` 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ben Monro (https://github.com/benmonro) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Svelte Testing Library

3 | 4 | 5 | chipmunk 11 | 12 | 13 |

Simple and complete Svelte testing utilities that encourage good testing practices.

14 | 15 | [**Read The Docs**][stl-docs] | [Edit the docs][stl-docs-repo] 16 | 17 | 18 | [![Build Status][build-badge]][build] 19 | [![Code Coverage][coverage-badge]][coverage] 20 | [![version][version-badge]][package] 21 | [![downloads][downloads-badge]][downloads] 22 | [![MIT License][license-badge]][license] 23 | 24 | [![All Contributors][contributors-badge]][contributors] 25 | [![PRs Welcome][prs-badge]][prs] 26 | [![Code of Conduct][coc-badge]][coc] 27 | [![Discord][discord-badge]][discord] 28 | 29 | [![Watch on GitHub][github-watch-badge]][github-watch] 30 | [![Star on GitHub][github-star-badge]][github-star] 31 | [![Tweet][twitter-badge]][twitter] 32 | 33 |
34 | 35 |
36 | 37 | [stl-docs]: https://testing-library.com/docs/svelte-testing-library/intro 38 | [stl-docs-repo]: https://github.com/testing-library/testing-library-docs 39 | [build-badge]: https://img.shields.io/github/actions/workflow/status/testing-library/svelte-testing-library/release.yml?style=flat-square 40 | [build]: https://github.com/testing-library/svelte-testing-library/actions 41 | [coverage-badge]: https://img.shields.io/codecov/c/github/testing-library/svelte-testing-library.svg?style=flat-square 42 | [coverage]: https://codecov.io/github/testing-library/svelte-testing-library 43 | [version-badge]: https://img.shields.io/npm/v/@testing-library/svelte.svg?style=flat-square 44 | [package]: https://www.npmjs.com/package/@testing-library/svelte 45 | [downloads-badge]: https://img.shields.io/npm/dm/@testing-library/svelte.svg?style=flat-square 46 | [downloads]: http://www.npmtrends.com/@testing-library/svelte 47 | [license-badge]: https://img.shields.io/github/license/testing-library/svelte-testing-library?color=b&style=flat-square 48 | [license]: https://github.com/testing-library/svelte-testing-library/blob/main/LICENSE 49 | [contributors-badge]: https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square 50 | [contributors]: #contributors 51 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 52 | [prs]: http://makeapullrequest.com 53 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 54 | [coc]: https://github.com/testing-library/svelte-testing-library/blob/main/CODE_OF_CONDUCT.md 55 | [discord-badge]: https://img.shields.io/discord/723559267868737556.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff&style=flat-square 56 | [discord]: https://discord.gg/testing-library 57 | [github-watch-badge]: https://img.shields.io/github/watchers/testing-library/svelte-testing-library.svg?style=social 58 | [github-watch]: https://github.com/testing-library/svelte-testing-library/watchers 59 | [github-star-badge]: https://img.shields.io/github/stars/testing-library/svelte-testing-library.svg?style=social 60 | [github-star]: https://github.com/testing-library/svelte-testing-library/stargazers 61 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20svelte-testing-library%20by%20%40@TestingLib%20https%3A%2F%2Fgithub.com%2Ftesting-library%2Fsvelte-testing-library%20%F0%9F%91%8D 62 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/testing-library/svelte-testing-library.svg?style=social 63 | 64 | ## Table of Contents 65 | 66 | 67 | 68 | 69 | - [The Problem](#the-problem) 70 | - [This Solution](#this-solution) 71 | - [Installation](#installation) 72 | - [Setup](#setup) 73 | - [Auto-cleanup](#auto-cleanup) 74 | - [Docs](#docs) 75 | - [Issues](#issues) 76 | - [🐛 Bugs](#-bugs) 77 | - [💡 Feature Requests](#-feature-requests) 78 | - [❓ Questions](#-questions) 79 | - [Contributors](#contributors) 80 | 81 | 82 | 83 | ## The Problem 84 | 85 | You want to write maintainable tests for your [Svelte][svelte] components. 86 | 87 | [svelte]: https://svelte.dev/ 88 | 89 | ## This Solution 90 | 91 | `@testing-library/svelte` is a lightweight library for testing Svelte 92 | components. It provides functions on top of `svelte` and 93 | `@testing-library/dom` so you can mount Svelte components and query their 94 | rendered output in the DOM. Its primary guiding principle is: 95 | 96 | > [The more your tests resemble the way your software is used, the more 97 | > confidence they can give you.][guiding-principle] 98 | 99 | [guiding-principle]: https://twitter.com/kentcdodds/status/977018512689455106 100 | 101 | ## Installation 102 | 103 | This module is distributed via [npm][npm] which is bundled with [node][node] and 104 | should be installed as one of your project's `devDependencies`: 105 | 106 | ```shell 107 | npm install --save-dev @testing-library/svelte 108 | ``` 109 | 110 | This library supports `svelte` versions `3`, `4`, and `5`. 111 | 112 | You may also be interested in installing `@testing-library/jest-dom` so you can 113 | use [the custom jest matchers][jest-dom]. 114 | 115 | [npm]: https://www.npmjs.com/ 116 | [node]: https://nodejs.org 117 | [jest-dom]: https://github.com/testing-library/jest-dom 118 | 119 | ## Setup 120 | 121 | We recommend using `@testing-library/svelte` with [Vitest][] as your test 122 | runner. To get started, add the `svelteTesting` plugin to your Vite or Vitest 123 | config. 124 | 125 | ```diff 126 | // vite.config.js 127 | import { svelte } from '@sveltejs/vite-plugin-svelte' 128 | + import { svelteTesting } from '@testing-library/svelte/vite' 129 | 130 | export default defineConfig({ 131 | plugins: [ 132 | svelte(), 133 | + svelteTesting(), 134 | ] 135 | }); 136 | ``` 137 | 138 | See the [setup docs][] for more detailed setup instructions, including for other 139 | test runners like Jest. 140 | 141 | [vitest]: https://vitest.dev/ 142 | [setup docs]: https://testing-library.com/docs/svelte-testing-library/setup 143 | 144 | ### Auto-cleanup 145 | 146 | In Vitest (via the `svelteTesting` plugin) and Jest (via the `beforeEach` and `afterEach` globals), 147 | this library will automatically setup and cleanup the test environment before and after each test. 148 | 149 | To do your own cleanup, or if you're using another framework, call the `setup` and `cleanup` functions yourself: 150 | 151 | ```js 152 | import { cleanup, render, setup } from '@testing-library/svelte' 153 | 154 | // before 155 | setup() 156 | 157 | // test 158 | render(/* ... */) 159 | 160 | // after 161 | cleanup() 162 | ``` 163 | 164 | To disable auto-cleanup in Vitest, set the `autoCleanup` option of the plugin to false: 165 | 166 | ```js 167 | svelteTesting({ autoCleanup: false }) 168 | ``` 169 | 170 | To disable auto-cleanup in Jest and other frameworks with global test hooks, 171 | set the `STL_SKIP_AUTO_CLEANUP` environment variable: 172 | 173 | ```shell 174 | STL_SKIP_AUTO_CLEANUP=1 jest 175 | ``` 176 | 177 | ## Docs 178 | 179 | See the [**docs**][stl-docs] over at the Testing Library website. 180 | 181 | ## Issues 182 | 183 | _Looking to contribute? Look for the [Good First Issue][good-first-issue] 184 | label._ 185 | 186 | [good-first-issue]: https://github.com/testing-library/svelte-testing-library/issues?utf8=✓&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc+label%3A"good+first+issue"+ 187 | 188 | ### 🐛 Bugs 189 | 190 | Please file an issue for bugs, missing documentation, or unexpected behavior. 191 | 192 | [**See Bugs**][bugs] 193 | 194 | [bugs]: https://github.com/testing-library/svelte-testing-library/issues?q=is%3Aissue+is%3Aopen+label%3Abug+sort%3Acreated-desc 195 | 196 | ### 💡 Feature Requests 197 | 198 | Please file an issue to suggest new features. Vote on feature requests by adding 199 | a 👍. This helps maintainers prioritize what to work on. 200 | 201 | [**See Feature Requests**][requests] 202 | 203 | [requests]: https://github.com/testing-library/svelte-testing-library/issues?q=is%3Aissue+sort%3Areactions-%2B1-desc+label%3Aenhancement+is%3Aopen 204 | 205 | ### ❓ Questions 206 | 207 | For questions related to using the library, please visit a support community 208 | instead of filing an issue on GitHub. 209 | 210 | - [Discord][discord] 211 | - [Stack Overflow][stackoverflow] 212 | 213 | [stackoverflow]: https://stackoverflow.com/questions/tagged/svelte-testing-library 214 | 215 | ## Contributors 216 | 217 | Thanks goes to these people ([emoji key][emojis]): 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 |
Ben Monro
Ben Monro

💻 ⚠️ 🤔 📖
Emil Tholin
Emil Tholin

💻 ⚠️ 🤔
Eduardo Rabelo
Eduardo Rabelo

⚠️ 💻 📖 💡
Tim Deschryver
Tim Deschryver

📖
Emanuele
Emanuele

💻 ⚠️ 📖
pngwn
pngwn

💻 ⚠️
Sebastian Silbermann
Sebastian Silbermann

💻
Rahim Alwer
Rahim Alwer

💻 📖 ⚠️ 👀
Bob
Bob

🐛 💻
Ron Merkin
Ron Merkin

💻
Ben McCann
Ben McCann

⚠️
John Bowser
John Bowser

💻 ⚠️
Yoann
Yoann

💻
Yanick Champoux
Yanick Champoux

💻
Michael Cousins
Michael Cousins

💻
247 | 248 | 249 | 250 | 251 | 252 | 253 | This project follows the [all-contributors][all-contributors] specification. 254 | Contributions of any kind welcome! 255 | 256 | [emojis]: https://github.com/all-contributors/all-contributors#emoji-key 257 | [all-contributors]: https://github.com/all-contributors/all-contributors 258 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import eslintPluginVitest from '@vitest/eslint-plugin' 3 | import eslintConfigPrettier from 'eslint-config-prettier' 4 | import eslintPluginJestDom from 'eslint-plugin-jest-dom' 5 | import eslintPluginPromise from 'eslint-plugin-promise' 6 | import eslintPluginSimpleImportSort from 'eslint-plugin-simple-import-sort' 7 | import eslintPluginSvelte from 'eslint-plugin-svelte' 8 | import eslintPluginTestingLibrary from 'eslint-plugin-testing-library' 9 | import eslintPluginUnicorn from 'eslint-plugin-unicorn' 10 | import globals from 'globals' 11 | import tseslint from 'typescript-eslint' 12 | 13 | export default tseslint.config( 14 | js.configs.recommended, 15 | tseslint.configs.strict, 16 | tseslint.configs.stylistic, 17 | eslintPluginUnicorn.configs['flat/recommended'], 18 | eslintPluginPromise.configs['flat/recommended'], 19 | eslintPluginSvelte.configs['flat/recommended'], 20 | eslintPluginSvelte.configs['flat/prettier'], 21 | eslintConfigPrettier, 22 | { 23 | name: 'settings', 24 | languageOptions: { 25 | ecmaVersion: 'latest', 26 | sourceType: 'module', 27 | parserOptions: { 28 | parser: tseslint.parser, 29 | extraFileExtensions: ['.svelte'], 30 | }, 31 | globals: { 32 | ...globals.browser, 33 | ...globals.node, 34 | ...globals.jest, 35 | }, 36 | }, 37 | }, 38 | { 39 | name: 'ignores', 40 | ignores: ['coverage', 'types'], 41 | }, 42 | { 43 | name: 'simple-import-sort', 44 | plugins: { 45 | 'simple-import-sort': eslintPluginSimpleImportSort, 46 | }, 47 | rules: { 48 | 'simple-import-sort/imports': 'error', 49 | 'simple-import-sort/exports': 'error', 50 | }, 51 | }, 52 | { 53 | name: 'tests', 54 | files: ['**/*.test.js'], 55 | extends: [ 56 | eslintPluginVitest.configs.recommended, 57 | eslintPluginJestDom.configs['flat/recommended'], 58 | eslintPluginTestingLibrary.configs['flat/dom'], 59 | ], 60 | rules: { 61 | 'testing-library/no-node-access': [ 62 | 'error', 63 | { allowContainerFirstChild: true }, 64 | ], 65 | }, 66 | }, 67 | { 68 | name: 'extras', 69 | rules: { 70 | 'unicorn/prevent-abbreviations': 'off', 71 | }, 72 | }, 73 | { 74 | name: 'svelte-extras', 75 | files: ['**/*.svelte'], 76 | rules: { 77 | 'svelte/no-unused-svelte-ignore': 'off', 78 | 'unicorn/filename-case': ['error', { case: 'pascalCase' }], 79 | 'unicorn/no-useless-undefined': 'off', 80 | }, 81 | }, 82 | { 83 | name: 'ts-extras', 84 | files: ['**/*.ts'], 85 | extends: [ 86 | tseslint.configs.strictTypeChecked, 87 | tseslint.configs.stylisticTypeChecked, 88 | ], 89 | languageOptions: { 90 | parserOptions: { 91 | projectService: true, 92 | }, 93 | }, 94 | } 95 | ) 96 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | import { VERSION as SVELTE_VERSION } from 'svelte/compiler' 2 | 3 | const SVELTE_TRANSFORM_PATTERN = 4 | SVELTE_VERSION >= '5' 5 | ? String.raw`^.+\.svelte(?:\.js)?$` 6 | : String.raw`^.+\.svelte$` 7 | 8 | export default { 9 | testMatch: ['/tests/**/*.test.js'], 10 | transform: { 11 | [SVELTE_TRANSFORM_PATTERN]: 'svelte-jester', 12 | }, 13 | moduleFileExtensions: ['js', 'svelte'], 14 | extensionsToTreatAsEsm: ['.svelte'], 15 | testEnvironment: 'jsdom', 16 | setupFilesAfterEnv: ['/tests/_jest-setup.js'], 17 | injectGlobals: true, 18 | moduleNameMapper: { 19 | '^vitest$': '/tests/_jest-vitest-alias.js', 20 | [String.raw`^@testing-library\/svelte$`]: '/src/index.js', 21 | }, 22 | resetMocks: true, 23 | restoreMocks: true, 24 | collectCoverageFrom: ['/src/**/*'], 25 | coveragePathIgnorePatterns: [ 26 | '/src/vite.js', 27 | '/src/vitest.js', 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /other/chipmunk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testing-library/svelte-testing-library/492dbd1d90f02bc3ded3448474fa6095df453557/other/chipmunk.png -------------------------------------------------------------------------------- /other/versions.txt: -------------------------------------------------------------------------------- 1 | for manual pushes 2 | 2.0.1 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@testing-library/svelte", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Simple and complete Svelte testing utilities that encourage good testing practices.", 5 | "main": "src/index.js", 6 | "exports": { 7 | ".": { 8 | "types": "./types/index.d.ts", 9 | "default": "./src/index.js" 10 | }, 11 | "./svelte5": { 12 | "types": "./types/index.d.ts", 13 | "default": "./src/index.js" 14 | }, 15 | "./vitest": { 16 | "types": "./types/vitest.d.ts", 17 | "default": "./src/vitest.js" 18 | }, 19 | "./vite": { 20 | "types": "./types/vite.d.ts", 21 | "default": "./src/vite.js" 22 | } 23 | }, 24 | "type": "module", 25 | "types": "types/index.d.ts", 26 | "license": "MIT", 27 | "homepage": "https://github.com/testing-library/svelte-testing-library#readme", 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/testing-library/svelte-testing-library.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/testing-library/svelte-testing-library/issues" 34 | }, 35 | "engines": { 36 | "node": ">= 10" 37 | }, 38 | "keywords": [ 39 | "testing", 40 | "svelte", 41 | "ui", 42 | "dom", 43 | "jsdom", 44 | "unit", 45 | "integration", 46 | "functional", 47 | "end-to-end", 48 | "e2e" 49 | ], 50 | "files": [ 51 | "src", 52 | "types" 53 | ], 54 | "scripts": { 55 | "all": "npm-run-all contributors:generate toc format types build test:vitest:* test:jest", 56 | "all:legacy": "npm-run-all types:legacy test:vitest:* test:jest", 57 | "toc": "doctoc README.md", 58 | "lint": "prettier . --check && eslint .", 59 | "format": "prettier . --write && eslint . --fix", 60 | "setup": "npm run install:5 && npm run all", 61 | "test": "vitest run --coverage", 62 | "test:watch": "vitest", 63 | "test:vitest:jsdom": "vitest run --coverage --environment jsdom", 64 | "test:vitest:happy-dom": "vitest run --coverage --environment happy-dom", 65 | "test:jest": "npx --node-options=\"--experimental-vm-modules --no-warnings\" jest --coverage", 66 | "types": "svelte-check", 67 | "types:legacy": "svelte-check --tsconfig tsconfig.legacy.json", 68 | "build": "tsc -p tsconfig.build.json && cp src/component-types.d.ts types", 69 | "contributors:add": "all-contributors add", 70 | "contributors:generate": "all-contributors generate", 71 | "preview-release": "./scripts/preview-release", 72 | "install:3": "./scripts/install-dependencies 3", 73 | "install:4": "./scripts/install-dependencies 4", 74 | "install:5": "./scripts/install-dependencies 5" 75 | }, 76 | "peerDependencies": { 77 | "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", 78 | "vite": "*", 79 | "vitest": "*" 80 | }, 81 | "peerDependenciesMeta": { 82 | "vite": { 83 | "optional": true 84 | }, 85 | "vitest": { 86 | "optional": true 87 | } 88 | }, 89 | "dependencies": { 90 | "@testing-library/dom": "9.x.x || 10.x.x" 91 | }, 92 | "devDependencies": { 93 | "@eslint/js": "^9.26.0", 94 | "@jest/globals": "^29.7.0", 95 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 96 | "@testing-library/jest-dom": "^6.6.3", 97 | "@testing-library/user-event": "^14.6.1", 98 | "@vitest/coverage-v8": "^3.1.3", 99 | "@vitest/eslint-plugin": "^1.1.44", 100 | "all-contributors-cli": "^6.26.1", 101 | "doctoc": "^2.2.1", 102 | "eslint": "^9.26.0", 103 | "eslint-config-prettier": "^10.1.5", 104 | "eslint-plugin-jest-dom": "^5.5.0", 105 | "eslint-plugin-promise": "^7.2.1", 106 | "eslint-plugin-simple-import-sort": "^12.1.1", 107 | "eslint-plugin-svelte": "^3.5.1", 108 | "eslint-plugin-testing-library": "^7.1.1", 109 | "eslint-plugin-unicorn": "^59.0.1", 110 | "expect-type": "^1.2.1", 111 | "globals": "^16.1.0", 112 | "happy-dom": "^17.4.6", 113 | "jest": "^29.7.0", 114 | "jest-environment-jsdom": "^29.7.0", 115 | "jsdom": "^26.1.0", 116 | "npm-run-all": "^4.1.5", 117 | "prettier": "^3.5.3", 118 | "prettier-plugin-svelte": "^3.3.3", 119 | "svelte": "^5.28.2", 120 | "svelte-check": "^4.1.7", 121 | "svelte-jester": "^5.0.0", 122 | "typescript": "^5.8.3", 123 | "typescript-eslint": "^8.32.0", 124 | "typescript-svelte-plugin": "^0.3.46", 125 | "vite": "^6.3.5", 126 | "vitest": "^3.1.3" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'es5', 5 | plugins: ['prettier-plugin-svelte'], 6 | overrides: [ 7 | { 8 | files: '*.svelte', 9 | options: { 10 | parser: 'svelte', 11 | }, 12 | }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'conventionalcommits', 3 | branches: ['main', { name: 'next', prerelease: true }], 4 | } 5 | -------------------------------------------------------------------------------- /scripts/install-dependencies: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Install dependencies for a given version of Svelte 3 | set -euxo pipefail 4 | 5 | svelte_version="${1-}" 6 | node_version=$(node --version | sed 's/^v\([0-9]*\).*/\1/') 7 | env_dir="tests/envs/svelte$svelte_version" 8 | env_dir_by_node="$env_dir/node$node_version" 9 | 10 | if [[ -d $env_dir_by_node ]]; then 11 | env_dir="$env_dir_by_node" 12 | fi 13 | 14 | if [[ "$svelte_version" == "5" ]]; then 15 | rm -rf coverage node_modules 16 | npm install 17 | exit 0 18 | fi 19 | 20 | if [[ -z "$svelte_version" ]]; then 21 | echo "Invalid usage: missing Svelte version" >&2; 22 | exit 1 23 | fi 24 | 25 | if [[ ! -d "$env_dir" ]]; then 26 | echo "Error: package.json for Svelte $svelte_version, Node $node_version not found" 1>&2 27 | exit 2 28 | fi 29 | 30 | rm -rf coverage node_modules "$env_dir/node_modules" 31 | pushd "$env_dir" 32 | npm install --no-package-lock --engine-strict 33 | npm ls "$env_dir" svelte 34 | popd 35 | mv "$env_dir/node_modules" . 36 | -------------------------------------------------------------------------------- /scripts/preview-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Preview the next release from a branch 3 | # 4 | # Prerequisites: 5 | # - You must have push access to repository at the `origin` URL 6 | # - The branch you are on must exist on `origin` 7 | 8 | set -euxo pipefail 9 | 10 | branch="$(git rev-parse --abbrev-ref HEAD)" 11 | repository_url="$(git remote get-url origin)" 12 | 13 | npx \ 14 | --package semantic-release@24 \ 15 | --package conventional-changelog-conventionalcommits@8 \ 16 | -- \ 17 | semantic-release \ 18 | --plugins="@semantic-release/commit-analyzer,@semantic-release/release-notes-generator" \ 19 | --dry-run \ 20 | --branches="$branch" \ 21 | --repository-url="$repository_url" 22 | -------------------------------------------------------------------------------- /src/component-types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { 3 | Component as ModernComponent, 4 | ComponentConstructorOptions as LegacyConstructorOptions, 5 | ComponentProps, 6 | EventDispatcher, 7 | mount, 8 | SvelteComponent as LegacyComponent, 9 | SvelteComponentTyped as Svelte3LegacyComponent, 10 | } from 'svelte' 11 | 12 | type IS_MODERN_SVELTE = ModernComponent extends (...args: any[]) => any 13 | ? true 14 | : false 15 | 16 | type IS_LEGACY_SVELTE_4 = 17 | EventDispatcher extends (...args: any[]) => any ? true : false 18 | 19 | /** A compiled, imported Svelte component. */ 20 | export type Component< 21 | P extends Record = any, 22 | E extends Record = any, 23 | > = IS_MODERN_SVELTE extends true 24 | ? ModernComponent | LegacyComponent

25 | : IS_LEGACY_SVELTE_4 extends true 26 | ? LegacyComponent

27 | : Svelte3LegacyComponent

28 | 29 | /** 30 | * The type of an imported, compiled Svelte component. 31 | * 32 | * In Svelte 5, this distinction no longer matters. 33 | * In Svelte 4, this is the Svelte component class constructor. 34 | */ 35 | export type ComponentType = C extends LegacyComponent 36 | ? new (...args: any[]) => C 37 | : C 38 | 39 | /** The props of a component. */ 40 | export type Props = ComponentProps 41 | 42 | /** 43 | * The exported fields of a component. 44 | * 45 | * In Svelte 5, this is the set of variables marked as `export`'d. 46 | * In Svelte 4, this is simply the instance of the component class. 47 | */ 48 | export type Exports = IS_MODERN_SVELTE extends true 49 | ? C extends ModernComponent 50 | ? E 51 | : C & { $set: never; $on: never; $destroy: never } 52 | : C 53 | 54 | /** 55 | * Options that may be passed to `mount` when rendering the component. 56 | * 57 | * In Svelte 4, these are the options passed to the component constructor. 58 | */ 59 | export type MountOptions = IS_MODERN_SVELTE extends true 60 | ? Parameters, Exports>>[1] 61 | : LegacyConstructorOptions> 62 | -------------------------------------------------------------------------------- /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 { createValidateOptions } from './validate-options.js' 11 | 12 | const { mount, unmount, updateProps, allowedOptions } = 13 | ModernCore.IS_MODERN_SVELTE ? ModernCore : LegacyCore 14 | 15 | /** Validate component options. */ 16 | const validateOptions = createValidateOptions(allowedOptions) 17 | 18 | export { mount, unmount, updateProps, validateOptions } 19 | export { UnknownSvelteOptionsError } from './validate-options.js' 20 | -------------------------------------------------------------------------------- /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 | const 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 | const unmount = (component) => { 38 | component.$destroy() 39 | } 40 | 41 | /** Update the component's props. */ 42 | const 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 | const 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 | const 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 | const 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 | const createValidateOptions = (allowedOptions) => (options) => { 19 | const isProps = !Object.keys(options).some((option) => 20 | allowedOptions.includes(option) 21 | ) 22 | 23 | if (isProps) { 24 | return { props: options } 25 | } 26 | 27 | // Check if any props and Svelte options were accidentally mixed. 28 | const unknownOptions = Object.keys(options).filter( 29 | (option) => !allowedOptions.includes(option) 30 | ) 31 | 32 | if (unknownOptions.length > 0) { 33 | throw new UnknownSvelteOptionsError(unknownOptions, allowedOptions) 34 | } 35 | 36 | return options 37 | } 38 | 39 | export { createValidateOptions, UnknownSvelteOptionsError } 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, setup } from './pure.js' 2 | 3 | // If we're running in a test runner that supports beforeEach/afterEach 4 | // we'll automatically run setup and cleanup before and after each test 5 | // this ensures that tests run in isolation from each other 6 | // if you don't like this then set the STL_SKIP_AUTO_CLEANUP env variable. 7 | if (typeof process !== 'undefined' && !process.env.STL_SKIP_AUTO_CLEANUP) { 8 | if (typeof beforeEach === 'function') { 9 | beforeEach(() => { 10 | setup() 11 | }) 12 | } 13 | 14 | if (typeof afterEach === 'function') { 15 | afterEach(async () => { 16 | await act() 17 | cleanup() 18 | }) 19 | } 20 | } 21 | 22 | // export all base queries, screen, etc. 23 | export * from '@testing-library/dom' 24 | 25 | // export svelte-specific functions and custom `fireEvent` 26 | export { UnknownSvelteOptionsError } from './core/index.js' 27 | export * from './pure.js' 28 | // `fireEvent` must be named to take priority over wildcard from @testing-library/dom 29 | export { fireEvent } from './pure.js' 30 | -------------------------------------------------------------------------------- /src/pure.js: -------------------------------------------------------------------------------- 1 | import { 2 | configure as configureDTL, 3 | fireEvent as baseFireEvent, 4 | getConfig as getDTLConfig, 5 | getQueriesForElement, 6 | prettyDOM, 7 | } from '@testing-library/dom' 8 | import * as Svelte from 'svelte' 9 | 10 | import { mount, unmount, updateProps, validateOptions } from './core/index.js' 11 | 12 | const targetCache = new Set() 13 | const componentCache = new Set() 14 | 15 | /** 16 | * Customize how Svelte renders the component. 17 | * 18 | * @template {import('./component-types.js').Component} C 19 | * @typedef {import('./component-types.js').Props | Partial>} SvelteComponentOptions 20 | */ 21 | 22 | /** 23 | * Customize how Testing Library sets up the document and binds queries. 24 | * 25 | * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] 26 | * @typedef {{ 27 | * baseElement?: HTMLElement 28 | * queries?: Q 29 | * }} RenderOptions 30 | */ 31 | 32 | /** 33 | * The rendered component and bound testing functions. 34 | * 35 | * @template {import('./component-types.js').Component} C 36 | * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] 37 | * 38 | * @typedef {{ 39 | * container: HTMLElement 40 | * baseElement: HTMLElement 41 | * component: import('./component-types.js').Exports 42 | * debug: (el?: HTMLElement | DocumentFragment) => void 43 | * rerender: (props: Partial>) => Promise 44 | * unmount: () => void 45 | * } & { 46 | * [P in keyof Q]: import('@testing-library/dom').BoundFunction 47 | * }} RenderResult 48 | */ 49 | 50 | /** 51 | * Render a component into the document. 52 | * 53 | * @template {import('./component-types.js').Component} C 54 | * @template {import('@testing-library/dom').Queries} [Q=typeof import('@testing-library/dom').queries] 55 | * 56 | * @param {import('./component-types.js').ComponentType} Component - The component to render. 57 | * @param {SvelteComponentOptions} options - Customize how Svelte renders the component. 58 | * @param {RenderOptions} renderOptions - Customize how Testing Library sets up the document and binds queries. 59 | * @returns {RenderResult} The rendered component and bound testing functions. 60 | */ 61 | const render = (Component, options = {}, renderOptions = {}) => { 62 | options = validateOptions(options) 63 | 64 | const baseElement = 65 | renderOptions.baseElement ?? options.target ?? document.body 66 | 67 | const queries = getQueriesForElement(baseElement, renderOptions.queries) 68 | 69 | const target = 70 | // eslint-disable-next-line unicorn/prefer-dom-node-append 71 | options.target ?? baseElement.appendChild(document.createElement('div')) 72 | 73 | targetCache.add(target) 74 | 75 | const component = mount( 76 | Component.default ?? Component, 77 | { ...options, target }, 78 | cleanupComponent 79 | ) 80 | 81 | componentCache.add(component) 82 | 83 | return { 84 | baseElement, 85 | component, 86 | container: target, 87 | debug: (el = baseElement) => { 88 | console.log(prettyDOM(el)) 89 | }, 90 | rerender: async (props) => { 91 | if (props.props) { 92 | console.warn( 93 | 'rerender({ props: {...} }) deprecated, use rerender({...}) instead' 94 | ) 95 | props = props.props 96 | } 97 | 98 | updateProps(component, props) 99 | await Svelte.tick() 100 | }, 101 | unmount: () => { 102 | cleanupComponent(component) 103 | }, 104 | ...queries, 105 | } 106 | } 107 | 108 | /** @type {import('@testing-library/dom'.Config | undefined} */ 109 | let originalDTLConfig 110 | 111 | /** 112 | * Configure `@testing-library/dom` for usage with Svelte. 113 | * 114 | * Ensures events fired from `@testing-library/dom` 115 | * and `@testing-library/user-event` wait for Svelte 116 | * to flush changes to the DOM before proceeding. 117 | */ 118 | const setup = () => { 119 | originalDTLConfig = getDTLConfig() 120 | 121 | configureDTL({ 122 | asyncWrapper: act, 123 | eventWrapper: Svelte.flushSync ?? ((cb) => cb()), 124 | }) 125 | } 126 | 127 | /** Reset dom-testing-library config. */ 128 | const cleanupDTL = () => { 129 | if (originalDTLConfig) { 130 | configureDTL(originalDTLConfig) 131 | originalDTLConfig = undefined 132 | } 133 | } 134 | 135 | /** Remove a component from the component cache. */ 136 | const cleanupComponent = (component) => { 137 | const inCache = componentCache.delete(component) 138 | 139 | if (inCache) { 140 | unmount(component) 141 | } 142 | } 143 | 144 | /** Remove a target element from the target cache. */ 145 | const cleanupTarget = (target) => { 146 | const inCache = targetCache.delete(target) 147 | 148 | if (inCache && target.parentNode === document.body) { 149 | target.remove() 150 | } 151 | } 152 | 153 | /** Unmount components, remove elements added to ``, and reset `@testing-library/dom`. */ 154 | const cleanup = () => { 155 | for (const component of componentCache) { 156 | cleanupComponent(component) 157 | } 158 | for (const target of targetCache) { 159 | cleanupTarget(target) 160 | } 161 | cleanupDTL() 162 | } 163 | 164 | /** 165 | * Call a function and wait for Svelte to flush pending changes. 166 | * 167 | * @template T 168 | * @param {(() => Promise) | () => T} [fn] - A function, which may be `async`, to call before flushing updates. 169 | * @returns {Promise} 170 | */ 171 | const act = async (fn) => { 172 | let result 173 | if (fn) { 174 | result = await fn() 175 | } 176 | await Svelte.tick() 177 | return result 178 | } 179 | 180 | /** 181 | * @typedef {(...args: Parameters) => Promise>} FireFunction 182 | */ 183 | 184 | /** 185 | * @typedef {{ 186 | * [K in import('@testing-library/dom').EventType]: (...args: Parameters) => Promise> 187 | * }} FireObject 188 | */ 189 | 190 | /** 191 | * Fire an event on an element. 192 | * 193 | * Consider using `@testing-library/user-event` instead, if possible. 194 | * @see https://testing-library.com/docs/user-event/intro/ 195 | * 196 | * @type {FireFunction & FireObject} 197 | */ 198 | const fireEvent = async (...args) => act(() => baseFireEvent(...args)) 199 | 200 | for (const [key, baseEvent] of Object.entries(baseFireEvent)) { 201 | fireEvent[key] = async (...args) => act(() => baseEvent(...args)) 202 | } 203 | 204 | export { act, cleanup, fireEvent, render, setup } 205 | -------------------------------------------------------------------------------- /src/vite.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import url from 'node:url' 3 | 4 | /** 5 | * Vite plugin to configure @testing-library/svelte. 6 | * 7 | * Ensures Svelte is imported correctly in tests 8 | * and that the DOM is cleaned up after each test. 9 | * 10 | * @param {{resolveBrowser?: boolean, autoCleanup?: boolean, noExternal?: boolean}} options 11 | * @returns {import('vite').Plugin} 12 | */ 13 | export const svelteTesting = ({ 14 | resolveBrowser = true, 15 | autoCleanup = true, 16 | noExternal = true, 17 | } = {}) => ({ 18 | name: 'vite-plugin-svelte-testing-library', 19 | config: (config) => { 20 | if (!process.env.VITEST) { 21 | return 22 | } 23 | 24 | if (resolveBrowser) { 25 | addBrowserCondition(config) 26 | } 27 | 28 | if (autoCleanup) { 29 | addAutoCleanup(config) 30 | } 31 | 32 | if (noExternal) { 33 | addNoExternal(config) 34 | } 35 | }, 36 | }) 37 | 38 | /** 39 | * Add `browser` to `resolve.conditions` before `node`. 40 | * 41 | * This ensures that Svelte's browser code is used in tests, 42 | * rather than its SSR code. 43 | * 44 | * @param {import('vitest/config').UserConfig} config 45 | */ 46 | const addBrowserCondition = (config) => { 47 | const resolve = config.resolve ?? {} 48 | const conditions = resolve.conditions ?? [] 49 | const nodeConditionIndex = conditions.indexOf('node') 50 | const browserConditionIndex = conditions.indexOf('browser') 51 | 52 | if ( 53 | nodeConditionIndex !== -1 && 54 | (nodeConditionIndex < browserConditionIndex || browserConditionIndex === -1) 55 | ) { 56 | conditions.splice(nodeConditionIndex, 0, 'browser') 57 | } 58 | 59 | resolve.conditions = conditions 60 | config.resolve = resolve 61 | } 62 | 63 | /** 64 | * Add auto-cleanup file to Vitest's setup files. 65 | * 66 | * @param {import('vitest/config').UserConfig} config 67 | */ 68 | const addAutoCleanup = (config) => { 69 | const test = config.test ?? {} 70 | let setupFiles = test.setupFiles ?? [] 71 | 72 | if (test.globals) { 73 | return 74 | } 75 | 76 | if (typeof setupFiles === 'string') { 77 | setupFiles = [setupFiles] 78 | } 79 | 80 | setupFiles.push( 81 | path.join(path.dirname(url.fileURLToPath(import.meta.url)), './vitest.js') 82 | ) 83 | 84 | test.setupFiles = setupFiles 85 | config.test = test 86 | } 87 | 88 | /** 89 | * Add `@testing-library/svelte` to Vite's noExternal rules, if not present. 90 | * 91 | * This ensures `@testing-library/svelte` is processed by `@sveltejs/vite-plugin-svelte` 92 | * in certain monorepo setups. 93 | */ 94 | const addNoExternal = (config) => { 95 | const ssr = config.ssr ?? {} 96 | let noExternal = ssr.noExternal ?? [] 97 | 98 | if (noExternal === true) { 99 | return 100 | } 101 | 102 | if (typeof noExternal === 'string' || noExternal instanceof RegExp) { 103 | noExternal = [noExternal] 104 | } 105 | 106 | if (!Array.isArray(noExternal)) { 107 | return 108 | } 109 | 110 | for (const rule of noExternal) { 111 | if (typeof rule === 'string' && rule === '@testing-library/svelte') { 112 | return 113 | } 114 | 115 | if (rule instanceof RegExp && rule.test('@testing-library/svelte')) { 116 | return 117 | } 118 | } 119 | 120 | noExternal.push('@testing-library/svelte') 121 | ssr.noExternal = noExternal 122 | config.ssr = ssr 123 | } 124 | -------------------------------------------------------------------------------- /src/vitest.js: -------------------------------------------------------------------------------- 1 | import { act, cleanup, setup } from '@testing-library/svelte' 2 | import { beforeEach } from 'vitest' 3 | 4 | beforeEach(() => { 5 | setup() 6 | 7 | return async () => { 8 | await act() 9 | cleanup() 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /tests/_env.js: -------------------------------------------------------------------------------- 1 | import { VERSION as SVELTE_VERSION } from 'svelte/compiler' 2 | 3 | export const IS_JSDOM = globalThis.navigator.userAgent.includes('jsdom') 4 | 5 | export const IS_HAPPYDOM = !IS_JSDOM // right now it's happy or js 6 | 7 | export const IS_JEST = Boolean(process.env.JEST_WORKER_ID) 8 | 9 | export const IS_SVELTE_5 = SVELTE_VERSION >= '5' 10 | 11 | export const MODE_LEGACY = 'legacy' 12 | 13 | export const MODE_RUNES = 'runes' 14 | 15 | export const COMPONENT_FIXTURES = [ 16 | { 17 | mode: MODE_LEGACY, 18 | component: './fixtures/Comp.svelte', 19 | isEnabled: true, 20 | }, 21 | { 22 | mode: MODE_RUNES, 23 | component: './fixtures/CompRunes.svelte', 24 | isEnabled: IS_SVELTE_5, 25 | }, 26 | ].filter(({ isEnabled }) => isEnabled) 27 | -------------------------------------------------------------------------------- /tests/_jest-setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/jest-globals' 2 | -------------------------------------------------------------------------------- /tests/_jest-vitest-alias.js: -------------------------------------------------------------------------------- 1 | import { describe, jest, test } from '@jest/globals' 2 | 3 | export { 4 | afterAll, 5 | afterEach, 6 | beforeAll, 7 | beforeEach, 8 | describe, 9 | expect, 10 | test, 11 | jest as vi, 12 | } from '@jest/globals' 13 | 14 | // Add support for describe.skipIf, test.skipIf, and test.runIf 15 | describe.skipIf = (condition) => (condition ? describe.skip : describe) 16 | test.skipIf = (condition) => (condition ? test.skip : test) 17 | test.runIf = (condition) => (condition ? test : test.skip) 18 | 19 | // Add support for `stubGlobal` 20 | jest.stubGlobal = (property, stub) => { 21 | if (typeof stub === 'function') { 22 | jest.spyOn(globalThis, property).mockImplementation(stub) 23 | } else { 24 | jest.replaceProperty(globalThis, property, stub) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/_vitest-setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest' 2 | -------------------------------------------------------------------------------- /tests/act.test.js: -------------------------------------------------------------------------------- 1 | import { setTimeout } from 'node:timers/promises' 2 | 3 | import { act, render, screen } from '@testing-library/svelte' 4 | import { userEvent } from '@testing-library/user-event' 5 | import { describe, expect, test } from 'vitest' 6 | 7 | import Comp from './fixtures/Comp.svelte' 8 | 9 | describe('act', () => { 10 | test('state updates are flushed', async () => { 11 | render(Comp) 12 | const button = screen.getByText('Button') 13 | 14 | expect(button).toHaveTextContent('Button') 15 | 16 | await act(() => { 17 | button.click() 18 | }) 19 | 20 | expect(button).toHaveTextContent('Button Clicked') 21 | }) 22 | 23 | test('accepts async functions', async () => { 24 | render(Comp) 25 | const button = screen.getByText('Button') 26 | 27 | await act(async () => { 28 | await setTimeout(10) 29 | button.click() 30 | }) 31 | 32 | expect(button).toHaveTextContent('Button Clicked') 33 | }) 34 | 35 | test('wires act into user-event', async () => { 36 | const user = userEvent.setup() 37 | render(Comp) 38 | const button = screen.getByText('Button') 39 | 40 | await user.click(button) 41 | 42 | expect(button).toHaveTextContent('Button Clicked') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/auto-cleanup.test.js: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' 2 | 3 | import { IS_JEST } from './_env.js' 4 | 5 | // TODO(mcous, 2024-12-08): clearing module cache and re-importing 6 | // in Jest breaks Svelte's environment checking heuristics. 7 | // Re-implement this test in a more accurate environment, without mocks. 8 | describe.skipIf(IS_JEST)('auto-cleanup', () => { 9 | const globalBeforeEach = vi.fn() 10 | const globalAfterEach = vi.fn() 11 | 12 | beforeEach(() => { 13 | vi.resetModules() 14 | globalThis.beforeEach = globalBeforeEach 15 | globalThis.afterEach = globalAfterEach 16 | }) 17 | 18 | afterEach(() => { 19 | delete process.env.STL_SKIP_AUTO_CLEANUP 20 | delete globalThis.beforeEach 21 | delete globalThis.afterEach 22 | }) 23 | 24 | test('calls afterEach with cleanup if globally defined', async () => { 25 | const { render } = await import('@testing-library/svelte') 26 | 27 | expect(globalAfterEach).toHaveBeenCalledTimes(1) 28 | expect(globalAfterEach).toHaveBeenLastCalledWith(expect.any(Function)) 29 | const globalCleanup = globalAfterEach.mock.lastCall[0] 30 | 31 | const { default: Comp } = await import('./fixtures/Comp.svelte') 32 | render(Comp, { props: { name: 'world' } }) 33 | await globalCleanup() 34 | 35 | expect(document.body).toBeEmptyDOMElement() 36 | }) 37 | 38 | test('does not call afterEach if process STL_SKIP_AUTO_CLEANUP is set', async () => { 39 | process.env.STL_SKIP_AUTO_CLEANUP = 'true' 40 | 41 | await import('@testing-library/svelte') 42 | 43 | expect(globalBeforeEach).toHaveBeenCalledTimes(0) 44 | expect(globalAfterEach).toHaveBeenCalledTimes(0) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/cleanup.test.js: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/svelte' 2 | import { describe, expect, test, vi } from 'vitest' 3 | 4 | import Mounter from './fixtures/Mounter.svelte' 5 | 6 | const onExecuted = vi.fn() 7 | const onDestroyed = vi.fn() 8 | const renderSubject = () => render(Mounter, { onExecuted, onDestroyed }) 9 | 10 | describe('cleanup', () => { 11 | test('cleanup deletes element', async () => { 12 | renderSubject() 13 | cleanup() 14 | 15 | expect(document.body).toBeEmptyDOMElement() 16 | }) 17 | 18 | test('cleanup unmounts component', () => { 19 | renderSubject() 20 | cleanup() 21 | 22 | expect(onDestroyed).toHaveBeenCalledTimes(1) 23 | }) 24 | 25 | test('cleanup handles unexpected errors during mount', () => { 26 | onExecuted.mockImplementation(() => { 27 | throw new Error('oh no!') 28 | }) 29 | 30 | expect(renderSubject).toThrowError() 31 | cleanup() 32 | 33 | expect(document.body).toBeEmptyDOMElement() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/context.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/svelte' 2 | import { expect, test } from 'vitest' 3 | 4 | import Comp from './fixtures/Context.svelte' 5 | 6 | test('can set a context', () => { 7 | const message = 'Got it' 8 | 9 | render(Comp, { 10 | context: new Map(Object.entries({ foo: { message } })), 11 | }) 12 | 13 | expect(screen.getByText(message)).toBeInTheDocument() 14 | }) 15 | -------------------------------------------------------------------------------- /tests/debug.test.js: -------------------------------------------------------------------------------- 1 | import { prettyDOM } from '@testing-library/dom' 2 | import { render } from '@testing-library/svelte' 3 | import { describe, expect, test, vi } from 'vitest' 4 | 5 | import Comp from './fixtures/Comp.svelte' 6 | 7 | describe('debug', () => { 8 | test('pretty prints the base element', () => { 9 | vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() }) 10 | 11 | const { baseElement, debug } = render(Comp, { props: { name: 'world' } }) 12 | 13 | debug() 14 | 15 | expect(console.log).toHaveBeenCalledTimes(1) 16 | expect(console.log).toHaveBeenCalledWith(prettyDOM(baseElement)) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/envs/svelte3/node16/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": "16.x.x" 5 | }, 6 | "devDependencies": { 7 | "@sveltejs/vite-plugin-svelte": "2.x.x", 8 | "@testing-library/dom": "9.x.x", 9 | "@testing-library/jest-dom": "^6.6.3", 10 | "@testing-library/user-event": "^14.6.1", 11 | "@vitest/coverage-v8": "0.x.x", 12 | "expect-type": "^1.2.1", 13 | "happy-dom": "14.x.x", 14 | "jest": "^29.7.0", 15 | "jest-environment-jsdom": "^29.7.0", 16 | "jsdom": "22.x.x", 17 | "npm-run-all": "^4.1.5", 18 | "svelte": "3.x.x", 19 | "svelte-check": "3.x.x", 20 | "svelte-jester": "3.x.x", 21 | "vite": "4.x.x", 22 | "vitest": "0.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/envs/svelte3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": ">=18" 5 | }, 6 | "devDependencies": { 7 | "@sveltejs/vite-plugin-svelte": "2.x.x", 8 | "@testing-library/dom": "^10.4.0", 9 | "@testing-library/jest-dom": "^6.6.3", 10 | "@testing-library/user-event": "^14.6.1", 11 | "@vitest/coverage-v8": "0.x.x", 12 | "expect-type": "^1.2.1", 13 | "happy-dom": "^17.4.6", 14 | "jest": "^29.7.0", 15 | "jest-environment-jsdom": "^29.7.0", 16 | "jsdom": "^26.1.0", 17 | "npm-run-all": "^4.1.5", 18 | "svelte": "3.x.x", 19 | "svelte-check": "3.x.x", 20 | "svelte-jester": "3.x.x", 21 | "vite": "4.x.x", 22 | "vitest": "0.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/envs/svelte4/node16/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": "16.x.x" 5 | }, 6 | "devDependencies": { 7 | "@sveltejs/vite-plugin-svelte": "2.x.x", 8 | "@testing-library/dom": "9.x.x", 9 | "@testing-library/jest-dom": "^6.6.3", 10 | "@testing-library/user-event": "^14.6.1", 11 | "@vitest/coverage-v8": "0.x.x", 12 | "expect-type": "^1.2.1", 13 | "happy-dom": "14.x.x", 14 | "jest": "^29.7.0", 15 | "jest-environment-jsdom": "^29.7.0", 16 | "jsdom": "22.x.x", 17 | "npm-run-all": "^4.1.5", 18 | "svelte": "4.x.x", 19 | "svelte-check": "3.x.x", 20 | "svelte-jester": "3.x.x", 21 | "vite": "4.x.x", 22 | "vitest": "0.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/envs/svelte4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": ">=18" 5 | }, 6 | "devDependencies": { 7 | "@sveltejs/vite-plugin-svelte": "3.x.x", 8 | "@testing-library/dom": "^10.4.0", 9 | "@testing-library/jest-dom": "^6.6.3", 10 | "@testing-library/user-event": "^14.6.1", 11 | "@vitest/coverage-v8": "2.x.x", 12 | "expect-type": "^1.2.1", 13 | "happy-dom": "^17.4.6", 14 | "jest": "^29.7.0", 15 | "jest-environment-jsdom": "^29.7.0", 16 | "jsdom": "^26.1.0", 17 | "npm-run-all": "^4.1.5", 18 | "svelte": "4.x.x", 19 | "svelte-check": "^4.1.7", 20 | "svelte-jester": "^5.0.0", 21 | "vite": "5.x.x", 22 | "vitest": "2.x.x" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/events.test.js: -------------------------------------------------------------------------------- 1 | import { fireEvent as fireEventDTL } from '@testing-library/dom' 2 | import { fireEvent, render, screen } from '@testing-library/svelte' 3 | import { describe, expect, test } from 'vitest' 4 | 5 | import { IS_SVELTE_5 } from './_env.js' 6 | import Comp from './fixtures/Comp.svelte' 7 | 8 | describe('events', () => { 9 | test('state changes are flushed after firing an event', async () => { 10 | render(Comp, { props: { name: 'World' } }) 11 | const button = screen.getByText('Button') 12 | 13 | const result = fireEvent.click(button) 14 | 15 | await expect(result).resolves.toBe(true) 16 | expect(button).toHaveTextContent('Button Clicked') 17 | }) 18 | 19 | test('calling `fireEvent` directly works too', async () => { 20 | render(Comp, { props: { name: 'World' } }) 21 | const button = screen.getByText('Button') 22 | 23 | const result = fireEvent( 24 | button, 25 | new MouseEvent('click', { 26 | bubbles: true, 27 | cancelable: true, 28 | }) 29 | ) 30 | 31 | await expect(result).resolves.toBe(true) 32 | expect(button).toHaveTextContent('Button Clicked') 33 | }) 34 | 35 | test.runIf(IS_SVELTE_5)('state changes are flushed synchronously', () => { 36 | render(Comp, { props: { name: 'World' } }) 37 | const button = screen.getByText('Button') 38 | 39 | const result = fireEventDTL.click(button) 40 | 41 | expect(result).toBe(true) 42 | expect(button).toHaveTextContent('Button Clicked') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /tests/fixtures/Comp.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 |

Hello {name}!

15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/fixtures/CompRunes.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |

Hello {name}!

12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/fixtures/Context.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
{ctx.message}
8 | -------------------------------------------------------------------------------- /tests/fixtures/Mounter.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/fixtures/Transitioner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | {#if show} 11 |
(introDone = true)}> 12 | {#if introDone} 13 |

Done

14 | {:else} 15 |

Pending

16 | {/if} 17 |
18 | {/if} 19 | -------------------------------------------------------------------------------- /tests/fixtures/Typed.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |

hello {name}

13 |

count: {count}

14 | 15 | -------------------------------------------------------------------------------- /tests/fixtures/TypedRunes.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |

hello {name}

8 |

count: {count}

9 | -------------------------------------------------------------------------------- /tests/mount.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/svelte' 2 | import { describe, expect, test, vi } from 'vitest' 3 | 4 | import Mounter from './fixtures/Mounter.svelte' 5 | 6 | const onMounted = vi.fn() 7 | const onDestroyed = vi.fn() 8 | const renderSubject = () => render(Mounter, { onMounted, onDestroyed }) 9 | 10 | describe('mount and destroy', () => { 11 | test('component is mounted', async () => { 12 | renderSubject() 13 | 14 | const content = screen.getByRole('button') 15 | 16 | expect(content).toBeInTheDocument() 17 | expect(onMounted).toHaveBeenCalledTimes(1) 18 | }) 19 | 20 | test('component is destroyed', async () => { 21 | const { unmount } = renderSubject() 22 | 23 | unmount() 24 | 25 | const content = screen.queryByRole('button') 26 | 27 | expect(content).not.toBeInTheDocument() 28 | expect(onDestroyed).toHaveBeenCalledTimes(1) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/multi-base.test.js: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/svelte' 2 | import { describe, expect, test } from 'vitest' 3 | 4 | import Comp from './fixtures/Comp.svelte' 5 | 6 | describe('multi-base', () => { 7 | const treeA = document.createElement('div') 8 | const treeB = document.createElement('div') 9 | 10 | test('container isolates trees from one another', () => { 11 | const { getByText: getByTextInA } = render( 12 | Comp, 13 | { 14 | target: treeA, 15 | props: { 16 | name: 'Tree A', 17 | }, 18 | }, 19 | { 20 | baseElement: treeA, 21 | } 22 | ) 23 | 24 | const { getByText: getByTextInB } = render( 25 | Comp, 26 | { 27 | target: treeB, 28 | props: { 29 | name: 'Tree B', 30 | }, 31 | }, 32 | { 33 | baseElement: treeB, 34 | } 35 | ) 36 | 37 | expect(() => getByTextInA('Hello Tree A!')).not.toThrow() 38 | expect(() => getByTextInB('Hello Tree A!')).toThrow() 39 | expect(() => getByTextInA('Hello Tree B!')).toThrow() 40 | expect(() => getByTextInB('Hello Tree B!')).not.toThrow() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/render-runes.test-d.ts: -------------------------------------------------------------------------------- 1 | import * as subject from '@testing-library/svelte' 2 | import { expectTypeOf } from 'expect-type' 3 | import { describe, test, vi } from 'vitest' 4 | 5 | import LegacyComponent from './fixtures/Typed.svelte' 6 | import Component from './fixtures/TypedRunes.svelte' 7 | 8 | describe('types', () => { 9 | test('render is a function that accepts a Svelte component', () => { 10 | subject.render(Component, { name: 'Alice', count: 42 }) 11 | subject.render(Component, { props: { name: 'Alice', count: 42 } }) 12 | }) 13 | 14 | test('rerender is a function that accepts partial props', async () => { 15 | const { rerender } = subject.render(Component, { name: 'Alice', count: 42 }) 16 | 17 | await rerender({ name: 'Bob' }) 18 | await rerender({ count: 0 }) 19 | }) 20 | 21 | test('invalid prop types are rejected', () => { 22 | // @ts-expect-error: name should be a string 23 | subject.render(Component, { name: 42 }) 24 | 25 | // @ts-expect-error: name should be a string 26 | subject.render(Component, { props: { name: 42 } }) 27 | }) 28 | 29 | test('render result has container and component', () => { 30 | const result = subject.render(Component, { name: 'Alice', count: 42 }) 31 | 32 | expectTypeOf(result).toExtend<{ 33 | container: HTMLElement 34 | component: { hello: string } 35 | debug: (el?: HTMLElement) => void 36 | rerender: (props: { name?: string; count?: number }) => Promise 37 | unmount: () => void 38 | }>() 39 | }) 40 | }) 41 | 42 | describe('legacy component types', () => { 43 | test('render accepts events', () => { 44 | const onGreeting = vi.fn() 45 | subject.render(LegacyComponent, { 46 | props: { name: 'Alice', count: 42 }, 47 | events: { greeting: onGreeting }, 48 | }) 49 | }) 50 | 51 | test('component $set and $on are not allowed', () => { 52 | const onGreeting = vi.fn() 53 | const { component } = subject.render(LegacyComponent, { 54 | name: 'Alice', 55 | count: 42, 56 | }) 57 | 58 | expectTypeOf(component).toExtend<{ hello: string }>() 59 | 60 | // @ts-expect-error: Svelte 5 mount does not return `$set` 61 | component.$on('greeting', onGreeting) 62 | 63 | // @ts-expect-error: Svelte 5 mount does not return `$set` 64 | component.$set({ name: 'Bob' }) 65 | 66 | // @ts-expect-error: Svelte 5 mount does not return `$destroy` 67 | component.$destroy() 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /tests/render-utilities.test-d.ts: -------------------------------------------------------------------------------- 1 | import * as subject from '@testing-library/svelte' 2 | import { expectTypeOf } from 'expect-type' 3 | import { describe, test } from 'vitest' 4 | 5 | import Component from './fixtures/Comp.svelte' 6 | 7 | describe('render query and utility types', () => { 8 | test('render result has default queries', () => { 9 | const result = subject.render(Component, { name: 'Alice' }) 10 | 11 | expectTypeOf(result.getByRole).parameters.toExtend< 12 | [role: subject.ByRoleMatcher, options?: subject.ByRoleOptions] 13 | >() 14 | }) 15 | 16 | test('render result can have custom queries', () => { 17 | const [getByVibes] = subject.buildQueries( 18 | (_container: HTMLElement, vibes: string) => { 19 | throw new Error(`unimplemented ${vibes}`) 20 | }, 21 | () => '', 22 | () => '' 23 | ) 24 | const result = subject.render( 25 | Component, 26 | { name: 'Alice' }, 27 | { queries: { getByVibes } } 28 | ) 29 | 30 | expectTypeOf(result.getByVibes).parameters.toExtend<[vibes: string]>() 31 | }) 32 | 33 | test('act is an async function', () => { 34 | expectTypeOf(subject.act).toExtend<() => Promise>() 35 | }) 36 | 37 | test('act accepts a sync function', () => { 38 | expectTypeOf(subject.act).toExtend<(fn: () => void) => Promise>() 39 | }) 40 | 41 | test('act accepts an async function', () => { 42 | expectTypeOf(subject.act).toExtend< 43 | (fn: () => Promise) => Promise 44 | >() 45 | }) 46 | 47 | test('fireEvent is an async function', () => { 48 | expectTypeOf(subject.fireEvent).toExtend< 49 | ( 50 | element: Element | Node | Document | Window, 51 | event: Event 52 | ) => Promise 53 | >() 54 | }) 55 | 56 | test('fireEvent[eventName] is an async function', () => { 57 | expectTypeOf(subject.fireEvent.click).toExtend< 58 | ( 59 | element: Element | Node | Document | Window, 60 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 61 | options?: {} 62 | ) => Promise 63 | >() 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /tests/render.test-d.ts: -------------------------------------------------------------------------------- 1 | import * as subject from '@testing-library/svelte' 2 | import { expectTypeOf } from 'expect-type' 3 | import { ComponentProps } from 'svelte' 4 | import { describe, test } from 'vitest' 5 | 6 | import Component from './fixtures/Typed.svelte' 7 | 8 | describe('types', () => { 9 | test('render is a function that accepts a Svelte component', () => { 10 | subject.render(Component, { name: 'Alice', count: 42 }) 11 | subject.render(Component, { props: { name: 'Alice', count: 42 } }) 12 | }) 13 | 14 | test('rerender is a function that accepts partial props', async () => { 15 | const { rerender } = subject.render(Component, { name: 'Alice', count: 42 }) 16 | 17 | await rerender({ name: 'Bob' }) 18 | await rerender({ count: 0 }) 19 | }) 20 | 21 | test('non-components are rejected', () => { 22 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 23 | class NotComponent {} 24 | 25 | // @ts-expect-error: component should be a Svelte component 26 | subject.render(NotComponent) 27 | }) 28 | 29 | test('invalid prop types are rejected', () => { 30 | // @ts-expect-error: name should be a string 31 | subject.render(Component, { name: 42 }) 32 | 33 | // @ts-expect-error: name should be a string 34 | subject.render(Component, { props: { name: 42 } }) 35 | }) 36 | 37 | test('render result has container and component', () => { 38 | const result = subject.render(Component, { name: 'Alice', count: 42 }) 39 | 40 | expectTypeOf(result).toExtend<{ 41 | container: HTMLElement 42 | component: { hello: string } 43 | debug: (el?: HTMLElement) => void 44 | rerender: (props: { name?: string; count?: number }) => Promise 45 | unmount: () => void 46 | }>() 47 | }) 48 | 49 | test('render function may be wrapped', () => { 50 | const renderSubject = (props: ComponentProps) => { 51 | return subject.render(Component, props) 52 | } 53 | 54 | renderSubject({ name: 'Alice', count: 42 }) 55 | // @ts-expect-error: name should be a string 56 | renderSubject(Component, { name: 42 }) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/render.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/svelte' 2 | import { beforeAll, describe, expect, test } from 'vitest' 3 | 4 | import { COMPONENT_FIXTURES } from './_env.js' 5 | 6 | describe.each(COMPONENT_FIXTURES)('render ($mode)', ({ component }) => { 7 | const props = { name: 'World' } 8 | let Comp 9 | 10 | beforeAll(async () => { 11 | Comp = await import(component) 12 | }) 13 | 14 | test('renders component into the document', () => { 15 | render(Comp, { props }) 16 | 17 | expect(screen.getByText('Hello World!')).toBeInTheDocument() 18 | }) 19 | 20 | test('accepts props directly', () => { 21 | render(Comp, props) 22 | expect(screen.getByText('Hello World!')).toBeInTheDocument() 23 | }) 24 | 25 | test('throws error when mixing svelte component options and props', () => { 26 | expect(() => { 27 | render(Comp, { props, name: 'World' }) 28 | }).toThrow(/Unknown options/) 29 | }) 30 | 31 | test('throws error when mixing target option and props', () => { 32 | expect(() => { 33 | render(Comp, { target: document.createElement('div'), name: 'World' }) 34 | }).toThrow(/Unknown options/) 35 | }) 36 | 37 | test('should return a container object wrapping the DOM of the rendered component', () => { 38 | const { container } = render(Comp, props) 39 | const firstElement = screen.getByTestId('test') 40 | 41 | expect(container.firstChild).toBe(firstElement) 42 | }) 43 | 44 | test('should return a baseElement object, which holds the container', () => { 45 | const { baseElement, container } = render(Comp, props) 46 | 47 | expect(baseElement).toBe(document.body) 48 | expect(baseElement.firstChild).toBe(container) 49 | }) 50 | 51 | test('if target is provided, use it as container and baseElement', () => { 52 | const target = document.createElement('div') 53 | const { baseElement, container } = render(Comp, { props, target }) 54 | 55 | expect(container).toBe(target) 56 | expect(baseElement).toBe(target) 57 | }) 58 | 59 | test('allow baseElement to be specified', () => { 60 | const customBaseElement = document.createElement('div') 61 | 62 | const { baseElement, container } = render( 63 | Comp, 64 | { props }, 65 | { baseElement: customBaseElement } 66 | ) 67 | 68 | expect(baseElement).toBe(customBaseElement) 69 | expect(baseElement.firstChild).toBe(container) 70 | }) 71 | 72 | test('should accept anchor option', () => { 73 | const baseElement = document.body 74 | const target = document.createElement('section') 75 | const anchor = document.createElement('div') 76 | baseElement.append(target) 77 | target.append(anchor) 78 | 79 | render(Comp, { props, target, anchor }, { baseElement }) 80 | const firstElement = screen.getByTestId('test') 81 | 82 | expect(target.firstChild).toBe(firstElement) 83 | // eslint-disable-next-line testing-library/no-node-access 84 | expect(target.lastChild).toBe(anchor) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /tests/rerender.test.js: -------------------------------------------------------------------------------- 1 | import { act, render, screen } from '@testing-library/svelte' 2 | import { beforeAll, describe, expect, test, vi } from 'vitest' 3 | 4 | import { COMPONENT_FIXTURES, IS_SVELTE_5, MODE_RUNES } from './_env.js' 5 | 6 | describe.each(COMPONENT_FIXTURES)('rerender ($mode)', ({ mode, component }) => { 7 | let Comp 8 | 9 | beforeAll(async () => { 10 | Comp = await import(component) 11 | }) 12 | 13 | test('updates props', async () => { 14 | const { rerender } = render(Comp, { name: 'World' }) 15 | const element = screen.getByText('Hello World!') 16 | 17 | await rerender({ name: 'Dolly' }) 18 | 19 | expect(element).toHaveTextContent('Hello Dolly!') 20 | }) 21 | 22 | test('warns if incorrect arguments shape used', async () => { 23 | vi.stubGlobal('console', { log: vi.fn(), warn: vi.fn(), error: vi.fn() }) 24 | 25 | const { rerender } = render(Comp, { name: 'World' }) 26 | const element = screen.getByText('Hello World!') 27 | 28 | await rerender({ props: { name: 'Dolly' } }) 29 | 30 | expect(element).toHaveTextContent('Hello Dolly!') 31 | expect(console.warn).toHaveBeenCalledTimes(1) 32 | expect(console.warn).toHaveBeenCalledWith( 33 | expect.stringMatching(/deprecated/iu) 34 | ) 35 | }) 36 | 37 | test.skipIf(mode === MODE_RUNES)('change props with accessors', async () => { 38 | const componentOptions = IS_SVELTE_5 39 | ? { name: 'World' } 40 | : { accessors: true, props: { name: 'World' } } 41 | 42 | const { component } = render(Comp, componentOptions) 43 | const element = screen.getByText('Hello World!') 44 | 45 | expect(element).toBeInTheDocument() 46 | expect(component.name).toBe('World') 47 | 48 | await act(() => { 49 | component.name = 'Planet' 50 | }) 51 | 52 | expect(element).toHaveTextContent('Hello Planet!') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /tests/transition.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/svelte' 2 | import { userEvent } from '@testing-library/user-event' 3 | import { beforeEach, describe, expect, test, vi } from 'vitest' 4 | 5 | import { IS_JSDOM, IS_SVELTE_5 } from './_env.js' 6 | import Transitioner from './fixtures/Transitioner.svelte' 7 | 8 | describe.skipIf(IS_SVELTE_5)('transitions', () => { 9 | if (IS_JSDOM) { 10 | beforeEach(() => { 11 | vi.stubGlobal('requestAnimationFrame', (fn) => 12 | setTimeout(() => fn(new Date()), 16) 13 | ) 14 | }) 15 | } 16 | 17 | test('on:introend', async () => { 18 | const user = userEvent.setup() 19 | 20 | render(Transitioner) 21 | const start = screen.getByRole('button') 22 | await user.click(start) 23 | 24 | const pending = screen.getByTestId('intro-pending') 25 | expect(pending).toBeInTheDocument() 26 | 27 | await waitFor(() => { 28 | const done = screen.queryByTestId('intro-done') 29 | expect(done).toBeInTheDocument() 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/vite-plugin.test.js: -------------------------------------------------------------------------------- 1 | import { svelteTesting } from '@testing-library/svelte/vite' 2 | import { beforeEach, describe, expect, test, vi } from 'vitest' 3 | 4 | import { IS_JEST } from './_env.js' 5 | 6 | describe.skipIf(IS_JEST)('vite plugin', () => { 7 | beforeEach(() => { 8 | vi.stubEnv('VITEST', '1') 9 | }) 10 | 11 | test('does not modify config if disabled', () => { 12 | const subject = svelteTesting({ 13 | resolveBrowser: false, 14 | autoCleanup: false, 15 | noExternal: false, 16 | }) 17 | 18 | const result = {} 19 | subject.config(result) 20 | 21 | expect(result).toEqual({}) 22 | }) 23 | 24 | test('does not modify config if not Vitest', () => { 25 | vi.stubEnv('VITEST', '') 26 | 27 | const subject = svelteTesting() 28 | 29 | const result = {} 30 | subject.config(result) 31 | 32 | expect(result).toEqual({}) 33 | }) 34 | 35 | test.each([ 36 | { 37 | config: () => ({ resolve: { conditions: ['node'] } }), 38 | expectedConditions: ['browser', 'node'], 39 | }, 40 | { 41 | config: () => ({ resolve: { conditions: ['svelte', 'node'] } }), 42 | expectedConditions: ['svelte', 'browser', 'node'], 43 | }, 44 | ])( 45 | 'adds browser condition if necessary', 46 | ({ config, expectedConditions }) => { 47 | const subject = svelteTesting({ 48 | resolveBrowser: true, 49 | autoCleanup: false, 50 | noExternal: false, 51 | }) 52 | 53 | const result = config() 54 | subject.config(result) 55 | 56 | expect(result).toEqual({ 57 | resolve: { 58 | conditions: expectedConditions, 59 | }, 60 | }) 61 | } 62 | ) 63 | 64 | test.each([ 65 | { 66 | config: () => ({}), 67 | expectedConditions: [], 68 | }, 69 | { 70 | config: () => ({ resolve: { conditions: [] } }), 71 | expectedConditions: [], 72 | }, 73 | { 74 | config: () => ({ resolve: { conditions: ['svelte'] } }), 75 | expectedConditions: ['svelte'], 76 | }, 77 | ])( 78 | 'skips browser condition if possible', 79 | ({ config, expectedConditions }) => { 80 | const subject = svelteTesting({ 81 | resolveBrowser: true, 82 | autoCleanup: false, 83 | noExternal: false, 84 | }) 85 | 86 | const result = config() 87 | subject.config(result) 88 | 89 | expect(result).toEqual({ 90 | resolve: { 91 | conditions: expectedConditions, 92 | }, 93 | }) 94 | } 95 | ) 96 | 97 | test.each([ 98 | { 99 | config: () => ({}), 100 | expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], 101 | }, 102 | { 103 | config: () => ({ test: { setupFiles: [] } }), 104 | expectedSetupFiles: [expect.stringMatching(/src\/vitest.js$/u)], 105 | }, 106 | { 107 | config: () => ({ test: { setupFiles: 'other-file.js' } }), 108 | expectedSetupFiles: [ 109 | 'other-file.js', 110 | expect.stringMatching(/src\/vitest.js$/u), 111 | ], 112 | }, 113 | ])('adds cleanup', ({ config, expectedSetupFiles }) => { 114 | const subject = svelteTesting({ 115 | resolveBrowser: false, 116 | autoCleanup: true, 117 | noExternal: false, 118 | }) 119 | 120 | const result = config() 121 | subject.config(result) 122 | 123 | expect(result).toEqual({ 124 | test: { 125 | setupFiles: expectedSetupFiles, 126 | }, 127 | }) 128 | }) 129 | 130 | test('skips cleanup in global mode', () => { 131 | const subject = svelteTesting({ 132 | resolveBrowser: false, 133 | autoCleanup: true, 134 | noExternal: false, 135 | }) 136 | 137 | const result = { test: { globals: true } } 138 | subject.config(result) 139 | 140 | expect(result).toEqual({ 141 | test: { 142 | globals: true, 143 | }, 144 | }) 145 | }) 146 | 147 | test.each([ 148 | { 149 | config: () => ({ ssr: { noExternal: [] } }), 150 | expectedNoExternal: ['@testing-library/svelte'], 151 | }, 152 | { 153 | config: () => ({}), 154 | expectedNoExternal: ['@testing-library/svelte'], 155 | }, 156 | { 157 | config: () => ({ ssr: { noExternal: 'other-file.js' } }), 158 | expectedNoExternal: ['other-file.js', '@testing-library/svelte'], 159 | }, 160 | { 161 | config: () => ({ ssr: { noExternal: /other/u } }), 162 | expectedNoExternal: [/other/u, '@testing-library/svelte'], 163 | }, 164 | ])('adds noExternal rule', ({ config, expectedNoExternal }) => { 165 | const subject = svelteTesting({ 166 | resolveBrowser: false, 167 | autoCleanup: false, 168 | noExternal: true, 169 | }) 170 | 171 | const result = config() 172 | subject.config(result) 173 | 174 | expect(result).toEqual({ 175 | ssr: { 176 | noExternal: expectedNoExternal, 177 | }, 178 | }) 179 | }) 180 | 181 | test.each([ 182 | { 183 | config: () => ({ ssr: { noExternal: true } }), 184 | expectedNoExternal: true, 185 | }, 186 | { 187 | config: () => ({ ssr: { noExternal: '@testing-library/svelte' } }), 188 | expectedNoExternal: '@testing-library/svelte', 189 | }, 190 | { 191 | config: () => ({ ssr: { noExternal: /svelte/u } }), 192 | expectedNoExternal: /svelte/u, 193 | }, 194 | ])('skips noExternal if able', ({ config, expectedNoExternal }) => { 195 | const subject = svelteTesting({ 196 | resolveBrowser: false, 197 | autoCleanup: false, 198 | noExternal: true, 199 | }) 200 | 201 | const result = config() 202 | subject.config(result) 203 | 204 | expect(result).toEqual({ 205 | ssr: { 206 | noExternal: expectedNoExternal, 207 | }, 208 | }) 209 | }) 210 | 211 | test('bails on noExternal if input is unexpected', () => { 212 | const subject = svelteTesting({ 213 | resolveBrowser: false, 214 | autoCleanup: false, 215 | noExternal: true, 216 | }) 217 | 218 | const result = { ssr: { noExternal: false } } 219 | subject.config(result) 220 | 221 | expect(result).toEqual({ 222 | ssr: { 223 | noExternal: false, 224 | }, 225 | }) 226 | }) 227 | }) 228 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./tsconfig.json"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "emitDeclarationOnly": true, 7 | "noEmit": false, 8 | "rootDir": "src", 9 | "outDir": "types" 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "node16", 4 | "allowJs": true, 5 | "noEmit": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "types": ["svelte", "vite/client", "vitest", "vitest/globals"], 9 | "baseUrl": "./", 10 | "paths": { 11 | "@testing-library/svelte": ["./src"] 12 | }, 13 | "plugins": [{ "name": "typescript-svelte-plugin" }] 14 | }, 15 | "include": ["src", "tests"] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.legacy.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./tsconfig.json"], 3 | "exclude": [ 4 | "tests/render-runes.test-d.ts", 5 | "tests/fixtures/CompRunes.svelte", 6 | "tests/fixtures/TypedRunes.svelte" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module' 2 | 3 | import { svelte } from '@sveltejs/vite-plugin-svelte' 4 | import { svelteTesting } from '@testing-library/svelte/vite' 5 | import { defineConfig } from 'vite' 6 | 7 | const require = createRequire(import.meta.url) 8 | 9 | export default defineConfig({ 10 | plugins: [svelte({ hot: false }), svelteTesting()], 11 | test: { 12 | environment: 'jsdom', 13 | setupFiles: ['./tests/_vitest-setup.js'], 14 | mockReset: true, 15 | unstubGlobals: true, 16 | unstubEnvs: true, 17 | coverage: { 18 | provider: 'v8', 19 | include: ['src/**/*'], 20 | }, 21 | alias: { 22 | '@testing-library/svelte/vite': require.resolve('./src/vite.js'), 23 | '@testing-library/svelte': require.resolve('./src/index.js'), 24 | }, 25 | }, 26 | }) 27 | --------------------------------------------------------------------------------