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

Commit 54700c5

Browse files
Ryan Kellerbahmutov
authored andcommitted
feat: module cache, more cy.get aliases, typings for TS (#45)
* Fixed users spec - had race condition * Drafted cypress commands * Lib functions as commands. Use node_modules for loading React * Switch over to using cy.mount. Docs updated * Version bumping incl. babel@7 * Added error boundaries - good example component * Line about err boundary component * Only load modules once. Refactoring * Bookmarking this for now * modules fixture * Use react displaynames * Overrode cy.get() to support JSX * Updated README.md * Add typings
1 parent 0cbcd9a commit 54700c5

File tree

10 files changed

+107
-49
lines changed

10 files changed

+107
-49
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ describe('HelloState component', () => {
3434
cy.mount(<HelloState />)
3535
// start testing!
3636
cy.contains('Hello Spider-man!')
37-
// mounted component is aliased as @Component
38-
cy.get('@Component')
37+
// mounted component can be selected via its name, function, or JSX
38+
// e.g. '@HelloState', HelloState, or <HelloState />
39+
cy.get(HelloState)
3940
.invoke('setState', { name: 'React' })
40-
cy.get('@Component')
41+
cy.get(HelloState)
4142
.its('state')
4243
.should('deep.equal', { name: 'React' })
4344
// check if GUI has rerendered

cypress/fixtures/modules.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"name": "react",
4+
"type": "file",
5+
"location": "node_modules/react/umd/react.development.js"
6+
},
7+
{
8+
"name": "react-dom",
9+
"type": "file",
10+
"location": "node_modules/react-dom/umd/react-dom.development.js"
11+
}
12+
]

cypress/integration/counter-spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('Counter cy.mounted before each test', () => {
4040
.click()
4141
.click()
4242
.click()
43-
cy.get('@Component')
43+
cy.get(Counter)
4444
.its('state')
4545
.should('deep.equal', {count: 3})
4646
})

cypress/integration/error-boundary-spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('Error Boundary', () => {
1818
)
1919
cy.get('h1')
2020
.should('have.text', 'Normal Child')
21-
cy.get('@Component')
21+
cy.get(ErrorBoundary)
2222
.its('state.error')
2323
.should('not.exist')
2424
})
@@ -33,10 +33,10 @@ describe('Error Boundary', () => {
3333
.should('contain', 'Something went wrong.')
3434
cy.get('header h3')
3535
.should('contain', 'failed to load')
36-
cy.get('@Component')
36+
cy.get(ErrorBoundary)
3737
.its('state.error.message')
3838
.should('equal', errorMessage)
39-
cy.get('@Component')
39+
cy.get(ErrorBoundary)
4040
.its('state.error.stack')
4141
.should('contain', 'ChildWithError')
4242
})

cypress/integration/hello-x-spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ describe('HelloState component', () => {
1414
cy.mount(<HelloState />)
1515
cy.contains('Hello Spider-man!')
1616
const stateToSet = { name: 'React' }
17-
cy.get('@Component')
17+
cy.get(HelloState)
1818
.invoke('setState', stateToSet)
19-
cy.get('@Component')
19+
cy.get(HelloState)
2020
.its('state')
2121
.should('deep.equal', stateToSet)
2222
cy.contains('Hello React!')

cypress/integration/transpiled-spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('Counter cy.mounted before each test', () => {
4040
.click()
4141
.click()
4242
.click()
43-
cy.get('@Component')
43+
cy.get(Transpiled)
4444
.its('state')
4545
.should('deep.equal', {count: 3})
4646
})

cypress/support/commands.js

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,20 @@ import { stylesCache, setXMLHttpRequest, setAlert } from '../../lib'
2929
@function cy.injectReactDOM
3030
**/
3131
Cypress.Commands.add('injectReactDOM', () => {
32-
var packages = {}
33-
// Yeah, due to how Cy resolves promises under the hood, a closure works but an @aliased cached asset doesn't
3432
return cy
3533
.log('Injecting ReactDOM for Unit Testing')
36-
.readFile('node_modules/react/umd/react.development.js', { log: false }).then(file => packages.React = file)
37-
.readFile('node_modules/react-dom/umd/react-dom.development.js', { log: false }).then(file => packages.ReactDOM = file)
3834
.then(() => {
35+
// Generate inline script tags for UMD modules
36+
const scripts = Cypress.modules
37+
.map(module => `<script>${module.source}</script>`)
38+
.join('')
3939
// include React and ReactDOM to force DOM to register all DOM event listeners
4040
// otherwise the component will NOT be able to dispatch any events
4141
// when it runs the second time
4242
// https://github.com/bahmutov/cypress-react-unit-test/issues/3
4343
var html = `<body>
4444
<div id="cypress-jsdom"></div>
45-
<script>${packages.React}</script>
46-
<script>${packages.ReactDOM}</script>
45+
${scripts}
4746
</body>`
4847
const document = cy.state('document')
4948
document.write(html)
@@ -94,18 +93,46 @@ Cypress.Commands.add('copyComponentStyles', (component) => {
9493
@param {Object} jsx
9594
@param {string} [Component] alias
9695
**/
97-
Cypress.Commands.add('mount', (jsx, alias = 'Component') => {
96+
Cypress.Commands.add('mount', (jsx, alias) => {
97+
// Get the displayname property via the component constructor
98+
const displayname = alias || jsx.type.prototype.constructor.name
9899
cy
99100
.injectReactDOM()
100-
.log('ReactDOM.render(<' + alias + ' ... />)')
101+
.log(`ReactDOM.render(<${displayname} ... />)`, jsx.props)
101102
.window({ log: false })
102103
.then(setXMLHttpRequest)
103104
.then(setAlert)
104105
.then(win => {
105106
const { ReactDOM } = win
106107
const document = cy.state('document')
107108
const component = ReactDOM.render(jsx, document.getElementById('cypress-jsdom'))
108-
cy.wrap(component, { log: false }).as(alias)
109+
cy.wrap(component, { log: false }).as(displayname)
109110
})
110111
cy.copyComponentStyles(jsx)
111112
})
113+
114+
/** Get one or more DOM elements by selector or alias.
115+
Features extended support for JSX and React.Component
116+
@function cy.get
117+
@param {string|object|function} selector
118+
@param {object} options
119+
@example cy.get('@Component')
120+
@example cy.get(<Component />)
121+
@example cy.get(Component)
122+
**/
123+
Cypress.Commands.overwrite('get', (originalFn, selector, options) => {
124+
switch (typeof selector) {
125+
case 'object':
126+
// If attempting to use JSX as a selector, reference the displayname
127+
if (selector.$$typeof && selector.$$typeof.toString().startsWith('Symbol(react')) {
128+
const displayname = selector.type.prototype.constructor.name
129+
return originalFn(`@${displayname}`, options)
130+
}
131+
case 'function':
132+
// If attempting to use the component name without JSX (testing in .js/.ts files)
133+
const displayname = selector.prototype.constructor.name
134+
return originalFn(`@${displayname}`, options)
135+
default:
136+
return originalFn(selector, options)
137+
}
138+
})

cypress/support/index.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
declare namespace Cypress {
2+
interface Cypress {
3+
stylesCache: any
4+
React: string
5+
ReactDOM: string
6+
Styles: string
7+
}
8+
// NOTE: By default, avoiding React.Component/Element typings
9+
// for many cases, we don't want to import @types/react
10+
interface Chainable<Subject = any> {
11+
injectReactDOM: () => Chainable<void>
12+
copyComponentStyles: (component: Symbol) => Chainable<void>
13+
mount: (component: Symbol, alias?: string) => Chainable<void>
14+
get<S = any>(alias: string | symbol | Function, options?: Partial<Loggable & Timeoutable>): Chainable<any>
15+
}
16+
}

cypress/support/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
/*
2+
Before All
3+
- Load and cache UMD modules specified in fixtures/modules.json
4+
These scripts are inlined in the document during unit tests
5+
modules.json should be an array, which implicitly sets the loading order
6+
Format: [{name, type, location}, ...]
7+
*/
8+
before(() => {
9+
Cypress.modules = []
10+
cy.log('Initializing UMD module cache')
11+
.fixture('modules')
12+
.then((modules = []) => {
13+
for (const module of modules) {
14+
let { name, type, location } = module
15+
cy.log(`Loading ${name} via ${type}`)
16+
.readFile(location)
17+
.then(source => Cypress.modules.push({ name, type, location, source }))
18+
}
19+
})
20+
})
21+
122
// ***********************************************************
223
// This example support/index.js is processed and
324
// loaded automatically before your test files.

package-lock.json

Lines changed: 11 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)