diff --git a/npm/cypress-schematic/src/e2e.spec.ts b/npm/cypress-schematic/src/e2e.spec.ts
index e4d6042755b0..add1d333e549 100644
--- a/npm/cypress-schematic/src/e2e.spec.ts
+++ b/npm/cypress-schematic/src/e2e.spec.ts
@@ -27,7 +27,7 @@ const cypressSchematicPackagePath = path.join(__dirname, '..')
const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-13', 'angular-14']
describe('ng add @cypress/schematic / only e2e', function () {
- this.timeout(1000 * 60 * 5)
+ this.timeout(1000 * 60 * 4)
for (const project of ANGULAR_PROJECTS) {
it('should install e2e files by default', async () => {
diff --git a/packages/driver/cypress/e2e/commands/agents.cy.js b/packages/driver/cypress/e2e/commands/agents.cy.js
index 012bc1957836..73ef7c9ff851 100644
--- a/packages/driver/cypress/e2e/commands/agents.cy.js
+++ b/packages/driver/cypress/e2e/commands/agents.cy.js
@@ -323,6 +323,10 @@ describe('src/cy/commands/agents', () => {
expect(cy.state('aliases').myStub).to.exist
})
+ it('stores the agent as the subject', function () {
+ expect(cy.state('aliases').myStub.subject).to.eq(this.stub)
+ })
+
it('assigns subject to runnable ctx', function () {
expect(this.myStub).to.eq(this.stub)
})
@@ -400,6 +404,10 @@ describe('src/cy/commands/agents', () => {
expect(cy.state('aliases')['my.stub']).to.exist
})
+ it('stores the agent as the subject', function () {
+ expect(cy.state('aliases')['my.stub'].subject).to.eq(this.stub)
+ })
+
it('assigns subject to runnable ctx', function () {
expect(this['my.stub']).to.eq(this.stub)
})
diff --git a/packages/driver/cypress/e2e/commands/aliasing.cy.js b/packages/driver/cypress/e2e/commands/aliasing.cy.js
index 18ff6704a99f..524c7c44259b 100644
--- a/packages/driver/cypress/e2e/commands/aliasing.cy.js
+++ b/packages/driver/cypress/e2e/commands/aliasing.cy.js
@@ -1,5 +1,5 @@
const { assertLogLength } = require('../../support/utils')
-const { _ } = Cypress
+const { _, $ } = Cypress
describe('src/cy/commands/aliasing', () => {
beforeEach(() => {
@@ -7,6 +7,14 @@ describe('src/cy/commands/aliasing', () => {
})
context('#as', () => {
+ it('is special utility command', () => {
+ cy.wrap('foo').as('f').then(() => {
+ const cmd = cy.queue.find({ name: 'as' })
+
+ expect(cmd.get('type')).to.eq('utility')
+ })
+ })
+
it('does not change the subject', () => {
const body = cy.$$('body')
@@ -26,13 +34,11 @@ describe('src/cy/commands/aliasing', () => {
cy.get('@body')
})
- it('stores the resulting subject chain as the alias', () => {
- cy.get('body').as('b').then(() => {
- const { subjectChain } = cy.state('aliases').b
+ it('stores the resulting subject as the alias', () => {
+ const $body = cy.$$('body')
- expect(subjectChain.length).to.eql(2)
- expect(subjectChain[0]).to.be.undefined
- expect(subjectChain[1].commandName).to.eq('get')
+ cy.get('body').as('b').then(() => {
+ expect(cy.state('aliases').b.subject.get(0)).to.eq($body.get(0))
})
})
@@ -44,15 +50,6 @@ describe('src/cy/commands/aliasing', () => {
})
})
- it('retries previous commands invoked inside custom commands', () => {
- Cypress.Commands.add('get2', (selector) => cy.get(selector))
-
- cy.get2('body').children('div').as('divs')
- cy.visit('/fixtures/dom.html')
-
- cy.get('@divs')
- })
-
it('retries primitives and assertions', () => {
const obj = {}
@@ -90,13 +87,29 @@ describe('src/cy/commands/aliasing', () => {
})
})
- it('retries previous commands invoked inside custom commands', () => {
- Cypress.Commands.add('get2', (selector) => cy.get(selector))
+ context('DOM subjects', () => {
+ it('assigns the remote jquery instance', () => {
+ const obj = {}
+
+ const jquery = () => {
+ return obj
+ }
+
+ cy.state('jQuery', jquery)
+
+ cy.get('input:first').as('input').then(function () {
+ expect(this.input).to.eq(obj)
+ })
+ })
+
+ it('retries previous commands invoked inside custom commands', () => {
+ Cypress.Commands.add('get2', (selector) => cy.get(selector))
- cy.get2('body').children('div').as('divs')
- cy.visit('/fixtures/dom.html')
+ cy.get2('body').children('div').as('divs')
+ cy.visit('/fixtures/dom.html')
- cy.get('@divs')
+ cy.get('@divs')
+ })
})
context('#assign', () => {
@@ -315,8 +328,9 @@ describe('src/cy/commands/aliasing', () => {
// sanity check without command overwrite
cy.wrap('alias value').as('myAlias')
.then(() => {
- expect(cy.getAlias('@myAlias')).to.exist
- expect(cy.getAlias('@myAlias').subjectChain).to.eql(['alias value'])
+ expect(cy.getAlias('@myAlias'), 'alias exists').to.exist
+ expect(cy.getAlias('@myAlias'), 'alias value')
+ .to.have.property('subject', 'alias value')
})
.then(() => {
// cy.get returns the alias
@@ -335,9 +349,10 @@ describe('src/cy/commands/aliasing', () => {
cy.wrap('alias value').as('myAlias')
.then(() => {
- expect(wrapCalled).to.be.true
- expect(cy.getAlias('@myAlias')).to.exist
- expect(cy.getAlias('@myAlias').subjectChain).to.eql(['alias value'])
+ expect(wrapCalled, 'overwrite was called').to.be.true
+ expect(cy.getAlias('@myAlias'), 'alias exists').to.exist
+ expect(cy.getAlias('@myAlias'), 'alias value')
+ .to.have.property('subject', 'alias value')
})
.then(() => {
// verify cy.get works in arrow function
@@ -367,8 +382,9 @@ describe('src/cy/commands/aliasing', () => {
.then(() => {
expect(wrapCalled, 'overwrite was called').to.be.true
expect(thenCalled, 'then was called').to.be.true
- expect(cy.getAlias('@myAlias')).to.exist
- expect(cy.getAlias('@myAlias').subjectChain).to.eql(['alias value'])
+ expect(cy.getAlias('@myAlias'), 'alias exists').to.exist
+ expect(cy.getAlias('@myAlias'), 'alias value')
+ .to.have.property('subject', 'alias value')
})
.then(() => {
// verify cy.get works in arrow function
@@ -384,9 +400,9 @@ describe('src/cy/commands/aliasing', () => {
// sanity test before the next one
cy.wrap(1).as('myAlias')
cy.wrap(2).then(function (subj) {
- expect(subj).to.equal(2)
- expect(this).to.not.be.undefined
- expect(this.myAlias).to.eq(1)
+ expect(subj, 'subject').to.equal(2)
+ expect(this, 'this is defined').to.not.be.undefined
+ expect(this.myAlias, 'this has the alias as a property').to.eq(1)
})
})
@@ -398,8 +414,8 @@ describe('src/cy/commands/aliasing', () => {
cy.wrap(1).as('myAlias')
cy.wrap(2).then(function (subj) {
- expect(subj).to.equal(2)
- expect(this).to.not.be.undefined
+ expect(subj, 'subject').to.equal(2)
+ expect(this, 'this is defined').to.not.be.undefined
expect(this.myAlias).to.eq(1)
})
})
@@ -412,61 +428,166 @@ describe('src/cy/commands/aliasing', () => {
cy.wrap(1).as('myAlias')
cy.wrap(2).then(function (subj) {
- expect(subj).to.equal(2)
- expect(this).to.not.be.undefined
+ expect(subj, 'subject').to.equal(2)
+ expect(this, 'this is defined').to.not.be.undefined
expect(this.myAlias).to.eq(1)
})
})
})
})
- context('#replaying subjects', () => {
- it('returns if subject is still in the document', () => {
- cy.get('#list').as('list').then((firstList) => {
- cy.get('@list').then((secondList) => {
- expect(firstList).to.eql(secondList)
+ context('#replayCommandsFrom', () => {
+ describe('subject in document', () => {
+ it('returns if subject is still in the document', () => {
+ cy.get('#list').as('list').then(() => {
+ const currentLength = cy.queue.length
+
+ cy.get('@list').then(() => {
+ // should only add the .get() and the .then()
+ expect(cy.queue.length).to.eq(currentLength + 2)
+ })
})
})
})
- it('requeries when reading alias', () => {
- cy
- .get('#list li')
- .as('items').then((firstItems) => {
- cy.$$('#list').append('
123456789')
+ describe('subject not in document', () => {
+ it('inserts into the queue', () => {
+ const existingNames = cy.queue.names()
- cy.get('@items').then((secondItems) => {
- expect(firstItems).to.have.length(3)
- expect(secondItems).to.have.length(4)
+ cy
+ .get('#list li').eq(0).as('firstLi').then(($li) => {
+ return $li.remove()
+ })
+ .get('@firstLi').then(() => {
+ expect(cy.queue.names()).to.deep.eq(
+ existingNames.concat(
+ ['get', 'eq', 'as', 'then', 'get', 'get', 'eq', 'then'],
+ ),
+ )
})
})
- })
- it('requeries when subject is not in the DOM', () => {
- cy
- .get('#list li')
- .as('items').then((firstItems) => {
- firstItems.remove()
- setTimeout(() => {
- cy.$$('#list').append('123456789')
- }, 50)
+ it('replays from last root to current', () => {
+ const first = cy.$$('#list li').eq(0)
+ const second = cy.$$('#list li').eq(1)
+
+ cy
+ .get('#list li').eq(0).as('firstLi').then(($li) => {
+ expect($li.get(0)).to.eq(first.get(0))
- cy.get('@items').then((secondItems) => {
- expect(secondItems).to.have.length(1)
+ return $li.remove()
+ })
+ .get('@firstLi').then(($li) => {
+ expect($li.get(0)).to.eq(second.get(0))
})
})
- })
- it('only retries up to last command', () => {
- cy
- .get('#list li')
- .then((items) => items.length)
- .as('itemCount')
- .then(() => cy.$$('#list li').remove())
-
- // Even though the list items have been removed from the DOM, 'then' can't be retried
- // so we just have the primitive value "3" as our subject.
- cy.get('@itemCount').should('eq', 3)
+ it('replays up until first root command', () => {
+ const existingNames = cy.queue.names()
+
+ cy
+ .get('body').noop({})
+ .get('#list li').eq(0).as('firstLi').then(($li) => {
+ return $li.remove()
+ })
+ .get('@firstLi').then(() => {
+ expect(cy.queue.names()).to.deep.eq(
+ existingNames.concat(
+ ['get', 'noop', 'get', 'eq', 'as', 'then', 'get', 'get', 'eq', 'then'],
+ ),
+ )
+ })
+ })
+
+ it('resets the chainerId allow subjects to be carried on', () => {
+ cy.get('#dom').find('#button').as('button').then(($button) => {
+ $button.remove()
+
+ cy.$$('#dom').append($('', { id: 'button' }))
+
+ return null
+ })
+
+ // when cy is a separate chainer there *was* a bug
+ // that cause the subject to null because of different
+ // chainer id's
+ cy.get('@button').then(($button) => {
+ expect($button).to.have.id('button')
+ })
+ })
+
+ it('skips commands which did not change, and starts at the first valid subject or parent command', () => {
+ const existingNames = cy.queue.names()
+
+ cy.$$('#list li').click(function () {
+ const ul = $(this).parent()
+ const lis = ul.children().clone()
+
+ // this simulates a re-render
+ ul.children().remove()
+ ul.append(lis)
+
+ return lis.first().remove()
+ })
+
+ cy
+ .get('#list li')
+ .then(($lis) => {
+ return $lis
+ })
+ .as('items')
+ .first()
+ .click()
+ .as('firstItem')
+ .then(() => {
+ expect(cy.queue.names()).to.deep.eq(
+ existingNames.concat(
+ ['get', 'then', 'as', 'first', 'click', 'as', 'then', 'get', 'should', 'then', 'get', 'should', 'then'],
+ ),
+ )
+ })
+ .get('@items')
+ .should('have.length', 2)
+ .then(() => {
+ expect(cy.queue.names()).to.deep.eq(
+ existingNames.concat(
+ ['get', 'then', 'as', 'first', 'click', 'as', 'then', 'get', 'get', 'should', 'then', 'get', 'should', 'then'],
+ ),
+ )
+ })
+ .get('@firstItem')
+ .should('contain', 'li 1')
+ .then(() => {
+ expect(cy.queue.names()).to.deep.eq(
+ existingNames.concat(
+ ['get', 'then', 'as', 'first', 'click', 'as', 'then', 'get', 'get', 'should', 'then', 'get', 'get', 'first', 'should', 'then'],
+ ),
+ )
+ })
+ })
+
+ it('inserts assertions', (done) => {
+ const existingNames = cy.queue.names()
+
+ cy
+ .get('#checkboxes input')
+ .eq(0)
+ .should('be.checked', 'cockatoo')
+ .as('firstItem')
+ .then(($input) => {
+ return $input.remove()
+ })
+ .get('@firstItem')
+ .then(() => {
+ expect(cy.queue.names()).to.deep.eq(
+ existingNames.concat(
+ ['get', 'eq', 'should', 'as', 'then', 'get', 'get', 'eq', 'should', 'then'],
+ ),
+ )
+
+ done()
+ })
+ })
})
})
diff --git a/packages/driver/cypress/e2e/commands/assertions.cy.js b/packages/driver/cypress/e2e/commands/assertions.cy.js
index 5781a08aff5c..d09ed0d52bbb 100644
--- a/packages/driver/cypress/e2e/commands/assertions.cy.js
+++ b/packages/driver/cypress/e2e/commands/assertions.cy.js
@@ -843,8 +843,23 @@ describe('src/cy/commands/assertions', () => {
done()
})
- cy.get('body').then((subject) => {
- expect(subject).to.match('body')
+ cy.get('body').then(() => {
+ expect(cy.currentSubject()).to.match('body')
+ })
+ })
+
+ it('sets type to child when subject matches', (done) => {
+ cy.on('log:added', (attrs, log) => {
+ if (attrs.name === 'assert') {
+ cy.removeAllListeners('log:added')
+ expect(log.get('type')).to.eq('child')
+
+ done()
+ }
+ })
+
+ cy.wrap('foo').then(() => {
+ expect('foo').to.eq('foo')
})
})
diff --git a/packages/driver/cypress/e2e/commands/commands.cy.js b/packages/driver/cypress/e2e/commands/commands.cy.js
index b9eb9ea7c0e6..3ce58a8f55c2 100644
--- a/packages/driver/cypress/e2e/commands/commands.cy.js
+++ b/packages/driver/cypress/e2e/commands/commands.cy.js
@@ -88,21 +88,6 @@ describe('src/cy/commands/commands', () => {
})
})
- it('throws when attempting to add an existing query', (done) => {
- cy.on('fail', (err) => {
- expect(err.message).to.eq('`Cypress.Commands._addQuery()` is used to create new queries, but `get` is an existing Cypress command or query, or is reserved internally by Cypress.\n\n If you want to override an existing command or query, use `Cypress.Commands.overrideQuery()` instead.')
- expect(err.docsUrl).to.eq('https://on.cypress.io/custom-commands')
-
- done()
- })
-
- Cypress.Commands._addQuery('get', () => {
- cy
- .get('[contenteditable]')
- .first()
- })
- })
-
it('allows calling .add with hover / mount', () => {
let calls = 0
@@ -135,21 +120,6 @@ describe('src/cy/commands/commands', () => {
.first()
})
})
-
- it('throws when attempting to add a query with the same name as an internal function', (done) => {
- cy.on('fail', (err) => {
- expect(err.message).to.eq('`Cypress.Commands._addQuery()` cannot create a new query named `addCommand` because that name is reserved internally by Cypress.')
- expect(err.docsUrl).to.eq('https://on.cypress.io/custom-commands')
-
- done()
- })
-
- Cypress.Commands._addQuery('addCommand', () => {
- cy
- .get('[contenteditable]')
- .first()
- })
- })
})
context('errors', () => {
diff --git a/packages/driver/cypress/e2e/commands/navigation.cy.js b/packages/driver/cypress/e2e/commands/navigation.cy.js
index ea59f3c59224..aeb12ad05931 100644
--- a/packages/driver/cypress/e2e/commands/navigation.cy.js
+++ b/packages/driver/cypress/e2e/commands/navigation.cy.js
@@ -1013,10 +1013,10 @@ describe('src/cy/commands/navigation', () => {
expect(win.location.href).to.include('/fixtures/jquery.html?foo=bar#dashboard?baz=quux')
})
- this.cyWin = cy.state('window')
+ this.win = cy.state('window')
this.eq = (attr, str) => {
- expect(this.cyWin.location[attr]).to.eq(str)
+ expect(this.win.location[attr]).to.eq(str)
}
})
@@ -2340,7 +2340,7 @@ describe('src/cy/commands/navigation', () => {
expect(this.lastLog).to.exist
expect(this.lastLog.get('state')).to.eq('pending')
expect(this.lastLog.get('message')).to.eq('--waiting for new page to load--')
- expect(this.lastLog.get('snapshots')).to.have.length(0)
+ expect(this.lastLog.get('snapshots')).to.not.exist
})
}).get('#dimensions').click()
.then(function () {
@@ -2367,7 +2367,7 @@ describe('src/cy/commands/navigation', () => {
expect(this.lastLog).to.exist
expect(this.lastLog.get('state')).to.eq('pending')
expect(this.lastLog.get('message')).to.eq('--waiting for new page to load--')
- expect(this.lastLog.get('snapshots')).to.have.length(0)
+ expect(this.lastLog.get('snapshots')).to.not.exist
})
cy
diff --git a/packages/driver/cypress/e2e/commands/querying/querying.cy.js b/packages/driver/cypress/e2e/commands/querying/querying.cy.js
index a66e6f8498b1..412953cb491f 100644
--- a/packages/driver/cypress/e2e/commands/querying/querying.cy.js
+++ b/packages/driver/cypress/e2e/commands/querying/querying.cy.js
@@ -18,6 +18,23 @@ describe('src/cy/commands/querying', () => {
})
})
+ // NOTE: FLAKY in CI, need to investigate further
+ it.skip('retries finding elements until something is found', () => {
+ const missingEl = $('', { id: 'missing-el' })
+
+ // wait until we're ALMOST about to time out before
+ // appending the missingEl
+ cy.on('command:retry', (options) => {
+ if ((options.total + (options._interval * 4)) > options._runnableTimeout) {
+ cy.$$('body').append(missingEl)
+ }
+ })
+
+ cy.get('#missing-el').then(($div) => {
+ expect($div).to.match(missingEl)
+ })
+ })
+
it('can increase the timeout', () => {
const missingEl = $('', { id: 'missing-el' })
@@ -270,7 +287,7 @@ describe('src/cy/commands/querying', () => {
})
})
- it('retries an alias when too many elements found', () => {
+ it('retries an alias when too many elements found without replaying commands', () => {
// add 500ms to the delta
cy.timeout(500, true)
@@ -278,15 +295,24 @@ describe('src/cy/commands/querying', () => {
const length = buttons.length - 2
+ const replayCommandsFrom = cy.spy(cy, 'replayCommandsFrom')
+
cy.on('command:retry', () => {
buttons.last().remove()
buttons = cy.$$('button')
})
+ const existingLen = cy.queue.length
+
// should eventually resolve after adding 1 button
cy
.get('button').as('btns')
.get('@btns').should('have.length', length).then(($buttons) => {
+ expect(replayCommandsFrom).not.to.be.called
+
+ // get, as, get, should, then == 5
+ expect(cy.queue.length - existingLen).to.eq(5) // we should not have replayed any commands
+
expect($buttons.length).to.eq(length)
})
})
@@ -371,22 +397,30 @@ describe('src/cy/commands/querying', () => {
})
})
- it('logs primitive aliases', () => {
- cy.noop('foo').as('f')
- .get('@f').then(function () {
- expect(this.lastLog.pick('$el', 'numRetries', 'referencesAlias', 'aliasType')).to.deep.eq({
- referencesAlias: { name: 'f' },
- aliasType: 'primitive',
- })
+ it('logs primitive aliases', (done) => {
+ cy.on('log:added', (attrs, log) => {
+ if (attrs.name === 'get') {
+ expect(log.pick('$el', 'numRetries', 'referencesAlias', 'aliasType')).to.deep.eq({
+ referencesAlias: { name: 'f' },
+ aliasType: 'primitive',
+ })
+
+ done()
+ }
})
+
+ cy
+ .noop('foo').as('f')
+ .get('@f')
})
it('logs immediately before resolving', (done) => {
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'get') {
- expect(log.pick('state', 'referencesAlias')).to.deep.eq({
+ expect(log.pick('state', 'referencesAlias', 'aliasType')).to.deep.eq({
state: 'pending',
referencesAlias: undefined,
+ aliasType: 'dom',
})
done()
@@ -419,7 +453,7 @@ describe('src/cy/commands/querying', () => {
referencesAlias: undefined,
}
- expect(this.lastLog.get('$el')).to.eq($body)
+ expect(this.lastLog.get('$el').get(0)).to.eq($body.get(0))
_.each(obj, (value, key) => {
expect(this.lastLog.get(key)).deep.eq(value, `expected key: ${key} to eq value: ${value}`)
@@ -705,8 +739,11 @@ describe('src/cy/commands/querying', () => {
this.logs = []
cy.on('log:added', (attrs, log) => {
- this.lastLog = log
- this.logs.push(log)
+ if (attrs.name === 'get') {
+ this.lastLog = log
+
+ this.logs.push(log)
+ }
})
return null
@@ -883,6 +920,32 @@ describe('src/cy/commands/querying', () => {
.get('@getUsers.0')
})
+ it('throws when alias property isnt just a digit', (done) => {
+ cy.on('fail', (err) => {
+ expect(err.message).to.include('`1b` is not a valid alias property. Only `numbers` or `all` is permitted.')
+
+ done()
+ })
+
+ cy
+ .server()
+ .route(/users/, {}).as('getUsers')
+ .get('@getUsers.1b')
+ })
+
+ it('throws when alias property isnt a digit or `all`', (done) => {
+ cy.on('fail', (err) => {
+ expect(err.message).to.include('`all ` is not a valid alias property. Only `numbers` or `all` is permitted.')
+
+ done()
+ })
+
+ cy
+ .server()
+ .route(/users/, {}).as('getUsers')
+ .get('@getUsers.all ')
+ })
+
_.each(['', 'foo', [], 1, null], (value) => {
it(`throws when options property is not an object. Such as: ${value}`, (done) => {
cy.on('fail', (err) => {
@@ -904,8 +967,7 @@ describe('src/cy/commands/querying', () => {
expect(lastLog.get('state')).to.eq('failed')
expect(lastLog.get('error')).to.eq(err)
expect(lastLog.get('$el').get(0)).to.eq(button.get(0))
-
- const consoleProps = this.logs[0].invoke('consoleProps')
+ const consoleProps = lastLog.invoke('consoleProps')
expect(consoleProps.Yielded).to.eq(button.get(0))
expect(consoleProps.Elements).to.eq(button.length)
@@ -1470,20 +1532,16 @@ space
})
describe('special characters', () => {
- const specialCharacters = '\' " [ ] { } . @ # $ % ^ & * ( ) , ; :'.split(' ')
-
- it(`finds content by string with characters`, () => {
- _.each(specialCharacters, (char) => {
+ _.each('\' " [ ] { } . @ # $ % ^ & * ( ) , ; :'.split(' '), (char) => {
+ it(`finds content by string with character: ${char}`, () => {
const span = $(`special char ${char} content`).appendTo(cy.$$('body'))
cy.contains('span', char).then(($span) => {
expect($span.get(0)).to.eq(span.get(0))
})
})
- })
- it(`finds content by regex with characters`, () => {
- _.each(specialCharacters, (char) => {
+ it(`finds content by regex with character: ${char}`, () => {
const span = $(`special char ${char} content`).appendTo(cy.$$('body'))
cy.contains('span', new RegExp(_.escapeRegExp(char))).then(($span) => {
@@ -1614,18 +1672,16 @@ space
}, () => {
beforeEach(function () {
this.logs = []
- this.listener = (attrs, log) => {
- this.lastLog = log
- this.logs.push(log)
- }
- cy.on('log:added', this.listener)
+ cy.on('log:added', (attrs, log) => {
+ if (attrs.name === 'contains') {
+ this.lastLog = log
- return null
- })
+ this.logs.push(log)
+ }
+ })
- afterEach(function () {
- cy.removeListener('log:added', this.listener)
+ return null
})
_.each([undefined, null], (val) => {
@@ -1724,13 +1780,10 @@ space
const button = cy.$$('#button')
cy.on('fail', (err) => {
- const [containsLog, shouldLog] = this.logs
-
- expect(shouldLog.get('state')).to.eq('failed')
- expect(shouldLog.get('error')).to.eq(err)
- expect(shouldLog.get('$el').get(0)).to.eq(button.get(0))
-
- const consoleProps = containsLog.invoke('consoleProps')
+ expect(this.lastLog.get('state')).to.eq('failed')
+ expect(this.lastLog.get('error')).to.eq(err)
+ expect(this.lastLog.get('$el').get(0)).to.eq(button.get(0))
+ const consoleProps = this.lastLog.invoke('consoleProps')
expect(consoleProps.Yielded).to.eq(button.get(0))
expect(consoleProps.Elements).to.eq(button.length)
@@ -1743,7 +1796,7 @@ space
it('throws when assertion is have.length > 1', function (done) {
cy.on('fail', (err) => {
- assertLogLength(this.logs, 2)
+ assertLogLength(this.logs, 1)
expect(err.message).to.eq('`cy.contains()` cannot be passed a `length` option because it will only ever return 1 element.')
expect(err.docsUrl).to.eq('https://on.cypress.io/contains')
diff --git a/packages/driver/cypress/e2e/commands/querying/shadow_dom.cy.js b/packages/driver/cypress/e2e/commands/querying/shadow_dom.cy.js
index 92ef326170d9..7e1e8fba3299 100644
--- a/packages/driver/cypress/e2e/commands/querying/shadow_dom.cy.js
+++ b/packages/driver/cypress/e2e/commands/querying/shadow_dom.cy.js
@@ -1,3 +1,5 @@
+const helpers = require('../../../support/helpers')
+
const { _ } = Cypress
describe('src/cy/commands/querying - shadow dom', () => {
@@ -216,7 +218,7 @@ describe('src/cy/commands/querying - shadow dom', () => {
cy.get('#shadow-element-1').shadow()
.then(function ($el) {
expect(this.lastLog.invoke('consoleProps')).to.deep.eq({
- 'Applied To': cy.$$('#shadow-element-1')[0],
+ 'Applied To': helpers.getFirstSubjectByName('get').get(0),
Yielded: Cypress.dom.getElements($el),
Elements: $el.length,
Command: 'shadow',
diff --git a/packages/driver/cypress/e2e/commands/querying/within.cy.js b/packages/driver/cypress/e2e/commands/querying/within.cy.js
index 701ceac17ede..d759ab8092d2 100644
--- a/packages/driver/cypress/e2e/commands/querying/within.cy.js
+++ b/packages/driver/cypress/e2e/commands/querying/within.cy.js
@@ -287,7 +287,7 @@ describe('src/cy/commands/querying/within', () => {
})
cy.on('fail', (err) => {
- expect(err.message).to.include('`cy.within()` failed because it requires a DOM element')
+ expect(err.message).to.include('`cy.within()` failed because this element')
done()
})
diff --git a/packages/driver/cypress/e2e/commands/traversals.cy.js b/packages/driver/cypress/e2e/commands/traversals.cy.js
index 44de3d22b7f0..5f2f08b9402d 100644
--- a/packages/driver/cypress/e2e/commands/traversals.cy.js
+++ b/packages/driver/cypress/e2e/commands/traversals.cy.js
@@ -1,6 +1,8 @@
const { assertLogLength } = require('../../support/utils')
const { _, $, dom } = Cypress
+const helpers = require('../../support/helpers')
+
describe('src/cy/commands/traversals', () => {
beforeEach(() => {
cy.visit('/fixtures/dom.html')
@@ -93,7 +95,7 @@ describe('src/cy/commands/traversals', () => {
})
cy.on('fail', (err) => {
- expect(err.message).to.include(`\`cy.${name}()\` failed because it requires a DOM element`)
+ expect(err.message).to.include(`\`cy.${name}()\` failed because this element`)
done()
})
@@ -206,7 +208,7 @@ describe('src/cy/commands/traversals', () => {
const yielded = Cypress.dom.getElements($el)
_.extend(obj, {
- 'Applied To': cy.$$('#list')[0],
+ 'Applied To': helpers.getFirstSubjectByName('get').get(0),
Yielded: yielded,
Elements: $el.length,
})
diff --git a/packages/driver/cypress/e2e/cypress/command_queue.cy.ts b/packages/driver/cypress/e2e/cypress/command_queue.cy.ts
index 0b47805e81da..be546a1bd627 100644
--- a/packages/driver/cypress/e2e/cypress/command_queue.cy.ts
+++ b/packages/driver/cypress/e2e/cypress/command_queue.cy.ts
@@ -11,6 +11,7 @@ const createCommand = (props = {}) => {
type: 'parent',
chainerId: _.uniqueId('ch'),
userInvocationStack: '',
+ injected: false,
fn () {},
}, props))
}
@@ -24,18 +25,16 @@ const log = (props = {}) => {
describe('src/cypress/command_queue', () => {
let queue
const state = (() => {}) as StateFunc
+ const timeout = () => {}
const whenStable = {} as IStability
- const stubCy = {
- timeout: () => {},
- cleanup: () => 0,
- fail: () => {},
- isCy: () => true,
- clearTimeout: () => {},
- setSubjectForChainer: () => {},
- }
+ const cleanup = () => 0
+ const fail = () => {}
+ const isCy = () => true
+ const clearTimeout = () => {}
+ const setSubjectForChainer = () => {}
beforeEach(() => {
- queue = new CommandQueue(state, whenStable, stubCy as any)
+ queue = new CommandQueue(state, timeout, whenStable, cleanup, fail, isCy, clearTimeout, setSubjectForChainer)
queue.add(createCommand({
name: 'get',
diff --git a/packages/driver/cypress/e2e/cypress/cy.cy.js b/packages/driver/cypress/e2e/cypress/cy.cy.js
index d84572ad8a7a..32dddd8e3551 100644
--- a/packages/driver/cypress/e2e/cypress/cy.cy.js
+++ b/packages/driver/cypress/e2e/cypress/cy.cy.js
@@ -493,46 +493,4 @@ describe('driver/src/cypress/cy', () => {
cy.bar()
})
})
-
- context('queries', {
- defaultCommandTimeout: 30,
- }, () => {
- it('throws when queries return a promise', (done) => {
- cy.on('fail', (err) => {
- expect(err.message).to.include('`cy.aQuery()` failed because you returned a promise from a query.\n\nQueries must be synchronous functions that return a function. You cannot invoke commands or return promises inside of them.')
- done()
- })
-
- Cypress.Commands._overwriteQuery('aQuery', () => Promise.resolve())
- cy.aQuery()
- })
-
- it('throws when a query returns a non-function value', (done) => {
- cy.on('fail', (err) => {
- expect(err.message).to.include('`cy.aQuery()` failed because you returned a value other than a function from a query.\n\nQueries must be synchronous functions that return a function.\n\nThe returned value was:\n\n > `1`')
- done()
- })
-
- Cypress.Commands._overwriteQuery('aQuery', () => 1)
- cy.aQuery()
- })
-
- it('throws when a command is invoked inside a query', (done) => {
- cy.on('fail', (err) => {
- expect(err.message).to.include('`cy.aQuery()` failed because you invoked a command inside a query.\n\nQueries must be synchronous functions that return a function. You cannot invoke commands or return promises inside of them.\n\nThe command invoked was:\n\n > `cy.visit()`')
- done()
- })
-
- Cypress.Commands._overwriteQuery('aQuery', () => cy.visit('/'))
- cy.aQuery()
- })
-
- // TODO: Make this work. Setting aside for now.
- it.skip('does allow queries to use other queries', () => {
- Cypress.Commands._overwriteQuery('aQuery', () => cy.bQuery())
- Cypress.Commands._overwriteQuery('bQuery', () => {})
-
- cy.aQuery()
- })
- })
})
diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
index a573356ba828..e929d07d6116 100644
--- a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
@@ -201,10 +201,10 @@ it('verifies number of cy commands', () => {
const actualCommands = Cypress._.reject(Object.keys(cy.commandFns), (command) => customCommands.includes(command))
const expectedCommands = [
'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select',
- 'selectFile', 'submit', 'type', 'clear', 'trigger', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then',
+ 'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then',
'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile',
'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit',
- 'focused', 'get', 'contains', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not',
+ 'focused', 'get', 'contains', 'root', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not',
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev',
'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'origin',
'mount',
diff --git a/packages/driver/cypress/support/helpers.js b/packages/driver/cypress/support/helpers.js
index 91f929ad7bea..c9cce623b9b3 100644
--- a/packages/driver/cypress/support/helpers.js
+++ b/packages/driver/cypress/support/helpers.js
@@ -1,5 +1,9 @@
const { _ } = Cypress
+const getFirstSubjectByName = (name) => {
+ return cy.queue.find({ name }).get('subject')
+}
+
const getQueueNames = () => {
return _.map(cy.queue, 'name')
}
@@ -24,5 +28,6 @@ function allowTsModuleStubbing () {
module.exports = {
getQueueNames,
+ getFirstSubjectByName,
allowTsModuleStubbing,
}
diff --git a/packages/driver/src/cy/aliases.ts b/packages/driver/src/cy/aliases.ts
index f842efd95059..ddd27ce60655 100644
--- a/packages/driver/src/cy/aliases.ts
+++ b/packages/driver/src/cy/aliases.ts
@@ -1,13 +1,9 @@
import _ from 'lodash'
import type { $Cy } from '../cypress/cy'
-import $utils from '../cypress/utils'
import $errUtils from '../cypress/error_utils'
-export const aliasRe = /^@.+/
-
-export const aliasIndexRe = /\.(all|[\d]+)$/
-
+const aliasRe = /^@.+/
const aliasDisplayRe = /^([@]+)/
const requestXhrRe = /\.request$/
@@ -20,14 +16,16 @@ const aliasDisplayName = (name) => {
// eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces
export const create = (cy: $Cy) => ({
addAlias (ctx, aliasObj) {
- const { alias } = aliasObj
+ const { alias, subject } = aliasObj
const aliases = cy.state('aliases') || {}
aliases[alias] = aliasObj
cy.state('aliases', aliases)
- ctx[alias] = $utils.getSubjectFromChain(aliasObj.subjectChain, cy)
+ const remoteSubject = cy.getRemotejQueryInstance(subject)
+
+ ctx[alias] = remoteSubject ?? subject
},
getAlias (name, cmd, log) {
diff --git a/packages/driver/src/cy/assertions.ts b/packages/driver/src/cy/assertions.ts
index 39faceb0db95..3966eb155f5e 100644
--- a/packages/driver/src/cy/assertions.ts
+++ b/packages/driver/src/cy/assertions.ts
@@ -104,6 +104,10 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
return assertions
}
+ const injectAssertionFns = (cmds) => {
+ return _.map(cmds, injectAssertion)
+ }
+
const injectAssertion = (cmd) => {
return ((subject) => {
// set assertions to itself or empty array
@@ -238,7 +242,6 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
ensureExistenceFor?: 'subject' | 'dom' | boolean
onFail?: (err?, isDefaultAssertionErr?: boolean, cmds?: any[]) => void
onRetry?: () => any
- subjectFn?: () => any
}
return {
@@ -304,6 +307,8 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
// ensure the error is about existence not about
// the downstream assertion.
try {
+ // Ensure the command is on the same origin as the AUT
+ cy.ensureCommandCanCommunicateWithAUT(err)
ensureExistence()
} catch (e2) {
err = e2
@@ -346,14 +351,6 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
return
}
- if (callbacks.subjectFn) {
- try {
- subject = callbacks.subjectFn()
- } catch (err) {
- return onFailFn(err)
- }
- }
-
// bail if we have no assertions and apply
// the default assertions if applicable
if (!cmds.length) {
@@ -386,7 +383,7 @@ export const create = (Cypress: ICypress, cy: $Cy) => {
return assertFn.apply(this, args.concat(true) as any)
}
- const fns = _.map(cmds, injectAssertion)
+ const fns = injectAssertionFns(cmds)
// TODO: remove any when the type of subject, the first argument of this function is specified.
const subjects: any[] = []
diff --git a/packages/driver/src/cy/commands/actions/selectFile.ts b/packages/driver/src/cy/commands/actions/selectFile.ts
index e680977c5ffa..946587b68894 100644
--- a/packages/driver/src/cy/commands/actions/selectFile.ts
+++ b/packages/driver/src/cy/commands/actions/selectFile.ts
@@ -4,7 +4,6 @@ import mime from 'mime-types'
import $dom from '../../../dom'
import $errUtils from '../../../cypress/error_utils'
-import $utils from '../../../cypress/utils'
import $actionability from '../../actionability'
import { addEventCoords, dispatch } from './trigger'
@@ -128,17 +127,15 @@ export default (Commands, Cypress, cy, state, config) => {
return
}
- const contents = $utils.getSubjectFromChain(aliasObj.subjectChain, cy)
-
- if (contents == null) {
+ if (aliasObj.subject == null) {
$errUtils.throwErrByPath('selectFile.invalid_alias', {
onFail: options._log,
- args: { alias: file.contents, subject: contents },
+ args: { alias: file.contents, subject: aliasObj.subject },
})
}
- if ($dom.isElement(contents)) {
- const subject = $dom.stringify(contents)
+ if ($dom.isElement(aliasObj.subject)) {
+ const subject = $dom.stringify(aliasObj.subject)
$errUtils.throwErrByPath('selectFile.invalid_alias', {
onFail: options._log,
@@ -149,7 +146,7 @@ export default (Commands, Cypress, cy, state, config) => {
return {
fileName: aliasObj.fileName,
...file,
- contents,
+ contents: aliasObj.subject,
}
}
diff --git a/packages/driver/src/cy/commands/agents.ts b/packages/driver/src/cy/commands/agents.ts
index 0e7cb24315fd..611e2573a951 100644
--- a/packages/driver/src/cy/commands/agents.ts
+++ b/packages/driver/src/cy/commands/agents.ts
@@ -235,7 +235,7 @@ export default function (Commands, Cypress, cy, state) {
agent.as = (alias) => {
cy.validateAlias(alias)
cy.addAlias(ctx, {
- subjectChain: [agent],
+ subject: agent,
command: log,
alias,
})
diff --git a/packages/driver/src/cy/commands/aliasing.ts b/packages/driver/src/cy/commands/aliasing.ts
index 5cc8fc86bdaa..e784164aac8b 100644
--- a/packages/driver/src/cy/commands/aliasing.ts
+++ b/packages/driver/src/cy/commands/aliasing.ts
@@ -1,54 +1,52 @@
import _ from 'lodash'
import $dom from '../../dom'
-export default function (Commands, Cypress, cy) {
- Commands._addQuery('as', function asFn (alias) {
- cy.ensureChildCommand(this, [alias])
- cy.validateAlias(alias)
+export default function (Commands, Cypress, cy, state) {
+ Commands.addAll({ type: 'utility', prevSubject: true }, {
+ as (subject, str) {
+ const ctx = this
- const prevCommand = cy.state('current').get('prev')
+ cy.validateAlias(str)
- prevCommand.set('alias', alias)
+ // this is the previous command
+ // which we are setting the alias as
+ const prev = state('current').get('prev')
- // Shallow clone of the existing subject chain, so that future commands running on the same chainer
- // don't apply here as well.
- const chainerId = cy.state('chainerId')
- const subjectChain = [...cy.state('subjects')[chainerId]]
+ prev.set('alias', str)
- const fileName = prevCommand.get('fileName')
-
- cy.addAlias(cy.state('ctx'), { subjectChain, command: prevCommand, alias, fileName })
-
- // Only need to update the log messages of previous commands once.
- // Subsequent invocations can shortcut to just return the subject unchanged.
- let alreadyDone = false
-
- return (subject) => {
- if (alreadyDone) {
- return subject
+ const noLogFromPreviousCommandIsAlreadyAliased = () => {
+ return _.every(prev.get('logs'), (log) => {
+ return log.get('alias') !== str
+ })
}
- alreadyDone = true
-
// TODO: change the log type from `any` to `Log`.
// we also need to set the alias on the last command log
// that matches our chainerId
const log: any = _.last(cy.queue.logs({
instrument: 'command',
event: false,
- chainerId,
+ chainerId: state('chainerId'),
}))
- const alreadyAliasedLog = _.map(prevCommand.get('logs'), 'attributes.alias').find((a) => a === alias)
-
- if (!alreadyAliasedLog && log) {
- log.set({
- alias,
- aliasType: $dom.isElement(subject) ? 'dom' : 'primitive',
- })
+ if (log) {
+ // make sure this alias hasn't already been applied
+ // to the previous command's logs by looping through
+ // all of its logs and making sure none of them are
+ // set to this alias
+ if (noLogFromPreviousCommandIsAlreadyAliased()) {
+ log.set({
+ alias: str,
+ aliasType: $dom.isElement(subject) ? 'dom' : 'primitive',
+ })
+ }
}
+ const fileName = prev.get('fileName')
+
+ cy.addAlias(ctx, { subject, command: prev, alias: str, fileName })
+
return subject
- }
+ },
})
}
diff --git a/packages/driver/src/cy/commands/misc.ts b/packages/driver/src/cy/commands/misc.ts
index cf0f1238c5b3..cc6a616979de 100644
--- a/packages/driver/src/cy/commands/misc.ts
+++ b/packages/driver/src/cy/commands/misc.ts
@@ -1,6 +1,7 @@
import _ from 'lodash'
import Promise from 'bluebird'
+import $Command from '../../cypress/command'
import $dom from '../../dom'
import $errUtils from '../../cypress/error_utils'
import type { Log } from '../../cypress/log'
@@ -23,6 +24,22 @@ export default (Commands, Cypress, cy, state) => {
},
log (msg, ...args) {
+ // https://github.com/cypress-io/cypress/issues/8084
+ // The return value of cy.log() corrupts the command stack, so cy.then() returned the wrong value
+ // when cy.log() is used inside it.
+ // The code below restore the stack when cy.log() is injected in cy.then().
+ if (state('current').get('injected')) {
+ const restoreCmdIndex = state('index') + 1
+
+ cy.queue.insert(restoreCmdIndex, $Command.create({
+ args: [cy.currentSubject()],
+ name: 'log-restore',
+ fn: (subject) => subject,
+ }))
+
+ state('index', restoreCmdIndex)
+ }
+
Cypress.log({
end: true,
snapshot: true,
diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts
index 9cd10c40d4ef..e69c740a975f 100644
--- a/packages/driver/src/cy/commands/navigation.ts
+++ b/packages/driver/src/cy/commands/navigation.ts
@@ -300,11 +300,6 @@ const stabilityChanged = async (Cypress, state, config, stable) => {
message: '--waiting for new page to load--',
event: true,
timeout: options.timeout,
- // If this was triggered as part of a cypress command, eg, clicking a form submit button, we don't want our
- // snapshot timing tied to when the current command resolves. This empty 'snapshots' array prevents
- // command.snapshotLogs() - which the command queue calls as part of resolving each command - from creating a
- // snapshot too early.
- snapshots: [],
consoleProps () {
return {
Note: 'This event initially fires when your application fires its \'beforeunload\' event and completes when your application fires its \'load\' event after the next page loads.',
diff --git a/packages/driver/src/cy/commands/querying/querying.ts b/packages/driver/src/cy/commands/querying/querying.ts
index 1b1df2ca9a55..64936aa91951 100644
--- a/packages/driver/src/cy/commands/querying/querying.ts
+++ b/packages/driver/src/cy/commands/querying/querying.ts
@@ -5,148 +5,8 @@ import $dom from '../../../dom'
import $elements from '../../../dom/elements'
import $errUtils from '../../../cypress/error_utils'
import type { Log } from '../../../cypress/log'
-import $utils from '../../../cypress/utils'
import { resolveShadowDomInclusion } from '../../../cypress/shadow_dom_utils'
import { getAliasedRequests, isDynamicAliasingPossible } from '../../net-stubbing/aliasing'
-import { aliasRe, aliasIndexRe } from '../../aliases'
-
-function getAlias (selector, log, cy) {
- const alias = selector.slice(1)
-
- return () => {
- let toSelect
-
- // Aliases come in two types: raw names, or names followed by an index.
- // For example, "@foo.bar" or "@foo.bar.1" or "@foo.bar.all", where 1 or all are the index.
- if ((cy.state('aliases') || {})[alias]) {
- toSelect = selector
- } else {
- // If the name isn't in our state, then this is probably a dynamic alias -
- // which is to say, it includes an index.
- toSelect = selector.replace(/\.(\d+|all)$/, '')
- }
-
- let aliasObj
-
- try {
- aliasObj = cy.getAlias(toSelect)
- } catch (err) {
- // possibly this is a dynamic alias, check to see if there is a request
- const requests = getAliasedRequests(alias, cy.state)
-
- if (!isDynamicAliasingPossible(cy.state) || !requests.length) {
- err.retry = false
- throw err
- }
-
- aliasObj = {
- alias,
- command: cy.state('routes')[requests[0].routeId].command,
- }
- }
-
- if (!aliasObj) {
- return
- }
-
- const { command } = aliasObj
-
- log && log.set('referencesAlias', { name: alias })
-
- /*
- * There are three cases for aliases, each explained in more detail below:
- * 1. Route aliases
- * 2. Intercept aliases
- * 3. Subject aliases (either DOM elements or primitives).
- */
-
- if (command.get('name') === 'route') {
- // In the case of a route alias, getRequestsByAlias handles selecting the proper index
- // and returns one or more requests.
- const requests = cy.getRequestsByAlias(alias) || null
-
- log && log.set({
- aliasType: 'route',
- consoleProps: () => {
- return {
- Alias: selector,
- Yielded: requests,
- }
- },
- })
-
- return requests
- }
-
- if (command.get('name') === 'intercept') {
- // Intercept aliases are fairly similar, but `getAliasedRequests` does *not* handle indexes
- // and we have to do it ourselves here.
-
- // Posible TODO: Unify this index identifying and selecting logic with that from `getRequestsByAlias`
- const requests = getAliasedRequests(aliasObj.alias, cy.state)
-
- // If the user provides an index ("@foo.1" or "@foo.all"), use that. Otherwise, return the most recent request.
- const match = selector.match(aliasIndexRe)
- const index = match ? match[1] : (requests.length - 1)
-
- const returnValue = index === 'all' ? requests : (requests[parseInt(index, 10)] || null)
-
- log && log.set({
- aliasType: 'intercept',
- consoleProps: () => {
- return {
- Alias: selector,
- Yielded: returnValue,
- }
- },
- })
-
- return returnValue
- }
-
- // If we've fallen through to here, then this is a subject alias - the result of one or more previous
- // cypress commands. We replay their subject chain (including possibly re-quering the DOM)
- // and use this as the result of the alias.
-
- // If we have a test similar to
- // cy.get('li').as('alias').then(li => li.remove())
- // cy.get('@alias').should('not.exist')
-
- // then Cypress can be very confused: the original 'get' command was not followed by 'should not exist'
- // but when reinvoked, it is! We therefore set a special piece of state,
- // which the 'should exist' assertion can read to determine if the *current* command is followed by a 'should not
- // exist' assertion.
- cy.state('aliasCurrentCommand', this)
- const subject = $utils.getSubjectFromChain(aliasObj.subjectChain, cy)
-
- cy.state('aliasCurrentCommand', undefined)
-
- if ($dom.isElement(subject)) {
- log && log.set({
- aliasType: 'dom',
- consoleProps: () => {
- return {
- Alias: selector,
- Yielded: $dom.getElements(subject),
- Elements: (subject as JQuery).length,
- }
- },
- })
- } else {
- log && log.set({
- aliasType: 'primitive',
- consoleProps: () => {
- return {
- Alias: selector,
- Yielded: subject,
- }
- },
- })
- }
-
- return subject
- }
-}
interface InternalGetOptions extends Partial {
_log?: Log
@@ -161,21 +21,6 @@ interface InternalContainsOptions extends Partial {
- /*
- * cy.get() is currently in a strange state: There are two implementations of it in this file, registered one after
- * another. It first is registered as a command (Commands.addAll()) - but below it, we *also* add .get()
- * via Commands._overwriteQuery(), which overwrites it.
- *
- * This is because other commands in the driver rely on the original .get() implementation, via
- * `cy.now('get', selector, getOptions)`.
- *
- * The key is that cy.now() relies on `cy.commandFns[name]` - which addAll() sets, but _overwriteQuery() does not.
- *
- * The upshot is that any test that relies on `cy.get()` is using the query-based implementation, but various
- * driver commands have access to the original implementation of .get() via cy.now(). This is a temporary state
- * of affairs while we refactor other commands to also be queries - we'll eventually be able to delete this
- * original version of .get() entirely.
- */
Commands.addAll({
get (selector, userOptions: Partial = {}) {
const ctx = this
@@ -311,8 +156,7 @@ export default (Commands, Cypress, cy, state) => {
}
if (aliasObj) {
- let { alias, command } = aliasObj
- let subject = $utils.getSubjectFromChain(aliasObj.subjectChain, cy)
+ let { subject, alias, command } = aliasObj
const resolveAlias = () => {
// if this is a DOM element
@@ -516,81 +360,6 @@ export default (Commands, Cypress, cy, state) => {
},
})
- Commands._overwriteQuery('get', function get (selector, userOptions: Partial = {}) {
- if ((userOptions === null) || _.isArray(userOptions) || !_.isPlainObject(userOptions)) {
- $errUtils.throwErrByPath('get.invalid_options', {
- args: { options: userOptions },
- })
- }
-
- const log = userOptions.log !== false && Cypress.log({
- message: selector,
- timeout: userOptions.timeout,
- consoleProps: () => ({}),
- })
-
- cy.state('current').set('timeout', userOptions.timeout)
- cy.state('current').set('_log', log)
-
- if (aliasRe.test(selector)) {
- return getAlias.call(this, selector, log, cy)
- }
-
- const withinSubject = cy.state('withinSubject')
- const includeShadowDom = resolveShadowDomInclusion(Cypress, userOptions.includeShadowDom)
-
- return () => {
- cy.ensureCommandCanCommunicateWithAUT()
-
- let $el
-
- try {
- let scope: (typeof withinSubject) | Node[] = withinSubject
-
- if (includeShadowDom) {
- const root = withinSubject ? withinSubject[0] : cy.state('document')
- const elementsWithShadow = $dom.findAllShadowRoots(root)
-
- scope = elementsWithShadow.concat(root)
- }
-
- $el = cy.$$(selector, scope)
-
- // jQuery v3 has removed its deprecated properties like ".selector"
- // https://jquery.com/upgrade-guide/3.0/breaking-change-deprecated-context-and-selector-properties-removed
- // but our error messages use this property to actually show the missing element
- // so let's put it back
- if ($el.selector == null) {
- $el.selector = selector
- }
- } catch (err: any) {
- if (err.message.startsWith('Syntax error')) {
- err.retry = false
- }
-
- // this is usually a sizzle error (invalid selector)
- if (log) {
- err.onFail = () => log.error(err)
- }
-
- throw err
- }
-
- log && log.set({
- $el,
- consoleProps: () => {
- return {
- Selector: selector,
- Yielded: $dom.getElements($el),
- Elements: $el.length,
- }
- },
- })
-
- return $el
- }
- })
-
Commands.addAll({ prevSubject: ['optional', 'window', 'document', 'element'] }, {
contains (subject, filter, text, userOptions: Partial = {}) {
// nuke our subject if its present but not an element.
diff --git a/packages/driver/src/cy/commands/querying/root.ts b/packages/driver/src/cy/commands/querying/root.ts
index 2498f3f1a439..6847c9b955e3 100644
--- a/packages/driver/src/cy/commands/querying/root.ts
+++ b/packages/driver/src/cy/commands/querying/root.ts
@@ -1,26 +1,40 @@
+import _ from 'lodash'
+import type { Log } from '../../../cypress/log'
+
+interface InternalRootOptions extends Partial {
+ _log?: Log
+}
+
export default (Commands, Cypress, cy, state) => {
- Commands._addQuery('root', function root (options: Partial = {}) {
- const log = options.log !== false && Cypress.log({
- timeout: options.timeout,
- })
-
- cy.state('current').set('timeout', options.timeout)
-
- return () => {
- cy.ensureCommandCanCommunicateWithAUT()
- const $el = state('withinSubject') || cy.$$('html')
-
- log && log.set({
- $el,
- consoleProps: () => {
- return {
- Command: 'root',
- Yielded: $el.get(0),
- }
- },
- })
-
- return $el
- }
+ Commands.addAll({
+ root (userOptions: Partial = {}) {
+ const options: InternalRootOptions = _.defaults({}, userOptions, { log: true })
+
+ if (options.log !== false) {
+ options._log = Cypress.log({
+ message: '',
+ timeout: options.timeout,
+ })
+ }
+
+ const log = ($el) => {
+ if (options._log) {
+ options._log!.set({ $el })
+ }
+
+ return $el
+ }
+
+ const withinSubject = state('withinSubject')
+
+ if (withinSubject) {
+ return log(withinSubject)
+ }
+
+ return cy.now('get', 'html', {
+ log: false,
+ timeout: options.timeout,
+ }).then(log)
+ },
})
}
diff --git a/packages/driver/src/cy/commands/waiting.ts b/packages/driver/src/cy/commands/waiting.ts
index 72a7b6f50abe..1c0a4873b2f8 100644
--- a/packages/driver/src/cy/commands/waiting.ts
+++ b/packages/driver/src/cy/commands/waiting.ts
@@ -205,7 +205,7 @@ export default (Commands, Cypress, cy, state) => {
// cy.route aliases have subject that has all XHR properties
// let's check one of them
- return aliasObj.subjectChain.length && Boolean((_.last(aliasObject.subjectChain) as any).xhrUrl)
+ return aliasObj.subject && Boolean(aliasObject.subject.xhrUrl)
}
if (command && !isNetworkInterceptCommand(command)) {
diff --git a/packages/driver/src/cy/commands/xhr.ts b/packages/driver/src/cy/commands/xhr.ts
index fbde1b49b868..a5ebef42aeb7 100644
--- a/packages/driver/src/cy/commands/xhr.ts
+++ b/packages/driver/src/cy/commands/xhr.ts
@@ -540,7 +540,7 @@ export default (Commands, Cypress, cy, state, config) => {
if (_.isString(o.response) && aliasObj) {
// reset the route's response to be the
// aliases subject
- options.response = $utils.getSubjectFromChain(aliasObj.subjectChain, cy)
+ options.response = aliasObj.subject
}
const url = getUrl(options)
diff --git a/packages/driver/src/cy/ensures.ts b/packages/driver/src/cy/ensures.ts
index 914d86382f7f..4c59bc2ad209 100644
--- a/packages/driver/src/cy/ensures.ts
+++ b/packages/driver/src/cy/ensures.ts
@@ -48,7 +48,7 @@ export const create = (state: StateFunc, expect: $Cy['expect']) => {
const ensureSubjectByType = (subject, type) => {
const current = state('current')
- let types: (string | boolean)[] = [].concat(type)
+ let types: string[] = [].concat(type)
// if we have an optional subject and nothing's
// here then just return cuz we good to go
@@ -103,21 +103,6 @@ export const create = (state: StateFunc, expect: $Cy['expect']) => {
}
}
- const ensureChildCommand = (command, args) => {
- const subjects = cy.state('subjects')
-
- if (subjects[command.get('chainerId')] === undefined) {
- const stringifiedArg = $utils.stringifyActual(args[0])
-
- $errUtils.throwErrByPath('miscellaneous.invoking_child_without_parent', {
- args: {
- cmd: command.get('name'),
- args: _.isString(args[0]) ? `\"${stringifiedArg}\"` : stringifiedArg,
- },
- })
- }
- }
-
const ensureElementIsNotAnimating = ($el, coords = [], threshold) => {
const lastTwo = coords.slice(-2)
@@ -272,6 +257,9 @@ export const create = (state: StateFunc, expect: $Cy['expect']) => {
}
const ensureElExistence = ($el) => {
+ // dont throw if this isnt even a DOM object
+ // return if not $dom.isJquery($el)
+
// ensure that we either had some assertions
// or that the element existed
if ($el && $el.length) {
@@ -444,7 +432,6 @@ export const create = (state: StateFunc, expect: $Cy['expect']) => {
// internal functions
ensureSubjectByType,
ensureRunnable,
- ensureChildCommand,
}
}
diff --git a/packages/driver/src/cy/xhrs.ts b/packages/driver/src/cy/xhrs.ts
index 754f13137def..61b0ddf10d44 100644
--- a/packages/driver/src/cy/xhrs.ts
+++ b/packages/driver/src/cy/xhrs.ts
@@ -3,6 +3,8 @@ import _ from 'lodash'
import $errUtils from '../cypress/error_utils'
import type { StateFunc } from '../cypress/state'
+const validAliasApiRe = /^(\d+|all)$/
+
const xhrNotWaitedOnByIndex = (state: StateFunc, alias: string, index: number, prop: 'requests' | 'responses') => {
// find the last request or response
// which hasnt already been used.
@@ -67,6 +69,12 @@ export const create = (state: StateFunc) => ({
prop = _.last(allParts)
}
+ if (prop && !validAliasApiRe.test(prop)) {
+ $errUtils.throwErrByPath('get.alias_invalid', {
+ args: { prop },
+ })
+ }
+
if (prop === '0') {
$errUtils.throwErrByPath('get.alias_zero', {
args: { alias },
diff --git a/packages/driver/src/cypress/chainer.ts b/packages/driver/src/cypress/chainer.ts
index 939489a7d4d5..96c63dcf05f6 100644
--- a/packages/driver/src/cypress/chainer.ts
+++ b/packages/driver/src/cypress/chainer.ts
@@ -1,7 +1,6 @@
+import _ from 'lodash'
import $stackUtils from './stack_utils'
-let idCounter = 1
-
export class $Chainer {
specWindow: Window
chainerId: string
@@ -11,7 +10,7 @@ export class $Chainer {
// The id prefix needs to be unique per origin, so there are not
// collisions when chainers created in a secondary origin are passed
// to the primary origin for the command log, etc.
- this.chainerId = `ch-${window.location.origin}-${idCounter++}`
+ this.chainerId = _.uniqueId(`ch-${window.location.origin}-`)
}
static remove (key) {
diff --git a/packages/driver/src/cypress/command.ts b/packages/driver/src/cypress/command.ts
index 9abb27d41886..afcd6d0a391f 100644
--- a/packages/driver/src/cypress/command.ts
+++ b/packages/driver/src/cypress/command.ts
@@ -1,8 +1,6 @@
import _ from 'lodash'
import utils from './utils'
-let idCounter = 1
-
export class $Command {
attributes!: Record
@@ -14,7 +12,7 @@ export class $Command {
// the id prefix needs to be unique per origin, so there are not
// collisions when commands created in a secondary origin are passed
// to the primary origin for the command log, etc.
- attrs.id = `${attrs.chainerId}-cmd-${idCounter++}`
+ attrs.id = _.uniqueId(`cmd-${window.location.origin}-`)
}
this.set(attrs)
@@ -35,39 +33,9 @@ export class $Command {
return this
}
- snapshotLogs () {
- this.get('logs').forEach((log) => {
- if (!log.get('snapshots')) {
- log.snapshot()
- }
- })
- }
-
finishLogs () {
- // TODO: Investigate whether or not we can reuse snapshots between logs
- // that snapshot at the same time
-
- // Finish each of the logs we have, turning any potential errors into actual ones.
- this.get('logs').forEach((log) => {
- if (log.get('next')) {
- log.snapshot()
- }
-
- if (log.get('_error')) {
- log.error(log.get('_error'))
- } else {
- log.set('snapshot', false)
- log.finish()
- }
- })
-
- // If the previous command is a query belonging to the same chainer,
- // we also ask it to end its own logs (and so on, up the chain).
- const prev = this.get('prev')
-
- if (prev && prev.get('query') && prev.get('chainerId') === this.get('chainerId')) {
- prev.finishLogs()
- }
+ // finish each of the logs we have
+ return _.invokeMap(this.get('logs'), 'finish')
}
log (log) {
diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts
index a09926246fdf..cc23dccbb590 100644
--- a/packages/driver/src/cypress/command_queue.ts
+++ b/packages/driver/src/cypress/command_queue.ts
@@ -11,6 +11,7 @@ import type $Command from './command'
import type { StateFunc } from './state'
import type { $Cy } from './cy'
import type { IStability } from '../cy/stability'
+import type { ITimeouts } from '../cy/timeouts'
const debugErrors = Debug('cypress:driver:errors')
@@ -19,8 +20,6 @@ const __stackReplacementMarker = (fn, args) => {
}
const commandRunningFailed = (Cypress, state, err) => {
- const current = state('current')
-
// allow for our own custom onFail function
if (err.onFail) {
err.onFail(err)
@@ -31,6 +30,8 @@ const commandRunningFailed = (Cypress, state, err) => {
return
}
+ const current = state('current')
+
return Cypress.log({
end: true,
snapshot: true,
@@ -55,68 +56,35 @@ const commandRunningFailed = (Cypress, state, err) => {
})
}
-/*
- * Queries are simple beasts: They take arguments, and return an idempotent function. They contain no retry
- * logic, have no awareness of cy.stop(), and are entirely synchronous.
- *
- * retryQuery is where we intergrate this simplicity with Cypress' retryability. It verifies the return value is
- * a sync function, and retries queries until they pass or time out. Commands invoke cy.verifyUpcomingAssertions
- * directly, but the command_queue is responsible for retrying queries.
- */
-function retryQuery (command: $Command, ret: any, cy: $Cy) {
- if ($utils.isPromiseLike(ret) && !cy.isCy(ret)) {
- $errUtils.throwErrByPath(
- 'query_command.returned_promise', {
- args: { name: command.get('name') },
- },
- )
- }
-
- if (!_.isFunction(ret)) {
- $errUtils.throwErrByPath(
- 'query_command.returned_non_function', {
- args: { name: command.get('name'), returned: ret },
- },
- )
- }
-
- const options = {
- timeout: command.get('timeout'),
- error: null,
- _log: command.get('_log'),
- }
-
- const onRetry = () => {
- return cy.verifyUpcomingAssertions(undefined, options, {
- onRetry,
- onFail: command.get('onFail'),
- subjectFn: () => {
- const subject = cy.currentSubject(command.get('chainerId'))
-
- cy.ensureSubjectByType(subject, command.get('prevSubject'))
-
- return ret(subject)
- },
- })
- }
-
- return onRetry()
-}
-
export class CommandQueue extends Queue<$Command> {
state: StateFunc
+ timeout: $Cy['timeout']
stability: IStability
- cy: $Cy
+ cleanup: $Cy['cleanup']
+ fail: $Cy['fail']
+ isCy: $Cy['isCy']
+ clearTimeout: ITimeouts['clearTimeout']
+ setSubjectForChainer: $Cy['setSubjectForChainer']
constructor (
state: StateFunc,
+ timeout: $Cy['timeout'],
stability: IStability,
- cy: $Cy,
+ cleanup: $Cy['cleanup'],
+ fail: $Cy['fail'],
+ isCy: $Cy['isCy'],
+ clearTimeout: ITimeouts['clearTimeout'],
+ setSubjectForChainer: $Cy['setSubjectForChainer'],
) {
super()
this.state = state
+ this.timeout = timeout
this.stability = stability
- this.cy = cy
+ this.cleanup = cleanup
+ this.fail = fail
+ this.isCy = isCy
+ this.clearTimeout = clearTimeout
+ this.setSubjectForChainer = setSubjectForChainer
}
logs (filter) {
@@ -173,9 +141,6 @@ export class CommandQueue extends Queue<$Command> {
}
private runCommand (command: $Command) {
- const isQuery = command.get('query')
- const name = command.get('name')
-
// bail here prior to creating a new promise
// because we could have stopped / canceled
// prior to ever making it through our first
@@ -198,21 +163,7 @@ export class CommandQueue extends Queue<$Command> {
let ret
let enqueuedCmd
- // Queries can invoke other queries - they are synchronous, and get added to the subject chain without
- // issue. But they cannot contain commands, which are async.
- // This callback watches to ensure users don't try and invoke any commands while inside a query.
const commandEnqueued = (obj) => {
- if (isQuery && !obj.query) {
- $errUtils.throwErrByPath(
- 'query_command.invoked_action', {
- args: {
- name,
- action: obj.name,
- },
- },
- )
- }
-
return enqueuedCmd = obj
}
@@ -230,14 +181,6 @@ export class CommandQueue extends Queue<$Command> {
// run the command's fn with runnable's context
try {
ret = __stackReplacementMarker(command.get('fn'), args)
-
- // Queries return a function which takes the current subject and returns the next subject. We wrap this in
- // retryQuery() - and let it retry until it passes, times out or is cancelled.
- // We save the original return value on the $Command though - it's what gets added to the subject chain later.
- if (isQuery) {
- command.set('queryFn', ret)
- ret = retryQuery(command, ret, this.cy)
- }
} catch (err) {
throw err
} finally {
@@ -250,7 +193,7 @@ export class CommandQueue extends Queue<$Command> {
// we cannot pass our cypress instance or our chainer
// back into bluebird else it will create a thenable
// which is never resolved
- if (this.cy.isCy(ret)) {
+ if (this.isCy(ret)) {
return null
}
@@ -258,7 +201,7 @@ export class CommandQueue extends Queue<$Command> {
$errUtils.throwErrByPath(
'miscellaneous.command_returned_promise_and_commands', {
args: {
- current: name,
+ current: command.get('name'),
called: enqueuedCmd.name,
},
},
@@ -275,7 +218,10 @@ export class CommandQueue extends Queue<$Command> {
// or an undefined value then throw
$errUtils.throwErrByPath(
'miscellaneous.returned_value_and_commands_from_custom_command', {
- args: { current: name, returned: ret },
+ args: {
+ current: command.get('name'),
+ returned: ret,
+ },
},
)
}
@@ -305,37 +251,8 @@ export class CommandQueue extends Queue<$Command> {
command.set({ subject })
- // When a command - query or normal - first passes, we verify that we have any snapshots needed.
- // The logs may still be pending, but we want to know the state of the DOM now, before any subsequent commands
- // run to alter it.
- command.snapshotLogs()
-
- if (isQuery) {
- subject = command.get('queryFn')
- // For queries, the "subject" here is the query's return value, which is a function which
- // accepts a subject and returns a subject, and can be re-invoked at any time.
-
- // We add the command name here only to make debugging easier; It should not be relied on functionally.
- subject.commandName = name
-
- // Even though we've snapshotted, we only end the logs a query's logs if we're at the end of a query
- // chain - either there is no next command (end of a test), the next command is an action, or the next
- // command belongs to another chainer (end of a chain).
-
- // This is done so that any query's logs remain in the 'pending' state until the subject chain is finished.
- this.cy.addQueryToChainer(command.get('chainerId'), subject)
-
- const next = command.get('next')
-
- if (!next || next.get('chainerId') !== command.get('chainerId') || !next.get('query')) {
- command.finishLogs()
- }
- } else {
- // For commands, the "subject" here is the command's return value, which replaces
- // the current subject chain. We cannot re-invoke commands - the return value here is final.
- this.cy.setSubjectForChainer(command.get('chainerId'), subject)
- command.finishLogs()
- }
+ // end / snapshot our logs if they need it
+ command.finishLogs()
// reset the nestedIndex back to null
this.state('nestedIndex', null)
@@ -343,6 +260,8 @@ export class CommandQueue extends Queue<$Command> {
// we're finished with the current command so set it back to null
this.state('current', null)
+ this.setSubjectForChainer(command.get('chainerId'), subject)
+
return subject
})
}
@@ -373,7 +292,7 @@ export class CommandQueue extends Queue<$Command> {
this.state('index', index + 1)
- this.cy.setSubjectForChainer(command.get('chainerId'), command.get('subject'))
+ this.setSubjectForChainer(command.get('chainerId'), command.get('subject'))
Cypress.action('cy:skipped:command:end', command)
@@ -397,12 +316,12 @@ export class CommandQueue extends Queue<$Command> {
}
// store the previous timeout
- const prevTimeout = this.cy.timeout()
+ const prevTimeout = this.timeout()
// If we have created a timeout but are in an unstable state, clear the
// timeout in favor of the on load timeout already running.
if (!this.state('isStable')) {
- this.cy.clearTimeout()
+ this.clearTimeout()
}
// store the current runnable
@@ -421,7 +340,7 @@ export class CommandQueue extends Queue<$Command> {
let fn
if (!runnable.state) {
- this.cy.timeout(prevTimeout)
+ this.timeout(prevTimeout)
}
// mutate index by incrementing it
@@ -471,13 +390,13 @@ export class CommandQueue extends Queue<$Command> {
commandRunningFailed(Cypress, this.state, err)
- return this.cy.fail(err)
+ return this.fail(err)
}
const { promise, reject, cancel } = super.run({
onRun: next,
onError,
- onFinish: this.cy.cleanup,
+ onFinish: this.cleanup,
})
this.state('promise', promise)
diff --git a/packages/driver/src/cypress/commands.ts b/packages/driver/src/cypress/commands.ts
index 74d62e1bfa50..13ee2648c62a 100644
--- a/packages/driver/src/cypress/commands.ts
+++ b/packages/driver/src/cypress/commands.ts
@@ -4,8 +4,6 @@ import { addCommand as addNetstubbingCommand } from '../cy/net-stubbing'
import $errUtils from './error_utils'
import $stackUtils from './stack_utils'
-import type { QueryFunction } from './utils'
-
const PLACEHOLDER_COMMANDS = ['mount', 'hover']
const builtInCommands = [
@@ -27,31 +25,79 @@ const getTypeByPrevSubject = (prevSubject) => {
return 'parent'
}
-const internalError = (path, name) => {
- $errUtils.throwErrByPath(path, {
- args: {
- name,
- },
- stack: (new cy.state('specWindow').Error('add command stack')).stack,
- errProps: {
- appendToStack: {
- title: 'From Cypress Internals',
- content: $stackUtils.stackWithoutMessage((new Error('add command internal stack')).stack || ''),
- } },
- })
-}
-
export default {
create: (Cypress, cy, state, config) => {
const reservedCommandNames = new Set(Object.keys(cy))
+ // create a single instance
+ // of commands
const commands = {}
-
+ const commandBackups = {}
// we track built in commands to ensure users cannot
// add custom commands with the same name
const builtInCommandNames = {}
let addingBuiltIns
+ const store = (obj) => {
+ commands[obj.name] = obj
+
+ return cy.addCommand(obj)
+ }
+
+ const storeOverride = (name, fn) => {
+ // grab the original function if its been backed up
+ // or grab it from the command store
+ const original = commandBackups[name] || commands[name]
+
+ if (!original) {
+ $errUtils.throwErrByPath('miscellaneous.invalid_overwrite', {
+ args: {
+ name,
+ },
+ })
+ }
+
+ // store the backup again now
+ commandBackups[name] = original
+
+ function originalFn (...args) {
+ const current = state('current')
+ let storedArgs = args
+
+ if (current.get('type') === 'child') {
+ storedArgs = args.slice(1)
+ }
+
+ current.set('args', storedArgs)
+
+ return original.fn.apply(this, args)
+ }
+
+ const overridden = _.clone(original)
+
+ overridden.fn = function (...args) {
+ args = ([] as any).concat(originalFn, args)
+
+ return fn.apply(this, args)
+ }
+
+ return cy.addCommand(overridden)
+ }
+
const Commands = {
+ _commands: commands, // for testing
+
+ each (fn) {
+ // perf loop
+ for (let name in commands) {
+ const command = commands[name]
+
+ fn(command)
+ }
+
+ // prevent loop comprehension
+ return null
+ },
+
addAllSync (obj) {
// perf loop
for (let name in obj) {
@@ -87,11 +133,31 @@ export default {
add (name, options, fn) {
if (builtInCommandNames[name]) {
- internalError('miscellaneous.invalid_new_command', name)
+ $errUtils.throwErrByPath('miscellaneous.invalid_new_command', {
+ args: {
+ name,
+ },
+ stack: (new state('specWindow').Error('add command stack')).stack,
+ errProps: {
+ appendToStack: {
+ title: 'From Cypress Internals',
+ content: $stackUtils.stackWithoutMessage((new Error('add command internal stack')).stack || ''),
+ } },
+ })
}
if (reservedCommandNames.has(name)) {
- internalError('miscellaneous.reserved_command', name)
+ $errUtils.throwErrByPath('miscellaneous.reserved_command', {
+ args: {
+ name,
+ },
+ stack: (new state('specWindow').Error('add command stack')).stack,
+ errProps: {
+ appendToStack: {
+ title: 'From Cypress Internals',
+ content: $stackUtils.stackWithoutMessage((new Error('add command internal stack')).stack!),
+ } },
+ })
}
// .hover & .mount are special case commands. allow as builtins so users
@@ -109,68 +175,27 @@ export default {
// normalize type by how they validate their
// previous subject (unless they're explicitly set)
- const type = options.type ?? getTypeByPrevSubject(prevSubject)
+ options.type = options.type ?? getTypeByPrevSubject(prevSubject)
+ const type = options.type
- commands[name] = {
+ return store({
name,
fn,
type,
prevSubject,
- }
-
- return cy.addCommand(commands[name])
+ })
},
overwrite (name, fn) {
- const original = commands[name]
-
- if (!original) {
- internalError('miscellaneous.invalid_overwrite', name)
- }
-
- function originalFn (...args) {
- const current = state('current')
- let storedArgs = args
-
- if (current.get('type') === 'child') {
- storedArgs = args.slice(1)
- }
-
- current.set('args', storedArgs)
-
- return original.fn.apply(this, args)
- }
-
- const overridden = _.clone(original)
-
- overridden.fn = function (...args) {
- args = ([] as any).concat(originalFn, args)
-
- return fn.apply(this, args)
- }
-
- return cy.addCommand(overridden)
- },
-
- _addQuery (name: string, fn: () => QueryFunction) {
- if (reservedCommandNames.has(name)) {
- internalError('miscellaneous.reserved_command_query', name)
- }
-
- if (cy[name]) {
- internalError('miscellaneous.invalid_new_query', name)
- }
-
- cy._addQuery({ name, fn })
- },
-
- _overwriteQuery (name, fn) {
- cy._addQuery({ name, fn })
+ return storeOverride(name, fn)
},
}
addingBuiltIns = true
+ // perf loop
for (let cmd of builtInCommands) {
+ // support "export default" syntax
+ cmd = cmd.default || cmd
cmd(Commands, Cypress, cy, state, config)
}
addingBuiltIns = false
diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts
index c6990885c033..9cb52cb136e6 100644
--- a/packages/driver/src/cypress/cy.ts
+++ b/packages/driver/src/cypress/cy.ts
@@ -3,7 +3,8 @@ import _ from 'lodash'
import Promise from 'bluebird'
import debugFn from 'debug'
-import $utils, { SubjectChain } from './utils'
+import $dom from '../dom'
+import $utils from './utils'
import $errUtils, { ErrorFromProjectRejectionEvent } from './error_utils'
import $stackUtils from './stack_utils'
@@ -210,7 +211,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
// Private methods
ensureSubjectByType: ReturnType['ensureSubjectByType']
ensureRunnable: ReturnType['ensureRunnable']
- ensureChildCommand: ReturnType['ensureChildCommand']
onBeforeWindowLoad: ReturnType['onBeforeWindowLoad']
@@ -244,14 +244,13 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
this.reset = this.reset.bind(this)
this.addCommandSync = this.addCommandSync.bind(this)
this.addCommand = this.addCommand.bind(this)
- this._addQuery = this._addQuery.bind(this)
this.now = this.now.bind(this)
+ this.replayCommandsFrom = this.replayCommandsFrom.bind(this)
this.onBeforeAppWindowLoad = this.onBeforeAppWindowLoad.bind(this)
this.onUncaughtException = this.onUncaughtException.bind(this)
this.setRunnable = this.setRunnable.bind(this)
this.cleanup = this.cleanup.bind(this)
this.setSubjectForChainer = this.setSubjectForChainer.bind(this)
- this.currentSubject = this.currentSubject.bind(this)
// init traits
@@ -351,7 +350,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
this.ensureSubjectByType = ensures.ensureSubjectByType
this.ensureRunnable = ensures.ensureRunnable
- this.ensureChildCommand = ensures.ensureChildCommand
this.ensureCommandCanCommunicateWithAUT = ensures.ensureCommandCanCommunicateWithAUT
const snapshots = createSnapshots(this.$$, state)
@@ -364,7 +362,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
this.overrides = createOverrides(state, config, focused, snapshots)
- this.queue = new CommandQueue(state, stability, this)
+ this.queue = new CommandQueue(state, this.timeout, stability, this.cleanup, this.fail, this.isCy, this.clearTimeout, this.setSubjectForChainer)
setTopOnError(Cypress, this)
@@ -374,7 +372,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
extendEvents(this)
Cypress.on('enqueue:command', (attrs: Cypress.EnqueuedCommand) => {
- this.enqueue($Command.create(attrs))
+ this.enqueue(attrs)
})
Cypress.on('cross:origin:automation:cookies', (cookies: AutomationCookie[]) => {
@@ -760,19 +758,23 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
// dont enqueue / inject any new commands if
// onInjectCommand returns false
const onInjectCommand = cy.state('onInjectCommand')
+ const injected = _.isFunction(onInjectCommand)
- if (_.isFunction(onInjectCommand) && onInjectCommand.call(cy, name, ...args) === false) {
- return
+ if (injected) {
+ if (onInjectCommand.call(cy, name, ...args) === false) {
+ return
+ }
}
- cy.enqueue($Command.create({
+ cy.enqueue({
name,
args,
type,
chainerId: chainer.chainerId,
userInvocationStack,
+ injected,
fn: firstCall ? cyFn : chainerFn,
- }))
+ })
}
$Chainer.add(name, callback)
@@ -829,76 +831,68 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
}
}
- _addQuery ({ name, fn }) {
- const cy = this
-
- const callback = (chainer, userInvocationStack, args) => {
- // dont enqueue / inject any new commands if
- // onInjectCommand returns false
- const onInjectCommand = cy.state('onInjectCommand')
-
- if (_.isFunction(onInjectCommand) && onInjectCommand.call(cy, name, ...args) === false) {
- return
- }
-
- // Queries are functions that accept args (which is called once each time the command is used in the spec
- // file), which return a function that accepts the subject (which is potentially called any number of times).
- // The outer function is used to store any needed state needed by a particular instance of the command, such as
- // a Cypress.log() instance, while the inner one (queryFn here) is the one that determines the next subject.
-
- // We enqueue the outer function as the "cypress command". See command_queue.ts for details on how this is
- // invoked and the inner function retried.
- const command = $Command.create({
- name,
- args,
- type: 'dual',
- chainerId: chainer.chainerId,
- userInvocationStack,
- query: true,
- prevSubject: 'optional',
- })
+ now (name, ...args) {
+ return Promise.resolve(
+ this.commandFns[name].apply(this, args),
+ )
+ }
- const cyFn = function (chainerId, ...args) {
- return fn.apply(command, args)
- }
+ replayCommandsFrom (current) {
+ const cy = this
- cyFn.originalFn = fn
- command.set('fn', cyFn)
+ // - starting with the aliased command
+ // - walk up to each prev command
+ // - until you reach a parent command
+ // - or until the subject is in the DOM
+ // - from that command walk down inserting
+ // every command which changed the subject
+ // - coming upon an assertion should only be
+ // inserted if the previous command should
+ // be replayed
- cy.enqueue(command)
- }
+ const commands = cy.getCommandsUntilFirstParentOrValidSubject(current)
- $Chainer.add(name, callback)
+ if (commands) {
+ let initialCommand = commands.shift()
- cy[name] = function (...args) {
- cy.ensureRunnable(name)
+ const commandsToInsert = _.reduce(commands, (memo, command, index) => {
+ const push = () => {
+ return memo.push(command)
+ }
- // this is the first call on cypress
- // so create a new chainer instance
- const chainer = new $Chainer(cy.specWindow)
+ if (!(command.get('type') !== 'assertion')) {
+ // if we're an assertion and the prev command
+ // is in the memo, then push this one
+ if (memo.includes(command.get('prev'))) {
+ push()
+ }
+ } else if (!(command.get('subject') === initialCommand.get('subject'))) {
+ // when our subjects dont match then
+ // reset the initialCommand to this command
+ // so the next commands can compare against
+ // this one to figure out the changing subjects
+ initialCommand = command
+
+ push()
+ }
- if (cy.state('chainerId')) {
- cy.linkSubject(chainer.chainerId, cy.state('chainerId'))
- }
+ return memo
+ }, [initialCommand])
- const userInvocationStack = $stackUtils.captureUserInvocationStack(cy.specWindow.Error)
+ const chainerId = this.state('chainerId')
- callback(chainer, userInvocationStack, args)
+ for (let c of commandsToInsert) {
+ // clone the command to prevent
+ // mutating its properties
+ const command = c.clone()
- // if we're the first call onto a cy
- // command, then kick off the run
- if (!cy.state('promise')) {
- cy.runQueue()
+ command.set('chainerId', chainerId)
+ cy.enqueue(command)
}
-
- return chainer
}
- }
- now (name, ...args) {
- return Promise.resolve(
- this.commandFns[name].apply(this, args),
- )
+ // prevent loop comprehension
+ return null
}
onBeforeAppWindowLoad (contentWindow) {
@@ -1121,7 +1115,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
return this.Cypress.action('app:navigation:changed', `page navigation event (${event})`)
}
- cleanup () {
+ private cleanup () {
// cleanup could be called during a 'stop' event which
// could happen in between a runnable because they are async
if (this.state('runnable')) {
@@ -1207,7 +1201,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
})
}
- private enqueue (command: $Command) {
+ private enqueue (obj: PartialBy) {
// if we have a nestedIndex it means we're processing
// nested commands and need to insert them into the
// index past the current index as opposed to
@@ -1234,9 +1228,37 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
// it onto the end of the queue
const index = _.isNumber(nestedIndex) ? nestedIndex : this.queue.length
- this.queue.insert(index, command)
+ this.queue.insert(index, $Command.create(obj))
- return this.Cypress.action('cy:command:enqueued', command.attributes)
+ return this.Cypress.action('cy:command:enqueued', obj)
+ }
+
+ // TODO: Replace any with Command type.
+ private getCommandsUntilFirstParentOrValidSubject (command, memo: any[] = []) {
+ if (!command) {
+ return null
+ }
+
+ // push these onto the beginning of the commands array
+ memo.unshift(command)
+
+ // break and return the memo
+ if ((command.get('type') === 'parent') || $dom.isAttached(command.get('subject'))) {
+ return memo
+ }
+
+ // A workaround to ensure that when looking back, aliases don't extend beyond the current
+ // chainer and commands invoked inside of it. This whole area (`replayCommandsFrom` for aliases)
+ // will be replaced with subject chains as part of the detached DOM work.
+ const chainerId = command.get('chainerId')
+ const prev = command.get('prev')
+ const prevChainer = prev.get('chainerId')
+
+ if (prevChainer !== chainerId && cy.state('subjectLinks')[prevChainer] !== chainerId) {
+ return memo
+ }
+
+ return this.getCommandsUntilFirstParentOrValidSubject(prev, memo)
}
private validateFirstCall (name, args, prevSubject: string[]) {
@@ -1258,7 +1280,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
private pushSubject (name, args, prevSubject: string[], chainerId) {
const subject = this.currentSubject(chainerId)
- if (prevSubject !== undefined) {
+ if (prevSubject) {
// make sure our current subject is valid for
// what we expect in this command
this.ensureSubjectByType(subject, prevSubject)
@@ -1273,26 +1295,18 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
* Use `currentSubject()` to get the subject. It reads from cy.state('subjects'), but the format and details of
* determining this should be considered an internal implementation detail of Cypress, subject to change at any time.
*
- * Currently, state('subjects') is an object, mapping chainerIds to the current subject and queries for that
- * chainer. For example, it might look like:
+ * Currently, state('subjects') is an object, mapping chainerIds to the current subject for that chainer. For
+ * example, it might look like:
*
* {
- * 'ch-http://localhost:3500-2': ['foobar'],
- * 'ch-http://localhost:3500-4': [],
- * 'ch-http://localhost:3500-4': [undefined, f(), f()],
+ * 'chainer2': 'foobar',
+ * 'chainer4': ,
* }
*
- * Do not read cy.state('subjects') directly; This is what currentSubject() is for, turning this structure into a
- * usable subject.
+ * Do not read this directly; Prefer currentSubject() instead.
*/
- currentSubject (chainerId: string = this.state('chainerId')) {
- const subjectChain: SubjectChain | undefined = (this.state('subjects') || {})[chainerId]
-
- if (subjectChain) {
- return $utils.getSubjectFromChain(subjectChain, this)
- }
-
- return undefined
+ currentSubject (chainerId = this.state('chainerId')) {
+ return (this.state('subjects') || {})[chainerId]
}
/*
@@ -1306,12 +1320,12 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
*
* In the current implementation, subjectLinks might look like:
* {
- * 'ch-http://localhost:3500-4': 'ch-http://localhost:3500-2',
+ * 'chainer4': 'chainer2',
* }
*
- * indicating that when we eventually resolve the subject of ch--4, it should *also* be used as the subject for
- * ch--2 - for example, `cy.then(() => { return cy.get('foo') }).log()`. The inner chainer (ch--4,
- * `cy.get('foo')`) is linked to the outer chainer (ch--2) - when we eventually .get('foo'), the resolved value
+ * indicating that when we eventually resolve the subject of chainer4, it should *also* be used as the subject for
+ * chainer2 - for example, `cy.then(() => { return cy.get('foo') }).log()`. The inner chainer (chainer4,
+ * `cy.get('foo')`) is linked to the outer chainer (chainer2) - when we eventually .get('foo'), the resolved value
* becomes the new subject for the outer chainer.
*
* Whenever we are in the middle of resolving one chainer and a new one is created, Cypress links the inner chainer
@@ -1324,10 +1338,10 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
* In this case, we want to break the connection between the inner chainer and the outer one, so that we can
* instead use the return value as the new subject. Is this case, you'll want cy.breakSubjectLinksToCurrentChainer().
*/
- linkSubject (childChainerId: string, parentChainerId: string) {
+ linkSubject (fromChainerId, toChainerId) {
const links = this.state('subjectLinks') || {}
- links[childChainerId] = parentChainerId
+ links[fromChainerId] = toChainerId
this.state('subjectLinks', links)
}
@@ -1349,17 +1363,17 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
/*
* setSubjectForChainer should be considered an internal implementation detail of Cypress. Do not use it directly
* outside of the Cypress codebase. It is currently used only by the command_queue, and if you think it's needed
- * elsewhere, consider if there's a way you can use existing functionality to achieve it instead.
+ * elsewhere, consider carefully before adding additional uses.
*
- * The command_queue calls setSubjectForChainer after a command has finished resolving, when we have the
- * final (non-$Chainer, non-promise) return value. This value becomes the current $Chainer's new subject - and the
- * new subject for any chainers it's linked to (see cy.linkSubject for details on that process).
+ * The command_queue calls setSubjectForChainer after a command has finished resolving, when we have the final
+ * (non-$Chainer, non-promise) return value. This value becomes the current $Chainer's new subject - and the new
+ * subject for any chainers it's linked to (see cy.linkSubject for details on that process).
*/
setSubjectForChainer (chainerId: string, subject: any) {
- const cySubjects = this.state('subjects') || {}
+ const cySubject = this.state('subjects') || {}
- cySubjects[chainerId] = [subject]
- this.state('subjects', cySubjects)
+ cySubject[chainerId] = subject
+ this.state('subjects', cySubject)
const links = this.state('subjectLinks') || {}
@@ -1368,31 +1382,6 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
}
}
- /*
- * addQueryToChainer should be considered an internal implementation detail of Cypress. Do not use it directly
- * outside of the Cypress codebase. It is currently used only by the command_queue, and if you think it's needed
- * elsewhere, consider if there's a way you can use existing functionality to achieve it instead.
- *
- * The command_queue calls addQueryToChainer after a query returns a function. This function is
- * is appended to the subject chain (which begins with 'undefined' if no previous subject exists), and used
- * to resolve cy.currentSubject() as needed.
- */
- addQueryToChainer (chainerId: string, queryFn: (subject: any) => any) {
- const cySubjects = this.state('subjects') || {}
-
- const subject = (cySubjects[chainerId] || [undefined]) as SubjectChain
-
- subject.push(queryFn)
- cySubjects[chainerId] = subject
- this.state('subjects', cySubjects)
-
- const links = this.state('subjectLinks') || {}
-
- if (links[chainerId]) {
- this.addQueryToChainer(links[chainerId], queryFn)
- }
- }
-
private doneEarly () {
this.queue.stop()
diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts
index 63f829a02bc2..c223a190be70 100644
--- a/packages/driver/src/cypress/error_messages.ts
+++ b/packages/driver/src/cypress/error_messages.ts
@@ -563,6 +563,10 @@ export default {
},
get: {
+ alias_invalid: {
+ message: '`{{prop}}` is not a valid alias property. Only `numbers` or `all` is permitted.',
+ docsUrl: 'https://on.cypress.io/get',
+ },
alias_zero: {
message: '`0` is not a valid alias property. Are you trying to ask for the first response? If so write `@{{alias}}.1`',
docsUrl: 'https://on.cypress.io/get',
@@ -861,14 +865,6 @@ export default {
message: '`Cypress.Commands.add()` cannot create a new command named `{{name}}` because that name is reserved internally by Cypress.',
docsUrl: 'https://on.cypress.io/custom-commands',
},
- invalid_new_query: {
- message: '`Cypress.Commands._addQuery()` is used to create new queries, but `{{name}}` is an existing Cypress command or query, or is reserved internally by Cypress.\n\n If you want to override an existing command or query, use `Cypress.Commands.overrideQuery()` instead.',
- docsUrl: 'https://on.cypress.io/custom-commands',
- },
- reserved_command_query: {
- message: '`Cypress.Commands._addQuery()` cannot create a new query named `{{name}}` because that name is reserved internally by Cypress.',
- docsUrl: 'https://on.cypress.io/custom-commands',
- },
invalid_overwrite: {
message: 'Cannot overwite command for: `{{name}}`. An existing command does not exist by that name.',
docsUrl: 'https://on.cypress.io/api',
@@ -1650,37 +1646,6 @@ export default {
},
},
- query_command: {
- docsUrl: 'https://on.cypress.io/custom-commands',
-
- returned_promise (obj) {
- return stripIndent`
- ${cmd(obj.name)} failed because you returned a promise from a query.
-
- Queries must be synchronous functions that return a function. You cannot invoke commands or return promises inside of them.`
- },
- invoked_action (obj) {
- return stripIndent`
- ${cmd(obj.name)} failed because you invoked a command inside a query.
-
- Queries must be synchronous functions that return a function. You cannot invoke commands or return promises inside of them.
-
- The command invoked was:
-
- > ${cmd(obj.action)}`
- },
- returned_non_function (obj) {
- return stripIndent`
- ${cmd(obj.name)} failed because you returned a value other than a function from a query.
-
- Queries must be synchronous functions that return a function.
-
- The returned value was:
-
- > \`${obj.returned}\``
- },
- },
-
selector_playground: {
defaults_invalid_arg: {
message: '`Cypress.SelectorPlayground.defaults()` must be called with an object. You passed: `{{arg}}`',
diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts
index c58469ea4c9f..30590923817f 100644
--- a/packages/driver/src/cypress/error_utils.ts
+++ b/packages/driver/src/cypress/error_utils.ts
@@ -147,7 +147,6 @@ const getUserInvocationStack = (err, state) => {
!userInvocationStack
|| err.isDefaultAssertionErr
|| (currentAssertionCommand && !current?.get('followedByShouldCallback'))
- || withInvocationStack?.get('selector')
) {
userInvocationStack = withInvocationStack?.get('userInvocationStack')
}
diff --git a/packages/driver/src/cypress/utils.ts b/packages/driver/src/cypress/utils.ts
index d9afe10461cf..bce7d4df618c 100644
--- a/packages/driver/src/cypress/utils.ts
+++ b/packages/driver/src/cypress/utils.ts
@@ -8,10 +8,6 @@ import $dom from '../dom'
import $jquery from '../dom/jquery'
import { $Location } from './location'
-export type QueryFunction = (any) => any
-
-export type SubjectChain = [any, ...QueryFunction[]];
-
const tagOpen = /\[([a-z\s='"-]+)\]/g
const tagClosed = /\[\/([a-z]+)\]/g
@@ -403,28 +399,4 @@ export default {
isPromiseLike (ret) {
return ret && _.isFunction(ret.then)
},
-
- /* Given a chain of functions, return the actual subject. `subjectChain` might look like any of:
- * []
- * ['foobar', f()]
- * [undefined, f(), f()]
- */
- getSubjectFromChain (subjectChain: SubjectChain, cy) {
- // If we're getting the subject of a previous command, then any log messages have already
- // been added to the command log; We don't want to re-add them every time we query
- // the current subject.
- cy.state('onBeforeLog', () => false)
-
- let subject = subjectChain[0]
-
- try {
- for (let i = 1; i < subjectChain.length; i++) {
- subject = subjectChain[i](subject)
- }
- } finally {
- cy.state('onBeforeLog', null)
- }
-
- return subject
- },
}