In this blog post, we will go over how we can automatically test a Gatsby site end-to-end (e2e), using Cypress on Gitlab CI.

Introduction

Gatsby

Gatsby is a static site generator (SSG) built upon React. It allows us to create “blazing” fast websites. In this example, we will use a simple blog starter template available and add a Cypress test.

Cypress

Fast, easy and reliable testing for anything that runs in a browser. - Cypress README

Cypress allows us to test a web application, how a real user would use the application. Cypress will be used to test our Gatsby application, though if it’s a site you can test it using Cypress.

Gitlab CI

Gitlab CI is a continuous integration pipeline that will allow us to run our tests automatically, such as when we merge code into the master branch.

Getting Started

Gatsby

Create a new Gatsby site, using this default Gatsby starter:

gatsby new gatsby-starter-blog https://github.com/gatsbyjs/gatsby-starter-blog
cd gatsby-starter-blog

(Optional) Typescript

Adding Typescript to a Gatsby web application.

yarn add typescript @types/react @types/react-dom @types/node -D
yarn add gatsby-plugin-typescript

Add the following to your gatsby-config.js.

module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-typescript`,
      options: {
        isTSX: true, // defaults to false
        jsxPragma: `jsx`, // defaults to "React"
        allExtensions: true, // defaults to false
      },
    },
  ],
}

Then create a new file tsconfig.json (in the project root, where the gatsby-config.js is).

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "esnext",
    "jsx": "preserve",
    "lib": ["dom", "esnext"],
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "noUnusedLocals": false,
    "allowJs": true
  },
  "exclude": ["node_modules", "public", ".cache"]
}

Cypress

Now to finally add Cypress to our application so we can test it. First, install the dependencies.

yarn add -D cypress cypress-axe axe-core start-server-and-test
# Add types
yarn add -D @types/cypress-axe

Next, let’s create a cypress.json folder in the project root.

{
  "baseUrl": "http://localhost:8000/",
   "integrationFolder": "cypress/e2e"
}

Next, let’s add some new “scripts” to the package.json file.

  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "build": "gatsby build",
    "develop": "gatsby develop",
    "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
    "start": "npm run develop",
    "serve": "gatsby serve",
    "clean": "gatsby clean",
    "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1",
    "test:e2e": "start-server-and-test 'yarn develop' http://localhost:8000 'yarn cy:open'",
    "test:e2e:ci": "start-server-and-test 'yarn develop' http://localhost:8000 'yarn cy:run'"
  }

These scripts allow us to start Cypress, cy:open opens a GUI to visualise our tests whereas cy:run does it all in the terminal (the browser runs in headless mode). Where we will run test:e2e:ci in our CI pipeline, here we use the start-server-and-test command to start our Gatsby server using yarn develop. Then we run cy:run to start our tests.

Structure

Create a new folder called cypress which will look something like this.

.
├── e2e
│   └── accessibility.test.ts
├── fixtures
│   └── graphql.json
├── plugins
│   └── index.js
├── support
│   ├── commands.js
│   ├── index.d.ts
│   └── index.js
└── tsconfig.json
mkdir -p cypress/support

Create a file at cypress/support/commands.js

Cypress.Commands.add(`assertRoute`, (route) => {
  cy.url().should(`equal`, `${window.location.origin}${route}`);
});

Add some custom types for Cypress index.d.ts if you are using Typescript.

/// <reference types="cypress" />

declare namespace Cypress {
  interface Chainable<Subject> {
    /**
     * Assert the current URL
     * @param route
     * @example cy.assertRoute('/page-2')
     */
    assertRoute(route: string): Chainable<any>;

    /**
     * Waits for Gatsby to finish the route change, in order to ensure event handlers are properly setup
     */
    waitForRouteChange(): Chainable<any>;
  }
}

Next create the index.js file, which should look something like this.

import "cypress-axe"
import "./commands"

Next, let’s create a plugin folder mkdir -p cypress/plugins.

// / <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

/**
 * @type {Cypress.PluginConfig}
 */
module.exports = (on, config) => {
  // `on` is used to hook into various events Cypress emits
  // `config` is the resolved Cypress config
  return config
}

Now finally let’s create our tests folder mkdir -p cypress/e2e.

cypress-axe

In this blog post we won’t go over any complicated Cypress test we will simply use cypress-axe to test the accessibility of our (a11y) of our website.

/// <reference types="../support/index" />
/// <reference types="cypress" />
/// <reference types="@types/cypress-axe" />

describe("Component accessibility test", () => {
  it("Main Page", () => {
    cy.visit("/")
    cy.wait(500)
    cy.injectAxe()
    cy.checkA11y({
      include: [["#___gatsby"]],
    })
  })
})

Note the /// comments at the top used to add types for cypress. The test above will go to our home page and test if it has any a11y violations and if so will fail the test.

We can now run our tests locally by running this command:

yarn run test:e2e

Gitlab CI

Now how can we automate this, so the tests will run say every time we make changes on the master branch to make sure we haven’t broken any a11y. Create a new .gitlab-ci.yml or add the following job to an existing CI file.

image: node:12.14.1

variables:
  CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - cache/Cypress
    - node_modules

stages:
  - test

before_script:
  - yarn install

tests:
  image: cypress/browsers:node12.14.1-chrome83-ff77
  stage: test
  script:
    - yarn test:e2e:ci

I won’t go into the details of what makes up a Gitlab CI file. At the top of the file, we will cache the node_modules file so we can share it between the job and the Cypress cache. The job itself is very simple it uses a cypress/browsers:node12.14.1-chrome83-ff77 Docker image which provides a headless chrome browser that Cypress can leverage to run the tests. As we won’t have access to a GUI in the Gitlab CI runner. The tests job is very simple it runs yarn test:e2e:ci to run our Cypress tests.

That’s it, quite simple to add Cypress tests that run in our CI pipeline.

Appendix