I recently started using Cypress.io at work and wanted to share some of the patterns I've found helpful. Our stack (in layman's terms) is Django, Ember, and React; Cypress provides a way to reach testing needs for both e2e and tests that do not require a backend. Below are some of my current notes on using Page Objects.


Page Object Location (React App)

Generic /tools/cypress/page-objects/*
App-specific /apps/<app_name>/cypress/page-objects/*

Base Class Inheritance

I recommend creating a base class in /tools/cypress/page.ts to extend from.

Your page object class should extend Page from tools/cypress/page and use it’s methods to interact with elements on your page.

Page Object Class and Methods

After extending the base class and creating attributes, I strongly recommend creating methods on the page object that can be called onto your test. For example:

export default class SchedulingAppPeopleViewPage extends Page {
 visit = visitable('#/scheduling');
 addShiftButton = byText('Add Shift');
 publishShiftButton = byText('Publish');
 getCancelButton = byText('Cancel');
 …
 …
 clearSchedule() {
   this.shiftMenu().click();
   this.clearScheduleOption().click();
   this.clearScheduleButton()
     .first()
     .click({ force: true }); // may not require force
 }

Notice the class has a few attributes followed by a clearSchedule() method. This structure allows us to call SchedulingAppPeopleViewPage().clearSchedule() in our test instead of writing out each step, making tests longer and more difficult to read.

Using Page Objects with Tests

Example of instantiating the SchedulingAppPeopleViewPage class and then calling clearSchedule:

it('Clear Schedule', () => {
   const page = new SchedulingAppPeopleViewPage();
   page.clearSchedule();
   page.shiftConfirmation().should('have.text', 'Schedule has been cleared.');
 });

Example Directory Structure

<App>
|-- cypress
|   |-- e2e
|   |-- no-backend
|   `-- support/(page-objects)

e2e tests that do not use mocks
no-backend tests that include mocks and/or a fake server response
support includes helper logic such as utils, custom configurations, and the page-objects subdirectory

Sample Test using Page Object & Login

I recommend setting up and tearing down tests using the before, after, beforeEach, and afterEach hooks as much as possible. Try keeping your test code to a minimum.

before() hook should include any logic required for all tests to run; it executes before all tests.

before(() => {
   // delete all shifts from db
   cy.request({
     url: '/custom_api/delete_shifts/1',
     failOnStatusCode: false,
     log: true,
   });
 });

beforeEach should include any logic required to run before each test. Notice here is where the main admin logs in and visits the scheduling app page. This reduces a “click on scheduling app from dashboard” step, reducing the amount of code written (no need to instantiate the DashboardPage object to click on dashboard either!).

beforeEach(() => {
   cy.login('mainAdmin');
   const page = new SchedulingAppPeopleViewPage();
   page.visit();
 });

afterEach() is good for cleaning up data between tests (aka it(“data should be clean for this test run because of the afterEach hook”). Notice a call to delete_shifts happens between each test run. edit: it's actually better to clean data before test runs, using the beforeEach() or before() hook, depending on need.

afterEach(() => {
   // clean data
   cy.request({ url: 'custom_api/delete_shifts/1', failOnStatusCode: false });
 });

Simplify Your Code

Tests should focus on asserting that actions are happening as we expect. The below tests are simplified by calling page-object methods instead of writing out each step of the testcase, allowing the assertions to be the focal point of each test run.

it('Add Shift Button | Save & Publish | Main Admin', () => {
   const page = new SchedulingAppPeopleViewPage();
   const isPublished = true;
   page.addShift(isPublished);
   page.shiftConfirmation().should('have.text', 'Shift has been saved!');
   cy.request('custom_api/get_scheduling_data/1/', { log: true }).then(response => {
     expect(response.body['unpublished shifts']).to.eq(0);
     expect(response.body['published shifts']).to.eq(1);
     expect(response.body['shifts count']).to.eq(
       response.body['published shifts'] + response.body['unpublished shifts'],
     ); // ensure there are no deleted or copied shifts
   });

Note: the above is an e2e example. Notice it’s 1) checking the shiftConfirmation (passive notification) and then 2) checking the backend for the new shift.

it('Add from shift cell', () => {
   const page = new SchedulingAppPeopleViewPage();
   page.addShiftFromCalendar();
   page.shiftConfirmation().should('have.text', 'Shift has been saved!');