Managing random dynamic elements in Cypress

Sometimes we may encounter elements that may (or may not) appear in the flow of our application, but are not the object of our test. An example of this may be the popup that prompts us to accept or deny cookies.

A potential solution to handle this problem could be to forge a cookie in the beforeEach() hook that makes our server detect that the cookies have already been accepted, but this could lead to other problems (such as Cloudfare, if it is the case, identifying the access as a potential forgery attack and returning a 403 error).

A more universal solution is to use a JavaScript snippet that generates an interval that checks for the presence of an element every “X” time and, if it appears, executes an action and discards the interval.

Since we are not “demanding” the presence of an element through a cy.get(), the interaction will only occur in parallel to the test in case the element appears.

It is important to be clear that this technique is invasive: we are altering the original behavior of the page, so it is necessary to study the use case in which to use it and make sure that it does not affect the scenario. Use with caution.

Pooling in the background

The trick is simple: we declare an interval that, in each execution, checks if the button exists and, if it does, presses it and eliminates the interval so that it is not executed again. In case the element does not appear, it will be executed again after poolingTimeMs milliseconds.

Clicking the button will cause the cookies to be accepted, causing the popup to disappear and not be displayed again until the cookies are deleted:

function tryToClickButtonUntilFound(
  buttonLocator: string,
  poolingIntervalMs: number
) {
  return new Cypress.Promise((resolve) => {
    const interval = setInterval(() => {
      const button = Cypress.$(buttonLocator);
      if (button.length) {
        button.trigger("click");
        clearInterval(interval);
        resolve("Button clicked");
      }
    }, poolingIntervalMs);
  });
}

Hook configuration

For our snippet to be executed in each test, it is enough to invoke it from the hook before(), inside our file e2e.js / e2e.ts:

before(): {
  tryToClickButtonUntilFound("accept-cookies-button", 3000);
}

Example

Let’s create a real example to illustrate this technique. We will create a scenario in which the user navigates to a page that may (or may not) display a popup to accept cookies.

We are going to use https://the-internet.herokuapp.com again, since it has an example of a popup managed by cookies. We will make our test randomly access this page or, failing that, another page that does not show the popup, closing the popup in case it appears but not failing in case the popup never appears.

Feature: Element that can be present or not is dismissed
    Scenario: Popup button is clicked if it appears, but it's not clicked otherwise
        Given the user navigates to a page that may show a popup or not
        When the user waits for 3 seconds
        Then the modal popup is not present in the page

We implement the step that navigates randomly between two pages:

import { Given, When, Then } from "@badeball/cypress-cucumber-preprocessor";

const ENTRY_AD_URL = "entry_ad";
const FLOATING_MENU_URL = "floating_menu";

const locators = {
  modal: "#modal:visible",
};

Given("the user navigates to a page that may show a popup or not", () => {
  cy.visit(Math.random() > 0.5 ? ENTRY_AD_URL : FLOATING_MENU_URL);
});

We add a step that forces an explicit wait so that, in case there is a popup, it can be displayed on the screen. This explicit wait is added in the example to simulate the time the test would take to continue running. Remember that in a real environment, explicit waits should be avoided at all costs:

When("the user waits for {int} seconds", (seconds: number) => {
  cy.log(`Waiting ${seconds} seconds...`);
  cy.wait(Math.ceil(seconds * 1000));
  cy.log(`Finished waiting`);
});

Finally, we check that the popup is not visible:

Then("the modal popup is not present in the page", () => {
  cy.get(locators.modal).should("not.exist");
});

For pooling to be executed before each test, we add the function to the hook before() in the e2e.ts file:

const COOKIE_POPUP_BUTTON_LOCATOR = "#modal:visible .modal-footer p";

function tryToClickButtonUntilFound(
  buttonLocator: string,
  poolingIntervalMs: number
) {
  return new Cypress.Promise((resolve) => {
    const intervalId = setInterval(() => {
      const button = Cypress.$(buttonLocator);
      if (button.length) {
        button.trigger("click");
        clearInterval(intervalId);
        resolve("Button clicked");
      }
    }, poolingIntervalMs);
  });
}

before(() => {
  tryToClickButtonUntilFound(COOKIE_POPUP_BUTTON_LOCATOR, 2000);
});

If the popup is not displayed, the test just goes on:

Otherwise, it will close the popup and will continue the test:

You can find the full example of the use of this technique here.