If there’s one thing we love to do here at Buoy, it’s testing. The Automation team has a few tools in our pocket (Cypress & Detox) which help us effectively and confidently test our applications, both of which are written in TypeScript (TS).

Why use TypeScript?

We have used Cypress as an e2e testing tool since the inception of our QA Automation team. When we first started the project it was written in JavaScript (JS), which is the common strategy to use when starting a Cypress project. So why would we want to rewrite the code we had into TS? There’s a few answers to that question: type safety, self documenting code, IntelliSense and overall better code quality.

Take type safety first: the ability to identify and fix errors that we find while coding and compiling gives us confidence in our e2e tests as well as the product we ship to our customers. See an example:

JS:

export class FooJs {
  get element() {
    return cy.get("element");
  }

  completeForm(text) {
    this.element.type(text);
  }
}

TS:

export class Foo {
  get element(): Cypress.Chainable<JQuery<HTMLElement>> {
    return cy.get("element");
  }

  completeForm(text: string): void {
    this.element.type(text);
  }
}

:information_source: We use the Page Object Model (POM) pattern when writing tests

What could go wrong here? This seems like a pretty straightforward example where any change to it seems like an easy fix. However, lets say the form gets another input field and we want to change the type to on object vs just a string.

completeForm(input: {text: string, moreText: string}): void {
  this.element.type(input.text);
  this.element.type(input.moreText);
}

The screenshot below shows what happens in plain ol’ JS. No visual error emitted to us in our FooJS function completeForm, so there would be no indication that we broke our test. If you tried to run your Cypress test with this code it would more than likely fail, but with TS we could cut out the need to run the test and fix it on the spot.

TS would also account for other usages of this file in our testing project if we were to build TS locally or in CI. If we kept it in JS, it could be missed (even through our review process) where it could make it to the main branch and our CI run would fail. The feedback loop that you can create with TS is quicker than JS, which gives the automation team confidence in every pull request we make.

The second reason we chose TS is self documenting code. I like to use Eagleson’s law as an example:

Any code of your own that you haven't looked at for six or more months might as well have been written by someone else.

This quote is something that probably resonates with most software engineers. We always find new or more efficient ways to build our code that we weren’t aware of at the time. Since TS lets us type our variables, interfaces, etc. we’re able to derive what the function is doing. This allows us to easily reintroduce ourselves to that portion of the codebase without needing to ask another engineer for context or scour through the code to determine what the code is doing.

A third reason why we chose TS is IntelliSense for Visual Studio Code. It allows us to autocomplete as we type, get information from hovering over functions and a plethora of other helpful features!

There are many reasons we chose TS over JS. Such as our usage of the Page Object Model pattern, better understanding of Cypress types and the ability to use tools such as eslint or tsc, but I wanted to highlight the main sellers for us!

How

The Cypress team and their extensive documentation do a great job detailing how you could initialize or migrate your project to TS.

We also have great full stack engineers in house that helped prop up the Cypress project with TS. Our Cypress project lives within the same repository as the application, so we were able to use the existing typescript package (using yarn) for Cypress.

First we created a tsconfig.json file that we extended from an existing tsconfig. This aligns us with the full stack engineers so we can easily pair and find examples within the code base.

Example:

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "target": "es5",
    "lib": ["es6", "dom"],
    "module": "commonjs",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "paths": {
      "@app/*": ["app/javascript/*"],
      "@i18n/*": ["cypress/i18n/*"],
      "@support/*": ["cypress/support/*"]
    }
  },
  "include": ["**/*.ts"]
}

We then updated our eslintjs.rc file to include Cypress so we could follow the same patterns (or make our own) as our fellow engineers.

Then, we created a file cypress/support/index.d.ts that houses all of our Cypress commands. This is one caveat where using TS with Cypress that I don’t particularly like, as it splits your custom Cypress commands into two files. The extra upkeep can seem redundant at times.

Lastly, we registered our tsconfig paths so we could alias our long path names. This allows us to easily import our modules without needing to create relative paths (ex. ../../support/Foo can be @support/Foo).

The code below will need to be called in your plugins/index.js file to properly register the paths when the Cypress node process is launched.

import tsConfig from "../tsconfig.json";
import * as tsConfigPaths from "tsconfig-paths";

const baseUrl = "./";
tsConfigPaths.register({
  baseUrl,
  paths: tsConfig.compilerOptions.paths,
});

And that’s it! It was quite simple to start using TypeScript with Cypress and continues to be a driving factor with our ever growing test suites.