import * as React from 'react'
import ReactDOM, { unmountComponentAtNode } from 'react-dom'
import getDisplayName from './getDisplayName'
import { injectStylesBeforeElement } from './utils'
const rootId = 'cypress-root'
const isComponentSpec = () => Cypress.spec.specType === 'component'
function checkMountModeEnabled() {
if (!isComponentSpec()) {
throw new Error(
`In order to use mount or unmount functions please place the spec in component folder`,
)
}
}
/**
* Inject custom style text or CSS file or 3rd party style resources
*/
const injectStyles = (options: MountOptions) => () => {
const document = cy.state('document')
const el = document.getElementById(rootId)
return injectStylesBeforeElement(options, document, el)
}
/**
* Mount a React component in a blank document; register it as an alias
* To access: use an alias or original component reference
* @function mount
* @param {React.ReactElement} jsx - component to mount
* @param {MountOptions} [options] - options, like alias, styles
* @see https://github.com/bahmutov/cypress-react-unit-test
* @see https://glebbahmutov.com/blog/my-vision-for-component-tests/
* @example
```
import Hello from './hello.jsx'
import {mount} from 'cypress-react-unit-test'
it('works', () => {
mount()
// use Cypress commands
cy.contains('Hello').click()
})
```
**/
export const mount = (jsx: React.ReactElement, options: MountOptions = {}) => {
checkMountModeEnabled()
// Get the display name property via the component constructor
// @ts-ignore FIXME
const componentName = getDisplayName(jsx.type, options.alias)
const displayName = options.alias || componentName
const message = options.alias
? `<${componentName} ... /> as "${options.alias}"`
: `<${componentName} ... />`
let logInstance: Cypress.Log
return cy
.then(() => {
if (options.log !== false) {
logInstance = Cypress.log({
name: 'mount',
message: [message],
})
}
})
.then(injectStyles(options))
.then(() => {
const document = cy.state('document') as Document
const reactDomToUse = options.ReactDom || ReactDOM
const el = document.getElementById(rootId)
if (!el) {
throw new Error(
[
'[cypress-react-unit-test] 🔥 Hmm, cannot find root element to mount the component.',
'Did you forget to include the support file?',
'Check https://github.com/bahmutov/cypress-react-unit-test#install please',
].join(' '),
)
}
const key =
// @ts-ignore provide unique key to the the wrapped component to make sure we are rerendering between tests
(Cypress?.mocha?.getRunner()?.test?.title || '') + Math.random()
const props = {
key,
}
const reactComponent = React.createElement(
options.strict ? React.StrictMode : React.Fragment,
props,
jsx,
)
// since we always surround the component with a fragment
// let's get back the original component
// @ts-ignore
const userComponent = reactComponent.props.children
reactDomToUse.render(reactComponent, el)
if (logInstance) {
const logConsoleProps = {
props: jsx.props,
description: 'Mounts React component',
home: 'https://github.com/bahmutov/cypress-react-unit-test',
}
const componentElement = el.children[0]
if (componentElement) {
// @ts-ignore
logConsoleProps.yielded = reactDomToUse.findDOMNode(componentElement)
}
logInstance.set('consoleProps', () => logConsoleProps)
if (el.children.length) {
logInstance.set(
'$el',
(el.children.item(0) as unknown) as JQuery,
)
}
}
return (
cy
.wrap(userComponent, { log: false })
.as(displayName)
// by waiting, we give the component's hook a chance to run
// https://github.com/bahmutov/cypress-react-unit-test/issues/200
.wait(1, { log: false })
.then(() => {
if (logInstance) {
logInstance.snapshot('mounted')
logInstance.end()
}
// by returning undefined we keep the previous subject
// which is the mounted component
return undefined
})
)
})
}
/**
* Removes the mounted component. Notice this command automatically
* queues up the `unmount` into Cypress chain, thus you don't need `.then`
* to call it.
* @see https://github.com/bahmutov/cypress-react-unit-test/tree/main/cypress/component/basic/unmount
* @example
```
import { mount, unmount } from 'cypress-react-unit-test'
it('works', () => {
mount(...)
// interact with the component using Cypress commands
// whenever you want to unmount
unmount()
})
```
*/
export const unmount = () => {
checkMountModeEnabled()
return cy.then(() => {
cy.log('unmounting...')
const selector = '#' + rootId
return cy.get(selector, { log: false }).then($el => {
unmountComponentAtNode($el[0])
})
})
}
/**
* Creates new instance of `mount` function with default options
* @function createMount
* @param {MountOptions} [defaultOptions] - defaultOptions for returned `mount` function
* @returns new instance of `mount` with assigned options
* @example
* ```
* import Hello from './hello.jsx'
* import {Â createMount } from 'cypress-react-unit-test'
*
* const mount = createMount({ strict: true, cssFile: 'path/to/any/css/file.css' })
*
* it('works', () => {
* mount()
* // use Cypress commands
* cy.get('button').should('have.css', 'color', 'rgb(124, 12, 109)')
* })
```
**/
export const createMount = (defaultOptions: MountOptions) => (
element: React.ReactElement,
options?: MountOptions,
) => mount(element, { ...defaultOptions, ...options })
/** @deprecated Should be removed in the next major version */
export default mount