diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..11022ce50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: 'Bug: ' +labels: bug +assignees: '' + +--- + + + + +**Describe the bug** + + +**To Reproduce** + + +**Expected behavior** + + +**Related information:** + + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3fa6b4914 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions & Discussions + url: https://github.com/vuejs/test-utils/discussions + about: Use GitHub discussions for message-board style questions and discussions. + - name: Vue.js 2 / test-utils v1 + url: https://github.com/vuejs/vue-test-utils + about: @vue/test-utils v1 for Vue.js 2 is in a separate repository diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..89a960818 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: 'Feature request:' +labels: '' +assignees: '' + +--- + + + + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 000000000..b1c0e1356 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,30 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:js-lib"], + // only 1 PR at the same time (to avoid cascading rebase) + "prConcurrentLimit": 1, + // auto-merge if build is OK + "automerge": true, + "packageRules": [ + // group vitest packages update + { + groupName: 'vitest', + matchPackagePrefixes: ['@vitest/', 'vitest'] + }, + // group vite packages update + { + groupName: 'vite', + matchPackagePrefixes: ['@vitejs/', 'vite'], + excludePackagePrefixes: ['vitest'] + }, + // group all minor dependencies, once a week + { + "matchPackagePatterns": ["*"], + "matchUpdateTypes": ["minor", "patch"], + "matchCurrentVersion": ">=1", + "groupName": "all non-major dependencies", + "groupSlug": "all-minor-patch", + "schedule": ["after 1am on Wednesday"] + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13e4541c9..bfe840e87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,9 @@ name: CI on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: @@ -16,19 +16,23 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 15.x] + node: [18, 20, 22] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4.1.0 + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} - - run: yarn install - - run: yarn lint - - run: yarn test - - run: yarn build + node-version: ${{ matrix.node }} + cache: 'pnpm' + - run: pnpm install + - run: pnpm run lint + - run: pnpm run test:coverage env: CI: true - - run: yarn test:build - - run: yarn tsd + - run: pnpm run test:build + - run: pnpm run tsd + - run: pnpm run vue-tsc + - run: pnpm run tsc:docs diff --git a/.gitignore b/.gitignore index 87c63fdf7..d1f75bfb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .idea node_modules -yarn-error.log dist coverage +docs/.vitepress/cache diff --git a/.vscode/launch.json b/.vscode/launch.json index 13bc8c82e..917c123a7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,28 +1,17 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Jest", - "type": "node", - "request": "launch", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "stopOnEntry": false, - "args": ["${fileBasename}", "--runInBand", "--detectOpenHandles"], - "cwd": "${workspaceFolder}", - "preLaunchTask": null, - "runtimeExecutable": null, - "runtimeArgs": ["--nolazy"], - "env": { - "NODE_ENV": "development" - }, - "console": "integratedTerminal", - "sourceMaps": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest", - } - } - ] -} +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "vitest", + "type": "node", + "request": "launch", + "autoAttachChildProcesses": true, + "skipFiles": ["/**", "**/node_modules/**"], + "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", + "args": ["run", "${relativeFile}"], + "smartStep": true, + "console": "integratedTerminal" + } + ] +} diff --git a/README.md b/README.md index 1801b5d76..67522c817 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,43 @@ -# vue-test-utils-next +# Vue Test Utils -The next iteration of Vue Test Utils. It targets Vue 3. +Component testing utils for Vue 3. + +## Languages + +[🇫🇷 French version of this README.md](https://github.com/vuejs/test-utils/tree/main/docs/fr/README.md) ## Installation and Usage -- yarn: `yarn add @vue/test-utils@next --dev` -- npm: `npm install @vue/test-utils@next --save-dev` +- yarn: `yarn add @vue/test-utils --dev` +- npm: `npm install @vue/test-utils --save-dev` -Get started with the [documentation](https://next.vue-test-utils.vuejs.org/). +Get started with the [documentation](https://test-utils.vuejs.org/). -## Coming from Vue 2 + Vue Test Utils? +## Coming from Vue 2 + Test Utils v1? -We plan on work on [some documentation to help people migrate](https://next.vue-test-utils.vuejs.org/migration/). +[Check the migration guide](https://test-utils.vuejs.org/migration/). It's still a work in progress. If you find a problem or something that doesn't work that previously did in Vue Test Utils v1, please open an issue. -At this point you will have better luck trying this out with a brand new Vue 3 app, as opposed to upgrading an existing Vue 2 app. Feedback and bug reports are welcome! +## Documentation -## Working with `.vue` files +See the [docs](https://test-utils.vuejs.org/). -There is [`vue-jest`](https://github.com/vuejs/vue-jest) for loading `.vue` files into Jest. The `next` branch contains support for Vue 3. Install it with `yarn add vue-jest@next`. It lacks support for some things, namely JSX. +## Development -If you don't want to configure things, you can download a repository with Vue 3, `@vue/test-utils@next`, `vue-jest@next` and TypeScript configured [here](https://github.com/lmiller1990/vtu-next-demo). +Get started by running `pnpm install`. You can run the tests with `pnpm test`. That's it! -## Documentation +## Contributing Docs -See the [docs](https://next.vue-test-utils.vuejs.org/). +All the documentation files can be found in `docs`. It contains the English markdown files while translation(s) are stored in their corresponding `` sub-folder(s): -## Development +- [`fr`](https://github.com/vuejs/test-utils/tree/main/docs/fr): French translation. + +Besides that, the `.vitepress` sub-folder contains the config and theme, including the i18n information. + +- `pnpm docs:dev`: Start the docs dev server. +- `pnpm docs:build`: Build the docs. + +To add or maintain the translations, we follow the [Vue Ecosystem Translation Guidelines](https://github.com/vuejs-translations/guidelines/blob/main/README_ECOSYSTEM.md). -It's a pretty small codebase at the moment. Get started by running `yarn install`. You can run the tests with `yarn test`. That's it! - -There is still some work left to do. See issues for some basic TODOs, or the table at the bottom of this page. - -## Contributing - -We plan on moving to RC sooner than later. If you want to add a feature, have a hack or ping someone in Discord to chat, or check out the issues and project board. - -There's also some [work left to do in docs](https://github.com/vuejs/vue-test-utils-next/issues?q=is%3Aopen+is%3Aissue+label%3Adocumentation). - -## Comparison with Vue Test Utils beta (targeting Vue 2) - -This is table for those coming from VTU 1, comparing the two APIs. Some things are still a work in progress. - -- ✅ - implemented -- ❌ - not yet implemented -- ⚰️ - will not be implemented (if you have a compelling use case, please open an issue) - -### Mounting Options - -| option | status | notes | -| ---------------- | ------ | ----------------------------------------------------------------------------------- | -| data | ✅ | -| slots | ✅ | has not been tested vigorously. Please try it out. | -| mocks | ✅ | nested in `global` | -| propsData | ✅ | now called `props` | -| provide | ✅ | nested in `global` | -| mixins | ✅ | (new!) nested in `global` | -| plugins | ✅ | (new!) nested in `global` | -| component | ✅ | (new!) nested in `global` | -| directives | ✅ | (new!) nested in `global` | -| stubs | ✅ | -| attachToDocument | ✅ | renamed `attachTo`. See [here](https://github.com/vuejs/vue-test-utils/pull/1492) | -| attrs | ✅ | -| scopedSlots | ⚰️ | scopedSlots are merged with `slots` in Vue 3 | -| context | ⚰️ | different from Vue 2, does not make sense anymore. | -| localVue | ⚰️ | may not make sense anymore since we do not mutate the global Vue instance in Vue 3. | -| listeners | ⚰️ | no longer exists in Vue 3 | -| parentComponent | ⚰️ | - -### Wrapper API (mount) - -| method | status | notes | -| -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | -| attributes | ✅ | -| classes | ✅ | -| exists | ✅ | -| find | ✅ | only `querySelector` syntax is supported. `find(Comp)` under discussion [here](https://github.com/vuejs/vue-test-utils/issues/1498) | -| emitted | ✅ | -| findAll | ✅ | see above. `.vm` is different to Vue 2. We are exploring options. | -| get | ✅ | -| html | ✅ | -| setValue | ✅ | works for select, checkbox, radio button, input, textarea. Returns `nextTick`. | -| text | ✅ | -| trigger | ✅ | returns `nextTick`. You can do `await wrapper.find('button').trigger('click')` | -| setProps | ✅ | -| props | ✅ | -| setData | ✅ | -| destroy | ✅ | renamed to `unmount` to match Vue 3 lifecycle hook name. | -| props | ✅ | -| isVisible | ✅ | -| contains | ⚰️ | use `find` | -| emittedByOrder | ⚰️ | use `emitted` | -| setSelected | ⚰️ | now part of `setValue` | -| setChecked | ⚰️ | now part of `setValue` | -| is | ⚰️ | -| isEmpty | ⚰️ | use matchers such as [this](https://github.com/testing-library/jest-dom#tobeempty) | -| isVueInstance | ⚰️ | -| name | ⚰️ | -| setMethods | ⚰️ | +- `pnpm docs:translation:status []`: Show the translation status for your language. If you don't specify a language, it will show the status for all languages. +- `pnpm docs:translation:compare `: Compare the docs with the latest checkpoint for your language. +- `pnpm docs:translation:update []`: Update the checkpoint for your language. The checkpoint will be set by the latest commit hash. However, you can also specify a commit hash manually. diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 7977722be..000000000 --- a/babel.config.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - presets: [ - ['@babel/preset-env', - { - targets: { - node: 'current' - } - } - ] - ] -} \ No newline at end of file diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js deleted file mode 100644 index c5e0860bf..000000000 --- a/docs/.vitepress/config.js +++ /dev/null @@ -1,124 +0,0 @@ -const package = require('../../package.json') -/** @typedef {import('vitepress').UserConfig} UserConfig */ - -/** @type {UserConfig} */ -const config = { - title: `Vue Test Utils for Vue 3 (${package.version})`, - description: 'The documentation for the official Vue Test Utils for Vue 3', - locales: { - '/': { - lang: 'en-US', - title: `Vue Test Utils for Vue 3 (${package.version})` - } - }, - head: [['link', { rel: 'icon', href: `/logo.png` }]], - themeConfig: { - repo: 'vuejs/vue-test-utils-next', - docsRepo: 'vuejs/vue-test-utils-next', - docsDir: 'docs', - docsBranch: 'master', - editLinks: true, - algolia: { - apiKey: 'ee1b8516c9e5a5be9b6c25684eafc42f', - indexName: 'vue_test_utils', - searchParameters: { - facetFilters: ['tags:next'] - } - }, - nav: [ - { text: 'Guide', link: '/guide/' }, - { text: 'API Reference', link: '/api/' }, - { text: 'Migrating from Vue 2', link: '/migration/' }, - { - text: 'Changelog', - link: 'https://github.com/vuejs/vue-test-utils-next/releases' - } - ], - sidebar: [ - { - text: 'Installation', - link: '/installation/' - }, - { - text: 'Essentials', - collapsable: false, - children: [ - { text: 'Getting Started', link: '/guide/' }, - { text: 'A Crash Course', link: '/guide/essentials/a-crash-course' }, - { - text: 'Conditional Rendering', - link: '/guide/essentials/conditional-rendering' - }, - { - text: 'Testing Emitted Events', - link: '/guide/essentials/event-handling' - }, - { text: 'Testing Forms', link: '/guide/essentials/forms' }, - { - text: 'Passing Data to Components', - link: '/guide/essentials/passing-data' - }, - { - text: 'Write components that are easy to test', - link: '/guide/essentials/easy-to-test' - } - ] - }, - { - text: 'Vue Test Utils in depth', - collapsable: false, - children: [ - { text: 'Slots', link: '/guide/advanced/slots' }, - { - text: 'Asynchronous Behavior', - link: '/guide/advanced/async-suspense' - }, - { - text: 'Making HTTP Requests', - link: '/guide/advanced/http-requests' - }, - { text: 'Transitions', link: '/guide/advanced/transitions' }, - { - text: 'Component Instance', - link: '/guide/advanced/component-instance' - }, - { - text: 'Reusability and Composition', - link: '/guide/advanced/reusability-composition' - }, - { text: 'Testing Vuex', link: '/guide/advanced/vuex' }, - { text: 'Testing Vue Router', link: '/guide/advanced/vue-router' }, - { - text: 'Third-party integration', - link: '/guide/advanced/third-party' - }, - { - text: 'Stubs and Shallow Mount', - link: '/guide/advanced/stubs-shallow-mount' - } - ] - }, - { - text: 'Extending Vue Test Utils', - collapsable: false, - children: [ - { text: 'Plugins', link: '/guide/extending-vtu/plugins' }, - { - text: 'Community and Learning', - link: '/guide/extending-vtu/community-learning' - } - ] - }, - { - text: 'Migrating from Vue 2', - link: '/migration/' - }, - { - text: 'API Reference', - link: '/api/' - } - ] - } -} - -module.exports = config diff --git a/docs/.vitepress/config/en.mts b/docs/.vitepress/config/en.mts new file mode 100644 index 000000000..f0e553066 --- /dev/null +++ b/docs/.vitepress/config/en.mts @@ -0,0 +1,121 @@ +import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' + +export const META_URL = '' +export const META_TITLE = 'Vue Test Utils' +export const META_DESCRIPTION = 'The official testing suite utils for Vue.js 3' + +export const enConfig: LocaleSpecificConfig = { + description: META_DESCRIPTION, + head: [ + ['meta', { property: 'og:url', content: META_URL }], + ['meta', { property: 'og:title', content: META_TITLE }], + ['meta', { property: 'og:description', content: META_DESCRIPTION }], + ['meta', { property: 'twitter:url', content: META_URL }], + ['meta', { property: 'twitter:title', content: META_TITLE }], + ['meta', { property: 'twitter:description', content: META_DESCRIPTION }], + ], + + themeConfig: { + editLink: { + pattern: 'https://github.com/vuejs/test-utils/edit/main/docs/:path', + text: 'Suggest changes to this page', + }, + + nav: [ + { text: 'Guide', link: '/guide/' }, + { text: 'API Reference', link: '/api/' }, + { text: 'Migrating from Vue 2', link: '/migration/' }, + { + text: 'Changelog', + link: 'https://github.com/vuejs/test-utils/releases' + } + ], + + sidebar: { + '/': [ + { + text: 'Installation', + link: '/installation/' + }, + { + text: 'Essentials', + items: [ + { text: 'Getting Started', link: '/guide/' }, + { text: 'A Crash Course', link: '/guide/essentials/a-crash-course' }, + { + text: 'Conditional Rendering', + link: '/guide/essentials/conditional-rendering' + }, + { + text: 'Testing Emitted Events', + link: '/guide/essentials/event-handling' + }, + { text: 'Testing Forms', link: '/guide/essentials/forms' }, + { + text: 'Passing Data to Components', + link: '/guide/essentials/passing-data' + }, + { + text: 'Write components that are easy to test', + link: '/guide/essentials/easy-to-test' + } + ] + }, + { + text: 'Vue Test Utils in depth', + items: [ + { text: 'Slots', link: '/guide/advanced/slots' }, + { + text: 'Asynchronous Behavior', + link: '/guide/advanced/async-suspense' + }, + { + text: 'Making HTTP Requests', + link: '/guide/advanced/http-requests' + }, + { text: 'Transitions', link: '/guide/advanced/transitions' }, + { + text: 'Component Instance', + link: '/guide/advanced/component-instance' + }, + { + text: 'Reusability and Composition', + link: '/guide/advanced/reusability-composition' + }, + { text: 'Testing v-model', link: '/guide/advanced/v-model' }, + { text: 'Testing Vuex', link: '/guide/advanced/vuex' }, + { text: 'Testing Vue Router', link: '/guide/advanced/vue-router' }, + { text: 'Testing Teleport', link: '/guide/advanced/teleport' }, + { + text: 'Stubs and Shallow Mount', + link: '/guide/advanced/stubs-shallow-mount' + }, + { text: 'Testing Server-side Rendering', link: '/guide/advanced/ssr' } + ] + }, + { + text: 'Extending Vue Test Utils', + items: [ + { text: 'Plugins', link: '/guide/extending-vtu/plugins' }, + { + text: 'Community and Learning', + link: '/guide/extending-vtu/community-learning' + } + ] + }, + { + text: 'FAQ', + link: '/guide/faq/' + }, + { + text: 'Migrating from Vue 2', + link: '/migration/' + }, + { + text: 'API Reference', + link: '/api/' + } + ] + } + } +} diff --git a/docs/.vitepress/config/fr.mts b/docs/.vitepress/config/fr.mts new file mode 100644 index 000000000..33ba5f222 --- /dev/null +++ b/docs/.vitepress/config/fr.mts @@ -0,0 +1,125 @@ +import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' + +export const META_URL = '' +export const META_TITLE = 'Vue Test Utils' +export const META_DESCRIPTION = 'La librairie officielle de tests unitaires pour Vue.js 3' + +export const frConfig: LocaleSpecificConfig = { + description: META_DESCRIPTION, + head: [ + ['meta', { property: 'og:url', content: META_URL }], + ['meta', { property: 'og:title', content: META_TITLE }], + ['meta', { property: 'og:description', content: META_DESCRIPTION }], + ['meta', { property: 'twitter:url', content: META_URL }], + ['meta', { property: 'twitter:title', content: META_TITLE }], + ['meta', { property: 'twitter:description', content: META_DESCRIPTION }], + ], + + themeConfig: { + editLink: { + pattern: 'https://github.com/vuejs/test-utils/edit/main/docs/:path', + text: 'Suggest changes to this page', + }, + + nav: [ + { text: 'Guide', link: '/fr/guide/' }, + { text: 'API', link: '/fr/api/' }, + { + text: 'FAQ', + link: '/fr/guide/faq/' + }, + { text: 'Migrer depuis Vue 2', link: '/fr/migration/' }, + { + text: 'Journal de modifications', + link: 'https://github.com/vuejs/test-utils/releases' + } + ], + + sidebar: { + '/fr': [ + { + text: 'Installation', + link: '/fr/installation/' + }, + { + text: 'Les Bases', + items: [ + { text: 'Pour commencer', link: '/fr/guide/' }, + { text: 'Cours rapide', link: '/fr/guide/essentials/a-crash-course' }, + { + text: 'Rendu conditionnel', + link: '/fr/guide/essentials/conditional-rendering' + }, + { + text: 'Tester les évènements émis', + link: '/fr/guide/essentials/event-handling' + }, + { text: 'Tester les formulaires', link: '/fr/guide/essentials/forms' }, + { + text: 'Passer des données aux Composants', + link: '/fr/guide/essentials/passing-data' + }, + { + text: 'Écrire des composants facile à tester', + link: '/fr/guide/essentials/easy-to-test' + } + ] + }, + { + text: 'Vue Test Utils en détail', + items: [ + { text: 'Slots', link: '/fr/guide/advanced/slots' }, + { + text: 'Comportement asynchrone', + link: '/fr/guide/advanced/async-suspense' + }, + { + text: 'Faire des requêtes HTTP', + link: '/fr/guide/advanced/http-requests' + }, + { text: 'Transitions', link: '/fr/guide/advanced/transitions' }, + { + text: 'Instance de Composant', + link: '/fr/guide/advanced/component-instance' + }, + { + text: 'Réutilisabilité et Composition', + link: '/fr/guide/advanced/reusability-composition' + }, + { text: 'Tester v-model', link: '/fr/guide/advanced/v-model' }, + { text: 'Tester Vuex', link: '/fr/guide/advanced/vuex' }, + { text: 'Tester Vue Router', link: '/fr/guide/advanced/vue-router' }, + { text: 'Tester Teleport', link: '/fr/guide/advanced/teleport' }, + { + text: 'Composants de Substitution (Stubs) et Montage Partiel', + link: '/fr/guide/advanced/stubs-shallow-mount' + }, + { text: 'Tester le Rendu côté Serveur (SSR)', link: '/fr/guide/advanced/ssr' } + ] + }, + { + text: 'Extensions de Vue Test Utils', + items: [ + { text: 'Plugins', link: '/fr/guide/extending-vtu/plugins' }, + { + text: 'Communauté et Apprentissage', + link: '/fr/guide/extending-vtu/community-learning' + } + ] + }, + { + text: 'FAQ', + link: '/fr/guide/faq/' + }, + { + text: 'Migrer depuis Vue 2', + link: '/fr/migration/' + }, + { + text: 'API', + link: '/fr/api/' + } + ] + } + } +} diff --git a/docs/.vitepress/config/index.mts b/docs/.vitepress/config/index.mts new file mode 100644 index 000000000..ad9c16ffe --- /dev/null +++ b/docs/.vitepress/config/index.mts @@ -0,0 +1,15 @@ +import { enConfig } from './en.mts' +import { frConfig } from './fr.mts' +import { zhConfig } from './zh.mts' +import { sharedConfig } from './shared.mts' +import { defineConfig } from 'vitepress' + +export default defineConfig({ + ...sharedConfig, + + locales: { + root: { label: 'English', lang: 'en-US', link: '/', ...enConfig }, + fr: { label: 'Français', lang: 'fr-FR', link: '/fr/', ...frConfig }, + zh: { label: '简体中文', lang: 'zh-CN', link: '/zh/', ...zhConfig } + } +}) diff --git a/docs/.vitepress/config/shared.mts b/docs/.vitepress/config/shared.mts new file mode 100644 index 000000000..ee7850372 --- /dev/null +++ b/docs/.vitepress/config/shared.mts @@ -0,0 +1,113 @@ +import { defineConfig } from 'vitepress' + +// TODO: +// export const META_IMAGE = 'https://test-utils.vuejs.org/social.png' +export const META_IMAGE = null +export const isProduction = + process.env.NETLIFY && process.env.CONTEXT === 'production' + +if (process.env.NETLIFY) { + console.log('Netlify build', process.env.CONTEXT) +} + +const rControl = /[\u0000-\u001f]/g +const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'“”‘’<>,.?/]+/g +const rCombining = /[\u0300-\u036F]/g + +/** + * Default slugification function + */ +export const slugify = (str: string): string => + str + .normalize('NFKD') + // Remove accents + .replace(rCombining, '') + // Remove control characters + .replace(rControl, '') + // Replace special characters + .replace(rSpecial, '-') + // ensure it doesn't start with a number + .replace(/^(\d)/, '_$1') + +export const sharedConfig = defineConfig({ + title: 'Vue Test Utils', + appearance: true, + + markdown: { + theme: { + dark: 'one-dark-pro', + light: 'github-light', + }, + + anchor: { + slugify, + }, + }, + + head: [ + ['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }], + + [ + 'meta', + { name: 'wwads-cn-verify', content: '7e7757b1e12abcb736ab9a754ffb617a' }, + ], + + [ + 'meta', + { + property: 'og:type', + content: 'website', + }, + ], + + [ + 'meta', + { + property: 'twitter:card', + content: 'summary_large_image', + }, + ], + // [ + // 'meta', + // { + // property: 'twitter:image', + // content: META_IMAGE, + // }, + // ], + ], + + themeConfig: { + logo: '/logo.svg', + outline: [2, 3], + + socialLinks: [ + { + icon: 'github', + link: 'https://github.com/vuejs/test-utils/', + }, + { + icon: 'discord', + link: 'https://chat.vuejs.org', + }, + ], + + footer: { + copyright: 'Copyright © 2014-present Evan You', + message: 'Released under the MIT License.', + }, + + editLink: { + pattern: 'https://github.com/vuejs/test-utils/edit/main/docs/:path', + text: 'Suggest changes', + }, + + algolia: { + appId: 'BH4D9OD16A', + apiKey: 'ee1b8516c9e5a5be9b6c25684eafc42f', + indexName: 'vue_test_utils', + searchParameters: { + facetFilters: ['tags:next'] + } + }, + }, +}) diff --git a/docs/.vitepress/config/zh.mts b/docs/.vitepress/config/zh.mts new file mode 100644 index 000000000..fd24b3a25 --- /dev/null +++ b/docs/.vitepress/config/zh.mts @@ -0,0 +1,154 @@ +import type { DefaultTheme, LocaleSpecificConfig } from 'vitepress' + +export const META_URL = '' +export const META_TITLE = 'Vue Test Utils' +export const META_DESCRIPTION = 'Vue.js 3 官方测试工具集' + +export const zhConfig: LocaleSpecificConfig = { + description: META_DESCRIPTION, + head: [ + ['meta', { property: 'og:url', content: META_URL }], + ['meta', { property: 'og:title', content: META_TITLE }], + ['meta', { property: 'og:description', content: META_DESCRIPTION }], + ['meta', { property: 'twitter:url', content: META_URL }], + ['meta', { property: 'twitter:title', content: META_TITLE }], + ['meta', { property: 'twitter:description', content: META_DESCRIPTION }] + ], + + themeConfig: { + editLink: { + pattern: 'https://github.com/vuejs/test-utils/edit/main/docs/:path', + text: '改进此页面的内容' + }, + + nav: [ + { text: '指南', link: '/zh/guide/' }, + { text: 'API 参考', link: '/zh/api/' }, + { text: '从 Vue 2 迁移', link: '/zh/migration/' }, + { + text: '更新日志', + link: 'https://github.com/vuejs/test-utils/releases' + } + ], + + sidebar: { + '/zh': [ + { + text: '安装', + link: '/zh/installation/' + }, + { + text: '基础知识', + items: [ + { + text: '开始', + link: '/zh/guide/' + }, + { + text: '快速上手', + link: '/zh/guide/essentials/a-crash-course' + }, + { + text: '条件渲染', + link: '/zh/guide/essentials/conditional-rendering' + }, + { + text: '测试事件触发', + link: '/zh/guide/essentials/event-handling' + }, + { + text: '测试表单', + link: '/zh/guide/essentials/forms' + }, + { + text: '传递数据到组件', + link: '/zh/guide/essentials/passing-data' + }, + { + text: '编写易于测试的组件', + link: '/zh/guide/essentials/easy-to-test' + } + ] + }, + { + text: '深入学习 Vue Test Utils', + items: [ + { + text: '插槽', + link: '/zh/guide/advanced/slots' + }, + { + text: '异步行为', + link: '/zh/guide/advanced/async-suspense' + }, + { + text: '发起 HTTP 请求', + link: '/zh/guide/advanced/http-requests' + }, + { + text: '过渡效果', + link: '/zh/guide/advanced/transitions' + }, + { + text: '组件实例', + link: '/zh/guide/advanced/component-instance' + }, + { + text: '复用与组合', + link: '/zh/guide/advanced/reusability-composition' + }, + { + text: '测试 v-model', + link: '/zh/guide/advanced/v-model' + }, + { + text: '测试 Vuex', + link: '/zh/guide/advanced/vuex' + }, + { + text: '测试 Vue Router', + link: '/zh/guide/advanced/vue-router' + }, + { + text: '测试 Teleport', + link: '/zh/guide/advanced/teleport' + }, + { + text: '测试替身 (stub) 和浅挂载', + link: '/zh/guide/advanced/stubs-shallow-mount' + }, + { + text: '测试服务端渲染', + link: '/zh/guide/advanced/ssr' + } + ] + }, + { + text: '扩展 Vue Test Utils', + items: [ + { + text: '插件', + link: '/zh/guide/extending-vtu/plugins' + }, + { + text: '社区与学习资源', + link: '/zh/guide/extending-vtu/community-learning' + } + ] + }, + { + text: '常见问题', + link: '/zh/guide/faq/' + }, + { + text: '从 Vue 2 迁移', + link: '/zh/migration/' + }, + { + text: 'API 参考', + link: '/zh/api/' + } + ] + } + } +} diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 000000000..d59dd22e2 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,8 @@ +.VPContent { + display: flex; + align-items: center; +} + +.VPHome { + flex-grow: 1; +} \ No newline at end of file diff --git a/docs/.vitepress/theme/index.mts b/docs/.vitepress/theme/index.mts new file mode 100644 index 000000000..716b55a15 --- /dev/null +++ b/docs/.vitepress/theme/index.mts @@ -0,0 +1,19 @@ +import { h } from 'vue' +import Theme from 'vitepress/theme' +import TranslationStatus from 'vitepress-translation-helper/ui/TranslationStatus.vue' +import status from '../translation-status.json' +import './custom.css' +const i18nLabels = { + fr: 'La traduction est synchronisée avec les docs du ${date} dont le hash du commit est ${hash}.', + zh: '该翻译已同步到了 ${date} 的版本,其对应的 commit hash 是 ${hash}。' +} + + +export default { + ...Theme, + Layout() { + return h(Theme.Layout, null, { + 'doc-before': () => h(TranslationStatus, { status, i18nLabels }), + }) + }, +} diff --git a/docs/.vitepress/translation-status.json b/docs/.vitepress/translation-status.json new file mode 100644 index 000000000..b7a6d4c97 --- /dev/null +++ b/docs/.vitepress/translation-status.json @@ -0,0 +1,10 @@ +{ + "fr": { + "hash": "644917a", + "date": "2024-05-28" + }, + "zh": { + "hash": "7c55128", + "date": "2024-11-28" + } +} \ No newline at end of file diff --git a/docs/.vitepress/tsconfig.vitepress.json b/docs/.vitepress/tsconfig.vitepress.json new file mode 100644 index 000000000..1a11e5d76 --- /dev/null +++ b/docs/.vitepress/tsconfig.vitepress.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true + }, + "include": ["./config", "./theme"] +} diff --git a/docs/api/index.md b/docs/api/index.md index 2c91ed740..7e7818207 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -7,12 +7,14 @@ sidebar: auto ## mount Creates a Wrapper that contains the mounted and rendered Vue component to test. +Note that when mocking dates/timers with Vitest, this must be called after +`vi.setSystemTime`. **Signature:** ```ts interface MountingOptions { - attachTo?: HTMLElement | string + attachTo?: Element | string attrs?: Record data?: () => {} extends Data ? any : Data extends object ? Partial : any props?: (RawProps & Props) | ({} extends Props ? null : never) @@ -42,22 +44,45 @@ test('mounts a component', () => { }) ``` -Notice that `mount` accepts a second parameter to define the component's state and app's global configuration. +Notice that `mount` accepts a second parameter to define the component's state configuration. + +**Example : mounting with component props and a Vue App plugin** + +```js +const wrapper = mount(Component, { + props: { + msg: 'world' + }, + global: { + plugins: [vuex] + } +}) +``` + +#### options.global + +Among component state, you can configure the aformentioned Vue 3 app by the [`MountingOptions.global` config property.](#global) This would be useful for providing mocked values which your components expect to have available. + +::: tip +If you find yourself having to set common App configuration for many of your tests, then you can set configuration for your entire test suite using the exported [`config` object.](#config) +::: ### attachTo -Specify the node to mount the component on. +Specify the node to mount the component on. This is not available when using `renderToString`. **Signature:** ```ts -attachTo?: HTMLElement | string +attachTo?: Element | string ``` **Details:** Can be a valid CSS selector, or an [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element) connected to the document. +Note that the component is appended to the node, it doesn't replace the whole content of the node. If you mount the component on the same node in multiple tests - make sure to unmount it after each test by calling `wrapper.unmount()`, this will remove the rendered elements from the node. + `Component.vue`: ```vue @@ -258,7 +283,7 @@ slots?: { [key: string]: Slot } & { default?: Slot } **Details:** -Slots can be a string, a component imported from a `.vue` file or a render function. Currently providing an object with a `template` key is not supported. +Slots can be a string or any valid component definition either imported from a `.vue` file or provided inline `Component.vue`: @@ -281,6 +306,7 @@ Slots can be a string, a component imported from a `.vue` file or a render funct `Component.spec.js`: ```js +import { h } from 'vue'; import { mount } from '@vue/test-utils' import Component from './Component.vue' import Bar from './Bar.vue' @@ -304,19 +330,19 @@ test('renders slots content', () => { ```ts type GlobalMountOptions = { - components?: Record + plugins?: (Plugin | [Plugin, ...any[]])[] config?: Partial> - directives?: Record mixins?: ComponentOptions[] mocks?: Record - plugins?: (Plugin | [Plugin, ...any[]])[] provide?: Record + components?: Record + directives?: Record + stubs?: Stubs = Record | Array renderStubDefaultSlot?: boolean - stubs?: Record } ``` -You can configure all the `global` options on both a per test basis and globally for all tests. [See here for how to configure project wide defaults](/api/#global-config-2). +You can configure all the `global` options on both a per test basis and also for the entire test suite. [See here for how to configure project wide defaults](#config-global). #### global.components @@ -354,9 +380,7 @@ export default { ```vue ``` @@ -406,7 +430,6 @@ directives?: Record ```js import { mount } from '@vue/test-utils' -import Component from './Component.vue' import Directive from '@/directives/Directive' @@ -766,7 +789,7 @@ test('global.stubs using a custom component', () => { ### shallow -Stubs out out all child components from the component. +Stubs out all child components from the component. **Signature:** @@ -806,9 +829,9 @@ test('shallow', () => { const wrapper = mount(Component, { shallow: true }) expect(wrapper.html()).toEqual( - `` + `` ) -} +}) ``` ::: tip @@ -998,11 +1021,17 @@ find(selector: K): DOMWrapper(selector: K): DOMWrapper find(selector: string): DOMWrapper find(selector: string): DOMWrapper +find(selector: string | RefSelector): DOMWrapper; ``` **Details:** -You can use the same syntax `querySelector` implements. `find` is basically an alias for `querySelector`. +You can use the same syntax `querySelector` implements. `find` is basically an alias for `querySelector`. In addition you can search for element refs. + +It is similar to `get`, but `find` returns an ErrorWrapper if an element is not found while [`get`](#get) will throw an error. + +As a rule of thumb, always use `find` when you are asserting something doesn't exist. If you are asserting something does exist, use [`get`](#get). + `Component.vue`: @@ -1010,6 +1039,7 @@ You can use the same syntax `querySelector` implements. `find` is basically an a ``` @@ -1024,6 +1054,7 @@ test('find', () => { wrapper.find('span') //=> found; returns DOMWrapper wrapper.find('[data-test="span"]') //=> found; returns DOMWrapper + wrapper.find({ ref: 'span' }) //=> found; returns DOMWrapper wrapper.find('p') //=> nothing found; returns ErrorWrapper }) ``` @@ -1057,12 +1088,13 @@ findAll(selector: string): DOMWrapper[] ```js import { mount } from '@vue/test-utils' -import Component from './Component.vue' +import BaseTable from './BaseTable.vue' test('findAll', () => { - const wrapper = mount(Component) + const wrapper = mount(BaseTable) - wrapper.findAll('[data-test="number"]') //=> found; returns array of DOMWrapper + // .findAll() returns an array of DOMWrappers + const thirdRow = wrapper.findAll('span')[2] }) ``` @@ -1073,9 +1105,12 @@ Finds a Vue Component instance and returns a `VueWrapper` if found. Returns `Err **Signature:** ```ts -findComponent(selector: new () => T): VueWrapper -findComponent(selector: FindComponentSelector): VueWrapper -findComponent(selector: any): VueWrapper +findComponent(selector: string): WrapperLike +findComponent(selector: T | Exclude>): VueWrapper> +findComponent>(selector: T | string): DOMWrapper +findComponent(selector: NameSelector | RefSelector): VueWrapper +findComponent(selector: T | FindComponentSelector): VueWrapper +findComponent(selector: FindComponentSelector): WrapperLike ``` **Details:** @@ -1143,12 +1178,58 @@ test('findComponent', () => { }) ``` +:::warning +If `ref` in component points to HTML element, `findComponent` will return empty wrapper. This is intended behaviour. +::: + +:::warning Usage with CSS selectors +Using `findComponent` with CSS selector might have confusing behavior + +Consider this example: + +```js +const ChildComponent = { + name: 'Child', + template: '
' +} +const RootComponent = { + name: 'Root', + components: { ChildComponent }, + template: '' +} +const wrapper = mount(RootComponent) +const rootByCss = wrapper.findComponent('.root') // => finds Root +expect(rootByCss.vm.$options.name).toBe('Root') +const childByCss = wrapper.findComponent('.child') +expect(childByCss.vm.$options.name).toBe('Root') // => still Root +``` + +The reason for such behavior is that `RootComponent` and `ChildComponent` are sharing same DOM node and only first matching component is included for each unique DOM node +::: + +:::info WrapperLike type when using CSS selector +When using `wrapper.findComponent('.foo')` for example then VTU will return the `WrapperLike` type. This is because functional components +would need a `DOMWrapper` otherwise a `VueWrapper`. You can force to return a `VueWrapper` by providing the correct component type: + +```typescript +wrapper.findComponent('.foo') // returns WrapperLike +wrapper.findComponent('.foo') // returns VueWrapper +wrapper.findComponent('.foo') // returns VueWrapper +``` +::: + ### findAllComponents **Signature:** ```ts -findAllComponents(selector: { name: string } | string): VueWrapper[] +findAllComponents(selector: string): WrapperLike[] +findAllComponents(selector: T | Exclude>): VueWrapper>[] +findAllComponents>(selector: string): DOMWrapper[] +findAllComponents>(selector: T): DOMWrapper[] +findAllComponents(selector: NameSelector): VueWrapper[] +findAllComponents(selector: T | FindAllComponentsSelector): VueWrapper[] +findAllComponents(selector: FindAllComponentsSelector): WrapperLike[] ``` **Details:** @@ -1183,9 +1264,13 @@ test('findAllComponents', () => { }) ``` +:::warning Usage with CSS selectors +`findAllComponents` has same behavior when used with CSS selector as [findComponent](#findcomponent) +::: + ### get -Gets an an element and returns a `DOMWrapper` if found. Otherwise it throws an error. +Gets an element and returns a `DOMWrapper` if found. Otherwise it throws an error. **Signature:** @@ -1198,9 +1283,9 @@ get(selector: string): Omit, 'exists'> **Details:** -It is similar to `find`, but `get` throws instead of returning a ErrorWrapper. +It is similar to `find`, but `get` throws an error if an element is not found while [`find`](#find) will return an ErrorWrapper. -As a rule of thumb, always use get except when you are asserting something doesn't exist. In that case use [`find`](#find). +As a rule of thumb, always use `get` except when you are asserting something doesn't exist. In that case use [`find`](#find). `Component.vue`: @@ -1239,7 +1324,7 @@ getComponent(selector: any): Omit { Returns the HTML of an element. +By default the output is formatted with [`js-beautify`](https://github.com/beautify-web/js-beautify) +to make snapshots more readable. Use `raw: true` option to receive the unformatted html string. + **Signature:** ```ts html(): string +html(options?: { raw?: boolean }): string ``` **Details:** @@ -1329,7 +1418,13 @@ import Component from './Component.vue' test('html', () => { const wrapper = mount(Component) - expect(wrapper.html()).toBe('

Hello world

') + expect(wrapper.html()).toBe( + '
\n' + + '

Hello world

\n' + + '
' + ) + + expect(wrapper.html({ raw: true })).toBe('

Hello world

') }) ``` @@ -1345,15 +1440,21 @@ isVisible(): boolean **Details:** +::: warning +`isVisible()` only works correctly if the wrapper is attached to the DOM using [`attachTo`](#attachTo) +::: + ```js const Component = { template: `
` } test('isVisible', () => { - const wrapper = mount(Component) + const wrapper = mount(Component, { + attachTo: document.body + }); - expect(wrapper.find('span').isVisible()).toBe(false) + expect(wrapper.find('span').isVisible()).toBe(false); }) ``` @@ -1440,7 +1541,9 @@ setData(data: Record): Promise `setData` does not allow setting new properties that are not defined in the component. +::: warning Also, notice that `setData` does not modify composition API `setup()` data. +::: `Component.vue`: @@ -1543,7 +1646,7 @@ Sets a value on DOM element. Including: **Signature:** ```ts -setValue(value: any, prop?: string): Promise +setValue(value: unknown, prop?: string): Promise ``` **Details:** @@ -1557,6 +1660,12 @@ setValue(value: any, prop?: string): Promise
The input has been checked!
+ + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('data', () => { + const wrapper = mount(Component, { + data() { + return { + message: 'vous' + }; + }, + }); + + expect(wrapper.html()).toContain('Bonjour vous'); +}); +``` + +### props + +Définie les `props` d'un composant lorsqu'il est monté. + +**Signature :** + +```ts +props?: (RawProps & Props) | ({} extends Props ? null : never) +``` + +**Utilisation :** + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('props', () => { + const wrapper = mount(Component, { + props: { + count: 5, + }, + }); + + expect(wrapper.html()).toContain('Compteur: 5'); +}); +``` + +### slots + +Définie les valeurs des slots sur un composant. + +**Signature :** + +```ts +type Slot = VNode | string | { render: Function } | Function | Component + +slots?: { [key: string]: Slot } & { default?: Slot } +``` + +**Utilisation :** + +Les `slots` peuvent être une `string` ou toute définition de composant valide importée d'un fichier `.vue` ou directement fournie. + +`Component.vue`: + +```vue + +``` + +`Bar.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { h } from 'vue'; +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; +import Bar from './Bar.vue'; + +test('affiche le contenu du slot', () => { + const wrapper = mount(Component, { + slots: { + default: 'Défaut', + first: h('h1', {}, 'Slot Nommé'), + second: Bar, + }, + }); + + expect(wrapper.html()).toBe('

Slot Nommé

Défaut
Bar
'); +}); +``` + +### global + +**Signature :** + +```ts +type GlobalMountOptions = { + plugins?: (Plugin | [Plugin, ...any[]])[] + config?: Partial> + mixins?: ComponentOptions[] + mocks?: Record + provide?: Record + components?: Record + directives?: Record + stubs?: Stubs = Record | Array + renderStubDefaultSlot?: boolean +}; +``` + +Vous pouvez configurer toutes les options `global` à la fois pour chaque test, mais aussi pour l'ensemble des tests. [Voir comment configurer les valeurs par défaut à l'échelle du projet](#config-global). + +#### global.components + +Enregistre les composants de manière globale pour le composant monté. + +**Signature :** + +```ts +components?: Record +``` + +**Utilisation :** + +`Component.vue`: + +```vue + + + +``` + +`GlobalComponent.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import GlobalComponent from '@/components/GlobalComponent'; +import Component from './Component.vue'; + +test('global.components', () => { + const wrapper = mount(Component, { + global: { + components: { + GlobalComponent, + }, + }, + }); + + expect(wrapper.find('.global-component').exists()).toBe(true); +}); +``` + +#### global.config + +Configure [la configuration globale de l'application Vue](https://v3.vuejs.org/api/application-config.html#application-config). + +**Signature :** + +```ts +config?: Partial> +``` + +#### global.directives + +Enregistre une [directive](https://v3.vuejs.org/api/directives.html#directives) de manière globale pour le composant monté. + +**Signature :** + +```ts +directives?: Record +``` + +**Utilisation :** + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; + +import Directive from '@/directives/Directive'; + +const Component = { + template: '
Foo
', +}; + +test('global.directives', () => { + const wrapper = mount(Component, { + global: { + directives: { + Bar: Directive // Bar correspond à v-bar + }, + }, + }); +}); +``` + +#### global.mixins + +Enregistre un [mixin](https://v3.vuejs.org/guide/mixins.html) de manière globale pour le composant monté. + +**Signature :** + +```ts +mixins?: ComponentOptions[] +``` + +**Utilisation :** + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('global.mixins', () => { + const wrapper = mount(Component, { + global: { + mixins: [mixin], + }, + }); +}); +``` + +#### global.mocks + +Simule une propriété d'instance globale. Peut être utilisé pour simuler `this.$store`, `this.$router`, etc. + +**Signature :** + +```ts +mocks?: Record +``` + +**Utilisation :** + +::: warning +Ceci est conçu pour simuler des variables injectées par des plugins tiers, pas les propriétés natives de Vue telles que `$root`, `$children`, etc. +::: + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('global.mocks', async () => { + const $store = { + dispatch: jest.fn(), + }; + + const wrapper = mount(Component, { + global: { + mocks: { + $store, + }, + }, + }); + + await wrapper.find('button').trigger('click'); + + expect($store.dispatch).toHaveBeenCalledWith('click'); +}); +``` + +#### global.plugins + +Installe des plugins sur le composant monté. + +**Signature :** + +```ts +plugins?: (Plugin | [Plugin, ...any[]])[] +``` + +**Utilisation :** + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +import myPlugin from '@/plugins/myPlugin'; + +test('global.plugins', () => { + mount(Component, { + global: { + plugins: [myPlugin], + }, + }); +}); +``` + +Pour utiliser des plugins avec des options, un tableau d'options peut être passé. + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('global.plugins avec options', () => { + mount(Component, { + global: { + plugins: [Plugin, [PluginWithOptions, 'argument 1', 'un autre argument']], + }, + }); +}); +``` + +#### global.provide + +Fournit des données utilisables dans la fonction `setup` via `inject`. + +**Signature :** + +```ts +provide?: Record +``` + +**Utilisation :** + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('global.provide', () => { + const wrapper = mount(Component, { + global: { + provide: { + Theme: 'sombre', + }, + }, + }); + + console.log(wrapper.html()); //=>
Le thème est sombre
+}); +``` + +Si vous utilisez un `Symbol` ES6 pour votre clé, vous pouvez l'utiliser dynamiquement : + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +const ThemeSymbol = Symbol(); + +mount(Component, { + global: { + provide: { + [ThemeSymbol]: 'value', + }, + }, +}); +``` + +#### global.renderStubDefaultSlot + +Affiche le contenu du `slot default`, même en utilisant `shallow` ou `shallowMount`. + +**Signature :** + +```ts +renderStubDefaultSlot?: boolean +``` + +**Utilisation :** + +**false** par défaut. + +`Component.vue` + +```vue + + + +``` + +`AnotherComponent.vue` + +```vue + +``` + +`Component.spec.js` + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('global.renderStubDefaultSlot', () => { + const wrapper = mount(ComponentWithSlots, { + slots: { + default: '
Mon contenu
', + }, + shallow: true, + global: { + renderStubDefaultSlot: true, + }, + }); + + expect(wrapper.html()).toBe( + '
Mon contenu
' + ); +}); +``` + +En raison de limitations techniques, **ce comportement ne peut pas être étendu aux slots autres que ceux par défaut**. + +#### global.stubs + +Définit un composant de remplacement (`stub`) global sur le composant monté. + +**Signature :** + +```ts +stubs?: Record +``` + +**Utilisation :** + +Substitue `Transition` et `TransitionGroup` par défaut. + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('global.stubs en utilisant la syntaxe en tableau', () => { + const wrapper = mount(Component, { + global: { + stubs: ['Foo'], + }, + }); + + expect(wrapper.html()).toEqual('
'); +}); + +test('global.stubs en utilisant la syntaxe en objet', () => { + const wrapper = mount(Component, { + global: { + stubs: { Foo: true }, + }, + }); + + expect(wrapper.html()).toEqual('
'); +}); + +test('global.stubs en utilisant un composant personnalisé', () => { + const CustomStub = { + name: 'CustomStub', + template: '

Contenu personnalisé du composant de substitution

', + } + + const wrapper = mount(Component, { + global: { + stubs: { Foo: CustomStub }, + }, + }); + + expect(wrapper.html()).toEqual('

Contenu personnalisé du composant de substitution

'); +}); +``` + +### shallow + +Substitue tous les composants enfants du composant. + +**Signature :** + +```ts +shallow?: boolean +``` + +**Utilisation :** + +**false** par défaut. + +`Component.vue` + +```vue + + + +``` + +`Component.spec.js` + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('shallow', () => { + const wrapper = mount(Component, { shallow: true }); + + expect(wrapper.html()).toEqual( + ``, + ); +}); +``` + +::: tip +Utiliser `shallowMount()` revient à monter un composant avec l'option `shallow: true`. +::: + +## Méthodes de Wrapper + +Lorsque vous utilisez `mount`, un `VueWrapper` est retourné avec un certain nombre de méthodes utiles pour les tests. Un `VueWrapper` est une enveloppe autour de votre instance de composant. + +Notez que des méthodes telles que `find` retournent un `DOMWrapper`, qui est une enveloppe autour des nœuds DOM dans votre composant et ses enfants. Les deux implémentent une API similaire. + +### attributes + +Retourne les attributs d'un nœud du DOM. + +**Signature :** + +```ts +attributes(): { [key: string]: string } +attributes(key: string): string +attributes(key?: string): { [key: string]: string } | string +``` + +**Utilisation :** + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('attributs', () => { + const wrapper = mount(Component); + + expect(wrapper.attributes('id')).toBe('foo'); + expect(wrapper.attributes('class')).toBe('bar'); +}); +``` + +### classes + +**Signature :** + +```ts +classes(): string[] +classes(className: string): boolean +classes(className?: string): string[] | boolean +``` + +**Utilisation :** + +Retourne les classes d'un élément sous la forme d'un tableau. + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('classes', () => { + const wrapper = mount(Component) + + expect(wrapper.classes()).toContain('my-span'); + expect(wrapper.classes('my-span')).toBe(true); + expect(wrapper.classes('not-existing')).toBe(false); +}); +``` + +### emitted + +Retourne tous les évènements émis par un Composant. + +**Signature :** + +```ts +emitted(): Record +emitted(eventName: string): undefined | T[] +emitted(eventName?: string): undefined | T[] | Record +``` + +**Utilisation :** + +Les arguments sont stockés dans un tableau, de sorte que vous pouvez vérifier les arguments qui ont été émis avec chaque événement. + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('emitted', () => { + const wrapper = mount(Component) + + // wrapper.emitted() sera égal à { politesse: [ ['bonjour'], ['au revoir'] ] } + + expect(wrapper.emitted()).toHaveProperty('politesse'); + expect(wrapper.emitted().greet).toHaveLength(2); + expect(wrapper.emitted().greet[0]).toEqual(['bonjour']); + expect(wrapper.emitted().greet[1]).toEqual(['au revoir']); +}); +``` + +### exists + +Vérifie si un élément existe ou non. + +**Signature :** + +```ts +exists(): boolean +``` + +**Utilisation :** + +Nous pouvons utiliser la même syntaxe que `querySelector` implémente. + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('existe', () => { + const wrapper = mount(Component); + + expect(wrapper.find('span').exists()).toBe(true); + expect(wrapper.find('p').exists()).toBe(false); +}); +``` + +### find + +Cherche un élément et retourne un `DOMWrapper` si un élément est trouvé. + +**Signature :** + +```ts +find(selector: K): DOMWrapper +find(selector: K): DOMWrapper +find(selector: string): DOMWrapper +find(selector: string): DOMWrapper +find(selector: string | RefSelector): DOMWrapper; +``` + +**Utilisation :** + +Vous pouvez utiliser la même syntaxe qu'implémente `querySelector`. `find` est en quelque sorte un alias pour `querySelector`. En plus, vous pouvez rechercher des références d'éléments. + +Il est similaire à `get`, mais `find` retourne un `ErrorWrapper` si un élément n'est pas trouvé tandis que [`get`](#get) lancera une erreur. + +En règle générale, utilisez toujours `find` lorsque vous souhaitez vérifier que quelque chose n'existe pas. Si vous savez que quelque chose existe, utilisez [`get`](#get). + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('cherche', () => { + const wrapper = mount(Component); + + wrapper.find('span') //=> trouvé; retourne DOMWrapper + wrapper.find('[data-test="span"]') //=> trouvé; retourne DOMWrapper + wrapper.find({ ref: 'span' }); //=> trouvé; retourne DOMWrapper + wrapper.find('p') //=> rien de trouvé; retourne ErrorWrapper +}); +``` + +### findAll + +Similaire à `find`, mais retourne un tableau de `DOMWrapper`. + +**Signature :** + +```ts +findAll(selector: K): DOMWrapper[] +findAll(selector: K): DOMWrapper[] +findAll(selector: string): DOMWrapper[] +findAll(selector: string): DOMWrapper[] +``` + +**Utilisation :** + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import BaseTable from './BaseTable.vue'; + +test('trouve plusieurs éléments', () => { + const wrapper = mount(BaseTable); + + // .findAll() retourne un tableau de DOMWrappers + const thirdRow = wrapper.findAll('span')[2]; +}); +``` + +### findComponent + +Trouve une instance de composant Vue et renvoie un `VueWrapper` si trouvé. Renvoie un `ErrorWrapper` sinon. + +**Signature :** + +```ts +findComponent(selector: string): WrapperLike +findComponent(selector: T | Exclude>): VueWrapper> +findComponent>(selector: T | string): DOMWrapper +findComponent(selector: NameSelector | RefSelector): VueWrapper +findComponent(selector: T | FindComponentSelector): VueWrapper +findComponent(selector: FindComponentSelector): WrapperLike +``` + +**Utilisation :** + +`findComponent` supporte plusieurs syntaxes : + +| Syntaxe | Exemple | Détails | +|------------------|-------------------------------|----------------------------------------------------------| +| querySelector | `findComponent('.component')` | Trouve comme à un sélecteur CSS standard. | +| Nom du Composant | `findComponent({name: 'a'})` | Trouve en PascalCase, snake-case et camelCase. | +| Ref du Composant | `findComponent({ref: 'ref'})` | Trouve la reference directe d'un composant enfant monté. | +| SFC | `findComponent(Component)` | Trouve directement un composant importé. | + +`Foo.vue` + +```vue + + + +``` + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js` + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +import Foo from '@/Foo.vue'; + +test('trouve le composant', () => { + const wrapper = mount(Component); + + // Toutes les requêtes suivantes retourneront un `VueWrapper` + + wrapper.findComponent('.foo'); + wrapper.findComponent('[data-test="foo"]'); + + wrapper.findComponent({ name: 'Foo' }); + + wrapper.findComponent({ ref: 'foo' }); + + wrapper.findComponent(Foo); +}); +``` + +:::warning +Si `ref` dans le composant pointe vers un élément HTML, `findComponent` renverra un conteneur vide. C'est le comportement attendu. +::: + +:::warning Utilisation avec des sélecteurs CSS +L'utilisation de `findComponent` avec un sélecteur CSS peut avoir un comportement innatendu. + +Voici un exemple : + +```js +const ChildComponent = { + name: 'Child', + template: '
', +}; +const RootComponent = { + name: 'Root', + components: { ChildComponent }, + template: '', +}; +const wrapper = mount(RootComponent); +const rootByCss = wrapper.findComponent('.root'); // => trouve Root +expect(rootByCss.vm.$options.name).toBe('Root'); +const childByCss = wrapper.findComponent('.child'); +expect(childByCss.vm.$options.name).toBe('Root'); // => toujours Root +``` + +La raison de ce comportement est que `RootComponent` et `ChildComponent` partagent le même nœud DOM et que seul le premier composant correspondant est inclus pour chaque nœud DOM unique. +::: + +:::info Type WrapperLike en utilisant un sélecteur CSS +Lors de l'utilisation de `wrapper.findComponent('.foo')` par exemple, VTU renverra le type `WrapperLike`. Cela est dû au fait que les composants fonctionnels auraient besoin d'un `DOMWrapper` au lieu d'un `VueWrapper`. Vous pouvez forcer le retour d'un `VueWrapper` en fournissant le type de composant correct : + +```typescript +wrapper.findComponent('.foo'); // retourne WrapperLike +wrapper.findComponent('.foo'); // retourne VueWrapper +wrapper.findComponent('.foo'); // retourne VueWrapper +``` +::: + +### findAllComponents + +**Signature :** + +```ts +findAllComponents(selector: string): WrapperLike[] +findAllComponents(selector: T | Exclude>): VueWrapper>[] +findAllComponents>(selector: string): DOMWrapper[] +findAllComponents>(selector: T): DOMWrapper[] +findAllComponents(selector: NameSelector): VueWrapper[] +findAllComponents(selector: T | FindAllComponentsSelector): VueWrapper[] +findAllComponents(selector: FindAllComponentsSelector): WrapperLike[] +``` + +**Utilisation :** + +Similaire à `findComponent` mais trouve toutes les instances de composant Vue qui correspondent à la requête. Renvoie un tableau de `VueWrapper`. + +:::warning +La syntaxe `ref` n'est pas prise en charge dans findAllComponents. Toutes les autres syntaxes de requête sont valides. +::: + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('trouve tous les composants', () => { + const wrapper = mount(Component); + + // Retourne un tableau de VueWrapper + wrapper.findAllComponents('[data-test="number"]'); +}); +``` + +:::warning Utilisation avec des sélecteurs CSS +`findAllComponents` a le même comportement lorsqu'il est utilisé avec un sélecteur CSS tout comme [findComponent](#findcomponent). +::: + +### get + +Récupère un élément et retourne un `DOMWrapper` si trouvé. Renvoie une erreur dans le cas contraire. + +**Signature :** + +```ts +get(selector: K): Omit, 'exists'> +get(selector: K): Omit, 'exists'> +get(selector: string): Omit, 'exists'> +get(selector: string): Omit, 'exists'> +``` + +**Utilisation :** + +It is similar to `find`, but `get` throws an error if an element is not found while [`find`](#find) will return an ErrorWrapper. +Similaire à `find`, mais `get` renvoie une erreur si un élément n'est pas trouvé tandis que [`find`](#find) renverra un `ErrorWrapper`. + +En règle générale, utilisez toujours `get` sauf lorsque vous voulez vérifier qu'un élément n'existe pas. Dans ce cas, utilisez [`find`](#find). + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('get', () => { + const wrapper = mount(Component); + + wrapper.get('span'); //=> trouvé; retourne DOMWrapper + + expect(() => wrapper.get('.not-there')).toThrowError(); +}); +``` + +### getComponent + +Récupère une instance de composant Vue et renvoie un `VueWrapper` si trouvé. Sinon, il génère une erreur. + +**Signature :** + +```ts +getComponent(selector: new () => T): Omit, 'exists'> +getComponent(selector: { name: string } | { ref: string } | string): Omit, 'exists'> +getComponent(selector: any): Omit, 'exists'> +``` + +**Utilisation :** + +Similaire à `findComponent`, mais `getComponent` renvoie une erreur si une instance de composant Vue n'est pas trouvée tandis que [`findComponent`](#findComponent) renverra un `ErrorWrapper`. + +**Syntaxes supportées :** + +| Syntaxe | Exemple | Détails | +|------------------|------------------------------|----------------------------------------------------------| +| querySelector | `getComponent('.component')` | Trouve comme à un sélecteur CSS standard. | +| Nom du Composant | `getComponent({name: 'a'})` | Trouve en PascalCase, snake-case et camelCase. | +| Ref du Composant | `getComponent({ref: 'ref'})` | Trouve la reference directe d'un composant enfant monté. | +| SFC | `getComponent(Component)` | Trouve directement un composant importé. | + +`Foo.vue` + +```vue + + + +``` + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js` + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +import Foo from '@/Foo.vue'; + +test('getComponent', () => { + const wrapper = mount(Component); + + wrapper.getComponent({ name: 'foo' }); // retourne un VueWrapper + wrapper.getComponent(Foo); // retourne un VueWrapper + + expect(() => wrapper.getComponent('.not-there')).toThrowError(); +}); +``` + +### html + +Renvoie le HTML d'un élément. + +Par défaut, la sortie est formatée avec [`js-beautify`](https://github.com/beautify-web/js-beautify) pour rendre les `snapshots` plus lisibles. Utilisez l'option `raw: true` pour recevoir la chaîne HTML non formatée. + +**Signature :** + +```ts +html(): string +html(options?: { raw?: boolean }): string +``` + +**Utilisation :** + +`Component.vue`: + +```vue + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('html', () => { + const wrapper = mount(Component); + + expect(wrapper.html()).toBe( + '
\n' + + '

Bonjour tout le monde

\n' + + '
' + ); + + expect(wrapper.html({ raw: true })).toBe('

Bonjour tout le monde

'); +}); +``` + +### isVisible + +Vérifie si un élément est visible ou non. + +**Signature :** + +```ts +isVisible(): boolean +``` + +**Utilisation :** + +::: warning +`isVisible()` ne fonctionne correctement que si le wrapper est attaché au DOM en utilisant [`attachTo`](#attachTo) +::: + +```js +const Component = { + template: `
`, +}; + +test('isVisible', () => { + const wrapper = mount(Component, { + attachTo: document.body + }); + + expect(wrapper.find('span').isVisible()).toBe(false); +}); +``` + +### props + +Retourne les propriétés (`props`) passées à un Composant Vue. + +**Signature :** + +```ts +props(): { [key: string]: any } +props(selector: string): any +props(selector?: string): { [key: string]: any } | any +``` + +**Utilisation :** + +`Component.vue`: + +```js +export default { + name: 'Component', + props: { + truthy: Boolean, + object: Object, + string: String, + }, +}; +``` + +```vue + + + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('props', () => { + const wrapper = mount(Component, { + global: { stubs: ['Foo'] }, + }); + + const foo = wrapper.getComponent({ name: 'Foo' }); + + expect(foo.props('truthy')).toBe(true); + expect(foo.props('object')).toEqual({}); + expect(foo.props('notExisting')).toEqual(undefined); + expect(foo.props()).toEqual({ + truthy: true, + object: {}, + string: 'string', + }); +}); +``` + +:::tip +En règle générale, testez les effets d'une `prop` transmise (une mise à jour du DOM, un événement émis, etc.). Cela rendra les tests plus puissants que de simplement vérifier qu'une prop a été transmise. +::: + +### setData + +Met à jour les données internes d'un composant. + +**Signature :** + +```ts +setData(data: Record): Promise +``` + +**Utilisation :** + +`setData` ne permet pas de définir de nouvelles propriétés qui ne sont pas définies dans le composant. + +De plus, notez que `setData` ne modifie pas les données de la fonction l'API de Composition : `setup()`. + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js`: + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('setData', async () => { + const wrapper = mount(Component); + expect(wrapper.html()).toContain('Compteur: 0'); + + await wrapper.setData({ count: 1 }); + + expect(wrapper.html()).toContain('Compteur: 1'); +}); +``` + +::: warning +Vous devriez utiliser `await` lorsque vous appelez `setData` pour vous assurer que Vue met à jour le DOM avant de faire une vérification. +::: + +### setProps + +Met à jour les `props` d'un composant. + +**Signature :** + +```ts +setProps(props: Record): Promise +``` + +**Utilisation :** + +`Component.vue`: + +```vue + + + +``` + +`Component.spec.js` + +```js +import { mount } from '@vue/test-utils'; +import Component from './Component.vue'; + +test('met à jour les props', async () => { + const wrapper = mount(Component, { + props: { + message: 'Bonjour' + }, + }); + + expect(wrapper.html()).toContain('Bonjour'); + + await wrapper.setProps({ message: 'Au revoir' }); + + expect(wrapper.html()).toContain('Au revoir'); +}); +``` + +::: warning +Vous devriez utiliser `await` lorsque vous appelez `setProps` pour vous assurer que Vue met à jour le DOM avant de faire une vérification. +::: + +### setValue + +Définit une valeur sur un élément du DOM. Incluant : + +- `` + - `type="checkbox"` et `type="radio"` sont détectés et auront `element.checked` de coché. +- `