Using TypeScript aliases in Cypress tests

How to configure TypeScript and Webpack path aliases to load application code from Cypress tests

In this post I will show how you can write end-to-end tests in TypeScript and how to import from test code your application source files using path aliases like this:

1
import {greeting} from '@app/greeting'

instead of brittle relative paths like this

1
import {greeting} from '../../app/src/greeting'

Note: the source code for this blog post is at bahmutov/using-ts-aliases-in-cypress-tests

Application

For this demo I will use a minimal example: just an HTML page index.html with some TypeScript code

index.html
1
2
3
4
5
6
<html>
<body>
<div id="app"></div>
<script src="src/app.ts"></script>
</body>
</html>

The code src/app.ts places the greeting imported from src/utils.ts into the DOM

src/utils.ts
1
export const greeting = 'Hello World'
src/app.ts
1
2
3
import { greeting } from "./utils"

document.getElementById('app').innerText = greeting

To serve the app I will use Parce bundler

1
npm i -D parcel-bundler
package.json
1
2
3
4
5
{
"scripts": {
"start": "parcel serve index.html"
}
}

When I run npm start the page is working as expected at localhost:1234

Application in action

Cypress Tests in TypeScript

We can add Cypress end-to-end tests to this project with

1
2
npm i -D cypress
+ [email protected]

To quickly scaffold everything, I prefer to use my little utility @bahmutov/cly which stands for "quickly". Or maybe it stands for "Cypress CLI"? Who knows.

1
2
3
4
5
6
$ npx @bahmutov/cly init
npx: installed 72 in 6.398s
scaffolding new Cypress project
✅ scaffolded "cypress" folder with a single example spec
you can configure additional options in cypress.json file
see https://on.cypress.io/configuration

We have cypress.json and cypress folder, let's change the contents of cypress/integration/spec.js to test our page.

cypress/integration/spec.js
1
2
3
4
5
/// <reference types="Cypress" />
it('shows greeting', function () {
cy.visit('http://localhost:1234')
cy.contains('#app', 'Hello World').should('be.visible')
})

Start the app in one terminal with npm start and open Cypress from another terminal with npx cypress open - the test should be green.

First test is passing

But if we write our application in TypeScript, let's also write our tests in TypeScript. The simplest way to configure test bundling is by installing @bahmutov/add-typescript-to-cypress package. We also need to install TypeScript module itself, and we need Webpack

1
2
3
4
npm install --save-dev @bahmutov/add-typescript-to-cypress typescript webpack
+ [email protected]
+ @bahmutov/[email protected]
+ [email protected]

Super, it even has created a default tsconfig.json file for us

tsconfig.json
1
2
3
4
5
6
{
"include": [
"node_modules/cypress",
"cypress/*/*.ts"
]
}

We can rename our test file from spec.js to spec.ts - and it should run the same. Since the tsconfig.json file is only necessary for our Cypress tests I will move it into the cypress folder. Do not forget to update the paths in tsconfig.json after moving.

Sharing code

Our application shows the greeting text - and I don't want to hardcode the string to find in my test code. Instead I think it is ok to load the greeting from the application code. It is simple to do using a relative path.

cypress/integration/spec.ts
1
2
3
4
5
import {greeting} from '../../src/utils'
it('shows greeting', function () {
cy.visit('http://localhost:1234')
cy.contains('#app', greeting).should('be.visible')
})

Nice, but I really dislike the long relative paths that use ../.. to get out of the Cypress integration folder. Luckily TypeScript and Webpack both have ways to define aliases to use shortcuts. We need TypeScript path aliases to make sure our TypeScript tooling (like VSCode IntelliSense) understands the spec files, while Webpack aliases are needed to find the code during bundling.

Our goal is to refer to all source files by @src/... from our spec files rather than ../../src/....

In the cypress/tsconfig.json add baseUrl and paths properties.

cypress/tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"include": [
"../node_modules/cypress",
"*/*.ts"
],
"compilerOptions": {
"baseUrl": "..",
"paths": {
"@src/*": ["src/*"]
}
}
}

Nice, now we can import greeting from the test file like this

cypress/integration/spec.ts
1
import {greeting} from '@src/utils'

VSCode can resolve the alias correctly, as shown by this popup

Path alias is working in TypeScript

But if we try to run Cypress test right now, we will get a nasty error

1
2
3
4
5
./cypress/integration/spec.ts
Module not found: Error: Can't resolve '@src/utils' in '/Users/gleb/git/using-ts-aliases-in-cypress-tests/cypress/integration'
resolve '@src/utils' in '/Users/gleb/git/using-ts-aliases-in-cypress-tests/cypress/integration'
Parsed request is a module
... goes on for 50 lines

This is due to the fact that Webpack bundler does not know about the path aliases in tsconfig.json. The simplest way is to tell Webpack how to alias modules by prefix. In file cypress/plugins/cy-ts-preprocessor.js add the following alias object to the existing resolve block:

cypress/plugins/cy-ts-preprocessor.js
1
2
3
4
5
6
7
8
9
10
const webpackOptions = {
resolve: {
extensions: ['.ts', '.js'],
// add the alias object
alias: {
'@src': path.resolve(__dirname, '../../src')
}
}
...
}

That is it, our tests can share code with application without fragile folder hops.

See also