Files
cypress/packages/errors/src/errTemplate.ts
Brian Mann 29841f32b9 feat: redesign server errors (#20072)
* 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>
2022-02-11 02:06:07 -05:00

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)
}