Skip to content

Commit b1f831a

Browse files
dmtrKovalenkoBarthélémy Ledouxelevatebart
authored
fix(@cypress/react): Devtools unpredictable resets (#15612)
* fix: Devtools unpredictable resets * Remove cleaning up from webpack-dev-server * Fix lint errors * Get back observer * fix: bring back cleanup (#15634) * fix: wait for fw teardown to do html teardown * fix: port responsibility of teardown to frameworks * chore: add comments * fix: typings * fix: react unmount cannot be called in the right hook * run dtslint Co-authored-by: Barthélémy Ledoux <bart@cypress.io> Co-authored-by: ElevateBart <ledouxb@gmail.com>
1 parent d9c3ae2 commit b1f831a

File tree

24 files changed

+182
-173
lines changed

24 files changed

+182
-173
lines changed

cli/types/cypress.d.ts

+12
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,18 @@ declare namespace Cypress {
527527
* @see https://on.cypress.io/catalog-of-events#App-Events
528528
*/
529529
off: Actions
530+
531+
/**
532+
* Trigger action
533+
* @private
534+
*/
535+
action: (action: string, ...args: any[]) => void
536+
537+
/**
538+
* Load files
539+
* @private
540+
*/
541+
onSpecWindow: (window: Window, specList: string[] | Array<() => Promise<void>>) => void
530542
}
531543

532544
type CanReturnChainable = void | Chainable | Promise<unknown>

npm/react/cypress/component/advanced/material-ui-example/button-spec.js

+8
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,11 @@ it('renders a button', () => {
99
</Button>,
1010
)
1111
})
12+
13+
it('renders a button with an icon', () => {
14+
mount(
15+
<Button variant="contained" color="primary" startIcon="⛹️">
16+
Hello World
17+
</Button>,
18+
)
19+
})

npm/react/cypress/component/advanced/renderless/mouse-movement.js

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export default class MouseMovement extends React.Component {
4343
clearTimeout(this.state.timer)
4444
this.props.onMoved(false)
4545
}
46+
4647
render () {
4748
return null
4849
}

npm/react/cypress/component/advanced/renderless/mouse-spec.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ describe('Renderless component', () => {
3131

3232
cy.get('@log')
3333
.invoke('getCalls')
34-
.then((calls) => calls.map((call) => call.args[0]))
34+
.then((calls) => {
35+
return calls.map((call) => {
36+
console.log('one', call.args[0])
37+
38+
return call.args[0]
39+
})
40+
})
3541
.should('deep.equal', [
3642
'MouseMovement constructor',
3743
'MouseMovement componentWillMount',

npm/react/src/mount.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ export const mount = (jsx: React.ReactNode, options: MountOptions = {}) => {
136136
})
137137
}
138138

139+
let initialInnerHtml = ''
140+
141+
Cypress.on('run:start', () => {
142+
initialInnerHtml = document.head.innerHTML
143+
})
144+
139145
/**
140146
* Removes the mounted component. Notice this command automatically
141147
* queues up the `unmount` into Cypress chain, thus you don't need `.then`
@@ -166,8 +172,17 @@ export const unmount = (options = { log: true }) => {
166172
})
167173
}
168174

169-
beforeEach(() => {
170-
unmount()
175+
// Cleanup before each run
176+
// NOTE: we cannot use unmount here because
177+
// we are not in the context of a test
178+
Cypress.on('test:before:run', () => {
179+
const el = document.getElementById(ROOT_ID)
180+
181+
if (el) {
182+
const wasUnmounted = ReactDOM.unmountComponentAtNode(el)
183+
184+
document.head.innerHTML = initialInnerHtml
185+
}
171186
})
172187

173188
/**

npm/vue/cypress/component/basic/code-coverage/Calculator-spec.js

+23
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,27 @@ describe('Calculator', () => {
2525
expect(includesVue, 'Calculator.vue is instrumented').to.be.true
2626
})
2727
})
28+
29+
it('adds two decimal numbers', () => {
30+
cy.viewport(400, 50)
31+
mount(Calculator)
32+
cy.get('[data-cy=a]').clear().type('22.6')
33+
cy.get('[data-cy=b]').clear().type('19.4')
34+
cy.contains('= 42')
35+
36+
cy.log('**check coverage**')
37+
cy.wrap(window)
38+
.its('__coverage__')
39+
// and it includes information even for this file
40+
.then(Object.keys)
41+
.should('include', Cypress.spec.absolute)
42+
.and((filenames) => {
43+
// coverage should include Calculator.vue file
44+
const includesVue = filenames.some((filename) => {
45+
return filename.endsWith('Calculator.vue')
46+
})
47+
48+
expect(includesVue, 'Calculator.vue is instrumented').to.be.true
49+
})
50+
})
2851
})

npm/vue/cypress/component/support.spec.js

-34
This file was deleted.

npm/vue/src/index.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,20 @@ function failTestOnVueError (err, vm, info) {
307307
window.top.onerror(err)
308308
}
309309

310-
function cyBeforeEach (cb: () => void) {
311-
Cypress.on('test:before:run', cb)
310+
let initialInnerHtml = ''
311+
312+
Cypress.on('run:start', () => {
313+
initialInnerHtml = document.head.innerHTML
314+
})
315+
316+
function registerAutoDestroy ($destroy: () => void) {
317+
Cypress.on('test:before:run', () => {
318+
$destroy()
319+
document.head.innerHTML = initialInnerHtml
320+
})
312321
}
313322

314-
enableAutoDestroy(cyBeforeEach)
323+
enableAutoDestroy(registerAutoDestroy)
315324

316325
/**
317326
* Mounts a Vue component inside Cypress browser.

npm/webpack-dev-server/index-template.html

+1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
<title>Components App</title>
88
</head>
99
<body>
10+
<div id="__cy_root"></div>
1011
</body>
1112
</html>
+2-36
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,12 @@
1-
/* eslint-disable no-console */
21
/*eslint-env browser */
32

4-
function appendTargetIfNotExists (id: string, tag = 'div', parent = document.body) {
5-
let node = document.getElementById(id)
6-
7-
node = document.createElement(tag)
8-
node.setAttribute('id', id)
9-
parent.appendChild(node)
10-
11-
return node
12-
}
13-
14-
export function init (importPromises, parent = (window.opener || window.parent)) {
15-
const Cypress = (window as any).Cypress = parent.Cypress
3+
export function init (importPromises, parent: Window = (window.opener || window.parent)) {
4+
const Cypress = window.Cypress = parent.Cypress
165

176
if (!Cypress) {
187
throw new Error('Tests cannot run without a reference to Cypress!')
198
}
209

2110
Cypress.onSpecWindow(window, importPromises)
2211
Cypress.action('app:window:before:load', window)
23-
24-
// In this variable, we save head
25-
// innerHTML to account for loader installed styles
26-
let headInnerHTML = ''
27-
28-
// before the run starts save
29-
Cypress.on('run:start', () => {
30-
headInnerHTML = document.head.innerHTML
31-
})
32-
33-
// Before all tests we are mounting the root element, **not beforeEach**
34-
// Cleaning up platform between tests is the responsibility of the specific adapter
35-
// because unmounting react/vue component should be done using specific framework API
36-
// (for devtools and to get rid of global event listeners from previous tests.)
37-
Cypress.on('test:before:run', () => {
38-
document.body.innerHTML = ''
39-
document.head.innerHTML = headInnerHTML
40-
appendTargetIfNotExists('__cy_root')
41-
})
42-
43-
return {
44-
restartRunner: Cypress.restartRunner,
45-
}
4612
}
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* We need this file to call window.Cypress
3+
*/
4+
export declare global {
5+
interface Window {
6+
Cypress: Cypress.Cypress;
7+
}
8+
}

npm/webpack-dev-server/tsconfig.json

+3-4
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
"outDir": "dist" /* Redirect output structure to the directory. */,
1818
// "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
1919
// "removeComments": true, /* Do not emit comments to output. */
20-
// "noEmit": true, /* Do not emit outputs. */
20+
// "noEmit": true, /* Do not emit outputs. */
2121
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
2222
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
2323
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
2424

2525
/* Strict Type-Checking Options */
2626
"strict": false /* Enable all strict type-checking options. */,
27-
"noImplicitAny": false,
27+
// "noImplicitAny": true,
2828

2929
/* Module Resolution Options */
3030
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
@@ -48,6 +48,5 @@
4848
"esModuleInterop": true
4949
},
5050
"include": ["src"],
51-
"exclude": ["*.js"],
52-
"exclude": ["node_modules"]
51+
"exclude": ["node_modules", "*.js"]
5352
}

packages/runner-ct/index.d.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
/// <reference path="../../cli/types/cypress.d.ts" />
33

44
declare module 'react-devtools-inline/frontend' {
5-
import * as React from 'react'
6-
75
export type DevtoolsProps = { browserTheme: 'dark' | 'light'}
86
export const initialize: (window: Window) => React.ComponentType<DevtoolsProps>;
97
}
108

119
declare module 'react-devtools-inline/backend' {
1210
export const initialize: (window: Window) => void
1311
export const activate: (window: Window) => void
12+
}
13+
14+
declare module "*.scss" {
15+
const value: Record<string, string>
16+
export default value
1417
}

packages/runner-ct/src/app/Plugins.tsx

+39-39
Original file line numberDiff line numberDiff line change
@@ -12,46 +12,46 @@ import styles from './RunnerCt.module.scss'
1212
interface PluginsProps {
1313
state: State
1414
pluginsHeight: number
15-
pluginRootContainerRef: React.MutableRefObject<HTMLDivElement>
1615
}
1716

1817
export const Plugins = namedObserver('Plugins',
19-
(props: PluginsProps) => (
20-
<Hidden
21-
type="layout"
22-
hidden={!props.state.isAnyPluginToShow}
23-
className={styles.ctPlugins}
24-
>
25-
<div className={styles.ctPluginsHeader}>
26-
{props.state.plugins.map((plugin) => (
27-
<button
28-
key={plugin.name}
29-
className={cs(styles.ctPluginToggleButton)}
30-
onClick={() => props.state.openDevtoolsPlugin(plugin)}
31-
>
32-
<span className={styles.ctPluginsName}>
33-
{plugin.name}
34-
</span>
35-
<div
36-
className={cs(styles.ctTogglePluginsSectionButton, {
37-
[styles.ctTogglePluginsSectionButtonOpen]: props.state.isAnyDevtoolsPluginOpen,
38-
})}
39-
>
40-
<FontAwesomeIcon
41-
icon='chevron-up'
42-
className={styles.ctPluginsName}
43-
/>
44-
</div>
45-
</button>
46-
))}
47-
</div>
18+
(props: PluginsProps) => {
19+
const ref = React.useRef<HTMLDivElement>(null)
20+
21+
return (
4822
<Hidden
49-
ref={props.pluginRootContainerRef}
50-
type="layout"
51-
className={styles.ctPluginsContainer}
52-
// deal with jumps when inspecting element
53-
hidden={!props.state.isAnyDevtoolsPluginOpen}
54-
style={{ height: props.pluginsHeight - PLUGIN_BAR_HEIGHT }}
55-
/>
56-
</Hidden>
57-
))
23+
type="visual"
24+
hidden={!props.state.isAnyPluginToShow}
25+
className={styles.ctPlugins}
26+
>
27+
<div className={styles.ctPluginsHeader}>
28+
{props.state.plugins.map((plugin) => (
29+
<button
30+
key={plugin.name}
31+
className={cs(styles.ctPluginToggleButton)}
32+
onClick={() => props.state.toggleDevtoolsPlugin(plugin, ref.current)}
33+
>
34+
<span className={styles.ctPluginsName}>
35+
{plugin.name}
36+
</span>
37+
<div
38+
className={cs(styles.ctTogglePluginsSectionButton, {
39+
[styles.ctTogglePluginsSectionButtonOpen]: props.state.isAnyDevtoolsPluginOpen,
40+
})}
41+
>
42+
<FontAwesomeIcon
43+
icon='chevron-up'
44+
className={styles.ctPluginsName}
45+
/>
46+
</div>
47+
</button>
48+
))}
49+
</div>
50+
<div
51+
ref={ref}
52+
className={styles.ctPluginsContainer}
53+
style={{ height: props.pluginsHeight - PLUGIN_BAR_HEIGHT }}
54+
/>
55+
</Hidden>
56+
)
57+
})

0 commit comments

Comments
 (0)