-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: Refactor assertion logging #23354
Changes from all commits
2ab7eb0
f8b0f40
70b7086
7951a02
6849134
e07ae97
8029429
971c272
443bd53
d612806
5f57dfb
4292f80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,29 @@ import $errUtils from '../../cypress/error_utils' | |
const reExistence = /exist/ | ||
const reHaveLength = /length/ | ||
|
||
const onBeforeLog = (log, command, commandLogId) => { | ||
log.set('commandLogId', commandLogId) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason we didn't just provide the log id instead of adding a new attribute on the log? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to use the log id initially, but you can't set a log's ID:
because the ID is meant to be completely unique - to identify a log instance, across the entire test. Updating it in the log would also require updating the mapping of id -> Log instance that the Also important to note that in the current implementation, the commandLogId isn't globally unique - it's unique (and reproducible) only within the context of a single command. Different instances of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically, this just serves a different purpose than the log's id. It's a bit of state-tracking for an instance of |
||
|
||
const previousLogInstance = command.get('logs').find(_.matchesProperty('attributes.commandLogId', commandLogId)) | ||
|
||
if (previousLogInstance) { | ||
// log.merge unsets any keys that aren't set on the new log instance. We | ||
// copy over 'snapshots' beforehand so that existing snapshots aren't lost | ||
// in the merge operation. | ||
log.set('snapshots', previousLogInstance.get('snapshots')) | ||
previousLogInstance.merge(log) | ||
|
||
if (previousLogInstance.get('end')) { | ||
previousLogInstance.end() | ||
} | ||
|
||
// Returning false prevents this new log from being added to the command log | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
export default function (Commands, Cypress, cy, state) { | ||
const shouldFnWithCallback = function (subject, fn) { | ||
state('current')?.set('followedByShouldCallback', true) | ||
|
@@ -24,8 +47,44 @@ export default function (Commands, Cypress, cy, state) { | |
} | ||
|
||
const shouldFn = function (subject, chainers, ...args) { | ||
const command = cy.state('current') | ||
|
||
// Most commands are responsible for creating and managing their own log messages directly. | ||
// .should(), however, is an exception - it is invoked by earlier commands, as part of | ||
// `verifyUpcomingAssertions`. This callback can also be invoked any number of times, but we only want | ||
// to display a few log messages (one for each assertion). | ||
|
||
// Therefore, we each time Cypress.log() is called, we need a way to identify if this log call | ||
// a duplicate of a previous one that's just being retried. This is the purpose of `commandLogId` - it should | ||
// remain the same across multiple invocations of verifyUpcomingAssertions(). | ||
|
||
// It is composed of two parts: assertionIndex and logIndex. Assertion index is "which .should() command are we | ||
// inside". Consider the following case: | ||
// `cy.noop(3).should('be.lessThan', 4).should('be.greaterThan', 2)` | ||
// cy.state('current') is always the 'noop' command, which rolls up the two upcoming assertions, lessThan and | ||
// greaterThan. `assertionIndex` lets us tell them apart even though they have the same logIndex of 0 (since it | ||
// resets each time .should() is called). | ||
|
||
// As another case, consider: | ||
// cy.noop(3).should((n) => { expect(n).to.be.lessThan(4); expect(n).to.be.greaterThan(2); }) | ||
// Here, assertionIndex is 0 for both - one .should() block generates two log messages. In this case, logIndex is | ||
// used to tell them apart, since it increments each time Cypress.log() is called within a single retry of a single | ||
// .should(). | ||
const assertionIndex = cy.state('upcomingAssertions') ? cy.state('upcomingAssertions').indexOf(command.get('currentAssertionCommand')) : 0 | ||
let logIndex = 0 | ||
|
||
if (_.isFunction(chainers)) { | ||
return shouldFnWithCallback.apply(this, [subject, chainers]) | ||
cy.state('onBeforeLog', (log) => { | ||
logIndex++ | ||
|
||
return onBeforeLog(log, command, `${assertionIndex}-${logIndex}`) | ||
}) | ||
|
||
try { | ||
return shouldFnWithCallback.apply(this, [subject, chainers]) | ||
} finally { | ||
cy.state('onBeforeLog', undefined) | ||
} | ||
} | ||
|
||
let exp = cy.expect(subject).to | ||
|
@@ -35,6 +94,7 @@ export default function (Commands, Cypress, cy, state) { | |
// since we are throwing our own error | ||
// without going through the assertion we need | ||
// to ensure our .should command gets logged | ||
logIndex++ | ||
const log = Cypress.log({ | ||
name: 'should', | ||
type: 'child', | ||
|
@@ -58,31 +118,40 @@ export default function (Commands, Cypress, cy, state) { | |
const isCheckingLengthOrExistence = isCheckingExistence || reHaveLength.test(chainers) | ||
|
||
const applyChainer = function (memo, value) { | ||
if (value === lastChainer && !isCheckingExistence) { | ||
logIndex++ | ||
cy.state('onBeforeLog', (log) => { | ||
return onBeforeLog(log, command, `${assertionIndex}-${logIndex}`) | ||
}) | ||
|
||
try { | ||
if (value === lastChainer && !isCheckingExistence) { | ||
// https://github.com/cypress-io/cypress/issues/16006 | ||
// Referring some commands like 'visible' triggers assert function in chai_jquery.js | ||
// It creates duplicated messages and confuses users. | ||
const cmd = memo[value] | ||
|
||
if (_.isFunction(cmd)) { | ||
try { | ||
return cmd.apply(memo, args) | ||
} catch (err: any) { | ||
// if we made it all the way to the actual | ||
// assertion but its set to retry false then | ||
// we need to log out this .should since there | ||
// was a problem with the actual assertion syntax | ||
if (err.retry === false) { | ||
return throwAndLogErr(err) | ||
const cmd = memo[value] | ||
|
||
if (_.isFunction(cmd)) { | ||
try { | ||
return cmd.apply(memo, args) | ||
} catch (err: any) { | ||
// if we made it all the way to the actual | ||
// assertion but its set to retry false then | ||
// we need to log out this .should since there | ||
// was a problem with the actual assertion syntax | ||
if (err.retry === false) { | ||
return throwAndLogErr(err) | ||
BlueWinds marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
throw err | ||
} | ||
|
||
throw err | ||
} else { | ||
return cmd | ||
} | ||
} else { | ||
return cmd | ||
return memo[value] | ||
} | ||
} else { | ||
return memo[value] | ||
} finally { | ||
cy.state('onBeforeLog', undefined) | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR does introduce one test change - the assertion log is in a
pending
state when initially added, and ispassed
asynchronously as part of thefinishAssertions()
function, which happens slightly later in the lifecycle of the command.Other tests verify that it moves from the
pending
state toresolved
before the next command executes - it's just slightly later in resolving the same promise chain thanlog:added
. I don't believe this is a meaningful difference, and so this can remain a chore even though I had to update one test. 🤷♀️