From fa4a19c1a37ae74a05084f929fe3b5f8ceb2ac48 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 18 Oct 2022 08:15:14 -0500 Subject: [PATCH 01/14] feat: enhance test error UI (#24266) --- .../e2e/commands/sessions/sessions.cy.js | 2 - .../driver/src/cy/commands/sessions/index.ts | 2 +- packages/driver/src/cypress/error_utils.ts | 2 +- packages/driver/src/cypress/log.ts | 12 +- packages/reporter/cypress/e2e/commands.cy.ts | 80 ++++++- .../reporter/cypress/e2e/test_errors.cy.ts | 5 +- .../reporter/cypress/e2e/unit/err_model.cy.ts | 6 + .../reporter/cypress/e2e/unit/events.cy.ts | 29 +-- .../reporter/src/attempts/attempt-model.ts | 32 ++- packages/reporter/src/attempts/attempts.tsx | 10 +- .../reporter/src/commands/command-model.ts | 21 +- packages/reporter/src/commands/command.tsx | 223 +++++++++--------- packages/reporter/src/commands/commands.scss | 27 +-- packages/reporter/src/errors/err-model.ts | 3 + packages/reporter/src/errors/errors.scss | 84 +++++-- packages/reporter/src/errors/test-error.tsx | 85 ++++--- packages/reporter/src/hooks/hook-model.ts | 4 +- packages/reporter/src/lib/events.ts | 13 +- packages/reporter/src/lib/tag.scss | 2 +- packages/reporter/src/lib/variables.scss | 7 +- packages/server/lib/socket-base.ts | 2 - 21 files changed, 395 insertions(+), 256 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js index c48f706dea7c..a77ca3303190 100644 --- a/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js +++ b/packages/driver/cypress/e2e/commands/sessions/sessions.cy.js @@ -585,7 +585,6 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[6].get()).to.deep.contain({ - showError: true, group: validateSessionGroup.id, }) @@ -679,7 +678,6 @@ describe('cy.session', { retries: 0 }, () => { }) expect(logs[6].get()).to.deep.contain({ - showError: true, group: validateSessionGroup.id, }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 51629c51746d..6e4c011e7c60 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -236,8 +236,8 @@ export default function (Commands, Cypress, cy) { // show validation error and allow sessions workflow to recreate the session if (restoreSession) { + err.isRecovered = true Cypress.log({ - showError: true, type: 'system', name: 'session', }) diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index d143c1ed337f..d40e497f8669 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -9,7 +9,7 @@ import $stackUtils, { StackAndCodeFrameIndex } from './stack_utils' import $utils from './utils' import type { HandlerType } from './runner' -const ERROR_PROPS = ['message', 'type', 'name', 'stack', 'parsedStack', 'fileName', 'lineNumber', 'columnNumber', 'host', 'uncaught', 'actual', 'expected', 'showDiff', 'isPending', 'docsUrl', 'codeFrame'] as const +const ERROR_PROPS = ['message', 'type', 'name', 'stack', 'parsedStack', 'fileName', 'lineNumber', 'columnNumber', 'host', 'uncaught', 'actual', 'expected', 'showDiff', 'isPending', 'isRecovered', 'docsUrl', 'codeFrame'] as const const ERR_PREPARED_FOR_SERIALIZATION = Symbol('ERR_PREPARED_FOR_SERIALIZATION') const crossOriginScriptRe = /^script error/i diff --git a/packages/driver/src/cypress/log.ts b/packages/driver/src/cypress/log.ts index 564002f9bb9c..4a66aab3b287 100644 --- a/packages/driver/src/cypress/log.ts +++ b/packages/driver/src/cypress/log.ts @@ -15,7 +15,7 @@ import type { StateFunc } from './state' const groupsOrTableRe = /^(groups|table)$/ const parentOrChildRe = /parent|child|system/ const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight'.split(' ') -const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps sessionInfo state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ') +const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements numResponses referencesAlias renderProps sessionInfo state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ') const BLACKLIST_PROPS = 'snapshots'.split(' ') let counter = 0 @@ -230,7 +230,7 @@ const defaults = function (state: StateFunc, config, obj) { } export class Log { - cy: any + createSnapshot: Function state: StateFunc config: any fireChangeEvent: ((log) => (void | undefined)) @@ -238,8 +238,8 @@ export class Log { private attributes: Record = {} - constructor (cy, state, config, fireChangeEvent, obj) { - this.cy = cy + constructor (createSnapshot, state, config, fireChangeEvent, obj) { + this.createSnapshot = createSnapshot this.state = state this.config = config // only fire the log:state:changed event as fast as every 4ms @@ -391,7 +391,7 @@ export class Log { } } - const snapshot = this.cy.createSnapshot(name, this.get('$el')) + const snapshot = this.createSnapshot(name, this.get('$el')) this.addSnapshot(snapshot, options) @@ -608,7 +608,7 @@ class LogManager { $errUtils.throwErrByPath('log.invalid_argument', { args: { arg: options } }) } - const log = new Log(cy, state, config, this.fireChangeEvent, options) + const log = new Log(cy.createSnapshot, state, config, this.fireChangeEvent, options) log.set(options) diff --git a/packages/reporter/cypress/e2e/commands.cy.ts b/packages/reporter/cypress/e2e/commands.cy.ts index 84bdbeb0a4bd..fffa69a6153d 100644 --- a/packages/reporter/cypress/e2e/commands.cy.ts +++ b/packages/reporter/cypress/e2e/commands.cy.ts @@ -926,24 +926,30 @@ describe('commands', { viewportHeight: 1000 }, () => { context('test error', () => { // this is a unique error permutation currently only observed in cy.session() where an error - // message should be presented during the session validation of a saves/restored session and the - // session command will attempt to recreate a valid session. - it('renders error instead of command', () => { - // font-family: $font-system; + // message should be presented if the session validation fails for a restored session because the + // session command recover and attempt to recreate a valid session. + it('renders recovered error for command', () => { cy.fixture('command_error').then((_commandErr) => { + const groupId = addCommand(runner, { + name: 'session', + message: 'mock restore', + state: 'passed', + type: 'system', + }) + addCommand(runner, { - id: 10, - number: 7, + type: 'system', name: 'validate', - displayMessage: 'mock session validation', state: 'failed', - showError: true, - err: _commandErr, - type: 'parent', + err: { + ..._commandErr, + isRecovered: true, + }, + groupLevel: 1, + group: groupId, }) addCommand(runner, { - id: 11, name: 'recreate session', message: 'mock recreate session cmd', state: 'success', @@ -955,6 +961,58 @@ describe('commands', { viewportHeight: 1000 }, () => { cy.contains('recreate session') cy.percySnapshot() }) + + it('renders recovered error for nested group command', () => { + cy.fixture('command_error').then((_commandErr) => { + const groupId = addCommand(runner, { + name: 'session', + message: 'mock restore', + state: 'passed', + type: 'system', + }) + + const nested = addCommand(runner, { + type: 'system', + name: 'validate', + state: 'failed', + group: groupId, + }) + + addCommand(runner, { + number: 8, + name: 'get', + message: 'does_not_exist', + state: 'failed', + err: { + ..._commandErr, + isRecovered: true, + }, + type: 'parent', + groupLevel: 2, + group: nested, + }) + + addCommand(runner, { + id: 12, + name: 'recreate session', + message: 'mock recreate session cmd', + state: 'success', + type: 'parent', + }) + }) + + cy.contains('.command', 'validate').as('validate') + .find('.command-expander') + .should('have.class', 'command-expander-is-open') + + cy.get('@validate').within(() => { + cy.contains('CommandError') + }) + + cy.contains('recreate session') + + cy.percySnapshot() + }) }) context('studio commands', () => { diff --git a/packages/reporter/cypress/e2e/test_errors.cy.ts b/packages/reporter/cypress/e2e/test_errors.cy.ts index 0634480fc91e..d2bd63474341 100644 --- a/packages/reporter/cypress/e2e/test_errors.cy.ts +++ b/packages/reporter/cypress/e2e/test_errors.cy.ts @@ -227,12 +227,13 @@ describe('test errors', () => { // NOTE: still needs to be implemented it.skip('renders and escapes markdown with leading/trailing whitespace', () => { + setError(commandErr) cy.get('.runnable-err-message') // https://github.com/cypress-io/cypress/issues/1360 // renders ** buzz ** as buzz - .contains('code', 'foo') - .and('not.contain', '`foo`') + .contains('strong', 'buzz') + .and('not.contain', '** buzz **') }) }) diff --git a/packages/reporter/cypress/e2e/unit/err_model.cy.ts b/packages/reporter/cypress/e2e/unit/err_model.cy.ts index 85f228f9468b..86d687b3096f 100644 --- a/packages/reporter/cypress/e2e/unit/err_model.cy.ts +++ b/packages/reporter/cypress/e2e/unit/err_model.cy.ts @@ -69,6 +69,12 @@ describe('Err model', () => { expect(err.stack).to.equal('the stack (path/to/file.js 45:203)') }) + it('updates isRecovered if specified', () => { + expect(err.isRecovered).to.be.false + err.update({ isRecovered: true }) + expect(err.isRecovered).to.be.true + }) + it('does nothing if props is undefined', () => { err.update() expect(err.name).to.equal('BadError') diff --git a/packages/reporter/cypress/e2e/unit/events.cy.ts b/packages/reporter/cypress/e2e/unit/events.cy.ts index 9c666dfe34fb..de05377037e6 100644 --- a/packages/reporter/cypress/e2e/unit/events.cy.ts +++ b/packages/reporter/cypress/e2e/unit/events.cy.ts @@ -306,22 +306,19 @@ describe('events', () => { expect(runner.emit).to.have.been.calledWith('runner:console:log', 'command id') }) - it('emits runner:console:error with test id on show:error', () => { - const test = { err: { isCommandErr: false } } + it('emits runner:console:error with error on show:error', () => { + const errorDetails = { err: { stack: 'hi' } } - runnablesStore.testById.returns(test) - events.emit('show:error', test) + events.emit('show:error', errorDetails) expect(runner.emit).to.have.been.calledWith('runner:console:error', { - err: test.err, + err: errorDetails.err, testId: undefined, logId: undefined, }) }) - it('emits runner:console:error with test id and command id on show:error when it is a command error and there is a matching command', () => { - const test = { err: { isCommandErr: true }, commandMatchingErr: () => { - return { id: 'matching command id', testId: 'test' } - } } + it('emits runner:console:error with error, test id and command id on show:error ', () => { + const test = { err: { isCommandErr: true }, commandId: 'matching command id', testId: 'test' } runnablesStore.testById.returns(test) events.emit('show:error', test) @@ -332,20 +329,6 @@ describe('events', () => { }) }) - it('emits runner:console:error with test id on show:error when it is a command error but there not a matching command', () => { - const test = { err: { isCommandErr: true }, commandMatchingErr: () => { - return null - } } - - runnablesStore.testById.returns(test) - events.emit('show:error', test) - expect(runner.emit).to.have.been.calledWith('runner:console:error', { - err: test.err, - testId: undefined, - logId: undefined, - }) - }) - it('emits runner:show:snapshot on show:snapshot', () => { events.emit('show:snapshot', 'command id') expect(runner.emit).to.have.been.calledWith('runner:show:snapshot', 'command id') diff --git a/packages/reporter/src/attempts/attempt-model.ts b/packages/reporter/src/attempts/attempt-model.ts index ca6a910a5373..59716cb78084 100644 --- a/packages/reporter/src/attempts/attempt-model.ts +++ b/packages/reporter/src/attempts/attempt-model.ts @@ -17,7 +17,7 @@ export default class Attempt { @observable agents: Agent[] = [] @observable sessions: Record = {} @observable commands: Command[] = [] - @observable err = new Err({}) + @observable err?: Err = undefined @observable hooks: Hook[] = [] // TODO: make this an enum with states: 'QUEUED, ACTIVE, INACTIVE' @observable isActive: boolean | null = null @@ -49,7 +49,10 @@ export default class Attempt { this.id = props.currentRetry || 0 this.test = test this._state = props.state - this.err.update(props.err) + + if (props.err) { + this.err = new Err(props.err) + } this.invocationDetails = props.invocationDetails @@ -78,6 +81,16 @@ export default class Attempt { return this._state || (this.isActive ? 'active' : 'processing') } + @computed get error () { + const command = this.err?.isCommandErr ? this.commandMatchingErr() : undefined + + return { + err: this.err, + testId: command?.testId, + commandId: command?.id, + } + } + @computed get isLast () { return this.id === this.test.lastAttempt.id } @@ -137,9 +150,14 @@ export default class Attempt { } } - commandMatchingErr () { + commandMatchingErr (): Command | undefined { + if (!this.err) { + return undefined + } + return _(this.hooks) .map((hook) => { + // @ts-ignore return hook.commandMatchingErr(this.err) }) .compact() @@ -155,7 +173,13 @@ export default class Attempt { this._state = props.state } - this.err.update(props.err) + if (props.err) { + if (this.err) { + this.err.update(props.err) + } else { + this.err = new Err(props.err) + } + } if (props.failedFromHookId) { const hook = _.find(this.hooks, { hookId: props.failedFromHookId }) diff --git a/packages/reporter/src/attempts/attempts.tsx b/packages/reporter/src/attempts/attempts.tsx index 2a40627d6c2a..56b338a7bf98 100644 --- a/packages/reporter/src/attempts/attempts.tsx +++ b/packages/reporter/src/attempts/attempts.tsx @@ -55,10 +55,12 @@ function renderAttemptContent (model: AttemptModel, studioActive: boolean) {
{model.hasCommands ? : }
-
- - {studioActive && model.state === 'failed' && } -
+ {model.state === 'failed' && ( +
+ + {studioActive && } +
+ )} ) } diff --git a/packages/reporter/src/commands/command-model.ts b/packages/reporter/src/commands/command-model.ts index 889a934d1085..dc4988685702 100644 --- a/packages/reporter/src/commands/command-model.ts +++ b/packages/reporter/src/commands/command-model.ts @@ -35,7 +35,6 @@ export interface CommandProps extends InstrumentProps { wallClockStartedAt?: string hookId: string isStudio?: boolean - showError?: boolean group?: number groupLevel?: number hasSnapshot?: boolean @@ -45,7 +44,7 @@ export interface CommandProps extends InstrumentProps { export default class Command extends Instrument { @observable.struct renderProps: RenderProps = {} @observable.struct sessionInfo?: SessionProps['sessionInfo'] - @observable err = new Err({}) + @observable err?: Err @observable event?: boolean = false @observable isLongRunning = false @observable number?: number @@ -56,7 +55,6 @@ export default class Command extends Instrument { @observable children: Array = [] @observable hookId: string @observable isStudio: boolean - @observable showError?: boolean = false @observable group?: number @observable groupLevel?: number @observable hasSnapshot?: boolean @@ -92,6 +90,7 @@ export default class Command extends Instrument { return this._isOpen || (this._isOpen === null && ( + this.err?.isRecovered || // command has nested commands (this.name !== 'session' && this.hasChildren && !this.event && this.type !== 'system') || // command has nested commands with children @@ -123,7 +122,10 @@ export default class Command extends Instrument { constructor (props: CommandProps) { super(props) - this.err.update(props.err) + if (props.err) { + this.err = new Err(props.err) + } + this.event = props.event this.number = props.number this.numElements = props.numElements @@ -136,7 +138,6 @@ export default class Command extends Instrument { this.wallClockStartedAt = props.wallClockStartedAt this.hookId = props.hookId this.isStudio = !!props.isStudio - this.showError = !!props.showError this.group = props.group this.hasSnapshot = !!props.hasSnapshot this.hasConsoleProps = !!props.hasConsoleProps @@ -148,7 +149,14 @@ export default class Command extends Instrument { update (props: CommandProps) { super.update(props) - this.err.update(props.err) + if (props.err) { + if (!this.err) { + this.err = new Err(props.err) + } else { + this.err.update(props.err) + } + } + this.event = props.event this.numElements = props.numElements this.renderProps = props.renderProps || {} @@ -159,7 +167,6 @@ export default class Command extends Instrument { this.timeout = props.timeout this.hasSnapshot = props.hasSnapshot this.hasConsoleProps = props.hasConsoleProps - this.showError = props.showError this._checkLongRunning() } diff --git a/packages/reporter/src/commands/command.tsx b/packages/reporter/src/commands/command.tsx index 8c18ecca5590..671f98f9decd 100644 --- a/packages/reporter/src/commands/command.tsx +++ b/packages/reporter/src/commands/command.tsx @@ -9,6 +9,7 @@ import Tooltip from '@cypress/react-tooltip' import appState, { AppState } from '../lib/app-state' import events, { Events } from '../lib/events' import FlashOnClick from '../lib/flash-on-click' +import StateIcon from '../lib/state-icon' import Tag from '../lib/tag' import { TimeoutID } from '../lib/types' import runnablesStore, { RunnablesStore } from '../runnables/runnables-store' @@ -258,6 +259,80 @@ interface Props { groupId?: number } +const CommandDetails = observer(({ model, groupId, aliasesWithDuplicates }) => ( + + + + {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} + + + {!!groupId && model.type === 'system' && model.state === 'failed' && } + {model.referencesAlias ? + + : + } + +)) + +const CommandControls = observer(({ model, commandName, events }) => { + const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 + const isSystemEvent = model.type === 'system' && model.event + const isSessionCommand = commandName === 'session' + const displayNumOfChildren = !isSystemEvent && !isSessionCommand && model.hasChildren && !model.isOpen + + const _removeStudioCommand = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + events.emit('studio:remove:command', model.number) + } + + return ( + + + {model.type === 'parent' && model.isStudio && ( + + )} + {isSessionCommand && ( + + )} + {!model.visible && ( + + + + + + )} + {displayNumOfElements && ( + + )} + + + + {displayNumOfChildren && ( + + )} + + + ) +}) + @observer class Command extends Component { @observable isOpen: boolean|null = null @@ -276,119 +351,60 @@ class Command extends Component { return null } - if (model.showError) { - // this error is rendered if an error occurs in session validation executed by cy.session - return - } - const commandName = model.name ? nameClassName(model.name) : '' - const displayNumOfElements = model.state !== 'pending' && model.numElements != null && model.numElements !== 1 - const isSystemEvent = model.type === 'system' && model.event - const isSessionCommand = commandName === 'session' - const displayNumOfChildren = !isSystemEvent && !isSessionCommand && model.hasChildren && !model.isOpen - const groupPlaceholder: Array = [] + let groupLevel = 0 + if (model.groupLevel !== undefined) { // cap the group nesting to 5 levels to keep the log text legible - const level = model.groupLevel < 6 ? model.groupLevel : 5 + groupLevel = model.groupLevel < 6 ? model.groupLevel : 5 - for (let i = 1; i < level; i++) { + for (let i = 1; i < groupLevel; i++) { groupPlaceholder.push() } } return ( -
  • -
    +
  • +
    - - -
    this._snapshot(true)} - onMouseLeave={() => this._snapshot(false)} + + - {groupPlaceholder} - - - - {model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)} - - - {model.referencesAlias ? - - : - } - - - {model.type === 'parent' && model.isStudio && ( - - )} - {isSessionCommand && ( - - )} - {!model.visible && ( - - - - - - )} - {displayNumOfElements && ( - - )} - - - - {displayNumOfChildren && ( - - )} - - -
    -
    -
    - - {this._children()} -
  • +
    this._snapshot(true)} + onMouseLeave={() => this._snapshot(false)} + > + {groupPlaceholder} + + +
    + + + + {this._children()} + + {model.err?.isRecovered && ( +
  • + )} + ) } @@ -481,15 +497,6 @@ class Command extends Component { }, 50) } } - - _removeStudioCommand = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - - const { model, events } = this.props - - events.emit('studio:remove:command', model.number) - } } export { Aliases, AliasesReferences, Message, Progress } diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index 0633227df7ed..ecc29616a416 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -131,20 +131,22 @@ .command-group-block { @include nested-command-dashes($gray-600); - width: 11px; + width: 12px; min-height: 28px; - min-width: 11px; + min-width: 12px; } } .command-info { + display: -webkit-box; + font-weight: 600; margin-left: 0; overflow: hidden; - width: 100%; - font-weight: 600; + padding-top: 4px; + padding-bottom: 4px; -webkit-line-clamp: 50; -webkit-box-orient: vertical; - display: -webkit-box; + width: 100%; .command-aliases, .command-message { @@ -254,9 +256,9 @@ color: $err-header-text; &:not(.command-type-system) { - border-left: 2px solid $fail; + border-left: $err-border; background-color: $err-header-background; - + &.command-is-interactive:hover { background: rgba($red-400, 0.3); } @@ -268,6 +270,10 @@ color: $err-header-text; } + .failed-indicator { + vertical-align: middle; + } + .command-group { border-color: $err-header-text; @include nested-command-dashes($err-header-text); @@ -279,13 +285,6 @@ } } - .command .runnable-err-wrapper { - padding: 0; - border: 0; - margin: 0; - margin-bottom: 5px; - } - // Custom Styles for Specific Commands .command-name-assert { .command-method { diff --git a/packages/reporter/src/errors/err-model.ts b/packages/reporter/src/errors/err-model.ts index 29aff35b2948..461a05e2c316 100644 --- a/packages/reporter/src/errors/err-model.ts +++ b/packages/reporter/src/errors/err-model.ts @@ -30,6 +30,7 @@ export interface ErrProps { docsUrl: string | string[] templateType: string codeFrame: CodeFrame + isRecovered: boolean } export default class Err { @@ -41,6 +42,7 @@ export default class Err { @observable templateType = '' // @ts-ignore @observable.ref codeFrame: CodeFrame + @observable isRecovered: boolean = false constructor (props?: Partial) { this.update(props) @@ -64,5 +66,6 @@ export default class Err { if (props.parsedStack) this.parsedStack = props.parsedStack if (props.templateType) this.templateType = props.templateType if (props.codeFrame) this.codeFrame = props.codeFrame + this.isRecovered = !!props.isRecovered } } diff --git a/packages/reporter/src/errors/errors.scss b/packages/reporter/src/errors/errors.scss index 2184e69d41f6..369a5ad2cfd1 100644 --- a/packages/reporter/src/errors/errors.scss +++ b/packages/reporter/src/errors/errors.scss @@ -51,8 +51,34 @@ $code-border-radius: 4px; } } - .runnable-err-wrapper { - cursor: default; + .show-recovered-test-err { + .runnable-err-header, + .runnable-err-body { + padding-left: 49px; + display: flex; + + .err-group-block { + border-left: 1px dotted $err-header-text; + border-image-slice: 0 0 0 1; + border-image-source: repeating-linear-gradient(0deg, transparent, $err-header-text, $err-header-text 4px); + width: 13px; + min-width: 13px; + } + } + + .runnable-err-header > .runnable-err-name { + padding: 5px 4px 5px 15px; + } + + .runnable-err-content { + padding: 0 12px 0 0; + } + } + + .runnable-err-content { + width: 100%; + overflow: scroll; + padding: 0 18px; } .studio-err-wrapper { @@ -60,13 +86,14 @@ $code-border-radius: 4px; } .runnable-err { - border-left: 2px solid $fail; background-color: $err-background; + border-left: $err-border; clear: both; color: $err-text; font-family: $monospace; + font-style: normal; margin-bottom: 0; - margin-top: 5px; + margin-top: 2px; white-space: pre-wrap; word-break: break-word; user-select: initial; @@ -74,39 +101,45 @@ $code-border-radius: 4px; } .runnable-err-header { - background-color: rgba($red-400, 0.05); + background-color: $err-header-background; display: flex; - justify-content: space-between; - padding: 5px 10px; font-weight: bold; + justify-content: space-between; + padding-left: 18px; + + svg { + color: $red-400; + align-self: center + } .runnable-err-name { - flex-grow: 2; - font-size: 13px; - line-height: 22px; color: $err-header-text; - - svg { - margin-right: 10px; - } + flex: auto; + font-size: 12px; + font-weight: 600; + line-height: 20px; + padding: 5px 4px 5px 24px; } } .runnable-err-docs-url { margin-left: 0.5em; cursor: pointer; - font-family: $font-sans; + font-family: $font-system; } .runnable-err-message { - font-family: $monospace; - font-size: 1em; - padding: 10px; + font-family: $font-system; + font-size: 14px; + font-weight: 400; + padding: 10px 0; code { background-color: rgba($black, 0.2); border-radius: 4px; color: $err-code-text; + font-size: 12px; + font-family: $monospace; padding: 2px 5px; } @@ -118,9 +151,10 @@ $code-border-radius: 4px; .runnable-err-stack-expander { align-items: center; - border-top: 1px solid rgba($red-400, 0.1); + border-top: 1px dashed rgba($red-400, 0.1); display: flex; - + padding: 10px 0; + flex-wrap: wrap-reverse; .collapsible-header { flex-grow: 1; @@ -147,7 +181,7 @@ $code-border-radius: 4px; div { cursor: pointer; outline: none; - padding: 14px 10px; + padding: 6px 0; width: 100%; .collapsible-header-text { @@ -184,10 +218,10 @@ $code-border-radius: 4px; div { color: $red-300; cursor: pointer; + font-family: $font-system; font-size: 14px; font-weight: 500; height: 100%; - padding: 14px 10px; width: 100%; &:focus { @@ -224,9 +258,9 @@ $code-border-radius: 4px; .test-err-code-frame { background-color: $gray-1000; - border: 1px solid rgba($red-400, 0.25); + border: 1px dashed rgba(245, 154, 169, 0.1); border-radius: $code-border-radius; - margin: 0 10px 10px; + margin: 0 0 10px; .runnable-err-file-path { background: rgba($gray-900, 0.5); @@ -234,7 +268,7 @@ $code-border-radius: 4px; border-top-right-radius: $code-border-radius; display: block; font-size: 14px; - line-height: 16px; + line-height: 20px; padding: 8px; word-break: break-all; diff --git a/packages/reporter/src/errors/test-error.tsx b/packages/reporter/src/errors/test-error.tsx index 58adae81a397..6c93517f1f19 100644 --- a/packages/reporter/src/errors/test-error.tsx +++ b/packages/reporter/src/errors/test-error.tsx @@ -1,5 +1,6 @@ import _ from 'lodash' import React, { MouseEvent } from 'react' +import cs from 'classnames' import { observer } from 'mobx-react' import Markdown from 'markdown-it' @@ -10,8 +11,7 @@ import ErrorStack from '../errors/error-stack' import events from '../lib/events' import FlashOnClick from '../lib/flash-on-click' import { onEnterOrSpace } from '../lib/util' -import Attempt from '../attempts/attempt-model' -import Command from '../commands/command-model' +import Err from './err-model' import { formattedMessage } from '../commands/command' import WarningIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/warning_x8.svg' @@ -26,28 +26,33 @@ const DocsUrl = ({ url }: DocsUrlProps) => { const urlArray = _.castArray(url) - return (<> - {_.map(urlArray, (url) => ( - - Learn more - - ))} - ) + return _.map(urlArray, (url) => ( + + Learn more + + )) } interface TestErrorProps { - model: Attempt | Command - onPrintToConsole?: () => void + err: Err + testId?: string + commandId?: number + // the command group level to nest the recovered in-test error + groupLevel: number } -const TestError = observer((props: TestErrorProps) => { +const TestError = (props: TestErrorProps) => { + const { err } = props + + if (!err || !err.displayMessage) return null + const md = new Markdown('zero') md.enable(['backticks', 'emphasis', 'escape']) - const onPrint = props.onPrintToConsole || (() => { - events.emit('show:error', props.model) - }) + const onPrint = () => { + events.emit('show:error', props) + } const _onPrintClick = (e: MouseEvent) => { e.stopPropagation() @@ -55,26 +60,35 @@ const TestError = observer((props: TestErrorProps) => { onPrint() } - const { err } = props.model const { codeFrame } = err - if (!err.displayMessage) return null + const groupPlaceholder: Array = [] + + if (err.isRecovered) { + // cap the group nesting to 5 levels to keep the log text legible + for (let i = 0; i < props.groupLevel; i++) { + groupPlaceholder.push() + } + } return ( -
    -
    -
    -
    - - {err.name} -
    -
    -
    - - +
    +
    + {groupPlaceholder} + +
    + {err.name}
    - {codeFrame && } - {err.stack && +
    +
    + {groupPlaceholder} +
    +
    + + +
    + {codeFrame && } + {err.stack && { > - } + } +
    ) -}) +} + +TestError.defaultProps = { + groupLevel: 0, +} -export default TestError +export default observer(TestError) diff --git a/packages/reporter/src/hooks/hook-model.ts b/packages/reporter/src/hooks/hook-model.ts index 8477d6c1c639..a745c2a1a762 100644 --- a/packages/reporter/src/hooks/hook-model.ts +++ b/packages/reporter/src/hooks/hook-model.ts @@ -117,8 +117,8 @@ export default class Hook implements HookProps { this.commands.splice(commandIndex, 1) } - commandMatchingErr (errToMatch: Err) { - return _(this.commands) + commandMatchingErr (errToMatch: Err): CommandModel | undefined { + return _(this.commands) // @ts-ignore .filter(({ err }) => { return err && err.message === errToMatch.message && err.message !== undefined }) diff --git a/packages/reporter/src/lib/events.ts b/packages/reporter/src/lib/events.ts index 31ccfc959503..9339d036f951 100644 --- a/packages/reporter/src/lib/events.ts +++ b/packages/reporter/src/lib/events.ts @@ -4,7 +4,8 @@ import appState, { AppState } from './app-state' import runnablesStore, { RunnablesStore, RootRunnable, LogProps } from '../runnables/runnables-store' import statsStore, { StatsStore } from '../header/stats-store' import scroller, { Scroller } from './scroller' -import TestModel, { UpdatableTestProps, UpdateTestCallback, TestProps } from '../test/test-model' +import { UpdatableTestProps, UpdateTestCallback, TestProps } from '../test/test-model' +import Err from '../errors/err-model' import type { ReporterStartInfo, ReporterRunState } from '@packages/types' @@ -151,13 +152,11 @@ const events: Events = { runner.emit('runner:console:log', testId, logId) }) - localBus.on('show:error', (test: TestModel) => { - const command = test.err.isCommandErr ? test.commandMatchingErr() : null - + localBus.on('show:error', ({ err, testId, commandId }: { err: Err, testId?: string, commandId?: number }) => { runner.emit('runner:console:error', { - err: test.err, - testId: command?.testId, - logId: command?.id, + err, + testId, + logId: commandId, }) }) diff --git a/packages/reporter/src/lib/tag.scss b/packages/reporter/src/lib/tag.scss index 7105b0af79e7..0951e7f94896 100644 --- a/packages/reporter/src/lib/tag.scss +++ b/packages/reporter/src/lib/tag.scss @@ -6,7 +6,7 @@ font-size: 12px; font-style: normal; font-weight: 500; - line-height: initial; + line-height: 18px; height: 18px; max-width: 200px; overflow: hidden; diff --git a/packages/reporter/src/lib/variables.scss b/packages/reporter/src/lib/variables.scss index 6201b66f5df2..5dc085420b5d 100644 --- a/packages/reporter/src/lib/variables.scss +++ b/packages/reporter/src/lib/variables.scss @@ -108,10 +108,11 @@ $yellow-medium: $orange-800; $link-text: $indigo-600; -$err-background: #2F2434; +$err-background: #2C2036; +$err-border: 2px solid $red-300; $err-code-background: rgba($red-400, 0.18); $err-code-text: $red-300; -$err-header-background: #3D2839; +$err-header-background: #3A243B; $err-header-text: $red-300; $err-text: $red-400; @@ -127,5 +128,5 @@ $reporter-contents-min-width: 170px; $font-sans: 'Fira Mono', 'Helvetica Neue', 'Arial', sans-serif; $open-sans: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; -$monospace: Consolas, Monaco, 'Andale Mono', monospace; +$monospace: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; $font-system: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 590b309cf4e1..a3a647b4ece1 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -48,8 +48,6 @@ const reporterEvents = [ // "go:to:file" 'runner:restart', 'runner:abort', - 'runner:console:log', - 'runner:console:error', 'runner:show:snapshot', 'runner:hide:snapshot', 'reporter:restarted', From b20ec54f6876b9f6ebe7cbcec169e92f8d0ac33d Mon Sep 17 00:00:00 2001 From: GitStart <1501599+gitstart@users.noreply.github.com> Date: Tue, 18 Oct 2022 18:48:22 +0100 Subject: [PATCH 02/14] feat: Hide `projectId` section when project is not connected to dashboard (#23823) * chore: Removes projectId section when projectId is not available * chore: Resolve suggested changes * chore: Remove redundant assertion from projectId Co-authored-by: Lachlan Miller Co-authored-by: Bill Glesias --- packages/app/cypress/e2e/settings.cy.ts | 2 +- .../app/src/settings/project/CloudSettings.cy.tsx | 2 +- packages/app/src/settings/project/CloudSettings.vue | 13 ++++++++++++- packages/app/src/settings/project/ProjectId.vue | 6 ------ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/app/cypress/e2e/settings.cy.ts b/packages/app/cypress/e2e/settings.cy.ts index 672e7cfaba25..831a155f19b8 100644 --- a/packages/app/cypress/e2e/settings.cy.ts +++ b/packages/app/cypress/e2e/settings.cy.ts @@ -409,7 +409,7 @@ describe('App: Settings without cloud', () => { cy.visitApp() cy.get(SidebarSettingsLinkSelector).click() cy.findByText('Dashboard settings').click() - cy.findByText('Project ID').should('exist') + cy.findByText('Project ID').should('not.exist') cy.withCtx((ctx, o) => { o.sinon.spy(ctx._apis.authApi, 'logIn') }) diff --git a/packages/app/src/settings/project/CloudSettings.cy.tsx b/packages/app/src/settings/project/CloudSettings.cy.tsx index e04b9a69704a..44ed67b72b05 100644 --- a/packages/app/src/settings/project/CloudSettings.cy.tsx +++ b/packages/app/src/settings/project/CloudSettings.cy.tsx @@ -38,7 +38,7 @@ describe('', () => { }, }) - cy.findByText(defaultMessages.settingsPage.projectId.title).should('be.visible') + cy.findByText(defaultMessages.settingsPage.projectId.title).should('not.exist') cy.findByText(defaultMessages.runs.connect.buttonUser).should('be.visible') cy.findByText(defaultMessages.settingsPage.recordKey.title).should('not.exist') diff --git a/packages/app/src/settings/project/CloudSettings.vue b/packages/app/src/settings/project/CloudSettings.vue index 166dca2f560f..621edf935d88 100644 --- a/packages/app/src/settings/project/CloudSettings.vue +++ b/packages/app/src/settings/project/CloudSettings.vue @@ -1,5 +1,15 @@ + +