How cy.intercept Works

Gleb Bahmutov

Distinguished Engineer

Cypress.io 

@bahmutov

📺 Watch this presentation at https://www.youtube.com/watch?v=LEeoQp1j93I

our planet is in imminent danger

https://lizkeogh.com/2019/07/02/off-the-charts/

+3 degrees Celsius will be the end.

survival is possible* but we need to act now

  • change your life
  • join an organization

rebellion.global          350.org

AGENDA

  1. Spying and stubbing

  2. Network control with cy.route

    1. window.fetch problem

  3. Network control with cy.intercept

  4. Problems vs problems

  5. Future work

Spy on a method

const person = {
  greet () {
    console.log('Hello')
  }
}
// somewhere inside the app
person.greet()

can we confirm the app calls "greet"?

Spy on a method

const person = {
  greet () {
    console.log('Hello')
  }
}
// somewhere inside the app
person.greet()
cy.spy(person, 'greet').as('greet')
cy.get('@greet')
  .should('have.been.calledOnce')
function spy(o, methodName) {
  const method = o[methodName].bind(o)
  o[methodName] = function () {
    this.called += 1
    return method.apply(null, arguments)
  }
}

Spy implementation

function stub(o, methodName, value) {
  o[methodName] = function () {
    this.called += 1
    return value
  }
}

Stub implementation

1. Need object reference

const person = {
  greet () {
    console.log('Hello')
  }
}
// somewhere inside the app
person.greet()
cy.spy(person, 'greet').as('greet')
cy.get('@greet')
  .should('have.been.calledOnce')

1. Need object reference

const person = {
  greet () {
    console.log('Hello')
  }
}
if (window.Cypress) {
  window.person = person
}

Maybe expose the reference when running inside Cypress

1. Need object reference

cy.spy(person, 'greet').as('greet')

"person" is an implementation detail 😐

cy.spy(console, 'log').as('log')

you can spy or stub Browser APIs (window, console, navigator, etc) 🙂

cy.get('@log')
  .should('have.been.calledOnceWith', 'Hello')

confirm the application's behavior at the periphery: DOM, console, cookies, network 🥳

Stubbing Browser API examples

Stubbing window.open

<a href="/about.html" target="_blank">About</a>
<script>
  document.querySelector('a')
    .addEventListener('click', (e) => {
      e.preventDefault()
      window.open('/about.html')
    })
</script>

How to prevent the window.open from opening in a new tab?

it('opens the about page', () => {
  cy.visit('index.html')
  cy.window().then(win => {
    cy.stub(win, 'open').as('open')
  })
  cy.get('a').click()
  cy.get('@open')
    .should('have.been.calledOnceWithExactly', '/about.html')
})
it('opens the about page', () => {
  cy.visit('index.html')
  cy.window().then(win => {
    cy.stub(win, 'open').as('open')
  })
  cy.get('a').click()
  cy.get('@open')
    .should('have.been.calledOnceWithExactly', '/about.html')
})
it('opens the about page', () => {
  cy.visit('index.html')
  cy.window().then(win => {
    cy.stub(win, 'open').callsFake((url, target) => {
      expect(target).to.be.undefined
      return win.open.wrappedMethod.call(win, url, '_self')
    }).as('open')
  })
  cy.get('a').click()
  cy.get('@open')
    .should('have.been.calledOnceWithExactly', '/about.html')
})
it('opens the about page', () => {
  cy.visit('index.html')
  cy.window().then(win => {
    cy.stub(win, 'open').callsFake((url, target) => {
      expect(target).to.be.undefined
      return win.open.wrappedMethod.call(win, url, '_self')
    }).as('open')
  })
  cy.get('a').click()
  cy.get('@open')
    .should('have.been.calledOnceWithExactly', '/about.html')
})

2. Timing matters

const person = {
  greet () {
    console.log('Hello')
  }
}
// somewhere inside the app
person.greet()
cy.spy(person, 'greet').as('greet')
cy.get('@greet')
  .should('have.been.calledOnce')

1

2

3

Auto-retry does this

it('opens the about page', () => {
  cy.visit('index.html')
  cy.window().then(win => {
    cy.stub(win, 'open').as('open')
  })
  cy.get('a').click()
  cy.get('@open')
    .should('have.been.calledOnceWithExactly', '/about.html')
})

1. set up the stub

2. then click

2. Timing matters

3. confirm browser behavior

<body>
  <script>
    window.open('/about.html')
  </script>
</body>

What if the app calls window.open immediately?

2. Timing matters

it('opens the about page', () => {
  cy.visit('index.html')
  cy.window().then(win => {
    cy.stub(win, 'open').as('open')
  })
})

too late!

<body>
  <script>
    window.open('/about.html')
  </script>
</body>

What if the app calls window.open immediately?

2. Timing matters

it('opens the about page', () => {
  cy.window().then(win => {
    cy.stub(win, 'open').as('open')
  })
  cy.visit('index.html')
})

⛔️ No, there is no window object yet

<body>
  <script>
    window.open('/about.html')
  </script>
</body>

What if the app calls window.open immediately?

2. Timing matters

it('opens the about page', () => {
  cy.visit('index.html', {
    onBeforeLoad (win) {
      cy.stub(win, 'open').as('open')  
    }
  }
})

window is there, but before any application code loads

<body>
  <script>
    window.open('/about.html')
  </script>
</body>

What if the app calls window.open immediately?

2. Timing matters

beforeEach(() => {
  cy.on('window:before:load', (win) => {
    cy.stub(win, 'open').as('open')
  })
})
it('opens the about page', () => {
  cy.visit('index.html')
})

Stub window on every page visited

💡 cy.on vs Cypress.on

beforeEach(() => {
  cy.on('window:before:load', (win) => {
    cy.stub(win, 'open').as('open')
  })
})
it('opens the about page', () => {
  cy.visit('index.html')
})

using cy.on inside the callback

requires test or hook function

💡 cy.on vs Cypress.on

beforeEach(() => {
  cy.on('window:before:load', (win) => {
    cy.stub(win, 'open').as('open')
  })
})
it('opens the about page', () => {
  cy.visit('index.html')
})
Cypress.on('window:before:load', (win) => {
  delete win.fetch
})
it('falls back to XMLHttpRequest', () => {
  cy.visit('index.html')
})

using cy.on inside the callback

requires test or hook function

cannot use any cy commands

can be outside any test or hook

So What About Network Spying and Stubbing?

cy.server is...

cy.window().then(win => {
  win.XMLHttpRequest = (options) => {
    const ajax = {
      open: cy.spy(),
      send: cy.spy(),
      ...
    }
    ...
    return ajax
  }
})

cy.route is just book-keeping

cy.server and cy.route stub the call to XMLHttpRequest before it leaves the browser

Browser window

App iframe

If application was calling window.fetch

Cypress.on('window:before:load', (win) => {
  delete win.fetch
  // hope the app polyfills fetch
  // via XMLHttpRequest
})
{
  "experimentalFetchPolyfill": true
}

we will inject polyfill if necessary

cy.route limits

cy.window().then(win => {
  cy.stub(win, 'fetch').resolves(...)
})

essentially ...

  • Does not behave like a real browser with caching, CORS, etc
  • What about requests made from WebWorkers or ServiceWorkers?
  • What about other requests like static resources?

Better Network Control

The new architecture

Intercept HTTP calls here

Any request can be observed or stubbed

(+ ServiceWorker, WebWorker)

cy.server()

cy.route()

Deprecated

Will be removed when we feel cy.intercept() can do it all

cy.intercept( routeMatcher )

Spy on requests matching the route

cy.intercept( routeMatcher, response )

Stub requests matching the route

cy.intercept(...).as('alias')

Give request an alias for waiting

General form

cy.intercept(url, routeHandler?)
cy.intercept(method, url, routeHandler?)
cy.intercept(routeMatcher, routeHandler?)

if present, than it is a stub*

*mostly

cy.intercept(url, routeHandler?)
cy.intercept(method, url, routeHandler?)
cy.intercept(routeMatcher, routeHandler?)
cy.intercept('http://example.com/widgets') // spy
cy.intercept('http://example.com/widgets', 
             { fixture: 'widgets.json' })  // stubs
cy.intercept('POST', 'http://example.com/widgets', 
             { statusCode: 200, body: 'it worked!' })
cy.intercept({ method: 'POST', 
               url: 'http://example.com/widgets' }, 
             { statusCode: 200, body: 'it worked!' })
cy.intercept({
  pathname: '/search',
  query: {
    q: 'some terms'
  }
})
cy.intercept({
  // this RegExp matches any URL beginning with 
  // 'http://api.example.com/widgets'
  url: /^http:\/\/api\.example\.com\/widgets/,
  headers: {
    'x-requested-with': 'exampleClient'
  }
})
// does not reply
cy.intercept('*-fruits').as('fruits')
// replies with a fruit
cy.intercept('favorite-*', ['Lemons 🍋']).as('favorite')
// does not reply
cy.intercept('favorite-fruits').as('favorite-fruits')
cy.visit('/')
cy.wait('@fruits') // first route matches
cy.wait('@favorite') // second route matches
// but the third route never gets the request
// since the second route has replied
cy.contains('li', 'Lemons 🍋').should('be.visible')

app makes

GET /favorite-fruits

Docs and Examples

Command syntax, examples

https://on.cypress.io/intercept

cy.route vs cy.intercept (cy.route2)

https://glebbahmutov.com/blog/cy-route-vs-route2/

"Stubbing using cy.intercept" recipe

https://github.com/cypress-io/cypress-example-recipes

🏅 90 cy.intercept example

tests

Docs and Examples

cy.route and cy.intercept equivalent tests

cy.intercept super power

Cypress

route handler can be programmatic

cy.intercept('POST', '/graphql', (req) => {
  if (req.body.hasOwnProperty('mutation')) {
    req.alias = 'gqlMutation'
  }
})

// assert that a matching request has been made
cy.wait('@gqlMutation')

Spy on some requests

*routeHandler but is a spy

cy.intercept('POST', '/graphql', (req) => {
  if (req.body.hasOwnProperty('mutation')) {
    req.reply({
      data: {
        id: 101
      }
    })
  }
})

if you call req.reply from the route handler, it becomes a stub*

Stub some requests

cy.intercept('POST', '/graphql', (req) => {
  if (req.body.hasOwnProperty('mutation')) {
    req.reply((res) => {
      // 'res' represents the real destination's response
      // three items in the response is enough
      res.data.items.length = 3
    })
  }
})

Change server's response

How it all works

cy.intercept(matcher, callback)

matcher

request

(via Cypress socket message)

Your callback code executes in the browser right where it was created

1

2

3

4

response

(via Cypress socket message)

stub

modified request

How it all works: response

cy.intercept(matcher, callback)

matcher

response

(via Cypress socket message)

You want to access the response with:

req.reply(res => {

  ...

})

1

2

3

modified response

(via Cypress socket message)

modified response

Warning: intercept happens AFTER the request has left the building browser

// server
app.get('/req-headers', (req, res) => {
  res.json(req.headers)
})

Request has left the browser

it('adds request header', () => {
  cy.visit('/headers')
  cy.intercept('/req-headers', (req) => {
    req.headers['x-custom-headers'] = 'added by cy.intercept'
  })
  cy.get('#get-headers').click()
  cy.contains('#output', 'accept-language')
    .should('contain', 'x-custom-header')
    .and('contain', 'added by cy.intercept')
})
// server
app.get('/req-headers', (req, res) => {
  res.json(req.headers)
})

Request has left the browser

browser does not show the custom header we have added inside cy.intercept

added header was received by the server

Added header is present in the data if you wait on the intercept

cy.intercept('/req-headers', (req) => {
  req.headers['x-custom-headers'] = 'added by cy.intercept'
}).as('headers')

cy.get('#get-headers').click()
cy.wait('@headers').its('request.headers')
  .should('have.property', 'x-custom-headers', 'added by cy.intercept')

Some cool examples

  1. change the HTML

  2. change the CSS

  3. network call does not happen

  4. cy.intercept + cy.spy combo

  5. cy.intercept + cy.clock

  6. loading element

  7. stubbing redirects

it('modifies the page itself', () => {
  // we are only interested in the HTML root resource
  cy.intercept({ pathname: '/' }, (req) => {
    req.reply((res) => {
      res.body += `<footer style="${style}">⚠️ This is a Cypress test ⚠️</footer>`
    })
  })

  cy.visit('/')
  cy.contains('footer', 'Cypress test')
    .should('be.visible')
})

change the HTML of the page

change the HTML of the page

change CSS resource

it('highlights LI elements using injected CSS', () => {
  cy.intercept('styles.css', (req) => {
    delete req.headers['if-modified-since']
    delete req.headers['if-none-match']

    req.reply((res) => {
      res.send(`${res.body}
        li {
          border: 1px solid pink;
        }
      `)
    })
  })

  cy.visit('/')
  // confirm the CSS was injected and applied
  cy.get('li').should('have.length.gt', 1)
    .first().invoke('css', 'border')
    .should('be.a', 'string')
    .and('include', 'solid')
})

change CSS resource

it('highlights LI elements using injected CSS', () => {
  cy.intercept('styles.css', (req) => {
    delete req.headers['if-modified-since']
    delete req.headers['if-none-match']

    req.reply((res) => {
      res.send(`${res.body}
        li {
          border: 1px solid pink;
        }
      `)
    })
  })

  cy.visit('/')
  // confirm the CSS was injected and applied
  cy.get('li').should('have.length.gt', 1)
    .first().invoke('css', 'border')
    .should('be.a', 'string')
    .and('include', 'solid')
})

caching & server response

Have to think if the browser caches the resource, or if the server responds with "not modified" empty response

using response to check UI

it('requests favorite fruits', function () {
  cy.intercept('/favorite-fruits').as('fetchFruits')
  cy.visit('/fruits.html')
  cy.wait('@fetchFruits').its('response.body')
    .then((fruits) => {
      cy.get('.favorite-fruits li')
        .should('have.length', fruits.length)

      fruits.forEach((fruit) => {
        cy.contains('.favorite-fruits li', fruit)
      })
    })
})

using response to check UI

Data inside the intercept

// wait on the request once
cy.wait('@fetchFruits')
// but get the latest request as many times as needed
cy.get('@fetchFruits').its('response.statusCode')
  .should('eq', 200)
cy.get('@fetchFruits').its('response.body')
  .should('have.length.gt', 3)

Multiple assertions against intercept

cy.wait + cy.get(s)

cy.wait('@fetchFruits').then(intercept => {
  expect(intercept.response.statusCode, 'status code').to.equal(200)
  expect(intercept.response.body, 'at least 3 fruits').to.have.length.gt(3)
})

Multiple assertions against intercept

cy.wait + cy.then

network call does not happen

Every 30 seconds GET /favorite-fruits

network call does not happen

Confirm then GET /favorite-fruits happens once at the start

network call does not happen

it('does not fetch for at least five seconds', () => {
  let polled

  cy.intercept('/favorite-fruits', () => {
    polled = true
  })
  cy.visit('/')
  cy.wrap()
    .should(() => {
      expect(polled, 'fetched fruits').to.be.true
      polled = false
    })
  // physically wait 5 seconds
  cy.wait(5000)
    .then(() => {
      expect(polled, 'no new requests').to.be.false
    })
})

network call does not happen

network call does not happen

it('does not fetch for at least five seconds (cy.spy)', () => {
  cy.intercept('/favorite-fruits', 
               cy.spy().as('reqForFruits') )

  cy.visit('/')
  // at some point the request happens
  cy.get('@reqForFruits').should('have.been.calledOnce')

  // physically wait 5 seconds
  cy.wait(5000)
  // new network calls have not happened
  cy.get('@reqForFruits').should('have.been.calledOnce')
})

Using cy.spy() to count calls

if you do not call req.reply() it is a network spy

network call does not happen

network call does not happen

Confirm then GET /favorite-fruits happens every 30 seconds

without waiting

cy.visit('/')
cy.get('@reqForFruits').should('have.been.calledOnce')
cy.wait(30000)
cy.get('@reqForFruits').should('have.been.calledTwice')

⛔️

cy.intercept + cy.clock

it('fetches every 30 seconds', () => {
  cy.clock()
  cy.intercept('/favorite-fruits', cy.spy().as('reqForFruits'))

  cy.visit('/fruits.html')
  // at some point the request happens
  cy.get('@reqForFruits').should('have.been.calledOnce')

  cy.tick(5000)
  // no new network calls
  cy.get('@reqForFruits').should('have.been.calledOnce')

  // but add 25 more seconds, and the app should have made a network call
  cy.tick(25000)
  cy.get('@reqForFruits').should('have.been.calledTwice')
})

cy.intercept + cy.clock

loading element

loading element: find it

it('shows loading element', () => {
  cy.intercept('/favorite-fruits', {
    body: ['Apple', 'Banana', 'Cantaloupe'],
    delay: 25000
  })
  cy.visit('/fruits.html')
})

loading element: find it

loading element: test it

it('shows loading element', () => {
  cy.intercept('/favorite-fruits', {
    body: ['Apple', 'Banana', 'Cantaloupe'],
    delay: 1000
  })
  cy.visit('/fruits.html')
  cy.get('.loader').should('be.visible')
  cy.get('.loader').should('not.exist')
})

loading element: test it

Is 1 second enough? Or too much?

loading element: promise

it('slows the reply by returning a Promise', () => {
  const fruits = ['Apple', 'Banana', 'Cantaloupe']
  cy.intercept('/favorite-fruits', (req) =>
    Cypress.Promise.delay(1000, fruits).then(req.reply)
  )

  cy.visit('/fruits.html')
  cy.get('.loader').should('be.visible')
  cy.get('.loader').should('not.exist')
})

loading element: promise

it('shows loading element for as little as possible', () => {
  let sendResponse
  const p = new Cypress.Promise((resolve) => {
    // save the resolve method
    // so this promise resolves when we call it
    sendResponse = resolve
  })
  cy.intercept('/favorite-fruits', (req) => {
    // wait for the trigger promise to resolve
    return p.then(() => req.reply(['Apple', 'Banana', 'Cantaloupe']))
  })
  cy.visit('/fruits.html')
  cy.get('.loader').should('be.visible').then(sendResponse)
  cy.get('.loader').should('not.exist')
})

loading element: test faster

loading element: test faster

it('stubs the redirect', () => {
  cy.intercept('/getout', (req) => {
    req.reply((res) => {
      expect(res.statusCode).to.equal(302)
      // the server wants to redirect us to another domain
      expect(res.headers).to.have.property('location', 'https://www.cypress.io')
      res.headers.location = '/'
      // need to provide something for the updated "res"
      // object to be used
      // https://github.com/cypress-io/cypress/issues/9555
      res.send('stay here')
    })
  })

  cy.get('#getout').click()
  cy.location('pathname').should('equal', '/') // redirect worked
})

stub redirects

stub redirects

cy.intercept "problems"

Change the response

it('returns different fruits every 30 seconds', () => {
  cy.clock()
  let k = 0

  // return difference responses on each call
  cy.intercept('/favorite-fruits', (req) => {
    k += 1
    switch (k) {
      case 1:
        return req.reply(['apples 🍎'])
      case 2:
        return req.reply(['grapes 🍇'])
      default:
        return req.reply(['kiwi 🥝'])
    }
  })

  cy.visit('/fruits.html')
  cy.contains('apples 🍎')
  cy.tick(30000)
  cy.contains('grapes 🍇')
  cy.tick(30000)
  cy.contains('kiwi 🥝')
})

changing the response

changing the response

// return difference responses on each call
const responses = [
  ['apples 🍎'], ['grapes 🍇']
]
cy.intercept('/favorite-fruits', (req) => {
  req.reply(responses.shift() || ['kiwi 🥝'])
})

equivalent code

You can program your own logic to dynamically vary intercepts

We are still trying to come up with a general way to overwrite interceptors

Issues with label "pkg/net-stubbing"

Plus closing bugs

More info:

Thank you 👏