How to Avoid Using Global Cypress Variables

Avoid clashing global types between Cypress and Jest by using local-cypress library.

Global variables are the worst thing in programming, but they are common in test runners due to history. Before the invention of bundlers, we used to include the test runner's script before the spec file to make it run.

1
2
3
<!-- or qunit or custom test runner script -->
<script src="vendor/mocha.js"></script>
<script src="tests/my-spec.js"></script>

The vendor/mocha.js would set up global variables like describe and it, and the tests/my-spec.js would use them to define and execute tests. In addition to the test functions, there would be other global variables, like Chai's expect and assert functions.

Both Cypress and Jest provide global variables like describe and expect for your tests to use. The global variables do not clash at runtime. If you are writing a unit test, you execute it with Jest. If you are writing a unit or end-to-end test, you execute it with Cypress. The clashing variables do conflict during the static type checking though. The TypeScript lint step breaks complaining about double registration of variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
node_modules/@types/jest/index.d.ts:41:13 - error TS2403: Subsequent variable declarations must have the same type.  Variable 'it' must be of type 'TestFunction', but here has type 'It'.

41 declare var it: jest.It;
~~

node_modules/cypress/types/mocha/index.d.ts:2583:13
2583 declare var it: Mocha.TestFunction;
~~
'it' was also declared here.

node_modules/cypress/types/cypress-expect.d.ts:2:15 - error TS2451: Cannot redeclare block-scoped variable 'expect'.

2 declare const expect: Chai.ExpectStatic
~~~~~~

node_modules/@types/jest/index.d.ts:47:15
47 declare const expect: jest.Expect;
~~~~~~
'expect' was also declared here.

The above messages are from cypress-io/cypress-and-jest-typescript-example.

Workarounds

We suggest solving the type clashes by either skipping checking the 3rd party types in the dependencies using skipLibCheck TS config option:

tsconfig.json
1
2
3
4
5
{
"compilerOptions": {
"skipLibCheck": true
}
}

Another solution is to explicitly list the types to load when linting; skipping Jest types for example when linting Cypress tests

tsconfig.json
1
2
3
4
5
6
7
{
"compilerOptions": {
// be explicit about types included
// to avoid clashing with Jest types
"types": ["cypress"]
}
}

Of course both workarounds have their limits. We do want to check our types, and listing only the types to use is a maintenance nightmare. What can we do instead?

Ava's example

If you ever used Ava test runner you might appreciate its lack of global variables. Every spec file has to import the test value explicitly.

1
2
3
4
5
6
7
8
9
10
const test = require('ava');

test('foo', t => {
t.pass();
});

test('bar', async t => {
const bar = Promise.resolve('bar');
t.is(await bar, 'bar');
});

Nice - no global variables or types. Can we do the same for Cypress tests?

Local Cypress

Cypress test runner includes everything one needs to write tests. Thus there are three global variable types:

  • the Cypress' own objects with commands cy and Cypress
  • the test definitions like describe and it coming from Mocha
  • the assertions like expect and assert from bundled Chai libray

Whenever one uses Cypress types (either by including it in the tsconfig.json file or by using the special comment line /// <reference types="cypress"> in the spec file) the types are described by the cypress/types/index.d.ts file.

This file just "assembles" the types by including types for Cypress and all its bundled tools.

The Cypress' top-level types index.d.ts file

If we want to shift Cypress from providing global types to having explicit local types, we would need to modify those three type files.

Local cy and Cypress objects

Let's start with cy and Cypress definitions. They are declared in the included file cypress-global-vars.d.ts.

The global declaration file for cy and Cypress

This file has nothing else, so let's "disable" the global variable types cy and Cypress by ... commenting out the reference line loading cypress-global-vars.d.ts from index.d.ts

index.d.ts
1
2
3
4
5
6
...
/// <reference path="./net-stubbing.ts" />
/// <reference path="./cypress.d.ts" />
// /// <reference path="./cypress-global-vars.d.ts" />
/// <reference path="./cypress-type-helpers.d.ts" />
/// <reference path="./cypress-expect.d.ts" />

We could write a script to do this automatically - I have even created an NPM package called local-cypress that does this during postinstall step. Just run npm i -D local-cypress or yarn add -D local-cypress postinstall-postinstall in your repo, and after every installation it will patch the node_modules/cypress/types/index.d.ts file if necessary.

If the cy and Cypress are no longer declared global (they are still ARE global variables at run-time), how do our specs "know" about them? We need to declare a type for local variables to be imported by our specs. Thus the package local-cypress defined "dummy" exports with the right types.

local-cy/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// <reference types="cypress" />
type cy = Cypress.cy & EventEmitter
type Cypress = Cypress.Cypress & EventEmitter

/**
* Object `cy` all Cypress API commands.
* @see https://on.cypress.io/api
* @type {Cypress.cy & EventEmitter}
* @example
* cy.get('button').click()
* cy.get('.result').contains('Expected text')
*/
// @ts-ignore
export const cy: cy = window.cy

/**
* Holds bundled Cypress utilities and constants.
* @see https://on.cypress.io/api
* @type {Cypress.Cypress & EventEmitter}
* @example
* Cypress.config("pageLoadTimeout") // => 60000
* Cypress.version // => "6.3.0"
* Cypress._ // => Lodash _
*/
// @ts-ignore
export const Cypress: Cypress = window.Cypress

Great - the spec can import cy and Cypress objects from local-cypress and get the right typings without globals. Here is a video of using it from the code editor

Importing and using Cypress from local-cypress package

Local Chai and Mocha

Similarly, the local-cypress comments out global Chai and Mocha declarations after install. Instead local-cypress provides locally exported definitions of all functions a typical spec might use.

cypress/integration/spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
import {cy, expect, describe, it} from 'local-cypress'
// import function from the application source
import { sum } from '../../src/foo'

describe('TypeScript spec', () => {
it('works', () => {
cy.wrap('foo').should('equal', 'foo')
})

it('calls TS source file', () => {
expect(sum(1, 2, 3, 4)).to.equal(10)
})
})

You can find the full example in repo bahmutov/local-cypress-and-jest-typescript-example that also shows TypeScript linting without any special workarounds; no need to skip the library check, no need to filter the list of types to load.

Note that Jest still brings its own global describe, it, expect and other definitions. Thus be sure to explicitly import them into your tests from local-cypress to get the right definitions

Importing it and cy definitions

For example, importing the Cypress' it declaration from local-cypress allows you to pass per-test configuration.

With the right type we get the test configuration intellisense

Future work

I intend to refactor the Cypress type files a little to make skipping global type declarations simpler - there is still room for improvement. While removing all global declarations in favor of locally defined variables is not on the immediate road map, I hope local-cypress proves that it is possible and useful.

What about Jest globals? Well, you could ask the Jest development team to deprecate and remove global variables and types.