|
1 | 1 | // having weak reference to styles prevents garbage collection
|
2 | 2 | // and "losing" styles when the next test starts
|
3 |
| -export const stylesCache = new Map() |
| 3 | +// import { stylesCache, setXMLHttpRequest, setAlert } from './index' |
4 | 4 |
|
5 |
| -export const setXMLHttpRequest = (w) => { |
| 5 | +const stylesCache = new Map() |
| 6 | + |
| 7 | +const setXMLHttpRequest = w => { |
6 | 8 | // by grabbing the XMLHttpRequest from app's iframe
|
7 | 9 | // and putting it here - in the test iframe
|
8 | 10 | // we suddenly get spying and stubbing 😁
|
9 | 11 | window.XMLHttpRequest = w.XMLHttpRequest
|
10 | 12 | return w
|
11 | 13 | }
|
12 | 14 |
|
13 |
| -export const setAlert = (w) => { |
| 15 | +const setAlert = w => { |
14 | 16 | window.alert = w.alert
|
15 | 17 | return w
|
16 | 18 | }
|
| 19 | + |
| 20 | +/** Initialize an empty document w/ ReactDOM and DOM events. |
| 21 | + @function cy.injectReactDOM |
| 22 | +**/ |
| 23 | +Cypress.Commands.add('injectReactDOM', () => { |
| 24 | + return cy.log('Injecting ReactDOM for Unit Testing').then(() => { |
| 25 | + // Generate inline script tags for UMD modules |
| 26 | + const scripts = Cypress.modules |
| 27 | + .map(module => `<script>${module.source}</script>`) |
| 28 | + .join('') |
| 29 | + // include React and ReactDOM to force DOM to register all DOM event listeners |
| 30 | + // otherwise the component will NOT be able to dispatch any events |
| 31 | + // when it runs the second time |
| 32 | + // https://github.com/bahmutov/cypress-react-unit-test/issues/3 |
| 33 | + var html = `<body> |
| 34 | + <div id="cypress-jsdom"></div> |
| 35 | + ${scripts} |
| 36 | + </body>` |
| 37 | + const document = cy.state('document') |
| 38 | + document.write(html) |
| 39 | + document.close() |
| 40 | + }) |
| 41 | +}) |
| 42 | + |
| 43 | +cy.stylesCache = stylesCache |
| 44 | +/** Caches styles from previously compiled components for reuse |
| 45 | + @function cy.copyComponentStyles |
| 46 | + @param {Object} component |
| 47 | +**/ |
| 48 | +Cypress.Commands.add('copyComponentStyles', component => { |
| 49 | + // need to find same component when component is recompiled |
| 50 | + // by the JSX preprocessor. Thus have to use something else, |
| 51 | + // like component name |
| 52 | + const hash = component.type.name |
| 53 | + const document = cy.state('document') |
| 54 | + let styles = document.querySelectorAll('head style') |
| 55 | + if (styles.length) { |
| 56 | + cy.log('injected %d styles', styles.length) |
| 57 | + cy.stylesCache.set(hash, styles) |
| 58 | + } else { |
| 59 | + cy.log('No styles injected for this component, checking cache') |
| 60 | + if (cy.stylesCache.has(hash)) { |
| 61 | + styles = cy.stylesCache.get(hash) |
| 62 | + } else { |
| 63 | + styles = null |
| 64 | + } |
| 65 | + } |
| 66 | + if (!styles) { |
| 67 | + return |
| 68 | + } |
| 69 | + const parentDocument = window.parent.document |
| 70 | + const projectName = Cypress.config('projectName') |
| 71 | + const appIframeId = "Your App: '" + projectName + "'" |
| 72 | + const appIframe = parentDocument.getElementById(appIframeId) |
| 73 | + var head = appIframe.contentDocument.querySelector('head') |
| 74 | + styles.forEach(function (style) { |
| 75 | + head.appendChild(style) |
| 76 | + }) |
| 77 | +}) |
| 78 | + |
| 79 | +/** |
| 80 | + * Mount a React component in a blank document; register it as an alias |
| 81 | + * To access: use an alias or original component reference |
| 82 | + * @function cy.mount |
| 83 | + * @param {Object} jsx - component to mount |
| 84 | + * @param {string} [Component] - alias to use later |
| 85 | + * @example |
| 86 | + ``` |
| 87 | + import Hello from './hello.jsx' |
| 88 | + // mount and access by alias |
| 89 | + cy.mount(<Hello />, 'Hello') |
| 90 | + // using default alias |
| 91 | + cy.get('@Component') |
| 92 | + // using specified alias |
| 93 | + cy.get('@Hello').its('state').should(...) |
| 94 | + // using original component |
| 95 | + cy.get(Hello) |
| 96 | + ``` |
| 97 | + **/ |
| 98 | +export const mount = (jsx, alias) => { |
| 99 | + // Get the display name property via the component constructor |
| 100 | + const displayname = alias || jsx.type.prototype.constructor.name |
| 101 | + cy.injectReactDOM() |
| 102 | + .log(`ReactDOM.render(<${displayname} ... />)`, jsx.props) |
| 103 | + .window({ log: false }) |
| 104 | + .then(setXMLHttpRequest) |
| 105 | + .then(setAlert) |
| 106 | + .then(win => { |
| 107 | + const { ReactDOM } = win |
| 108 | + const document = cy.state('document') |
| 109 | + const component = ReactDOM.render( |
| 110 | + jsx, |
| 111 | + document.getElementById('cypress-jsdom') |
| 112 | + ) |
| 113 | + cy.wrap(component, { log: false }).as(displayname) |
| 114 | + }) |
| 115 | + cy.copyComponentStyles(jsx) |
| 116 | +} |
| 117 | + |
| 118 | +Cypress.Commands.add('mount', mount) |
| 119 | + |
| 120 | +/** Get one or more DOM elements by selector or alias. |
| 121 | + Features extended support for JSX and React.Component |
| 122 | + @function cy.get |
| 123 | + @param {string|object|function} selector |
| 124 | + @param {object} options |
| 125 | + @example cy.get('@Component') |
| 126 | + @example cy.get(<Component />) |
| 127 | + @example cy.get(Component) |
| 128 | +**/ |
| 129 | +Cypress.Commands.overwrite('get', (originalFn, selector, options) => { |
| 130 | + switch (typeof selector) { |
| 131 | + case 'object': |
| 132 | + // If attempting to use JSX as a selector, reference the displayname |
| 133 | + if ( |
| 134 | + selector.$$typeof && |
| 135 | + selector.$$typeof.toString().startsWith('Symbol(react') |
| 136 | + ) { |
| 137 | + const displayname = selector.type.prototype.constructor.name |
| 138 | + return originalFn(`@${displayname}`, options) |
| 139 | + } |
| 140 | + case 'function': |
| 141 | + // If attempting to use the component name without JSX (testing in .js/.ts files) |
| 142 | + const displayname = selector.prototype.constructor.name |
| 143 | + return originalFn(`@${displayname}`, options) |
| 144 | + default: |
| 145 | + return originalFn(selector, options) |
| 146 | + } |
| 147 | +}) |
| 148 | + |
| 149 | +const moduleNames = [ |
| 150 | + { |
| 151 | + name: 'react', |
| 152 | + type: 'file', |
| 153 | + location: 'node_modules/react/umd/react.development.js' |
| 154 | + }, |
| 155 | + { |
| 156 | + name: 'react-dom', |
| 157 | + type: 'file', |
| 158 | + location: 'node_modules/react-dom/umd/react-dom.development.js' |
| 159 | + } |
| 160 | +] |
| 161 | + |
| 162 | +/* |
| 163 | +Before All |
| 164 | +- Load and cache UMD modules specified in fixtures/modules.json |
| 165 | + These scripts are inlined in the document during unit tests |
| 166 | + modules.json should be an array, which implicitly sets the loading order |
| 167 | + Format: [{name, type, location}, ...] |
| 168 | +*/ |
| 169 | +before(() => { |
| 170 | + Cypress.modules = [] |
| 171 | + cy.log('Initializing UMD module cache').then(() => { |
| 172 | + for (const module of moduleNames) { |
| 173 | + let { name, type, location } = module |
| 174 | + cy.log(`Loading ${name} via ${type}`) |
| 175 | + .readFile(location) |
| 176 | + .then(source => Cypress.modules.push({ name, type, location, source })) |
| 177 | + } |
| 178 | + }) |
| 179 | +}) |
0 commit comments