UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

1,665 lines (1,318 loc) 60.4 kB
const { _, Promise, $ } = Cypress describe('src/cy/commands/connectors', () => { describe('with jquery', () => { before(() => { cy .visit('/fixtures/jquery.html') .then(function (win) { this.body = win.document.body.outerHTML }) }) beforeEach(function () { const doc = cy.state('document') $(doc.body).empty().html(this.body) }) context('#spread', () => { it('spreads an array into individual arguments', () => { cy.noop([1, 2, 3]).spread((one, two, three) => { expect(one).to.eq(1) expect(two).to.eq(2) expect(three).to.eq(3) }) }) it('spreads a jQuery wrapper into individual arguments', () => { cy.noop($('div')).spread((first, second) => { expect(first.tagName).to.eq('DIV') expect(first.innerText).to.eq('div') expect(second.tagName).to.eq('DIV') expect(second.innerText).to.contain('Nested Find') }) }) it('passes timeout option to spread', () => { cy.timeout(50) cy.noop([1, 2, 3]).spread({ timeout: 150 }, () => { Promise.delay(100) }) }) describe('errors', { defaultCommandTimeout: 50, }, () => { it('throws when subject isn\'t array-like', (done) => { cy.on('fail', (err) => { expect(err.message).to.eq('`cy.spread()` requires the existing subject be array-like.') expect(err.docsUrl).to.eq('https://on.cypress.io/spread') done() }) cy.noop({}).spread(() => {}) }) it('throws when promise timeout', (done) => { const logs = [] cy.on('log:added', (attrs, log) => { logs?.push(log) }) cy.on('fail', (err) => { expect(logs.length).to.eq(1) expect(logs[0].get('error')).to.eq(err) expect(err.message).to.include('`cy.spread()` timed out after waiting `20ms`.') done() }) cy.noop([1, 2, 3]).spread({ timeout: 20 }, () => { return new Promise(() => {}) }) }) }) }) context('#then', () => { it('converts raw DOM elements', () => { const div = cy.$$('div:first').get(0) cy.wrap(div).then(($div) => { expect($div.get(0)).to.eq(div) }) }) it('does not insert a mocha callback', () => { cy.noop().then(() => { expect(cy.queue.length).to.eq(2) }) }) it('passes timeout option to then', () => { cy.timeout(50) return cy.then({ timeout: 150 }, () => { return Promise.delay(100) }) }) it('can resolve nested thens', () => { cy.get('div:first').then(() => { cy.get('div:first').then(() => { cy.get('div:first') }) }) }) it('can resolve cypress commands inside of a promise', () => { let _then = false cy.wrap(null).then(() => { return Promise.delay(10).then(() => { cy.then(() => { _then = true }) }) }).then(() => { expect(_then).to.be.true }) }) it('can resolve chained cypress commands inside of a promise', () => { let _then = false cy.wrap(null).then(() => { return Promise.delay(10).then(() => { cy.get('div:first').then(() => { _then = true }) }) }).then(() => { expect(_then).to.be.true }) }) it('can resolve cypress instance inside of a promise', () => { cy.then(() => { return Promise.delay(10).then(() => { return cy }) }) }) it('passes values to the next command', () => { cy .wrap({ foo: 'bar' }).then((obj) => { return obj.foo }).then((val) => { expect(val).to.eq('bar') }) }) it('does not throw when returning thenables with cy commands', () => { cy .wrap({ foo: 'bar' }) .then((obj) => { return new Promise((resolve) => { cy.wait(10) resolve(obj.foo) }) }) }) it('should pass the eventual resolved thenable value downstream', () => { cy .wrap({ foo: 'bar' }) .then((obj) => { return cy .wait(10) .then(() => { return obj.foo }).then((value) => { expect(value).to.eq('bar') return value }) }) .then((val) => { expect(val).to.eq('bar') }) }) it('should not pass the eventual resolve thenable value downstream because thens are not connected', () => { cy .wrap({ foo: 'bar' }) .then((obj) => { cy .wait(10) .then(() => { return obj.foo }) .then((value) => { expect(value).to.eq('bar') return value }) }) cy.then((val) => { expect(val).to.be.undefined }) }) it('passes the existing subject if ret is undefined', () => { cy.wrap({ foo: 'bar' }).then((obj) => { return undefined }).then((obj) => { expect(obj).to.deep.eq({ foo: 'bar' }) }) }) it('sets the subject to null when given null', () => { cy.wrap({ foo: 'bar' }).then((obj) => { return null }).then((obj) => { expect(obj).to.be.null }) }) describe('errors', { defaultCommandTimeout: 100, }, () => { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { this.lastLog = log this.logs?.push(log) }) return null }) it('throws when promise timeout', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(this.logs.length).to.eq(1) expect(lastLog.get('error')).to.eq(err) expect(err.message).to.include('`cy.then()` timed out after waiting `150ms`.') done() }) cy.then({ timeout: 150 }, () => { return new Promise(() => {}) }) }) it('throws when mixing up async + sync return values', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(this.logs.length).to.eq(1) expect(lastLog.get('error')).to.eq(err) expect(err.message).to.include('`cy.then()` failed because you are mixing up async and sync code.') done() }) cy.then(() => { cy.wait(5000) return 'foo' }) }) it('unbinds command:enqueued in the case of an error thrown', function (done) { const listeners = [] cy.on('fail', () => { listeners.push(cy.listeners('command:enqueued').length) expect(this.logs.length).to.eq(1) expect(listeners).to.deep.eq([1, 0]) done() }) cy.then(() => { listeners.push(cy.listeners('command:enqueued').length) throw new Error('foo') }) }) }) describe('yields to remote jQuery subject', () => { beforeEach(function () { this.remoteWindow = cy.state('window') }) it('calls the callback function with the remote jQuery subject', function () { this.remoteWindow.$.fn.foo = () => {} cy .get('div:first').then(function ($div) { expect($div).to.be.instanceof(this.remoteWindow.$) return $div }) .then(function ($div) { expect($div).to.be.instanceof(this.remoteWindow.$) }) }) it('does not store the remote jquery object as the subject', () => { cy .get('div:first').then(function ($div) { expect($div).to.be.instanceof(this.remoteWindow.$) return $div }) .then(function () { expect(cy.state('subject')).not.to.be.instanceof(this.remoteWindow.$) }) }) }) }) context('#invoke', () => { beforeEach(function () { this.remoteWindow = cy.state('window') }) describe('assertion verification', { defaultCommandTimeout: 200, }, () => { beforeEach(function () { delete this.remoteWindow.$.fn.foo this.logs = [] cy.on('log:added', (attrs, log) => { this.lastLog = log return this.logs?.push(log) }) return null }) it('eventually passes the assertion', function () { cy.on('command:retry', _.after(2, () => { this.remoteWindow.$.fn.foo = () => { return 'foo' } })) cy.get('div:first').invoke('foo').should('eq', 'foo').then(function () { const { lastLog } = this expect(lastLog.get('name')).to.eq('assert') expect(lastLog.get('state')).to.eq('passed') expect(lastLog.get('ended')).to.be.true }) }) it('eventually fails the assertion', function (done) { cy.on('command:retry', _.after(2, () => { this.remoteWindow.$.fn.foo = () => { return 'foo' } })) cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include(lastLog.get('error').message) expect(err.message).not.to.include('undefined') expect(lastLog.get('name')).to.eq('assert') expect(lastLog.get('state')).to.eq('failed') expect(lastLog.get('error')).to.be.an.instanceof(chai.AssertionError) done() }) cy.get('div:first').invoke('foo').should('eq', 'bar') }) it('can still fail on invoke', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include(lastLog.get('error').message) expect(err.message).not.to.include('undefined') expect(lastLog.get('name')).to.eq('invoke') expect(lastLog.get('state')).to.eq('failed') done() }) cy.get('div:first').invoke('foobarbaz') }) it('does not log an additional log on failure', function (done) { this.remoteWindow.$.fn.foo = () => { return 'foo' } cy.on('fail', () => { expect(this.logs.length).to.eq(3) done() }) cy.get('div:first').invoke('foo').should('eq', 'bar') }) }) describe('remote DOM subjects', () => { it('is invoked on the remote DOM subject', function () { this.remoteWindow.$.fn.foo = () => { return 'foo' } cy.get('div:first').invoke('foo').then((str) => { expect(str).to.eq('foo') }) }) it('re-wraps the remote element if its returned', () => { const parent = cy.$$('div:first').parent() expect(parent).to.exist cy.get('div:first').invoke('parent').then(function ($parent) { expect($parent).to.be.instanceof(this.remoteWindow.$) expect(cy.state('subject')).to.match(parent) }) }) }) describe('function property', () => { beforeEach(function () { this.obj = { foo () { return 'foo' }, bar (num1, num2) { return num1 + num2 }, err () { throw new Error('fn.err failed.') }, baz: 10, } }) it('changes subject to function invocation', function () { cy.noop(this.obj).invoke('foo').then((str) => { expect(str).to.eq('foo') }) }) it('works with numerical indexes', () => { let i = 0 const fn = () => { i++ return i === 5 } cy.noop([_.noop, fn]).invoke(1).should('be.true') }) it('works with 0 as a value if object has property 0', () => { let i = 0 const fn = () => { if (i++ === 0) { return 'cy.noop is undocumented' } return 'and I don\'t understand what it is' } cy.wrap([fn, 'bar']).invoke(0).should('eq', 'cy.noop is undocumented') cy.wrap({ '0': fn }).invoke(0).should('eq', 'and I don\'t understand what it is') }) it('forwards any additional arguments', function () { cy.noop(this.obj).invoke('bar', 1, 2).then((num) => { expect(num).to.eq(3) }) const obj = { bar () { return undefined }, } cy.noop(obj).invoke('bar').then((val) => { expect(val).to.be.undefined }) }) it('invokes reduced prop', () => { const obj = { foo: { bar: { baz () { return 'baz' }, }, }, } cy.wrap(obj).invoke('foo.bar.baz').should('eq', 'baz') }) it('handles properties on the prototype', () => { const num = new Number(10) cy.noop(num).invoke('valueOf').then((num) => { expect(num).to.eq(10) }) }) it('retries until function exists on the subject', () => { const obj = {} cy.on('command:retry', _.after(3, () => { obj.foo = () => { return 'bar' } })) cy.wrap(obj).invoke('foo').then((val) => { expect(val).to.eq('bar') }) }) it('retries until property is a function', () => { const obj = { foo: '', } cy.on('command:retry', _.after(3, () => { obj.foo = () => { return 'bar' } })) cy.wrap(obj).invoke('foo').then((val) => { expect(val).to.eq('bar') }) }) it('retries until property is a function when initially undefined', () => { const obj = { foo: undefined, } cy.on('command:retry', _.after(3, () => { obj.foo = () => { return 'bar' } })) cy.wrap(obj).invoke('foo').then((val) => { expect(val).to.eq('bar') }) }) it('retries until value matches assertions', () => { const obj = { foo () { return 'foo' }, } cy.on('command:retry', _.after(3, () => { obj.foo = () => { return 'bar' } })) cy.wrap(obj).invoke('foo').should('eq', 'bar') }); [null, undefined].forEach((val) => { it(`changes subject to '${val}' without throwing default assertion existence`, () => { const obj = { foo () { return val }, } cy.wrap(obj).invoke('foo').then((val2) => { expect(val2).to.eq(val) }) }) }) describe('errors', { defaultCommandTimeout: 100, }, () => { it('bubbles up automatically', function (done) { cy.on('fail', (err) => { expect(err.message).to.include('fn.err failed.') done() }) cy.noop(this.obj).invoke('err') }) it('throws when prop is not a function', (done) => { const obj = { foo: /re/, } cy.on('fail', (err) => { expect(err.message).to.include('Timed out retrying after 100ms: `cy.invoke()` errored because the property: `foo` returned a `regexp` value instead of a function. `cy.invoke()` can only be used on properties that return callable functions.') expect(err.message).to.include('`cy.invoke()` waited for the specified property `foo` to return a function, but it never did.') expect(err.message).to.include('If you want to assert on the property\'s value, then switch to use `cy.its()` and add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: \'bar\' }).its(\'foo\').should(\'eq\', \'bar\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/invoke') done() }) cy.wrap(obj).invoke('foo') }) it('throws when reduced prop is not a function', (done) => { const obj = { foo: { bar: 'bar', }, } cy.on('fail', (err) => { expect(err.message).to.include('Timed out retrying after 100ms: `cy.invoke()` errored because the property: `bar` returned a `string` value instead of a function. `cy.invoke()` can only be used on properties that return callable functions.') expect(err.message).to.include('`cy.invoke()` waited for the specified property `bar` to return a function, but it never did.') expect(err.message).to.include('If you want to assert on the property\'s value, then switch to use `cy.its()` and add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: \'bar\' }).its(\'foo\').should(\'eq\', \'bar\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/invoke') done() }) cy.wrap(obj).invoke('foo.bar') }) }) }) describe('accepts a options argument', () => { it('changes subject to function invocation', () => { cy.noop({ foo () { return 'foo' } }).invoke({ log: false }, 'foo').then((str) => { expect(str).to.eq('foo') }) }) it('forwards any additional arguments', () => { cy.noop({ bar (num1, num2) { return num1 + num2 } }).invoke({ log: false }, 'bar', 1, 2).then((num) => { expect(num).to.eq(3) }) cy.noop({ bar () { return undefined } }).invoke({ log: false }, 'bar').then((val) => { expect(val).to.be.undefined }) }) it('works with numerical indexes', () => { let i = 0 const fn = () => { i++ return i === 5 } cy.noop([_.noop, fn]).invoke({}, 1).should('be.true') }) describe('errors', { defaultCommandTimeout: 100, }, () => { beforeEach(function () { cy.on('log:added', (attrs, log) => { this.lastLog = log }) return null }) it('throws when function name is missing', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('`cy.invoke()` expects the functionName argument to have a value') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ foo () { return 'foo' } }).invoke({}) }) it('throws when function name is not of type string but of type boolean', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('`cy.invoke()` only accepts a string or a number as the functionName argument.') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ foo () { return 'foo' } }).invoke({}, true) }) it('throws when function name is not of type string but of type function', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('`cy.invoke()` only accepts a string or a number as the functionName argument.') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ foo () { return 'foo' } }).invoke(() => { return {} }) }) it('throws when first parameter is neither of type object nor of type string nor of type number', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('`cy.invoke()` only accepts a string or a number as the functionName argument.') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ foo () { return 'foo' } }).invoke(true, 'show') }) }) describe('.log', () => { beforeEach(function () { this.obj = { foo: 'foo bar baz', num: 123, bar () { return 'bar' }, attr (key, value) { const obj = {} obj[key] = value return obj }, sum (a, b) { return a + b }, } cy.on('log:added', (attrs, log) => { this.lastLog = log }) return null }) it('logs obj as a function', function () { cy.noop(this.obj).invoke({ log: true }, 'bar').then(function () { const obj = { name: 'invoke', message: '.bar()', } const { lastLog } = this _.each(obj, (value, key) => { expect(lastLog.get(key)).to.deep.eq(value) }) }) }) it('logs obj with arguments', function () { cy.noop(this.obj).invoke({ log: true }, 'attr', 'numbers', [1, 2, 3]).then(function () { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'invoke', Function: '.attr(numbers, [1, 2, 3])', 'With Arguments': ['numbers', [1, 2, 3]], Subject: this.obj, Yielded: { numbers: [1, 2, 3] }, }) }) }) it('can be disabled', function () { cy.noop(this.obj).invoke({ log: true }, 'sum', 1, 2).then(function () { expect(this.lastLog.invoke('consoleProps')).to.have.property('Function', '.sum(1, 2)') this.lastLog = undefined }) cy.noop(this.obj).invoke({ log: false }, 'sum', 1, 2).then(function () { expect(this.lastLog).to.be.undefined }) }) }) }) describe('.log', () => { beforeEach(function () { this.obj = { foo: 'foo bar baz', num: 123, bar () { return 'bar' }, attr (key, value) { const obj = {} obj[key] = value return obj }, sum (...args) { return _.reduce(args, (memo, num) => { return memo + num }, 0) }, math: { sum: (...args) => { return this.obj.sum(...args) }, }, } this.logs = [] cy.on('log:added', (attrs, log) => { this.lastLog = log this.logs?.push(log) }) return null }) it('logs $el if subject is element', () => { cy.get('div:first').invoke('hide').then(function ($el) { const { lastLog } = this expect(lastLog.get('$el').get(0)).to.eq($el.get(0)) }) }) it('does not log $el if subject isnt element', function () { cy.noop(this.obj).invoke('bar').then(function () { const { lastLog } = this expect(lastLog.get('$el')).not.to.exist }) }) it('logs obj as a function', function () { cy.noop(this.obj).invoke('bar').then(function () { const obj = { name: 'invoke', message: '.bar()', } const { lastLog } = this _.each(obj, (value, key) => { expect(lastLog.get(key)).to.deep.eq(value) }) }) }) it('logs obj with arguments', function () { cy.noop(this.obj).invoke('attr', 'numbers', [1, 2, 3]).then(function () { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'invoke', Function: '.attr(numbers, [1, 2, 3])', 'With Arguments': ['numbers', [1, 2, 3]], Subject: this.obj, Yielded: { numbers: [1, 2, 3] }, }) }) }) it('#consoleProps as a function property without args', function () { cy.noop(this.obj).invoke('bar').then(function () { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'invoke', Function: '.bar()', Subject: this.obj, Yielded: 'bar', }) }) }) it('#consoleProps as a function property with args', function () { cy.noop(this.obj).invoke('sum', 1, 2, 3).then(function () { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'invoke', Function: '.sum(1, 2, 3)', 'With Arguments': [1, 2, 3], Subject: this.obj, Yielded: 6, }) }) }) it('#consoleProps as a function reduced property with args', function () { cy.noop(this.obj).invoke('math.sum', 1, 2, 3).then(function () { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'invoke', Function: '.math.sum(1, 2, 3)', 'With Arguments': [1, 2, 3], Subject: this.obj, Yielded: 6, }) }) }) it('#consoleProps as a function on DOM element', () => { cy.get('div:first').invoke('hide').then(function ($btn) { const consoleProps = this.lastLog.invoke('consoleProps') expect(consoleProps).to.deep.eq({ Command: 'invoke', Function: '.hide()', Subject: $btn.get(0), Yielded: $btn.get(0), }) }) }) }) describe('errors', { defaultCommandTimeout: 100, }, () => { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { this.lastLog = log this.logs?.push(log) }) return null }) it('throws when property does not exist on the subject', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.invoke()` errored because the property: `foo` does not exist on your subject.') expect(err.message).to.include('`cy.invoke()` waited for the specified property `foo` to exist, but it never did.') expect(err.message).to.include('If you do not expect the property `foo` to exist, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: \'bar\' }).its(\'quux\').should(\'not.exist\')`') expect(lastLog.get('error').message).to.include(err.message) expect(err.docsUrl).to.eq('https://on.cypress.io/invoke') done() }) cy.wrap({}).invoke('foo') }) it('throws without a subject', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('cy.invoke("queue")') expect(err.message).to.include('child command before running a parent command') done() }) cy.invoke('queue') }) it('logs once when not dom subject', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(this.logs.length).to.eq(1) expect(lastLog.get('error')).to.eq(err) done() }) cy.invoke({}) }) it('throws when failing assertions', function (done) { const obj = { foo () { return 'foo' }, } cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.eq('Timed out retrying after 100ms: expected \'foo\' to equal \'bar\'') expect(lastLog.get('error').message).to.eq('Timed out retrying after 100ms: expected \'foo\' to equal \'bar\'') done() }) cy.wrap(obj).invoke('foo').should('eq', 'bar') }) it('throws when initial subject is undefined', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.invoke()` errored because your subject is: `undefined`. You cannot invoke any functions such as `foo` on a `undefined` value.') expect(err.message).to.include('If you expect your subject to be `undefined`, then add an assertion such as:') expect(err.message).to.include('`cy.wrap(undefined).should(\'be.undefined\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/invoke') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap(undefined).invoke('foo') }) it('throws when property value is undefined', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.invoke()` errored because the property: `foo` is not a function, and instead returned a `undefined` value.') expect(err.message).to.include('`cy.invoke()` waited for the specified property `foo` to become a callable function, but it never did.') expect(err.message).to.include('If you expect the property `foo` to be `undefined`, then switch to use `cy.its()` and add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: undefined }).its(\'foo\').should(\'be.undefined\')`') expect(lastLog.get('error').message).to.include(err.message) expect(err.docsUrl).to.eq('https://on.cypress.io/invoke') done() }) cy.wrap({ foo: undefined }).invoke('foo') }) it('throws when nested property value is undefined', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.invoke()` errored because the property: `baz` does not exist on your subject.') expect(lastLog.get('error').message).to.include(err.message) expect(err.docsUrl).to.eq('https://on.cypress.io/invoke') done() }) const obj = { foo: { bar: {}, }, } cy.wrap(obj).invoke('foo.bar.baz.fizz') }) }) }) context('#its', () => { beforeEach(function () { this.remoteWindow = cy.state('window') }) it('proxies to #invokeFn', () => { const fn = () => { return 'bar' } cy.wrap({ foo: fn }).its('foo').should('eq', fn) }) it('works with numerical indexes', () => { cy.wrap(['foo', 'bar']).its(1).should('eq', 'bar') }) it('works with 0 as a value if object has property 0', () => { cy.wrap(['foo', 'bar']).its(0).should('eq', 'foo') cy.wrap({ '0': 'whoa' }).its(0).should('eq', 'whoa') // eslint-disable-next-line no-sparse-arrays cy.wrap([/*empty*/, 'spooky']).its(0).should('not.exist') }) it('reduces into dot separated values', () => { const obj = { foo: { bar: { baz: 'baz', }, }, } cy.wrap(obj).its('foo.bar.baz').should('eq', 'baz') }) it('does not invoke a function and uses as a property', () => { const fn = () => { return 'fn' } fn.bar = 'bar' cy.wrap(fn).its('bar').should('eq', 'bar') }) it('does not invoke a function with multiple its', () => { const fn = () => { return 'fn' } fn.bar = () => { return 'bar' } fn.bar.baz = 'baz' cy.wrap(fn).its('bar').its('baz').should('eq', 'baz') }) it('does not invoke a function and uses as a reduced property', () => { const fn = () => { return 'fn' } fn.bar = () => { return 'bar' } fn.bar.baz = 'baz' const obj = { foo: fn, } cy.wrap(obj).its('foo.bar.baz').should('eq', 'baz') }) it('does not invoke a function and can assert it throws', () => { const err = new Error('nope cant access me') const obj = { foo () { throw err }, } cy.wrap(obj).its('foo').should('throw', 'nope cant access me') }) it('returns property', () => { cy.noop({ baz: 'baz' }).its('baz').then((num) => { expect(num).to.eq('baz') }) }) it('returns property on remote subject', function () { this.remoteWindow.$.fn.baz = 123 cy.get('div:first').its('baz').then((num) => { expect(num).to.eq(123) }) }) it('handles string subjects', () => { const str = 'foobarbaz' cy.noop(str).its('length').then((num) => { expect(num).to.eq(str.length) }) }) it('handles number subjects', () => { const num = 12345 const { toFixed } = top.Number.prototype cy.wrap(num).its('toFixed').should('eq', toFixed) }) it('retries by default until property exists without an assertion', () => { const obj = {} cy.on('command:retry', _.after(3, () => { return obj.foo = 'bar' })) cy.wrap(obj).its('foo').then((val) => { expect(val).to.eq('bar') }) }) it('retries until property is not undefined without an assertion', () => { const obj = { foo: undefined, } cy.on('command:retry', _.after(3, () => { return obj.foo = 'bar' })) cy.wrap(obj).its('foo').then((val) => { expect(val).to.eq('bar') }) }) it('retries until property is not null without an assertion', () => { const obj = { foo: null, } cy.on('command:retry', _.after(3, () => { return obj.foo = 'bar' })) cy.wrap(obj).its('foo').then((val) => { expect(val).to.eq('bar') }) }) it('retries when yielded undefined value and using assertion', () => { const obj = { foo: '' } cy.stub(obj, 'foo').get( cy.stub() .onCall(0).returns(undefined) .onCall(1).returns(undefined) .onCall(2).returns(true), ) cy.wrap(obj).its('foo').should('eq', true) }) it('retries until property does NOT exist with an assertion', () => { const obj = { foo: '', } cy.on('command:retry', _.after(3, () => { return delete obj.foo })) cy.wrap(obj).its('foo').should('not.exist').then((val) => { expect(val).to.be.undefined }) }) it('passes when property does not exist on the subject with assertions', () => { cy.wrap({}).its('foo').should('not.exist') cy.wrap({}).its('foo').should('be.undefined') cy.wrap({}).its('foo').should('not.be.ok') // TODO: should these really pass here? // isn't this the same situation as: cy.should('not.have.class', '...') // // when we use the 'eq' and 'not.eq' chainer aren't we effectively // saying that it must *have* a value as opposed to the property not // existing at all? // // does a tree falling in the forest really make a sound? cy.wrap({}).its('foo').should('eq', undefined) cy.wrap({}).its('foo').should('not.eq', 'bar') }) it('passes when nested property does not exist on the subject with assertions', () => { const obj = { foo: {}, } cy.wrap(obj).its('foo').should('not.have.property', 'bar') cy.wrap(obj).its('foo.bar').should('not.exist') cy.wrap(obj).its('foo.bar.baz').should('not.exist') }) it('passes when property value is null with assertions', () => { const obj = { foo: null, } cy.wrap(obj).its('foo').should('be.null') cy.wrap(obj).its('foo').should('eq', null) }) it('passes when property value is undefined with assertions', () => { const obj = { foo: undefined, } cy.wrap(obj).its('foo').should('be.undefined') cy.wrap(obj).its('foo').should('eq', undefined) }) describe('accepts a options argument and works as without options argument', () => { it('proxies to #invokeFn', () => { const fn = () => { return 'bar' } cy.wrap({ foo: fn }).its('foo', { log: false }).should('eq', fn) }) it('does not invoke a function and uses as a property', () => { const fn = () => { return 'fn' } fn.bar = 'bar' cy.wrap(fn).its('bar', { log: false }).should('eq', 'bar') }) it('works with numerical indexes', () => { cy.wrap(['foo', 'bar']).its(1, {}).should('eq', 'bar') }) describe('.log', () => { beforeEach(function () { this.obj = { foo: 'foo bar baz', num: 123, } cy.on('log:added', (attrs, log) => { this.lastLog = log }) return null }) it('logs obj as a property', function () { cy.noop(this.obj).its('foo', { log: true }).then(function () { const obj = { name: 'its', message: '.foo', } const { lastLog } = this _.each(obj, (value, key) => { expect(lastLog.get(key)).to.deep.eq(value) }) }) }) it('#consoleProps as a regular property', function () { cy.noop(this.obj).its('num', { log: true }).then(function () { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'its', Property: '.num', Subject: this.obj, Yielded: 123, }) }) }) }) }) describe('.log', () => { beforeEach(function () { this.obj = { foo: 'foo bar baz', num: 123, bar () { return 'bar' }, attr (key, value) { const obj = {} obj[key] = value return obj }, sum (...args) { return _.reduce(args, (memo, num) => { return memo + num }, 0) }, baz () {}, } this.obj.baz.quux = () => { return 'quux' } this.obj.baz.lorem = 'ipsum' this.logs = [] cy.on('log:added', (attrs, log) => { this.lastLog = log this.logs?.push(log) }) return null }) it('logs immediately before resolving', (done) => { cy.on('log:added', (attrs, log) => { if (log.get('name') === 'its') { expect(log.get('state')).to.eq('pending') expect(log.get('message')).to.eq('.foo') done() } }) cy.noop({ foo: 'foo' }).its('foo') }) it('snapshots after invoking', () => { cy.noop({ foo: 'foo' }).its('foo').then(function () { const { lastLog } = this expect(lastLog.get('snapshots').length).to.eq(1) expect(lastLog.get('snapshots')[0]).to.be.an('object') }) }) it('ends', () => { cy.noop({ foo: 'foo' }).its('foo').then(function () { const { lastLog } = this expect(lastLog.get('state')).to.eq('passed') expect(lastLog.get('ended')).to.be.true }) }) it('logs obj as a property', function () { cy.noop(this.obj).its('foo').then(function () { const obj = { name: 'its', message: '.foo', } const { lastLog } = this _.each(obj, (value, key) => { expect(lastLog.get(key)).to.deep.eq(value) }) }) }) it('#consoleProps as a regular property', function () { cy.noop(this.obj).its('num').then(function () { expect(this.lastLog.invoke('consoleProps')).to.deep.eq({ Command: 'its', Property: '.num', Subject: this.obj, Yielded: 123, }) }) }) it('can be disabled', function () { cy.noop(this.obj).its('num', { log: true }).then(function () { expect(this.lastLog.invoke('consoleProps')).to.have.property('Property', '.num') this.lastLog = undefined }) cy.noop(this.obj).its('num', { log: false }).then(function () { expect(this.lastLog).to.be.undefined }) }) }) describe('errors', { defaultCommandTimeout: 100, }, () => { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { if (attrs.name === 'its') { this.lastLog = log } this.logs?.push(log) }) return null }) it('throws without a subject', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('cy.its("wat")') expect(err.message).to.include('child command before running a parent command') done() }) return cy.its('wat') }) it('throws when property does not exist', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `foo` does not exist on your subject.') expect(err.message).to.include('`cy.its()` waited for the specified property `foo` to exist, but it never did.') expect(err.message).to.include('If you do not expect the property `foo` to exist, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: \'bar\' }).its(\'quux\').should(\'not.exist\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({}).its('foo') }) it('throws when property is undefined', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `foo` returned a `undefined` value.') expect(err.message).to.include('`cy.its()` waited for the specified property `foo` to become accessible, but it never did.') expect(err.message).to.include('If you expect the property `foo` to be `undefined`, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: undefined }).its(\'foo\').should(\'be.undefined\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ foo: undefined }).its('foo') }) it('throws when property is null', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `foo` returned a `null` value.') expect(err.message).to.include('`cy.its()` waited for the specified property `foo` to become accessible, but it never did.') expect(err.message).to.include('If you expect the property `foo` to be `null`, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: null }).its(\'foo\').should(\'be.null\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ foo: null }).its('foo') }) it('throws the traversalErr as precedence when property does not exist even if the additional assertions fail', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `b` does not exist on your subject.') expect(err.message).to.include('`cy.its()` waited for the specified property `b` to exist, but it never did.') expect(err.message).to.include('If you do not expect the property `b` to exist, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: \'bar\' }).its(\'quux\').should(\'not.exist\')`') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ a: 'a' }).its('b').should('be.true') }) it('throws the traversalErr as precedence when property value is undefined even if the additional assertions fail', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `a` returned a `undefined` value.') expect(err.message).to.include('`cy.its()` waited for the specified property `a` to become accessible, but it never did.') expect(err.message).to.include('If you expect the property `a` to be `undefined`, then add an assertion such as:') expect(err.message).to.include('`cy.wrap({ foo: undefined }).its(\'foo\').should(\'be.undefined\')`') expect(err.docsUrl).to.eq('https://on.cypress.io/its') expect(lastLog.get('error').message).to.include(err.message) done() }) cy.wrap({ a: undefined }).its('a').should('be.true') }) it('does not display parenthesis on command', function (done) { const obj = { foo: { bar () {}, }, } obj.foo.bar.baz = () => { return 'baz' } cy.on('fail', (err) => { const { lastLog } = this expect(lastLog.get('error').message).to.include(err.message) expect(lastLog.invoke('consoleProps').Property).to.eq('.foo.bar.baz') done() }) cy.wrap(obj).its('foo.bar.baz').should('eq', 'baz') }) it('can handle getter that throws', (done) => { const spy = cy.spy((err) => { expect(err.message).to.eq('Timed out retrying after 100ms: some getter error') done() }).as('onFail') cy.on('fail', spy) const obj = {} Object.defineProperty(obj, 'foo', { get () { throw new Error('some getter error') }, }) cy.wrap(obj).its('foo') }) it('throws when reduced property does not exist on the subject', function (done) { cy.on('fail', (err) => { const { lastLog } = this expect(err.message).to.include('Timed out retrying after 100ms: `cy.its()` errored because the property: `baz` does not exist on your subject.') expect(err.docsUrl).to.eq('https://on.cypress.io/its') expect(lastLog.get('error').message).to.include(err.message) expect(lastLog.get('error').message).to.include(err.message) done() }) const obj = { foo: { bar: {}, },