import { getDisplayUrlMatcher } from '@packages/driver/src/cy/net-stubbing/route-matcher-log' import type { RouteMatcherOptions } from '@packages/net-stubbing/lib/external-types' const testFail = (cb, expectedDocsUrl = 'https://on.cypress.io/intercept') => { cy.on('fail', (err) => { // @ts-ignore const docsUrl = Array.isArray(err.docsUrl) ? err.docsUrl[0] : err.docsUrl expect(docsUrl).to.eq(expectedDocsUrl) cb(err) }) } // TODO: Network retries leak between tests, causing flake. describe('network stubbing', { retries: 2 }, function () { const { $, _, sinon, state, Promise } = Cypress beforeEach(function () { cy.spy(Cypress.utils, 'warning') }) context('cy.intercept()', function () { context('emits as expected', function () { beforeEach(function () { // we don't use cy.spy() because it causes an infinite loop with logging events this.sandbox = sinon.createSandbox() this.emit = this.sandbox.spy(Cypress, 'emit').withArgs('backend:request', 'net', 'route:added') this.testRoute = function (options, handler, expectedEvent, expectedRoute) { cy.intercept(options, handler).then(function () { const routeId = _.findKey(state('routes'), { handler }) const route = state('routes')[routeId!] expectedEvent.routeId = routeId expect(this.emit).to.be.calledWith('backend:request', 'net', 'route:added', expectedEvent) expect(route.handler).to.deep.eq(expectedRoute.handler) expect(route.options).to.deep.eq(expectedRoute.options) }) } }) afterEach(function () { this.sandbox.restore() }) it('url, body and stores Route', function () { const handler = 'bar' const url = 'http://foo.invalid' const expectedEvent = { routeMatcher: { url: { type: 'glob', value: url, }, }, staticResponse: { body: 'bar', }, hasInterceptor: false, } const expectedRoute = { options: { url }, handler, } this.testRoute(url, handler, expectedEvent, expectedRoute) }) it('url, HTTPController and stores Route', function () { const handler = () => { return {} } const url = 'http://foo.invalid' const expectedEvent = { routeMatcher: { url: { type: 'glob', value: url, }, }, hasInterceptor: true, } const expectedRoute = { options: { url }, handler, } this.testRoute(url, handler, expectedEvent, expectedRoute) }) it('regex values stringified and other values copied and stores Route', function () { const handler = () => { return {} } const options = { auth: { username: 'foo', password: /.*/, }, headers: { 'Accept-Language': /hylian/i, 'Content-Encoding': 'corrupted', }, hostname: /any.com/, https: true, method: 'POST', path: '/bing?foo', pathname: '/bazz', port: [1, 2, 3, 4, 5, 6], query: { bar: 'baz', quuz: /(.*)quux/gi, }, url: 'http://foo.invalid', } const expectedEvent = { routeMatcher: { auth: { username: { type: 'glob', value: options.auth.username, }, password: { type: 'regex', value: '/.*/', }, }, headers: { 'accept-language': { type: 'regex', value: '/hylian/i', }, 'content-encoding': { type: 'glob', value: options.headers['Content-Encoding'], }, }, hostname: { type: 'regex', value: '/any.com/', }, https: options.https, method: { type: 'glob', value: options.method, }, path: { type: 'glob', value: options.path, }, pathname: { type: 'glob', value: options.pathname, }, port: options.port, query: { bar: { type: 'glob', value: options.query.bar, }, quuz: { type: 'regex', value: '/(.*)quux/gi', }, }, url: { type: 'glob', value: options.url, }, }, hasInterceptor: true, } const expectedRoute = { options, handler, } this.testRoute(options, handler, expectedEvent, expectedRoute) }) it('mergeRouteMatcher + string url works', function () { const url = '/foo*' const handler = (req) => { // @ts-ignore const routeId = _.findKey(state('routes'), { handler }) const route = state('routes')[routeId!] // @ts-ignore expectedEvent.routeId = routeId expect(this.emit).to.be.calledWith('backend:request', 'net', 'route:added', expectedEvent) expect(route.handler).to.deep.eq(expectedRoute.handler) expect(route.options).to.deep.eq(expectedRoute.options) req.reply('a') } const expectedRoute = { options: { url, middleware: true }, handler, } const expectedEvent = { routeMatcher: { url: { type: 'glob', value: url, }, middleware: true, }, hasInterceptor: true, } cy.intercept(url, { middleware: true }, handler).as('get') .then(() => { return $.get('/foo') }) .wait('@get') }) // @see https://github.com/cypress-io/cypress/pull/16390 it('mergeRouteMatcher + regex url works', function () { const url = /^\/foo.*/ const handler = (req) => { // @ts-ignore const routeId = _.findKey(state('routes'), { handler }) const route = state('routes')[routeId!] // @ts-ignore expectedEvent.routeId = routeId expect(this.emit).to.be.calledWith('backend:request', 'net', 'route:added', expectedEvent) expect(route.handler).to.deep.eq(expectedRoute.handler) expect(route.options).to.deep.eq(expectedRoute.options) req.reply('a') } const expectedRoute = { options: { url, middleware: true }, handler, } const expectedEvent = { routeMatcher: { url: { type: 'regex', value: String(url), }, middleware: true, }, hasInterceptor: true, } cy.intercept(url, { middleware: true }, handler).as('get') .then(() => { return $.get('/foo') }) .wait('@get') }) }) // https://github.com/cypress-io/cypress/issues/8729 it('resolve ambiguity between overloaded definitions', () => { cy.intercept('POST', '/post-only').as('create') cy.window().then((win) => { win.eval( `fetch("/post-only", { method: 'POST', // *GET, POST, PUT, DELETE, etc. });`, ) }) cy.wait('@create') }) // https://github.com/cypress-io/cypress/issues/9313 it('lower-cased method works', () => { cy.intercept({ method: 'post', url: '/post-only', }).as('create') cy.window().then((win) => { win.eval( `fetch("/post-only", { method: 'post', // *GET, POST, PUT, DELETE, etc. });`, ) }) cy.wait('@create') }) // @see https://github.com/cypress-io/cypress/issues/16117 it('can statically stub a url response with headers', () => { cy.intercept('/url', { headers: { foo: 'bar' }, body: 'something' }) }) // TODO: implement warning in cy.intercept if appropriate // https://github.com/cypress-io/cypress/issues/2372 it.skip('warns if a percent-encoded URL is used', function () { cy.intercept('GET', '/foo%25bar').then(function () { expect(Cypress.utils.warning).to.be.calledWith('A URL with percent-encoded characters was passed to cy.intercept(), but cy.intercept() expects a decoded URL.\n\nDid you mean to pass "/foo%bar"?') }) }) // NOTE: see todo on 'warns if a percent-encoded URL is used' it.skip('does not warn if an invalid percent-encoded URL is used', function () { cy.intercept('GET', 'http://example.com/%E0%A4%A').then(function () { expect(Cypress.utils.warning).to.not.be.called }) }) context('overrides', function () { it('chains middleware with string matcher as expected', function () { const e: string[] = [] cy .intercept('/dump-headers*', { middleware: true }, (req) => { e.push('mware req handler') req.on('before:response', (res) => { e.push('mware before:response') }) req.on('response', (res) => { e.push('mware response') }) req.on('after:response', (res) => { e.push('mware after:response') }) }) .intercept('/dump-headers*', (req) => { e.push('normal req handler') req.reply(() => { e.push('normal res handler') }) }) .then(() => { return $.get('/dump-headers') }) .wrap(e).should('have.all.members', [ 'mware req handler', 'normal req handler', 'mware before:response', 'normal res handler', 'mware response', 'mware after:response', ]) }) it('chains request handlers from bottom-up', function (done) { cy.intercept('/dump-headers*', (req) => { req.reply((res) => { expect(res.body).to.include('"x-foo":"bar"') done() }) }) .intercept('/dump-headers*', (req) => req.headers['x-foo'] = 'bar') .then(() => { $.get('/dump-headers') }) }) /** * https://github.com/cypress-io/cypress/issues/9302 * https://github.com/cypress-io/cypress/discussions/9339 * https://github.com/cypress-io/cypress/issues/4460 */ it('can override a StaticResponse with another StaticResponse', function () { cy.intercept('GET', '/items*', []) .intercept('GET', '/items*', ['foo', 'bar']) .then(() => { return $.getJSON('/items') }) .should('deep.eq', ['foo', 'bar']) }) /** * https://github.com/cypress-io/cypress/discussions/9587 */ it('can override an interceptor with another interceptor', function () { cy.intercept('GET', '**/mydata?**', (req) => { throw new Error('this should not be called') }) .intercept('GET', '/mydata*', (req) => { req.reply({ body: [1, 2, 3, 4, 5] }) }).as('mydata') .then(() => { return $.getJSON('/mydata?abc') }) .should('deep.eq', [1, 2, 3, 4, 5]) .wait('@mydata') }) it('sends a StaticResponse if the newest stub is a StaticResponse', function () { cy.intercept('/foo*', () => { throw new Error('this should not be called') }).as('interceptor') .intercept('/foo*', { body: 'something' }).as('staticresponse') .intercept('/foo*').as('spy') .then(() => { return $.get('/foo') }) .should('deep.eq', 'something') .wait('@spy') .wait('@staticresponse') .get('@interceptor.all').should('have.length', 0) }) it('sends a StaticResponse if a request handler does not supply a response', function () { cy.intercept('/foo*', { body: 'something' }).as('staticresponse') .intercept('/foo*', () => { }).as('interceptor') .then(() => { return $.get('/foo') }) .should('deep.eq', 'something') .wait('@interceptor') .wait('@staticresponse') }) context('request handler chaining', function () { it('passes request through in reverse order', function () { cy.intercept('/dump-method', function (req) { expect(req.method).to.eq('PATCH') req.reply() }).intercept('/dump-method', function (req) { expect(req.method).to.eq('POST') req.method = 'PATCH' }).intercept('/dump-method', function (req) { expect(req.method).to.eq('GET') req.method = 'POST' }).visit('/dump-method').contains('PATCH') }) it('stops passing request through once req.reply called', function () { cy.intercept('/dump-method', function (req) { throw new Error('this should not have been reached') }).intercept('/dump-method', function (req) { req.reply() }).visit('/dump-method').contains('GET') }) }) context('response handler chaining', function () { it('passes response through in reverse order', function () { cy.intercept('/dump-method', function (req) { req.on('before:response', (res) => { expect(res.body).to.contain('new body') }) }).intercept('/dump-method', function (req) { req.on('before:response', (res) => { expect(res.body).to.contain('GET') res.body = 'new body' }) }).visit('/dump-method') .contains('new body') }) it('stops passing response through once res.send called', function () { cy.intercept('/dump-method', function (req) { req.on('before:response', (res) => { throw new Error('this should not have been reached') }) }).intercept('/dump-method', function (req) { req.on('before:response', (res) => { res.send() }) }).visit('/dump-method').contains('GET') }) }) // https://github.com/cypress-io/cypress/issues/16292 describe('multibyte utf8', () => { const multibyteUtf8 = [ // 1. When there's problem in the chunkEnd // * 2 bytes '12345678901234567890123Ф', // * 3 bytes '12345678901234567890123안', '1234567890123456789012안', // * 4 bytes '12345678901234567890123😀', '1234567890123456789012😀', '123456789012345678901😀', // 2. When there's a problem in the chunkBegin // * 2 bytes 'dummyФ12345678901234567890123', // * 3 bytes 'dummy안12345678901234567890123', 'dummy안1234567890123456789012', // * 4 bytes 'dummy😀12345678901234567890123', 'dummy😀1234567890123456789012', 'dummy😀123456789012345678901', ] multibyteUtf8.forEach((str) => { it(str, () => { cy.intercept('https://example.com/test', { body: { result: 'ok' }, }).as('testRequest') cy.window().then(() => { let xhr = new XMLHttpRequest() xhr.open('POST', 'https://example.com/test') xhr.setRequestHeader('Content-Type', 'application/json') xhr.send(str) }) cy.wait('@testRequest') .its('request') .then((req) => { expect(req.body).to.eq(str) }) }) }) }) }) context('logging', function () { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { if (attrs.instrument === 'route') { this.lastLog = log this.logs.push(log) } }) }) it('has name of route', function () { cy.intercept('/foo', {}).then(function () { expect(this.lastLog.get('name')).to.eq('route') }) }) it('uses the wildcard URL', function () { cy.intercept('*', {}).then(function () { expect(this.lastLog.get('url')).to.eq('*') }) }) // @see https://github.com/cypress-io/cypress/issues/9403 // see getDisplayUrlMatcher unit tests for more test cases it('stringifies complex matchers', function () { cy.intercept({ hostname: 'foo.net', port: 1234 }).then(function () { expect(this.lastLog.get('url')).to.eq('{hostname: foo.net, port: 1234}') expect(this.lastLog.get('method')).to.eq('*') }) }) describe('numResponses', function () { it('is initially 0', function () { cy.intercept(/foo/, {}).then(() => { let lastLog lastLog = this.lastLog expect(lastLog.get('numResponses')).to.eq(0) }) }) it('is incremented to 2', function () { cy.intercept(/foo/, {}).then(function () { $.get('/foo') }).wrap(this).invoke('lastLog.get', 'numResponses').should('eq', 1) }) it('is incremented for each matching request', function () { cy.intercept(/foo/, {}).then(function () { return Promise.all([$.get('/foo'), $.get('/foo'), $.get('/foo')]) }).wrap(this).invoke('lastLog.get', 'numResponses').should('eq', 3) }) }) }) context('errors', function () { beforeEach(function () { this.logs = [] cy.on('log:added', (attrs, log) => { // @ts-ignore if (log.get('name') !== 'intercept') { return } this.lastLog = log this.logs.push(log) }) }) it('url must be a string or regexp', function (done) { testFail((err) => { expect(err.message).to.include('`url` must be a string or a regular expression') done() }) // @ts-ignore: should fail cy.intercept({ // @ts-ignore url: {}, }) }) // TODO: not currently implemented it.skip('fails when method is invalid', function (done) { testFail((err) => { expect(err.message).to.include('cy.intercept() was called with an invalid method: \'POSTS\'.') done() }) cy.intercept('post', '/foo', {}) }) it('requires a url when given a response', function (done) { testFail((err) => { expect(err.message).to.include('The RouteMatcher does not contain any keys. You must pass something to match on.') done() }) cy.intercept({}) }) it('requires arguments', function (done) { testFail((err) => { expect(err.message).to.include('An invalid RouteMatcher was supplied to `cy.intercept()`. The RouteMatcher does not contain any keys. You must pass something to match on.') done() }) // @ts-ignore - should fail cy.intercept() }) it('cannot merge url with url', function (done) { testFail((err) => { expect(err.message).to.include('When invoking \`cy.intercept()\` with a \`RouteMatcher\` as the second parameter, \`url\` can only be specified as the first parameter') done() }) // @ts-ignore - should fail cy.intercept('/foo', { url: '/bar' }, () => {}) }) it('cannot pass RouteMatcherOptions in 2nd arg with no handler', function (done) { testFail((err) => { expect(err.message).to.include('When invoking \`cy.intercept()\` with a \`RouteMatcher\` as the second parameter, a handler (function or \`StaticResponse\`) must be specified as the third parameter.') done() }) // sadly this passes typecheck, but the runtime error will prevent this cy.intercept('/foo', { middleware: true }) }) context('with an unexpected number of arguments', function () { it('cy.intercept(url, handler?)', function (done) { testFail((err) => { expect(err.message).to.include('The `cy.intercept(url, handler?)` signature accepts a maximum of 2 arguments, but 3 arguments were passed.') done() }) // @ts-ignore cy.intercept('/url', { forceNetworkError: true }, () => {}) }) it('cy.intercept(url, mergeRouteMatcher, handler)', function (done) { testFail((err) => { expect(err.message).to.include('The `cy.intercept(url, mergeRouteMatcher, handler)` signature accepts a maximum of 3 arguments, but 4 arguments were passed.') done() }) // @ts-ignore cy.intercept('/url', { middleware: true }, { forceNetworkError: true }, () => {}) }) it('cy.intercept(method, url, handler?)', function (done) { testFail((err) => { expect(err.message).to.include('The `cy.intercept(method, url, handler?)` signature accepts a maximum of 3 arguments, but 4 arguments were passed.') done() }) // @ts-ignore cy.intercept('POST', '/url', { forceNetworkError: true }, () => {}) }) }) context('with invalid RouteMatcher', function () { it('requires unique header names', function (done) { testFail((err) => { expect(err.message).to.include('`FOO` was specified more than once in `headers`. Header fields can only be matched once (HTTP header field names are case-insensitive).') done() }) cy.intercept({ headers: { foo: 'bar', FOO: 'bar', }, }) }) it('requires StringMatcher header values', function (done) { testFail((err) => { expect(err.message).to.include('`headers.wrong` must be a string or a regular expression.') done() }) // @ts-ignore this is invalid on purpose cy.intercept({ headers: { good: 'string', fine: /regexp/, // @ts-ignore wrong: 3, }, }) }) it('errors on matchUrlAgainstPath usage', function (done) { testFail((err) => { expect(err.message).to.include('`matchUrlAgainstPath` was removed in Cypress 7.0.0') done() }) // @ts-ignore cy.intercept({ matchUrlAgainstPath: true }) }) it('errors on unknown prop', function (done) { testFail((err) => { expect(err.message).to.include('An unknown \`RouteMatcher\` property was passed: `wrong`') done() }) // @ts-ignore cy.intercept({ wrong: true }) }) it('times must be a positive integer', function (done) { testFail((err) => { expect(err.message).to.include('`times` must be a positive integer.') done() }) cy .intercept({ times: 9.75 }) }) it('string hostname must be a valid domain name', function (done) { testFail((err) => { expect(err.message).to.include('`hostname` must be a valid host name or domain name.') done() }) cy.intercept({ hostname: 'http://website.web' }) }) // https://github.com/cypress-io/cypress/issues/17015 it('string hostname can be "localhost"', () => { cy.intercept({ hostname: 'localhost' }) }) it('string hostname can be unicode', () => { cy.intercept({ hostname: 'はじめよう.みんな' }) }) }) context('with invalid handler', function () { [false, null].forEach(function (handler) { const name = String(handler) it(`${name} fails`, function (done) { testFail((err) => { expect(err).to.eq(this.lastLog.get('error')) expect(err.message).to.contain(`You passed: ${name}`) done() }) // @ts-ignore - this should error cy.intercept('/', handler) }) }) }) context('with invalid StaticResponse', function () { [ [ 'forceNetworkError set but not alone', { forceNetworkError: true, body: 'aaa', }, 'must be the only option', ], [ 'statusCode out of range', { statusCode: -1, }, 'must be a number', ], ].forEach(function ([name, handler, expectedErr]) { it(`${name} fails`, function (done) { testFail((err) => { expect(err).to.eq(this.lastLog.get('error')) expect(err.message).to.contain(expectedErr) expect(err.message).to.contain(`You passed: ${JSON.stringify(handler, null, 2)}`) done() }) // @ts-ignore - this should error cy.intercept('/', handler) }) }) }) }) }) context('events', function () { // @see https://github.com/cypress-io/cypress/issues/9170 it('gracefully handles request received without a known route', function () { cy.intercept('/valid.json', (req) => { req.reply({ bad: 'should not be received' }) }) .then(() => { const routeIds = _.keys(state('routes')) // delete the driver-side route - the server-side route will still exist and cause an event // to be emitted to the driver delete state('routes')[routeIds[0]] expect(state('routes')).to.deep.eq({}) return $.get('/fixtures/valid.json') }) .then((body) => { expect(body).to.include({ foo: 1 }) }) }) it('gracefully handles response received without a known route', function () { cy.intercept('/valid.json', (req) => { state('routes', {}) req.reply((res) => { res.send({ bad: 'should not be received' }) }) }) .then(() => { return $.get('/fixtures/valid.json') }) .then((body) => { expect(body).to.include({ foo: 1 }) }) }) it('gracefully handles response completed without a known route', function () { cy.intercept('/fixtures/valid.json*', (req) => { req.reply((res) => { state('routes', {}) res.send({ bar: 2 }) }) }) .then(() => { return $.get('/fixtures/valid.json') }) .then((body) => { expect(body).to.include({ bar: 2 }) }) }) }) context('network handling', function () { // @see https://github.com/cypress-io/cypress/issues/8497 it('can load transfer-encoding: chunked redirects', function () { cy.intercept('*') const url4 = 'http://localhost:3501/fixtures/generic.html' const url3 = `http://localhost:3501/redirect?href=${encodeURIComponent(url4)}` const url2 = `https://localhost:3502/redirect?chunked=1&href=${encodeURIComponent(url3)}` const url1 = `https://localhost:3502/redirect?chunked=1&href=${encodeURIComponent(url2)}` cy.visit(url1) .location('href').should('eq', url4) }) context('can intercept against any domain', function () { beforeEach(function () { // reset origin cy.visit('http://localhost:3500/fixtures/generic.html') }) it('different origin (HTTP)', function () { cy.intercept('/foo*').as('foo') .then(() => { $.get('http://baz.foobar.com:3501/foo') }) .wait('@foo') }) it('different origin with response interception (HTTP)', function () { cy.intercept('/fixtures/xhr.html*', (req) => { req.reply((res) => { expect(res.body).to.include('xhr fixture') res.body = 'replaced the body' }) }).as('foo') .then(() => { return $.get('http://baz.foobar.com:3501/fixtures/xhr.html') .then((responseText) => { expect(responseText).to.eq('replaced the body') }) }) .wait('@foo').its('response.body').should('eq', 'replaced the body') }) // @see https://github.com/cypress-io/cypress/issues/8487 it('different origin (HTTPS)', function () { cy.intercept('/foo*', 'somethin').as('foo') .then(() => { $.get('https://bar.foobar.com.invalid:3502/foo') }) .wait('@foo') }) it('different origin with response interception (HTTPS)', function () { cy.intercept('/fixtures/xhr.html*', (req) => { req.reply((res) => { expect(res.body).to.include('xhr fixture') res.body = 'replaced the body' }) }).as('foo') .then(() => { return $.get('https://bar.foobar.com:3502/fixtures/xhr.html') .then((responseText) => { expect(responseText).to.eq('replaced the body') }) }) .wait('@foo').its('response.body').should('eq', 'replaced the body') }) }) // @see https://github.com/cypress-io/cypress/issues/9599 context('cors preflight', function () { // a different domain from the page own domain // NOTE: this domain is redirected back to the local host test server // using "hosts" setting in the "cypress.json" file const corsUrl = 'http://diff.foobar.com:3501/no-cors' before(() => { cy.visit('http://127.0.0.1:3500/fixtures/dom.html') }) it('responds to OPTIONS requests', function () { // `/no-cors/` will respond with a 200, but missing valid preflight response cy.request({ method: 'OPTIONS', url: corsUrl }) .then((res) => { expect(res.headers).to.not.have.property('access-control-allow-origin') // so this ajax request should fail due to CORS return $.ajax({ method: 'DELETE', url: corsUrl }) .then(() => { throw new Error('should not succeed') }) .catch((res) => { expect(res).to.include({ statusText: 'error' }) }) }) .intercept('/no-cors', (req) => { req.reply(`intercepted ${req.method}`) }) .then(() => { // but now, the same ajax request succeeds, because the cy.intercept provides CORS return $.ajax({ method: 'DELETE', url: corsUrl }) .then((res) => { expect(res).to.eq('intercepted DELETE') }) }) }) it('can be overwritten', function () { cy.intercept('OPTIONS', '/no-cors', (req) => { req.reply({ headers: { 'access-control-allow-origin': 'http://wrong.invalid', }, }) }) .intercept('/no-cors', (req) => { req.reply(`intercepted ${req.method}`) }) .then(() => { return $.ajax({ method: 'DELETE', url: corsUrl }) .then(() => { throw new Error('should not succeed') }) .catch((res) => { expect(res).to.include({ statusText: 'error' }) }) }) }) }) // https://github.com/cypress-io/cypress/issues/15050 describe('cors expose header', () => { // a different domain from the page own domain const corsUrl = 'http://diff.foobar.com:3501/cors' before(() => { cy.visit('http://127.0.0.1:3500/fixtures/dom.html') }) it('headers option is not set => no expose-header', () => { cy .intercept('/cors*', {}) .as('corsRequest') .then(() => { return $.get(corsUrl) }) cy.wait('@corsRequest').then((res) => { let headers = res.response?.headers expect(headers).to.not.have.property('access-control-expose-headers') }) }) it('headers option only has the accessible headers from the cors request => no expose-header', () => { cy .intercept('/cors*', { body: { success: true }, headers: { 'cache-control': 'no-cache', 'content-language': 'en-US', 'content-type': 'text/html', 'Expires': 'Wed, 21 Oct 2015 07:28:00 GMT', 'Last-Modified': 'Wed, 21 Oct 2015 07:28:00 GMT', 'Pragma': 'no-cache', }, }) .as('corsRequest') .then(() => { return $.get(corsUrl) }) cy.wait('@corsRequest').then((res) => { let headers = res.response?.headers expect(headers).to.not.have.property('access-control-expose-headers') }) }) it('headers option does not have the accessible header => include expose-header', () => { cy .intercept('/cors*', { body: { success: true }, headers: { 'cache-control': 'no-cache', 'content-language': 'en-US', 'x-token': 'token', }, }) .as('corsRequest') .then(() => { return $.get(corsUrl) }) cy.wait('@corsRequest').then((res) => { let headers = res.response?.headers expect(headers!['access-control-expose-headers']).to.eq('*') expect(headers!['x-token']).to.eq('token') }) }) it('headers option has access-control-expose-headers => does not override', () => { cy .intercept('/cors*', { body: { success: true }, headers: { 'access-control-expose-headers': 'x-token', 'x-token': 'token', }, }) .as('corsRequest') .then(() => { return $.get(corsUrl) }) cy.wait('@corsRequest').then((res) => { let headers = res.response?.headers expect(headers!['access-control-expose-headers']).to.eq('x-token') expect(headers!['x-token']).to.eq('token') }) }) }) }) context('stubbing with static responses', function () { it('can stub a response with static body as string', function (done) { cy.intercept({ url: '*', }, 'hello world').then(() => { $.get('/abc123').done((responseText, _, xhr) => { expect(xhr.status).to.eq(200) expect(responseText).to.eq('hello world') done() }) }) }) it('can stub a cy.visit with static body', function () { cy.intercept('/foo', 'hello cruel world').visit('/foo').document().should('contain.text', 'hello cruel world') }) it('can stub a response with an empty StaticResponse', function (done) { cy.intercept('/*', {}).then(() => { $.get('/').done((responseText, _, xhr) => { expect(xhr.status).to.eq(200) expect(responseText).to.eq('') done() }) }) }) it('can stub a response with a network error', function (done) { cy.intercept('/*', { forceNetworkError: true, }).then(() => { $.get('/').fail((xhr) => { expect(xhr.statusText).to.eq('error') expect(xhr.status).to.eq(0) done() }) }) }) it('can use regular strings as response', function () { cy.intercept('/foo*', 'foo bar baz').as('getFoo').then(function (win) { $.get('/foo') }).wait('@getFoo').then(function (res) { expect(res.response!.body).to.eq('foo bar baz') }) }) it('can stub requests with uncommon HTTP methods', function () { cy.intercept('PROPFIND', '/foo', 'foo bar baz').as('getFoo').then(function (win) { $.ajax({ url: '/foo', method: 'PROPFIND', }) }).wait('@getFoo').then(function (res) { expect(res.response!.body).to.eq('foo bar baz') }) }) it('can stub a response with an array', function (done) { const response = ['foo', 'bar', { foo: 'baz' }] cy.intercept({ url: '*', }, response).then(() => { $.get('/abc123').done((responseJson, _, xhr) => { expect(xhr.status).to.eq(200) expect(responseJson).to.deep.eq(response) expect(xhr.getResponseHeader('content-type')).to.eq('application/json') done() }) }) }) it('does not drop falsy static responses', function (done) { cy.intercept({ url: '*', }, { body: false }).then(() => { $.get('/abc123').done((responseText, _, xhr) => { expect(xhr.status).to.eq(200) expect(responseText).to.eq(false) done() }) }) }) it('still works after a cy.visit', function () { cy.intercept(/foo/, { body: JSON.stringify({ foo: 'bar' }), headers: { 'content-type': 'application/json', }, }).as('getFoo').visit('http://localhost:3500/fixtures/jquery.html').window().then(function (win) { return new Promise(function (resolve) { $.get('/foo').done(_.ary(resolve, 0)) }) }).wait('@getFoo').its('request.url').should('include', '/foo').visit('http://localhost:3500/fixtures/generic.html').window().then(function (win) { return new Promise(function (resolve) { $.get('/foo').done(_.ary(resolve, 0)) }) }).wait('@getFoo').its('request.url').should('include', '/foo') }) // @see https://github.com/cypress-io/cypress/issues/15841 it('prevents requests from reaching destination server', function () { const v = String(Date.now()) // this test creates server-side state via /set-var to test if requests are being sent or not cy.then(async () => { await $.get(`/set-var?v=${v}`) expect(await $.get('/get-var')).to.eq(v) }) .intercept('/set-var*', { statusCode: 200, body: 'else' }) .then(async () => { await $.get(`/set-var?v=something`) expect(await $.get('/get-var')).to.eq(v) }) }) // @see https://github.com/cypress-io/cypress/issues/8532 context('can stub a response with an empty array', function () { const assertEmptyArray = (done) => { return () => { $.get('/abc123').done((responseJson, _, xhr) => { expect(xhr.status).to.eq(200) expect(responseJson).to.deep.eq([]) expect(xhr.getResponseHeader('content-type')).to.eq('application/json') done() }) } } it('with explicit StaticResponse', function (done) { cy.intercept({ url: '*', }, { body: [], }).then(assertEmptyArray(done)) }) it('with body shorthand', function (done) { cy.intercept('*', []).then(assertEmptyArray(done)) }) it('with method, url, res shorthand', function (done) { cy.intercept('GET', '*', []).then(assertEmptyArray(done)) }) it('in req.reply', function (done) { cy.intercept('*', (req) => req.reply([])).then(assertEmptyArray(done)) }) it('in res.send', function (done) { cy.intercept('*', (req) => req.reply((res) => res.send(200, []))).then(assertEmptyArray(done)) }) }) context('fixtures', function () { it('can stub a response with a JSON object', function () { cy.intercept({ method: 'POST', url: '/test-xhr', }, { 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 with content-type override', function () { cy.intercept({ method: 'POST', url: '/test-xhr', }, { headers: { 'content-type': 'text/plain', }, 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', function () { cy.intercept({ method: 'POST', url: '/test-xhr', }, { fixture: 'null.json', }).visit('/fixtures/xhr-triggered.html').get('#trigger-xhr').click() cy.contains('#result', '""').should('be.visible') }) // @see https://github.com/cypress-io/cypress/issues/8623 it('works with images', function () { cy.visit('/fixtures/img-embed.html') .contains('div', 'error loading image') .intercept('non-existing-image.png', { fixture: 'media/cypress.png' }) .reload() .contains('div', 'it loaded') }) // @see https://github.com/cypress-io/cypress/issues/15898 // @see https://github.com/cypress-io/cypress/issues/16223 it('works when uploading a binary file', function () { cy.fixture('media/cypress.png').as('image') cy.intercept('POST', '/upload').as('upload') cy.window().then((win) => { const blob = Cypress.Blob.base64StringToBlob(this.image, 'image/png') const xhr = new win.XMLHttpRequest() const formData = new win.FormData() formData.append('file', blob) xhr.open('POST', '/upload', true) xhr.send(formData) }) cy.wait('@upload') }) it('can stub a response with an ArrayBuffer', function () { const stub = new Uint8Array(2) stub[0] = 35 stub[1] = 2 const assertBody = (body: ArrayBuffer) => { const uint8 = new Uint8Array(body) stub.forEach((value, index) => { expect(uint8[index]).to.eq(value) }) } cy.intercept('/binary*', { body: stub.buffer, headers: { 'content-type': 'application/octet-stream', }, statusCode: 200, }).as('get') .visit('/fixtures/display-binary.html') .wait('@get').its('response.body').should(assertBody) .get('#result').should('have.text', stub.join(', ')) }) }) }) context('intercepting request', function () { it('receives the original request in handler', function (done) { cy.intercept('/def456*', function (req) { req.reply({ statusCode: 404, }) expect(req).to.include({ method: 'GET', httpVersion: '1.1', }) expect(req.url).to.match(/^http:\/\/localhost:3500\/def456/) done() }).then(function () { $.get('/def456') }) }) it('receives the original request body in handler', function (done) { cy.intercept('/aaa', function (req) { expect(req.body).to.eq('foo-bar-baz') done() }).then(function () { $.post('/aaa', 'foo-bar-baz') }) }) it('can modify an ArrayBuffer request body', function () { const modifiedUint8 = new Uint8Array(2) modifiedUint8[0] = 35 modifiedUint8[1] = 2 const assertBody = (body: ArrayBuffer) => { const uint8 = new Uint8Array(body) modifiedUint8.forEach((value, index) => { expect(uint8[index]).to.eq(value) }) } cy.intercept('/binary*', function (req) { req.body = modifiedUint8.buffer }).as('post') .visit('/fixtures/dump-binary.html') .wait('@post').its('response.body').should(assertBody) .get('#result').should('have.text', modifiedUint8.join(', ')) }) it('can modify original request body and have it passed to next handler', function (done) { cy.intercept('/post-only', function (req) { expect(req.body).to.eq('quuz') done() }).intercept('/post-only', function (req) { expect(req.body).to.eq('quux') req.body = 'quuz' }).intercept('/post-only', function (req) { expect(req.body).to.eq('foo-bar-baz') req.body = 'quux' }).then(function () { $.post('/post-only', 'foo-bar-baz') }) }) it('can modify a cy.visit before it goes out', function () { cy.intercept('/dump-headers', function (req) { expect(req.headers['foo']).to.eq('bar') req.headers['foo'] = 'quux' }).then(function () { cy.visit({ url: '/dump-headers', headers: { 'foo': 'bar', }, }) cy.get('body').should('contain.text', '"foo":"quux"') }) }) it('can modify the request URL and headers', function (done) { cy.intercept('/does-not-exist', function (req) { expect(req.headers['foo']).to.eq('bar') req.url = 'http://localhost:3500/dump-headers' req.headers['foo'] = 'quux' }).then(function () { const xhr = new XMLHttpRequest() xhr.open('GET', '/does-not-exist') xhr.setRequestHeader('foo', 'bar') xhr.send() xhr.onload = () => { expect(xhr.responseText).to.contain('"foo":"quux"') done() } }) }) it('can delete a request header', function () { cy.intercept('/dump-headers*', function (req) { expect(req.headers).to.include({ 'foo': 'bar' }) delete req.headers['foo'] }).as('get') .then(() => { return $.get({ url: '/dump-headers', headers: { 'foo': 'bar', }, }) }) .should('not.include', 'foo') .wait('@get') }) it('can modify the request method', function (done) { cy.intercept('/dump-method', function (req) { expect(req.method).to.eq('POST') req.method = 'PATCH' }).then(function () { $.post('/dump-method').done((responseText) => { expect(responseText).to.contain('request method: PATCH') done() }) }) }) it('can modify the request body', function (done) { const body = '{"foo":"bar"}' cy.intercept('/post-only', function (req) { expect(req.body).to.eq('quuz') req.headers['content-type'] = 'application/json' req.body = body }).then(function () { $.post('/post-only', 'quuz').done((responseText) => { expect(responseText).to.contain(body) done() }) }) }) it('can add a body to a request that does not have one', function (done) { const body = '{"foo":"bar"}' cy.intercept('/post-only*', function (req) { expect(req.body).to.eq('') expect(req.method).to.eq('GET') req.method = 'POST' req.headers['content-type'] = 'application/json' req.body = body }).then(function () { $.get('/post-only').done((responseText) => { expect(responseText).to.contain(body) done() }) }) }) it('can reply with a JSON fixture', function () { cy.intercept({ method: 'POST', url: '/test-xhr', }, (req) => { req.reply({ fixture: 'valid.json', }) }).visit('/fixtures/xhr-triggered.html').get('#trigger-xhr').click() cy.contains('{"foo":1,"bar":{"baz":"cypress"}}') }) it('can delay and throttle a StaticResponse', function (done) { const payload = 'A'.repeat(10 * 1024) const throttleKbps = 10 const delay = 250 const expectedSeconds = payload.length / (1024 * throttleKbps) + delay / 1000 cy.intercept('/timeout*', (req) => { this.start = Date.now() req.reply({ statusCode: 200, body: payload, throttleKbps, delay, }) }).then(() => { return $.get('/timeout').then((responseText) => { expect(Date.now() - this.start).to.be.closeTo(expectedSeconds * 1000 + 100, 100) expect(responseText).to.eq(payload) done() }) }) }) it('can delay with deprecated delayMs param', function (done) { const delayMs = 250 cy.intercept('/timeout*', (req) => { this.start = Date.now() req.reply({ delayMs, }) }).then(() => { return $.get('/timeout').then((responseText) => { expect(Date.now() - this.start).to.be.closeTo(250 + 100, 100) done() }) }) }) // @see https://github.com/cypress-io/cypress/issues/14446 it('should delay the same amount on every response', () => { const delay = 250 const testDelay = () => { const start = Date.now() return $.get('/timeout').then((responseText) => { expect(Date.now() - start).to.be.closeTo(delay, 50) expect(responseText).to.eq('foo') }) } cy.intercept('/timeout*', { statusCode: 200, body: 'foo', delay, }).as('get') .then(() => testDelay()).wait('@get') .then(() => testDelay()).wait('@get') .then(() => testDelay()).wait('@get') }) // https://github.com/cypress-io/cypress/issues/15188 it('delay works correctly with 204 No Content', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('No response ever occurred') done() }) cy.intercept('POST', '/post-only', { statusCode: 204, // delay is not respected delay: 5000, }).as('create') cy.window().then((win) => { win.eval( `fetch("/post-only", { method: 'POST', // *GET, POST, PUT, DELETE, etc. });`, ) }) cy.wait('@create', { timeout: 500 }) }) // @see https://github.com/cypress-io/cypress/issues/15901 it('can intercept utf-8 request bodies without crashing', function () { cy.intercept('POST', 'http://localhost:5000/api/sample') cy.visit('/fixtures/utf8-post.html') }) // https://github.com/cypress-io/cypress/issues/16327 context('request url querystring', () => { // In the code below, cy.window() is used instead of $.get. // It's because XHR is not sent on Firefox and it's flaky on Chrome. it('parse query correctly', () => { cy.intercept({ url: '/users*' }, (req) => { expect(req.query.someKey).to.deep.equal('someValue') expect(req.query).to.deep.equal({ someKey: 'someValue' }) }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) context('reconcile changes', () => { it('by assigning a new query parameter obj', () => { cy.intercept({ url: '/users*' }, (req) => { req.query = { a: 'b', } expect(req.url).to.eq('http://localhost:3500/users?a=b') }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) it('by setting new properties', () => { cy.intercept({ url: '/users*' }, (req) => { expect(req.query.a).to.eq('b') req.query.c = 'd' expect(req.url).to.eq('http://localhost:3500/users?a=b&c=d') }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?a=b') xhr.send() }) cy.wait('@getUrl') }) it('by doing both', () => { cy.intercept({ url: '/users*' }, (req) => { req.query = { a: 'b', } expect(req.query.a).to.eq('b') req.query.c = 'd' expect(req.url).to.eq('http://localhost:3500/users?a=b&c=d') }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) it('by deleting query member', () => { cy.intercept({ url: '/users*' }, (req) => { req.query = { a: 'b', c: 'd', } delete req.query.c expect(req.url).to.eq('http://localhost:3500/users?a=b') }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) context('by setting new url', () => { it('absolute path', () => { cy.intercept({ url: '/users*' }, (req) => { req.url = 'http://localhost:3500/users?a=b' expect(req.query).to.deep.eq({ a: 'b' }) }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) it('relative path', () => { cy.intercept({ url: '/users*' }, (req) => { req.url = '/users?a=b' expect(req.query).to.deep.eq({ a: 'b' }) expect(req.url).to.eq('http://localhost:3500/users?a=b') }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) it('empty string', () => { cy.intercept({ url: '/users*' }, (req) => { req.url = '' expect(req.query).to.deep.eq({}) }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) }) context('throwing errors correctly', () => { it('defineproperty', (done) => { cy.on('fail', (err) => { expect(err.message).to.eq('`defineProperty()` is not allowed.') done() }) cy.intercept({ url: '/users*' }, (req) => { Object.defineProperty(req.query, 'key', { enumerable: false, configurable: false, writable: false, value: 'static', }) expect(req.query).to.deep.eq({}) }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) it('setPrototypeOf', (done) => { cy.on('fail', (err) => { expect(err.message).to.eq('`setPrototypeOf()` is not allowed.') done() }) cy.intercept({ url: '/users*' }, (req) => { Object.setPrototypeOf(req.query, null) expect(req.query).to.deep.eq({}) }).as('getUrl') cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users?someKey=someValue') xhr.send() }) cy.wait('@getUrl') }) }) }) }) context('request events', function () { context('can end response', () => { for (const eventName of ['before:response', 'response']) { it(`in \`${eventName}\``, () => { const expectBeforeResponse = eventName === 'response' let beforeResponseCalled = false cy.intercept('/foo*', (req) => { req.on('response', (res) => { throw new Error('response should not be reached') }) req.on('before:response', (res) => { beforeResponseCalled = true if (!expectBeforeResponse) { throw new Error('before:response should not be reached') } }) }).as('first') .intercept('/foo*', (req) => { // @ts-ignore req.on(eventName, (res) => { res.send({ statusCode: 200, fixture: 'valid.json', }) }) }).as('second') .then(() => { return $.getJSON('/foo') }) .should('include', { 'foo': 1 }) .wait('@first').wait('@second') .then(() => { expect(beforeResponseCalled).to.eq(expectBeforeResponse) }) }) } }) context('errors', function () { it('when unknown eventName is passed', function (done) { testFail((err) => { expect(err.message).to.contain('An invalid event name was passed as the first parameter to \`req.on()\`.') done() }) cy.intercept('/foo', function (req) { // @ts-ignore req.on('totally-bad', _.noop) }).visit('/foo') }) it('when no function is passed', function (done) { testFail((err) => { expect(err.message).to.contain('\`req.on()\` requires the second parameter to be a function.') done() }) cy.intercept('/foo', function (req) { // @ts-ignore req.on('before:response', false) }).visit('/foo') }) }) }) context('body parsing', function () { [ ['application/json', '{"foo":"bar"}'], ['application/vnd.api+json', '{}'], ].forEach(([contentType, expectedBody]) => { it(`automatically parses ${contentType} request bodies`, function () { const p = Promise.defer() cy.intercept('/post-only', (req) => { expect(req.headers['content-type']).to.eq(contentType) expect(req.body).to.deep.eq({ foo: 'bar' }) p.resolve() }).as('post') .then(() => { return $.ajax({ url: '/post-only', method: 'POST', contentType, data: JSON.stringify({ foo: 'bar' }), }) }).then((responseText) => { expect(responseText).to.include(`request body:
${expectedBody}`) return p }) .wait('@post').its('request.body').should('deep.eq', { foo: 'bar' }) }) }) it('doesn\'t automatically parse JSON request bodies if content-type is wrong', function () { const p = Promise.defer() cy.intercept('/post-only', (req) => { expect(req.body).to.deep.eq(JSON.stringify({ foo: 'bar' })) p.resolve() }).as('post') .then(() => { return $.ajax({ url: '/post-only', method: 'POST', contentType: 'text/html', data: JSON.stringify({ foo: 'bar' }), }) }).wrap(p) .wait('@post').its('request.body').should('eq', JSON.stringify({ foo: 'bar' })) }) it('sets body to string if JSON is malformed', function () { const p = Promise.defer() cy.intercept('/post-only*', (req) => { expect(req.body).to.deep.eq('{ foo::: }') p.resolve() }).as('post') .then(() => { return $.ajax({ url: '/post-only', method: 'POST', contentType: 'application/json', // invalid JSON data: '{ foo::: }', }).catch(() => p) }) .wait('@post').its('request.body').should('deep.eq', '{ foo::: }') }) }) context('matches requests as expected', function () { it('handles querystrings as expected', function () { cy.intercept('*', 'it worked').as('final') .intercept({ query: { foo: 'b*r', baz: /quu[x]/, }, }).as('third') .intercept({ path: '/abc?foo=bar&baz=qu*x*', }).as('second') .intercept({ pathname: '/abc', }).as('first') .then(() => { return $.get('/abc?foo=bar&baz=quux') }) .wait('@first') .wait('@second') .wait('@third') .wait('@final') }) // @see https://github.com/cypress-io/cypress/issues/8921 it('with case-insensitive header matching', function () { cy.intercept({ headers: { 'X-some-Thing': 'foo', }, }) .as('foo') .then(() => { $.get({ url: '/foo', headers: { 'X-SOME-THING': 'foo', }, }) }) .wait('@foo') }) // @see https://github.com/cypress-io/cypress/issues/9379 context('falls back to matching by path if plain string is passed', function () { it('matches globs against path', function (done) { cy.intercept('/foo/*', (req) => { expect(req.url).to.include('/foo/1') done() }) .then(() => { $.ajax({ url: '/foo/1', cache: true }) }) }) it('matches nested globs against path', function (done) { cy.intercept('/foo/*/bar', (req) => { expect(req.url).to.match(/\/foo\/1\/bar$/) done() }) .then(() => { $.get({ url: '/foo/1/bar', cache: true }) }) }) }) context('with `times`', function () { it('only uses each handler N times', function () { const third = sinon.stub() cy .intercept({ url: '/foo*', times: 3 }, 'fourth').as('4') .intercept({ url: '/foo*', times: 2 }, third).as('3') .intercept({ url: '/foo*', times: 2 }, 'second').as('2') .intercept({ url: '/foo*', times: 1 }, 'first').as('1') .then(async () => { const expectGet = (expected) => $.get('/foo').then((res) => expect(res).to.eq(expected)) await Promise.mapSeries([ 'first', 'second', 'second', 'fourth', 'fourth', 'fourth', ], expectGet) expect(third).to.be.calledTwice // now that matches are exhausted, it should fall through await $.get('/foo').catch((xhr) => { expect(xhr).to.include({ status: 404, }) }) }) .get('@1.all').should('have.length', 1) .get('@2.all').should('have.length', 2) .get('@3.all').should('have.length', 2) .get('@4.all').should('have.length', 3) }) context('stopPropagation: true', () => { it('works with continue', () => { cy.intercept({ method: 'POST', times: 1, url: '/post-only', }, (req) => { req.continue((res) => { res.body = 'stubbed data' }) }).as('interceptor') cy.visit('fixtures/request.html') cy.get('#request').click() cy.get('#result').should('contain', 'stubbed data') cy.get('#request').click() cy.get('#result').should('contain', 'client') }) it('works with reply', () => { cy.intercept({ method: 'POST', times: 1, url: '/post-only', }, (req) => { req.reply('stubbed data') }).as('interceptor') cy.visit('fixtures/request.html') cy.get('#request').click() cy.get('#result').should('contain', 'stubbed data') cy.get('#request').click() cy.get('#result').should('contain', 'client') }) it('works with reply and fallthrough', () => { let times = 0 cy.intercept({ method: 'POST', times: 3, url: '/post-only', }, (req) => { req.reply(`${req.body === 'foo' ? 'foo' : 'nothing'} stubbed data ${times++}`) }) cy.intercept({ method: 'POST', times: 2, url: '/post-only', }, (req) => { req.body = 'foo' }) cy.visit('fixtures/request.html') cy.get('#request').click() cy.get('#result').should('contain', 'foo stubbed data 0') cy.get('#request').click() cy.get('#result').should('contain', 'foo stubbed data 1') cy.get('#request').click() cy.get('#result').should('contain', 'nothing stubbed data 2') cy.get('#request').click() cy.get('#result').should('contain', 'client') }) }) }) }) context('with StaticResponse shorthand', function () { it('req.reply(body)', function () { cy.intercept('/foo*', function (req) { req.reply('baz') }) .then(() => $.get('/foo')) .should('eq', 'baz') }) it('req.reply(json)', function () { cy.intercept('/foo*', function (req) { req.reply({ baz: 'quux' }) }) .then(() => $.getJSON('/foo')) .should('deep.eq', { baz: 'quux' }) }) it('req.reply(status)', function () { cy.intercept('/foo*', function (req) { req.reply(777) }) .then(() => { return new Promise((resolve) => { $.get('/foo').fail((x) => resolve(x.status)) }) }) .should('eq', 777) }) it('req.reply(status, body)', function () { cy.intercept('/foo*', function (req) { req.reply(777, 'bar') }) .then(() => { return new Promise((resolve) => { $.get('/foo').fail((xhr) => resolve(_.pick(xhr, 'status', 'responseText'))) }) }).should('include', { status: 777, responseText: 'bar', }) }) it('req.reply(status, json)', function () { cy.intercept('/foo*', function (req) { req.reply(777, { bar: 'baz' }) }) .then(() => { return new Promise((resolve) => { $.get('/foo').fail((xhr) => resolve(_.pick(xhr, 'status', 'responseJSON'))) }) }).should('deep.include', { status: 777, responseJSON: { bar: 'baz' }, }) }) it('req.reply(status, json, headers)', function () { cy.intercept('/foo*', function (req) { req.reply(777, { bar: 'baz' }, { 'x-quux': 'quuz' }) }) .then(() => { return new Promise((resolve) => { $.get('/foo').fail((xhr) => resolve(_.pick(xhr, 'status', 'responseJSON', 'getAllResponseHeaders'))) }) }).should('deep.include', { status: 777, responseJSON: { bar: 'baz' }, }).invoke('getAllResponseHeaders') .should('include', 'x-quux: quuz') .and('include', 'content-type: application/json') }) it('can forceNetworkError', function (done) { cy.intercept('/foo*', function (req) { req.reply({ forceNetworkError: true }) }) .then(() => { $.get('/foo').fail((xhr) => { expect(xhr).to.include({ status: 0, statusText: 'error', readyState: 0, }) done() }) }) }) }) context('request handler chaining', function () { it('passes request through in reverse order', function () { cy.intercept('/dump-method', function (req) { expect(req.method).to.eq('PATCH') req.reply() }).intercept('/dump-method', function (req) { expect(req.method).to.eq('POST') req.method = 'PATCH' }).intercept('/dump-method', function (req) { expect(req.method).to.eq('GET') req.method = 'POST' }).visit('/dump-method').contains('PATCH') }) it('stops passing request through once req.reply called', function () { cy.intercept('/dump-method', function (req) { throw new Error('this should not have been reached') }).intercept('/dump-method', function (req) { req.reply() }).visit('/dump-method').contains('GET') }) }) context('errors', function () { it('fails test if req.reply is called twice in req handler', function (done) { testFail((err) => { expect(err.message).to.contain('`req.reply()` and/or `req.continue()` were called to signal request completion multiple times, but a request can only be completed once') done() }) cy.intercept('/dump-method', function (req) { req.reply() req.reply() }).visit('/dump-method') }) it('fails test if req.reply is called after req handler finishes', function (done) { testFail((err) => { expect(err.message).to.contain('> `req.reply()` was called after the request handler finished executing') done() }) cy.intercept('/dump-method', function (req) { setTimeout(() => req.reply(), 50) }).visit('/dump-method') }) it('fails test if req.reply is called after req handler resolves', function (done) { testFail((err) => { expect(err.message).to.contain('> `req.reply()` was called after the request handler finished executing') done() }) cy.intercept('/dump-method', function (req) { setTimeout(() => req.reply(), 50) return Promise.resolve() }).visit('/dump-method') }) it('fails test if an exception is thrown in req handler', function (done) { cy.on('fail', (err2) => { expect(err2).to.eq(err) done() }) const err = new Error('bar') cy.intercept('/foo', () => { throw err }).visit('/foo') }) it('fails test if req.reply is called with an invalid StaticResponse', function (done) { testFail((err) => { expect(err.message).to.contain('must be a number between 100 and 999 (inclusive).') done() }) cy.intercept('/foo', (req) => { req.reply({ statusCode: 1 }) }).visit('/foo') }) it('can timeout in request handler', function (done) { testFail((err) => { Cypress.config('defaultCommandTimeout', 5000) expect(err.message).to.match(/^A request callback passed to `cy.intercept\(\)` timed out after returning a Promise that took more than the `defaultCommandTimeout` of `50ms` to resolve\./) done() }) Cypress.config('defaultCommandTimeout', 50) cy.intercept('/foo', () => { return Promise.delay(200) }).visit('/foo') }) }) context('correctly determines the content-length of an intercepted request', function () { it('when body is empty', function (done) { cy.intercept('/post-only', function (req) { expect(req.headers['content-length']).to.eq('0') done() }).intercept('/post-only', function (req) { req.body = '' }) .then(() => { $.post('/post-only', 'foo') }) }) it('when body contains ascii', function (done) { cy.intercept('/post-only', function (req) { expect(req.headers['content-length']).to.eq('18') done() }).intercept('/post-only', function (req) { req.body = 'this is only ascii' }) .then(() => { $.post('/post-only', 'bar') }) }) it('when body contains unicode', function (done) { cy.intercept('/post-only', function (req) { expect(req.headers['content-length']).to.eq('8') done() }).intercept('/post-only', function (req) { req.body = '🙃🤔' }) .then(() => { $.post('/post-only', 'baz') }) }) }) }) context('intercepting response', function () { it('receives the original response in handler', function (done) { cy.intercept('/json-content-type*', function (req) { req.reply(function (res) { expect(res.body).to.deep.eq({}) done() }) expect(req.url).to.match(/^http:\/\/localhost:3500\/json-content-type/) }).then(function () { $.get('/json-content-type') }) }) it('intercepts redirects as expected', function () { const href = `/fixtures/generic.html?t=${Date.now()}` const url = `/redirect?href=${encodeURIComponent(href)}` cy.intercept('/redirect?href=*', (req) => { req.reply((res) => { expect(res.statusCode).to.eq(301) expect(res.headers.location).to.eq(href) res.send() }) }) .as('redirect') .intercept('/fixtures/generic.html?t=*').as('dest') .then(() => fetch(url)) .wait('@redirect') .wait('@dest') }) it('can simply wait on redirects without intercepting', function () { const href = `/fixtures/generic.html?t=${Date.now()}` const url = `/redirect?href=${encodeURIComponent(href)}` cy.intercept('/redirect*') .as('redirect') .intercept('/fixtures/generic.html*').as('dest') .then(() => fetch(url)) .wait('@redirect') .wait('@dest') }) // @see https://github.com/cypress-io/cypress/issues/7967 it('can skip redirects via followRedirect', function () { const href = `/fixtures/generic.html?t=${Date.now()}` const url = `/redirect?href=${encodeURIComponent(href)}` cy.intercept('/redirect', (req) => { req.followRedirect = true req.reply((res) => { expect(res.body).to.include('Some generic content') expect(res.statusCode).to.eq(200) res.send() }) }) .then(() => fetch(url)) }) it('intercepts cached responses as expected', { browser: '!firefox', // TODO: why does firefox behave differently? transparently returns cached response }, function () { // use a queryparam to bust cache from previous runs of this test const url = `/fixtures/generic.html?t=${Date.now()}` let hits = 0 cy.intercept('/fixtures/generic.html*', (req) => { req.reply((res) => { // the second time the request is sent, headers should have been passed // that result in Express serving a 304 // Cypress is not expected to understand cache mechanisms at this point - // if the user wants to break caching, they can DIY by editing headers const expectedStatusCode = [200, 304][hits] expect(expectedStatusCode).to.exist expect(res.statusCode).to.eq(expectedStatusCode) hits++ res.send() }) }) .as('foo') .then(() => _.times(2, () => fetch(url))) .wait('@foo') .wait('@foo') .then(() => { expect(hits).to.eq(2) }) }) it('can intercept a large proxy response', function (done) { cy.intercept('/1mb', (req) => { req.reply((res) => { res.send() }) }).then(() => { $.get('/1mb').done((responseText) => { // NOTE: the log from this when it fails is so long that it causes the browser to lock up :[ expect(responseText).to.eq('X'.repeat(1024 * 1024)) done() }) }) }) it('can delay a proxy response using res.setDelay', function (done) { cy.intercept('/timeout*', (req) => { req.reply((res) => { this.start = Date.now() res.setDelay(1000).send('delay worked') }) }).then(() => { $.get('/timeout') .done((responseText) => { expect(Date.now() - this.start).to.be.closeTo(1100, 100) expect(responseText).to.include('delay worked') done() }) }) }) it('can \'delay\' a proxy response using Promise.delay', function (done) { cy.intercept('/timeout*', (req) => { req.reply((res) => { this.start = Date.now() return Promise.delay(1000) .then(() => { res.send('Promise.delay worked') }) }) }).then(() => { $.get('/timeout').then((responseText) => { expect(Date.now() - this.start).to.be.closeTo(1000, 100) expect(responseText).to.eq('Promise.delay worked') done() }) }) }) it('can throttle a proxy response using res.setThrottle', function (done) { cy.intercept('/1mb*', (req) => { // don't let gzip make response smaller and throw off the timing delete req.headers['accept-encoding'] req.reply((res) => { this.start = Date.now() res.setThrottle(1024).send() }) }).then(() => { $.get('/1mb').done((responseText) => { // 1MB @ 1MB/s = ~1 second expect(Date.now() - this.start).to.be.closeTo(1000, 250) expect(responseText).to.eq('X'.repeat(1024 * 1024)) done() }) }) }) it('can throttle a static response using res.setThrottle', function (done) { const payload = 'A'.repeat(10 * 1024) const kbps = 10 const expectedSeconds = payload.length / (1024 * kbps) cy.intercept('/timeout*', (req) => { req.reply((res) => { this.start = Date.now() res.setThrottle(kbps).send(payload) }) }).then(() => { $.get('/timeout').done((responseText) => { expect(Date.now() - this.start).to.be.closeTo(expectedSeconds * 1000, 250) expect(responseText).to.eq(payload) done() }) }) }) it('can delay and throttle a static response', function (done) { const payload = 'A'.repeat(10 * 1024) const kbps = 20 let expectedSeconds = payload.length / (1024 * kbps) const delay = 500 expectedSeconds += delay / 1000 cy.intercept('/timeout*', (req) => { req.reply((res) => { this.start = Date.now() res.setThrottle(kbps).setDelay(delay).send({ statusCode: 200, body: payload, }) }) }).then(() => { $.get('/timeout').done((responseText) => { expect(Date.now() - this.start).to.be.closeTo(expectedSeconds * 1000, 100) expect(responseText).to.eq(payload) done() }) }) }) it('can reply with a JSON fixture', function () { cy.intercept({ method: 'POST', url: '/test-xhr', }, (req) => { req.url = '/timeout' req.method = 'GET' req.reply((res) => { res.send({ headers: { 'content-type': 'application/json', }, fixture: 'valid.json', }) }) }).visit('/fixtures/xhr-triggered.html').get('#trigger-xhr').click() cy.contains('{"foo":1,"bar":{"baz":"cypress"}}') }) it('can delete a response header', function () { cy .then(() => { const xhr = $.get('/json-content-type') return xhr.then(() => { return xhr.getAllResponseHeaders() }) }) .should('include', 'content-type: application/json') .intercept('/json-content-type*', function (req) { req.reply((res) => { delete res.headers['content-type'] }) }).as('get') .then(() => { const xhr = $.get('/json-content-type') return xhr.then(() => { return xhr.getAllResponseHeaders() }) }) .should('not.include', 'content-type') .wait('@get') }) // https://github.com/cypress-io/cypress/issues/17084 it('does not overwrite the json-related content-type header', () => { cy.intercept('/json-content-type', (req) => { req.on('response', (res) => { res.send({ statusCode: 500, headers: { 'content-type': 'application/problem+json', 'access-control-allow-origin': '*', }, body: { status: 500, title: 'Internal Server Error', }, }) }) }) .then(() => { return fetch('/json-content-type') .then((res) => { expect(res.headers.get('content-type')).to.eq('application/problem+json') }) }) }) context('body parsing', function () { [ 'application/json', 'application/vnd.api+json', ].forEach((contentType) => { it(`automatically parses ${contentType} response bodies`, function () { const p = Promise.defer() cy.intercept(`/json-content-type*`, (req) => { req.reply((res) => { expect(res.headers['content-type']).to.eq(contentType) expect(res.body).to.deep.eq({}) p.resolve() }) }).as('get') .then(() => { return $.get(`/json-content-type?contentType=${encodeURIComponent(contentType)}`) }).then((responseJson) => { expect(responseJson).to.deep.eq({}) return p }) .wait('@get').its('response.body').should('deep.eq', {}) }) }) it('doesn\'t automatically parse JSON response bodies if content-type is wrong', function () { const p = Promise.defer() cy.intercept('/fixtures/json.txt*', (req) => { req.reply((res) => { expect(res.body).to.eq('{ "foo": "bar" }') p.resolve() }) }).as('get') .then(() => { return $.get('/fixtures/json.txt') }).then((responseText) => { expect(responseText).to.deep.eq('{ "foo": "bar" }') return p }) .wait('@get').its('response.body').should('deep.eq', '{ "foo": "bar" }') }) // @see https://github.com/cypress-io/cypress/issues/16722 it('doesn\'t automatically parse response bodies if content is binary', function () { const expectedBody = [120, 42, 7] const assertBody = (body: ArrayBuffer) => { const uint8 = new Uint8Array(body) expectedBody.forEach((value, index) => { expect(uint8[index]).to.eq(value) }) } cy.intercept('/binary*', (req) => { req.on('response', (res) => { expect(_.isArrayBuffer(res.body)).to.eq(true) assertBody(res.body) }) }).as('get') .visit('/fixtures/display-binary.html') .wait('@get').its('response.body').should(assertBody) .get('#result').should('have.text', expectedBody.join(', ')) }) it('sets body to string if JSON is malformed', function () { const p = Promise.defer() cy.intercept('/fixtures/invalid.json*', (req) => { req.reply((res) => { expect(res.headers['content-type']).to.match(/^application\/json/) expect(res.body).to.eq('{ foo:::: }') p.resolve() }) }).as('get') .then(() => { return $.get('/fixtures/invalid.json').catch(() => p) }) .wait('@get').its('response.body').should('deep.eq', '{ foo:::: }') }) }) context('with StaticResponse', function () { it('res.send(body)', function () { cy.intercept('/custom-headers*', function (req) { req.reply((res) => { res.send('baz') }) }) .then(() => { return $.get('/custom-headers') .then((_a, _b, xhr) => { expect(xhr.status).to.eq(200) expect(xhr.responseText).to.eq('baz') expect(xhr.getAllResponseHeaders()) .to.include('x-foo: bar') }) }) }) it('res.send(json)', function () { cy.intercept('/custom-headers*', function (req) { req.reply((res) => { res.send({ baz: 'quux' }) }) }) .then(() => { return $.getJSON('/custom-headers') .then((data, _b, xhr) => { expect(xhr.status).to.eq(200) expect(xhr.getAllResponseHeaders()) .to.include('x-foo: bar') .and.include('content-type: application/json') expect(data).to.deep.eq({ baz: 'quux' }) }) }) }) it('res.send(status)', function (done) { cy.intercept('/custom-headers*', function (req) { req.reply((res) => { res.send(777) }) }) .then(() => { $.getJSON('/custom-headers') .fail((xhr) => { expect(xhr.status).to.eq(777) expect(xhr.getAllResponseHeaders()) .to.include('x-foo: bar') expect(xhr.responseText).to.include('hello there') done() }) }) }) it('res.send(status, body)', function (done) { cy.intercept('/custom-headers*', function (req) { req.reply((res) => { res.send(777, 'bar') }) }) .then(() => { $.get('/custom-headers') .fail((xhr) => { expect(xhr.status).to.eq(777) expect(xhr.responseText).to.eq('bar') expect(xhr.getAllResponseHeaders()) .to.include('x-foo: bar') done() }) }) }) it('res.send(status, json)', function (done) { cy.intercept('/custom-headers*', function (req) { req.reply((res) => { res.send(777, { bar: 'baz' }) }) }) .then(() => { $.getJSON('/custom-headers') .fail((xhr) => { expect(xhr.status).to.eq(777) expect(xhr.responseJSON).to.deep.eq({ bar: 'baz' }) expect(xhr.getAllResponseHeaders()) .to.include('x-foo: bar') .and.include('content-type: application/json') done() }) }) }) it('res.send(status, json, headers)', function (done) { cy.intercept('/custom-headers*', function (req) { req.reply((res) => { res.send(777, { bar: 'baz' }, { 'x-quux': 'quuz' }) }) }) .then(() => { $.getJSON('/custom-headers') .fail((xhr) => { expect(xhr.status).to.eq(777) expect(xhr.responseJSON).to.deep.eq({ bar: 'baz' }) expect(xhr.getAllResponseHeaders()) .to.include('x-foo: bar') // headers should be merged .and.include('x-quux: quuz') .and.include('content-type: application/json') done() }) }) }) it('res.send({ fixture })', function () { cy.intercept('/foo*', function (req) { req.reply((res) => { res.send({ statusCode: 200, fixture: 'valid.json', }) }) }) .then(() => { return $.getJSON('/foo') }) .should('include', { foo: 1 }) }) it('can forceNetworkError', function (done) { cy.intercept('/foo*', function (req) { req.reply((res) => { res.send({ forceNetworkError: true }) }) }) .then(() => { $.get('/foo').fail((xhr) => { expect(xhr).to.include({ status: 0, statusText: 'error', readyState: 0, }) done() }) }) }) it('can delay and throttle', function (done) { const payload = 'A'.repeat(10 * 1024) const throttleKbps = 50 const delay = 50 const expectedSeconds = payload.length / (1024 * throttleKbps) + delay / 1000 cy.intercept('/timeout*', (req) => { req.reply((res) => { this.start = Date.now() // ensure .throttle and .delay are overridden res.setThrottle(1e6).setDelay(1).send({ statusCode: 200, body: payload, throttleKbps, delay, }) }) }).then(() => { return $.get('/timeout').then((responseText) => { expect(Date.now() - this.start).to.be.closeTo(expectedSeconds * 1000 + 475, 500) expect(responseText).to.eq(payload) done() }) }) }) }) context('response handler chaining', function () { it('passes response through in reverse order', function () { cy.intercept('/dump-method', function (req) { req.reply((res) => { expect(res.body).to.contain('new body') }) }).intercept('/dump-method', function (req) { req.reply((res) => { expect(res.body).to.contain('GET') res.body = 'new body' }) }).visit('/dump-method') .contains('new body') }) it('stops passing response through once res.send called', function () { cy.intercept('/dump-method', function (req) { req.reply((res) => { throw new Error('this should not have been reached') }) }).intercept('/dump-method', function (req) { req.reply((res) => { res.send() }) }).visit('/dump-method').contains('GET') }) }) context('errors', function () { it('fails test if res.send is called twice in req handler', function (done) { testFail((err) => { expect(err.message).to.contain('`res.send()` was called multiple times in a response handler, but the response can only be sent once.') done() }) cy.intercept('/dump-method', function (req) { req.reply(function (res) { res.send() res.send() }) }).visit('/dump-method') }) it('fails test if an exception is thrown in res handler', function (done) { cy.on('fail', (err2) => { expect(err2).to.eq(err) done() }) const err = new Error('bar') cy.intercept('/foo*', (req) => { req.reply(() => { throw err }) }) .then(() => { $.get('/foo') }) .wait(1000) }) it('fails test if res.send is called with an invalid StaticResponse', function (done) { testFail((err) => { expect(err.message).to.include('must be a number between 100 and 999 (inclusive).') done() }) cy.intercept('/foo*', (req) => { req.reply((res) => { res.send({ statusCode: 1 }) }) }) .then(() => { $.get('/foo') }) }) it('fails test if network error occurs retrieving response and response is intercepted', function (done) { testFail((err) => { expect(err.message) .to.contain('A callback was provided to intercept the upstream response, but a network error occurred while making the request:') .and.contain('Error: connect ECONNREFUSED 127.0.0.1:3333') done() }) cy.intercept('/should-err*', function (req) { req.reply(() => {}) }).then(function () { $.get('http://localhost:3333/should-err') }) }) it('doesn\'t fail test if network error occurs retrieving response and response is not intercepted', { // TODO: for some reason, this test is busted in FF browser: '!firefox', }, function () { cy.intercept('/should-err*', function (req) { req.reply() }) .as('err') .then(function () { return new Promise((resolve) => { $.get('http://localhost:3333/should-err') .fail((xhr) => { expect(xhr).to.include({ status: 0, statusText: 'error', }) resolve() }) }) }) .wait('@err', { timeout: 50 }) }) it('can timeout in req.reply handler', function (done) { testFail((err) => { Cypress.config('defaultCommandTimeout', 5000) expect(err.message).to.match(/^A response handler timed out after returning a Promise that took more than the `defaultCommandTimeout` of `50ms` to resolve\./) done() }) cy.intercept('/timeout', (req) => { Cypress.config('defaultCommandTimeout', 50) req.reply(() => { return Promise.delay(200) }) }) .visit('/timeout') }) it('can timeout when retrieving upstream response', { responseTimeout: 25, }, function (done) { cy.once('fail', (err) => { expect(err.message).to.match(/^A callback was provided to intercept the upstream response, but the request timed out after the `responseTimeout` of `25ms`\./) .and.match(/ESOCKETTIMEDOUT|ETIMEDOUT/) done() }) cy.intercept('/timeout*', (req) => { req.reply(_.noop) }).then(() => { $.get('/timeout?ms=50') }) }) }) }) context('waiting and aliasing', function () { const testFailWaiting = (cb) => testFail(cb, 'https://on.cypress.io/wait') it('can wait on a single response using "alias"', function () { cy.intercept('/foo*', 'bar') .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar') }) it('can timeout waiting on a single response using "alias"', function (done) { testFailWaiting((err) => { expect(err.message).to.contain('No response ever occurred.') done() }) cy.intercept('/foo*', () => new Promise(_.noop)) .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar', { timeout: 100 }) }) it('can wait on a single response using "alias.response"', function () { cy.intercept('/foo*', 'bar') .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar.response') }) it('can timeout waiting on a single response using "alias.response"', function (done) { testFailWaiting((err) => { expect(err.message).to.contain('No response ever occurred.') done() }) cy.intercept('/foo*', () => new Promise(_.noop)) .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar.response', { timeout: 100 }) }) it('can wait on a single request using "alias.request"', function () { cy.intercept('/foo*') .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar.request') }) it('can timeout waiting on a single request using "alias.request"', function (done) { testFailWaiting((err) => { expect(err.message).to.contain('No request ever occurred.') done() }) cy.intercept('/foo*') .as('foo.bar') .wait('@foo.bar.request', { timeout: 100 }) }) it('can incrementally wait on responses', function () { cy.intercept('/foo*', 'bar') .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar') }) it('can timeout incrementally waiting on responses', function (done) { testFailWaiting((err) => { expect(err.message).to.contain('for the 1st response to the route') done() }) cy.intercept('/foo*', () => new Promise(_.noop)) .as('foo.bar') .then(() => { $.get('/foo') $.get('/foo') }) .wait('@foo.bar', { timeout: 100 }) .wait('@foo.bar', { timeout: 100 }) }) it('can incrementally wait on requests', function () { cy.intercept('/foo*', (req) => { req.reply(_.noop) // only request will be received, no response }) .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar.request') .then(() => { $.get('/foo') }) .wait('@foo.bar.request') }) it('can timeout incrementally waiting on requests', function (done) { testFailWaiting((err) => { expect(err.message).to.contain('for the 2nd request to the route') done() }) cy.intercept('/foo*', (req) => { req.reply(_.noop) // only request will be received, no response }) .as('foo.bar') .then(() => { $.get('/foo') }) .wait('@foo.bar.request') .wait('@foo.bar.request', { timeout: 100 }) }) it('can alias a route without stubbing it', function () { cy.intercept(/fixtures\/app/).as('getFoo').then(function () { $.get('/fixtures/app.json') }).wait('@getFoo').then(function (res) { const log = cy.queue.logs({ displayName: 'xhr', })[0] expect(log.get('alias')).to.eq('getFoo') expect(res.response!.body).to.deep.eq({ some: 'json', foo: { bar: 'baz', }, }) }) }) // @see https://github.com/cypress-io/cypress/issues/8999 it('can spy on a 204 no body response', function () { cy.intercept('/status-code*').as('status') .then(() => { $.get('/status-code?code=204') }) .wait('@status').its('response.statusCode').should('eq', 204) }) // @see https://github.com/cypress-io/cypress/issues/8934 it('can spy on a 304 not modified image response', function () { const url = `/fixtures/media/cypress.png?i=${Date.now()}` cy.intercept(url).as('image') .then(() => { $.get({ url, cache: true }) }) .then(() => { if (Cypress.isBrowser('firefox')) { // strangely, Firefox requires some time to be waited before the first image response will be cached cy.wait(1000) } }) .then(() => { $.get({ url, cache: true }) }) .wait('@image').its('response.statusCode').should('eq', 200) .wait('@image').its('response.statusCode').should('eq', 304) }) // https://github.com/cypress-io/cypress/issues/14522 it('different aliases are used for the same url', () => { cy.intercept('/status-code*').as('status') .then(() => { $.get('/status-code?code=204') }) .wait('@status').its('response.statusCode').should('eq', 204) cy.intercept('/status-code*').as('status2') .then(() => { $.get('/status-code?code=301') }) .wait('@status').its('response.statusCode').should('eq', 301) .wait('@status2').its('response.statusCode').should('eq', 301) }) // https://github.com/cypress-io/cypress/issues/9549 it('should handle aborted requests', () => { cy.intercept('https://jsonplaceholder.cypress.io/todos/1').as('xhr') cy.visit('fixtures/xhr-abort.html') cy.get('#btn').click() cy.get('pre').contains('delectus') // response body renders to page cy.wait('@xhr') }) // @see https://github.com/cypress-io/cypress/issues/9062 it('can spy on a request using forceNetworkError', function () { cy.intercept('/foo*', { forceNetworkError: true }) .as('err') .then(() => { $.get('/foo') }) .wait('@err').should('have.property', 'error') .and('include', { message: 'forceNetworkError called', name: 'Error', }) .get('@err').should('not.have.property', 'response') }) // @see https://github.com/cypress-io/cypress/issues/15823 it('can override an alias using .as', function () { cy.intercept('/users*').as('getUsers') cy.intercept('/users*', { body: { data: 'fake data' }, statusCode: 200 }).as('getUsers') .then(() => { $.get('/users') }) .wait('@getUsers') }) // @see https://github.com/cypress-io/cypress/issues/16451 it('yields the expected interception when two requests are raced', function () { cy.intercept('/foo*', { times: 1 }, { delay: 100, body: 'bar' }).as('a') cy.intercept('/foo*', { times: 1 }, 'foo').as('a') cy.then(() => { $.get('/foo') $.get('/foo') }) .wait('@a').its('response.body').should('eq', 'foo') .wait('@a').its('response.body').should('eq', 'bar') }) // @see https://github.com/cypress-io/cypress/issues/9306 context('cy.get(alias)', function () { it('gets the latest Interception by alias', function () { cy.intercept('/foo*', { bar: 'baz' }).as('alias') .then(() => { $.get('/foo') $.get('/foo') }) .wait('@alias').wait('@alias').then((interception) => { cy.get('@alias').then((interception2) => { expect(interception).to.not.be.null expect(interception).to.eq(interception2) }) }) }) it('gets all aliased Interceptions by alias.all', function () { cy.intercept('/foo*', { bar: 'baz' }).as('alias') .then(() => { $.get('/foo') $.get('/foo') }) .wait('@alias').wait('@alias') cy.get('@alias.all').then((interceptions) => { expect(interceptions).to.have.length(2) }) }) it('gets indexed Interception by alias.number', function () { let interception cy.intercept('/foo*', { bar: 'baz' }).as('alias') .then(() => { $.get('/foo') $.get('/foo') }) .wait('@alias').then((_interception) => { interception = _interception }).wait('@alias') cy.get('@alias.0').then((interception2) => { expect(interception).to.not.be.null expect(interception).to.eq(interception2) }) }) it('gets per-request aliased Interceptions', function () { cy.intercept('/foo*', (req) => { req.alias = 'alias' req.reply({ bar: 'baz' }) }) .then(() => { $.get('/foo') }) .wait('@alias').then((interception) => { cy.get('@alias').then((interception2) => { expect(interception).to.not.be.null expect(interception).to.eq(interception2) }) }) }) it('yields null when no requests have been made', function () { cy.intercept('/foo').as('foo') cy.get('@foo').should('be.null') }) }) context('with an intercepted request', function () { it('can dynamically alias the request', function () { cy.intercept('/foo*', (req) => { req.alias = 'fromInterceptor' }) .then(() => { $.get('/foo') }) .wait('@fromInterceptor') }) it('can time out on a dynamic alias', function (done) { testFailWaiting((err) => { expect(err.message).to.contain('for the 1st request to the route') done() }) cy.intercept('/foo', (req) => { req.alias = 'fromInterceptor' }) .wait('@fromInterceptor', { timeout: 100 }) }) it('dynamic aliases are fulfilled before route aliases', function (done) { testFailWaiting((err) => { expect(err.message).to.contain('for the 1st request to the route: `fromAs`') done() }) cy.intercept('/foo*', (req) => { req.alias = 'fromInterceptor' }) .as('fromAs') .then(() => { $.get('/foo') }) .wait('@fromInterceptor') // this will fail - dynamic aliasing maintains the existing wait semantics, including that each request can only be waited once .wait('@fromAs', { timeout: 100 }) }) it('fulfills both dynamic aliases when two are defined', function () { cy.intercept('/foo*', (req) => { req.alias = 'fromInterceptor' }) .intercept('/foo*', (req) => { expect(req.alias).to.be.undefined req.alias = 'fromInterceptor2' }) .then(() => { $.get('/foo') }) .wait('@fromInterceptor') .wait('@fromInterceptor2') }) }) // @see https://github.com/cypress-io/cypress/issues/8695 context('yields request', function () { it('when not intercepted', function () { cy.intercept('/post-only').as('foo') .then(() => { $.post('/post-only', 'some body') }).wait('@foo').its('request.body').should('eq', 'some body') }) it('when intercepted', function () { cy.intercept('/post-only', (req) => { req.body = 'changed' }).as('foo') .then(() => { $.post('/post-only', 'some body') }).wait('@foo').its('request.body').should('eq', 'changed') }) it('when static response body is provided', function () { cy.intercept('/post-only', { static: 'response' }).as('foo') .then(() => { $.post('/post-only', 'some body') }).wait('@foo').its('request.body').should('eq', 'some body') }) }) // @see https://github.com/cypress-io/cypress/issues/8536 context('yields response', function () { const testResponse = (expectedBody, done) => { return () => { $.get('/xml') cy.wait('@foo').then((request) => { expect(request.response!.body).to.eq(expectedBody) done() }) } } it('when not stubbed', function (done) { cy.intercept('/xml*').as('foo') .then(testResponse('bar', done)) }) it('when stubbed with StaticResponse', function (done) { cy.intercept('/xml*', 'something different') .as('foo') .then(testResponse('something different', done)) }) it('when stubbed with req.reply', function (done) { cy.intercept('/xml*', (req) => req.reply('something different')) .as('foo') .then(testResponse('something different', done)) }) it('when stubbed with res.send', function (done) { cy.intercept('/xml*', (req) => req.reply((res) => res.send('something different'))) .as('foo') .then(testResponse('something different', done)) }) context('when stubbed with fixture', function () { it('with cy.intercept', function (done) { cy.intercept('/xml*', { fixture: 'null.json' }) .as('foo') .then(testResponse('', done)) }) it('with req.reply', function (done) { cy.intercept('/xml*', (req) => req.reply({ fixture: 'null.json' })) .as('foo') .then(testResponse('', done)) }) it('with res.send', function (done) { cy.intercept('/xml*', (req) => { return req.continue((res) => { return res.send({ fixture: 'null.json', headers: { // since `res.headers['content-type'] is already 'application/xml' from origin, // we must explicitly set the content-type to JSON here. // luckily changing content-type like this is not a typical use case 'content-type': 'application/json', }, }) }) }) .as('foo') .then(testResponse('', done)) }) }) }) // @see https://github.com/cypress-io/cypress/issues/9580 context('overwrite cy.intercept', () => { it('works with an alias by default', () => { // sanity test before testing it with the cy.intercept overwrite cy.intercept('/foo*', 'my value').as('netAlias') .then(() => { return $.get('/foo') }) cy.wait('@netAlias').its('response.body').should('equal', 'my value') }) it('works with an alias and function', () => { let myInterceptCalled cy.spy(cy, 'log') Cypress.Commands.overwrite('intercept', function (originalIntercept, ...args) { return cy.log('intercept!').then(() => { myInterceptCalled = true return originalIntercept(...args) }) }) cy.intercept('/foo*', 'my value').as('netAlias') .then(() => { return $.get('/foo') }) .then(() => { expect(myInterceptCalled, 'my intercept was called').to.be.true expect(cy.log).to.have.been.calledWith('intercept!') }) cy.wait('@netAlias').its('response.body').should('equal', 'my value') }) it('works with an alias and arrow function', () => { let myInterceptCalled cy.spy(cy, 'log') Cypress.Commands.overwrite('intercept', (originalIntercept, ...args) => { return cy.log('intercept!').then(() => { myInterceptCalled = true return originalIntercept(...args) }) }) cy.intercept('/foo*', 'my value').as('netAlias') .then(() => { return $.get('/foo') }) .then(() => { expect(myInterceptCalled, 'my intercept was called').to.be.true expect(cy.log).to.have.been.calledWith('intercept!') }) cy.wait('@netAlias').its('response.body').should('equal', 'my value') }) it('works with dynamic alias', () => { let myInterceptCalled cy.spy(cy, 'log') Cypress.Commands.overwrite('intercept', (originalIntercept, ...args) => { return cy.log('intercept!').then(() => { myInterceptCalled = true return originalIntercept(...args) }) }) cy.intercept('/foo*', (req) => { req.alias = 'netAlias' req.reply('my value') }) .then(() => { return $.get('/foo') }) .then(() => { expect(myInterceptCalled, 'my intercept was called').to.be.true expect(cy.log).to.have.been.calledWith('intercept!') }) cy.wait('@netAlias').its('response.body').should('equal', 'my value') }) // https://github.com/cypress-io/cypress/issues/14444 it('can use dot in request alias', () => { cy.intercept('/users', (req) => { req.alias = 'get.url' req.reply('foo') }) cy.window().then((win) => { const xhr = new win.XMLHttpRequest() xhr.open('GET', '/users') xhr.send() }) cy.wait('@get.url') }) }) }) context('unit tests', function () { context('#getDisplayUrlMatcher', function () { function testDisplayUrl (title: string, expectedDisplayUrl: string, matcher: Partial) { return it(title, function () { expect(getDisplayUrlMatcher(matcher)).to.eq(expectedDisplayUrl) }) } testDisplayUrl('with only url', 'http://google.net', { url: 'http://google.net' }) testDisplayUrl('with url + method', 'http://google.net', { method: 'GET', url: 'http://google.net' }) testDisplayUrl('with regex url', '/foo/', { url: /foo/ }) testDisplayUrl('with only path', '/foo', { path: '/foo' }) testDisplayUrl('with only pathname', '/foo', { pathname: '/foo' }) testDisplayUrl('with host + port', '{hostname: foo.net, port: 1234}', { hostname: 'foo.net', port: 1234 }) testDisplayUrl('with url and query', '{url: http://something, query: {a: b}}', { url: 'http://something', query: { a: 'b' } }) }) }) })