@@ -28,15 +28,22 @@ const injectStyles = (options: MountOptions) => {
28
28
* @example
29
29
```
30
30
import Hello from './hello.jsx'
31
- import {mount} from '@cypress/react'
31
+ import { mount } from '@cypress/react'
32
32
it('works', () => {
33
33
mount(<Hello onClick={cy.stub()} />)
34
34
// use Cypress commands
35
35
cy.contains('Hello').click()
36
36
})
37
37
```
38
38
**/
39
- export const mount = ( jsx : React . ReactNode , options : MountOptions = { } ) => {
39
+ export const mount = ( jsx : React . ReactNode , options : MountOptions = { } ) => _mount ( 'mount' , jsx , options )
40
+
41
+ /**
42
+ * @see `mount`
43
+ * @param type The type of mount executed
44
+ * @param rerenderKey If specified, use the provided key rather than generating a new one
45
+ */
46
+ const _mount = ( type : 'mount' | 'rerender' , jsx : React . ReactNode , options : MountOptions = { } , rerenderKey ?: string ) : globalThis . Cypress . Chainable < MountReturn > => {
40
47
// Get the display name property via the component constructor
41
48
// @ts -ignore FIXME
42
49
const componentName = getDisplayName ( jsx . type , options . alias )
@@ -60,9 +67,9 @@ export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
60
67
)
61
68
}
62
69
63
- const key =
70
+ const key = rerenderKey ??
64
71
// @ts -ignore provide unique key to the the wrapped component to make sure we are rerendering between tests
65
- ( Cypress ?. mocha ?. getRunner ( ) ?. test ?. title || '' ) + Math . random ( )
72
+ ( Cypress ?. mocha ?. getRunner ( ) ?. test ?. title as string || '' ) + Math . random ( )
66
73
const props = {
67
74
key,
68
75
}
@@ -74,37 +81,48 @@ export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
74
81
)
75
82
// since we always surround the component with a fragment
76
83
// let's get back the original component
77
- // @ts -ignore
78
- const userComponent = reactComponent . props . children
84
+ const userComponent = ( reactComponent . props as {
85
+ key : string
86
+ children : React . ReactNode
87
+ } ) . children
79
88
80
89
reactDomToUse . render ( reactComponent , el )
81
90
82
91
if ( options . log !== false ) {
83
92
Cypress . log ( {
84
- name : 'mount' ,
93
+ name : type ,
94
+ type : 'parent' ,
85
95
message : [ message ] ,
86
96
$el : ( el . children . item ( 0 ) as unknown ) as JQuery < HTMLElement > ,
87
97
consoleProps : ( ) => {
88
98
return {
89
99
// @ts -ignore protect the use of jsx functional components use ReactNode
90
100
props : jsx . props ,
91
- description : ' Mounts React component',
101
+ description : type === 'mount' ? ' Mounts React component' : 'Rerenders mounted React component',
92
102
home : 'https://github.com/cypress-io/cypress' ,
93
103
}
94
104
} ,
95
105
} ) . snapshot ( 'mounted' ) . end ( )
96
106
}
97
107
98
108
return (
99
- cy
100
- . wrap ( userComponent , { log : false } )
109
+ // Separate alias and returned value. Alias returns the component only, and the thenable returns the additional functions
110
+ cy . wrap < React . ReactNode > ( userComponent )
101
111
. as ( displayName )
112
+ . then ( ( ) => {
113
+ return cy . wrap < MountReturn > ( {
114
+ component : userComponent ,
115
+ rerender : ( newComponent ) => _mount ( 'rerender' , newComponent , options , key ) ,
116
+ unmount,
117
+ } , { log : false } )
118
+ } )
102
119
// by waiting, we delaying test execution for the next tick of event loop
103
120
// and letting hooks and component lifecycle methods to execute mount
104
121
// https://github.com/bahmutov/cypress-react-unit-test/issues/200
105
122
. wait ( 0 , { log : false } )
106
123
)
107
- } )
124
+ // Bluebird types are terrible. I don't think the return type can be carried without this cast
125
+ } ) as unknown as globalThis . Cypress . Chainable < MountReturn >
108
126
}
109
127
110
128
let initialInnerHtml = ''
@@ -129,7 +147,7 @@ Cypress.on('run:start', () => {
129
147
})
130
148
```
131
149
*/
132
- export const unmount = ( options = { log : true } ) => {
150
+ export const unmount = ( options = { log : true } ) : globalThis . Cypress . Chainable < JQuery < HTMLElement > > => {
133
151
return cy . then ( ( ) => {
134
152
const selector = `#${ ROOT_ID } `
135
153
@@ -185,12 +203,13 @@ export const createMount = (defaultOptions: MountOptions) => {
185
203
}
186
204
187
205
/** @deprecated Should be removed in the next major version */
206
+ // TODO: Remove
188
207
export default mount
189
208
190
209
// I hope to get types and docs from functions imported from ./index one day
191
210
// but for now have to document methods in both places
192
211
// like this: import {mount} from './index'
193
-
212
+ // TODO: Clean up types
194
213
export interface ReactModule {
195
214
name : string
196
215
type : string
@@ -254,6 +273,23 @@ export interface MountReactComponentOptions {
254
273
255
274
export type MountOptions = Partial < StyleOptions & MountReactComponentOptions >
256
275
276
+ export interface MountReturn {
277
+ /**
278
+ * The component that was rendered.
279
+ */
280
+ component : React . ReactNode
281
+ /**
282
+ * Rerenders the specified component with new props. This allows testing of components that store state (`setState`)
283
+ * or have asynchronous updates (`useEffect`, `useLayoutEffect`).
284
+ */
285
+ rerender : ( component : React . ReactNode ) => globalThis . Cypress . Chainable < MountReturn >
286
+ /**
287
+ * Removes the mounted component.
288
+ * @see `unmount`
289
+ */
290
+ unmount : ( ) => globalThis . Cypress . Chainable < JQuery < HTMLElement > >
291
+ }
292
+
257
293
/**
258
294
* The `type` property from the transpiled JSX object.
259
295
* @example
0 commit comments