diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 6bb15f5cc..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = { - env: { - node: true - }, - extends: [ - 'prettier', - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended' - ], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module' - }, - plugins: ['prettier', '@typescript-eslint'], - rules: { - 'prettier/prettier': ['error'], - '@typescript-eslint/no-extra-semi': 'off', - 'no-restricted-imports': [ - 'error', - { - patterns: ['src/*'] - } - ], - - // Currently, disabled to avoid a lot of changes during migration - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'prefer-const': 'off', - 'no-prototype-builtins': 'off' - } -} diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..67bf729cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,50 @@ +name: Bug report +description: Create a report to help us improve +labels: ['bug'] +title: 'Bug: ' +type: 'Bug' +body: + - type: markdown + attributes: + value: | + Thanks for your interest in the project! We appreciate bugs filed and PRs submitted! + + Please use a template below to create a minimal reproduction + 👉 https://stackblitz.com/github/vuejs/create-vue-templates/tree/main/typescript-vitest?file=src%2Fcomponents%2F__tests__%2FHelloWorld.spec.ts + + For test-utils v1 and Vue v2, please report at: + 👉 https://github.com/vuejs/vue-test-utils + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: input + id: reproduction + attributes: + label: Reproduction + placeholder: https://stackblitz.com/... + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + - type: textarea + id: environment + attributes: + label: Environment + placeholder: Environment + description: Output of `npx envinfo --system --npmPackages vue,@vue/test-utils,vitest,jest`. + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e7e8c63e9..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -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:** -- `@vue/test-utils` version: 2.x.x -- `Vue` version: 3.x.x -- `node` version: -- `npm` (or `yarn`) version: - -**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-requests.yml b/.github/ISSUE_TEMPLATE/feature-requests.yml new file mode 100644 index 000000000..3bbc903f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-requests.yml @@ -0,0 +1,33 @@ +name: Feature request +description: Suggest an idea for this project +type: Feature +title: 'Feature request: ' +body: + - type: markdown + attributes: + value: | + Thanks for your interest in the project – we appreciate your ideas! Please bear in mind that maintainers might need to request additional information or context before adding a new feature. They need to think about edge cases and how this feature fits in the codebase, so a feature request might take some time to move forward. + - type: textarea + id: feature-description + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + id: feature-solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: feature-alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: feature-additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 89a960818..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -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 index cef9edbd4..b1c0e1356 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,6 +1,6 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base"], + "extends": ["config:js-lib"], // only 1 PR at the same time (to avoid cascading rebase) "prConcurrentLimit": 1, // auto-merge if build is OK diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e2fd9b02..fc2dff288 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,25 +16,23 @@ jobs: strategy: matrix: - node: [14, 16] + node: [20, 22, 24] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install pnpm - uses: pnpm/action-setup@v2.2.2 - with: - version: 7.5.0 + uses: pnpm/action-setup@v4.1.0 - name: Use Node.js ${{ matrix.node }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: 'pnpm' - run: pnpm install - run: pnpm run lint - run: pnpm run test:coverage - - run: pnpm run build env: CI: true - 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 621d577e1..d1f75bfb8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist coverage +docs/.vitepress/cache diff --git a/README.md b/README.md index eedbb618d..67522c817 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ 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 --dev` @@ -21,63 +25,19 @@ See the [docs](https://test-utils.vuejs.org/). Get started by running `pnpm install`. You can run the tests with `pnpm test`. That's it! -## Comparison with Vue Test Utils v1 (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 | ✅ | -| 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 | ⚰️ | no longer required - Vue 3 there is no global Vue instance to mutate. | -| 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 | ⚰️ | +## Contributing Docs + +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): + +- [`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). + +- `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/docs/.vitepress/config.ts b/docs/.vitepress/config.ts deleted file mode 100644 index 53f6f5fd7..000000000 --- a/docs/.vitepress/config.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { defineConfig } from 'vitepress' -import packageJSON from '../../package.json' - -export default defineConfig({ - title: `Vue Test Utils`, - description: 'The documentation for the official Vue Test Utils', - locales: { - '/': { - lang: 'en-US', - title: `Vue Test Utils` - } - }, - head: [['link', { rel: 'icon', href: `/logo.png` }]], - themeConfig: { - repo: 'vuejs/test-utils', - docsRepo: 'vuejs/test-utils', - docsDir: 'docs', - docsBranch: 'main', - editLinks: true, - algolia: { - appId: 'BH4D9OD16A', - 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/test-utils/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: 'Testing Teleport', link: '/guide/advanced/teleport' }, - { - 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/' - } - ] - } -}) 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 a5826c6e2..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) @@ -44,7 +46,7 @@ test('mounts a component', () => { Notice that `mount` accepts a second parameter to define the component's state configuration. -**Example: mounting with component props and a Vue App plugin** +**Example : mounting with component props and a Vue App plugin** ```js const wrapper = mount(Component, { @@ -67,18 +69,20 @@ If you find yourself having to set common App configuration for many of your tes ### 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 @@ -825,9 +829,9 @@ test('shallow', () => { const wrapper = mount(Component, { shallow: true }) expect(wrapper.html()).toEqual( - `` + `` ) -} +}) ``` ::: tip @@ -1024,6 +1028,11 @@ find(selector: string | RefSelector): DOMWrapper; 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`: ```vue @@ -1097,8 +1106,8 @@ Finds a Vue Component instance and returns a `VueWrapper` if found. Returns `Err ```ts findComponent(selector: string): WrapperLike -findComponent(selector: T | Exclude): VueWrapper> -findComponent(selector: T | string): DOMWrapper +findComponent(selector: T | Exclude>): VueWrapper> +findComponent>(selector: T | string): DOMWrapper findComponent(selector: NameSelector | RefSelector): VueWrapper findComponent(selector: T | FindComponentSelector): VueWrapper findComponent(selector: FindComponentSelector): WrapperLike @@ -1170,7 +1179,7 @@ test('findComponent', () => { ``` :::warning -If `ref` in component points to HTML element, `findComponent` will return empty wrapper. This is intended behaviour +If `ref` in component points to HTML element, `findComponent` will return empty wrapper. This is intended behaviour. ::: :::warning Usage with CSS selectors @@ -1215,9 +1224,9 @@ wrapper.findComponent('.foo') // returns VueWrapper ```ts findAllComponents(selector: string): WrapperLike[] -findAllComponents(selector: T | Exclude): VueWrapper>[] -findAllComponents(selector: string): DOMWrapper[] -findAllComponents(selector: T): DOMWrapper[] +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[] @@ -1274,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`: @@ -1315,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:** @@ -1405,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

') }) ``` @@ -1421,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); }) ``` @@ -1516,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`: @@ -1633,6 +1660,12 @@ setValue(value: unknown, 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é. +- `