Run synchronous request before `cy.visit` on every...
# i-need-help
n
I'm introducing feature flagging to our cypress tests. We use feature flags on both the frontend and the backend, so simply mocking flag values on the frontend would not be sufficient to test E2E behaviour. I've created a development-only API route, which allows flag values to be overridden for the session. The plan is to have Cypress make a request to that route before each test, so that the backend will return the appropriate flags for that test. The flags are provided on a per test basis by passing a
flags
property to the test options. I've attempted to make that request a couple ways, but each attempt has either failed, or been flaky due to race conditions. 1. Modify
cy.visit
using
Cypress.Commands.overwrite
, so that flag overrides are applied before each visit.
Copy code
js
Cypress.Commands.overwrite('visit', (original, url, options) => {
  cy.request({
    method: 'POST',
    url: '/dev/flags/override',
    body: Cypress.config('flags') || {},
  }).then(() => {
    original(url, options);
  });
});
This is the most straight forward solution, and seems to correctly apply flags for the first test of each spec. However when this override is applied, an error begins occurring in specs that call
visit
in
beforeEach
.
Copy code
CypressError: Timed out retrying after 4050ms: `cy.click()` failed because the page updated while this command was executing.
OR
Copy code
Timed out retrying after 4050ms: cy.first() failed because the page updated as a result of this command, but you tried to continue the command chain. The subject is no longer attached to the DOM, and Cypress cannot requery the page after commands such as cy.first().
The first test in a spec will pass normally, but each test after that will fail. I suspected it might be because the newly overwritten
visit
command resolves when the override request completes, rather than when the
original
call completes. So I tried to address that manually:
Copy code
js
Cypress.Commands.overwrite(
  'visit',
  (original, url, options) =>
    new Promise((resolve) => {
      cy.request({
        method: 'POST',
        url: '/dev/flags/override',
        body: Cypress.config('flags') || {},
      }).then(() => {
        original(url, options).then(resolve);
      });
    }),
);
This didn't work because Cypress commands doesn't use promises. 2. Run the flag override request in global
before
and
beforeEach
hooks
This worked sometimes, but was flaky. My understanding is that due to the way Cypress schedules tasks, it's not possible to guarantee that tasks scheduled in the global
before
and
beforeEach
hooks would resolve before tasks in the spec's
before
and
beforeEach
hooks would run. This means if there's any latency in the flag override request, the override would not be ready by the time
cy.visit
is called.
Copy code
js
const requestFlags = () =>
  cy.request({
    method: 'POST',
      url: '/dev/flags/override',
      body: Cypress.config('flags') || {},
    }),
  });
  before(() => {
    requestFlags();
  });
  beforeEach(() => {
    requestFlags();
  });
requestFlags
would need to be run in both
before
and
beforeEach
, because some of our specs do navigation in a
before
hook. In which case, the navigation would occur before the
requestFlags
call in the
beforeEach
hook. We're working to move away from calling
visit
in
before
hooks, because we want tests to be better decoupled from each other. But that's a work in progress. Unfortunately, I cannot provide a reproduction repo at this time, as this is a proprietary project. If needed, I can try throwing together a minimum implementation of a server/client using this flag override approach for the purpose of debugging this and coming to a better solution. Though I'm hoping someone with more Cypress experience might be able to point out what I'm doing wrong / if there's a better approach. Any assistance would be greatly appreciated, thank you 🙂
e
I also use feature flagging and I've wrapped my flagging request code in a custom Cypress command. I call it directly after authenticating with my app in a custom
login
command.
Copy code
/**  Login with the API */
Cypress.Commands.add('login', ({ user } = {}) => {
    cy.loginWithAPI({ user }).then(() => {
        cy.getAllFeatureFlags()
    })
})
I then just call this
cy.login
command in a
beforeEach
hook. By wrapping this request in a custom command you are able to enforce synchronously using
.then()
https://docs.cypress.io/guides/core-concepts/variables-and-aliases#Closures For you, in you
beforeEach
, you can always do something like below
Copy code
cy.requestFlags().then(() => {
     cy.visit('/')
})
Alternatively, just calling
cy.requestFlags()
in a
before
hook should also be enough for the command to finish before moving forward. Since my feature flagging request is bundled with my login request, which uses
cy.session
, I only call it once per test suite, even though it's used in the
beforeEach
. I'm able to persist the fetched feature flag values by using a
Cypress.env
variable. You can see I basically save off the fetched flags after making the initial response. I can then access that same
Cypress.env
variable and use it however I want in my tests.
Copy code
Cypress.Commands.add('getAllFeatureFlags', () => {
    Cypress.log({ name: 'getAllFeatureFlags' })

    return cy
        .request({
            method: 'GET',
            url: '/feature-flags',
            headers: {
                Accept: 'application/json',
            },
        })
        .then((rsp) => {
            Cypress.env('featureFlags', rsp)
        })
})
I'm not sure if I addressed everything you needed as there was a lot your question so please clarify if I missed anything of if the explanation isn't clear.
g
you do not need
cy.then
, like
Copy code
js
cy.requestFlags().then(() => {
     cy.visit('/')
})
is the same as
Copy code
js
cy.requestFlags()
cy.visit('/')
For feature flags, check out my blogs at https://cypress.tips/search