From f7d32e50696c73f92be3481453ffba6799d2c681 Mon Sep 17 00:00:00 2001 From: AtofStryker Date: Thu, 14 Nov 2024 11:57:40 -0500 Subject: [PATCH 1/2] chore: fix deprecation warnings and refactor react reporter to use hooks wip --- packages/driver/cypress/support/utils.ts | 18 +- packages/reporter/src/attempts/attempts.tsx | 51 ++-- .../reporter/src/collapsible/collapsible.tsx | 103 ++++---- packages/reporter/src/commands/command.tsx | 204 +++++++--------- .../reporter/src/errors/error-code-frame.tsx | 47 ++-- packages/reporter/src/errors/test-error.tsx | 14 +- packages/reporter/src/lib/flash-on-click.tsx | 66 ++---- packages/reporter/src/lib/switch.tsx | 44 ++-- packages/reporter/src/main.tsx | 185 +++++++-------- .../src/runnables/runnable-and-suite.tsx | 41 ++-- .../src/runnables/runnable-header.tsx | 73 +++--- packages/reporter/src/runnables/runnables.tsx | 78 +++--- packages/reporter/src/test/test.tsx | 224 +++++++----------- 13 files changed, 490 insertions(+), 658 deletions(-) diff --git a/packages/driver/cypress/support/utils.ts b/packages/driver/cypress/support/utils.ts index d96a83a96c47..473b8afbea93 100644 --- a/packages/driver/cypress/support/utils.ts +++ b/packages/driver/cypress/support/utils.ts @@ -13,15 +13,17 @@ export const getCommandLogWithText = (command, type?) => { .closest('.command') } -export const findReactInstance = function (dom) { +// This work around is super hacky to get the appState from the Test Mobx Observable Model +// this is needed to pause the runner to assert on the test +export const findAppStateFromTest = function (dom) { let key = _.keys(dom).find((key) => key.startsWith('__reactFiber')) as string let internalInstance = dom[key] if (internalInstance == null) return null return internalInstance._debugOwner - ? internalInstance._debugOwner.stateNode - : internalInstance.return.stateNode + ? internalInstance._debugOwner.memoizedProps.model.store.appState + : internalInstance.return.memoizedProps.model.store.appState } export const clickCommandLog = (sel, type?) => { @@ -31,13 +33,17 @@ export const clickCommandLog = (sel, type?) => { .then(() => { const commandLogEl = getCommandLogWithText(sel, type) - const reactCommandInstance = findReactInstance(commandLogEl[0]) + const activeTestEl = commandLogEl[0].closest('li.test.runnable.runnable-active') - if (!reactCommandInstance) { + // We are manually manipulating the state of the appState to stop the runner. + // This does NOT happen in the wild and is only for testing purposes. + const appStateInstance = findAppStateFromTest(activeTestEl) + + if (!appStateInstance) { assert(false, 'failed to get command log React instance') } - reactCommandInstance.props.appState.isRunning = false + appStateInstance.isRunning = false const inner = $(commandLogEl).find('.command-wrapper-text') inner.get(0).click() diff --git a/packages/reporter/src/attempts/attempts.tsx b/packages/reporter/src/attempts/attempts.tsx index a1b974872986..4b3dd9af81fd 100644 --- a/packages/reporter/src/attempts/attempts.tsx +++ b/packages/reporter/src/attempts/attempts.tsx @@ -1,6 +1,6 @@ import cs from 'classnames' import { observer } from 'mobx-react' -import React, { Component } from 'react' +import React from 'react' import type { TestState } from '@packages/types' import Agents from '../agents/agents' @@ -52,7 +52,7 @@ function renderAttemptContent (model: AttemptModel, studioActive: boolean) { -
+
{model.hasCommands ? : }
{model.state === 'failed' && ( @@ -71,37 +71,24 @@ interface AttemptProps { studioActive: boolean } -@observer -class Attempt extends Component { - componentDidUpdate () { - this.props.scrollIntoView() - } - - render () { - const { model, studioActive } = this.props - - // HACK: causes component update when command log is added - model.commands.length - - return ( -
  • = observer(({ model, scrollIntoView, studioActive }) => { + return ( +
  • + } + hideExpander + headerClass='attempt-name' + contentClass='attempt-content' + isOpen={model.isOpen} > - } - hideExpander - headerClass='attempt-name' - contentClass='attempt-content' - isOpen={model.isOpen} - > - {renderAttemptContent(model, studioActive)} - -
  • - ) - } -} + {renderAttemptContent(model, studioActive)} + + + ) +}) const Attempts = observer(({ test, scrollIntoView, studioActive }: {test: TestModel, scrollIntoView: Function, studioActive: boolean}) => { return (
      contentClass?: string - hideExpander: boolean + hideExpander?: boolean + children: React.ReactNode } -interface State { - isOpen: boolean -} - -class Collapsible extends Component { - static defaultProps = { - isOpen: false, - headerClass: '', - headerStyle: {}, - contentClass: '', - hideExpander: false, - } +const Collapsible: React.FC = ({ isOpen: isOpenAsProp = false, header, headerClass = '', headerStyle = {}, headerExtras, contentClass = '', hideExpander = false, containerRef = null, toggleOpen = () => undefined, children }) => { + const [isOpen, setIsOpen] = useState(isOpenAsProp) - constructor (props: Props) { - super(props) + useEffect(() => { + setIsOpen(isOpenAsProp) + }, [isOpenAsProp]) - this.state = { isOpen: props.isOpen || false } + const _onClick = (e: MouseEvent) => { + e.stopPropagation() + setIsOpen(!isOpen) } - componentDidUpdate (prevProps: Props) { - if (this.props.isOpen != null && this.props.isOpen !== prevProps.isOpen) { - this.setState({ isOpen: this.props.isOpen }) - } + const _onKeyPress = () => { + setIsOpen(!isOpen) } - render () { - return ( -
      -
      + return ( +
      +
      +
      -
      - {!this.props.hideExpander && } - - {this.props.header} - -
      + {!hideExpander && } + + {header} +
      - {this.props.headerExtras}
      - {this.state.isOpen && ( -
      - {this.props.children} -
      - )} + {headerExtras}
      - ) - } - - _toggleOpen = () => { - this.setState({ isOpen: !this.state.isOpen }) - } - - _onClick = (e: MouseEvent) => { - e.stopPropagation() - this._toggleOpen() - } - - _onKeyPress = () => { - this._toggleOpen() - } + {isOpen && ( +
      + {children} +
      + )} +
      + ) } export default Collapsible diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index d518fddcd715..1baeb9690d33 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -1,18 +1,17 @@ import _ from 'lodash' import cs from 'classnames' import Markdown from 'markdown-it' -import { action, observable, makeObservable } from 'mobx' import { observer } from 'mobx-react' -import React, { Component } from 'react' +import React, { useState } from 'react' import Tooltip from '@cypress/react-tooltip' -import appState, { AppState } from '../lib/app-state' -import events, { Events } from '../lib/events' +import appState from '../lib/app-state' +import events from '../lib/events' import FlashOnClick from '../lib/flash-on-click' import StateIcon from '../lib/state-icon' import Tag from '../lib/tag' import type { TimeoutID } from '../lib/types' -import runnablesStore, { RunnablesStore } from '../runnables/runnables-store' +import runnablesStore from '../runnables/runnables-store' import type { Alias, AliasObject } from '../instruments/instrument-model' import { determineTagType } from '../sessions/utils' @@ -290,9 +289,6 @@ const Progress = observer(({ model }: ProgressProps) => { interface Props { model: CommandModel aliasesWithDuplicates: Array | null - appState: AppState - events: Events - runnablesStore: RunnablesStore groupId?: number } @@ -369,97 +365,28 @@ const CommandControls = observer(({ model, commandName, events }) => { ) }) -@observer -class Command extends Component { - @observable isOpen: boolean|null = null - private _showTimeout?: TimeoutID +const Command: React.FC = observer(({ model, aliasesWithDuplicates, groupId }) => { + const [showTimeout, setShowTimeout] = useState(undefined) - static defaultProps = { - appState, - events, - runnablesStore, - } - - constructor (props: Props) { - super(props) - makeObservable(this) + if (model.group && groupId !== model.group) { + return null } - render () { - const { model, aliasesWithDuplicates } = this.props - - if (model.group && this.props.groupId !== model.group) { - return null - } - - const commandName = model.name ? nameClassName(model.name) : '' - const groupPlaceholder: Array = [] + const commandName = model.name ? nameClassName(model.name) : '' + const groupPlaceholder: Array = [] - let groupLevel = 0 + let groupLevel = 0 - if (model.groupLevel !== undefined) { - // cap the group nesting to 5 levels to keep the log text legible - groupLevel = model.groupLevel < 6 ? model.groupLevel : 5 + if (model.groupLevel !== undefined) { + // cap the group nesting to 5 levels to keep the log text legible + groupLevel = model.groupLevel < 6 ? model.groupLevel : 5 - for (let i = 1; i < groupLevel; i++) { - groupPlaceholder.push() - } + for (let i = 1; i < groupLevel; i++) { + groupPlaceholder.push() } - - return ( - <> -
    • -
      - - -
      this._snapshot(true)} - onMouseLeave={() => this._snapshot(false)} - > - {groupPlaceholder} - - -
      -
      -
      - - {this._children()} -
    • - {model.showError && ( -
    • - -
    • - )} - - ) } - _children () { - const { appState, events, model, runnablesStore } = this.props - + const _children = () => { if (!model.hasChildren || !model.isOpen) { return null } @@ -470,9 +397,6 @@ class Command extends Component { @@ -481,27 +405,27 @@ class Command extends Component { ) } - _isPinned () { - return this.props.appState.pinnedSnapshotId === this.props.model.id + const _isPinned = () => { + return appState.pinnedSnapshotId === model.id } - _shouldShowClickMessage = () => { - return !this.props.appState.isRunning && !!this.props.model.hasConsoleProps + const _shouldShowClickMessage = () => { + return !appState.isRunning && !!model.hasConsoleProps } - @action _toggleColumnPin = () => { - if (this.props.appState.isRunning) return + const _toggleColumnPin = () => { + if (appState.isRunning) return - const { testId, id } = this.props.model + const { testId, id } = model - if (this._isPinned()) { - this.props.appState.pinnedSnapshotId = null - this.props.events.emit('unpin:snapshot', testId, id) - this._snapshot(true) + if (_isPinned()) { + appState.pinnedSnapshotId = null + events.emit('unpin:snapshot', testId, id) + _snapshot(true) } else { - this.props.appState.pinnedSnapshotId = id as number - this.props.events.emit('pin:snapshot', testId, id) - this.props.events.emit('show:command', testId, id) + appState.pinnedSnapshotId = id as number + events.emit('pin:snapshot', testId, id) + events.emit('show:command', testId, id) } } @@ -522,31 +446,79 @@ class Command extends Component { // over many commands, unless you're hovered for // 50ms, it won't show the snapshot at all. so we // optimize for both snapshot showing + restoring - _snapshot (show: boolean) { - const { model, runnablesStore } = this.props - + const _snapshot = (show: boolean) => { if (show) { runnablesStore.attemptingShowSnapshot = true - this._showTimeout = setTimeout(() => { + setShowTimeout(setTimeout(() => { runnablesStore.showingSnapshot = true - this.props.events.emit('show:snapshot', model.testId, model.id) - }, 50) + events.emit('show:snapshot', model.testId, model.id) + }, 50)) } else { runnablesStore.attemptingShowSnapshot = false - clearTimeout(this._showTimeout as TimeoutID) + clearTimeout(showTimeout as TimeoutID) setTimeout(() => { // if we are currently showing a snapshot but // we aren't trying to show a different snapshot if (runnablesStore.showingSnapshot && !runnablesStore.attemptingShowSnapshot) { runnablesStore.showingSnapshot = false - this.props.events.emit('hide:snapshot', model.testId, model.id) + events.emit('hide:snapshot', model.testId, model.id) } }, 50) } } -} + + return ( + <> +
    • +
      + + +
      _snapshot(true)} + onMouseLeave={() => _snapshot(false)} + > + {groupPlaceholder} + + +
      +
      +
      + + {_children()} +
    • + {model.showError && ( +
    • + +
    • + )} + + ) +}) export { Aliases, AliasesReferences, Message, Progress } diff --git a/packages/reporter/src/errors/error-code-frame.tsx b/packages/reporter/src/errors/error-code-frame.tsx index ec4fa4bf0cde..1316743003a0 100644 --- a/packages/reporter/src/errors/error-code-frame.tsx +++ b/packages/reporter/src/errors/error-code-frame.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react' +import React, { useEffect, useRef } from 'react' import { observer } from 'mobx-react' import Prism from 'prismjs' @@ -9,28 +9,27 @@ interface Props { codeFrame: CodeFrame } -@observer -class ErrorCodeFrame extends Component { - componentDidMount () { - Prism.highlightAllUnder(this.refs.codeFrame as ParentNode) - } - - render () { - const { line, frame, language } = this.props.codeFrame - - // since we pull out 2 lines above the highlighted code, it will always - // be the 3rd line unless it's at the top of the file (lines 1 or 2) - const highlightLine = Math.min(line, 3) - - return ( -
      - -
      -          {frame}
      -        
      -
      - ) - } -} +const ErrorCodeFrame: React.FC = observer(({ codeFrame }) => { + const codeFrameRef = useRef(null) + + const { line, frame, language } = codeFrame + + // since we pull out 2 lines above the highlighted code, it will always + // be the 3rd line unless it's at the top of the file (lines 1 or 2) + const highlightLine = Math.min(line, 3) + + useEffect(() => { + Prism.highlightAllUnder(codeFrameRef.current as unknown as ParentNode) + }, []) + + return ( +
      + +
      +        {frame}
      +      
      +
      + ) +}) export default ErrorCodeFrame diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index 26ee4772a0b6..6010114b4a62 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -44,12 +44,10 @@ interface TestErrorProps { testId?: string commandId?: number // the command group level to nest the recovered in-test error - groupLevel: number + groupLevel?: number } -const TestError = (props: TestErrorProps) => { - const { err } = props - +const TestError: React.FC = ({ err, groupLevel = 0, testId, commandId }) => { if (!err || !err.displayMessage) return null const md = new Markdown('zero') @@ -57,7 +55,7 @@ const TestError = (props: TestErrorProps) => { md.enable(['backticks', 'emphasis', 'escape']) const onPrint = () => { - events.emit('show:error', props) + events.emit('show:error', { err, groupLevel, testId, commandId }) } const _onPrintClick = (e: MouseEvent) => { @@ -72,7 +70,7 @@ const TestError = (props: TestErrorProps) => { if (err.isRecovered) { // cap the group nesting to 5 levels to keep the log text legible - for (let i = 0; i < props.groupLevel; i++) { + for (let i = 0; i < groupLevel; i++) { groupPlaceholder.push() } } @@ -121,8 +119,4 @@ const TestError = (props: TestErrorProps) => { ) } -TestError.defaultProps = { - groupLevel: 0, -} - export default observer(TestError) diff --git a/packages/reporter/src/lib/flash-on-click.tsx b/packages/reporter/src/lib/flash-on-click.tsx index 377b86149fc6..99350f6be7f0 100644 --- a/packages/reporter/src/lib/flash-on-click.tsx +++ b/packages/reporter/src/lib/flash-on-click.tsx @@ -1,7 +1,6 @@ -import { action, observable, makeObservable } from 'mobx' +import { action } from 'mobx' import { observer } from 'mobx-react' -import PropTypes from 'prop-types' -import React, { Children, cloneElement, Component, MouseEvent, ReactElement, ReactNode } from 'react' +import React, { Children, cloneElement, MouseEvent, ReactElement, ReactNode, useState } from 'react' // @ts-ignore import Tooltip from '@cypress/react-tooltip' @@ -10,55 +9,34 @@ interface Props { onClick: ((e: MouseEvent) => void) shouldShowMessage?: (() => boolean) wrapperClassName?: string + children: React.ReactNode } -@observer -class FlashOnClick extends Component { - static propTypes = { - message: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, - shouldShowMessage: PropTypes.func, - wrapperClassName: PropTypes.string, - } - - static defaultProps = { - shouldShowMessage: () => true, - } - - @observable _show = false - - constructor (props: Props) { - super(props) - makeObservable(this) - } - - render () { - const child = Children.only(this.props.children) - - return ( - - {cloneElement(child as ReactElement, { onClick: this._onClick })} - - ) - } - - @action _onClick = (e: MouseEvent) => { - const { onClick, shouldShowMessage } = this.props +const FlashOnClick: React.FC = observer(({ message, onClick, wrapperClassName, children, shouldShowMessage = () => true }) => { + const [show, setShow] = useState(false) + const _onClick = (e: MouseEvent) => { onClick(e) if (shouldShowMessage && !shouldShowMessage()) return - this._show = true + setShow(true) setTimeout(action('hide:console:message', () => { - this._show = false + setShow(false) }), 1500) } -} + const child = Children.only(children) + + return ( + + {cloneElement(child as ReactElement, { onClick: _onClick })} + + ) +}) export default FlashOnClick diff --git a/packages/reporter/src/lib/switch.tsx b/packages/reporter/src/lib/switch.tsx index ab25cffcb8a1..f1050dc0e6d3 100644 --- a/packages/reporter/src/lib/switch.tsx +++ b/packages/reporter/src/lib/switch.tsx @@ -1,6 +1,5 @@ -import { action, makeObservable } from 'mobx' import { observer } from 'mobx-react' -import React, { Component } from 'react' +import React from 'react' interface Props { value: boolean @@ -9,34 +8,23 @@ interface Props { onUpdate: (e: MouseEvent) => void } -@observer -class Switch extends Component { - @action _onClick = (e: MouseEvent) => { - const { onUpdate } = this.props - +const Switch: React.FC = observer(({ value, 'data-cy': dataCy, size = 'lg', onUpdate }) => { + const _onClick = (e: MouseEvent) => { onUpdate(e) } - constructor (props: Props) { - super(props) - makeObservable(this) - } - - render () { - const { 'data-cy': dataCy, size = 'lg', value } = this.props - - return ( - - ) - } -} + return ( + + ) +}) export default Switch diff --git a/packages/reporter/src/main.tsx b/packages/reporter/src/main.tsx index e8a201ce1e96..a70fa5f3070a 100644 --- a/packages/reporter/src/main.tsx +++ b/packages/reporter/src/main.tsx @@ -2,14 +2,14 @@ import { action, runInAction } from 'mobx' import { observer } from 'mobx-react' import cs from 'classnames' -import React, { Component } from 'react' +import React, { useEffect, useRef } from 'react' import { createRoot } from 'react-dom/client' // @ts-ignore import EQ from 'css-element-queries/src/ElementQueries' import type { RunnablesErrorModel } from './runnables/runnable-error' import appState, { AppState } from './lib/app-state' -import events, { Runner, Events } from './lib/events' +import events, { Events, Runner } from './lib/events' import runnablesStore, { RunnablesStore } from './runnables/runnables-store' import scroller, { Scroller } from './lib/scroller' import statsStore, { StatsStore } from './header/stats-store' @@ -20,6 +20,16 @@ import Runnables from './runnables/runnables' import TestingPreferences from './preferences/testing-preferences' import type { MobxRunnerStore } from '@packages/app/src/store/mobx-runner-store' +function usePrevious (value) { + const ref = useRef() + + useEffect(() => { + ref.current = value + }) + + return ref.current +} + export interface BaseReporterProps { appState: AppState className?: string @@ -38,108 +48,87 @@ export interface BaseReporterProps { } export interface SingleReporterProps extends BaseReporterProps{ - runMode: 'single' + runMode?: 'single' } -@observer -class Reporter extends Component { - static defaultProps: Partial = { - runMode: 'single', - appState, - events, - runnablesStore, - scroller, - statsStore, - } - - render () { - const { - appState, - className, - runnablesStore, - scroller, - error, - statsStore, - studioEnabled, - renderReporterHeader = (props: ReporterHeaderProps) =>
      , - runnerStore, - } = this.props - - return ( -
      - {renderReporterHeader({ appState, statsStore, runnablesStore })} - {appState?.isPreferencesMenuOpen ? ( - - ) : ( - runnerStore.spec && - )} -
      - ) - } - - // this hook will only trigger if we switch spec file at runtime - // it never happens in normal e2e but can happen in component-testing mode - componentDidUpdate (newProps: BaseReporterProps) { - if (!this.props.runnerStore.spec) { - throw Error(`Expected runnerStore.spec not to be null.`) - } - - this.props.runnablesStore.setRunningSpec(this.props.runnerStore.spec.relative) - if ( - this.props.resetStatsOnSpecChange && - this.props.runnerStore.specRunId !== newProps.runnerStore.specRunId - ) { - runInAction('reporter:stats:reset', () => { - this.props.statsStore.reset() +const Reporter: React.FC = observer(({ runner, className, error, runMode = 'single', studioEnabled, autoScrollingEnabled, isSpecsListOpen, resetStatsOnSpecChange, renderReporterHeader = (props: ReporterHeaderProps) =>
      , runnerStore }) => { + const previousSpecRunId = usePrevious(runnerStore.specRunId) + const mounted = useRef() + + useEffect(() => { + if (!mounted.current) { + // do componentDidMount logic + mounted.current = true + + if (!runnerStore.spec) { + throw Error(`Expected runnerStore.spec not to be null.`) + } + + action('set:scrolling', () => { + // set the initial enablement of auto scroll configured inside the user preferences when the app is loaded + appState.setAutoScrollingUserPref(autoScrollingEnabled) + })() + + action('set:specs:list', () => { + appState.setSpecsList(isSpecsListOpen ?? false) + })() + + events.init({ + appState, + runnablesStore, + scroller, + statsStore, }) - } - } - - componentDidMount () { - const { appState, runnablesStore, runner, scroller, statsStore, autoScrollingEnabled, isSpecsListOpen, runnerStore } = this.props - if (!runnerStore.spec) { - throw Error(`Expected runnerStore.spec not to be null.`) + events.listen(runner) + + shortcuts.start() + EQ.init() + runnablesStore.setRunningSpec(runnerStore.spec.relative) + } else { + // do componentDidUpdate logic + + if (!runnerStore.spec) { + throw Error(`Expected runnerStore.spec not to be null.`) + } + + runnablesStore.setRunningSpec(runnerStore.spec.relative) + if ( + resetStatsOnSpecChange && + runnerStore.specRunId !== previousSpecRunId + ) { + // @ts-expect-error + runInAction('reporter:stats:reset', () => { + statsStore.reset() + }) + } } - action('set:scrolling', () => { - // set the initial enablement of auto scroll configured inside the user preferences when the app is loaded - appState.setAutoScrollingUserPref(autoScrollingEnabled) - })() - - action('set:specs:list', () => { - appState.setSpecsList(isSpecsListOpen ?? false) - })() - - this.props.events.init({ - appState, - runnablesStore, - scroller, - statsStore, - }) - - this.props.events.listen(runner) - - shortcuts.start() - EQ.init() - this.props.runnablesStore.setRunningSpec(runnerStore.spec.relative) - } - - componentWillUnmount () { - shortcuts.stop() - } -} + return () => shortcuts.stop() + }, []) + + return ( +
      + {renderReporterHeader({ appState, statsStore, runnablesStore })} + {appState?.isPreferencesMenuOpen ? ( + + ) : ( + runnerStore.spec && + )} +
      + ) +}) declare global { interface Window { diff --git a/packages/reporter/src/runnables/runnable-and-suite.tsx b/packages/reporter/src/runnables/runnable-and-suite.tsx index e7743a5a2d36..89560c4664b3 100644 --- a/packages/reporter/src/runnables/runnable-and-suite.tsx +++ b/packages/reporter/src/runnables/runnable-and-suite.tsx @@ -1,7 +1,7 @@ import cs from 'classnames' import _ from 'lodash' import { observer } from 'mobx-react' -import React, { Component, MouseEvent } from 'react' +import React, { MouseEvent } from 'react' import { indent } from '../lib/util' @@ -76,30 +76,21 @@ export interface RunnableProps { // in order to mess with its internal state. converting it to a functional // component breaks that, so it needs to stay a Class-based component or // else the driver tests need to be refactored to support it being functional -@observer -class Runnable extends Component { - static defaultProps = { - appState, - } - - render () { - const { appState, model, studioEnabled, canSaveStudioLogs } = this.props - - return ( -
    • - {model.type === 'test' - ? - : } -
    • - ) - } -} +const Runnable: React.FC = observer(({ appState: appStateProps = appState, model, studioEnabled, canSaveStudioLogs }) => { + return ( +
    • + {model.type === 'test' + ? + : } +
    • + ) +}) export { Suite } diff --git a/packages/reporter/src/runnables/runnable-header.tsx b/packages/reporter/src/runnables/runnable-header.tsx index e28ee9512e0e..3a411cb5e6ad 100644 --- a/packages/reporter/src/runnables/runnable-header.tsx +++ b/packages/reporter/src/runnables/runnable-header.tsx @@ -1,5 +1,5 @@ import { observer } from 'mobx-react' -import React, { Component, ReactElement } from 'react' +import React, { ReactElement } from 'react' import type { StatsStore } from '../header/stats-store' import { formatDuration, getFilenameParts } from '../lib/util' @@ -12,53 +12,48 @@ interface RunnableHeaderProps { statsStore: StatsStore } -@observer -class RunnableHeader extends Component { - render () { - const { spec, statsStore } = this.props - - const relativeSpecPath = spec.relative - - if (spec.relative === '__all') { - if (spec.specFilter) { - return renderRunnableHeader( - Specs matching "{spec.specFilter}", - ) - } +const RunnableHeader: React.FC = observer(({ spec, statsStore }) => { + const relativeSpecPath = spec.relative + if (spec.relative === '__all') { + if (spec.specFilter) { return renderRunnableHeader( - All Specs, + Specs matching "{spec.specFilter}", ) } - const displayFileName = () => { - const specParts = getFilenameParts(spec.name) - - return ( - <> - {specParts[0]}{specParts[1]} - - ) - } + return renderRunnableHeader( + All Specs, + ) + } - const fileDetails = { - absoluteFile: spec.absolute, - column: 0, - displayFile: displayFileName(), - line: 0, - originalFile: relativeSpecPath, - relativeFile: relativeSpecPath, - } + const displayFileName = () => { + const specParts = getFilenameParts(spec.name) - return renderRunnableHeader( + return ( <> - - {Boolean(statsStore.duration) && ( - {formatDuration(statsStore.duration)} - )} - , + {specParts[0]}{specParts[1]} + ) } -} + + const fileDetails = { + absoluteFile: spec.absolute, + column: 0, + displayFile: displayFileName(), + line: 0, + originalFile: relativeSpecPath, + relativeFile: relativeSpecPath, + } + + return renderRunnableHeader( + <> + + {Boolean(statsStore.duration) && ( + {formatDuration(statsStore.duration)} + )} + , + ) +}) export default RunnableHeader diff --git a/packages/reporter/src/runnables/runnables.tsx b/packages/reporter/src/runnables/runnables.tsx index 3bde4ec38b12..cc562e317e6b 100644 --- a/packages/reporter/src/runnables/runnables.tsx +++ b/packages/reporter/src/runnables/runnables.tsx @@ -1,7 +1,7 @@ import _ from 'lodash' import { action } from 'mobx' import { observer } from 'mobx-react' -import React, { Component, MouseEvent } from 'react' +import React, { MouseEvent, useEffect, useRef } from 'react' import events, { Events } from '../lib/events' import { RunnablesError, RunnablesErrorModel } from './runnable-error' @@ -154,46 +154,46 @@ export interface RunnablesProps { canSaveStudioLogs: boolean } -@observer -class Runnables extends Component { - render () { - const { error, runnablesStore, spec, studioEnabled, canSaveStudioLogs } = this.props - - return ( -
      - - -
      - ) - } - - componentDidMount () { - const { scroller, appState } = this.props - - let maybeHandleScroll: UserScrollCallback | undefined = undefined - - if (window.__CYPRESS_MODE__ === 'open') { - // in open mode, listen for scroll events so that users can pause the command log auto-scroll - // by manually scrolling the command log - maybeHandleScroll = action('user:scroll:detected', () => { - if (appState && appState.isRunning) { - appState.temporarilySetAutoScrolling(false) - } - }) +const Runnables: React.FC = observer(({ appState, scroller, error, runnablesStore, spec, studioEnabled, canSaveStudioLogs }) => { + const mounted = useRef() + const containerRef = useRef(null) + + useEffect(() => { + if (!mounted.current) { + mounted.current = true + + let maybeHandleScroll: UserScrollCallback | undefined = undefined + + if (window.__CYPRESS_MODE__ === 'open') { + // in open mode, listen for scroll events so that users can pause the command log auto-scroll + // by manually scrolling the command log + maybeHandleScroll = action('user:scroll:detected', () => { + if (appState && appState.isRunning) { + appState.temporarilySetAutoScrolling(false) + } + }) + } + + // we need to always call scroller.setContainer, but the callback can be undefined + // so we pass maybeHandleScroll. If we don't, Cypress blows up with an error like + // `A container must be set on the scroller with scroller.setContainer(container)` + scroller.setContainer(containerRef.current as Element, maybeHandleScroll) } + }) - // we need to always call scroller.setContainer, but the callback can be undefined - // so we pass maybeHandleScroll. If we don't, Cypress blows up with an error like - // `A container must be set on the scroller with scroller.setContainer(container)` - scroller.setContainer(this.refs.container as Element, maybeHandleScroll) - } -} + return ( +
      + + +
      + ) +}) export { RunnablesList } diff --git a/packages/reporter/src/test/test.tsx b/packages/reporter/src/test/test.tsx index 665fc444b9cd..ed785ea20b08 100644 --- a/packages/reporter/src/test/test.tsx +++ b/packages/reporter/src/test/test.tsx @@ -1,5 +1,5 @@ import { observer } from 'mobx-react' -import React, { Component, createRef, RefObject, MouseEvent } from 'react' +import React, { MouseEvent, useEffect, useRef, useState } from 'react' // @ts-ignore import Tooltip from '@cypress/react-tooltip' import cs from 'classnames' @@ -8,7 +8,6 @@ import events, { Events } from '../lib/events' import appState, { AppState } from '../lib/app-state' import Collapsible from '../collapsible/collapsible' import { indent } from '../lib/util' -import runnablesStore, { RunnablesStore } from '../runnables/runnables-store' import TestModel from './test-model' import scroller, { Scroller } from '../lib/scroller' @@ -21,173 +20,127 @@ import ClipboardIcon from '@packages/frontend-shared/src/assets/icons/general-cl import WarningIcon from '@packages/frontend-shared/src/assets/icons/warning_x16.svg' interface StudioControlsProps { - events: Events - model: TestModel + events?: Events canSaveStudioLogs: boolean } -interface StudioControlsState { - copySuccess: boolean -} +const StudioControls: React.FC = observer(({ events: eventsProps = events, canSaveStudioLogs }) => { + const [copySuccess, setCopySuccess] = useState(false) -@observer -class StudioControls extends Component { - static defaultProps = { - events, - } - - state = { - copySuccess: false, - } - - _cancel = (e: MouseEvent) => { + const _cancel = (e: MouseEvent) => { e.preventDefault() - this.props.events.emit('studio:cancel') + eventsProps.emit('studio:cancel') } - _save = (e: MouseEvent) => { + const _save = (e: MouseEvent) => { e.preventDefault() - this.props.events.emit('studio:save') + eventsProps.emit('studio:save') } - _copy = (e: MouseEvent) => { + const _copy = (e: MouseEvent) => { e.preventDefault() - this.props.events.emit('studio:copy:to:clipboard', () => { - this.setState({ copySuccess: true }) + events.emit('studio:copy:to:clipboard', () => { + setCopySuccess(true) }) } - _endCopySuccess = () => { - if (this.state.copySuccess) { - this.setState({ copySuccess: false }) + const _endCopySuccess = () => { + if (copySuccess) { + setCopySuccess(false) } } - render () { - const { canSaveStudioLogs } = this.props - const { copySuccess } = this.state - - return ( -
      - Cancel - + Cancel + + - - -
      - ) - } -} + {copySuccess ? ( + + ) : ( + + )} + + + +
      + ) +}) interface TestProps { - events: Events - appState: AppState - runnablesStore: RunnablesStore - scroller: Scroller + events?: Events + appState?: AppState + scroller?: Scroller model: TestModel studioEnabled: boolean canSaveStudioLogs: boolean } -@observer -class Test extends Component { - static defaultProps = { - events, - appState, - runnablesStore, - scroller, - } +const Test: React.FC = observer(({ model, events: eventsProps = events, appState: appStateProps = appState, scroller: scrollerProps = scroller, studioEnabled, canSaveStudioLogs }) => { + const containerRef = useRef(null) - containerRef: RefObject + const mounted = useRef() - constructor (props: TestProps) { - super(props) - - this.containerRef = createRef() - } + useEffect(() => { + _scrollIntoView() + if (!mounted.current) { + mounted.current = true + } else { + model.callbackAfterUpdate() + } + }) - componentDidMount () { - this._scrollIntoView() - } + const _launchStudio = (e: MouseEvent) => { + e.preventDefault() + e.stopPropagation() - componentDidUpdate () { - this._scrollIntoView() - this.props.model.callbackAfterUpdate() + eventsProps.emit('studio:init:test', model.id) } - _scrollIntoView () { - const { appState, model, scroller } = this.props - const { state } = model - - if (appState.autoScrollingEnabled && (appState.isRunning || appState.studioActive) && state !== 'processing') { + const _scrollIntoView = () => { + if (appStateProps.autoScrollingEnabled && (appStateProps.isRunning || appStateProps.studioActive) && model.state !== 'processing') { window.requestAnimationFrame(() => { // since this executes async in a RAF the ref might be null - if (this.containerRef.current) { - scroller.scrollIntoView(this.containerRef.current as HTMLElement) + if (containerRef.current) { + scrollerProps.scrollIntoView(containerRef.current as HTMLElement) } }) } } - render () { - const { model } = this.props - - return ( - - {this._contents()} - - ) - } - - _header () { - const { appState, model } = this.props - + const _header = () => { return (<> - + {model.title} {model.state} - {this._controls()} + {_controls()} ) } - _controls () { + const _controls = () => { let controls: Array = [] - if (this.props.model.state === 'failed') { + if (model.state === 'failed') { controls.push( - + @@ -195,12 +148,12 @@ class Test extends Component { ) } - if (this.props.studioEnabled && !appState.studioActive) { + if (studioEnabled && !appStateProps.studioActive) { controls.push( , ) } @@ -216,25 +169,28 @@ class Test extends Component { ) } - _contents () { - const { appState, model } = this.props - + const _contents = () => { return (
      - this._scrollIntoView()} /> - {appState.studioActive && } + _scrollIntoView()} /> + {appStateProps.studioActive && }
      ) } - _launchStudio = (e: MouseEvent) => { - e.preventDefault() - e.stopPropagation() - - const { model, events } = this.props - - events.emit('studio:init:test', model.id) - } -} + return ( + + {_contents()} + + ) +}) export default Test From d17dcf581057cb0bf43aa4b3c945b6c53204c3aa Mon Sep 17 00:00:00 2001 From: AtofStryker Date: Mon, 17 Mar 2025 15:29:46 -0400 Subject: [PATCH 2/2] fix reporter tests and run in CI --- packages/reporter/cypress/e2e/agents.cy.ts | 2 +- packages/reporter/cypress/e2e/aliases.cy.ts | 2 +- packages/reporter/cypress/e2e/commands.cy.ts | 2 +- packages/reporter/cypress/e2e/header.cy.ts | 2 +- packages/reporter/cypress/e2e/hooks.cy.ts | 2 +- packages/reporter/cypress/e2e/memory.cy.ts | 2 +- packages/reporter/cypress/e2e/routes.cy.ts | 2 +- packages/reporter/cypress/e2e/runnables.cy.ts | 2 +- packages/reporter/cypress/e2e/shortcuts.cy.ts | 2 +- packages/reporter/cypress/e2e/spec_title.cy.ts | 2 +- packages/reporter/cypress/e2e/suites.cy.ts | 2 +- packages/reporter/cypress/e2e/test_errors.cy.ts | 2 +- packages/reporter/cypress/e2e/tests.cy.ts | 2 +- packages/reporter/src/main.tsx | 11 +++++++---- 14 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/reporter/cypress/e2e/agents.cy.ts b/packages/reporter/cypress/e2e/agents.cy.ts index 5a11678763b5..afc1e6ce0e38 100644 --- a/packages/reporter/cypress/e2e/agents.cy.ts +++ b/packages/reporter/cypress/e2e/agents.cy.ts @@ -27,7 +27,7 @@ describe('agents', () => { }) start = () => { - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/aliases.cy.ts b/packages/reporter/cypress/e2e/aliases.cy.ts index 370b53421335..c6b61a2f1a09 100644 --- a/packages/reporter/cypress/e2e/aliases.cy.ts +++ b/packages/reporter/cypress/e2e/aliases.cy.ts @@ -26,7 +26,7 @@ describe('aliases', () => { }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/commands.cy.ts b/packages/reporter/cypress/e2e/commands.cy.ts index 3b793d02e54e..ce0267afb0ad 100644 --- a/packages/reporter/cypress/e2e/commands.cy.ts +++ b/packages/reporter/cypress/e2e/commands.cy.ts @@ -27,7 +27,7 @@ describe('commands', { viewportHeight: 1000 }, () => { }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', {}) addCommand(runner, { diff --git a/packages/reporter/cypress/e2e/header.cy.ts b/packages/reporter/cypress/e2e/header.cy.ts index 6f057d33ca52..4e4c8cc1b2d5 100755 --- a/packages/reporter/cypress/e2e/header.cy.ts +++ b/packages/reporter/cypress/e2e/header.cy.ts @@ -32,7 +32,7 @@ describe('header', () => { }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', { ...runnables, testFilter: opts?.testFilter, totalUnfilteredTests: opts?.totalUnfilteredTests }) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/hooks.cy.ts b/packages/reporter/cypress/e2e/hooks.cy.ts index 8e5e95b50cf8..62e37ff4c126 100644 --- a/packages/reporter/cypress/e2e/hooks.cy.ts +++ b/packages/reporter/cypress/e2e/hooks.cy.ts @@ -26,7 +26,7 @@ describe('hooks', () => { }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/memory.cy.ts b/packages/reporter/cypress/e2e/memory.cy.ts index 0e11fc21e462..a14903235f47 100644 --- a/packages/reporter/cypress/e2e/memory.cy.ts +++ b/packages/reporter/cypress/e2e/memory.cy.ts @@ -29,7 +29,7 @@ function visitAndRenderReporter (studioEnabled: boolean = false, studioActive: b }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', { studioActive }) }) diff --git a/packages/reporter/cypress/e2e/routes.cy.ts b/packages/reporter/cypress/e2e/routes.cy.ts index 5fbd7796df22..587adb018718 100644 --- a/packages/reporter/cypress/e2e/routes.cy.ts +++ b/packages/reporter/cypress/e2e/routes.cy.ts @@ -27,7 +27,7 @@ describe('routes', () => { }) start = () => { - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/runnables.cy.ts b/packages/reporter/cypress/e2e/runnables.cy.ts index 93fd70651b42..eb108faa239d 100644 --- a/packages/reporter/cypress/e2e/runnables.cy.ts +++ b/packages/reporter/cypress/e2e/runnables.cy.ts @@ -45,7 +45,7 @@ describe('runnables', () => { start = (renderProps?: Partial) => { render(renderProps) - return cy.get('.reporter').then(() => { + return cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/shortcuts.cy.ts b/packages/reporter/cypress/e2e/shortcuts.cy.ts index 0c1d152dcaec..4ada861cb4ff 100755 --- a/packages/reporter/cypress/e2e/shortcuts.cy.ts +++ b/packages/reporter/cypress/e2e/shortcuts.cy.ts @@ -36,7 +36,7 @@ describe('shortcuts', function () { }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', this.runnables) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/spec_title.cy.ts b/packages/reporter/cypress/e2e/spec_title.cy.ts index 07520189a102..51f89915d69c 100644 --- a/packages/reporter/cypress/e2e/spec_title.cy.ts +++ b/packages/reporter/cypress/e2e/spec_title.cy.ts @@ -13,7 +13,7 @@ describe('spec title', () => { win.render({ runner, runnerStore: { spec } }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', {}) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/suites.cy.ts b/packages/reporter/cypress/e2e/suites.cy.ts index fc3ce4972452..ba91185ab3e7 100644 --- a/packages/reporter/cypress/e2e/suites.cy.ts +++ b/packages/reporter/cypress/e2e/suites.cy.ts @@ -29,7 +29,7 @@ describe('suites', () => { }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/test_errors.cy.ts b/packages/reporter/cypress/e2e/test_errors.cy.ts index f9a6ad35b3df..a9f797e01e39 100644 --- a/packages/reporter/cypress/e2e/test_errors.cy.ts +++ b/packages/reporter/cypress/e2e/test_errors.cy.ts @@ -20,7 +20,7 @@ describe('test errors', () => { // @ts-ignore runnablesWithErr.suites[0].tests[0].err = err - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnablesWithErr) runner.emit('reporter:start', {}) }) diff --git a/packages/reporter/cypress/e2e/tests.cy.ts b/packages/reporter/cypress/e2e/tests.cy.ts index bb5081ccbaf4..b1ba923e33fa 100644 --- a/packages/reporter/cypress/e2e/tests.cy.ts +++ b/packages/reporter/cypress/e2e/tests.cy.ts @@ -28,7 +28,7 @@ function visitAndRenderReporter (studioEnabled: boolean = false, studioActive: b }) }) - cy.get('.reporter').then(() => { + cy.get('.reporter.mounted').then(() => { runner.emit('runnables:ready', runnables) runner.emit('reporter:start', { studioActive }) }) diff --git a/packages/reporter/src/main.tsx b/packages/reporter/src/main.tsx index a70fa5f3070a..f9207f6c7672 100644 --- a/packages/reporter/src/main.tsx +++ b/packages/reporter/src/main.tsx @@ -2,7 +2,7 @@ import { action, runInAction } from 'mobx' import { observer } from 'mobx-react' import cs from 'classnames' -import React, { useEffect, useRef } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' // @ts-ignore import EQ from 'css-element-queries/src/ElementQueries' @@ -53,12 +53,11 @@ export interface SingleReporterProps extends BaseReporterProps{ const Reporter: React.FC = observer(({ runner, className, error, runMode = 'single', studioEnabled, autoScrollingEnabled, isSpecsListOpen, resetStatsOnSpecChange, renderReporterHeader = (props: ReporterHeaderProps) =>
      , runnerStore }) => { const previousSpecRunId = usePrevious(runnerStore.specRunId) - const mounted = useRef() + const [isMounted, setIsMounted] = useState(false) useEffect(() => { - if (!mounted.current) { + if (!isMounted) { // do componentDidMount logic - mounted.current = true if (!runnerStore.spec) { throw Error(`Expected runnerStore.spec not to be null.`) @@ -85,8 +84,11 @@ const Reporter: React.FC = observer(({ runner, className, e shortcuts.start() EQ.init() runnablesStore.setRunningSpec(runnerStore.spec.relative) + // we need to know when the test is mounted for our reporter tests. see + setIsMounted(true) } else { // do componentDidUpdate logic + // TODO: is this in the right place? if (!runnerStore.spec) { throw Error(`Expected runnerStore.spec not to be null.`) @@ -110,6 +112,7 @@ const Reporter: React.FC = observer(({ runner, className, e return (
      {renderReporterHeader({ appState, statsStore, runnablesStore })} {appState?.isPreferencesMenuOpen ? (