Skip to content
This repository was archived by the owner on Mar 5, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ See [Recipes](./docs/recipes.md) for more examples.
## API

- `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
- `createMount` factory function that creates new `mount` function with default options
- `unmount` removes previously mounted component, mostly useful to test how the component cleans up after itself
- `mountHook` mounts a given React Hook in a test component for full testing, see `hooks` example

Expand Down
26 changes: 25 additions & 1 deletion cypress/component/basic/styles/css-file/css-file-spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="cypress" />
import React from 'react'
import { mount } from 'cypress-react-unit-test'
import { createMount, mount } from 'cypress-react-unit-test'

describe('cssFile', () => {
it('is loaded', () => {
Expand Down Expand Up @@ -64,4 +64,28 @@ describe('cssFile', () => {
expect(parseFloat(value), 'height is < 30px').to.be.lessThan(30)
})
})

context('Using createMount to simplify global css experience', () => {
const mount = createMount({
cssFiles: 'cypress/component/basic/styles/css-file/index.css',
})

it('createMount green button', () => {
const Component = () => <button className="green">Green button</button>
mount(<Component />)

cy.get('button')
.should('have.class', 'green')
.and('have.css', 'background-color', 'rgb(0, 255, 0)')
})

it('createMount blue button', () => {
const Component = () => <button className="blue">blue button</button>
mount(<Component />)

cy.get('button')
.should('have.class', 'blue')
.and('have.css', 'background-color', 'rgb(0, 0, 255)')
})
})
})
4 changes: 4 additions & 0 deletions cypress/component/basic/styles/css-file/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
button.green {
background-color: #00ff00;
}

button.blue {
background-color: #0000ff;
}
251 changes: 4 additions & 247 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,248 +1,5 @@
import * as React from 'react'
import ReactDOM, { unmountComponentAtNode } from 'react-dom'
import getDisplayName from './getDisplayName'
import { injectStylesBeforeElement } from './utils'
export * from './mount'
export * from './mountHook'

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(<Hello onClick={cy.stub()} />)
// 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(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<HTMLElement>,
)
}
}

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])
})
})
}

// mounting hooks inside a test component mostly copied from
// https://github.com/testing-library/react-hooks-testing-library/blob/master/src/pure.js
function resultContainer<T>() {
let value: T | undefined | null = null
let error: Error | null = null
const resolvers: any[] = []

const result = {
get current() {
if (error) {
throw error
}
return value
},
get error() {
return error
},
}

const updateResult = (val: T | undefined, err: Error | null = null) => {
value = val
error = err
resolvers.splice(0, resolvers.length).forEach(resolve => resolve())
}

return {
result,
addResolver: (resolver: any) => {
resolvers.push(resolver)
},
setValue: (val: T) => updateResult(val),
setError: (err: Error) => updateResult(undefined, err),
}
}

type TestHookProps = {
callback: () => void
onError: (e: Error) => void
children: (...args: any[]) => any
}

function TestHook({ callback, onError, children }: TestHookProps) {
try {
children(callback())
} catch (err) {
if (err.then) {
throw err
} else {
onError(err)
}
}

// TODO decide what the test hook component should show
// maybe nothing, or maybe useful information about the hook?
// maybe its current properties?
// return <div>TestHook</div>
return null
}

/**
* Mounts a React hook function in a test component for testing.
*
* @see https://github.com/bahmutov/cypress-react-unit-test#advanced-examples
*/
export const mountHook = (hookFn: (...args: any[]) => any) => {
const { result, setValue, setError } = resultContainer()

return mount(
React.createElement(TestHook, {
callback: hookFn,
onError: setError,
children: setValue,
}),
).then(() => {
cy.wrap(result)
})
}

export default mount
/** @deprecated */
export { default } from './mount'
Loading