UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

1,864 lines (1,564 loc) 75.5 kB
const { stripIndent } = require('common-tags') const { _, $, Promise } = Cypress describe('src/cy/commands/xhr', () => { before(() => { cy .visit('/fixtures/jquery.html') .then(function (win) { const h = $(win.document.head) h.find('script').remove() this.head = h.prop('outerHTML') this.body = win.document.body.outerHTML }) }) beforeEach(function () { const doc = cy.state('document') $(doc.head).empty().html(this.head) $(doc.body).empty().html(this.body) }) context('#startXhrServer', () => { it('continues to be a defined properties', () => { cy .server() .route({ url: /foo/ }).as('getFoo') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.open('GET', '/foo') expect(xhr.onload).to.be.a('function') expect(xhr.onerror).to.be.a('function') expect(xhr.onreadystatechange).to.be.a('function') }) }) it('prevents infinite recursion', () => { let onloaded = false let onreadystatechanged = false cy .server() .route({ url: /foo/ }).as('getFoo') .window().then((win) => { const handlers = ['onload', 'onerror', 'onreadystatechange'] const wrap = () => { handlers.forEach((handler) => { const bak = xhr[handler] xhr[handler] = (...args) => { if (_.isFunction(bak)) { bak.apply(xhr, args) } } }) } const xhr = new win.XMLHttpRequest xhr.addEventListener('readystatechange', wrap, false) xhr.onreadystatechange = function () { throw new Error('NOOO') } xhr.onreadystatechange xhr.onreadystatechange = () => { onreadystatechanged = true } xhr.open('GET', '/foo') xhr.onload = function () { throw new Error('NOOO') } xhr.onload xhr.onload = () => { onloaded = true } xhr.send() return null }) .wait('@getFoo').then((xhr) => { expect(onloaded).to.be.true expect(onreadystatechanged).to.be.true expect(xhr.status).to.eq(404) }) }) // NOTE: flaky about 50% of the time in Firefox... // temporarily skipping for now, but this needs // to be reenabled after launch once we have time // to look at the underlying failure cause it.skip('allows multiple readystatechange calls', () => { let responseStatuses = 0 cy .server() .route({ url: /longtext.txt/ }).as('getLongText') .task('create:long:file') .window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState === 3) { return responseStatuses++ } } xhr.open('GET', `/_test-output/longtext.txt?${Cypress._.random(0, 1e6)}`) xhr.send() return null }) .wait('@getLongText').then((xhr) => { expect(responseStatuses).to.be.gt(1) expect(xhr.status).to.eq(200) }) }) // https://github.com/cypress-io/cypress/issues/5864 it('does not exceed max call stack', () => { cy .server() .route({ url: /foo/ }).as('getFoo') .window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/foo') // This tests an old bug where calling onreadystatechange's getter would // create nested wrapper functions and exceed the max stack depth when called. // 20000 nested calls should be enough to break the stack in most implementations xhr.onreadystatechange = () => { return {} } for (let i = 0; i < 20000; i++) { xhr.onreadystatechange() } xhr.send() return null }) .wait('@getFoo').then((xhr) => { expect(xhr.status).to.eq(404) }) }) it('works with jquery too', () => { let failed = false let onloaded = false cy .server() .route({ url: /foo/ }).as('getFoo') .window().then((win) => { const handlers = ['onload', 'onerror', 'onreadystatechange'] const wrap = function () { const xhr = this handlers.forEach((handler) => { const bak = xhr[handler] xhr[handler] = (...args) => { if (_.isFunction(bak)) { bak.apply(xhr, args) } } }) } const { open } = win.XMLHttpRequest.prototype win.XMLHttpRequest.prototype.open = function (...args) { this.addEventListener('readystatechange', wrap, false) open.apply(this, args) } win.$.get('/foo') .fail(() => { failed = true }) .always(() => { onloaded = true }) return null }) .wait('@getFoo').then((xhr) => { expect(failed).to.be.true expect(onloaded).to.be.true expect(xhr.status).to.eq(404) }) }) it('calls existing onload handlers', () => { let onloaded = false cy .server() .route({ url: /foo/ }).as('getFoo') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.onload = () => { onloaded = true } xhr.open('GET', '/foo') xhr.send() return null }) .wait('@getFoo').then((xhr) => { expect(onloaded).to.be.true expect(xhr.status).to.eq(404) }) }) it('calls onload handlers attached after xhr#send', () => { let onloaded = false cy .server() .route({ url: /foo/ }).as('getFoo') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.open('GET', '/foo') xhr.send() xhr.onload = () => { return onloaded = true } return null }) .wait('@getFoo').then((xhr) => { expect(onloaded).to.be.true expect(xhr.status).to.eq(404) }) }) it('calls onload handlers attached after xhr#send asynchronously', () => { let onloaded = false cy .server() .route({ url: /timeout/ }).as('getTimeout') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.open('GET', '/timeout?ms=100') xhr.send() _.delay(() => { xhr.onload = () => { onloaded = true } }, 20) return null }) .wait('@getTimeout').then((xhr) => { expect(onloaded).to.be.true expect(xhr.status).to.eq(200) }) }) it('fallbacks even when onreadystatechange is overriden', () => { let onloaded = false let onreadystatechanged = false cy .server() .route({ url: /timeout/ }).as('get.timeout') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.open('GET', '/timeout?ms=100') xhr.send() xhr.onreadystatechange = () => { onreadystatechanged = true } xhr.onload = () => { onloaded = true } return null }) .wait('@get.timeout').then((xhr) => { expect(onloaded).to.be.true expect(onreadystatechanged).to.be.true expect(xhr.status).to.eq(200) }) }) describe('url rewriting', () => { it('has a FQDN absolute-relative url', () => { cy .server() .route({ url: /foo/, }).as('getFoo') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('/foo') return null }).wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:3500/foo') expect(this.open).to.be.calledWith('GET', '/foo') }) }) it('has a relative URL', () => { cy .server() .route(/foo/).as('getFoo') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('foo') return null }).wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:3500/fixtures/foo') expect(this.open).to.be.calledWith('GET', 'foo') }) }) it('resolves relative urls correctly when base tag is present', () => { cy .server() .route({ url: /foo/, }).as('getFoo') .window().then(function (win) { win.$('<base href=\'/\'>').appendTo(win.$('head')) this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('foo') return null }).wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:3500/foo') expect(this.open).to.be.calledWith('GET', 'foo') }) }) it('resolves relative urls correctly when base tag is present on nested routes', () => { cy .server() .route({ url: /foo/, }).as('getFoo') .window().then(function (win) { win.$('<base href=\'/nested/route/path\'>').appendTo(win.$('head')) this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('../foo') return null }).wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:3500/nested/foo') expect(this.open).to.be.calledWith('GET', '../foo') }) }) it('allows cross origin requests to go out as necessary', () => { cy .server() .route(/foo/).as('getFoo') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('http://localhost:3501/foo') return null }).wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:3501/foo') expect(this.open).to.be.calledWith('GET', 'http://localhost:3501/foo') }) }) it('rewrites FQDN url\'s for stubs', () => { cy .server() .route({ url: /foo/, response: {}, }).as('getFoo') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('http://localhost:9999/foo') return null }).wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:9999/foo') expect(this.open).to.be.calledWith('GET', '/__cypress/xhrs/http://localhost:9999/foo') }) }) it('rewrites absolute url\'s for stubs', () => { cy .server() .route(/foo/, {}).as('getFoo') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('/foo') return null }).wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:3500/foo') expect(this.open).to.be.calledWith('GET', '/__cypress/xhrs/http://localhost:3500/foo') }) }) it('rewrites 404\'s url\'s for stubs', () => { cy .server({ force404: true }) .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') return new Promise((resolve) => { win.$.ajax({ method: 'POST', url: '/foo', data: JSON.stringify({ foo: 'bar' }), }).fail(() => { resolve() }) }) }) .then(function () { const { xhr } = cy.state('responses')[0] expect(xhr.url).to.eq('http://localhost:3500/foo') expect(this.open).to.be.calledWith('POST', '/__cypress/xhrs/http://localhost:3500/foo') }) }) it('rewrites urls with nested segments', () => { cy .server() .route({ url: /phones/, response: {}, }) .as('getPhones') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('phones/phones.json') return null }) .wait('@getPhones') .then(function () { const { xhr } = cy.state('responses')[0] expect(xhr.url).to.eq('http://localhost:3500/fixtures/phones/phones.json') expect(this.open).to.be.calledWith('GET', '/__cypress/xhrs/http://localhost:3500/fixtures/phones/phones.json') }) }) it('does not rewrite CORS', () => { cy.window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') return new Promise((resolve) => { win.$.get('http://www.google.com/phones/phones.json').fail(() => { resolve() }) }) }) .then(function () { const { xhr } = cy.state('requests')[0] expect(xhr.url).to.eq('http://www.google.com/phones/phones.json') expect(this.open).to.be.calledWith('GET', 'http://www.google.com/phones/phones.json') }) }) it('can stub real CORS requests too', () => { cy .server() .route({ url: /phones/, response: {}, }) .as('getPhones') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('http://www.google.com/phones/phones.json') return null }) .wait('@getPhones') .then(function () { const { xhr } = cy.state('responses')[0] expect(xhr.url).to.eq('http://www.google.com/phones/phones.json') expect(this.open).to.be.calledWith('GET', '/__cypress/xhrs/http://www.google.com/phones/phones.json') }) }) it('can stub CORS string routes', () => { cy .server() .route('http://localhost:3501/fixtures/app.json').as('getPhones') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('http://localhost:3501/fixtures/app.json') return null }) .wait('@getPhones') .then(function () { const { xhr } = cy.state('responses')[0] expect(xhr.url).to.eq('http://localhost:3501/fixtures/app.json') expect(this.open).to.be.calledWith('GET', 'http://localhost:3501/fixtures/app.json') }) }) it('sets display correctly when there is no remoteOrigin', () => { // this is an example of having cypress act as your webserver // when the remoteHost is <root> cy .server() .route({ url: /foo/, response: {}, }) .as('getFoo') .window().then(function (win) { // trick cypress into thinking the remoteOrigin is location:9999 cy.stub(cy, 'getRemoteLocation').withArgs('origin').returns('') this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('/foo') return null }) .wait('@getFoo').then(function (xhr) { expect(xhr.url).to.eq('http://localhost:3500/foo') expect(this.open).to.be.calledWith('GET', '/__cypress/xhrs/http://localhost:3500/foo') }) }) it('decodes proxy urls', () => { cy .server() .route({ url: /users/, response: {}, }) .as('getUsers') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('/users?q=(id eq 123)') return null }) .wait('@getUsers') .then(function () { const { xhr } = cy.state('responses')[0] expect(xhr.url).to.eq('http://localhost:3500/users?q=(id eq 123)') const url = encodeURI('users?q=(id eq 123)') expect(this.open).to.be.calledWith('GET', `/__cypress/xhrs/http://localhost:3500/${url}`) }) }) it('decodes proxy urls #2', () => { cy .server() .route(/accounts/, {}).as('getAccounts') .window().then(function (win) { this.open = cy.spy(cy.state('server').options, 'onOpen') win.$.get('/accounts?page=1&%24filter=(rowStatus+eq+1)&%24orderby=name+asc&includeOpenFoldersCount=true&includeStatusCount=true') return null }) .wait('@getAccounts') .then(function () { const { xhr } = cy.state('responses')[0] expect(xhr.url).to.eq('http://localhost:3500/accounts?page=1&$filter=(rowStatus+eq+1)&$orderby=name+asc&includeOpenFoldersCount=true&includeStatusCount=true') const url = 'accounts?page=1&%24filter=(rowStatus+eq+1)&%24orderby=name+asc&includeOpenFoldersCount=true&includeStatusCount=true' expect(this.open).to.be.calledWith('GET', `/__cypress/xhrs/http://localhost:3500/${url}`) }) }) }) describe('#onResponse', () => { it('calls onResponse callback with cy context + proxy xhr', (done) => { cy .server() .route({ url: /foo/, response: { foo: 'bar' }, onResponse (xhr) { expect(this).to.eq(cy) expect(xhr.responseBody).to.deep.eq({ foo: 'bar' }) done() }, }) .window().then((win) => { win.$.get('/foo') return null }) }) }) describe('#onAbort', () => { it('calls onAbort callback with cy context + proxy xhr', (done) => { cy .server() .route({ url: /foo/, response: {}, onAbort (xhr) { expect(this).to.eq(cy) expect(xhr.aborted).to.be.true done() }, }) .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.open('GET', '/foo') xhr.send() xhr.abort() return null }) }) }) describe('request parsing', () => { it('adds parses requestBody into JSON', (done) => { cy .server() .route({ method: 'POST', url: /foo/, response: {}, onRequest (xhr) { expect(this).to.eq(cy) expect(xhr.requestBody).to.deep.eq({ foo: 'bar' }) done() }, }) .window().then((win) => { win.$.ajax({ type: 'POST', url: '/foo', data: JSON.stringify({ foo: 'bar' }), dataType: 'json', }) return null }) }) // https://github.com/cypress-io/cypress/issues/65 it('provides the correct requestBody on multiple requests', () => { const post = function (win, obj) { win.$.ajax({ type: 'POST', url: '/foo', data: JSON.stringify(obj), dataType: 'json', }) return null } cy .server() .route('POST', /foo/, {}).as('getFoo') .window().then((win) => { return post(win, { foo: 'bar1' }) }) .wait('@getFoo').its('requestBody').should('deep.eq', { foo: 'bar1' }) .window().then((win) => { return post(win, { foo: 'bar2' }) }) .wait('@getFoo').its('requestBody').should('deep.eq', { foo: 'bar2' }) }) it('handles arraybuffer', () => { cy .server() .route('GET', /arraybuffer/).as('getBuffer') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.responseType = 'arraybuffer' xhr.open('GET', '/arraybuffer') xhr.send() return null }) .wait('@getBuffer').then((xhr) => { expect(xhr.status).eq(200) expect(xhr.responseBody.byteLength).gt(0) expect(xhr.responseBody.toString()).to.eq('[object ArrayBuffer]') }) }) it('handles xml', () => { cy .server() .route('GET', /xml/).as('getXML') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.open('GET', '/xml') xhr.send() return null }) .wait('@getXML').its('responseBody').should('eq', '<foo>bar</foo>') }) }) describe('issue #84', () => { it('does not incorrectly match options', () => { cy .server() .route({ method: 'GET', url: /answers/, status: 503, response: {}, }) .route(/forms/, []).as('getForm') .window().then((win) => { win.$.getJSON('/forms') return null }) .wait('@getForm').its('status').should('eq', 200) }) }) describe('#issue #85', () => { it('correctly returns the right XHR alias', () => { cy .server() .route({ method: 'POST', url: /foo/, response: {}, }) .as('getFoo') .route(/folders/, { foo: 'bar' }).as('getFolders') .window().then((win) => { win.$.getJSON('/folders') win.$.post('/foo', {}) return null }) .wait('@getFolders') .wait('@getFoo') .route(/folders/, { foo: 'baz' }).as('getFoldersWithSearch') .window().then((win) => { win.$.getJSON('/folders/123/activities?foo=bar') return null }) .wait('@getFoldersWithSearch').its('url') .should('contain', '?foo=bar') }) }) describe('.log', () => { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { if (attrs.name === 'xhr') { this.lastLog = log this.logs.push(log) } }) return null }) context('requests', () => { it('immediately logs xhr obj', () => { cy .server() .route(/foo/, {}).as('getFoo') .window().then((win) => { win.$.get('foo') return null }).then(function () { const { lastLog } = this expect(lastLog.pick('name', 'displayName', 'event', 'alias', 'aliasType', 'state')).to.deep.eq({ name: 'xhr', displayName: 'xhr stub', event: true, alias: 'getFoo', aliasType: 'route', state: 'pending', }) const snapshots = lastLog.get('snapshots') expect(snapshots.length).to.eq(1) expect(snapshots[0].name).to.eq('request') expect(snapshots[0].body).to.be.an('object') }) }) // https://github.com/cypress-io/cypress/issues/8018 it('logs empty string response as stubbed', () => { cy .server() .route(/foo/, '').as('getFoo') .window().then((win) => { win.$.get('foo') return null }).then(function () { const { lastLog } = this expect(lastLog.pick('name', 'displayName', 'event', 'alias', 'aliasType', 'state')).to.deep.eq({ name: 'xhr', displayName: 'xhr stub', event: true, alias: 'getFoo', aliasType: 'route', state: 'pending', }) const snapshots = lastLog.get('snapshots') expect(snapshots.length).to.eq(1) expect(snapshots[0].name).to.eq('request') }) }) it('does not end xhr requests when the associated command ends', () => { let logs = null cy .server() .route({ url: /foo/, response: {}, delay: 50, }).as('getFoo') .window().then((w) => { w.$.getJSON('foo') w.$.getJSON('foo') w.$.getJSON('foo') return null }) .then(() => { const cmd = cy.queue.find({ name: 'window' }) logs = cmd.get('next').get('logs') expect(logs.length).to.eq(3) _.each(logs, (log) => { expect(log.get('name')).to.eq('xhr') expect(log.get('end')).not.to.be.true }) }).wait(['@getFoo', '@getFoo', '@getFoo']).then(() => { _.each(logs, (log) => { expect(log.get('name')).to.eq('xhr') expect(log.get('ended')).to.be.true }) }) }) it('updates log immediately whenever an xhr is aborted', () => { let xhrs = null cy .server() .route({ url: /foo/, response: {}, delay: 50, }).as('getFoo') .window().then((win) => { const xhr1 = win.$.getJSON('foo1') win.$.getJSON('foo2') xhr1.abort() return null }).then(() => { xhrs = cy.queue.logs({ name: 'xhr' }) expect(xhrs[0].get('state')).to.eq('failed') expect(xhrs[0].get('error').name).to.eq('AbortError') expect(xhrs[0].get('snapshots').length).to.eq(2) expect(xhrs[0].get('snapshots')[0].name).to.eq('request') expect(xhrs[0].get('snapshots')[0].body).to.be.a('object') expect(xhrs[0].get('snapshots')[1].name).to.eq('aborted') expect(xhrs[0].get('snapshots')[1].body).to.be.a('object') expect(cy.state('requests').length).to.eq(2) // the abort should have set its response expect(cy.state('responses').length).to.eq(1) }) .wait(['@getFoo', '@getFoo']).then(() => { // should not re-snapshot after the response expect(xhrs[0].get('snapshots').length).to.eq(2) }) }) it('can access requestHeaders', () => { cy .server() .route(/foo/, {}).as('getFoo') .window().then((win) => { win.$.ajax({ method: 'GET', url: '/foo', headers: { 'x-token': '123', }, }) return null }) .wait('@getFoo').its('requestHeaders').should('have.property', 'x-token', '123') }) }) context('responses', () => { beforeEach(() => { cy .server() .route(/foo/, {}).as('getFoo') .window().then((win) => { win.$.get('foo_bar') return null }) .wait('@getFoo') }) it('logs obj', function () { const obj = { name: 'xhr', displayName: 'xhr stub', event: true, message: '', type: 'parent', aliasType: 'route', referencesAlias: undefined, alias: 'getFoo', } const { lastLog } = this _.each(obj, (value, key) => { expect(lastLog.get(key)).to.deep.eq(value, `expected key: ${key} to eq value: ${value}`) }) }) it('ends', function () { const { lastLog } = this expect(lastLog.get('state')).to.eq('passed') }) it('snapshots again', function () { const { lastLog } = this expect(lastLog.get('snapshots').length).to.eq(2) expect(lastLog.get('snapshots')[0].name).to.eq('request') expect(lastLog.get('snapshots')[0].body).to.be.an('object') expect(lastLog.get('snapshots')[1].name).to.eq('response') expect(lastLog.get('snapshots')[1].body).to.be.an('object') }) }) }) describe('errors', { defaultCommandTimeout: 200, }, () => { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { if (attrs.name === 'xhr') { this.lastLog = log this.logs.push(log) } }) return null }) it('sets err on log when caused by code errors', function (done) { const uncaughtException = cy.stub().returns(true) cy.on('uncaught:exception', uncaughtException) cy.on('fail', (err) => { const { lastLog } = this expect(this.logs.length).to.eq(1) expect(lastLog.get('name')).to.eq('xhr') expect(lastLog.get('error').message).contain('foo is not defined') // since this is AUT code, we should allow error to be caught in 'uncaught:exception' hook // https://github.com/cypress-io/cypress/issues/987 expect(uncaughtException).calledOnce done() }) cy.window().then((win) => { return new Promise(() => { win.$.get('http://www.google.com/foo.json') .fail(() => { foo.bar() // eslint-disable-line no-undef }) }) }) }) it('causes errors caused by onreadystatechange callback function', function (done) { const e = new Error('onreadystatechange caused this error') cy.on('fail', (err) => { const { lastLog } = this expect(this.logs.length).to.eq(1) expect(lastLog.get('name')).to.eq('xhr') expect(err).to.eq(lastLog.get('error')) expect(err).to.eq(e) done() }) cy .window().then((win) => { return new Promise((resolve) => { const xhr = new win.XMLHttpRequest xhr.open('GET', '/foo') xhr.onreadystatechange = function () { throw e } xhr.send() }) }) }) }) }) context('#server', () => { it('logs deprecation warning', () => { cy.stub(Cypress.utils, 'warning') cy.server() .then(function () { expect(Cypress.utils.warning).to.be.calledWithMatch(/^`cy\.server\(\)` has been deprecated and will be moved to a plugin in a future release\. Consider migrating to using `cy\.intercept\(\)` instead\./) }) }) it('sets serverIsStubbed', () => { cy.server().then(() => { expect(cy.state('serverIsStubbed')).to.be.true }) }) it('can disable serverIsStubbed', () => { cy.server({ enable: false }).then(() => { expect(cy.state('serverIsStubbed')).to.be.false }) }) it('sends enable to server', () => { const set = cy.spy(cy.state('server'), 'set') cy.server().then(() => { expect(set).to.be.calledWithExactly({ enable: true }) }) }) it('can disable the server after enabling it', () => { const set = cy.spy(cy.state('server'), 'set') cy .server() .route(/app/, {}).as('getJSON') .window().then((win) => { win.$.get('/fixtures/app.json') return null }) .wait('@getJSON').its('responseBody').should('deep.eq', {}) .server({ enable: false }) .then(() => { expect(set).to.be.calledWithExactly({ enable: false }) }) .window().then((win) => { win.$.get('/fixtures/app.json') return null }) .wait('@getJSON').its('responseBody').should('not.deep.eq', {}) }) it('sets delay at 0 by default', () => { cy .server() .route('*', {}) .then(() => { expect(cy.state('server').getRoutes()[0].delay).to.eq(0) }) }) it('sets ignore as function by default', () => { cy.server() cy.route('*', {}) .then(() => { expect(cy.state('server').getRoutes()[0].ignore).to.be.a('function') }) }) it('passes down options.delay to routes', () => { cy .server({ delay: 100 }) .route('*', {}) .then(() => { expect(cy.state('server').getRoutes()[0].delay).to.eq(100) }) }) it('passes event argument to xhr.onreadystatechange', (done) => { cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.onreadystatechange = function (e) { expect(e).to.be.an.instanceof(win.Event) done() } xhr.open('GET', 'http://localhost:3500/') }) }) describe('errors', () => { context('argument signature', () => { _.each(['asdf', 123, null, undefined], (arg) => { it(`throws on bad argument: ${arg}`, (done) => { cy.on('fail', (err) => { expect(err.message).to.include('`cy.server()` accepts only an object literal as its argument') expect(err.docsUrl).to.eq('https://on.cypress.io/server') done() }) cy.server(arg) }) }) }) it('after turning off server it throws attempting to route', (done) => { cy.on('fail', (err) => { expect(err.message).to.eq('`cy.route()` cannot be invoked before starting the `cy.server()`') expect(err.docsUrl).to.eq('https://on.cypress.io/server') done() }) cy.server() cy.route(/app/, {}) cy.server({ enable: false }) cy.route(/app/, {}) }) describe('.log', () => { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { if (attrs.name === 'xhr') { this.lastLog = log this.logs.push(log) } }) return null }) it('provides specific #onFail', function (done) { cy.on('fail', (err) => { const obj = { name: 'xhr', referencesAlias: undefined, alias: 'getFoo', aliasType: 'route', type: 'parent', error: err, instrument: 'command', message: '', event: true, } const { lastLog } = this _.each(obj, (value, key) => { expect(value).deep.eq(lastLog.get(key), `expected key: ${key} to eq value: ${value}`) }) done() }) cy .server() .route(/foo/, {}).as('getFoo') .window().then((win) => { return win.$.get('/foo').done(() => { throw new Error('specific ajax error') }) }) }) }) }) }) context('#route', () => { beforeEach(function () { this.expectOptionsToBe = (opts) => { const options = this.route.getCall(0).args[0] _.each(opts, (value, key) => { expect(options[key]).to.deep.eq(opts[key], `failed on property: (${key})`) }) } cy.server().then(function () { this.route = cy.spy(cy.state('server'), 'route') }) }) it('logs deprecation warning', () => { cy.stub(Cypress.utils, 'warning') cy.route('*') .then(function () { expect(Cypress.utils.warning).to.be.calledWithMatch(/^`cy\.route\(\)` has been deprecated and will be moved to a plugin in a future release\. Consider migrating to using `cy\.intercept\(\)` instead\./) }) }) it('accepts url, response', () => { cy.route('/foo', {}).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: '/foo', response: {}, }) }) }) it('accepts regex url, response', () => { cy.route(/foo/, {}).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: /foo/, response: {}, }) }) }) it('does not mutate other routes when using shorthand', () => { cy .route('POST', /foo/, {}).as('getFoo') .route(/bar/, {}).as('getBar') .then(function () { expect(this.route.firstCall.args[0].method).to.eq('POST') expect(this.route.secondCall.args[0].method).to.eq('GET') }) }) it('accepts url, response, onRequest', () => { const onRequest = () => {} cy.route({ url: '/foo', response: {}, onRequest, }).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: '/foo', response: {}, onRequest, onResponse: undefined, }) }) }) it('accepts url, response, onRequest, onResponse', () => { const onRequest = () => {} const onResponse = () => {} cy.route({ url: '/foo', response: {}, onRequest, onResponse, }).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: '/foo', response: {}, onRequest, onResponse, }) }) }) it('accepts method, url, response', () => { cy.route('GET', '/foo', {}).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: '/foo', response: {}, }) }) }) it('accepts method, url, response, onRequest', () => { const onRequest = () => {} cy.route({ method: 'GET', url: '/foo', response: {}, onRequest, }).then(function () { this.expectOptionsToBe({ method: 'GET', url: '/foo', status: 200, response: {}, onRequest, onResponse: undefined, }) }) }) it('accepts method, url, response, onRequest, onResponse', () => { const onRequest = () => {} const onResponse = () => {} cy.route({ method: 'GET', url: '/foo', response: {}, onRequest, onResponse, }).then(function () { this.expectOptionsToBe({ method: 'GET', url: '/foo', status: 200, response: {}, onRequest, onResponse, }) }) }) it('uppercases method', () => { cy.route('get', '/foo', {}).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: '/foo', response: {}, }) }) }) it('accepts string or regex as the url', () => { cy.route('get', /.*/, {}).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: /.*/, response: {}, }) }) }) it('does not require response or method when not stubbing', () => { cy .server() .route(/users/).as('getUsers') .then(function () { this.expectOptionsToBe({ status: 200, method: 'GET', url: /users/, }) }) }) it('does not require response when not stubbing', () => { cy .server() .route('POST', /users/).as('createUsers') .then(function () { this.expectOptionsToBe({ status: 200, method: 'POST', url: /users/, }) }) }) it('accepts an object literal as options', () => { const onRequest = () => {} const onResponse = () => {} const opts = { method: 'PUT', url: '/foo', status: 200, response: {}, onRequest, onResponse, } cy.route(opts).then(function () { this.expectOptionsToBe(opts) }) }) it('can accept wildcard * as URL and converts to /.*/ regex', () => { const opts = { url: '*', response: {}, } cy.route(opts).then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: /.*/, originalUrl: '*', response: {}, }) }) }) // FIXME: I have no idea why this is skipped, this test is rly old it.skip('can explicitly done() in onRequest function from options', (done) => { cy .server() .route({ method: 'POST', url: '/users', response: {}, onRequest () { done() }, }) .then(() => { cy.state('window').$.post('/users', 'name=brian') }) }) it('can accept response as a function', () => { const users = [{}, {}] const getUsers = () => { return users } cy.route(/users/, getUsers) .then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: /users/, response: users, }) }) }) it('invokes response function with runnable.ctx', function () { const ctx = this const getUsers = function () { expect(this === ctx).to.be.true } cy.route(/users/, getUsers) }) it('passes options as argument', function () { const getUsers = function (opts) { expect(opts).to.be.an('object') expect(opts.method).to.eq('GET') } cy.route(/users/, getUsers) }) it('can accept response as a function which returns a promise', () => { const users = [{}, {}] const getUsers = () => { return new Promise((resolve) => { setTimeout(() => { resolve(users) }, 10) }) } cy.route(/users/, getUsers) .then(function () { this.expectOptionsToBe({ method: 'GET', status: 200, url: /users/, response: users, }) }) }) it('can accept a function which returns options', () => { const users = [{}, {}] const getRoute = () => { return { method: 'GET', url: /users/, status: 201, response () { return Promise.resolve(users) }, } } cy.route(getRoute) .then(function () { this.expectOptionsToBe({ method: 'GET', status: 201, url: /users/, response: users, }) }) }) it('invokes route function with runnable.ctx', function () { const ctx = this const getUsers = function () { expect(this === ctx).to.be.true return { url: /foo/, } } cy.route(getUsers) }) // FIXME: I have no idea why this is skipped, this test is rly old it.skip('adds multiple routes to the responses array', () => { cy .route('foo', {}) .route('bar', {}) .then(() => { expect(cy.state('sandbox').server.responses).to.have.length(2) }) }) it('can use regular strings as response', () => { cy .route('/foo', 'foo bar baz').as('getFoo') .window().then((win) => { win.$.get('/foo') return null }) .wait('@getFoo').then((xhr) => { expect(xhr.responseBody).to.eq('foo bar baz') }) }) it('can stub requests with uncommon HTTP methods', () => { cy .route('PROPFIND', '/foo', 'foo bar baz').as('getFoo') .window().then((win) => { win.$.ajax({ url: '/foo', method: 'PROPFIND', }) return null }) .wait('@getFoo').then((xhr) => { expect(xhr.responseBody).to.eq('foo bar baz') }) }) // https://github.com/cypress-io/cypress/issues/2372 it('warns if a percent-encoded URL is used', () => { cy.spy(Cypress.utils, 'warning') cy.route('GET', '/foo%25bar') .then(() => { expect(Cypress.utils.warning).to.be.calledWith(stripIndent`\ A \`url\` with percent-encoded characters was passed to \`cy.route()\`, but \`cy.route()\` expects a decoded \`url\`. Did you mean to pass "/foo%bar"? https://on.cypress.io/route`) }) }) it('does not warn if an invalid percent-encoded URL is used', () => { cy.spy(Cypress.utils, 'warning') cy.route('GET', 'http://example.com/%E0%A4%A') .then(() => { expect(Cypress.utils.warning).to.not.be.calledWithMatch(/percent\-encoded characters/) }) }) // FIXME: I have no idea why this is skipped, this test is rly old it.skip('does not error when response is null but respond is false', () => { cy.route({ url: /foo/, respond: false, }) }) describe('request response alias', () => { it('matches xhrs with lowercase methods', () => { cy .route(/foo/, {}).as('getFoo') .window().then((win) => { const xhr = new win.XMLHttpRequest xhr.open('get', '/foo') xhr.send() }).wait('@getFoo') }) it('can pass an alias reference to route', () => { cy .noop({ foo: 'bar' }).as('foo') .route(/foo/, '@foo').as('getFoo') .window().then((win) => { win.$.getJSON('foo') return null }) .wait('@getFoo').then(function (xhr) { expect(xhr.responseBody).to.deep.eq({ foo: 'bar' }) expect(xhr.responseBody).to.deep.eq(this.foo) }) }) it('can pass an alias when using a response function', () => { const getFoo = () => { return Promise.resolve('@foo') } cy .noop({ foo: 'bar' }).as('foo') .route(/foo/, getFoo).as('getFoo') .window().then((win) => { win.$.getJSON('foo') return null }) .wait('@getFoo').then(function (xhr) { expect(xhr.responseBody).to.deep.eq({ foo: 'bar' }) expect(xhr.responseBody).to.deep.eq(this.foo) }) }) it('can alias a route without stubbing it', () => { cy .route(/fixtures\/app/).as('getFoo') .window().then((win) => { win.$.get('/fixtures/app.json') return null }) .wait('@getFoo').then((xhr) => { const log = cy.queue.logs({ name: 'xhr' })[0] expect(log.get('displayName')).to.eq('xhr') expect(log.get('alias')).to.eq('getFoo') expect(xhr.responseBody).to.deep.eq({ some: 'json', foo: { bar: 'baz', }, }) }) }) }) describe('response fixtures', () => { it('works if the JSON file has an object', () => { cy .server() .route({ method: 'POST', url: '/test-xhr', response: 'fixture:valid.json', }) .visit('/fixtures/xhr-triggered.html') .get('#trigger-xhr') .click() cy.contains('#result', '{"foo":1,"bar":{"baz":"cypress"}}').should('be.visible') }) it('works if the JSON file has null content', () => { cy .server() .route({ method: 'POST', url: '/test-xhr', response: 'fixture:null.json', }) .visit('/fixtures/xhr-triggered.html') .get('#trigger-xhr') .click() cy.contains('#result', '""').should('be.visible') }) it('works if the JSON file has number content', () => { cy .server() .route({ method: 'POST', url: '/test-xhr', response: 'fixture:number.json', }) .visit('/fixtures/xhr-triggered.html') .get('#trigger-xhr') .click() cy.contains('#result', 14).should('be.visible') }) it('works if the JSON file has boolean content', () => { cy .server() .route({ method: 'POST', url: '/test-xhr', response: 'fixture:boolean.json', }) .visit('/fixtures/xhr-triggered.html') .get('#trigger-xhr') .click() cy.contains('#result', /true/).should('be.visible') }) }) 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 if cy.server() hasnt been invoked', (done) => { cy.state('serverIsStubbed', false) cy.on('fail', (err) => { expect(err.message).to.include('`cy.route()` cannot be invoked before starting the `cy.server()`') done() }) cy.route() }) it('throws on use of whitelist option', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('The `cy.server()` `whitelist` option has been renamed to `ignore`. Please rename `whitelist` to `ignore`.') done() }) cy.server({ whitelist: () => { } }) }) it('url must be a string or regexp', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('`cy.route()` was called with an invalid `url`. `url` must be either a string or regular expression.') expect(err.docsUrl).to.eq('https://on.cypress.io/route') done() }) cy.route({ url: {}, }) }) it('url must be a string or regexp when a function', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('`cy.route()` was called with an invalid `url`. `url` must be either a string or regular expression.') expect(err.docsUrl).to.eq('https://on.cypress.io/route') done() }) const getUrl = () => { return Promise.resolve({ url: {}