Skip to content
This repository was archived by the owner on Mar 5, 2022. It is now read-only.

Commit 269d6ed

Browse files
feat: createMount (#419)
1 parent 84a6add commit 269d6ed

File tree

6 files changed

+307
-248
lines changed

6 files changed

+307
-248
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ See [Recipes](./docs/recipes.md) for more examples.
101101
## API
102102

103103
- `mount` is the most important function, allows to mount a given React component as a mini web application and interact with it using Cypress commands
104+
- `createMount` factory function that creates new `mount` function with default options
104105
- `unmount` removes previously mounted component, mostly useful to test how the component cleans up after itself
105106
- `mountHook` mounts a given React Hook in a test component for full testing, see `hooks` example
106107

cypress/component/basic/styles/css-file/css-file-spec.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="cypress" />
22
import React from 'react'
3-
import { mount } from 'cypress-react-unit-test'
3+
import { createMount, mount } from 'cypress-react-unit-test'
44

55
describe('cssFile', () => {
66
it('is loaded', () => {
@@ -64,4 +64,28 @@ describe('cssFile', () => {
6464
expect(parseFloat(value), 'height is < 30px').to.be.lessThan(30)
6565
})
6666
})
67+
68+
context('Using createMount to simplify global css experience', () => {
69+
const mount = createMount({
70+
cssFiles: 'cypress/component/basic/styles/css-file/index.css',
71+
})
72+
73+
it('createMount green button', () => {
74+
const Component = () => <button className="green">Green button</button>
75+
mount(<Component />)
76+
77+
cy.get('button')
78+
.should('have.class', 'green')
79+
.and('have.css', 'background-color', 'rgb(0, 255, 0)')
80+
})
81+
82+
it('createMount blue button', () => {
83+
const Component = () => <button className="blue">blue button</button>
84+
mount(<Component />)
85+
86+
cy.get('button')
87+
.should('have.class', 'blue')
88+
.and('have.css', 'background-color', 'rgb(0, 0, 255)')
89+
})
90+
})
6791
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
button.green {
22
background-color: #00ff00;
33
}
4+
5+
button.blue {
6+
background-color: #0000ff;
7+
}

lib/index.ts

Lines changed: 4 additions & 247 deletions
Original file line numberDiff line numberDiff line change
@@ -1,248 +1,5 @@
1-
import * as React from 'react'
2-
import ReactDOM, { unmountComponentAtNode } from 'react-dom'
3-
import getDisplayName from './getDisplayName'
4-
import { injectStylesBeforeElement } from './utils'
1+
export * from './mount'
2+
export * from './mountHook'
53

6-
const rootId = 'cypress-root'
7-
8-
const isComponentSpec = () => Cypress.spec.specType === 'component'
9-
10-
function checkMountModeEnabled() {
11-
if (!isComponentSpec()) {
12-
throw new Error(
13-
`In order to use mount or unmount functions please place the spec in component folder`,
14-
)
15-
}
16-
}
17-
18-
/**
19-
* Inject custom style text or CSS file or 3rd party style resources
20-
*/
21-
const injectStyles = (options: MountOptions) => () => {
22-
const document = cy.state('document')
23-
const el = document.getElementById(rootId)
24-
return injectStylesBeforeElement(options, document, el)
25-
}
26-
27-
/**
28-
* Mount a React component in a blank document; register it as an alias
29-
* To access: use an alias or original component reference
30-
* @function mount
31-
* @param {React.ReactElement} jsx - component to mount
32-
* @param {MountOptions} [options] - options, like alias, styles
33-
* @see https://github.com/bahmutov/cypress-react-unit-test
34-
* @see https://glebbahmutov.com/blog/my-vision-for-component-tests/
35-
* @example
36-
```
37-
import Hello from './hello.jsx'
38-
import {mount} from 'cypress-react-unit-test'
39-
it('works', () => {
40-
mount(<Hello onClick={cy.stub()} />)
41-
// use Cypress commands
42-
cy.contains('Hello').click()
43-
})
44-
```
45-
**/
46-
export const mount = (jsx: React.ReactElement, options: MountOptions = {}) => {
47-
checkMountModeEnabled()
48-
49-
// Get the display name property via the component constructor
50-
// @ts-ignore FIXME
51-
const componentName = getDisplayName(jsx.type, options.alias)
52-
const displayName = options.alias || componentName
53-
const message = options.alias
54-
? `<${componentName} ... /> as "${options.alias}"`
55-
: `<${componentName} ... />`
56-
let logInstance: Cypress.Log
57-
58-
return cy
59-
.then(() => {
60-
if (options.log !== false) {
61-
logInstance = Cypress.log({
62-
name: 'mount',
63-
message: [message],
64-
})
65-
}
66-
})
67-
.then(injectStyles(options))
68-
.then(() => {
69-
const document = cy.state('document') as Document
70-
const reactDomToUse = options.ReactDom || ReactDOM
71-
72-
const el = document.getElementById(rootId)
73-
74-
if (!el) {
75-
throw new Error(
76-
[
77-
'[cypress-react-unit-test] 🔥 Hmm, cannot find root element to mount the component.',
78-
'Did you forget to include the support file?',
79-
'Check https://github.com/bahmutov/cypress-react-unit-test#install please',
80-
].join(' '),
81-
)
82-
}
83-
84-
const key =
85-
// @ts-ignore provide unique key to the the wrapped component to make sure we are rerendering between tests
86-
(Cypress?.mocha?.getRunner()?.test?.title || '') + Math.random()
87-
const props = {
88-
key,
89-
}
90-
91-
const reactComponent = React.createElement(React.Fragment, props, jsx)
92-
// since we always surround the component with a fragment
93-
// let's get back the original component
94-
// @ts-ignore
95-
const userComponent = reactComponent.props.children
96-
reactDomToUse.render(reactComponent, el)
97-
98-
if (logInstance) {
99-
const logConsoleProps = {
100-
props: jsx.props,
101-
description: 'Mounts React component',
102-
home: 'https://github.com/bahmutov/cypress-react-unit-test',
103-
}
104-
const componentElement = el.children[0]
105-
106-
if (componentElement) {
107-
// @ts-ignore
108-
logConsoleProps.yielded = reactDomToUse.findDOMNode(componentElement)
109-
}
110-
111-
logInstance.set('consoleProps', () => logConsoleProps)
112-
113-
if (el.children.length) {
114-
logInstance.set(
115-
'$el',
116-
(el.children.item(0) as unknown) as JQuery<HTMLElement>,
117-
)
118-
}
119-
}
120-
121-
return (
122-
cy
123-
.wrap(userComponent, { log: false })
124-
.as(displayName)
125-
// by waiting, we give the component's hook a chance to run
126-
// https://github.com/bahmutov/cypress-react-unit-test/issues/200
127-
.wait(1, { log: false })
128-
.then(() => {
129-
if (logInstance) {
130-
logInstance.snapshot('mounted')
131-
logInstance.end()
132-
}
133-
134-
// by returning undefined we keep the previous subject
135-
// which is the mounted component
136-
return undefined
137-
})
138-
)
139-
})
140-
}
141-
142-
/**
143-
* Removes the mounted component. Notice this command automatically
144-
* queues up the `unmount` into Cypress chain, thus you don't need `.then`
145-
* to call it.
146-
* @see https://github.com/bahmutov/cypress-react-unit-test/tree/main/cypress/component/basic/unmount
147-
* @example
148-
```
149-
import { mount, unmount } from 'cypress-react-unit-test'
150-
it('works', () => {
151-
mount(...)
152-
// interact with the component using Cypress commands
153-
// whenever you want to unmount
154-
unmount()
155-
})
156-
```
157-
*/
158-
export const unmount = () => {
159-
checkMountModeEnabled()
160-
161-
return cy.then(() => {
162-
cy.log('unmounting...')
163-
const selector = '#' + rootId
164-
return cy.get(selector, { log: false }).then($el => {
165-
unmountComponentAtNode($el[0])
166-
})
167-
})
168-
}
169-
170-
// mounting hooks inside a test component mostly copied from
171-
// https://github.com/testing-library/react-hooks-testing-library/blob/master/src/pure.js
172-
function resultContainer<T>() {
173-
let value: T | undefined | null = null
174-
let error: Error | null = null
175-
const resolvers: any[] = []
176-
177-
const result = {
178-
get current() {
179-
if (error) {
180-
throw error
181-
}
182-
return value
183-
},
184-
get error() {
185-
return error
186-
},
187-
}
188-
189-
const updateResult = (val: T | undefined, err: Error | null = null) => {
190-
value = val
191-
error = err
192-
resolvers.splice(0, resolvers.length).forEach(resolve => resolve())
193-
}
194-
195-
return {
196-
result,
197-
addResolver: (resolver: any) => {
198-
resolvers.push(resolver)
199-
},
200-
setValue: (val: T) => updateResult(val),
201-
setError: (err: Error) => updateResult(undefined, err),
202-
}
203-
}
204-
205-
type TestHookProps = {
206-
callback: () => void
207-
onError: (e: Error) => void
208-
children: (...args: any[]) => any
209-
}
210-
211-
function TestHook({ callback, onError, children }: TestHookProps) {
212-
try {
213-
children(callback())
214-
} catch (err) {
215-
if (err.then) {
216-
throw err
217-
} else {
218-
onError(err)
219-
}
220-
}
221-
222-
// TODO decide what the test hook component should show
223-
// maybe nothing, or maybe useful information about the hook?
224-
// maybe its current properties?
225-
// return <div>TestHook</div>
226-
return null
227-
}
228-
229-
/**
230-
* Mounts a React hook function in a test component for testing.
231-
*
232-
* @see https://github.com/bahmutov/cypress-react-unit-test#advanced-examples
233-
*/
234-
export const mountHook = (hookFn: (...args: any[]) => any) => {
235-
const { result, setValue, setError } = resultContainer()
236-
237-
return mount(
238-
React.createElement(TestHook, {
239-
callback: hookFn,
240-
onError: setError,
241-
children: setValue,
242-
}),
243-
).then(() => {
244-
cy.wrap(result)
245-
})
246-
}
247-
248-
export default mount
4+
/** @deprecated */
5+
export { default } from './mount'

0 commit comments

Comments
 (0)