Skip to content
This repository was archived by the owner on Mar 5, 2022. It is now read-only.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ describe('HelloState component', () => {
cy.mount(<HelloState />)
// start testing!
cy.contains('Hello Spider-man!')
// mounted component is aliased as @Component
cy.get('@Component')
// mounted component can be selected via its name, function, or JSX
// e.g. '@HelloState', HelloState, or <HelloState />
cy.get(HelloState)
.invoke('setState', { name: 'React' })
cy.get('@Component')
cy.get(HelloState)
.its('state')
.should('deep.equal', { name: 'React' })
// check if GUI has rerendered
Expand Down
12 changes: 12 additions & 0 deletions cypress/fixtures/modules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"name": "react",
"type": "file",
"location": "node_modules/react/umd/react.development.js"
},
{
"name": "react-dom",
"type": "file",
"location": "node_modules/react-dom/umd/react-dom.development.js"
}
]
2 changes: 1 addition & 1 deletion cypress/integration/counter-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Counter cy.mounted before each test', () => {
.click()
.click()
.click()
cy.get('@Component')
cy.get(Counter)
.its('state')
.should('deep.equal', {count: 3})
})
Expand Down
6 changes: 3 additions & 3 deletions cypress/integration/error-boundary-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Error Boundary', () => {
)
cy.get('h1')
.should('have.text', 'Normal Child')
cy.get('@Component')
cy.get(ErrorBoundary)
.its('state.error')
.should('not.exist')
})
Expand All @@ -33,10 +33,10 @@ describe('Error Boundary', () => {
.should('contain', 'Something went wrong.')
cy.get('header h3')
.should('contain', 'failed to load')
cy.get('@Component')
cy.get(ErrorBoundary)
.its('state.error.message')
.should('equal', errorMessage)
cy.get('@Component')
cy.get(ErrorBoundary)
.its('state.error.stack')
.should('contain', 'ChildWithError')
})
Expand Down
4 changes: 2 additions & 2 deletions cypress/integration/hello-x-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ describe('HelloState component', () => {
cy.mount(<HelloState />)
cy.contains('Hello Spider-man!')
const stateToSet = { name: 'React' }
cy.get('@Component')
cy.get(HelloState)
.invoke('setState', stateToSet)
cy.get('@Component')
cy.get(HelloState)
.its('state')
.should('deep.equal', stateToSet)
cy.contains('Hello React!')
Expand Down
2 changes: 1 addition & 1 deletion cypress/integration/transpiled-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Counter cy.mounted before each test', () => {
.click()
.click()
.click()
cy.get('@Component')
cy.get(Transpiled)
.its('state')
.should('deep.equal', {count: 3})
})
Expand Down
45 changes: 36 additions & 9 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,20 @@ import { stylesCache, setXMLHttpRequest, setAlert } from '../../lib'
@function cy.injectReactDOM
**/
Cypress.Commands.add('injectReactDOM', () => {
var packages = {}
// Yeah, due to how Cy resolves promises under the hood, a closure works but an @aliased cached asset doesn't
return cy
.log('Injecting ReactDOM for Unit Testing')
.readFile('node_modules/react/umd/react.development.js', { log: false }).then(file => packages.React = file)
.readFile('node_modules/react-dom/umd/react-dom.development.js', { log: false }).then(file => packages.ReactDOM = file)
.then(() => {
// Generate inline script tags for UMD modules
const scripts = Cypress.modules
.map(module => `<script>${module.source}</script>`)
.join('')
// include React and ReactDOM to force DOM to register all DOM event listeners
// otherwise the component will NOT be able to dispatch any events
// when it runs the second time
// https://github.com/bahmutov/cypress-react-unit-test/issues/3
var html = `<body>
<div id="cypress-jsdom"></div>
<script>${packages.React}</script>
<script>${packages.ReactDOM}</script>
${scripts}
</body>`
const document = cy.state('document')
document.write(html)
Expand Down Expand Up @@ -94,18 +93,46 @@ Cypress.Commands.add('copyComponentStyles', (component) => {
@param {Object} jsx
@param {string} [Component] alias
**/
Cypress.Commands.add('mount', (jsx, alias = 'Component') => {
Cypress.Commands.add('mount', (jsx, alias) => {
// Get the displayname property via the component constructor
const displayname = alias || jsx.type.prototype.constructor.name
cy
.injectReactDOM()
.log('ReactDOM.render(<' + alias + ' ... />)')
.log(`ReactDOM.render(<${displayname} ... />)`, jsx.props)
.window({ log: false })
.then(setXMLHttpRequest)
.then(setAlert)
.then(win => {
const { ReactDOM } = win
const document = cy.state('document')
const component = ReactDOM.render(jsx, document.getElementById('cypress-jsdom'))
cy.wrap(component, { log: false }).as(alias)
cy.wrap(component, { log: false }).as(displayname)
})
cy.copyComponentStyles(jsx)
})

/** Get one or more DOM elements by selector or alias.
Features extended support for JSX and React.Component
@function cy.get
@param {string|object|function} selector
@param {object} options
@example cy.get('@Component')
@example cy.get(<Component />)
@example cy.get(Component)
**/
Cypress.Commands.overwrite('get', (originalFn, selector, options) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

switch (typeof selector) {
case 'object':
// If attempting to use JSX as a selector, reference the displayname
if (selector.$$typeof && selector.$$typeof.toString().startsWith('Symbol(react')) {
const displayname = selector.type.prototype.constructor.name
return originalFn(`@${displayname}`, options)
}
case 'function':
// If attempting to use the component name without JSX (testing in .js/.ts files)
const displayname = selector.prototype.constructor.name
return originalFn(`@${displayname}`, options)
default:
return originalFn(selector, options)
}
})
16 changes: 16 additions & 0 deletions cypress/support/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
declare namespace Cypress {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super, adding types for new cy methods

interface Cypress {
stylesCache: any
React: string
ReactDOM: string
Styles: string
}
// NOTE: By default, avoiding React.Component/Element typings
// for many cases, we don't want to import @types/react
interface Chainable<Subject = any> {
injectReactDOM: () => Chainable<void>
copyComponentStyles: (component: Symbol) => Chainable<void>
mount: (component: Symbol, alias?: string) => Chainable<void>
get<S = any>(alias: string | symbol | Function, options?: Partial<Loggable & Timeoutable>): Chainable<any>
}
}
21 changes: 21 additions & 0 deletions cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
/*
Before All
- Load and cache UMD modules specified in fixtures/modules.json
These scripts are inlined in the document during unit tests
modules.json should be an array, which implicitly sets the loading order
Format: [{name, type, location}, ...]
*/
before(() => {
Cypress.modules = []
cy.log('Initializing UMD module cache')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting solution here, really interesting

.fixture('modules')
.then((modules = []) => {
for (const module of modules) {
let { name, type, location } = module
cy.log(`Loading ${name} via ${type}`)
.readFile(location)
.then(source => Cypress.modules.push({ name, type, location, source }))
}
})
})

// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
Expand Down
41 changes: 11 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.