refactor(server): convert reporter to TypeScript with type enhancements (#33879)

* refactor(server): convert reporter to TypeScript

Basic TypeScript conversion of the server reporter module and its unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(server): enhance types for reporter TypeScript conversion

Adds more precise type annotations on top of the initial TS conversion,
particularly around mocha runnable object shapes where runtime checks
default to undefined — improving debuggability if bugs surface in the
reporter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill Glesias
2026-05-22 14:53:25 -04:00
committed by GitHub
parent 11ff6e6d5a
commit 538bfcfd21
10 changed files with 376 additions and 150 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ import assert from 'assert'
import recordMode from './record'
import * as errors from '../errors'
import Reporter from '../reporter'
import { Reporter } from '../reporter'
import browserUtils from '../browsers'
import { openProject } from '../open_project'
import * as videoCapture from '../video_capture'
+1 -1
View File
@@ -10,7 +10,7 @@ import * as config from './config'
import * as errors from './errors'
import preprocessor from './plugins/preprocessor'
import runEvents from './plugins/run_events'
import Reporter from './reporter'
import { Reporter } from './reporter'
import * as savedState from './saved_state'
import { SocketCt } from './socket-ct'
import { SocketE2E } from './socket-e2e'
@@ -1,32 +1,55 @@
const _ = require('lodash')
const path = require('path')
const { stackUtils } = require('@packages/errors')
import _ from 'lodash'
import path from 'path'
import { stackUtils } from '@packages/errors'
// mocha-* is used to allow us to have later versions of mocha specified in devDependencies
// and prevents accidentally upgrading this one
// TODO: look into upgrading this to version in driver
const Mocha = require('mocha-7.2.0')
const mochaReporters = require('mocha-7.2.0/lib/reporters')
const mochaCreateStatsCollector = require('mocha-7.2.0/lib/stats-collector')
import Mocha from 'mocha-7.2.0'
import mochaReporters from 'mocha-7.2.0/lib/reporters'
import mochaCreateStatsCollector from 'mocha-7.2.0/lib/stats-collector'
import Debug from 'debug'
import { overrideRequire } from './override_require'
import type {
CypressTestStatusInfo,
InternalRunnable,
ReporterEventHandlers,
ReporterEventName,
ReporterHook,
ReporterResults,
ReporterStats,
ReporterTest,
ReporterTestAttempt,
ReporterTestError,
RetriesConfig,
RunnableAttemptPayload,
RunnablePayload,
RunnableState,
} from './types/reporter'
const debug = Debug('cypress:server:reporter')
const mochaColor = mochaReporters.Base.color
const mochaSymbols = mochaReporters.Base.symbols
const debug = require('debug')('cypress:server:reporter')
const { overrideRequire } = require('./override_require')
// override calls to `require('mocha*')` when to always resolve with a mocha we control
// otherwise mocha will be resolved from project's node_modules and might not work with our code
const customReporterMochaPath = path.dirname(require.resolve('mocha-7.2.0'))
const buildAttemptMessage = (currentRetry, totalRetries) => {
interface IconStatus {
overallStatusSymbol: string | undefined
overallStatusSymbolColor: string | undefined
overallStatusColor: string
}
const buildAttemptMessage = (currentRetry: number, totalRetries: number): string => {
return `(Attempt ${currentRetry} of ${totalRetries})`
}
// Used for experimentalRetries, where we want to display the passed/failed
// status of an attempt in the console
const getIconStatus = (status) => {
let overallStatusSymbol
let overallStatusSymbolColor
let overallStatusColor
const getIconStatus = (status: RunnableState | undefined): IconStatus => {
let overallStatusSymbol: string | undefined
let overallStatusSymbolColor: string | undefined
let overallStatusColor: string
switch (status) {
case 'passed':
@@ -75,7 +98,7 @@ overrideRequire((depPath, _load) => {
// Mocha.Runnable.prototype.titlePath = ->
// @parent.titlePath().concat([@title])
const getTitlePath = function (runnable, titles = []) {
const getTitlePath = function (runnable: InternalRunnable, titles: string[] = []): string[] {
// `originalTitle` is a Mocha Hook concept used to associated the
// hook to the test that executed it
if (runnable.originalTitle) {
@@ -97,7 +120,7 @@ const getTitlePath = function (runnable, titles = []) {
return titles
}
const createSuite = function (obj, parent) {
const createSuite = function (obj: RunnablePayload, parent?: InternalRunnable) {
const suite = new Mocha.Suite(obj.title, {})
if (parent) {
@@ -107,10 +130,10 @@ const createSuite = function (obj, parent) {
suite.file = obj.file
suite.root = !!obj.root
return suite
return suite as unknown as InternalRunnable
}
const createRunnable = function (obj, parent) {
const createRunnable = function (obj: RunnablePayload, parent?: InternalRunnable) {
let fn
const { body } = obj
@@ -137,10 +160,10 @@ const createRunnable = function (obj, parent) {
}
if (parent) {
runnable.parent = parent
runnable.parent = parent as unknown as Mocha.Suite
}
return runnable
return runnable as unknown as InternalRunnable
}
const mochaProps = {
@@ -148,17 +171,17 @@ const mochaProps = {
'retries': '_retries',
}
const toMochaProps = (testProps) => {
const toMochaProps = (testProps: Record<string, unknown>) => {
return _.each(mochaProps, (val, key) => {
if (testProps.hasOwnProperty(key)) {
testProps[val] = testProps[key]
return delete testProps[key]
delete testProps[key]
}
})
}
const toAttemptProps = (runnable) => {
const toAttemptProps = (runnable: InternalRunnable): RunnableAttemptPayload => {
return _.pick(runnable, [
'err',
'state',
@@ -169,14 +192,14 @@ const toAttemptProps = (runnable) => {
])
}
const mergeRunnable = (eventName) => {
return (function (testProps, runnables) {
toMochaProps(testProps)
const mergeRunnable = (eventName: ReporterEventName) => {
return (function (testProps: RunnablePayload, runnables: Record<string, InternalRunnable>) {
toMochaProps(testProps as Record<string, unknown>)
const runnable = runnables[testProps.id]
const runnable = runnables[testProps.id!]
if (eventName === 'test:before:run') {
if (testProps._currentRetry > runnable._currentRetry) {
if ((testProps._currentRetry ?? 0) > (runnable._currentRetry ?? 0)) {
debug('test retried:', testProps.title)
const prevAttempts = runnable.prevAttempts || []
@@ -190,14 +213,14 @@ const mergeRunnable = (eventName) => {
}
}
return [_.extend(runnable, testProps)]
return [_.extend(runnable, testProps) as InternalRunnable]
})
}
const safelyMergeRunnable = function (hookProps, runnables) {
const safelyMergeRunnable = function (hookProps: RunnablePayload, runnables: Record<string, InternalRunnable>) {
const { hookId, title, hookName, body, type } = hookProps
if (!runnables[hookId]) {
if (hookId && !runnables[hookId]) {
runnables[hookId] = {
hookId,
type,
@@ -207,13 +230,13 @@ const safelyMergeRunnable = function (hookProps, runnables) {
}
}
return [_.extend({}, runnables[hookProps.id], hookProps)]
return [_.extend({}, runnables[hookProps.id!], hookProps) as InternalRunnable]
}
const mergeErr = function (runnable, runnables, stats) {
const mergeErr = function (runnable: RunnablePayload, runnables: Record<string, InternalRunnable>, _stats: ReporterStats) {
// this will always be a test because
// we reset hook id's to match tests
let test = runnables[runnable.id]
let test = runnables[runnable.id!]
test.err = runnable.err
test.state = 'failed'
@@ -230,19 +253,17 @@ const mergeErr = function (runnable, runnables, stats) {
// in the terminal, as well as updating the test state as it may have changed in the client-side runner.
test = _.extend({}, test, { title: runnable.title, _cypressTestStatusInfo: runnable._cypressTestStatusInfo, state: runnable.state })
return [test, test.err]
return [test, test.err] as [InternalRunnable, ReporterTestError | string | undefined]
}
const setDate = function (obj, runnables, stats) {
let e; let s
s = obj.start
const setDate = function (obj: RunnablePayload, _runnables: Record<string, InternalRunnable>, stats: ReporterStats) {
const s = obj.start
if (s) {
stats.wallClockStartedAt = new Date(s)
}
e = obj.end
const e = obj.end
if (e) {
stats.wallClockEndedAt = new Date(e)
@@ -251,13 +272,13 @@ const setDate = function (obj, runnables, stats) {
return []
}
const orNull = function (prop) {
const orNull = function <T>(prop: T | null | undefined): T | null {
if (prop == null) return null
return prop
}
const events = {
const events: ReporterEventHandlers = {
'start': setDate,
'end': setDate,
'suite': mergeRunnable('suite'),
@@ -274,8 +295,18 @@ const events = {
'test:before:run': mergeRunnable('test:before:run'), // our own custom event
}
class Reporter {
constructor (reporterName = 'spec', reporterOptions = {}, projectRoot) {
export class Reporter {
reporterName!: string
projectRoot!: string | undefined
reporterOptions!: Record<string, unknown>
stats!: ReporterStats
retriesConfig!: RetriesConfig
runnables!: Record<string, InternalRunnable>
mocha!: Mocha
runner!: Mocha.Runner
reporter!: Mocha.reporters.Base & { done?: (failures: number, resolve: () => void) => void }
constructor (reporterName = 'spec', reporterOptions: Record<string, unknown> = {}, projectRoot?: string) {
if (!(this instanceof Reporter)) {
return new Reporter(reporterName)
}
@@ -283,24 +314,31 @@ class Reporter {
this.reporterName = reporterName
this.projectRoot = projectRoot
this.reporterOptions = reporterOptions
this.retriesConfig = {}
this.normalizeTest = this.normalizeTest.bind(this)
}
setRunnables (rootRunnable, cypressConfig) {
if (!rootRunnable) {
setRunnables (rootRunnable: RunnablePayload | unknown | undefined, cypressConfig?: unknown) {
if (Array.isArray(rootRunnable)) {
rootRunnable = rootRunnable[0] as RunnablePayload | undefined
}
if (!rootRunnable || typeof rootRunnable !== 'object') {
rootRunnable = { title: '' }
}
const config = cypressConfig as { retries?: RetriesConfig } | undefined
// manage stats ourselves
this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, skipped: 0, failures: 0 }
this.retriesConfig = cypressConfig ? cypressConfig.retries : {}
this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, skipped: 0, failures: 0, wallClockDuration: 0 }
this.retriesConfig = config?.retries ?? {}
this.runnables = {}
rootRunnable = this._createRunnable(rootRunnable, 'suite')
rootRunnable = this._createRunnable(rootRunnable as RunnablePayload, 'suite')
const reporter = Reporter.loadReporter(this.reporterName, this.projectRoot)
this.mocha = new Mocha({ reporter })
this.mocha.suite = rootRunnable
this.runner = new Mocha.Runner(rootRunnable)
this.mocha.suite = rootRunnable as unknown as Mocha.Suite
this.runner = new Mocha.Runner(rootRunnable as unknown as Mocha.Suite)
mochaCreateStatsCollector(this.runner)
this.reporter = new this.mocha._reporter(this.runner, {
@@ -323,9 +361,9 @@ class Reporter {
// Override the default reporter to always show test timing even for fast tests
// and display slow ones in yellow rather than red
this.runner._events.pass[2] = function (test) {
this.runner._events.pass[2] = function (test: InternalRunnable) {
// can possibly be undefined if the test fails before being run, such as in before/beforeEach hooks
const cypressTestMetaData = test._cypressTestStatusInfo
const cypressTestMetaData: CypressTestStatusInfo | undefined = test._cypressTestStatusInfo
const durationColor = test.speed === 'slow' ? 'medium' : 'fast'
@@ -345,19 +383,20 @@ class Reporter {
} else {
// If there have been no retries and experimental retries is configured,
// DON'T decorate the last test in the console as an attempt.
if (cypressTestMetaData?.attempts > 1) {
if ((cypressTestMetaData?.attempts ?? 0) > 1) {
const lastTestStatus = getIconStatus(test.state)
const attempts = cypressTestMetaData!.attempts!
fmt =
Array(indents).join(' ') +
mochaColor(lastTestStatus.overallStatusColor, ` ${ lastTestStatus.overallStatusSymbol}${buildAttemptMessage(cypressTestMetaData.attempts, test.retries + 1)}`)
mochaColor(lastTestStatus.overallStatusColor, ` ${ lastTestStatus.overallStatusSymbol}${buildAttemptMessage(attempts, (test.retries ?? 0) + 1)}`)
// or Log: `✓(Attempt 3 of 3) test title` when the overall outerStatus of a test has passed
// eslint-disable-next-line no-console
console.log(fmt, test.title)
}
const finalTestStatus = getIconStatus(cypressTestMetaData.outerStatus || test.state)
const finalTestStatus = getIconStatus(cypressTestMetaData?.outerStatus || test.state)
const finalMessaging =
Array(indents).join(' ') +
@@ -375,9 +414,9 @@ class Reporter {
const originalFailPrint = this.runner._events.fail[2]
this.runner._events.fail[2] = function (test) {
this.runner._events.fail[2] = function (test: InternalRunnable) {
// can possibly be undefined if the test fails before being run, such as in before/beforeEach hooks
const cypressTestMetaData = test._cypressTestStatusInfo
const cypressTestMetaData: CypressTestStatusInfo | undefined = test._cypressTestStatusInfo
// print the default if the experiment is not configured
if (!retriesConfig?.experimentalStrategy) {
@@ -388,12 +427,13 @@ class Reporter {
// If there have been no retries and experimental retries is configured,
// DON'T decorate the last test in the console as an attempt.
if (cypressTestMetaData?.attempts > 1) {
if ((cypressTestMetaData?.attempts ?? 0) > 1) {
const lastTestStatus = getIconStatus(test.state)
const attempts = cypressTestMetaData!.attempts!
const fmt =
Array(indents).join(' ') +
mochaColor(lastTestStatus.overallStatusColor, ` ${ lastTestStatus.overallStatusSymbol}${buildAttemptMessage(cypressTestMetaData.attempts, test.retries + 1)}`)
mochaColor(lastTestStatus.overallStatusColor, ` ${ lastTestStatus.overallStatusSymbol}${buildAttemptMessage(attempts, (test.retries ?? 0) + 1)}`)
// Log: `✖(Attempt 3 of 3) test title (300ms)` when a test fails and none of the retries have passed
// eslint-disable-next-line no-console
@@ -417,7 +457,7 @@ class Reporter {
}
}
_createRunnable (runnableProps, type, parent) {
_createRunnable (runnableProps: RunnablePayload, type: 'suite' | 'test', parent?: InternalRunnable): InternalRunnable {
const runnable = (() => {
switch (type) {
case 'suite':
@@ -442,54 +482,56 @@ class Reporter {
runnable.id = runnableProps.id
this.runnables[runnableProps.id] = runnable
this.runnables[runnableProps.id!] = runnable
return runnable
}
emit (event, arg) {
emit (event: ReporterEventName | string, arg?: RunnablePayload) {
// iI using GA retries, log the retry attempt as the status of the attempt is not used
if (event === 'retry' && this.reporterName === 'spec' && !this.retriesConfig?.experimentalStrategy) {
if (event === 'retry' && this.reporterName === 'spec' && !this.retriesConfig?.experimentalStrategy && arg) {
this._logRetry(arg)
}
// If using experimental retries, log the attempt after the test attempt runs to accurately represent the attempt pass/fail status
// We don't log the last attempt as this is handled by the main pass/fail handler defined above, and would ultimately log AFTER the test complete test status is reported
// from the mocha:pass/mocha:fail event
if (event === 'test:after:run' && this.reporterName === 'spec' && this.retriesConfig?.experimentalStrategy && !arg.final) {
if (event === 'test:after:run' && this.reporterName === 'spec' && this.retriesConfig?.experimentalStrategy && arg && !arg.final) {
this._logRetry(arg)
}
// ignore the event if we haven't accounted for it
if (!events[event]) return
if (!(event in events)) return
const args = this.parseArgs(event, arg)
const args = this.parseArgs(event as ReporterEventName, arg)
return this.runner && this.runner.emit.apply(this.runner, args)
return this.runner && this.runner.emit.apply(this.runner, args as Parameters<Mocha.Runner['emit']>)
}
parseArgs (event, arg) {
parseArgs (event: ReporterEventName, arg?: RunnablePayload): [string, ...unknown[]] {
const normalizeArgs = events[event]
const args = _.isFunction(normalizeArgs)
? normalizeArgs.call(this, arg, this.runnables, this.stats)
? normalizeArgs.call(this, arg ?? { id: '' }, this.runnables, this.stats)
: [arg]
// the normalizeArgs function needs the id property, but we want to remove
// and other private props before logging/emitting to mocha
args[0] = _.isPlainObject(args[0]) ? _.omit(args[0], ['id', 'hookId']) : args[0]
if (_.isPlainObject(args[0])) {
args[0] = _.omit(args[0] as object, ['id', 'hookId'])
}
debug('got mocha event \'%s\' with args: %o', event, args)
return [event].concat(args)
return [event, ...args] as [string, ...unknown[]]
}
_logRetry (test) {
const runnable = this.runnables[test.id]
_logRetry (test: RunnablePayload) {
const runnable = this.runnables[test.id!]
// Merge the runnable with the updated test props to gain most recent status from the app runnable (in the case a passed test is retried).
_.extend(runnable, test)
const padding = ' '.repeat(runnable.titlePath().length)
const padding = ' '.repeat(runnable.titlePath!().length)
// Don't display a pass/fail symbol if we don't know the status.
let mochaSymbolToDisplay = ''
@@ -510,7 +552,7 @@ class Reporter {
}
}
const attemptMessage = mochaColor(mochaColorScheme, buildAttemptMessage(test.currentRetry + 1, test.retries + 1))
const attemptMessage = mochaColor(mochaColorScheme, buildAttemptMessage((test.currentRetry ?? 0) + 1, (test.retries ?? 0) + 1))
// Log: `(Attempt 1 of 2) test title` when a test attempts without experimentalRetries configured.
// OR
@@ -521,7 +563,7 @@ class Reporter {
return console.log(`${padding}${mochaSymbolToDisplay}${attemptMessage} ${test.title}`)
}
normalizeHook (hook = {}) {
normalizeHook (hook: InternalRunnable = {}): ReporterHook {
return {
hookId: hook.hookId,
hookName: hook.hookName,
@@ -530,8 +572,8 @@ class Reporter {
}
}
normalizeTest (test = {}) {
let outerTest = _.clone(test)
normalizeTest (test: InternalRunnable = {}): ReporterTest {
const outerTest = _.clone(test)
// In the case tests were skipped or another case where they haven't run,
// test._cypressTestStatusInfo.outerStatus will be undefined. In this case,
@@ -554,21 +596,34 @@ class Reporter {
title: getTitlePath(outerTest),
state: orNull(outerTest.state),
body: orNull(outerTest.body),
displayError: orNull(outerTest.err && outerTest.err.stack),
attempts: _.map((outerTest.prevAttempts || []).concat([test]), (attempt) => {
const err = attempt.err && {
name: attempt.err.name,
message: attempt.err.message,
stack: attempt.err.stack && stackUtils.stackWithoutMessage(attempt.err.stack),
codeFrame: attempt.err.codeFrame,
}
displayError: orNull(
typeof outerTest.err === 'object' && outerTest.err?.stack
? outerTest.err.stack
: null,
),
attempts: _.map((outerTest.prevAttempts || []).concat([test]), (attempt): ReporterTestAttempt => {
const errSource = attempt.err
const err = errSource ? {
name: typeof errSource === 'object' ? errSource.name : undefined,
message: typeof errSource === 'object' ? errSource.message : undefined,
stack: typeof errSource === 'object'
? errSource.stack
? stackUtils.stackWithoutMessage(errSource.stack)
: errSource.stack
: undefined,
codeFrame: typeof errSource === 'object' ? errSource.codeFrame : undefined,
} : undefined
return {
state: orNull(attempt.state),
error: orNull(err),
timings: orNull(attempt.timings),
failedFromHookId: orNull(attempt.failedFromHookId),
wallClockStartedAt: orNull(attempt.wallClockStartedAt && new Date(attempt.wallClockStartedAt)),
wallClockStartedAt: orNull(
attempt.wallClockStartedAt != null
? new Date(attempt.wallClockStartedAt)
: null,
),
wallClockDuration: orNull(attempt.wallClockDuration),
videoTimestamp: null,
}
@@ -578,14 +633,14 @@ class Reporter {
return normalizedTest
}
end () {
end (): Promise<ReporterResults> | ReporterResults {
if (this.reporter.done) {
const {
failures,
} = this.runner
return new Promise((resolve, reject) => {
return this.reporter.done(failures, resolve)
return new Promise<void>((resolve) => {
return this.reporter.done!(failures, resolve)
}).then(() => {
return this.results()
})
@@ -594,7 +649,7 @@ class Reporter {
return this.results()
}
results () {
results (): ReporterResults {
const tests = _
.chain(this.runnables)
.filter({ type: 'test' })
@@ -618,7 +673,10 @@ class Reporter {
const { wallClockStartedAt, wallClockEndedAt } = this.stats
if (wallClockStartedAt && wallClockEndedAt) {
this.stats.wallClockDuration = wallClockEndedAt - wallClockStartedAt
const startedAt = wallClockStartedAt instanceof Date ? wallClockStartedAt.getTime() : Date.parse(String(wallClockStartedAt))
const endedAt = wallClockEndedAt instanceof Date ? wallClockEndedAt.getTime() : Date.parse(String(wallClockEndedAt))
this.stats.wallClockDuration = endedAt - startedAt
}
this.stats.suites = suites.length
@@ -644,26 +702,25 @@ class Reporter {
}
}
static setVideoTimestamp (videoStart, tests = []) {
return _.map(tests, (test) => {
// if we have a wallClockStartedAt
let wcs
static setVideoTimestamp (videoStart: Date | number, tests: ReporterTestAttempt[] = []) {
const videoStartMs = videoStart instanceof Date ? videoStart.getTime() : videoStart
wcs = test.wallClockStartedAt
return _.map(tests, (test) => {
const wcs = test.wallClockStartedAt
if (wcs) {
test.videoTimestamp = test.wallClockStartedAt - videoStart
test.videoTimestamp = wcs.getTime() - videoStartMs
}
return test
})
}
static create (reporterName, reporterOptions, projectRoot) {
static create (reporterName: string, reporterOptions: Record<string, unknown>, projectRoot?: string) {
return new Reporter(reporterName, reporterOptions, projectRoot)
}
static loadReporter (reporterName, projectRoot) {
static loadReporter (reporterName: string, projectRoot?: string): string | unknown {
let p
debug('trying to load reporter:', reporterName)
@@ -692,7 +749,7 @@ class Reporter {
// that is local (./custom-reporter.js)
// or one installed by the user through npm
try {
p = path.resolve(projectRoot, reporterName)
p = path.resolve(projectRoot ?? '', reporterName)
// try local
debug('trying to require local reporter with path:', p)
@@ -701,15 +758,15 @@ class Reporter {
// absolute path as the reporterName which avoids
// joining projectRoot unnecessarily
return require(p)
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') {
// bail early if the error wasn't MODULE_NOT_FOUND
// because that means theres something actually wrong
// with the found reporter
throw err
}
p = path.resolve(projectRoot, 'node_modules', reporterName)
p = path.resolve(projectRoot ?? '', 'node_modules', reporterName)
// try npm. if this fails, we're out of options, so let it throw
debug('trying to require local reporter with path:', p)
@@ -718,12 +775,10 @@ class Reporter {
}
}
static getSearchPathsForReporter (reporterName, projectRoot) {
static getSearchPathsForReporter (reporterName: string, projectRoot?: string): string[] {
return _.uniq([
path.resolve(projectRoot, reporterName),
path.resolve(projectRoot, 'node_modules', reporterName),
path.resolve(projectRoot ?? '', reporterName),
path.resolve(projectRoot ?? '', 'node_modules', reporterName),
])
}
}
module.exports = Reporter
+183 -25
View File
@@ -1,21 +1,71 @@
interface ReporterTestAttempt {
state: 'skipped' | 'failed' | 'passed'
error: any
timings: any
failedFromHookId: any
wallClockStartedAt: Date
wallClockDuration: number
videoTimestamp: any
export type RunnableState = 'passed' | 'failed' | 'pending' | 'skipped'
interface ReporterCodeFrame {
line: number
column: number
originalFile: string
relativeFile: string
absoluteFile: string
frame: string
language: string
}
interface ReporterTest {
testId: string
export interface ReporterTestError {
name?: string
message?: string
stack?: string
codeFrame?: ReporterCodeFrame
}
export interface ReporterTestAttempt {
state: RunnableState | null
error: ReporterTestError | null
timings: unknown
failedFromHookId: string | null
wallClockStartedAt: Date | null
wallClockDuration: number | null
videoTimestamp: number | null
}
export interface ReporterTest {
testId: string | null
title: string[]
state: 'skipped' | 'passed' | 'failed'
body: string
displayError: any
state: RunnableState | null
body: string | null
displayError: string | null
attempts: ReporterTestAttempt[]
}
export interface ReporterHook {
hookId: string | undefined
hookName: string | undefined
title: string[]
body: string | undefined
}
export interface ReporterStats {
suites: number
tests: number
passes: number
pending: number
skipped: number
failures: number
wallClockDuration: number
wallClockStartedAt?: Date | string
wallClockEndedAt?: Date | string
}
interface ReporterMochaStats {
suites: number
tests: number
passes: number
pending: number
failures: number
start?: string
end?: string
duration?: number
}
export interface BaseReporterResults {
error?: string
stats: {
@@ -31,18 +81,126 @@ export interface BaseReporterResults {
}
}
export interface ReporterResults extends BaseReporterResults {
export interface ReporterResults {
error?: string
stats: ReporterStats
reporter: string
reporterStats: {
suites: number
tests: number
passes: number
pending: number
failures: number
start: string
end: string
duration: number
}
hooks: any[]
reporterStats: ReporterMochaStats
hooks: ReporterHook[]
tests: ReporterTest[]
}
export interface CypressTestStatusInfo {
attempts?: number
strategy?: string
outerStatus?: RunnableState
shouldAttemptsContinue?: boolean
}
export interface RunnableAttemptPayload {
err?: ReporterTestError | string
state?: RunnableState
timings?: unknown
failedFromHookId?: string
wallClockStartedAt?: number | Date
wallClockDuration?: number
}
export interface RunnablePayload {
id?: string
title?: string
body?: string
type?: 'test' | 'hook' | 'suite'
state?: RunnableState
root?: boolean
file?: string
duration?: number
timedOut?: boolean
async?: number | boolean
sync?: boolean
err?: ReporterTestError | string
_retries?: number
_currentRetry?: number
currentRetry?: number
retries?: number
hookId?: string
hookName?: string
originalTitle?: string
final?: boolean
failedFromHookId?: string
prevAttempts?: RunnableAttemptPayload[]
_cypressTestStatusInfo?: CypressTestStatusInfo
timings?: unknown
wallClockStartedAt?: number | Date
wallClockDuration?: number
tests?: RunnablePayload[]
suites?: RunnablePayload[]
start?: number | Date
end?: number | Date
}
export type RetriesConfig = {
experimentalStrategy?: string
experimentalOptions?: Record<string, unknown>
runMode?: number | boolean | null
openMode?: number | boolean | null
}
export type ReporterEventName =
| 'start'
| 'end'
| 'suite'
| 'suite end'
| 'test'
| 'test end'
| 'hook'
| 'retry'
| 'hook end'
| 'pass'
| 'pending'
| 'fail'
| 'test:after:run'
| 'test:before:run'
type ReporterEventHandler = (
arg: RunnablePayload,
runnables: Record<string, InternalRunnable>,
stats: ReporterStats,
) => unknown[]
export type ReporterEventHandlers = {
[K in ReporterEventName]?: ReporterEventHandler | true
}
export interface InternalRunnable {
id?: string
title?: string
body?: string
type?: 'test' | 'hook' | 'suite'
state?: RunnableState
root?: boolean
file?: string
duration?: number
timedOut?: boolean
async?: number | boolean
sync?: boolean
err?: ReporterTestError | string
_retries?: number
_currentRetry?: number
hookId?: string
hookName?: string
originalTitle?: string
failedFromHookId?: string
prevAttempts?: RunnableAttemptPayload[]
_cypressTestStatusInfo?: CypressTestStatusInfo
timings?: unknown
wallClockStartedAt?: number | Date
wallClockDuration?: number
parent?: InternalRunnable
tests?: InternalRunnable[]
suites?: InternalRunnable[]
titlePath?: () => string[]
fullTitle?: () => string
speed?: string
retries?: number
}
@@ -1,5 +1,5 @@
import type { ProjectBase } from '../project-base'
import type { BaseReporterResults, ReporterResults } from '../types/reporter'
import type { BaseReporterResults, ReporterResults, ReporterTestError } from '../types/reporter'
import { log, stackUtils, stripAnsi } from '@packages/errors'
import Debug from 'debug'
import pDefer, { DeferredPromise } from 'p-defer'
@@ -7,8 +7,8 @@ import pDefer, { DeferredPromise } from 'p-defer'
const debug = Debug('cypress:util:crash_handling')
/** Matches attempt `error` shape from `reporter.js` `normalizeTest` for Cypress Cloud. */
export const fatalErrorToAttemptError = (error: Error) => {
const codeFrame = (error as { codeFrame?: unknown }).codeFrame
export const fatalErrorToAttemptError = (error: Error): ReporterTestError => {
const codeFrame = (error as { codeFrame?: ReporterTestError['codeFrame'] }).codeFrame
const stackLines = error.stack ? stackUtils.stackWithoutMessage(error.stack) : undefined
return {
@@ -19,14 +19,27 @@ export const fatalErrorToAttemptError = (error: Error) => {
}
}
const parseReporterTimestamp = (value?: Date | string): number | undefined => {
if (!value) {
return undefined
}
if (value instanceof Date) {
return value.getTime()
}
return Date.parse(value)
}
export const patchRunResultsAfterCrash = (
error: Error,
reporterResults: ReporterResults,
mostRecentRunnable: { id?: string } | undefined,
): ReporterResults => {
const endTime: number = reporterResults?.stats?.wallClockEndedAt ? Date.parse(reporterResults?.stats?.wallClockEndedAt) : new Date().getTime()
const wallClockDuration = reporterResults?.stats?.wallClockStartedAt ?
endTime - Date.parse(reporterResults.stats.wallClockStartedAt) : 0
const endTime: number = parseReporterTimestamp(reporterResults?.stats?.wallClockEndedAt) ?? new Date().getTime()
const wallClockStartedAt = parseReporterTimestamp(reporterResults?.stats?.wallClockStartedAt)
const wallClockDuration = wallClockStartedAt ?
endTime - wallClockStartedAt : 0
const endTimeStamp = new Date(endTime).toJSON()
// in crash situations, the most recent report will not have the triggering test
@@ -127,10 +140,10 @@ export class EarlyExitTerminator {
console.log('')
log(error)
const runResults: BaseReporterResults = (this.intermediateStats && this.pendingRunnable) ?
const runResults = (this.intermediateStats && this.pendingRunnable) ?
patchRunResultsAfterCrash(error, this.intermediateStats, this.pendingRunnable) :
defaultStats(error)
this.terminator.resolve(runResults)
this.terminator.resolve(runResults as BaseReporterResults)
}
}
@@ -32,7 +32,7 @@ const errors = require(`../../lib/errors`)
const cypress = require(`../../lib/cypress`)
const ProjectBase = require(`../../lib/project-base`).ProjectBase
const { ServerBase } = require(`../../lib/server-base`)
const Reporter = require(`../../lib/reporter`)
const { Reporter } = require(`../../lib/reporter`)
const browsers = require(`../../lib/browsers`).default
const videoCapture = require(`../../lib/video_capture`)
const browserUtils = require(`../../lib/browsers/utils`).default
@@ -1,7 +1,7 @@
require('../spec_helper')
import '../spec_helper'
const Reporter = require(`../../lib/reporter`)
const snapshot = require('snap-shot-it')
import { Reporter } from '../../lib/reporter'
import snapshot from 'snap-shot-it'
describe('lib/reporter', () => {
beforeEach(function () {
@@ -10,7 +10,7 @@ Learn more at https://on.cypress.io/reporters
Error: Cannot find module '/foo/bar/.projects/e2e/node_modules/module-does-not-exist'
Require stack:
- lib/reporter.js
- lib/reporter.ts
[stack trace lines]
`