feat(component-testing): changes to the driver and reporter preparing for runner-ct (#14434)

* chore: update driver with component testing

* feat: bring ct changes in reporter

* test: update script utils test

* fix: type issue

* test: add test for new ct behavior in driver

runScript can now use promises instead of files.
Thi test this new behavior

* tests(ct): tests for the reporter runable store

* fix: remove changes on event handling in driver

* build: augment zip size to avoid zip errors

* test: add renderin tests for reporter multispec

* test: better matcher for runnableHistory

Co-authored-by: Jessica Sachs <jess@jessicasachs.io>

* test: make the runscripts eval tests clearer

* refactor(reporter): main interface instead of type

* fix(reporter): runInAction when specRunId changes

* refactor(driver): remove restartRunner function

Co-authored-by: Jessica Sachs <jess@jessicasachs.io>
This commit is contained in:
Barthélémy Ledoux
2021-01-07 18:30:52 -06:00
committed by GitHub
parent 74f6db0fb4
commit dd559d9862
11 changed files with 188 additions and 31 deletions

View File

@@ -1,3 +1,4 @@
const Promise = require('bluebird')
const $scriptUtils = require('@packages/driver/src/cypress/script_utils')
const $networkUtils = require('@packages/driver/src/cypress/network_utils')
const $sourceMapUtils = require('@packages/driver/src/cypress/source_map_utils')
@@ -42,10 +43,19 @@ describe('src/cypress/script_utils', () => {
it('evals each script', () => {
return $scriptUtils.runScripts(scriptWindow, scripts)
.then(() => {
expect($sourceMapUtils.extractSourceMap).to.be.calledTwice
expect($sourceMapUtils.extractSourceMap).to.be.calledWith(scripts[0], 'the script contents')
expect($sourceMapUtils.extractSourceMap).to.be.calledWith(scripts[1], 'the script contents')
expect(scriptWindow.eval).to.be.calledTwice
expect(scriptWindow.eval).to.be.calledWith('the script contents\n//# sourceURL=http://localhost:3500cypress/integration/script1.js')
expect(scriptWindow.eval).to.be.calledWith('the script contents\n//# sourceURL=http://localhost:3500cypress/integration/script2.js')
})
})
})
context('#runPromises', () => {
it('handles promises and doesnt try to fetch + eval manually', async () => {
const scriptsAsPromises = [Promise.resolve(), Promise.resolve()]
const result = await $scriptUtils.runScripts({}, scriptsAsPromises)
expect(result).to.have.length(scriptsAsPromises.length)
})
})
})

View File

@@ -352,7 +352,7 @@ module.exports = (Commands, Cypress, cy, state, config) => {
Cypress.on('test:before:run:async', () => {
// reset any state on the backend
Cypress.backend('reset:server:state')
return Cypress.backend('reset:server:state')
})
Cypress.on('test:before:run', reset)

View File

@@ -1,5 +1,5 @@
const _ = require('lodash')
const Promise = require('bluebird')
const Bluebird = require('bluebird')
const $networkUtils = require('./network_utils')
const $sourceMapUtils = require('./source_map_utils')
@@ -28,13 +28,24 @@ const evalScripts = (specWindow, scripts = []) => {
return null
}
const runScripts = (specWindow, scripts) => {
return Promise
const runScriptsFromUrls = (specWindow, scripts) => {
return Bluebird
.map(scripts, (script) => fetchScript(specWindow, script))
.map(extractSourceMap)
.then((scripts) => evalScripts(specWindow, scripts))
}
// Supports either scripts as objects or as async import functions
const runScripts = (specWindow, scripts) => {
// if scripts contains at least one promise
if (scripts.length && typeof scripts[0].then === 'function') {
// merge the awaiting of the promises
return Bluebird.all(scripts)
}
return runScriptsFromUrls(specWindow, scripts)
}
module.exports = {
runScripts,
}

View File

@@ -58,6 +58,21 @@ describe('runnables', () => {
cy.percySnapshot()
})
it('displays multi-spec reporters', () => {
start({ runMode: 'multi', allSpecs: [
{
relative: 'fizz',
},
{
relative: 'buzz',
},
] })
// ensure the page is loaded before taking snapshot
cy.contains('buzz').should('be.visible')
cy.percySnapshot()
})
it('displays the "No test" error when there are no tests', () => {
runnables.suites = []
start()

View File

@@ -234,6 +234,22 @@ describe('runnables store', () => {
})
})
context('#setRunningSpec', () => {
it('sets the current runnable as the path passed', () => {
instance.setRunnables({ tests: [createTest('1')] })
instance.setRunningSpec('specPath')
expect(instance.runningSpec).to.equal('specPath')
})
it('add the previous path to the spec history', () => {
instance.setRunnables({ tests: [createTest('1')] })
instance.setRunningSpec('previousSpecPath')
instance.setRunningSpec('nextSpecPath')
expect(instance.runningSpec).to.equal('nextSpecPath')
expect(instance.runnablesHistory['previousSpecPath']).not.to.be.undefined
})
})
context('#reset', () => {
it('resets flags to default values', () => {
instance.setRunnables({ tests: [createTest('1')] })
@@ -259,5 +275,12 @@ describe('runnables store', () => {
instance.reset()
expect(instance.testById('1')).to.be.undefined
})
it('resets runnablesHistory', () => {
instance.setRunnables({ tests: [createTest('1')] })
instance.setRunningSpec('previous')
instance.reset()
expect(instance.runnablesHistory).to.be.empty
})
})
})

View File

@@ -10,13 +10,13 @@ import Controls from './controls'
import Stats from './stats'
import { StatsStore } from './stats-store'
interface Props {
export interface ReporterHeaderProps {
appState: AppState
events?: Events
statsStore: StatsStore
}
const Header = observer(({ appState, events = defaultEvents, statsStore }: Props) => (
const Header = observer(({ appState, events = defaultEvents, statsStore }: ReporterHeaderProps) => (
<header>
<Tooltip placement='bottom' title={<p>View All Tests <span className='kbd'>F</span></p>} wrapperClassName='focus-tests' className='cy-tooltip'>
<button onClick={() => events.emit('focus:tests')}>

View File

@@ -1,5 +1,7 @@
import { action } from 'mobx'
/* global Cypress, JSX */
import { action, runInAction } from 'mobx'
import { observer } from 'mobx-react'
import cs from 'classnames'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { render } from 'react-dom'
@@ -15,10 +17,10 @@ import scroller, { Scroller } from './lib/scroller'
import statsStore, { StatsStore } from './header/stats-store'
import shortcuts from './lib/shortcuts'
import Header from './header/header'
import Header, { ReporterHeaderProps } from './header/header'
import Runnables from './runnables/runnables'
export interface ReporterProps {
interface BaseReporterProps {
appState: AppState
autoScrollingEnabled?: boolean
runnablesStore: RunnablesStore
@@ -27,11 +29,24 @@ export interface ReporterProps {
statsStore: StatsStore
events: Events
error?: RunnablesErrorModel
resetStatsOnSpecChange?: boolean
renderReporterHeader?: (props: ReporterHeaderProps) => JSX.Element;
spec: Cypress.Cypress['spec']
/** Used for component testing front-end */
specRunId?: string | null
}
export interface SingleReporterProps extends BaseReporterProps{
runMode: 'single',
}
export interface MultiReporterProps extends BaseReporterProps{
runMode: 'multi',
allSpecs: Array<Cypress.Cypress['spec']>
}
@observer
class Reporter extends Component<ReporterProps> {
class Reporter extends Component<SingleReporterProps | MultiReporterProps> {
static propTypes = {
autoScrollingEnabled: PropTypes.bool,
error: PropTypes.shape({
@@ -52,6 +67,7 @@ class Reporter extends Component<ReporterProps> {
}
static defaultProps = {
runMode: 'single',
appState,
events,
runnablesStore,
@@ -60,27 +76,63 @@ class Reporter extends Component<ReporterProps> {
}
render () {
const { appState } = this.props
const {
appState,
runMode,
runnablesStore,
scroller,
error,
events,
statsStore,
renderReporterHeader = (props: ReporterHeaderProps) => <Header {...props}/>,
} = this.props
return (
<div className='reporter'>
<Header appState={appState} statsStore={this.props.statsStore} />
<Runnables
appState={appState}
error={this.props.error}
runnablesStore={this.props.runnablesStore}
scroller={this.props.scroller}
spec={this.props.spec}
/>
<div className={cs('reporter', { multiSpecs: runMode === 'multi' })}>
{renderReporterHeader({ appState, statsStore })}
{this.props.runMode === 'single' ? (
<Runnables
appState={appState}
error={error}
runnablesStore={runnablesStore}
scroller={scroller}
spec={this.props.spec}
/>
) : this.props.allSpecs.map((spec) => (
<Runnables
key={spec.relative}
appState={appState}
error={error}
runnablesStore={runnablesStore}
scroller={scroller}
spec={spec}
/>
))}
<ForcedGcWarning
appState={appState}
events={this.props.events}/>
events={events}/>
</div>
)
}
// 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) {
this.props.runnablesStore.setRunningSpec(this.props.spec.relative)
if (
this.props.resetStatsOnSpecChange &&
this.props.specRunId !== newProps.specRunId
) {
runInAction('reporter:stats:reset', () => {
this.props.statsStore.reset()
})
}
}
componentDidMount () {
const { appState, autoScrollingEnabled, runnablesStore, runner, scroller, statsStore } = this.props
const { spec, appState, autoScrollingEnabled, runnablesStore, runner, scroller, statsStore } = this.props
action('set:scrolling', () => {
appState.setAutoScrolling(autoScrollingEnabled)
@@ -97,6 +149,7 @@ class Reporter extends Component<ReporterProps> {
shortcuts.start()
EQ.init()
this.props.runnablesStore.setRunningSpec(spec.relative)
}
componentWillUnmount () {
@@ -108,7 +161,7 @@ declare global {
interface Window {
Cypress: any
state: AppState
render: ((props: Partial<ReporterProps>) => void)
render: ((props: Partial<BaseReporterProps>) => void)
}
}

View File

@@ -43,6 +43,17 @@ type TestOrSuite<T> = T extends TestProps ? TestProps : SuiteProps
class RunnablesStore {
@observable isReady = defaults.isReady
@observable runnables: RunnableArray = []
/**
* Stores a list of all the runables files where the reporter
* has passed without any specific order.
*
* key: spec FilePath
* content: RunableArray
*/
@observable runnablesHistory: Record<string, RunnableArray> = {}
runningSpec: string | null = null
hasTests: boolean = false
hasSingleTest: boolean = false
@@ -195,9 +206,23 @@ class RunnablesStore {
})
this.runnables = []
this.runnablesHistory = {}
this._tests = {}
this._runnablesQueue = []
}
@action
setRunningSpec (specPath: string) {
const previousSpec = this.runningSpec
this.runningSpec = specPath
if (!previousSpec || previousSpec === specPath) {
return
}
this.runnablesHistory[previousSpec] = this.runnables
}
}
export { RunnablesStore }

View File

@@ -345,3 +345,17 @@
}
}
}
.reporter.multiSpecs {
overflow-y: auto;
.container {
flex-grow: 0;
overflow-y: unset;
.wrap {
margin-bottom: 0;
}
}
}

View File

@@ -42,14 +42,14 @@ const RunnablesList = observer(({ runnables }: RunnablesListProps) => (
</div>
))
interface RunnablesContentProps {
export interface RunnablesContentProps {
runnablesStore: RunnablesStore
specPath: string
error?: RunnablesErrorModel
}
const RunnablesContent = observer(({ runnablesStore, specPath, error }: RunnablesContentProps) => {
const { isReady, runnables } = runnablesStore
const { isReady, runnables, runnablesHistory } = runnablesStore
if (!isReady) {
return <Loading />
@@ -61,10 +61,16 @@ const RunnablesContent = observer(({ runnablesStore, specPath, error }: Runnable
error = noTestsError(specPath)
}
return error ? <RunnablesError error={error} /> : <RunnablesList runnables={runnables} />
if (error) {
return <RunnablesError error={error} />
}
const isRunning = specPath === runnablesStore.runningSpec
return <RunnablesList runnables={isRunning ? runnables : runnablesHistory[specPath]} />
})
interface RunnablesProps {
export interface RunnablesProps {
error?: RunnablesErrorModel
runnablesStore: RunnablesStore
spec: Cypress.Cypress['spec']

View File

@@ -73,7 +73,7 @@ const checkZipSize = function (zipPath) {
const zipSize = filesize(stats.size, { round: 0 })
console.log(`zip file size ${zipSize}`)
const MAX_ALLOWED_SIZE_MB = os.platform() === 'win32' ? 245 : 200
const MAX_ALLOWED_SIZE_MB = os.platform() === 'win32' ? 265 : 218
const MAX_ZIP_FILE_SIZE = megaBytes(MAX_ALLOWED_SIZE_MB)
if (stats.size > MAX_ZIP_FILE_SIZE) {