From 42c7d3c1454466840901eacf70eb563371b5aabb Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Thu, 13 Aug 2020 16:52:42 -0400 Subject: [PATCH 1/6] add examples for setState from enzyme --- cypress/component/basic/enzyme/README.md | 3 + cypress/component/basic/enzyme/spec.js | 81 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 cypress/component/basic/enzyme/README.md create mode 100644 cypress/component/basic/enzyme/spec.js diff --git a/cypress/component/basic/enzyme/README.md b/cypress/component/basic/enzyme/README.md new file mode 100644 index 00000000..2efd6f8a --- /dev/null +++ b/cypress/component/basic/enzyme/README.md @@ -0,0 +1,3 @@ +# Enzyme examples + +This folder shows several examples from [Enzyme docs](https://enzymejs.github.io/enzyme/). Find the tests in the [spec.js](spec.js) file. diff --git a/cypress/component/basic/enzyme/spec.js b/cypress/component/basic/enzyme/spec.js new file mode 100644 index 00000000..244ac64d --- /dev/null +++ b/cypress/component/basic/enzyme/spec.js @@ -0,0 +1,81 @@ +/// +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() + }) + }) + }) + + 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') + }) + }) +}) From 13b97f755a517f21ad820092e942802498401ef1 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Thu, 13 Aug 2020 18:02:19 -0400 Subject: [PATCH 2/6] add more examples --- cypress/component/basic/enzyme/README.md | 44 ++++++++++++++++++++++++ cypress/component/basic/enzyme/spec.js | 20 +++++++++++ 2 files changed, 64 insertions(+) diff --git a/cypress/component/basic/enzyme/README.md b/cypress/component/basic/enzyme/README.md index 2efd6f8a..8584f2e2 100644 --- a/cypress/component/basic/enzyme/README.md +++ b/cypress/component/basic/enzyme/README.md @@ -1,3 +1,47 @@ # Enzyme examples This folder shows several examples from [Enzyme docs](https://enzymejs.github.io/enzyme/). Find the tests in the [spec.js](spec.js) file. + +## 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 }) +``` + +## 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') +}) +``` diff --git a/cypress/component/basic/enzyme/spec.js b/cypress/component/basic/enzyme/spec.js index 244ac64d..1e36469b 100644 --- a/cypress/component/basic/enzyme/spec.js +++ b/cypress/component/basic/enzyme/spec.js @@ -53,6 +53,26 @@ describe('Enzyme', () => { }).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') + }) }) context('setState', () => { From 46d0b50e9fdb629ab6d3b4abd6460c94dee4bbe4 Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Fri, 14 Aug 2020 11:28:42 -0400 Subject: [PATCH 3/6] add context section --- cypress/component/basic/enzyme/README.md | 29 +++++++++++++++++++ .../component/basic/enzyme/context-spec.js | 27 +++++++++++++++++ .../basic/enzyme/simple-component.jsx | 11 +++++++ .../component/basic/enzyme/simple-context.js | 3 ++ 4 files changed, 70 insertions(+) create mode 100644 cypress/component/basic/enzyme/context-spec.js create mode 100644 cypress/component/basic/enzyme/simple-component.jsx create mode 100644 cypress/component/basic/enzyme/simple-context.js diff --git a/cypress/component/basic/enzyme/README.md b/cypress/component/basic/enzyme/README.md index 8584f2e2..052df01b 100644 --- a/cypress/component/basic/enzyme/README.md +++ b/cypress/component/basic/enzyme/README.md @@ -2,6 +2,12 @@ This folder shows several examples from [Enzyme docs](https://enzymejs.github.io/enzyme/). Find the tests in the [spec.js](spec.js) file. +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 `mount` +- 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. @@ -45,3 +51,26 @@ it('mounts cloned component', () => { cy.contains('.foo', 'second').should('be.visible') }) ``` + +## 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( + + + , +) +``` + +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..f5dc93ff --- /dev/null +++ b/cypress/component/basic/enzyme/context-spec.js @@ -0,0 +1,27 @@ +/// +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') + }) + }) +}) diff --git a/cypress/component/basic/enzyme/simple-component.jsx b/cypress/component/basic/enzyme/simple-component.jsx new file mode 100644 index 00000000..469285ac --- /dev/null +++ b/cypress/component/basic/enzyme/simple-component.jsx @@ -0,0 +1,11 @@ +import React from 'react' +import { SimpleContext } from './simple-context' + +export class SimpleComponent extends React.Component { + render() { + console.log('context %o', this.context) + return
{this.context.name || 'context not set'}
+ } +} + +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: '' }) From ecab2e4753908b791cfefeab93f67b161d2f6a7d Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Fri, 14 Aug 2020 12:10:27 -0400 Subject: [PATCH 4/6] finish context --- cypress/component/basic/enzyme/README.md | 24 +++++++- .../component/basic/enzyme/context-spec.js | 23 ++++++++ .../basic/enzyme/{spec.js => props-spec.js} | 24 -------- .../basic/enzyme/simple-component.jsx | 14 ++++- cypress/component/basic/enzyme/state-spec.js | 56 +++++++++++++++++++ 5 files changed, 115 insertions(+), 26 deletions(-) rename cypress/component/basic/enzyme/{spec.js => props-spec.js} (73%) create mode 100644 cypress/component/basic/enzyme/state-spec.js diff --git a/cypress/component/basic/enzyme/README.md b/cypress/component/basic/enzyme/README.md index 052df01b..63bb2596 100644 --- a/cypress/component/basic/enzyme/README.md +++ b/cypress/component/basic/enzyme/README.md @@ -1,6 +1,6 @@ # Enzyme examples -This folder shows several examples from [Enzyme docs](https://enzymejs.github.io/enzyme/). Find the tests in the [spec.js](spec.js) file. +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`: @@ -22,6 +22,8 @@ cy.wrap(c) .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: @@ -52,6 +54,8 @@ it('mounts cloned component', () => { }) ``` +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. @@ -73,4 +77,22 @@ 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 index f5dc93ff..c1a9745a 100644 --- a/cypress/component/basic/enzyme/context-spec.js +++ b/cypress/component/basic/enzyme/context-spec.js @@ -23,5 +23,28 @@ describe('Enzyme', () => { ) 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/spec.js b/cypress/component/basic/enzyme/props-spec.js similarity index 73% rename from cypress/component/basic/enzyme/spec.js rename to cypress/component/basic/enzyme/props-spec.js index 1e36469b..0eaa8514 100644 --- a/cypress/component/basic/enzyme/spec.js +++ b/cypress/component/basic/enzyme/props-spec.js @@ -74,28 +74,4 @@ describe('Enzyme', () => { cy.contains('.foo', 'second').should('be.visible') }) }) - - 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') - }) - }) }) diff --git a/cypress/component/basic/enzyme/simple-component.jsx b/cypress/component/basic/enzyme/simple-component.jsx index 469285ac..3b1cb9b5 100644 --- a/cypress/component/basic/enzyme/simple-component.jsx +++ b/cypress/component/basic/enzyme/simple-component.jsx @@ -2,9 +2,21 @@ 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'}
+ return ( + <> +
{this.context.name || 'context not set'}
+
{this.state.id}
+ + ) } } 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') + }) + }) +}) From cd75599faa0e98a74bc10edaf2603f0fa7057bcf Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Fri, 14 Aug 2020 12:15:35 -0400 Subject: [PATCH 5/6] link example to the root README --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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/) From db68a757aa1d3d9fee065d29eef924a599d3f90c Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Fri, 14 Aug 2020 12:48:29 -0400 Subject: [PATCH 6/6] Update cypress/component/basic/enzyme/README.md Co-authored-by: Dmitriy Kovalenko --- cypress/component/basic/enzyme/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/component/basic/enzyme/README.md b/cypress/component/basic/enzyme/README.md index 63bb2596..c31c0560 100644 --- a/cypress/component/basic/enzyme/README.md +++ b/cypress/component/basic/enzyme/README.md @@ -4,7 +4,7 @@ This folder shows several examples from [Enzyme docs](https://enzymejs.github.io 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 `mount` +- 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)