Skip to content

Commit 9ba3ed3

Browse files
author
Blue F
authored
chore: Refactor assertion logging (#23354)
* chore: Refactor assertion logging * Remove duplicate snapshot on command failure; Prevent circular json error when inspecting commands * Fix for 'next' attribute in logs * Add comments about logging and assertions * Fix duplicate logs on not.exist assertions * Move command-merging logic into asserting.ts * Fix TS error * Fix race condition with cross-origin tests * Review updates
1 parent ab23d77 commit 9ba3ed3

File tree

8 files changed

+141
-218
lines changed

8 files changed

+141
-218
lines changed

packages/driver/cypress/e2e/commands/assertions.cy.js

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,15 @@ describe('src/cy/commands/assertions', () => {
329329
it('resolves eventually not exist', () => {
330330
const button = cy.$$('button:first')
331331

332-
cy.on('command:retry', _.after(2, _.once(() => {
332+
cy.on('command:retry', _.after(3, _.once(() => {
333333
button.remove()
334334
})))
335335

336336
cy.get('button:first').click().should('not.exist')
337+
338+
cy.then(function () {
339+
assertLogLength(this.logs, 3)
340+
})
337341
})
338342

339343
it('resolves all 3 assertions', (done) => {
@@ -715,7 +719,6 @@ describe('src/cy/commands/assertions', () => {
715719
it('does not log ensureElExistence errors', function (done) {
716720
cy.on('fail', (err) => {
717721
assertLogLength(this.logs, 1)
718-
719722
done()
720723
})
721724

@@ -790,19 +793,18 @@ describe('src/cy/commands/assertions', () => {
790793
cy.noop({}).should('have.property', 'foo')
791794
})
792795

793-
it('ends and snapshots immediately and sets child', (done) => {
796+
it('snapshots immediately and sets child', (done) => {
794797
cy.on('log:added', (attrs, log) => {
795-
if (attrs.name === 'assert') {
796-
cy.removeAllListeners('log:added')
798+
if (attrs.name !== 'assert') {
799+
return
800+
}
797801

798-
expect(log.get('ended')).to.be.true
799-
expect(log.get('state')).to.eq('passed')
800-
expect(log.get('snapshots').length).to.eq(1)
801-
expect(log.get('snapshots')[0]).to.be.an('object')
802-
expect(log.get('type')).to.eq('child')
802+
cy.removeAllListeners('log:added')
803+
expect(log.get('snapshots').length).to.eq(1)
804+
expect(log.get('snapshots')[0]).to.be.an('object')
805+
expect(log.get('type')).to.eq('child')
803806

804-
done()
805-
}
807+
done()
806808
})
807809

808810
cy.get('body').then(() => {

packages/driver/src/cy/assertions.ts

Lines changed: 9 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,11 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
222222
return null
223223
}
224224

225-
const finishAssertions = (assertions) => {
226-
return _.each(assertions, (log) => {
227-
log.snapshot()
225+
const finishAssertions = () => {
226+
cy.state('current').get('logs').forEach((log) => {
227+
if (log.get('next') || !log.get('snapshots')) {
228+
log.snapshot()
229+
}
228230

229231
const e = log.get('_error')
230232

@@ -238,7 +240,6 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
238240

239241
type VerifyUpcomingAssertionsCallbacks = {
240242
ensureExistenceFor?: 'subject' | 'dom' | boolean
241-
onPass?: Function
242243
onFail?: (err?, isDefaultAssertionErr?: boolean, cmds?: any[]) => void
243244
onRetry?: () => any
244245
}
@@ -256,10 +257,6 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
256257
// case where there are no upcoming assertion commands
257258
const isDefaultAssertionErr = cmds.length === 0
258259

259-
if (options.assertions == null) {
260-
options.assertions = []
261-
}
262-
263260
_.defaults(callbacks, {
264261
ensureExistenceFor: 'dom',
265262
})
@@ -296,9 +293,6 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
296293

297294
const onPassFn = () => {
298295
cy.state('overrideAssert', undefined)
299-
if (_.isFunction(callbacks.onPass)) {
300-
return callbacks.onPass.call(this, cmds, options.assertions)
301-
}
302296

303297
return subject
304298
}
@@ -342,8 +336,9 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
342336
onFail.call(this, err, isDefaultAssertionErr, cmds)
343337
}
344338
} catch (e3) {
345-
finishAssertions(options.assertions)
346339
throw e3
340+
} finally {
341+
finishAssertions()
347342
}
348343

349344
if (_.isFunction(onRetry)) {
@@ -380,108 +375,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
380375
.catch(onFailFn)
381376
}
382377

383-
let i = 0
384-
385-
const cmdHasFunctionArg = (cmd) => {
386-
return _.isFunction(cmd.get('args')[0])
387-
}
388-
389378
const overrideAssert = function (...args) {
390-
let cmd = cmds[i]
391-
const setCommandLog = (log) => {
392-
// our next log may not be an assertion
393-
// due to page events so make sure we wait
394-
// until we see page events
395-
if (log.get('name') !== 'assert') {
396-
return
397-
}
398-
399-
// when we do immediately unbind this function
400-
cy.state('onBeforeLog', null)
401-
402-
const insertNewLog = (log) => {
403-
cmd.log(log)
404-
405-
return options.assertions.push(log)
406-
}
407-
408-
// its possible a single 'should' will assert multiple
409-
// things such as the case with have.property. we can
410-
// detect when that is happening because cmd will be null.
411-
// if thats the case then just set cmd to be the previous
412-
// cmd and do not increase 'i'
413-
// this will prevent 2 logs from ever showing up but still
414-
// provide errors when the 1st assertion fails.
415-
if (!cmd) {
416-
cmd = cmds[i - 1]
417-
} else {
418-
i += 1
419-
}
420-
421-
// if our command has a function argument
422-
// then we know it may contain multiple
423-
// assertions
424-
if (cmdHasFunctionArg(cmd)) {
425-
let index = cmd.get('assertionIndex')
426-
let assertions = cmd.get('assertions')
427-
428-
// https://github.com/cypress-io/cypress/issues/4981
429-
// `assertions` is undefined because assertions added by
430-
// `should` command are not handled yet.
431-
// So, don't increase i and go back to the last command.
432-
if (!assertions) {
433-
i -= 1
434-
cmd = cmds[i - 1]
435-
index = cmd.get('assertionIndex')
436-
assertions = cmd.get('assertions')
437-
}
438-
439-
// always increase the assertionIndex
440-
// so our next assertion matches up
441-
// to the correct index
442-
const incrementIndex = () => {
443-
return cmd.set('assertionIndex', index += 1)
444-
}
445-
446-
// if we dont have an assertion at this
447-
// index then insert a new log
448-
const assertion = assertions[index]
449-
450-
if (!assertion) {
451-
assertions.push(log)
452-
incrementIndex()
453-
454-
return insertNewLog(log)
455-
}
456-
457-
// else just merge this log
458-
// into the previous assertion log
459-
incrementIndex()
460-
assertion.merge(log)
461-
462-
// dont output a new log
463-
return false
464-
}
465-
466-
// if we already have a log
467-
// then just update its attrs from
468-
// the new log
469-
const l = cmd.getLastLog()
470-
471-
if (l) {
472-
l.merge(log)
473-
474-
// and make sure we return false
475-
// which prevents a new log from
476-
// being added
477-
return false
478-
}
479-
480-
return insertNewLog(log)
481-
}
482-
483-
cy.state('onBeforeLog', setCommandLog)
484-
485379
// send verify=true as the last arg
486380
return assertFn.apply(this, args.concat(true) as any)
487381
}
@@ -537,7 +431,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
537431

538432
setSubjectAndSkip()
539433

540-
finishAssertions(options.assertions)
434+
finishAssertions()
541435

542436
return onPassFn()
543437
})
@@ -547,7 +441,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
547441
// when we're told not to retry
548442
if (err.retry === false) {
549443
// finish the assertions
550-
finishAssertions(options.assertions)
444+
finishAssertions()
551445

552446
// and then push our command into this err
553447
try {

packages/driver/src/cy/commands/asserting.ts

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ import $errUtils from '../../cypress/error_utils'
77
const reExistence = /exist/
88
const reHaveLength = /length/
99

10+
const onBeforeLog = (log, command, commandLogId) => {
11+
log.set('commandLogId', commandLogId)
12+
13+
const previousLogInstance = command.get('logs').find(_.matchesProperty('attributes.commandLogId', commandLogId))
14+
15+
if (previousLogInstance) {
16+
// log.merge unsets any keys that aren't set on the new log instance. We
17+
// copy over 'snapshots' beforehand so that existing snapshots aren't lost
18+
// in the merge operation.
19+
log.set('snapshots', previousLogInstance.get('snapshots'))
20+
previousLogInstance.merge(log)
21+
22+
if (previousLogInstance.get('end')) {
23+
previousLogInstance.end()
24+
}
25+
26+
// Returning false prevents this new log from being added to the command log
27+
return false
28+
}
29+
30+
return true
31+
}
32+
1033
export default function (Commands, Cypress, cy, state) {
1134
const shouldFnWithCallback = function (subject, fn) {
1235
state('current')?.set('followedByShouldCallback', true)
@@ -24,8 +47,44 @@ export default function (Commands, Cypress, cy, state) {
2447
}
2548

2649
const shouldFn = function (subject, chainers, ...args) {
50+
const command = cy.state('current')
51+
52+
// Most commands are responsible for creating and managing their own log messages directly.
53+
// .should(), however, is an exception - it is invoked by earlier commands, as part of
54+
// `verifyUpcomingAssertions`. This callback can also be invoked any number of times, but we only want
55+
// to display a few log messages (one for each assertion).
56+
57+
// Therefore, we each time Cypress.log() is called, we need a way to identify if this log call
58+
// a duplicate of a previous one that's just being retried. This is the purpose of `commandLogId` - it should
59+
// remain the same across multiple invocations of verifyUpcomingAssertions().
60+
61+
// It is composed of two parts: assertionIndex and logIndex. Assertion index is "which .should() command are we
62+
// inside". Consider the following case:
63+
// `cy.noop(3).should('be.lessThan', 4).should('be.greaterThan', 2)`
64+
// cy.state('current') is always the 'noop' command, which rolls up the two upcoming assertions, lessThan and
65+
// greaterThan. `assertionIndex` lets us tell them apart even though they have the same logIndex of 0 (since it
66+
// resets each time .should() is called).
67+
68+
// As another case, consider:
69+
// cy.noop(3).should((n) => { expect(n).to.be.lessThan(4); expect(n).to.be.greaterThan(2); })
70+
// Here, assertionIndex is 0 for both - one .should() block generates two log messages. In this case, logIndex is
71+
// used to tell them apart, since it increments each time Cypress.log() is called within a single retry of a single
72+
// .should().
73+
const assertionIndex = cy.state('upcomingAssertions') ? cy.state('upcomingAssertions').indexOf(command.get('currentAssertionCommand')) : 0
74+
let logIndex = 0
75+
2776
if (_.isFunction(chainers)) {
28-
return shouldFnWithCallback.apply(this, [subject, chainers])
77+
cy.state('onBeforeLog', (log) => {
78+
logIndex++
79+
80+
return onBeforeLog(log, command, `${assertionIndex}-${logIndex}`)
81+
})
82+
83+
try {
84+
return shouldFnWithCallback.apply(this, [subject, chainers])
85+
} finally {
86+
cy.state('onBeforeLog', undefined)
87+
}
2988
}
3089

3190
let exp = cy.expect(subject).to
@@ -35,6 +94,7 @@ export default function (Commands, Cypress, cy, state) {
3594
// since we are throwing our own error
3695
// without going through the assertion we need
3796
// to ensure our .should command gets logged
97+
logIndex++
3898
const log = Cypress.log({
3999
name: 'should',
40100
type: 'child',
@@ -58,31 +118,40 @@ export default function (Commands, Cypress, cy, state) {
58118
const isCheckingLengthOrExistence = isCheckingExistence || reHaveLength.test(chainers)
59119

60120
const applyChainer = function (memo, value) {
61-
if (value === lastChainer && !isCheckingExistence) {
121+
logIndex++
122+
cy.state('onBeforeLog', (log) => {
123+
return onBeforeLog(log, command, `${assertionIndex}-${logIndex}`)
124+
})
125+
126+
try {
127+
if (value === lastChainer && !isCheckingExistence) {
62128
// https://github.com/cypress-io/cypress/issues/16006
63129
// Referring some commands like 'visible' triggers assert function in chai_jquery.js
64130
// It creates duplicated messages and confuses users.
65-
const cmd = memo[value]
66-
67-
if (_.isFunction(cmd)) {
68-
try {
69-
return cmd.apply(memo, args)
70-
} catch (err: any) {
71-
// if we made it all the way to the actual
72-
// assertion but its set to retry false then
73-
// we need to log out this .should since there
74-
// was a problem with the actual assertion syntax
75-
if (err.retry === false) {
76-
return throwAndLogErr(err)
131+
const cmd = memo[value]
132+
133+
if (_.isFunction(cmd)) {
134+
try {
135+
return cmd.apply(memo, args)
136+
} catch (err: any) {
137+
// if we made it all the way to the actual
138+
// assertion but its set to retry false then
139+
// we need to log out this .should since there
140+
// was a problem with the actual assertion syntax
141+
if (err.retry === false) {
142+
return throwAndLogErr(err)
143+
}
144+
145+
throw err
77146
}
78-
79-
throw err
147+
} else {
148+
return cmd
80149
}
81150
} else {
82-
return cmd
151+
return memo[value]
83152
}
84-
} else {
85-
return memo[value]
153+
} finally {
154+
cy.state('onBeforeLog', undefined)
86155
}
87156
}
88157

packages/driver/src/cy/commands/waiting.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,16 +300,16 @@ export default (Commands, Cypress, cy, state) => {
300300
if (responsesOrErr.hasSpecBridgeError) {
301301
delete responsesOrErr.hasSpecBridgeError
302302
if (options.log) {
303+
// skip this 'wait' log since it was already added through the primary
303304
Cypress.state('onBeforeLog', (log) => {
304-
// skip this 'wait' log since it was already added through the primary
305305
if (log.get('name') === 'wait') {
306306
// unbind this function so we don't impact any other logs
307307
cy.state('onBeforeLog', null)
308308

309309
return false
310310
}
311311

312-
return
312+
return true
313313
})
314314
}
315315

0 commit comments

Comments
 (0)