Skip to content

Commit

Permalink
fix: No unnecessary snapshotting (#19311)
Browse files Browse the repository at this point in the history
Co-authored-by: Brian Mann <brian.mann86@gmail.com>
  • Loading branch information
Blue F and brian-mann committed Dec 20, 2021
1 parent 01f0261 commit b6c4ba1
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 11 deletions.
113 changes: 107 additions & 6 deletions packages/driver/cypress/integration/commands/assertions_spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
const { assertLogLength } = require('../../support/utils')
const { $, _ } = Cypress

const captureCommands = () => {
const commands = []

let current

cy.on('command:start', (command) => {
current = command
commands.push({
name: command.attributes.name,
snapshots: 0,
retries: 0,
})
})

cy.on('command:retry', () => {
commands[commands.length - 1].retries++
})

cy.on('snapshot', () => {
// Snapshots can occur outside the context of a command - for example, `expect(foo).to.exist` without any wrapping cy command.
// So we keep track of the current command when one starts, and if we're not inside that, create an 'empty' command
// for the snapshot to belong to
if (!commands.length || current !== cy.state('current')) {
current = null
commands.push({ name: null, snapshots: 0, retries: 0 })
}

commands[commands.length - 1].snapshots++
})

return () => _.cloneDeep(commands)
}

describe('src/cy/commands/assertions', () => {
before(() => {
cy
Expand All @@ -10,10 +43,14 @@ describe('src/cy/commands/assertions', () => {
})
})

let testCommands

beforeEach(function () {
const doc = cy.state('document')

$(doc.body).empty().html(this.body)

testCommands = captureCommands()
})

context('#should', () => {
Expand All @@ -29,8 +66,14 @@ describe('src/cy/commands/assertions', () => {
})

it('returns the subject for chainability', () => {
cy.noop({ foo: 'bar' }).should('deep.eq', { foo: 'bar' }).then((obj) => {
expect(obj).to.deep.eq({ foo: 'bar' })
cy
.noop({ foo: 'bar' }).should('deep.eq', { foo: 'bar' })
.then((obj) => {
expect(testCommands()).to.eql([
{ name: 'noop', snapshots: 0, retries: 0 },
{ name: 'should', snapshots: 1, retries: 0 },
{ name: 'then', snapshots: 0, retries: 0 },
])
})
})

Expand Down Expand Up @@ -121,11 +164,19 @@ describe('src/cy/commands/assertions', () => {
cy.wrap(obj).then(() => {
setTimeout(() => {
obj.foo = 'baz'
}
, 100)
}, 100)

cy.wrap(obj)
}).should('deep.eq', { foo: 'baz' })
})
.should('deep.eq', { foo: 'baz' })
.then(() => {
expect(testCommands()).to.containSubset([
{ name: 'wrap', snapshots: 1, retries: 0 },
{ name: 'then', snapshots: 0, retries: 0 },
{ name: 'wrap', snapshots: 2, retries: (r) => r > 1 },
{ name: 'then', snapshots: 0, retries: 0 },
])
})
})

// https://github.com/cypress-io/cypress/issues/16006
Expand All @@ -151,6 +202,18 @@ describe('src/cy/commands/assertions', () => {
cy.get('button:first').should(($button) => {
expect($button).to.have.class('ready')
})
.then(() => {
expect(testCommands()).to.eql([
// cy.get() has 2 snapshots, 1 for itself, and 1
// for the .should(...) assertion.

// TODO: Investigate whether or not the 2 commands are
// snapshotted at the same time. If there's no tick between
// them, we could reuse the snapshots
{ name: 'get', snapshots: 2, retries: 2 },
{ name: 'then', snapshots: 0, retries: 0 },
])
})
})

it('works with regular objects', () => {
Expand Down Expand Up @@ -1475,7 +1538,7 @@ describe('src/cy/commands/assertions', () => {
done()
})

cy.get('div').should(($divs) => {
cy.get('div', { timeout: 100 }).should(($divs) => {
expect($divs, 'Filter should have 1 items').to.have.length(1)
})
})
Expand Down Expand Up @@ -2927,4 +2990,42 @@ describe('src/cy/commands/assertions', () => {
cy.get('.foo').should('not.exist')
})
})

context('implicit assertions', () => {
// https://github.com/cypress-io/cypress/issues/18549
// A targeted test for the above issue - in the absence of retries, only a single snapshot
// should be taken.
it('only snapshots once when failing to find DOM elements and not retrying', (done) => {
cy.on('fail', (err) => {
expect(testCommands()).to.eql([{
name: 'get',
snapshots: 1,
retries: 0,
}])

done()
})

cy.get('.badId', { timeout: 0 })
})

// https://github.com/cypress-io/cypress/issues/18549
// This issue was also causing two DOM snapshots to be taken every 50ms
// while waiting for an element to exist. The first test is sufficient to
// prevent regressions of the specific issue, but this one is intended to
// more generally assert that retries do not trigger multiple snapshots.
it('only snapshots once when retrying assertions', (done) => {
cy.on('fail', (err) => {
expect(testCommands()).to.containSubset([{
name: 'get',
snapshots: 1,
retries: (v) => v > 1,
}])

done()
})

cy.get('.badId', { timeout: 1000 })
})
})
})
38 changes: 33 additions & 5 deletions packages/driver/src/cy/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,19 @@ export const create = (Cypress, cy) => {
obj.$el = $dom.wrap(value)
}

// if we are simply verifying the upcoming
// assertions then do not immediately end or snapshot
// else do
// `verifying` represents whether we're deciding whether or not to resolve
// a command (true) or of we're actually performing a user-facing assertion
// (false).

// If we're verifying upcoming assertions (implicit or explicit),
// then we don't need to take a DOM snapshot - one will be taken later when
// retries time out or the command otherwise entirely fails or passes.
// We save the error on _error because we may use it to construct the
// timeout error which we eventually do display to the user.

// If we're actually performing an assertion which will be displayed to the
// user though, then we want to take a DOM snapshot and display this error
// (if any) in the log message on screen.
if (verifying) {
obj._error = error
} else {
Expand Down Expand Up @@ -283,6 +293,7 @@ export const create = (Cypress, cy) => {
}

const onPassFn = () => {
cy.state('overrideAssert', undefined)
if (_.isFunction(callbacks.onPass)) {
return callbacks.onPass.call(this, cmds, options.assertions)
}
Expand All @@ -305,6 +316,7 @@ export const create = (Cypress, cy) => {
err = e2
}

cy.state('overrideAssert', undefined)
err.isDefaultAssertionErr = isDefaultAssertionErr

options.error = err
Expand Down Expand Up @@ -340,6 +352,24 @@ export const create = (Cypress, cy) => {
// bail if we have no assertions and apply
// the default assertions if applicable
if (!cmds.length) {
// In general in cypress, when assertions fail we want to take a DOM
// snapshot to display to the user. In this case though, when we invoke
// ensureExistence, we're not going to display the error (if there is
// one) to the user - we're only deciding whether to resolve this current
// command (assertions pass) or fail (and probably retry). A DOM snapshot
// isn't necessary in either case - one will be taken later as part of the
// command (if they pass) or when we time out retrying.

// Chai assertions have a signature of (passed, message, value, actual,
// expected, error). Our assertFn, defined earlier in the file, adds
// on a 7th arg, "verifying", which defaults to false. We here override
// the assert function with our own, which just invokes the old one
// with verifying = true. This override is cleaned up immediately
// afterwards, in either onPassFn or onFailFn.
cy.state('overrideAssert', function (...args) {
return assertFn.apply(this, args.concat(true) as any)
})

return Promise
.try(ensureExistence)
.then(onPassFn)
Expand Down Expand Up @@ -494,8 +524,6 @@ export const create = (Cypress, cy) => {
return cy.state('overrideAssert', undefined)
}

// store this in case our test ends early
// and we reset between tests
cy.state('overrideAssert', overrideAssert)

return Promise
Expand Down
1 change: 1 addition & 0 deletions packages/driver/src/cy/snapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const create = ($$, state) => {
}

const createSnapshot = (name, $elToHighlight) => {
Cypress.action('cy:snapshot', name)
// create a unique selector for this el
// but only IF the subject is truly an element. For example
// we might be wrapping a primitive like "$([1, 2]).first()"
Expand Down
3 changes: 3 additions & 0 deletions packages/driver/src/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@ class $Cypress {
case 'cy:scrolled':
return this.emit('scrolled', ...args)

case 'cy:snapshot':
return this.emit('snapshot', ...args)

case 'app:uncaught:exception':
return this.emitMap('uncaught:exception', ...args)

Expand Down

3 comments on commit b6c4ba1

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b6c4ba1 Dec 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/9.1.2/circle-develop-b6c4ba144cd6ae3d210789bbb69b9aacc6a92094/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b6c4ba1 Dec 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/9.1.2/circle-develop-b6c4ba144cd6ae3d210789bbb69b9aacc6a92094/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on b6c4ba1 Dec 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/9.1.2/circle-develop-b6c4ba144cd6ae3d210789bbb69b9aacc6a92094/cypress.tgz

Please sign in to comment.