Merge branch 'develop' into feature-multidomain

This commit is contained in:
mjhenkes
2022-04-06 09:51:49 -05:00
23 changed files with 761 additions and 197 deletions

View File

@@ -40,7 +40,7 @@ macWorkflowFilters: &mac-workflow-filters
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ feature-multidomain, << pipeline.git.branch >> ]
- equal: [ use-contexts, << pipeline.git.branch >> ]
- equal: [ new-cmd-group-9.x, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>

View File

@@ -5764,6 +5764,7 @@ declare namespace Cypress {
name: string
/** Override *name* for display purposes only */
displayName: string
/** additional information to include in the log */
message: any
/** Set to false if you want to control the finishing of the command in the log yourself */
autoEnd: boolean

View File

@@ -201,7 +201,7 @@ const startXhrServer = (cy, state, config) => {
Cypress.ProxyLogging.addXhrLog({ xhr, route, log, stack })
return log.snapshot('request')
return log?.snapshot('request')
},
onLoad: (xhr) => {

View File

@@ -0,0 +1,42 @@
import { $Command } from '../cypress/command'
import $errUtils from '../cypress/error_utils'
export default (Cypress, userOptions: Cypress.LogGroup.Config, fn: Cypress.LogGroup.ApiCallback) => {
const cy = Cypress.cy
const shouldEmitLog = userOptions.log === undefined ? true : userOptions.log
const options: Cypress.InternalLogConfig = {
...userOptions,
instrument: 'command',
groupStart: true,
emitOnly: !shouldEmitLog,
}
const log = Cypress.log(options)
if (!_.isFunction(fn)) {
$errUtils.throwErrByPath('group.missing_fn', { onFail: log })
}
// An internal command is inserted to create a divider between
// commands inside group() callback and commands chained to it.
const restoreCmdIndex = cy.state('index') + 1
const endLogGroupCmd = $Command.create({
name: 'end-logGroup',
injected: true,
})
const forwardYieldedSubject = () => {
if (log) {
log.endGroup()
}
return endLogGroupCmd.get('prev').get('subject')
}
cy.queue.insert(restoreCmdIndex, endLogGroupCmd.set('fn', forwardYieldedSubject))
return fn(log)
}

View File

@@ -128,7 +128,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy,
const _reject = (err) => {
cleanup()
log.error(err)
log?.error(err)
reject(err)
}

View File

@@ -22,8 +22,8 @@ export function getDisplayUrlMatcher (matcher: RouteMatcherOptions): string {
return $utils.stringify(displayMatcher)
}
export function getRouteMatcherLogConfig (matcher: RouteMatcherOptions, isStubbed: boolean, alias: string | void, staticResponse?: StaticResponse): Partial<Cypress.LogConfig> {
const obj: Partial<Cypress.LogConfig> = {
export function getRouteMatcherLogConfig (matcher: RouteMatcherOptions, isStubbed: boolean, alias: string | void, staticResponse?: StaticResponse): Partial<Cypress.InternalLogConfig> {
const obj: Partial<Cypress.InternalLogConfig> = {
name: 'route',
method: String(matcher.method || '*'),
url: getDisplayUrlMatcher(matcher),

View File

@@ -583,7 +583,9 @@ export default {
docsUrl: 'https://on.cypress.io/go',
},
},
group: {
missing_fn: '`group` API must be called with a function.',
},
hover: {
not_implemented: {
message: [

View File

@@ -112,7 +112,7 @@ export const LogUtils = {
},
}
const defaults = function (state, config, obj) {
const defaults = function (state: Cypress.State, config, obj) {
const instrument = obj.instrument != null ? obj.instrument : 'command'
// dont set any defaults if this
@@ -129,7 +129,7 @@ const defaults = function (state, config, obj) {
// but in cases where the command purposely does not log
// then it could still be logged during a failure, which
// is why we normalize its type value
if (!parentOrChildRe.test(obj.type)) {
if (typeof obj.type === 'string' && !parentOrChildRe.test(obj.type)) {
// does this command have a previously linked command
// by chainer id
obj.type = (current != null ? current.hasPreviouslyLinkedCommand() : undefined) ? 'child' : 'parent'
@@ -206,18 +206,18 @@ const defaults = function (state, config, obj) {
},
})
const logGroup = _.last(state('logGroup'))
const logGroupIds = state('logGroupIds') || []
if (logGroup) {
obj.group = logGroup
if (logGroupIds.length) {
obj.group = _.last(logGroupIds)
}
if (obj.groupEnd) {
state('logGroup', _.slice(state('logGroup'), 0, -1))
state('logGroupIds', _.slice(logGroupIds, 0, -1))
}
if (obj.groupStart) {
state('logGroup', (state('logGroup') || []).concat(obj.id))
state('logGroupIds', (logGroupIds).concat(obj.id))
}
return obj
@@ -225,7 +225,7 @@ const defaults = function (state, config, obj) {
class Log {
cy: any
state: any
state: Cypress.State
config: any
fireChangeEvent: ((log) => (void | undefined))
obj: any
@@ -236,7 +236,8 @@ class Log {
this.cy = cy
this.state = state
this.config = config
this.fireChangeEvent = fireChangeEvent
// only fire the log:state:changed event as fast as every 4ms
this.fireChangeEvent = _.debounce(fireChangeEvent, 4)
this.obj = defaults(state, config, obj)
extendEvents(this)
@@ -376,6 +377,13 @@ class Log {
}
error (err) {
const logGroupIds = this.state('logGroupIds') || []
// current log was responsible to creating the current log group so end the current group
if (_.last(logGroupIds) === this.attributes.id) {
this.endGroup()
}
this.set({
ended: true,
error: err,
@@ -404,6 +412,10 @@ class Log {
return this
}
endGroup () {
this.state('logGroupIds', _.slice(this.state('logGroupIds'), 0, -1))
}
getError (err) {
return err.stack || err.message
}
@@ -561,16 +573,8 @@ class LogManager {
this.logs[id] = true
}
// only fire the log:state:changed event
// as fast as every 4ms
fireChangeEvent (log) {
const triggerStateChanged = () => {
return this.trigger(log, 'command:log:changed')
}
const debounceFn = _.debounce(triggerStateChanged, 4)
return debounceFn()
return this.trigger(log, 'command:log:changed')
}
createLogFn (cy, state, config) {

View File

@@ -50,7 +50,7 @@ function getDisplayUrl (url: string) {
return url
}
function getDynamicRequestLogConfig (req: Omit<ProxyRequest, 'log'>): Partial<Cypress.LogConfig> {
function getDynamicRequestLogConfig (req: Omit<ProxyRequest, 'log'>): Partial<Cypress.InternalLogConfig> {
const last = _.last(req.interceptions)
let alias = last ? last.interception.request.alias || last.route.alias : undefined
@@ -64,7 +64,7 @@ function getDynamicRequestLogConfig (req: Omit<ProxyRequest, 'log'>): Partial<Cy
}
}
function getRequestLogConfig (req: Omit<ProxyRequest, 'log'>): Partial<Cypress.LogConfig> {
function getRequestLogConfig (req: Omit<ProxyRequest, 'log'>): Partial<Cypress.InternalLogConfig> {
function getStatus (): string | undefined {
const { stubbed, reqModified, resModified } = req.flags
@@ -392,7 +392,7 @@ export default class ProxyLogging {
const proxyRequest = new ProxyRequest(preRequest)
const logConfig = getRequestLogConfig(proxyRequest as Omit<ProxyRequest, 'log'>)
proxyRequest.log = this.Cypress.log(logConfig).snapshot('request')
proxyRequest.log = this.Cypress.log(logConfig)?.snapshot('request')
this.proxyRequests.push(proxyRequest as ProxyRequest)

View File

@@ -1774,10 +1774,9 @@ export default {
test = getTestById(testId)
if (test) {
// pluralize the instrument
// as a property on the runnable
let name
const logs = test[name = `${instrument}s`] != null ? test[name] : (test[name] = [])
// pluralize the instrument as a property on the runnable
const name = `${instrument}s`
const logs = test[name] != null ? test[name] : (test[name] = [])
// else push it onto the logs
return logs.push(attrs)

27
packages/driver/types/cy/logGroup.d.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
// The type declarations for Cypress Log Group & the corresponding configuration permutations
declare namespace Cypress {
declare namespace LogGroup {
type ApiCallback = (log: Cypress.Log) => Chainable<S>
type LogGroup = (cypress: Cypress.Cypress, options: Partial<LogGroupConfig>, callback: LogGroupCallback) => Chainable<S>
interface Config {
// the JQuery element for the command. This will highlight the command
// in the main window when debugging
$el?: JQuery
// whether or not to emit a log to the UI
// when disabled, child logs will not be nested in the UI
log?: boolean
// name of the group - defaults to current command's name
name?: string
// additional information to include in the log
message?: string
// timeout of the group command - defaults to defaultCommandTimeout
timeout?: number
// the type of log
// system - log generated by Cypress
// parent - log generated by Command
// child - log generated by Chained Command
type?: Cypress.InternalLogConfig['type']
}
}
}

119
packages/driver/types/cypress/log.d.ts vendored Normal file
View File

@@ -0,0 +1,119 @@
// The type declarations for Cypress Logs & the corresponding configuration permutations
declare namespace Cypress {
interface Cypress {
log(options: Partial<LogConfig | InternalLogConfig>): Log | undefined
}
interface Log extends Log {
set<K extends keyof LogConfig | InternalLogConfig>(key: K, value: LogConfig[K]): InternalLog
set(options: Partial<LogConfig | InternalLogConfig>)
groupEnd(): void
}
type ReferenceAlias = {
cardinal: number,
name: string,
ordinal: string,
}
type Snapshot = {
body?: {get: () => any},
htmlAttrs?: {[key: string]: any},
name?: string
}
type ConsoleProps = {
Command?: string
Snapshot?: string
Elements?: number
Selector?: string
Yielded?: HTMLElement
Event?: string
Message?: string
actual?: any
expected?: any
}
type RenderProps = {
indicator?: 'aborted' | 'pending' | 'successful' | 'bad'
message?: string
}
interface InternalLogConfig {
// the JQuery element for the command. This will highlight the command
// in the main window when debugging
$el?: JQuery | string
alias?: string
aliasType?: 'agent' | 'route' | 'primitive' | 'dom' | undefined
browserPreRequest?: any
callCount?: number
chainerId?: string
commandName?: string
// provide the content to display in the dev tool's console when a log is
// clicked from the Reporter's Command Log
consoleProps?: () => Command | Command
coords?: {
left: number
leftCenter: number
top: number
topCenter: number
x: number
y: number
}
count?: number
// the name override for display purposes only
displayName?: string
// whether or not to show the log in the Reporter UI or only
// store the log details on the command and log manager
emitOnly?: boolean
end?: boolean
ended?: boolean
err?: Error
error?: Error
// whether or not the generated log was an event or command
event?: boolean
expected?: string
functionName?: string
// whether or not to start a new log group
groupStart?: boolean
hookId?: number
id?: string
// defaults to command
instrument?: 'agent' | 'command' | 'route'
// whether or not the xhr route had a corresponding response stubbed out
isStubbed?: boolean
// additional information to include in the log if not overridden
// the render props message
// defaults to command arguments for command instrument
message?: string | Array<string> | any[]
method?: string
// name of the log
name?: string
numElements?: number
// the number of xhr responses that occurred. This is only applicable to
// logs defined with instrument=route
numResponses?: number
referencesAlias?: ReferenceAlias[]
renderProps?: () => RenderProps | RenderProps
response?: string | object
selector?: any
snapshot?: boolean
snapshots?: []
state?: "failed" | "passed" | "pending" // representative of Mocha.Runnable.constants (not publicly exposed by Mocha types)
status?: number
testCurrentRetry?: number
testId?: string
// timeout of the group command - defaults to defaultCommandTimeout
timeout?: number
// the type of log
// system - log generated by Cypress
// parent - log generated by Command
// child - log generated by Chained Command
type?: 'system' | 'parent' | 'child' | ((current: State['state']['current'], subject: State['state']['subject']) => 'parent' | 'child')
url?: string
viewportHeight?: number
viewportWidth?: number
visible?: boolean
wallClockStartedAt?: string
}
}

View File

@@ -1,5 +1,7 @@
// NOTE: this is for internal Cypress types that we don't want exposed in the public API but want for development
// TODO: find a better place for this
/// <reference path="./cy/logGroup.d.ts" />
/// <reference path="./cypress/log.d.ts" />
interface InternalWindowLoadDetails {
type: 'same:domain' | 'cross:domain' | 'cross:domain:failure'
@@ -73,97 +75,14 @@ declare namespace Cypress {
warning: (message: string) => void
}
type Log = ReturnType<Cypress.log>
type ReferenceAlias = {
cardinal: number,
name: string,
ordinal: string,
}
type Snapshot = {
body?: {get: () => any},
htmlAttrs?: {[key: string]: any},
name?: string
}
type ConsoleProps = {
Command?: string
Snapshot?: string
Elements?: number
Selector?: string
Yielded?: HTMLElement
Event?: string
Message?: string
actual?: any
expected?: any
}
type RenderProps = {
indicator?: 'aborted' | 'pending' | 'successful' | 'bad'
message?: string
}
interface LogConfig {
$el?: jQuery<any> | string
message: any
instrument?: 'route' | 'command'
isStubbed?: boolean
alias?: string
aliasType?: 'route'
referencesAlias?: ReferenceAlias[]
chainerId?: string
commandName?: string
coords?: {
left: number
leftCenter: number
top: number
topCenter: number
x: number
y: number
}
count?: number
callCount?: number
type?: 'parent' | 'child'
event?: boolean
end?: boolean
ended?: boolean
expected?: string
functionName?: string
name?: string
id?: string
hookId?: number
method?: string
url?: string
status?: number
state?: "failed" | "passed" | "pending" // representative of Mocha.Runnable.constants (not publicly exposed by Mocha types)
numResponses?: number
numElements?: number
numResponses?: number
response?: string | object
testCurrentRetry?: number
timeout?: number
testId?: string
err?: Error
error?: Error
snapshot?: boolean
snapshots?: []
selector?: any
viewportHeight?: number
viewportWidth?: number
visible?: boolean
wallClockStartedAt?: string
renderProps?: () => RenderProps | RenderProps
consoleProps?: () => Command | Command
browserPreRequest?: any
}
// Extend Cypress.state properties here
interface State {
(k: '$autIframe', v?: JQuery<HTMLIFrameElement>): JQuery<HTMLIFrameElement> | undefined
(k: 'routes', v?: RouteMap): RouteMap
(k: 'aliasedRequests', v?: AliasedRequest[]): AliasedRequest[]
(k: 'document', v?: Document): Document
(k: 'window', v?: Window): Window
(k: 'logGroupIds', v?: Array<InternalLogConfig['id']>): Array<InternalLogConfig['id']>
(k: string, v?: any): any
state: Cypress.state
}
@@ -172,7 +91,6 @@ declare namespace Cypress {
(k: keyof ResolvedConfigOptions, v?: any): any
}
// Extend Cypress.state properties here
interface ResolvedConfigOptions {
$autIframe: JQuery<HTMLIFrameElement>
document: Document

View File

@@ -2,7 +2,7 @@
"projectId": "ypt4pf",
"baseUrl": "http://localhost:5006",
"viewportWidth": 400,
"viewportHeight": 450,
"viewportHeight": 1000,
"reporter": "../../node_modules/cypress-multi-reporters/index.js",
"reporterOptions": {
"configFile": "../../mocha-reporter-config.json"

View File

@@ -154,6 +154,18 @@
"testId": "r3",
"timeout": 4000,
"type": "parent"
},
{
"hookId": "r3",
"id": 240,
"instrument": "command",
"message": "System Event Command",
"name": "cmd",
"state": "passed",
"event": true,
"testId": "r3",
"timeout": 4000,
"type": "system"
}
],
"invocationDetails": {

View File

@@ -281,7 +281,7 @@ describe('aliases', () => {
cy.get('.command-wrapper')
.first()
.within(() => {
cy.get('.num-children').should('not.be.visible')
cy.get('.num-children').should('not.exist')
cy.contains('.command-interceptions', 'getPosts')
})
@@ -535,7 +535,7 @@ describe('aliases', () => {
cy.get('.command-wrapper')
.first()
.within(() => {
cy.get('.num-children').should('not.be.visible')
cy.get('.num-children').should('not.exist')
cy.contains('.command-alias', 'dropdown')
})

View File

@@ -42,7 +42,143 @@ describe('commands', () => {
})
it('displays all the commands', () => {
cy.get('.command').should('have.length', 9)
addCommand(runner, {
id: 102,
name: 'get',
message: '#element',
state: 'passed',
timeout: 4000,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 124,
name: 'within',
state: 'passed',
type: 'child',
timeout: 4000,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 125,
name: 'get',
message: '#my_element',
state: 'passed',
timeout: 4000,
group: 124,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 129,
name: 'within',
state: 'passed',
type: 'child',
group: 124,
timeout: 4000,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 130,
name: 'get',
message: '#my_element that _has_ a really long message to show **wrapping** works as expected',
state: 'passed',
timeout: 4000,
groupLevel: 1,
group: 129,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 1229,
name: 'within',
state: 'passed',
type: 'child',
group: 130,
groupLevel: 1,
timeout: 4000,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 1311,
name: 'get',
message: '#my_element_nested',
state: 'passed',
timeout: 4000,
groupLevel: 2,
group: 1229,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 1291,
name: 'assert',
type: 'child',
message: 'has class named .omg',
state: 'passed',
timeout: 4000,
group: 1229,
groupLevel: 2,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 1291,
name: 'log',
message: 'do something else',
state: 'passed',
timeout: 4000,
group: 130,
groupLevel: 1,
wallClockStartedAt: inProgressStartedAt,
})
addCommand(runner, {
id: 135,
name: 'and',
type: 'child',
message: 'has class named .lets-roll',
state: 'passed',
timeout: 4000,
group: 124,
wallClockStartedAt: inProgressStartedAt,
})
const indicators = ['successful', 'pending', 'aborted', 'bad']
indicators.forEach((indicator, index) => {
addCommand(runner, {
id: 1600 + index,
name: 'xhr',
event: true,
state: 'passed',
timeout: 4000,
renderProps: {
indicator,
message: `${indicator} indicator`,
},
wallClockStartedAt: inProgressStartedAt,
})
})
const assertStates = ['passed', 'pending', 'failed']
assertStates.forEach((state, index) => {
addCommand(runner, {
id: 1700 + index,
name: 'assert',
type: 'child',
message: 'expected **element** to have length of **16** but got **12** instead',
state,
timeout: 4000,
wallClockStartedAt: inProgressStartedAt,
})
})
cy.get('.command').should('have.length', 27)
cy.percySnapshot()
})
@@ -61,6 +197,12 @@ describe('commands', () => {
})
it('includes the state class', () => {
addCommand(runner, {
name: 'log',
message: 'command-warning-state',
state: 'warning',
})
cy.contains('#exists').closest('.command')
.should('have.class', 'command-state-passed')
@@ -69,6 +211,9 @@ describe('commands', () => {
cy.contains('#in-progress').closest('.command')
.should('have.class', 'command-state-pending')
cy.contains('command-warning-state').closest('.command')
.should('have.class', 'command-state-warning')
})
it('displays the number', () => {
@@ -114,13 +259,42 @@ describe('commands', () => {
})
it('shows indicator when specified', () => {
cy.contains('GET ---').closest('.command').find('.command-message .fa-circle')
.should('be.visible')
const indicators = ['successful', 'pending', 'aborted', 'bad']
indicators.forEach((indicator) => {
addCommand(runner, {
name: 'xhr',
event: true,
renderProps: {
indicator,
message: `${indicator} indicator`,
},
})
})
indicators.forEach((indicator) => {
cy.contains(`${indicator} indicator`).closest('.command').find('.command-message .fa-circle')
.should('be.visible')
})
cy.percySnapshot()
})
it('includes the renderProps indicator as a class name when specified', () => {
cy.contains('Lorem ipsum').closest('.command').find('.command-message .fa-circle')
.should('have.class', 'bad')
it('assert commands for each state', () => {
const assertStates = ['passed', 'pending', 'failed']
assertStates.forEach((state) => {
addCommand(runner, {
name: 'assert',
type: 'child',
message: `expected **element** to have **state of ${state}**`,
state,
})
})
cy.get('.command').should('have.length', 13)
cy.percySnapshot()
})
describe('progress bar', () => {
@@ -218,14 +392,16 @@ describe('commands', () => {
})
})
context('duplicates', () => {
it('collapses consecutive duplicate events into one', () => {
context('event duplicates', () => {
it('collapses consecutive duplicate events into group', () => {
cy.get('.command-name-xhr').should('have.length', 3)
})
it('displays number of duplicates', () => {
cy.contains('GET --- /dup').closest('.command').find('.num-children')
.should('have.text', '4')
.trigger('mouseover')
.get('.cy-tooltip').should('have.text', 'This event occurred 4 times')
})
it('expands all events after clicking arrow', () => {
@@ -328,6 +504,133 @@ describe('commands', () => {
})
})
context('command group', () => {
let groupId
beforeEach(() => {
groupId = addCommand(runner, {
name: 'group',
message: 'example group command',
type: 'parent',
})
addCommand(runner, {
name: 'get',
message: '#my_nested_element',
group: groupId,
})
})
it('group is open by default when all nested command have passed', () => {
addCommand(runner, {
name: 'log',
message: 'chained log example',
})
cy.contains('chained log example') // ensure test content has loaded
cy.get('.command-name-group')
.should('have.class', 'command-is-open')
.find('.command-expander')
.should('be.visible')
.closest('.command-name-group')
.click()
cy.get('.command-name-group')
.should('not.have.class', 'command-is-open')
cy.get('.command-name-group')
.find('.num-children')
.should('have.text', '1')
.trigger('mouseover')
.get('.cy-tooltip').should('have.text', '1 log currently hidden')
.percySnapshot()
})
it('group is open by default when last nested command failed', () => {
addCommand(runner, {
name: 'log',
message: 'chained log example',
state: 'failed',
group: groupId,
})
cy.contains('chained log example') // ensure test content has loaded
cy.get('.command-name-group')
.should('have.class', 'command-is-open')
.find('.command-expander')
.should('be.visible')
.closest('.command-name-group')
.click()
cy.get('.command-name-group')
.find('.num-children')
.should('not.exist')
.percySnapshot()
})
it('clicking opens and closes the group', () => {
cy.get('.command-name-group')
.find('.num-children')
.should('not.exist')
cy.get('.command-name-group')
.should('have.class', 'command-is-open')
.find('.command-expander')
.should('be.visible')
.closest('.command-name-group')
.click()
cy.get('.command-name-group')
.find('.num-children')
.should('be.visible')
.should('have.text', '1')
cy.get('.command-name-group')
.should('not.have.class', 'command-is-open')
})
it('displays with nested logs', () => {
const nestedGroupId = addCommand(runner, {
name: 'group-2',
state: 'passed',
type: 'child',
group: groupId,
})
addCommand(runner, {
name: 'get',
message: '#my_element_nested',
state: 'passed',
group: nestedGroupId,
})
addCommand(runner, {
name: 'assert',
type: 'child',
message: 'has class named .omg',
group: nestedGroupId,
})
addCommand(runner, {
name: 'log',
message: 'chained log example',
state: 'passed',
group: groupId,
})
cy.get('.command-name-group')
.should('have.class', 'command-is-open')
cy.get('.command-name-group-2')
.should('have.class', 'command-is-open')
.click()
cy.percySnapshot()
})
})
context('studio commands', () => {
beforeEach(() => {
addCommand(runner, {

View File

@@ -27,6 +27,110 @@ describe('Command model', () => {
clock.restore()
})
context('.visible', () => {
let command: CommandModel
it('sets visible to true for command has visible elements associated to it', () => {
command = new CommandModel(commandProps({ visible: true }))
expect(command.visible).to.be.true
})
it('sets visible to false for command has hidden elements associated to it', () => {
command = new CommandModel(commandProps({ visible: false }))
expect(command.visible).to.be.false
})
it('sets visible to true for command that does not associate with visibility', () => {
command = new CommandModel(commandProps({ visible: undefined }))
expect(command.visible).to.be.true
})
})
context('.numChildren', () => {
context('event log', () => {
it('with no children', () => {
const command = new CommandModel(commandProps({ event: true }))
expect(command.numChildren).to.eq(1)
})
it('with children', () => {
const command = new CommandModel(commandProps({ event: true }))
command.addChild(new CommandModel(commandProps()))
expect(command.numChildren).to.eq(2)
command.addChild(new CommandModel(commandProps()))
expect(command.numChildren).to.eq(3)
})
})
context('command log', () => {
it('with no children', () => {
const command = new CommandModel(commandProps({}))
expect(command.numChildren).to.eq(0)
})
it('with children', () => {
const command = new CommandModel(commandProps({}))
command.addChild(new CommandModel(commandProps()))
expect(command.numChildren).to.eq(1)
command.addChild(new CommandModel(commandProps()))
expect(command.numChildren).to.eq(2)
})
it('with children that are a command group', () => {
const command = new CommandModel(commandProps({}))
command.addChild(new CommandModel(commandProps()))
const commandGroup = new CommandModel(commandProps())
commandGroup.addChild(new CommandModel(commandProps()))
commandGroup.addChild(new CommandModel(commandProps()))
command.addChild(commandGroup)
expect(command.numChildren).to.eq(4)
})
})
})
context('.hasChildren', () => {
context('event log', () => {
it('with no children', () => {
const command = new CommandModel(commandProps({ event: true }))
expect(command.hasChildren).to.be.false
})
it('with one or more children', () => {
const command = new CommandModel(commandProps({ event: true }))
command.addChild(new CommandModel(commandProps()))
expect(command.hasChildren).to.be.true
})
})
context('command log', () => {
it('with no children', () => {
const command = new CommandModel(commandProps({}))
expect(command.hasChildren).to.be.false
})
it('with one or more children', () => {
const command = new CommandModel(commandProps({}))
command.addChild(new CommandModel(commandProps()))
expect(command.hasChildren).to.be.true
})
})
})
context('.isLongRunning', () => {
describe('when model is pending on initialization and LONG_RUNNING_THRESHOLD passes', () => {
let command: CommandModel
@@ -46,26 +150,26 @@ describe('Command model', () => {
expect(command.isLongRunning).to.be.false
})
})
})
describe('when model is not pending on initialization, is updated to pending, and LONG_RUNNING_THRESHOLD passes', () => {
let command: CommandModel
describe('when model is not pending on initialization, is updated to pending, and LONG_RUNNING_THRESHOLD passes', () => {
let command: CommandModel
beforeEach(() => {
command = new CommandModel(commandProps({ state: null }))
clock.tick(300)
command.update({ state: 'pending' } as CommandProps)
})
beforeEach(() => {
command = new CommandModel(commandProps({ state: null }))
clock.tick(300)
command.update({ state: 'pending' } as CommandProps)
})
it('sets isLongRunning to true if model is still pending', () => {
clock.tick(LONG_RUNNING_THRESHOLD)
expect(command.isLongRunning).to.be.true
})
it('sets isLongRunning to true if model is still pending', () => {
clock.tick(LONG_RUNNING_THRESHOLD)
expect(command.isLongRunning).to.be.true
})
it('does not set isLongRunning to true if model is no longer pending', () => {
command.state = 'passed'
clock.tick(LONG_RUNNING_THRESHOLD)
expect(command.isLongRunning).to.be.false
it('does not set isLongRunning to true if model is no longer pending', () => {
command.state = 'passed'
clock.tick(LONG_RUNNING_THRESHOLD)
expect(command.isLongRunning).to.be.false
})
})
})
})

View File

@@ -158,10 +158,16 @@ export const addCommand = (runner: EventEmitter, log: Partial<CommandModel>) =>
state: 'passed',
testId: 'r3',
testCurrentRetry: 0,
timeout: 4000,
type: 'parent',
url: 'http://example.com',
hasConsoleProps: true,
}
runner.emit('reporter:log:add', Object.assign(defaultLog, log))
const commandLog = Object.assign(defaultLog, log)
runner.emit('reporter:log:add', commandLog)
// return command log id to enable adding new command to command group
return commandLog.id
}

View File

@@ -9,7 +9,7 @@ const LONG_RUNNING_THRESHOLD = 1000
interface RenderProps {
message?: string
indicator?: string
indicator?: 'successful' | 'pending' | 'aborted' | 'bad'
interceptions?: Array<{
command: 'intercept' | 'route'
alias?: string
@@ -34,7 +34,6 @@ export interface CommandProps extends InstrumentProps {
group?: number
hasSnapshot?: boolean
hasConsoleProps?: boolean
}
export default class Command extends Instrument {
@@ -45,10 +44,9 @@ export default class Command extends Instrument {
@observable number?: number
@observable numElements: number
@observable timeout?: number
@observable visible?: boolean = true
@observable visible?: boolean
@observable wallClockStartedAt?: string
@observable children: Array<Command> = []
@observable isChild = false
@observable hookId: string
@observable isStudio: boolean
@observable showError?: boolean = false
@@ -64,9 +62,21 @@ export default class Command extends Instrument {
return this.renderProps.message || this.message
}
private countNestedCommands (children) {
if (children.length === 0) {
return 0
}
return children.length + children.reduce((previousValue, child) => previousValue + this.countNestedCommands(child.children), 0)
}
@computed get numChildren () {
// and one to include self so it's the total number of same events
return this.children.length + 1
if (this.event) {
// add one to include self so it's the total number of same events
return this.children.length + 1
}
return this.countNestedCommands(this.children)
}
@computed get isOpen () {
@@ -75,6 +85,7 @@ export default class Command extends Instrument {
return this._isOpen || (this._isOpen === null
&& (
(this.group && this.type === 'system' && this.hasChildren) ||
(this.hasChildren && !this.event && this.type !== 'system') ||
_.some(this.children, (v) => v.hasChildren) ||
_.last(this.children)?.isOpen ||
(_.some(this.children, (v) => v.isLongRunning) && _.last(this.children)?.state === 'pending') ||
@@ -88,7 +99,13 @@ export default class Command extends Instrument {
}
@computed get hasChildren () {
return this.numChildren > 1
if (this.event) {
// if the command is an event log, we add one to the number of children count to include
// itself in the total number of same events that render when the group is closed
return this.numChildren > 1
}
return this.numChildren > 0
}
constructor (props: CommandProps) {
@@ -100,15 +117,16 @@ export default class Command extends Instrument {
this.numElements = props.numElements
this.renderProps = props.renderProps || {}
this.timeout = props.timeout
this.visible = props.visible
// command log that are not associated with elements will not have a visibility
// attribute set. i.e. cy.visit(), cy.readFile() or cy.log()
this.visible = props.visible === undefined || props.visible
this.wallClockStartedAt = props.wallClockStartedAt
this.hookId = props.hookId
this.isStudio = !!props.isStudio
this.showError = props.showError
this.showError = !!props.showError
this.group = props.group
this.hasSnapshot = props.hasSnapshot
this.hasConsoleProps = props.hasConsoleProps
this.hasSnapshot = !!props.hasSnapshot
this.hasConsoleProps = !!props.hasConsoleProps
this._checkLongRunning()
}
@@ -119,7 +137,9 @@ export default class Command extends Instrument {
this.event = props.event
this.numElements = props.numElements
this.renderProps = props.renderProps || {}
this.visible = props.visible
// command log that are not associated with elements will not have a visibility
// attribute set. i.e. cy.visit(), cy.readFile() or cy.log()
this.visible = props.visible === undefined || props.visible
this.timeout = props.timeout
this.hasSnapshot = props.hasSnapshot
this.hasConsoleProps = props.hasConsoleProps
@@ -142,7 +162,6 @@ export default class Command extends Instrument {
}
addChild (command: Command) {
command.isChild = true
command.setGroup(this.id)
this.children.push(command)
}

View File

@@ -22,13 +22,22 @@ const md = new Markdown()
const displayName = (model: CommandModel) => model.displayName || model.name
const nameClassName = (name: string) => name.replace(/(\s+)/g, '-')
const formattedMessage = (message: string) => message ? md.renderInline(message) : ''
const visibleMessage = (model: CommandModel) => {
if (model.visible) return ''
const invisibleMessage = (model: CommandModel) => {
if (model.visible) {
return ''
}
return model.numElements > 1 ?
'One or more matched elements are not visible.' :
'This element is not visible.'
}
const numberOfChildrenMessage = (numChildren, event?: boolean) => {
if (event) {
return `This event occurred ${numChildren} times`
}
return `${numChildren} ${numChildren > 1 ? 'logs' : 'log'} currently hidden`
}
const shouldShowCount = (aliasesWithDuplicates: Array<Alias> | null, aliasName: Alias, model: CommandModel) => {
if (model.aliasType !== 'route') {
@@ -216,17 +225,22 @@ class Command extends Component<Props> {
return <TestError model={model} onPrintToConsole={this._onClick}/>
}
const commandName = model.name ? nameClassName(model.name) : ''
const isSystemEvent = model.type === 'system' && model.event
const isSessionCommand = commandName === 'session'
const displayNumOfChildren = !isSystemEvent && !isSessionCommand && model.hasChildren && !model.isOpen
return (
<li
className={cs(
'command',
`command-name-${model.name ? nameClassName(model.name) : ''}`,
`command-name-${commandName}`,
`command-state-${model.state}`,
`command-type-${model.type}`,
{
'command-is-studio': model.isStudio,
'command-is-event': !!model.event,
'command-is-invisible': model.visible != null && !model.visible,
'command-is-invisible': !model.visible,
'command-has-num-elements': model.state !== 'pending' && model.numElements != null,
'command-is-pinned': this._isPinned(),
'command-with-indicator': !!model.renderProps.indicator,
@@ -236,7 +250,6 @@ class Command extends Component<Props> {
'command-has-console-props': model.hasConsoleProps,
'multiple-elements': model.numElements > 1,
'command-has-children': model.hasChildren,
'command-is-child': model.isChild,
'command-is-open': this._isOpen(),
},
)}
@@ -258,9 +271,11 @@ class Command extends Component<Props> {
<i className='fas fa-spinner fa-spin' />
<span>{model.number || ''}</span>
</span>
<span className='command-pin'>
<i className='fas fa-thumbtack' />
</span>
{!model.hasChildren && (
<span className='command-pin'>
<i className='fas fa-thumbtack' />
</span>
)}
<span className='command-method'>
<span>{model.event && model.type !== 'system' ? `(${displayName(model)})` : displayName(model)}</span>
</span>
@@ -269,7 +284,7 @@ class Command extends Component<Props> {
</span>
<span className='command-controls'>
<i className='far fa-times-circle studio-command-remove' onClick={this._removeStudioCommand} />
<Tooltip placement='top' title={visibleMessage(model)} className='cy-tooltip'>
<Tooltip placement='top' title={invisibleMessage(model)} className='cy-tooltip'>
<i className='command-invisible far fa-eye-slash' />
</Tooltip>
<Tooltip placement='top' title={`${model.numElements} matched elements`} className='cy-tooltip'>
@@ -278,9 +293,11 @@ class Command extends Component<Props> {
<span className='alias-container'>
<Interceptions model={model} />
<Aliases model={model} aliasesWithDuplicates={aliasesWithDuplicates} isOpen={this._isOpen()} />
<Tooltip placement='top' title={`This event occurred ${model.numChildren} times`} className='cy-tooltip'>
<span className={cs('num-children', { 'has-alias': model.alias, 'has-children': model.numChildren > 1 })}>{model.numChildren}</span>
</Tooltip>
{displayNumOfChildren && (
<Tooltip placement='top' title={numberOfChildrenMessage(model.numChildren, model.event)} className='cy-tooltip'>
<span className={cs('num-children', { 'has-alias': model.alias, 'has-children': model.numChildren > 1 })}>{model.numChildren}</span>
</Tooltip>
)}
</span>
</span>
@@ -376,6 +393,13 @@ class Command extends Component<Props> {
_snapshot (show: boolean) {
const { model, runnablesStore } = this.props
// do not trigger the show:snapshot event for commands groups
// TODO: remove this behavior in 10.0+ when a group
// can both be expanded and collapsed and pinned
if (model.hasChildren) {
return
}
if (show) {
runnablesStore.attemptingShowSnapshot = true

View File

@@ -10,6 +10,7 @@
}
.command {
background-color: #eef1f4;
cursor: default;
margin: 0;
}
@@ -82,23 +83,12 @@
border-top: 0;
}
.command-type-child {
.command-method {
&:before {
float: left;
content: "-";
margin-right: 2px;
padding-left: 5px;
}
}
}
.command-type-system.command-is-event,
.command-name-session {
> span > .command-wrapper {
.num-children {
display: none;
}
.command-type-child > span > div > div > .command-method {
&:before {
float: left;
content: "-";
margin-right: 2px;
padding-left: 5px;
}
}
@@ -527,18 +517,12 @@
font-style: normal;
}
.command-has-num-elements .num-elements,
.num-children,
.command-has-children.command-is-open
> span
> .command-wrapper
.num-children.has-children {
.command-has-num-elements .num-elements {
display: none;
}
.command-has-num-elements.no-elements .num-elements,
.command-has-num-elements.multiple-elements .num-elements,
.command-has-children .num-children.has-children {
.command-has-num-elements.multiple-elements .num-elements {
display: inline;
}

View File

@@ -6,7 +6,6 @@
.num-children {
border-radius: 5px;
color: #fff;
display: none;
font-size: 85%;
line-height: 1;
margin-left: 5px;
@@ -18,6 +17,7 @@
.num-elements {
background-color: #ababab;
display: none;
}
.num-children {