Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inject an assertion after the request but before the response #3514

Closed
arcdev1 opened this issue Feb 17, 2019 · 5 comments
Closed

Inject an assertion after the request but before the response #3514

arcdev1 opened this issue Feb 17, 2019 · 5 comments
Labels
pkg/driver This is due to an issue in the packages/driver directory stage: ready for work The issue is reproducible and in scope topic: network type: feature New feature that does not currently exist

Comments

@arcdev1
Copy link

arcdev1 commented Feb 17, 2019

Current behavior:

It is very common to have the UI display some kind of spinner or loading message while awaiting a response from an API call. Unfortunately, there is currently no way to "pause" a network request so that we can perform assertions before the response is received. The only option is to use a time delay which is flakey and an anti-pattern.

Desired behavior:

To test this in a non-flaky way, it would be nice to be able to inject an assertion after the event that triggers the request but before the response is received. Something like:

cy.route({
  onBeforeResponse: req => cy.get('[data-test-id=spinner]').should('be.visible') 
 ...
})

This would make the state much more deterministic as we would know that the request has been issued but that the response has not yet returned.

It's probably not possible to actually pause a request after it has been issued, but it should be possible to prevent the request from happening until some other event has taken place. For example, this could be implemented internally by wrapping the onBeforeResponse callback in a promise and delaying the request until onBeforeResponse is resolved. That way code like the following would be possible:

cy.route({
  onBeforeResponse: req => cy.get('[data-test-id=spinner]').should('be.visible') 
 ...
}).as('apiCall')
cy.get('#button-that-triggers-apiCall').click()
cy.wait('@apiCall')
cy.get('[data-test-id=spinner]').should('not.be.visible') 

Versions

Cypress: 3.1.5
macOS: Mojave 10.14.2
Chrome: Version 72.0.3626.109 (Official Build) (64-bit)

ref: #687

@jennifer-shehane
Copy link
Member

Hey @arcdev1, thanks for opening the feature request, this sounds like a good idea.

As you mentioned, the current workaround is to pass a delay to the cy.route() like so:

it "shows a loader", ->
  cy.route({
    url: /runs[^\/]/
    response: @runs
    delay: 5000
  }).as("getRuns")
  cy.visit("/#/projects/#{@project.id}")
  cy.get(".loader")

@jennifer-shehane jennifer-shehane added type: feature New feature that does not currently exist pkg/driver This is due to an issue in the packages/driver directory labels Feb 19, 2019
@cypress-bot cypress-bot bot added the stage: ready for work The issue is reproducible and in scope label Feb 19, 2019
@arcdev1
Copy link
Author

arcdev1 commented Feb 19, 2019

Thanks @jennifer-shehane.

For reference/inspiration...

Here's how I accomplish the kind of test I'm talking about using Puppeteer and Jest. Notice that the test is highly deterministic in that my assertion is guaranteed to run after the request was issued but before the response is received. This is especially true because toMatch actually polls the DOM and doesn't resolve until my element is found.

  it("tells me when we're searching", async done => {
    expect.assertions(3)
    await page.setRequestInterception(true)
    page.on('request', async req => {
      if (req.url().includes('someapi.com')) {
        const msg = await page.$('#msg')
        await expect(msg).toMatch('Searching...')

        await req.respond({
          headers: { 'Access-Control-Allow-Origin': '*' },
          body: JSON.stringify(dummyResults),
          contentType: 'application/json'
        })

        done()
      }
    })
    await expect(page).toFill('#movie-name', 'star')
    await expect(page).toClick('#search-button')
  }) 

Puppeteer's whole network event model might serve as a good inspiration for #687

@RyanClementsHax
Copy link

RyanClementsHax commented May 19, 2021

Hello!

Since the deprecation of route() in version 6.0.0, the workaround for this has changed. I figured I would share my solution. It has only been tested on version 7.3.0. I welcome any feedback.

it('displays a loading spinner when loading the resource', () => {
  let resolveReq
  cy.intercept('GET', '/resource', req => {
    return new Cypress.Promise(resolve => {
      resolveReq = resolve
    }).then(req.reply)
  }).as('getResource')
  cy.visit('/')
  cy.get('#spinner')
    .should('be.visible')
    .then(() => resolveReq())
  cy.wait('@getResource')
    .its('response.statusCode')
    .should('eq', 200)
})

P.S. Cypress is awsome! I'm having a great time.

@flotwig
Copy link
Contributor

flotwig commented Dec 15, 2022

This is supported with cy.intercept():

cy.intercept('/something', (req) => {
  expect(req).to.be.something
  req.continue()
}

Closing as supported.

@flotwig flotwig closed this as completed Dec 15, 2022
@joshribakoff-sm
Copy link

joshribakoff-sm commented Jan 27, 2023

@flotwig It doesn't work. Note the OP is asking for cy.get().should() and you posted an example that doesn't use that API.

cy.intercept('/something', (req) => {
     cy.get('[data-testid="skeleton"]').should('exist');
  req.continue()
}

All this does is immediately send the request, instead of pausing it like asked. Additionally, the cy.get() prints it matched 0 elements, and the .should() is effectively ignored.

Could we re-open this? One of the selling points of Cypress is "time travel debugging", but it is painstakingly difficult to do something simple as the OP explained :)

What we're trying to do is wait for a loading indicator prior to sending a mock data to the app. Otherwise, the app can load too fast and Cypress tries to find the loading indicator after the app already loaded.

If I add delay: 1000 to a mocked request, one of my suites constantly flakes... it seems like if I have tests in the suite that do not await the loading state, the tests run faster than the mock requests are sent, and tests randomly fail and just show a "loading state".

Seems like it's literally impossible to deterministically test a sufficiently complex application w/ Cypress these days. The only workaround I can think of is to bypass cy.intercept() completely, and instead use the window object to inject a mock service into my application from Cypress... which sort of defeats the point of using Cypress.

The workaround that did work for me and is more in the spirit of the OP's request than the arbitrary delays is https://blog.dai.codes/cypress-loading-state-tests/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg/driver This is due to an issue in the packages/driver directory stage: ready for work The issue is reproducible and in scope topic: network type: feature New feature that does not currently exist
Projects
None yet
Development

No branches or pull requests

5 participants