Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(@cypress/react): Devtools unpredictable resets #15612

Merged
merged 15 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/runner-ct/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
/// <reference path="../../cli/types/cypress.d.ts" />

declare module 'react-devtools-inline/frontend' {
import * as React from 'react'

export type DevtoolsProps = { browserTheme: 'dark' | 'light'}
export const initialize: (window: Window) => React.ComponentType<DevtoolsProps>;
}

declare module 'react-devtools-inline/backend' {
export const initialize: (window: Window) => void
export const activate: (window: Window) => void
}

declare module "*.scss" {
const value: Record<string, string>
export default value
}
89 changes: 42 additions & 47 deletions packages/runner-ct/src/app/Plugins.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,57 @@
import cs from 'classnames'
import * as React from 'react'
import State from '../lib/state'
import { Hidden } from '../lib/Hidden'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import styles from './RunnerCt.module.scss'
import { observer } from 'mobx-react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { PLUGIN_BAR_HEIGHT } from './RunnerCt'
import { UIPlugin } from '../plugins/UIPlugin'
import { Hidden } from '../lib/Hidden'

interface PluginsProps {
state: State
pluginsHeight: number
pluginRootContainer: React.MutableRefObject<HTMLDivElement>
}

export const Plugins = observer(
function Plugins (props: PluginsProps) {
function onClick (plugin: UIPlugin) {
return props.state.openDevtoolsPlugin(plugin)
}
export function Plugins (props: PluginsProps) {
const ref = React.useRef<HTMLDivElement>(null)

function handlePluginClick (plugin: UIPlugin) {
props.state.toggleDevtoolsPlugin(plugin, ref.current)
}

return (
<Hidden
type="layout"
hidden={!props.state.isAnyPluginToShow}
className={styles.ctPlugins}
>
<div className={styles.ctPluginsHeader}>
{props.state.plugins.map((plugin) => (
<button
key={plugin.name}
onClick={() => onClick(plugin)}
className={cs(styles.ctPluginToggleButton)}
return (
<Hidden
type='visual'
hidden={!props.state.isAnyPluginToShow}
className={styles.ctPlugins}
>
<div className={styles.ctPluginsHeader}>
{props.state.plugins.map((plugin) => (
<button
key={plugin.name}
onClick={() => handlePluginClick(plugin)}
className={cs(styles.ctPluginToggleButton)}
>
<span className={styles.ctPluginsName}>{plugin.name}</span>
<div
className={cs(styles.ctTogglePluginsSectionButton, {
[styles.ctTogglePluginsSectionButtonOpen]: props.state.isAnyDevtoolsPluginOpen,
})}
>
<span className={styles.ctPluginsName}>{plugin.name}</span>
<div
className={cs(styles.ctTogglePluginsSectionButton, {
[styles.ctTogglePluginsSectionButtonOpen]: props.state.isAnyDevtoolsPluginOpen,
})}
>
<FontAwesomeIcon
icon='chevron-up'
className={styles.ctPluginsName}
/>
</div>
</button>
))}
</div>
<FontAwesomeIcon
icon='chevron-up'
className={styles.ctPluginsName}
/>
</div>
</button>
))}
</div>

<Hidden
type="layout"
ref={props.pluginRootContainer}
className={styles.ctPluginsContainer}
// deal with jumps when inspecting element
hidden={!props.state.isAnyDevtoolsPluginOpen}
style={{ height: props.pluginsHeight - PLUGIN_BAR_HEIGHT }}
/>
</Hidden>
)
},
)
<div
ref={ref}
className={styles.ctPluginsContainer}
style={{ height: props.pluginsHeight - PLUGIN_BAR_HEIGHT }}
/>
</Hidden>
)
}
36 changes: 16 additions & 20 deletions packages/runner-ct/src/app/RunnerCt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import './RunnerCt.scss'
import { Plugins } from './Plugins'
import { NoSpecSelected } from './NoSpecSelected'

interface AppProps {
interface RunnerCtProps {
state: State
eventManager: typeof EventManager
config: Cypress.RuntimeConfigOptions
config: Cypress.RuntimeConfigOptions & Cypress.ResolvedConfigOptions
}

export const DEFAULT_PLUGINS_HEIGHT = 300
Expand All @@ -44,11 +44,10 @@ export const AUT_IFRAME_MARGIN = {
Y: 16,
}

const App: React.FC<AppProps> = observer(
function App (props: AppProps) {
const RunnerCt: React.FC<RunnerCtProps> = observer(
function RunnerCt (props: RunnerCtProps) {
const searchRef = React.useRef<HTMLInputElement>(null)
const splitPaneRef = React.useRef<{ splitPane: HTMLDivElement }>(null)
const pluginRootContainer = React.useRef<null | HTMLDivElement>(null)

const { state, eventManager, config } = props

Expand Down Expand Up @@ -81,9 +80,7 @@ const App: React.FC<AppProps> = observer(
}

React.useEffect(() => {
if (pluginRootContainer.current) {
state.initializePlugins(config, pluginRootContainer.current)
}
state.initializePlugins(config)
}, [])

React.useEffect(() => {
Expand Down Expand Up @@ -220,14 +217,6 @@ const App: React.FC<AppProps> = observer(
/>
)

const autRunnerContent = state.spec
? <Iframes {...props} />
: (
<NoSpecSelected>
<KeyboardHelper />
</NoSpecSelected>
)

const MainAreaComponent: React.FC | typeof SplitPane = props.state.spec
? SplitPane
: (props) => <div>{props.children}</div>
Expand Down Expand Up @@ -266,7 +255,6 @@ const App: React.FC<AppProps> = observer(
}}
ref={splitPaneRef}
onChange={debounce(onSpecListPaneChange)}

>
<SpecList
specs={filteredSpecs}
Expand Down Expand Up @@ -313,14 +301,22 @@ const App: React.FC<AppProps> = observer(
},
)}>
<Header {...props} ref={headerRef} />
{autRunnerContent}
{
state.spec
? <Iframes {...props} />
: (
<NoSpecSelected>
<KeyboardHelper />
</NoSpecSelected>
)
}
<Message state={state} />
</div>

<Plugins
key="plugins"
state={props.state}
pluginsHeight={hideIfScreenshotting(() => state.pluginsHeight)}
pluginRootContainer={pluginRootContainer}
/>
</SplitPane>
</MainAreaComponent>
Expand All @@ -330,4 +326,4 @@ const App: React.FC<AppProps> = observer(
},
)

export default App
export default React.memo(RunnerCt, () => true)
5 changes: 3 additions & 2 deletions packages/runner-ct/src/iframe/iframes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,12 @@ export default class Iframes extends Component {

this.props.eventManager.setup(config)

const $autIframe = this._loadIframes(spec)

// This is extremely required to not run test till devtools registered
when(() => this.props.state.readyToRunTests, () => {
window.Cypress.on('window:before:load', this.props.state.registerDevtools)

const $autIframe = this._loadIframes(spec)

this.props.eventManager.initialize($autIframe, config)
})
}
Expand Down
19 changes: 7 additions & 12 deletions packages/runner-ct/src/lib/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,19 +329,19 @@ export default class State {
}
}

loadReactDevTools = (rootElement: HTMLElement) => {
loadReactDevTools = () => {
return import(/* webpackChunkName: "ctChunk-reactdevtools" */ '../plugins/ReactDevtools')
.then(action((ReactDevTools) => {
this.plugins = [
ReactDevTools.create(rootElement),
ReactDevTools.create(),
]
}))
}

@action
initializePlugins = (config: Cypress.RuntimeConfigOptions, rootElement: HTMLElement) => {
initializePlugins = (config: Cypress.RuntimeConfigOptions & Cypress.ResolvedConfigOptions) => {
if (config.env.reactDevtools && !config.isTextTerminal) {
this.loadReactDevTools(rootElement)
this.loadReactDevTools()
.then(action(() => {
this.readyToRunTests = true
}))
Expand Down Expand Up @@ -370,34 +370,29 @@ export default class State {
}

@action
openDevtoolsPlugin = (plugin: UIPlugin) => {
toggleDevtoolsPlugin = (plugin: UIPlugin, domElement: HTMLElement) => {
if (this.activePlugin === plugin.name) {
plugin.unmount()
this.setActivePlugin(null)
// set this back to default to force the AUT to resize vertically
// if the aspect ratio is very long on the Y axis.
this.pluginsHeight = PLUGIN_BAR_HEIGHT
} else {
plugin.mount()
this.setActivePlugin(plugin.name)
// set this to force the AUT to resize vertically if the aspect ratio is very long
// on the Y axis.
this.pluginsHeight = DEFAULT_PLUGINS_HEIGHT
plugin.mount(domElement)
}
}

@action
toggleDevtoolsPlugin = () => {
this.openDevtoolsPlugin(this.plugins[0]) // temporal solution change when will be more than 1 plugin
}

@computed
get isAnyDevtoolsPluginOpen () {
return this.activePlugin !== null
}

@computed
get isAnyPluginToShow () {
return this.plugins.length > 0
return Boolean(this.plugins.length > 0 && this.spec)
}
}
29 changes: 15 additions & 14 deletions packages/runner-ct/src/plugins/ReactDevtools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,35 @@ import {
initialize as initializeBackend,
} from 'react-devtools-inline/backend'
import { ReactDevtoolsFallback } from './ReactDevtoolsFallback'
import { initialize as initializeFrontend } from 'react-devtools-inline/frontend'
import { DevtoolsProps, initialize as initializeFrontend } from 'react-devtools-inline/frontend'
import { UIPlugin } from './UIPlugin'

const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)')

export function create (root: HTMLElement): UIPlugin {
let DevTools = () => null
export function create (): UIPlugin {
// This doesn't really have sense right now due, but we need to deal with this in future
// For now react-split-pane view is recreating virtual tree on each render
// Thats why when `state.spec` changed domElement will be recreated and content will be flushed
let DevTools: React.ComponentType<DevtoolsProps> = ReactDevtoolsFallback
let isMounted = false
let isFirstMount = true
let _contentWindow = null

// @ts-expect-error yes it is required to render it with concurrent mode
const devtoolsRoot = ReactDomExperimental.unstable_createRoot(root)

function mount () {
if (!document.querySelector('.aut-iframe')) {
devtoolsRoot.render(<ReactDevtoolsFallback />)

return
}
let _contentWindow: Window | null = null
let devtoolsRoot: { render: (component: JSX.Element) => void, unmount: () => void } | null = null

function mount (domElement?: HTMLElement) {
if (!isFirstMount) {
// if devtools were unmounted it is closing the bridge, so we need to reinitialize the bridge on our side
DevTools = initializeFrontend(_contentWindow)
activateBackend(_contentWindow)
}

if (domElement) {
// @ts-expect-error unstable is not typed
devtoolsRoot = ReactDomExperimental.unstable_createRoot(domElement)
}

devtoolsRoot.render(<DevTools browserTheme={prefersDarkScheme ? 'dark' : 'light'} />)

isMounted = true
isFirstMount = false
}
Expand Down
4 changes: 3 additions & 1 deletion packages/runner-ct/src/plugins/ReactDevtoolsFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import './devtools-fallback.scss'

export const ReactDevtoolsFallback: React.FC = () => {
return (
<p className='react-devtools-fallback'>Select a spec or re-run the current spec to activate devtools.</p>
<p className='react-devtools-fallback'>
Select a spec or re-run the current spec to activate devtools.
</p>
)
}
2 changes: 1 addition & 1 deletion packages/runner-ct/src/plugins/UIPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ export type UIPlugin = {
name: string
type: 'devtools'
initialize: (contentWindow: Window) => void
mount: () => void
mount: (element: HTMLElement) => void
unmount: () => void
}
2 changes: 2 additions & 0 deletions packages/runner-ct/static/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<!-- Uncomment to connect using standalone react-devtools -->
<!-- <script src="http://localhost:8097"></script> -->
elevatebart marked this conversation as resolved.
Show resolved Hide resolved
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{projectName}}</title>
Expand Down