|
1 | | -import * as React from 'react' |
2 | | -import ReactDOM, { unmountComponentAtNode } from 'react-dom' |
3 | | -import getDisplayName from './getDisplayName' |
4 | | -import { injectStylesBeforeElement } from './utils' |
| 1 | +export * from './mount' |
| 2 | +export * from './mountHook' |
5 | 3 |
|
6 | | -const rootId = 'cypress-root' |
7 | | - |
8 | | -const isComponentSpec = () => Cypress.spec.specType === 'component' |
9 | | - |
10 | | -function checkMountModeEnabled() { |
11 | | - if (!isComponentSpec()) { |
12 | | - throw new Error( |
13 | | - `In order to use mount or unmount functions please place the spec in component folder`, |
14 | | - ) |
15 | | - } |
16 | | -} |
17 | | - |
18 | | -/** |
19 | | - * Inject custom style text or CSS file or 3rd party style resources |
20 | | - */ |
21 | | -const injectStyles = (options: MountOptions) => () => { |
22 | | - const document = cy.state('document') |
23 | | - const el = document.getElementById(rootId) |
24 | | - return injectStylesBeforeElement(options, document, el) |
25 | | -} |
26 | | - |
27 | | -/** |
28 | | - * Mount a React component in a blank document; register it as an alias |
29 | | - * To access: use an alias or original component reference |
30 | | - * @function mount |
31 | | - * @param {React.ReactElement} jsx - component to mount |
32 | | - * @param {MountOptions} [options] - options, like alias, styles |
33 | | - * @see https://github.com/bahmutov/cypress-react-unit-test |
34 | | - * @see https://glebbahmutov.com/blog/my-vision-for-component-tests/ |
35 | | - * @example |
36 | | - ``` |
37 | | - import Hello from './hello.jsx' |
38 | | - import {mount} from 'cypress-react-unit-test' |
39 | | - it('works', () => { |
40 | | - mount(<Hello onClick={cy.stub()} />) |
41 | | - // use Cypress commands |
42 | | - cy.contains('Hello').click() |
43 | | - }) |
44 | | - ``` |
45 | | - **/ |
46 | | -export const mount = (jsx: React.ReactElement, options: MountOptions = {}) => { |
47 | | - checkMountModeEnabled() |
48 | | - |
49 | | - // Get the display name property via the component constructor |
50 | | - // @ts-ignore FIXME |
51 | | - const componentName = getDisplayName(jsx.type, options.alias) |
52 | | - const displayName = options.alias || componentName |
53 | | - const message = options.alias |
54 | | - ? `<${componentName} ... /> as "${options.alias}"` |
55 | | - : `<${componentName} ... />` |
56 | | - let logInstance: Cypress.Log |
57 | | - |
58 | | - return cy |
59 | | - .then(() => { |
60 | | - if (options.log !== false) { |
61 | | - logInstance = Cypress.log({ |
62 | | - name: 'mount', |
63 | | - message: [message], |
64 | | - }) |
65 | | - } |
66 | | - }) |
67 | | - .then(injectStyles(options)) |
68 | | - .then(() => { |
69 | | - const document = cy.state('document') as Document |
70 | | - const reactDomToUse = options.ReactDom || ReactDOM |
71 | | - |
72 | | - const el = document.getElementById(rootId) |
73 | | - |
74 | | - if (!el) { |
75 | | - throw new Error( |
76 | | - [ |
77 | | - '[cypress-react-unit-test] 🔥 Hmm, cannot find root element to mount the component.', |
78 | | - 'Did you forget to include the support file?', |
79 | | - 'Check https://github.com/bahmutov/cypress-react-unit-test#install please', |
80 | | - ].join(' '), |
81 | | - ) |
82 | | - } |
83 | | - |
84 | | - const key = |
85 | | - // @ts-ignore provide unique key to the the wrapped component to make sure we are rerendering between tests |
86 | | - (Cypress?.mocha?.getRunner()?.test?.title || '') + Math.random() |
87 | | - const props = { |
88 | | - key, |
89 | | - } |
90 | | - |
91 | | - const reactComponent = React.createElement(React.Fragment, props, jsx) |
92 | | - // since we always surround the component with a fragment |
93 | | - // let's get back the original component |
94 | | - // @ts-ignore |
95 | | - const userComponent = reactComponent.props.children |
96 | | - reactDomToUse.render(reactComponent, el) |
97 | | - |
98 | | - if (logInstance) { |
99 | | - const logConsoleProps = { |
100 | | - props: jsx.props, |
101 | | - description: 'Mounts React component', |
102 | | - home: 'https://github.com/bahmutov/cypress-react-unit-test', |
103 | | - } |
104 | | - const componentElement = el.children[0] |
105 | | - |
106 | | - if (componentElement) { |
107 | | - // @ts-ignore |
108 | | - logConsoleProps.yielded = reactDomToUse.findDOMNode(componentElement) |
109 | | - } |
110 | | - |
111 | | - logInstance.set('consoleProps', () => logConsoleProps) |
112 | | - |
113 | | - if (el.children.length) { |
114 | | - logInstance.set( |
115 | | - '$el', |
116 | | - (el.children.item(0) as unknown) as JQuery<HTMLElement>, |
117 | | - ) |
118 | | - } |
119 | | - } |
120 | | - |
121 | | - return ( |
122 | | - cy |
123 | | - .wrap(userComponent, { log: false }) |
124 | | - .as(displayName) |
125 | | - // by waiting, we give the component's hook a chance to run |
126 | | - // https://github.com/bahmutov/cypress-react-unit-test/issues/200 |
127 | | - .wait(1, { log: false }) |
128 | | - .then(() => { |
129 | | - if (logInstance) { |
130 | | - logInstance.snapshot('mounted') |
131 | | - logInstance.end() |
132 | | - } |
133 | | - |
134 | | - // by returning undefined we keep the previous subject |
135 | | - // which is the mounted component |
136 | | - return undefined |
137 | | - }) |
138 | | - ) |
139 | | - }) |
140 | | -} |
141 | | - |
142 | | -/** |
143 | | - * Removes the mounted component. Notice this command automatically |
144 | | - * queues up the `unmount` into Cypress chain, thus you don't need `.then` |
145 | | - * to call it. |
146 | | - * @see https://github.com/bahmutov/cypress-react-unit-test/tree/main/cypress/component/basic/unmount |
147 | | - * @example |
148 | | - ``` |
149 | | - import { mount, unmount } from 'cypress-react-unit-test' |
150 | | - it('works', () => { |
151 | | - mount(...) |
152 | | - // interact with the component using Cypress commands |
153 | | - // whenever you want to unmount |
154 | | - unmount() |
155 | | - }) |
156 | | - ``` |
157 | | - */ |
158 | | -export const unmount = () => { |
159 | | - checkMountModeEnabled() |
160 | | - |
161 | | - return cy.then(() => { |
162 | | - cy.log('unmounting...') |
163 | | - const selector = '#' + rootId |
164 | | - return cy.get(selector, { log: false }).then($el => { |
165 | | - unmountComponentAtNode($el[0]) |
166 | | - }) |
167 | | - }) |
168 | | -} |
169 | | - |
170 | | -// mounting hooks inside a test component mostly copied from |
171 | | -// https://github.com/testing-library/react-hooks-testing-library/blob/master/src/pure.js |
172 | | -function resultContainer<T>() { |
173 | | - let value: T | undefined | null = null |
174 | | - let error: Error | null = null |
175 | | - const resolvers: any[] = [] |
176 | | - |
177 | | - const result = { |
178 | | - get current() { |
179 | | - if (error) { |
180 | | - throw error |
181 | | - } |
182 | | - return value |
183 | | - }, |
184 | | - get error() { |
185 | | - return error |
186 | | - }, |
187 | | - } |
188 | | - |
189 | | - const updateResult = (val: T | undefined, err: Error | null = null) => { |
190 | | - value = val |
191 | | - error = err |
192 | | - resolvers.splice(0, resolvers.length).forEach(resolve => resolve()) |
193 | | - } |
194 | | - |
195 | | - return { |
196 | | - result, |
197 | | - addResolver: (resolver: any) => { |
198 | | - resolvers.push(resolver) |
199 | | - }, |
200 | | - setValue: (val: T) => updateResult(val), |
201 | | - setError: (err: Error) => updateResult(undefined, err), |
202 | | - } |
203 | | -} |
204 | | - |
205 | | -type TestHookProps = { |
206 | | - callback: () => void |
207 | | - onError: (e: Error) => void |
208 | | - children: (...args: any[]) => any |
209 | | -} |
210 | | - |
211 | | -function TestHook({ callback, onError, children }: TestHookProps) { |
212 | | - try { |
213 | | - children(callback()) |
214 | | - } catch (err) { |
215 | | - if (err.then) { |
216 | | - throw err |
217 | | - } else { |
218 | | - onError(err) |
219 | | - } |
220 | | - } |
221 | | - |
222 | | - // TODO decide what the test hook component should show |
223 | | - // maybe nothing, or maybe useful information about the hook? |
224 | | - // maybe its current properties? |
225 | | - // return <div>TestHook</div> |
226 | | - return null |
227 | | -} |
228 | | - |
229 | | -/** |
230 | | - * Mounts a React hook function in a test component for testing. |
231 | | - * |
232 | | - * @see https://github.com/bahmutov/cypress-react-unit-test#advanced-examples |
233 | | - */ |
234 | | -export const mountHook = (hookFn: (...args: any[]) => any) => { |
235 | | - const { result, setValue, setError } = resultContainer() |
236 | | - |
237 | | - return mount( |
238 | | - React.createElement(TestHook, { |
239 | | - callback: hookFn, |
240 | | - onError: setError, |
241 | | - children: setValue, |
242 | | - }), |
243 | | - ).then(() => { |
244 | | - cy.wrap(result) |
245 | | - }) |
246 | | -} |
247 | | - |
248 | | -export default mount |
| 4 | +/** @deprecated */ |
| 5 | +export { default } from './mount' |
0 commit comments