End-to-end testing on VA.gov is accomplished using a front-end testing framework called Cypress. Cypress tests run in the browser and programmatically simulate a real user using a web application, or product. These tests are used to verify that the product works as expected across different browsers and viewport dimensions.

The following documentation details Cypress best practices.

Running tests

Cypress supports Chrome, Edge, Firefox, and a few others. You can run tests in headless mode or via the test runner. Continuous Integration and builds run Cypress tests in Chrome.

Headless mode

To run headless tests, run yarn cy:run.

By default, yarn cy:run runs Cypress tests headlessly in an Electron browser. You may specify another browser, and if you would like to run headless tests in another browser, you will have to explicitly include the --headless flag. We are currently running our tests in Chrome using the setup below:

yarn cy:run --headless --browser chrome
CODE

You may experience some performance issues where particular long-running tests (such as an exhaustive test of a form) may take an extremely long time (unless your setup is optimized for them, as in the case of our CI, which tests the production build and also has the specs to cancel out the performance burden).

For this reason, for local development, it might be better to run specific specs, even more so in the test runner. The Cypress team is investigating various issues regarding performance that may likely be related to this.

Test runner

To run tests in the Cypress test runner, run yarn cy:open.

There is a dropdown menu in the top right of the test runner that allows you to select a browser in which to run your tests. In our experience, Firefox has yielded the fastest test runs when testing locally, although it is currently a beta feature. The tests in CI will run in the default browser, which is Electron.

The test runner provides the ability to pause tests, and time travel, which allows you to see snapshots of your tests.

With the test runner, you can use Cypress's "Selector Playground". This allows you to click on elements in the DOM and copy that element's selector to use in your test. However, as mentioned elsewhere, selecting elements by CSS attributes is discouraged in favor of selecting by a test ID, which is in turn considered a fallback if selecting by other attributes (label text, role, etc.) is not feasible. The Selector Playground follows this best practice and automatically attempts to determine the selector by looking at data-cydata-test, and data-testid before falling back to a CSS selector.

You may find it useful to append certain options to the commands above.

Things to note

Automatic waiting

Cypress automatically waits for commands to execute before moving on to the next one. This eliminates the need to use the timeout constants in platform/testing/e2e/timeouts.js.

Cypress queues its commands instead of running them synchronously, so doing something like this will not work.

Third-party plugins

Cypress has many third-party plugins available. If you find yourself needing to do something that isn't natively supported, there may be a plugin for it.

Cypress Form Tester

Source file: vets-website/src/platform/testing/e2e/cypress/support/form-tester at main · department-of-veterans-affairs/vets-website

The form tester is a utility that automates Cypress end-to-end (E2E) tests on forms contained within applications on VA.gov. The form tester automatically fills out forms using data from JSON files that represent the body of the API request that's sent upon submitting the form.

Use the form tester to test forms on VA.gov applications.

Please see the Form tester utility for more information.

Cypress custom commands

Custom Cypress commands abstract away common behaviors that are required across VA.gov applications. The following custom commands are available:

Cypress testing library selectors

In addition to Cypress’ comprehensive API for interacting with elements, the VSP platform utilizes the Cypress Testing Library which allows us to test UI components in a user-centric way. This library gives us access to all of DOM Testing Library's findBy*findAllBy*queryBy, and queryAllBy commands off the global cy object.

Please note: The Cypress Testing Library queries should be preferred over Cypress’ cy.get() or cy.contains() for selecting elements.

The following is a list of queries provided by the Cypress Testing Library, in the order in which we recommend them* (e.g., prefer findByLabelText over findByRole over findByTestId).

find

findAll

findByLabelText

findByPlaceholderText

findByText

findByAltText

findByTitle

findByDisplayValue

findByRole

findByTestId

findAllByLabelText

findAllByPlaceholderText

findAllByText

findAllByAltText

findAllByTitle

findAllByDisplayValue

findAllByRole

findAllByTestId

* Note: the get_queries are not supported because for reasonable Cypress tests you need retry-ability an find\* queries already support that.

The TestId queries look at the data-testid attributes of DOM elements (see the next section).

data-testid Attribute

While the official Cypress documentation recommends the data-cy attribute, we recommend that you use the data-testid attribute because it is test-framework agnostic.

Add the data-testid attribute to your markup when your test would otherwise need to reference elements by CSS attributes as the last resort. As much as possible, prefer writing selectors for data-testid attributes over CSS selectors (ids and classes).

The goal is to write tests that resemble how your users use your app, hence the order of precedence for selecting elements.

Page Objects

JavaScript objects can be used to create page objects in Cypress tests. In test scenarios where multiple pages are interacted with and where the same test actions are performed multiple times, we recommend using page objects to make tests more readable, reduce duplication of code, and reduce total lines of code. Examples of page object usage can be found in the COVID-19 Vaccine Stay Informed tool tests, the Claims Status tests, and the Address Validation tests.

Here’s an example of a page object that contains two reusable blocks of code:

import featureTogglesEnabled from '../fixtures/toggle-covid-feature.json';

class UnsubscribePage {
  loadPage(status, pageId) {
    cy.server();
    let optOutRoute;
    if (status === 200) optOutRoute = '**';
    cy.route('PUT', `**/covid_vaccine/v0/registration/opt_out${optOutRoute}`, {
      status,
    });
    cy.route('GET', '/v0/feature_toggles*', featureTogglesEnabled).as(
      'feature',
    );
    cy.visit(
      `health-care/covid-19-vaccine/stay-informed/unsubscribe?sid=${pageId}`,
    );
    cy.get('#covid-vaccination-heading-unsubscribe').contains('Unsubscribe');
  }

  confirmUnsubscription(status) {
    if (status === 'failed') {
      cy.get('.va-introtext').contains(
        "We're sorry. We couldn't unsubscribe you from COVID-19 vaccine updates at this time. Please try again later.",
      );
    } else {
      cy.get('.va-introtext').contains(
        "You've unsubscribed from COVID-19 vaccine updates. We won't send you any more emails.",
      );
    }
  }
}

export default UnsubscribePage;
CODE

These can then be used to make a short, concise, and readable test as follows:

import UnsubscribePage from './page-objects/UnsubscribePage';

describe('COVID-19 Vaccination Preparation Form', () => {
  it('should successfully unsubscribe the user', () => {
    const unsubscribePage = new UnsubscribePage();
    unsubscribePage.loadPage(200, 12345);
    unsubscribePage.confirmUnsubscription();
  });
});
CODE