diff --git a/assets/img/guides/component-testing/cy-mount-must-be-implemented.png b/assets/img/guides/component-testing/cy-mount-must-be-implemented.png new file mode 100644 index 0000000000..ae885f0a88 Binary files /dev/null and b/assets/img/guides/component-testing/cy-mount-must-be-implemented.png differ diff --git a/components/MainContentHeader.vue b/components/MainContentHeader.vue index 3e63d142b7..0ba5ac4167 100644 --- a/components/MainContentHeader.vue +++ b/components/MainContentHeader.vue @@ -8,6 +8,10 @@ export default { e2eSpecific: { type: Boolean, required: false, + }, + componentSpecific: { + type: Boolean, + required: false, } } } @@ -19,5 +23,6 @@ export default { {{ title }} + diff --git a/components/global/CodeGroupVue2Vue3.vue b/components/global/CodeGroupVue2Vue3.vue new file mode 100644 index 0000000000..b4a72dc539 --- /dev/null +++ b/components/global/CodeGroupVue2Vue3.vue @@ -0,0 +1,12 @@ + diff --git a/content/_data/sidebar.json b/content/_data/sidebar.json index 4a80ec5191..0e16c06a90 100644 --- a/content/_data/sidebar.json +++ b/content/_data/sidebar.json @@ -59,10 +59,6 @@ { "title": "Rendering Components Correctly", "slug": "rendering-components-correctly" - }, - { - "title": "Creating a cy.mount Command", - "slug": "creating-a-cy-mount-command" } ] }, @@ -572,6 +568,10 @@ "title": "log", "slug": "log" }, + { + "title": "mount", + "slug": "mount" + }, { "title": "next", "slug": "next" diff --git a/content/api/commands/mount.md b/content/api/commands/mount.md new file mode 100644 index 0000000000..a565248a2d --- /dev/null +++ b/content/api/commands/mount.md @@ -0,0 +1,632 @@ +--- +title: mount +componentSpecific: true +--- + + + +Cypress does not have a `cy.mount()` command out-of-the-box. See below for info +on how to craft your own. + + + +For +[Component Testing](/guides/overview/choosing-testing-type#What-is-Component-Testing), +we recommend creating a custom `cy.mount()` command which wraps the mount +command from the framework-specific libraries in your component tests. Doing so +offers a few advantages: + +- You don't need to import the mount command into every test as the `cy.mount()` + command is available globally. +- You can set up common scenarios that you usually have to do in each test, like + wrapping a component in a + [React Provider](https://reactjs.org/docs/context.html) or adding + [Vue plugins](https://vuejs.org/v2/guide/plugins.html). + +If you attempt to use `cy.mount()` before creating it, you will get a warning: + +cy.mount() must be implemented by the user. + +Let's take a look at how to implement the command. + +## Creating a New `cy.mount()` Command + +To use `cy.mount()` you will need to add a +[custom command](/api/cypress-api/custom-commands) to the commands file. Below +are examples that you can start with for your commands: + + + + + + +## Adding TypeScript Typings for `cy.mount()` Commands + +When working in +[TypeScript](https://docs.cypress.io/guides/tooling/typescript-support), you +will need to add custom typings for your commands to get code completion and to +avoid any TypeScript errors. + +The typings will need to be in a location that any code can access, therefore, +we recommend creating a `cypress.d.ts` file in the root directory, and use this +example as a starting point for customizing your own command: + + + + + + +If your tests have trouble finding the types for the custom commands, manually +include the `cypress.d.ts` file in all your `tsconfig.json` files like so: + +```json +"include": ["./src", "cypress.d.ts"] +``` + +## Additional Mount Commands + +You're not limited to a single `cy.mount()` command. If needed, you can create +any number of custom mount commands, as long as they have unique names. + +Below are some examples for common uses cases and libraries. + +## React Examples + +If your React component relies on provider to work properly, you will need to +wrap your component in that provider in your component tests. This is a good use +case to create a custom mount command that wraps your components for you. + +Below are a few examples that demonstrate how. These examples can be adjusted +for most other providers that you will need to support. + +### React Router + +If you have a component that consumes a hook or component from +[React Router](https://reactrouter.com/), you will need to make sure the +component has access to a React Router provider. Below is a sample mount command +that uses `MemoryRouter` to wrap the component. Setup props for `MemoryRouter` +can be passed in the options param as well: + +```jsx +import { mount } from '@cypress/react' +import { MemoryRouter } from 'react-router-dom' + +Cypress.Commands.add('mountWithRouter', (component, options = {}) => { + const { routerProps = { initialEntries: ['/'] }, ...mountOptions } = options + + const wrapped = {component} + + return mount(wrapped, mountOptions) +}) +``` + +Typings: + +```ts +import { MountOptions, MountReturn } from '@cypress/react' +import { MemoryRouterProps } from 'react-router-dom' + +declare global { + namespace Cypress { + interface Chainable { + /** + * Mounts a React node + * @param jsx React Node to mount + * @param options Additional options to pass into mount + */ + mountWithRouter( + jsx: React.ReactNode, + options?: MountOptions & { routerProps?: MemoryRouterProps } + ): Cypress.Chainable + } + } +} +``` + +In this setup, you can pass in custom props for the `MemoryRouter` in the +`options` param. Below is an example test that ensures an active link has the +correct class applied to it by initializing the router with `initialEntries` +pointed to a particular route: + +```jsx +import { Navigation } from './Navigation' + +it('home link should be active when url is "/"', () => { + // No need to pass in custom initialEntries as default url is '/' + cy.mountWithRouter() + + cy.get('a').contains('Home').should('have.class', 'active') +}) + +it('login link should be active when url is "/login"', () => { + cy.mountWithRouter(, { + routerProps: { + initialEntries: ['/login'], + }, + }) + + cy.get('a').contains('Login').should('have.class', 'active') +}) +``` + +### Redux + +To use a component that consumes state or actions from a +[Redux](https://react-redux.js.org/) store, you can create a `mountWithRedux` +command that will wrap your component in a Redux Provider: + +```jsx +import { mount } from '@cypress/react' +import { Provider } from 'react-redux' +import { getStore } from '../../src/store' + +Cypress.Commands.add('mountWithRedux', (component, options = {}) => { + // Use the default store if one is not provided + const { reduxStore = getStore(), ...mountOptions } = options + + const wrapped = {component} + + return mount(wrapped, mountOptions) +}) +``` + +Typings: + +```ts +import { MountOptions, MountReturn } from '@cypress/react' +import { EnhancedStore } from '@reduxjs/toolkit' +import { RootState } from './src/StoreState' + +declare global { + namespace Cypress { + interface Chainable { + /** + * Mounts a React node + * @param jsx React Node to mount + * @param options Additional options to pass into mount + */ + mountWithRedux( + jsx: React.ReactNode, + options?: MountOptions & { reduxStore?: EnhancedStore } + ): Cypress.Chainable + } + } +} +``` + +You can pass in an instance of the store that the provider will use on the +options param, which can be useful to initialize a store with certain data for +tests. It is important that the store be initialized with each new test to +ensure changes to the store don't affect other tests: + +```jsx +import { getStore } from '../redux/store' +import { setUser } from '../redux/userSlice' +import { UserProfile } from './UserProfile' + +it('User profile should display user name', () => { + const user = { name: 'test person' } + + // getStore is a factory method that creates a new store + const store = getStore() + + // setUser is an action exported from the user slice + store.dispatch(setUser(user)) + + cy.mountWithRedux(, { reduxStore: store }) + + cy.get('div.name').should('have.text', user.name) +}) +``` + +## Vue Examples + +Adding plugins and global components are some common scenarios for creating +custom mount commands in Vue. Below are examples that demonstrate how set up a +mount command for a few popular Vue libraries. These examples can be adapted to +other libraries as well. + +### Vuetify + +This example shows how to set a global `cy.mount()` command that configures +[Vuetify](https://vuetifyjs.com/), ensuring components that utilize the UI +library display properly during test runs. + +Vuetify is a plugin that must be registered and have a few custom attributes on +the root element setup for styling to appear correct. + + + + + + +Typings: + +```ts +import { mount } from '@cypress/vue' + +type MountParams = Parameters +type OptionsParam = MountParams[1] + +declare global { + namespace Cypress { + interface Chainable { + /** + * Helper mount function for Vue Components + * @param component Vue Component or JSX Element to mount + * @param options Options passed to Vue Test Utils + */ + mount(component: any, options?: OptionsParam): Chainable + } + } +} +``` + +Usage: + +```ts +import Button from './Button.vue' + +it('Shows a button', () => { + cy.mount(Button, { + slots: { + default: () => 'Click Me', + }, + }) + cy.contains('Click Me').should('exist') +}) +``` + +### Vue Router + +To wire up a plugin such as Vue Router, you can create a custom command to +register the plugin and pass in a custom implementation of the router via the +options param. + + + + + + +### Global Components + +If you have components that are registered globally in the main application +file, you will need to set them up in your mount command as well. + + + + + diff --git a/content/guides/getting-started/creating-a-cy-mount-command.md b/content/guides/getting-started/creating-a-cy-mount-command.md deleted file mode 100644 index 6d12da3610..0000000000 --- a/content/guides/getting-started/creating-a-cy-mount-command.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Creating a cy.mount Command ---- - -CONTENT TBD diff --git a/pages/api/_.vue b/pages/api/_.vue index 4dffb96dd6..f1292d71fd 100644 --- a/pages/api/_.vue +++ b/pages/api/_.vue @@ -111,6 +111,7 @@ export default { diff --git a/pages/guides/_.vue b/pages/guides/_.vue index 79aa4eb482..51d1659c0b 100644 --- a/pages/guides/_.vue +++ b/pages/guides/_.vue @@ -92,6 +92,7 @@ export default {