mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-09 16:50:23 -06:00
* chore: rename errors.js -> errors.ts * refactor: type safety on errors * refactor: add err_template for consistent error formatting * fix a few system tests * fix tests; update snapshots * Fix types * normalize snapshot - remove chalk ansi colors * more unit test fixes * more system test fixes * circleci build * backtick always in stdout, fix error formatting and failing snapshots * refactor: create @packages/errors * fix import * fix import * fixing build / tests * remove extraneous file * move warnIfExplicitCiBuildId * fix build / tests * Fix * error, type fixes, documentation, standardize child process error serialization * fix import * build errors on install * wrote specs generating visual images of all errors * remove unused dep * sanitize stack traces * add image diffing - if base images don't exist, create them - if base images don't match and local, overwrite them, if in CI throw - if base images are stale and local, delete them, if in CI throw * remove Courier New + MesloLGS NF font * type fixes, remove Bluebird, general cleanup * TS Cleanup * skip typecheck on tests for now * yarn.lock * fix @types/chai version * fix yarn.lock * Different version of mocha types so it isnt patched * errors spec snapshot * CI fix * fixes * store snapshot images in circle artifacts * dont change artifact destination prefix * use Courier Prime * antialias the text * decrease pixelmatch threshold, fail in CI only when changed pixels > 100 * increase timeout * overflow: hidden, remove new Promise, add debug logging Co-Authored-By: Tim Griesser <tgriesser@gmail.com> * run unit tests w/ concurrency=1 * unique window per file * disable app hardware acceleration + use in process gpu + single process * do not do image diffing - conditionally convert html to images - store html snapshots - do not store images in git * store snapshot html * Merge branch 'tgriesser/chore/refactor-errors' of https://github.com/cypress-io/cypress into tgriesser/chore/refactor-errors * remove concurrency * fix assertion * fixing ci * Link in readme * pass the browsers to listItems * fix: build @packages/errors in CI, defer import to prevent errors locally * Merge branch 'develop' into tgriesser/chore/refactor-errors * develop: chore: fix cypress npm package artifact upload path (#20023) chore(driver): move cy.within logic into it's own file (#20036) chore: update automerge workflows (#19982) fix(selectFile): use target window's File/DataTransfer classes (#20003) chore: Update Chrome (stable) to 98.0.4758.80 and Chrome (beta) to 98.0.4758.80 (#19995) fix: Adjust ffmpeg CLI args for performance (#19983) build: allow unified to run cypress on Apple Silicon (arm64) (backport #19067 to 9.x) (#19968) * fix run-if-ci.sh * remove dead code * Mark the .html files as generated in gitattributes * fix running single error case, slice out more of the brittle stack * remove additional brittle stack line * firefox web security error * nest inside of describe * reformat and redesign errors * more error cleanup and standardization * additional formatting of errors, code cleanup, refactoring * update ansi colors to match terminal colors * cleanup remaining loose ends, update several errors, compact excess formatters * fix types * additional formatting, remove TODO's, ensure no [object Object] in output * add test for 412 server response on invalid schema * update unknown dashboard error on creating run * use fs.access instead of fs.stat for perf * added PLUGINS_FILE_NOT_FOUND error - separated out from PLUGINS_FILE_ERROR - add system tests for both cases - update snapshots - remove stack trace from PLUGINS_FILE_NOT_FOUND fka PLUGINS_FILE_ERROR * add plugins system test around plugins synchronously throwing on require * remove forcing process.cwd() to be packages/server, update affected code - this was a long needed hangover from very old code that was doing unnecessary things due to respawning electron from node and handling various entrypoints into booting cypress - this also makes the root yarn dev and dev-debug work correctly because options.cwd is set correctly now * perf: lazy load chalk * remove excessive line since the file exists but is invalid * fix types * add system test when plugins function throws a synchronous error * create new PLUGINS_INVALID_EVENT_ERROR, wire it up correctly for error template use - properly pass error instance from child to ensure proper user stack frames - move error display code into the right place * only show a single stack trace, either originalError or internal cypressError * push error html snapshots * fix tests, types * fix test * fix failing tests * fix tests * fixes lots of broken tests * more test fixes * fixes more tests * fix type checking * wip: consistent handling of interpolated values * feat: fixing up errors, added simple error comparison tool * wrapping up error formatting * Fixes for unit tests * fix PLUGINS_VALIDATION_ERROR * fix fs.readdir bug, show rows even if there's only markdown formatting [SKIP CI] * when in base-list, show full width of errors * Fix type errors * added searching and filtering for files based on error name * fix: system tests * updated NO_SPECS_FOUND error to properly join searched folder + pattern - join patterns off of process.cwd, not projectRoot - highlight original specPattern in yellow, baseDir in blue - add tests * fixes failing tests * fix test * preserve original spec pattern, display relative to projectRoot for terminal banner * make the nodeVersion path display in gray, not white * fix tests, pass right variables * fix chrome:canary snapshots * update snapshots * update snapshot * strip newlines caused by "Still waiting to connect to {browser}..." * don't remove the snapshotHtmlFolder in CI, add additional verification snapshots match to error keys symmetrically * update snapshot * update snapshot * update snapshot * update snapshot * update snapshot * update snapshot * update gitignore * fix snapshot * update snapshot html * update logic for parsing the resolve pattern matching, add tests * update snapshots * update snapshot * update snapshot * update snapshot * fix failing test * fix: error_message_spec * fix snapshot * run each variant through an it(...) so multiple failures are received * add newlines to multiline formatters, add fmt.stringify, allow format overrides * stringify invalid return values from plugins * move config validation errors into packages/errors, properly highlight and stringify values * add component testing yarn commands * fix the arrow not showing on details * fix typescript error * fixed lots of poorly written tests that weren't actually testing anything. created settings validation error when given a string validation result. * fixes tests * fixes tests, adds new error template for validating within an array list (for browser validation) * remove dupe line * fix copy for consistency, update snapshots * remove redundant errors, standardize formatting and phrasing * more formatting * remove excess snapshots * prune out excessive error snapshot html files when not in CI * add missing tests, add case for when config validation fails without a fileType * fixes test * update snapshot * update snapshot * update snapshot * sort uniqErrors + errorKeys prior to assertion - assert one by one * add system test for binding to an event with the wrong handler * fixes tests * set more descriptive errors when setting invalid plugin events, or during plugin event validation * remove duplicate PLUGINS_EVENT_ERROR, collapse into existing error * use the same multiline formatting as @packages/errors * standardize verbiage and highlighting for consistency * fix incorrect error path * fixes tests, standardized and condensed more language * Update packages/errors/src/errors.ts Co-authored-by: Ryan Manuel <ryanm@cypress.io> * Update guides/error-handling.md Co-authored-by: Ryan Manuel <ryanm@cypress.io> * Update guides/error-handling.md Co-authored-by: Ryan Manuel <ryanm@cypress.io> * added some final todo's * fix types Co-authored-by: Tim Griesser <tgriesser10@gmail.com> Co-authored-by: Tim Griesser <tgriesser@gmail.com> Co-authored-by: Ryan Manuel <ryanm@cypress.io>
345 lines
8.9 KiB
TypeScript
345 lines
8.9 KiB
TypeScript
import assert from 'assert'
|
|
import chalk from 'chalk'
|
|
import _ from 'lodash'
|
|
import stripAnsi from 'strip-ansi'
|
|
import { trimMultipleNewLines } from './errorUtils'
|
|
import { stripIndent } from './stripIndent'
|
|
|
|
import type { ErrTemplateResult, SerializedError } from './errorTypes'
|
|
|
|
interface ListOptions {
|
|
prefix?: string
|
|
color?: keyof typeof theme
|
|
}
|
|
|
|
export const theme = {
|
|
blue: chalk.blueBright,
|
|
gray: chalk.gray,
|
|
white: chalk.white,
|
|
yellow: chalk.yellow,
|
|
magenta: chalk.magentaBright,
|
|
}
|
|
|
|
type AllowedPartialArg = Guard | Format | PartialErr | null
|
|
|
|
type AllowedTemplateArg = StackTrace | AllowedPartialArg
|
|
|
|
export class PartialErr {
|
|
constructor (readonly strArr: TemplateStringsArray, readonly args: AllowedTemplateArg[]) {}
|
|
}
|
|
|
|
interface FormatConfig {
|
|
block?: true
|
|
color?: typeof theme[keyof typeof theme]
|
|
stringify?: boolean
|
|
}
|
|
|
|
type ToFormat = string | number | Error | object | null | Guard | AllowedTemplateArg
|
|
|
|
class Format {
|
|
constructor (
|
|
readonly type: keyof typeof fmtHighlight,
|
|
readonly val: ToFormat,
|
|
readonly config: FormatConfig,
|
|
) {
|
|
this.color = config.color || fmtHighlight[this.type]
|
|
}
|
|
|
|
private color: typeof theme[keyof typeof theme]
|
|
|
|
formatVal (target: 'ansi' | 'markdown'): string {
|
|
if (this.val instanceof Guard) {
|
|
return `${this.val.val}`
|
|
}
|
|
|
|
const str = target === 'ansi' ? this.formatAnsi() : this.formatMarkdown()
|
|
|
|
// add a newline to ensure this is on its own line
|
|
return isMultiLine(str) ? `\n\n${str}` : str
|
|
}
|
|
|
|
private formatAnsi () {
|
|
const val = this.prepVal('ansi')
|
|
|
|
if (this.type === 'terminal') {
|
|
return `${theme.gray('$')} ${this.color(val)}`
|
|
}
|
|
|
|
return this.color(val)
|
|
}
|
|
|
|
private formatMarkdown () {
|
|
if (this.type === 'comment') {
|
|
return `${this.val}`
|
|
}
|
|
|
|
const val = this.prepVal('markdown')
|
|
|
|
if (this.type === 'terminal') {
|
|
return `${'```'}\n$ ${val}\n${'```'}`
|
|
}
|
|
|
|
if (this.type === 'code') {
|
|
return `${'```'}\n${val}\n${'```'}`
|
|
}
|
|
|
|
return mdFence(this.prepVal('markdown'))
|
|
}
|
|
|
|
private prepVal (target: 'ansi' | 'markdown'): string {
|
|
if (this.val instanceof PartialErr) {
|
|
return prepMessage(this.val.strArr, this.val.args, target, true)
|
|
}
|
|
|
|
if (isErrorLike(this.val)) {
|
|
return `${this.val.name}: ${this.val.message}`
|
|
}
|
|
|
|
if (this.val && (this.config?.stringify || typeof this.val === 'object' || Array.isArray(this.val))) {
|
|
return JSON.stringify(this.val, null, 2)
|
|
}
|
|
|
|
return `${this.val}`
|
|
}
|
|
}
|
|
|
|
function mdFence (val: string) {
|
|
// Don't double fence values
|
|
if (val.includes('`')) {
|
|
return val
|
|
}
|
|
|
|
if (isMultiLine(val)) {
|
|
return `\`\`\`\n${val}\n\`\`\``
|
|
}
|
|
|
|
return `\`${val}\``
|
|
}
|
|
|
|
function isMultiLine (val: string) {
|
|
return Boolean(val.split('\n').length > 1)
|
|
}
|
|
|
|
function makeFormat (type: keyof typeof fmtHighlight, config?: FormatConfig) {
|
|
return (val: ToFormat, overrides?: FormatConfig) => {
|
|
return new Format(type, val, {
|
|
...config,
|
|
...overrides,
|
|
})
|
|
}
|
|
}
|
|
|
|
const fmtHighlight = {
|
|
meta: theme.gray,
|
|
comment: theme.gray,
|
|
path: theme.blue,
|
|
code: theme.blue,
|
|
url: theme.blue,
|
|
flag: theme.magenta,
|
|
stringify: theme.magenta,
|
|
highlight: theme.yellow,
|
|
highlightSecondary: theme.magenta,
|
|
highlightTertiary: theme.blue,
|
|
terminal: theme.blue,
|
|
} as const
|
|
|
|
export const fmt = {
|
|
meta: makeFormat('meta'),
|
|
comment: makeFormat('comment'),
|
|
path: makeFormat('path'),
|
|
code: makeFormat('code', { block: true }),
|
|
url: makeFormat('url'),
|
|
flag: makeFormat('flag'),
|
|
stringify: makeFormat('stringify', { stringify: true }),
|
|
highlight: makeFormat('highlight'),
|
|
highlightSecondary: makeFormat('highlightSecondary'),
|
|
highlightTertiary: makeFormat('highlightTertiary'),
|
|
terminal: makeFormat('terminal'),
|
|
off: guard,
|
|
listItem,
|
|
listItems,
|
|
listFlags,
|
|
stackTrace,
|
|
cypressVersion,
|
|
}
|
|
|
|
function cypressVersion (version: string) {
|
|
const parts = version.split('.')
|
|
|
|
if (parts.length !== 3) {
|
|
throw new Error('Cypress version provided must be in x.x.x format')
|
|
}
|
|
|
|
return guard(`Cypress version ${version}`)
|
|
}
|
|
|
|
function _item (item: string, options: ListOptions = {}) {
|
|
const { prefix, color } = _.defaults(options, {
|
|
prefix: '',
|
|
color: 'blue',
|
|
})
|
|
|
|
return stripIndent`${theme.gray(prefix)}${theme[color](item)}`
|
|
}
|
|
|
|
function listItem (item: string, options: ListOptions = {}) {
|
|
_.defaults(options, {
|
|
prefix: ' > ',
|
|
})
|
|
|
|
return guard(_item(item, options))
|
|
}
|
|
|
|
function listItems (items: string[], options: ListOptions = {}) {
|
|
_.defaults(options, {
|
|
prefix: ' - ',
|
|
})
|
|
|
|
return guard(items
|
|
.map((item) => _item(item, options))
|
|
.join('\n'))
|
|
}
|
|
|
|
function listFlags (
|
|
obj: Record<string, string | undefined | null>,
|
|
mapper: Record<string, string>,
|
|
) {
|
|
return guard(_
|
|
.chain(mapper)
|
|
.map((flag, key) => {
|
|
const v = obj[key]
|
|
|
|
if (v) {
|
|
return `The ${flag} flag you passed was: ${theme.yellow(v)}`
|
|
}
|
|
|
|
return undefined
|
|
})
|
|
.compact()
|
|
.join('\n')
|
|
.value())
|
|
}
|
|
|
|
export class Guard {
|
|
constructor (readonly val: string | number) {}
|
|
}
|
|
|
|
/**
|
|
* Prevents a string from being colored "blue" when wrapped in the errTemplate
|
|
* tag template literal
|
|
*/
|
|
export function guard (val: string | number) {
|
|
return new Guard(val)
|
|
}
|
|
|
|
/**
|
|
* Marks the value as "details". This is when we print out the stack trace to the console
|
|
* (if it's an error), or use the stack trace as the originalError
|
|
*/
|
|
export class StackTrace {
|
|
/**
|
|
* @param {string | Error | object} stackTrace
|
|
*/
|
|
constructor (readonly val: string | Error | object) {}
|
|
}
|
|
|
|
export function stackTrace (val: string | Error | object) {
|
|
return new StackTrace(val)
|
|
}
|
|
|
|
export function isErrorLike (err: any): err is SerializedError | Error {
|
|
return err && typeof err === 'object' && Boolean('name' in err && 'message' in err)
|
|
}
|
|
|
|
/**
|
|
* Creates a "partial" that can be interpolated into the full Error template. The partial runs through
|
|
* stripIndent prior to being added into the template
|
|
*/
|
|
export const errPartial = (templateStr: TemplateStringsArray, ...args: AllowedPartialArg[]) => {
|
|
return new PartialErr(templateStr, args)
|
|
}
|
|
|
|
let originalError: Error | undefined = undefined
|
|
let details: string | undefined
|
|
|
|
/**
|
|
* Creates a consistently formatted object to return from the error call.
|
|
*
|
|
* For the console:
|
|
* - By default, wrap every arg in yellow, unless it's "guarded" or is a "details"
|
|
* - Details stack gets logged at the end of the message in gray | magenta
|
|
*
|
|
* For the browser:
|
|
* - Wrap every arg in backticks, for better rendering in markdown
|
|
* - If details is an error, it gets provided as originalError
|
|
*/
|
|
export const errTemplate = (strings: TemplateStringsArray, ...args: AllowedTemplateArg[]): ErrTemplateResult => {
|
|
const msg = trimMultipleNewLines(prepMessage(strings, args, 'ansi'))
|
|
|
|
return {
|
|
message: msg,
|
|
details,
|
|
originalError,
|
|
messageMarkdown: trimMultipleNewLines(stripAnsi(prepMessage(strings, args, 'markdown'))),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes an `errTemplate` / `errPartial` and converts it into a string, formatted conditionally
|
|
* depending on the target environment
|
|
*
|
|
* @param templateStrings
|
|
* @param args
|
|
* @param target
|
|
* @returns
|
|
*/
|
|
function prepMessage (templateStrings: TemplateStringsArray, args: AllowedTemplateArg[], target: 'ansi' | 'markdown', isPartial: boolean = false): string {
|
|
// Reset the originalError to undefined on each new template string pass, we only need it to guard
|
|
if (!isPartial) {
|
|
originalError = undefined
|
|
details = undefined
|
|
}
|
|
|
|
const templateArgs: string[] = []
|
|
|
|
for (const arg of args) {
|
|
// We assume null/undefined values are skipped when rendering, for conditional templating
|
|
if (arg == null) {
|
|
templateArgs.push('')
|
|
} else if (arg instanceof Guard) {
|
|
// Guard prevents any formatting
|
|
templateArgs.push(`${arg.val}`)
|
|
} else if (arg instanceof Format) {
|
|
// Format = stringify & color ANSI, or make a markdown block
|
|
templateArgs.push(arg.formatVal(target))
|
|
} else if (arg instanceof StackTrace) {
|
|
assert(!originalError, `Cannot use fmt.stackTrace() multiple times in the same errTemplate`)
|
|
assert(!isPartial, `Cannot use fmt.stackTrace() in errPartial template string`)
|
|
|
|
if (isErrorLike(arg.val)) {
|
|
originalError = arg.val
|
|
details = originalError.stack
|
|
} else {
|
|
if (process.env.CYPRESS_INTERNAL_ENV !== 'production') {
|
|
throw new Error(`Cannot use arg.stackTrace with a non error-like value, saw ${JSON.stringify(arg.val)}`)
|
|
}
|
|
|
|
const err = new Error()
|
|
|
|
err.stack = typeof arg.val === 'string' ? arg.val : JSON.stringify(arg.val)
|
|
originalError = err
|
|
details = err.stack
|
|
}
|
|
|
|
templateArgs.push('')
|
|
} else if (arg instanceof PartialErr) {
|
|
// Partial error = prepMessage + interpolate
|
|
templateArgs.push(prepMessage(arg.strArr, arg.args, target, true))
|
|
} else {
|
|
throw new Error(`Invalid value passed to prepMessage, saw ${arg}`)
|
|
}
|
|
}
|
|
|
|
return stripIndent(templateStrings, ...templateArgs)
|
|
}
|