cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
47.07k stars 3.19k forks source link

[Cypress 6.0.0] Overriding interceptors doesn't work. #9302

Closed ar7casper closed 3 years ago

ar7casper commented 3 years ago

Current behavior

With Version <6.0.0, if you used c.route with a stub, you could then use it again with a new stub and it would take the second fixture for the second request.

With version >6.0.0 It doesn't happen, the override doesn't work with cy.intercept and it always returns the initial fixture.

cy.intercept(
  {
     method: 'GET',
     url: 'items',
  },
   { fixture: 'items/empty-list.json' }
);

// Some more testing of the app, and then fetching again -

cy.intercept(
  {
     method: 'GET',
     url: 'items',
  },
   { fixture: 'items/list-1.json' }
);

The use case is very simple. Imagine my app loads with 0 items, using a GET. Then, I do some stuff, do a POST, and then GET those items again. I want to stub it with [item], instead of the initial [].

So what happens is that it always return [], not [item] as it used to. I'm quite sure it's not a problem with my code, used to work perfectly fine till today.

The first image is the initial intercept, you can see it gets 2 image The second image is what is shown for the second GET, the one that should've matched and return [item] image

Desired behavior

I want to be able to override interceptors

filipemir commented 3 years ago

I was really excited to use the new intercept ability to be able to dynamically respond to requests with different data based on query params or other URL-specific data. But this omission is even worse than the original problem: it altogether prevents us from testing failure cases. For example: testing that the UI responds as expected to when a given request responds with a 200 vs when it responds with a 404. Is this not an expected use case? Is there an intended workaround I'm missing?

rinero-bigpanda commented 3 years ago

I believe this is the crux of the issue https://github.com/cypress-io/cypress/blob/197f3097f7beaa09cd4dd1e2a0e9a39610f5ed69/packages/net-stubbing/lib/server/route-matching.ts#L110-L116

Assuming I understand everything correctly - it looks like the order by which the routes are being matched is the original order they're pushed in.

Changing the code to this might solve it

 export function getRouteForRequest (routes: BackendRoute[], req: CypressIncomingRequest, prevRoute?: BackendRoute) { 
   const possibleRoutes = prevRoute ? routes.slice(_.findIndex(routes, prevRoute) + 1) : routes 

-   return _.find(possibleRoutes, (route) => { 
+   return _.find(possibleRoutes.slice().reverse(), (route) => { 
     return _doesRouteMatch(route.routeMatcher, req) 
   }) 
 } 

From what I could tell, previously it let the driver do this matching instead https://github.com/cypress-io/cypress/blob/197f3097f7beaa09cd4dd1e2a0e9a39610f5ed69/packages/driver/src/cypress/server.js#L268-L277

Unfortunately, I don't have much time setting up a local environment to test & verify that it's indeed the issue - if anyone wants to do it they're more than welcome to use my code diff :)

Romanchuk commented 3 years ago

This issue blocks me to update to 6.0.0 version

ThatGuyHS commented 3 years ago

Just wanted to add that I am also blocked from updating due to this.

flotwig commented 3 years ago

This issue blocks me to update to 6.0.0 version

@Romanchuk @ThatGuyHS this shouldn't block you from upgrading to 6.0.0, using cy.intercept is entirely optional

Romanchuk commented 3 years ago

@flotwig Yes, but for me cy.intercept is an only reason to upgrade

raphaelmatori commented 3 years ago

+1

Once cy.intercept() is declared for some route, you can not override its behaviour. It is quite annoying and until fixed will block me to update since we usually mock a '200' response, but we also need to test other ones.

Looking forward a pull request to fix it.

jennifer-shehane commented 3 years ago

We are currently looking into ways to make this possible with cy.intercept() while still maintaining the value of the other features introduced in cy.intercept(). There's some investigation we need to do to ensure all use cases can be covered in a way that makes sense.

samijaber commented 3 years ago

For the future, it would be useful to mention this critical breaking change in your CHANGELOG or migration guides. All of these resources imply that cy.intercept is pretty much a drop-in replacement for cy.route:

There is nothing indicating the issues mentioned in this thread. I recommend you adjust both articles above and mention this difference for others attempting to upgrade.

js1m commented 3 years ago

It is indeed a huge blocker. And what @samijaber said:

Inability to overwrite or clear an interceptor should be explicitly stated in the docs.

In my case, I am testing my application to re-authenticate after a failing request and then to retry that failed request with the new credentials. No way to do that if I cannot override or clear the previous interceptor.

manojhans commented 3 years ago

It also breaks the below cases which were working with cy.route():

Before:

cy.route('POST', **/ticket/search*, { fixture: 'search.json' }).as('search'); cy.wait('@search').wait('@search').get('@search.all').should('have.length', 2);

After:

cy.intercept('POST', **/ticket/search*, { fixture: 'search.json' }).as('search'); cy.wait('@search').wait('@search').get('@search.all').should('have.length', 2);

Results:

Screenshot 2020-12-16 at 16 15 09

Before:

cy.route('POST', **/ticket/search*, { fixture: 'search.json' }).as('search'); cy.get('@search').its('response.statusCode').should('eq', 200);

After:

cy.intercept('POST', **/ticket/search*, { fixture: 'search.json' }).as('search'); cy.get('@search').its('response.statusCode').should('eq', 200);

Results:

Screenshot 2020-12-16 at 16 10 45

though it's working fine with below code:

cy.intercept('POST', **/ticket/search*, { fixture: 'search.json' }).as('search'); cy.wait('@search').its('response.statusCode').should('eq', 200);

but not with this:

cy.intercept('POST', **/ticket/search*, { fixture: 'search.json' }).as('search'); cy.wait('@search'); cy.get('@search').its('response.statusCode').should('eq', 200);

Also, is there any way to get the last call of the same endpoint? something like this:

cy.wait('@search').last().its('request.body').should('deep.equal', { key: 'Something', });

nicklemmon commented 3 years ago

Huge thanks for the Cypress team for all of their hard work on this and other new features. Complex feature changes and new APIs always have some rough edges. Always impressed with the quality of your work. 👏

jennifer-shehane commented 3 years ago

@manojhans The issue with cy.get() returning null was fixed in 6.2.0: https://github.com/cypress-io/cypress/issues/9306

manojhans commented 3 years ago

Thanks @jennifer-shehane. It's working fine in 6.2.0

jcdesousa commented 3 years ago

This is a blocker for us, we had to stop the migration due to this issue.

flotwig commented 3 years ago

@jcdesousa shouldn't block you from anything. cy.route is still available in 6.0.0

jcdesousa commented 3 years ago

@flotwig You are right, but the cy.server() and cy.route() was marked as deprecated in Cypress 6.0.0. We then migrated to the intercept() method but now we are having some tests failing because is not overriding.

jennifer-shehane commented 3 years ago

@jcdesousa We're working on a solution as previously mentioned. https://github.com/cypress-io/cypress/issues/9302#issuecomment-743121233 It requires some investigation, so in the meantime you will have to revert back to cy.route(). Sorry for the inconvenience.

bahmutov commented 3 years ago

@ar7casper would the solution in https://github.com/cypress-io/cypress/pull/14513 solve your use-case?

It would use the first fixture just once on the very first request. And it would use the second fixture once, which would happen on the second request from the app

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/empty-list.json' }
);

// Some more testing of the app, and then fetching again -

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/list-1.json' }
);
nicklemmon commented 3 years ago

Nice! I think that would work for our use case as well:

  1. We stub an error response
  2. We verify an error message with retry button appears
  3. We stub a successful response
  4. We ask Cypress to click the retry button
  5. We verify the output
ThatGuyHS commented 3 years ago

That solution would solve my use case as well! @bahmutov

phishy commented 3 years ago

I was hoping to use overriding as a test organization tactic. For example: having several interceptors in a beforeEach, and then having one interceptor override in the more specific test itself to change a single piece of data to test a specific thing. Thus reducing repetition by forcing every test to declare the same intercepts.

bahmutov commented 3 years ago

@phishy we were thinking about this and here is how it breaks down in your particular case

Let's say you set up intercepts in beforeEach hook. Are they used in the hook itself? A common situation that the author of this issue is experiencing (and I had the same in todomvc tests) is you set up the intercepts and visit the site inside the hook. Let's say start with empty data

beforeEach(() => {
  cy.intercept('/todos', { fixture: 'empty.json' })
  cy.visit('/')
})

it('starts with empty list', () => {
  cy.get('.todo').should('have.length', 0)
})

Great. Now let's see you want to simulate an API error. What happens when the server says 404 to GET /todos

it('shows an error if todos do not load', () => {
  // using magical command
  cy.overrideIntercept('/todos', { forceNetworkError: true })
  cy.get('.loading-error').should('be.visible')
})

Well, the above test will NOT work. By the time you override the intercept, the beforeEach hook has already finished running completely, and the original intercept has returned its empty list. Well, maybe you could trick it, and use cy.reload inside the test

it('shows an error if todos do not load', () => {
  // using magical command
  cy.overrideIntercept('/todos', { forceNetworkError: true })
  cy.reload()
  cy.get('.loading-error').should('be.visible')
})

Ok, this should work. Or we could use the {times: 1} approach. On the initial visit we expect the /todos request, so let's set it up

beforeEach(() => {
  cy.intercept('/todos', { times: 1, fixture: 'empty.json' })
  cy.visit('/')
})

it('starts with empty list', () => {
  cy.get('.todo').should('have.length', 0)
})

it('shows an error if todos do not load', () => {
  cy.intercept('/todos', { forceNetworkError: true })
  cy.reload()
  cy.get('.loading-error').should('be.visible')
})

Just one more observation: you can always avoid overriding intercepts in my opinion by setting up a new test "stack" of hooks. In our example, we can avoid overriding and times: 1 syntax by:

describe('tests start with empty list', () => {
  beforeEach(() => {
    cy.intercept('/todos', { fixture: 'empty.json' })
    cy.visit('/')
  })

  it('starts with empty list', () => {
    cy.get('.todo').should('have.length', 0)
  })
})

it('shows an error if todos do not load', () => {
  cy.intercept('/todos', { forceNetworkError: true })
  cy.visit()
  cy.get('.loading-error').should('be.visible')
})

So I am pretty optimistic about times: N being explicit and declarative way to solve this problem for most (but not all, we understand that there are more case you might want to test) use cases.

artem-3dhubs commented 3 years ago

Same for me. I am using automatically recorded responses to process all the network requests an app does during the test. This logic runs before all the test cases in the test suite and sets up the mocks.

I would like to override some of the specific routes for the particular test case and assert the recorded response. That means I want to discard the previous intercept and apply the new one while keeping the same recorded mock. The times option does not seem to help.

I guess I could implement some workaround that will always use the global intercept, but it would be way more straightforward if native API just supported overriding same way as route did.

Also the way I managed to process multiple mocked responses for the same route with the route API is by using undocumented now('route', ...) command that I invoked in route's onRequest callback to shift to the next mocked response by effectively overriding the previous route.

I was really excited to see the new intercept API that supported possibility to pass mocks at the actual request time. But overriding is still crucial to me.

Let me know if I am missing something or you need some more details about my use case.

bahmutov commented 3 years ago

@artem-3dhubs can you give us an example of the test and the override logic you want to do?

PS: you can always create more complex programmatic intercepts with overrides yourself since cy.intercept allows callbacks, like https://glebbahmutov.com/blog/cypress-intercept-problems/#no-overwriting-interceptors-again I feel like we are trying to create a nice syntax around it

artem-3dhubs commented 3 years ago

@bahmutov here is my use case:

scene<{ meta: { userId: string } }>(
  'My Test Scene',
  {
    record: false,
    setup: () => {
      // async script that runs in `record: true` mode and creates the scene
      // (eg. does actual API requests like user registration etc)
      // returns meta that is saved as part of the fixture.
      return { userId: 1 };
    },
  },
  getMeta => {
    // During the test run all the http requests are collected automatically
    // and appended to this fixture (in `record: true` mode).

    // In `record: false` mode the requests are mocked with data from the fixture

    beforeEach(() => {
      const meta = getMeta();

      // All the http calls are mocked by the scene in `record: false` mode
      cy.visit(`/user/${meta.userId}`);
    });

    it('can change name', () => {
      const meta = getMeta();
      const newName = 'My lovely name';

      // this is currently my own version of intercept command that is a wrapper above route
      // under the hood
      //  - in `record: false` mode I read the test fixture file and map the corresponding route
      //  - in `record: true` mode I collect the actual server response and append to fixture file
      cy.intercept('PATCH', `/api/user/${meta.userId}`).as('updateUser');

      cy.get('user-name-input')
        .clear()
        .type(newName)
        .blur();

      cy.wait('@updateUser').then(xhr => assert.equal(xhr.response.body.name, newName));

      cy.get('user-name').should('have.text', newName);
    });
  }
);

PS: after reading the article I see that I could definitely do some work around. Callback is exactly what I missed in route and had to make a workaround by using cy.now('route') in onRequest.

SnorreDanielsen commented 3 years ago

@bahmutov in your example you are using the intercept in the beforeEach hook. In my use case I do it in the test itself, so like this:

beforeEach(() => {
  cy.intercept('/todos', { fixture: 'empty.json' })
})

it('should get the default value from empty.json', () => {
  cy.visit('/')
  cy.get('.todo').should('have.length', 0)
})

it('should get the custom value from oneValue.json', () => {
  cy.intercept('/todos', { fixture: 'oneValue.json' })
  cy.visit('/')
  cy.get('.todo').should('have.length', 1)
})

Do you have a recommended approach on how I could get this working with the fix you are planning to implement?

bahmutov commented 3 years ago

Sure @SnorreDanielsen Think how often does the empty list will be used in each test - once, so I would test it like this:

beforeEach(() => {
  cy.intercept('/todos', { times: 1, fixture: 'empty.json' })
})

it('should get the default value from empty.json', () => {
  cy.visit('/')
  cy.get('.todo').should('have.length', 0)
})

it('should get the custom value from oneValue.json', () => {
  cy.intercept('/todos', { fixture: 'oneValue.json' })
  cy.visit('/') // uses the empty fixture
  cy.reload() // will use the oneValue.json
  cy.get('.todo').should('have.length', 1)
})

But read my entire comment in https://github.com/cypress-io/cypress/issues/9302#issuecomment-769185675 - I think you should regroup your tests to avoid "unwinding" the intercepts because the tests are not "artificially" overlapping.

CinkadeusBG commented 3 years ago

So what are we doing with this? Its been a while and I cannot be expected to create a separate context every time I want a different API response mocked on the same url. That's not a valid solution given our test layouts.

OldSchooled commented 3 years ago

@ar7casper would the solution in #14513 solve your use-case?

It would use the first fixture just once on the very first request. And it would use the second fixture once, which would happen on the second request from the app

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/empty-list.json' }
);

// Some more testing of the app, and then fetching again -

cy.intercept(
  {
     method: 'GET',
     url: 'items',
     times: 1
  },
   { fixture: 'items/list-1.json' }
);

This would solve several use-cases for us and would be a really important update, not only for our not-so-simple upgrade to cypress 6+ but also for our current suite of tests in general. Having this implementation quickly would be rather significant for us.

bhaeussermann commented 3 years ago

The solution involving the "times" property does not seem practical for complex tests. The "counting" aspect seems very brittle, since if one request happened to be called once more than expected by the application, it would immediately be out of sync with the test. This seems like it would be a nightmare to maintain.

It also would not work for my project unless I'd make some drastic changes to my testing code. In a testing file we typically define a function that sets a number of default routes, and then override certain routes in different tests / contexts for testing special cases. Consider the following fictitious code as an illustration:

describe('Manage employees', () => {
    context('General', () => {
        beforeEach(() => {
            stubLookupRoutesDefaults();
            cy.visit('myPageUrl');
        });

        // Some general tests
        ...
    });

    // Test Seychelles-specific logic
    context('Client in Seychelles', () => {
        beforeEach(() => {
            stubLookupRoutesDefaults();
            // Override with currencies relevant for Seychelles.
            cy.intercept('GET', 'currencies', ...);
            // Override with with packages having Seychelles-like properties.
            cy.intercept('GET', 'packages', ...);
            cy.visit('myPageUrl');
        });

        // Some Seychelles-specific tests
        ...
    });

    // Test logic specific to Mauritius clients
    context('Client in Mauritius', () => {
        beforeEach(() => {
            stubLookupRoutesDefaults();
            // Override with banks having Mauritius-like properties.
            cy.intercept('GET', 'banks', ...);
            cy.visit('myPageUrl');
        });

        // Some Mauritius-specific tests
        ...
    });
});

function stubLookupRoutesDefaults() {
    // Stub about 10 lookup API routes.
    cy.intercept(...);
    cy.intercept(...);
    ...
}

Being able to override certain routes is the simplest way of achieving the testing logic above. Anything else suggested in this thread would significantly impact the complexity of the testing code.

I don't understand why allowing to override routes (eg. by matching routes from last backwards instead of forwards) is not a viable solution. Will someone please explain this to me?

Jerome-Gdrs commented 3 years ago

Same for me!! Override routes would be a much better solution, or we would have to rethink and rewrite a lot of our tests!

jennifer-shehane commented 3 years ago

@bhaeussermann Thanks for sharing your feedback! This is very helpful.

We have not ruled out implementing overriding interceptors. It's just very complex for us to design a clear API for people to override interceptors, have all of the new middleware features of intercept, AND possibly deliver the 'times' feature (because we believe this may be valuable outside of just this overwriting scenario).

The intercept should work how people intuitively expect it to work in all of these situations. We don't want to release something that is confusing. Also we want to make sure that we don't release an API that we later have to instantly retract due to confusion or introduce unnecessary breaking features, etc.

The team is working on it. Even though there's not work directly on this exact PR.

diggabyte commented 3 years ago

@jennifer-shehane as for the api design, perhaps being explicit rather than implicit, with like a cy.clearIntercept method could be considered?

Jerome-Gdrs commented 3 years ago

I think this would be a solution for us ;)

pszalanski commented 3 years ago

@diggabyte could you please elaborate on how this cy.clearIntercept could work?

OldSchooled commented 3 years ago

@diggabyte, an implementation like cy.clearIntercept("@alias") is something my team has been desperately wanting since the beginning of our cypress adventure.

@pszalanski, in my case, it would be a definitive and sensical way to clear/reset the usage of an intercept.

I.e. "This intercept is no longer registered/valid, is no longer watching/intercepting/stubbing this path and is no longer saving and queuing the requests/responses that can be fetched by cy.wait("@alias")"

It would explicitly do what times would do implicitly. Instead of saying, "After n uses, this intercept is no longer registered," it would say, "Unregister this intercept now."

Like the times implementation, this would solve several use-cases for my team. In contrast to times, however, cy.clearIntercept would be much easier for us to implement. cy.clearIntercept would also be, for our cases, preferred over simply allowing intercept overrides (like before). Although it would require extra work for us to implement (as opposed to simply allowing overrides like with route), it would be useful enough to us in other areas that it would be worth the effort to us.

To stress the point and to summarize, cy.clearIntercept would be gold for us. times would work too, but something like cy.clearIntercept would be the clear winner by far for my team.

pvogel2 commented 3 years ago

Hey everyone, i ran into the same issues and needed to intercept a route in a test multiple times with different responses, this is the current workaround:

const responses = [settingsInitial, settingsDone];
const url = '<some api endpoint>';
cy.intercept('GET', url, (req) => req.reply(responses.shift()))
  .as('loadUserSettings');
sybrendotinga commented 3 years ago

Hey everyone, i ran into the same issues and needed to intercept a route in a test multiple times with different responses, this is the current workaround:

const responses = [settingsInitial, settingsDone];
const url = '<some api endpoint>';
cy.intercept('GET', url, (req) => req.reply(responses.shift()))
  .as('loadUserSettings');

Awesome suggestion! Never thought of that! I did the following to use fixtures and also override different status codes:

        const responses = []
        cy.fixture('/api/security/signup/confirm/422.json').then((fixture) => {
          // first response
          responses.push({ status: STATUS_422, body: fixture })
        })

        cy.fixture('/api/security/signup/confirm/200.json').then((fixture) => {
          // second response
          responses.push({ status: STATUS_200, body: fixture })
        })

        cy.intercept('PUT', '**/security/signup/confirm', (req) => {
          const { status, body } = responses.shift()
          req.reply(status, body)
        })
MichaelBiermann commented 3 years ago

„Overwrite“ API is tricky, I don’t have the solution, but I thought about your proposals and how it works for a product having some thousands of tests. Our test core team is maintaining beforeEach... functions to create a lot of default routes (25+). There is a fixed set of variants (see XHelpers...) for different purposes. The individual test developer usually does not extend the default-functions of the basis team, which is maintaining the helpers (see api. and XHelpers.).

  await api.setupBeforeEach(
    /* useMock*/true,
    /* defaultFixture*/true,
    /* routes*/XHelpers.DL_COMMON_MOCK_RESPONSES,
    /* overrideRouteCallback */ undefined,
    /* enabledFeatureFlags */ enabledFeatureFlags,
  );

Whenever there is no default variant which fits by 100%, the individual test (test developer) needs to fine-tune/overwrite the responses. With cy.route we can manage but awaiting the alias is very tricky, especially for new members of the test team.

async function getDefaultUserMockResponse(api, objectNamedisplay: string) {
  // manipulate user settings
  const defaultUserMockResponse = await cy.fixture("userResponse");
  const parameter = defaultUserMockResponse.user.parameters.find(param => param.name === "OBJECT_NAME_DISPLAY");
  if (parameter) {
    parameter.value = dwcObjectNamedisplay;
  } else {
    defaultUserMockResponse.user.parameters.push({ name: "OBJECT_NAME_DISPLAY", value: objectNamedisplay });
  }
  return defaultUserMockResponse;
}
  // overwrite default user repsonse
  const defaultUserMockResponseDepObjList = await getDefaultUserMockResponse(api, objectNamedisplay);
  const routes = XHelpers.DEFAULT_MOCK_REPONSES;
  routes.push({
    protocol: "GET",
    path: "**/fpa/services/rest/epm/session?action=logon**",
    response: defaultUserMockResponseDepObjList,
    as: "userResponse",
  });

  await api.setupBeforeEach(
      /* useMock */ true,
      /* defaultFixture */ true,
      /* routes */ routes,
      /* overrideRouteCallback */ undefined,
      /* enabledFeatureFlags */[
      "MODELING_SHOW_DEP_VIEWS",         // dependent object list
      "MODELING_LANDING_PAGE_STATUS",    // replacement: deployment status -> object status + deployed on
      "MODELING_VIEW_SOURCE_REPLACE",    // required to replace a sourve (drag&drop new source + mapping)
    ]);
}

The "times" (times) suggestion is not optimal for us, because we also need to overwrite the first response, while times = 1 would work for at least 95% of the responses.

The clear-alias (clearIntercept) suggestion would also remove the default and some very specific urls (especially Regex urls/paths can make clear/replace complex).

default (beforeEach):

    {
      protocol: "GET",
      path: "**/repository/dependencies/**",
      response: [],
      as: "dependencies",
    },

individual test logic:

      await api.mockServerCall("GET", "**/repository/dependencies/?ids=38F400506DB1A7F916000702CF63F41E**", "fixture:databuilder/dependencyList/dependenciesOrders", "dependencies");
      await api.mockServerCall("GET", "**/repository/dependencies/?ids=DC0602C077FEE7E0170005029AA15500**", "fixture:databuilder/dependencyList/dependenciesOrders_with_Address_dim", "dependencies");

but what to do if you need to overwrite the default for the first and all following calls?

      await api.mockServerCall("GET", "**/repository/dependencies/**", "fixture:databuilder/dependencyList/dependenciesDefaultPurchasing", "dependencies");

The "override" (overrideIntercept) suggestion (exact same url/path string, alias, ...) would help me a lot, to overwrite a few parameters of requests like .../logon where user parameters like preferred time zone are returned. The default responses are maintained by basis colleagues in function beforeEachGeneral, default value is "UTC". Sure, Basis team could add one degree of freedom after the other to the beforeEach... API helper functions (see api.setupBeforeEach), but that would make the APIs user experience a pain to use.

japhex commented 3 years ago

This would seriously help us loads with our suite. Our example is a graphql single endpoint, and I want to change certain responses dependent on when we fire mutations. e.g. when we load a table, load with intiial mock data, after firing a successful create record mutation then responding with a new set of mock data to include that record. Structure wise within our tests and app I'm really happy with it, it just all relies on this being possible!

witeck commented 3 years ago

If you want to make the answer dependent on the parameter value, this example might be helpful too:

this.replays = {};

cy.fixture('q1-response.json').then((fixture) => {
    this.replays['qValue1'] = {body: fixture};
});
cy.fixture('q2-response.json').then((fixture) => {
    this.replays['qValue2'] = {body: fixture};
});

cy.intercept({method: 'POST', url: 'my/url'}, (req) => {
    const urlParams = new URLSearchParams(req.body);
    const qValue = urlParams.get('q');
    req.reply(this.replays[decodeURI(qValue)]);
}).as("answer");

The way to extract q parameter may differ depending on how it was passed.

rinero-bigpanda commented 3 years ago

I'm in the processes of doing a user-land workaround but it's still not fully functional. That's what I was able to do so far:

// This is called from the `support/index.js` file
beforeEach(() => {
  const staticMimeTypes = ['text/css', 'text/html', 'text/javascript', 'font/otf', 'font/ttf', 'font/woff', 'font/woff2', 'image/avif', 'image/webp', 'image/apng', 'image/svg+xml', 'image/*'];
  const staticFileExtensions = ['\\.html$', '\\.css$', '\\.js$', '\\.map$', '\\.md$', '\\.woff$', '\\.woff2$', '\\.ttf$', '\\.eot$', '\\.jpg$', '\\.jpeg$', '\\.png$', '\\.svg$'];
  const methods = ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'];
  const ignoredDomains = ['content-autofill.googleapis.com', 'safebrowsing.googleapis.com', 'accounts.google.com', 'clientservices.googleapis.com'];

  cy.intercept({
    url: /.*/,
  }, (req) => {

    if (staticMimeTypes.some(mime => (req.headers.accept || '').includes(mime))
        || staticFileExtensions.some(extension => req.url.match(extension))
        || ignoredDomains.some(url => req.url.includes(url))
        || !req.method
        || methods.every(method => req.method !== method)) {
      req.reply();
    } else {
      const mocks = cy.state('__INTERNAL_MOCK_DEFINITIONS__');
      const mock = mocks.find(mock => mock.method === req.method && Cypress.minimatch(req.url, mock.url));
      if (mock) {
        req.reply(mock.status, mock.response);
      } else {
        throw new Error(`Route not stubbed.\nPlease add a cy.route() for url: "${req.url}" and method "${req.method}".`)
      }
    }
  });
});

Then, I also have a mock command that looks like this:

Cypress.Commands.add('mock', { prevSubject: true }, ({ method, url, response, status, headers }) => {
  cy.state('__INTERNAL_MOCK_DEFINITIONS__', [{
    method: method || 'GET',
    url,
    response,
    status,
    headers
  }].concat(cy.state('__INTERNAL_MOCK_DEFINITIONS__') || []));
});

this allows me to do something like this:

  cy.wrap({
    method: 'POST',
    url: '**/login',
    status: 200,
    response: {
      data: {}
    },
  }).mock();

The reason this "works" is because I have just one route handler that matches everything, then, in that handler I check the routes defined through mock but because of this command works it checks the routes in reverse order, takes the first one and returns the reply defined in it. Basically what's done here is that I'm implementing the route matching logic by myself.

There are problems though, as you can see, there are a bunch of urls I need to ignore because they're called by the browser itself that for some reason are also being intercepted here 😳

In addition, sometimes requests aren't being intercepted here even though they should, and it passes through to the underlying server. (in my case the server doesn't actually know about these routes and it responds with a 404).

Also, it looks like it needs to "warm up" because when I use cypress open, initial run fails spectacularly but then I hit the re-run button it works. If I use cypress run it always fails 🙄

If anyone makes progress with this kind of workaround I'd be happy to know about :) Either this or implementing an override mechanism within cy.intercept but I'd need something to upgrade to Cypress@6 because that's the only reason for me to upgrade.

spamsch commented 3 years ago

How about this quick monkey patch to solve the issue until an official solution is provided? Works for me with 6.6.0. Just put the following into your commands.js. Not sure about side effects but studying the code did not reveal any downsides of implementing a clearIntercept that way.

Assume you have defined your intercept as follows

cy.intercept('GET', '.netlify/functions/subjects/[0-9]*').as('GET-one-subject')

and calling it like that

cy.wait('@GET-one-subject')

then you can clear it with

cy.clearIntercept('GET-one-subject')

Here is the command

Cypress.Commands.add('clearIntercept', (aliasName) => {
  const routes = Cypress.state('routes')
  Object.entries(routes).forEach(([key, value]) => {
    const { alias } = value
    if (Object.is(alias, aliasName)) {
      delete routes[key]
    }
  })
  Cypress.state('routes', routes)
})

Update: This does not quite work as expected in all cases. Needs more investigation as more elements of the state seem to be touched.

HendrikThePendric commented 3 years ago

We are currently looking into ways to make this possible with cy.intercept() while still maintaining the value of the other features introduced in cy.intercept(). There's some investigation we need to do to ensure all use cases can be covered in a way that makes sense.

(11th December 2020)

At this point in time, are you able to say wether interceptors will eventually get overriding behaviour @jennifer-shehane?

I am asking because this issue is preventing us from rolling out some internal tooling using cy.intercept. If this is eventually going to be implemented, we can wait, but if not we should start building a workaround in our tooling.

chris5marsh commented 3 years ago

We've been running into this problem as well - trying to return different stubs for the same request, so we can test different responses.

Our solution is to use Cypress.config() to set a variable in each different test, and then check for that config variable in cy.intercept.

In my-test.spec.ts:

it('should return response One', () => {
  Cypress.config('request_type', 'type_one');
  cy.visit('/some-page'); // This page makes a GET call to /some-api-route
  cy.wait('@getResponseOne');
});

it('should return response Two', () => {
  Cypress.config('request_type', 'type_two');
  cy.visit('/some-page'); // This page makes a GET call to /some-api-route
  cy.wait('@getResponseTwo');
});

And in our intercepts.js file:

cy.intercept(
  {
    url: '/some-api-route',
    method: 'GET',
  },
  (req) => {
    const requestType = Cypress.config('request_type');
    if (requestType === 'type_one') {
      req.alias = 'getResponseOne';
      req.reply({ fixture: 'response-one.json' });
    } else if (requestType === 'type_two') {
      req.alias = 'getResponseTwo';
      req.reply({ fixture: 'response-two.json' });
    }
  },
);

Values set by Cypress.config() in a spec file are only in scope for the current spec file, so you don't need to worry about polluting the global config scope.

Hopefully there will be a way to clear/override intercepts introduced soon, but until then this method seems to work well for us!

FrancoisCamus commented 3 years ago

Is the improvement already being worked on in PR https://github.com/cypress-io/cypress/pull/14543 ?

flotwig commented 3 years ago

@FrancoisCamus indeed :) Feel free to take a look at the issue comment in #14543 and let me know if it will cover your needs.

ejpg commented 3 years ago

What about this workaround? is working for me with cypress 6.2.0

let fixture

beforeEach(() => {
    fixture = 'one.json'

    cy.intercept('GET', '/some-api-route', (req) => {
    req.reply({ fixture })
    })

})

When('I need the second fixture', () => {
    fixture = 'two.json'

    ...
})
Tintef commented 3 years ago

In my case using a global variable to change the fixture wasn't a solution, as I need to return a different fixture in the same test (I tried with @chris5marsh and @ejpg solutions with no luck).

What works for me is using a routeHandler and deciding which fixture to return depending on the request body/query params:

    cy.intercept({ url: <your-url-here>, method: <your-method-here> }, (req) => {
      if (req.body.direction === 'desc') {
        req.reply({ fixture: 'sorted.data.json' });
      } else {
        req.reply({ fixture: 'data.json' });
      }
    }).as('students');

    // Note: in this case, the method is `POST`.