diff --git a/README.md b/README.md index d87e3338..134c2729 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ Stubbing clock | ✅ | ✅ Code coverage | ✅ | ✅ +If you are coming from Jest + RTL world, read [Test The Interface Not The Implementation](https://glebbahmutov.com/blog/test-the-interface/). + +If you are coming from Enzyme world, check out the [enzyme](cypress/component/basic/enzyme) example. + ## Blog posts - [My Vision for Component Tests in Cypress](https://glebbahmutov.com/blog/my-vision-for-component-tests/) @@ -126,6 +130,8 @@ Spec | Description [alert-spec.js](cypress/component/basic/alert-spec.js) | Component tries to use `window.alert` [counter-set-state](cypress/component/basic/counter-set-state) | Counter component that uses `this.state` [counter-use-hooks](cypress/component/basic/counter-use-hooks) | Counter component that uses `useState` hook +[document-spec](cypress/component/basic/document) | Checks `document` dimensions from the component +[enzyme](cypress/component/basic/enzyme) | Several specs showing how to recreate Enzyme's `setProps`, `setState`, and `setContext` methods. [emotion-spec.js](cypress/component/basic/emotion-spec.js) | Confirms the component is using `@emotion/core` and styles are set [error-boundary-spec.js](cypress/component/basic/error-boundary-spec.js) | Checks if an error boundary component works [pure-component-spec.js](cypress/component/basic/pure-component.spec.js) | Tests stateless component @@ -142,7 +148,6 @@ Spec | Description [typescript](cypress/component/basic/typescript) | A spec written in TypeScript [unmount](cypress/component/basic/unmount) | Verifies the component's behavior when it is unmounted from the DOM [use-lodash-fp](cypress/component/basic/use-lodash-fp) | Imports and tests methods from `lodash/fp` dependency -[document-spec](cypress/component/basic/document) | Checks `document` dimensions from the component [styled-components](cypress/component/basic/styled-components) | Test components that use [styled-components](https://www.styled-components.com/) diff --git a/cypress/component/basic/enzyme/README.md b/cypress/component/basic/enzyme/README.md new file mode 100644 index 00000000..c31c0560 --- /dev/null +++ b/cypress/component/basic/enzyme/README.md @@ -0,0 +1,98 @@ +# Enzyme examples + +This folder shows several examples from [Enzyme docs](https://enzymejs.github.io/enzyme/). + +In general if you are migrating from Enzyme to `cypress-react-unit-test`: + +- there is no shallow mounting, only the full mounting. Thus `cypress-react-unit-test` has `mount` which is similar to the Enzyme's `render`. It renders the full HTML and CSS output of your component. +- you can mock [children components](https://github.com/bahmutov/cypress-react-unit-test/tree/main/cypress/component/advanced/mocking-component) if you want to avoid running "expensive" components during tests +- the test is running as a "mini" web application. Thus if you want to set a context around component, then set the [context around the component](https://github.com/bahmutov/cypress-react-unit-test/tree/main/cypress/component/advanced/context) + +## setState + +If you want to change the component's internal state, use the component reference. You can get it by using the special property `ref` when mounting. + +```js +// get the component reference using "ref" prop +// and place it into the object for Cypress to "wait" for it +let c = {} +mount( (c.instance = i)} />) +cy.wrap(c) + .its('instance') + .invoke('setState', { count: 10 }) +``` + +See [state-spec.js](state-spec.js) file. + +## setProps + +There is no direct implementation of `setProps`. If you want to see how the component behaves with different props: + +```js +it('mounts component with new props', () => { + mount() + cy.contains('initial').should('be.visible') + + mount() + cy.contains('second').should('be.visible') +}) +``` + +If you want to reuse properties, you can even clone the component + +```js +it('mounts cloned component', () => { + const cmp = + mount(cmp) + cy.contains('initial').should('be.visible') + + const cloned = Cypress._.cloneDeep(cmp) + // change a property, leaving the rest unchanged + cloned.props.foo = 'second' + mount(cloned) + cy.contains('.foo', 'second').should('be.visible') +}) +``` + +See [props-spec.js](props-spec.js) file. + +## context + +Enzyme's `mount` method allows passing the [React context](https://reactjs.org/docs/context.html) as the second argument to the JSX component like `SimpleComponent` below. + +```js +function SimpleComponent(props, context) { + const { name } = context + return
{name || 'not set'}
+} +``` + +Since the above syntax is [deprecated](https://reactjs.org/docs/legacy-context.html), `cypress-react-unit-test` does not support it. Instead use `createContext` and `Context.Provider` to surround the mounted component, just like you would do in a regular application code. + +```js +mount( + + + , +) +``` + +Instead of setting a new context, mount the same component but surround it with a different context provider + +```js +const cmp = +mount( + + {cmp} + , +) + +// same component, different provider +mount( + + {cmp} + , +) +``` + +See [context-spec.js](context-spec.js) for more examples. diff --git a/cypress/component/basic/enzyme/context-spec.js b/cypress/component/basic/enzyme/context-spec.js new file mode 100644 index 00000000..c1a9745a --- /dev/null +++ b/cypress/component/basic/enzyme/context-spec.js @@ -0,0 +1,50 @@ +/// +import React from 'react' +import { mount } from 'cypress-react-unit-test' +import { SimpleContext } from './simple-context' +import { SimpleComponent } from './simple-component.jsx' + +// testing components that use Context React API +// https://reactjs.org/docs/context.html +describe('Enzyme', () => { + context('setContext', () => { + it('does not provide the context', () => { + mount() + cy.contains('context not set').should('be.visible') + }) + + it('provides the context', () => { + // surround the component with the real provider but + // set the value prop to whatever the test requires + mount( + + + , + ) + cy.contains('test context').should('be.visible') + }) + + it('mounts new context', () => { + // instead of setting the context from the test + // just mount the component again with a different provider around it + const cmp = + + mount( + + {cmp} + , + ) + cy.contains('first context').should('be.visible') + cy.contains('.id', '0x123').should('be.visible') + + // same component, different provider + mount( + + {cmp} + , + ) + cy.contains('second context').should('be.visible') + cy.contains('.id', '0x123').should('be.visible') + }) + }) +}) diff --git a/cypress/component/basic/enzyme/props-spec.js b/cypress/component/basic/enzyme/props-spec.js new file mode 100644 index 00000000..0eaa8514 --- /dev/null +++ b/cypress/component/basic/enzyme/props-spec.js @@ -0,0 +1,77 @@ +/// +import React from 'react' +import { mount } from 'cypress-react-unit-test' + +class Foo extends React.Component { + constructor(props) { + super(props) + + this.state = { + count: 0, + } + } + + componentDidMount() { + console.log('componentDidMount called') + } + + componentDidUpdate() { + console.log('componentDidUpdate called') + } + + render() { + const { id, foo } = this.props + return ( +
+ {foo} count {this.state.count} +
+ ) + } +} + +describe('Enzyme', () => { + // example test copied from + // https://github.com/enzymejs/enzyme/blob/master/packages/enzyme-test-suite/test/shared/methods/setProps.jsx + + context('setProps', () => { + it('gets props from the component', () => { + mount() + cy.contains('initial').should('be.visible') + + cy.get('@Foo') + .its('props') + .then(props => { + console.log('current props', props) + expect(props).to.deep.equal({ + id: 'foo', + foo: 'initial', + }) + // you can get current props of the component + // but not change them - they are read-only + expect(() => { + props.foo = 'change 1' + }).to.throw() + }) + }) + + it('mounts component with new props', () => { + mount() + cy.contains('initial').should('be.visible') + + mount() + cy.contains('second').should('be.visible') + }) + + it('mounts cloned component', () => { + const cmp = + mount(cmp) + cy.contains('initial').should('be.visible') + + const cloned = Cypress._.cloneDeep(cmp) + // change a property, leaving the rest unchanged + cloned.props.foo = 'second' + mount(cloned) + cy.contains('.foo', 'second').should('be.visible') + }) + }) +}) diff --git a/cypress/component/basic/enzyme/simple-component.jsx b/cypress/component/basic/enzyme/simple-component.jsx new file mode 100644 index 00000000..3b1cb9b5 --- /dev/null +++ b/cypress/component/basic/enzyme/simple-component.jsx @@ -0,0 +1,23 @@ +import React from 'react' +import { SimpleContext } from './simple-context' + +export class SimpleComponent extends React.Component { + constructor(props) { + super(props) + this.state = { + id: props.id || 'unknown id', + } + } + + render() { + console.log('context %o', this.context) + return ( + <> +
{this.context.name || 'context not set'}
+
{this.state.id}
+ + ) + } +} + +SimpleComponent.contextType = SimpleContext diff --git a/cypress/component/basic/enzyme/simple-context.js b/cypress/component/basic/enzyme/simple-context.js new file mode 100644 index 00000000..982c4450 --- /dev/null +++ b/cypress/component/basic/enzyme/simple-context.js @@ -0,0 +1,3 @@ +// https://reactjs.org/docs/context.html +import { createContext } from 'react' +export const SimpleContext = createContext({ name: '' }) diff --git a/cypress/component/basic/enzyme/state-spec.js b/cypress/component/basic/enzyme/state-spec.js new file mode 100644 index 00000000..aa5fc8e0 --- /dev/null +++ b/cypress/component/basic/enzyme/state-spec.js @@ -0,0 +1,56 @@ +/// +import React from 'react' +import { mount } from 'cypress-react-unit-test' + +class Foo extends React.Component { + constructor(props) { + super(props) + + this.state = { + count: 0, + } + } + + componentDidMount() { + console.log('componentDidMount called') + } + + componentDidUpdate() { + console.log('componentDidUpdate called') + } + + render() { + const { id, foo } = this.props + return ( +
+ {foo} count {this.state.count} +
+ ) + } +} + +describe('Enzyme', () => { + context('setState', () => { + it('sets component state', () => { + // get the component reference using "ref" prop + // and place it into the object for Cypress to "wait" for it + let c = {} + mount( (c.instance = i)} />) + cy.contains('initial').should('be.visible') + + cy.log('**check state**') + cy.wrap(c) + .its('instance.state') + .should('deep.equal', { count: 0 }) + + cy.log('**setState**') + cy.wrap(c) + .its('instance') + .invoke('setState', { count: 10 }) + cy.wrap(c) + .its('instance.state') + .should('deep.equal', { count: 10 }) + cy.contains('initial count 10') + }) + }) +})