Cypress Alias Documentation Trick

How to document a function that sets a Cypress alias.

Recently a Cypress asked in my Discord channel the following question:

I have a question, is the below a good practice? I have a 'Test' class (in another file, of course), in it there is a method that takes text from an element. Is assigning an alias in the class and then using that alias in the test a correct approach? Because it's a bit unclear in the test, especially when the project grows, where a given alias comes from.

The original test code with an alias

How does the test "know" if a page object method sets a Cypress alias? Let me describe how I would write the same test.

🎁 You can find the source code for this blog post in the repo bahmutov/cypress-alias-example.

The original code

I will start with placing the Test class in the Cypress E2E support file.

cypress/e2e/test.js
1
2
3
4
5
export class Test {
getText() {
return cy.get('button').invoke('text').as('buttonText')
}
}
cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
8
9
import { Test } from './test'

const test = new Test()

it('has the right text', () => {
cy.visit('public/index.html')
test.getText() // it provides the alias '@buttonText'
cy.get('@buttonText').then((text) => console.log(text))
})

The test passes, even if it unclear from the screenshot what it is checking.

The passing test

Tip: if all you do inside a cy.then(callback) is calling another function, you can use point-free or tacit programming style to write less code.

1
2
test.getText() // it provides the alias '@buttonText'
cy.get('@buttonText').then(console.log)

Printing values to the console does not show you the value. Thus I prefer using simple assertions to log the value in the Cypress Log

1
2
test.getText() // it provides the alias '@buttonText'
cy.get('@buttonText').should('be.a', 'string')

Use an assertion to log the subject text

Nice.

Change the page object

I don't object to Page Objects in general, but I do think they should be much simpler than most people try to make them. For instance (pun intended), the page objects should never be objects with its own internal state. Thus let's refactor Test to be a simple static object, no need to even use new Test to call a static method getText

cypress/e2e/test.js
1
2
3
4
5
export const Test = {
getText() {
return cy.get('button').invoke('text').as('buttonText')
},
}
cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
import { Test } from './test'

it('has the right text', () => {
cy.visit('public/index.html')
Test.getText() // it provides the alias '@buttonText'
cy.get('@buttonText').should('be.a', 'string')
})

Works exactly the same. The Test object is a collection of static methods, even if it creates a global alias side effect @buttonText.

Change the comment placement

In the original code, the user placed the comment at the call site in the spec file

1
2
// cypress/e2e/spec.cy.js
Test.getText() // it provides the alias '@buttonText'

A better place for this comment would be in the page object itself using a JSDoc method documentation.

cypress/e2e/test.js
1
2
3
4
5
6
7
8
export const Test = {
/**
* it provides the alias `@buttonText`
*/
getText() {
return cy.get('button').invoke('text').as('buttonText')
},
}

Any test code calling Test.getText method can see the documentation snippet.

JSDoc snippet shown in my VSCode

Do not hard-code the alias

Great. But we still hard-code the alias buttonText. If we call the same getText method twice, we will overwrite the previous subject. We can make it better by passing the alias name as an argument.

cypress/e2e/test.js
1
2
3
4
5
6
7
8
9
export const Test = {
/**
* it provides the alias `@buttonText`.
* @param {string} aliasName Default is `buttonText`
*/
getText(aliasName = 'buttonText') {
return cy.get('button').invoke('text').as(aliasName)
},
}
cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
8
import { Test } from './test'

it('has the right text', () => {
cy.visit('public/index.html')
Test.getText('myText')
// some time later
cy.get('@myText').should('be.a', 'string')
})

The IntelliSense snippet shows the alias parameter.

JSDoc with param

Tip: you can make your JSDoc snippets better by adding a few examples:

cypress/e2e/test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const Test = {
/**
* it provides the alias `@buttonText`.
* @param {string} aliasName Default is `buttonText`
* @example
* Test.getText('myText')
* cy.get('@myText').should('be.a', 'string')
* @example
* Test.getText()
* cy.get('@buttonText').should('equal', 'Click me')
*/
getText(aliasName = 'buttonText') {
return cy.get('button').invoke('text').as(aliasName)
},
}

JSDoc with examples

I use a lot of JSDoc examples in my tests and utility functions.

Write Page Objects using TypeScript

Even when coding my specs using JavaScript, I like using TypeScript for utilities. So I add TypeScript dev dependency and a tsconfig.json file

tsconfig.json
1
2
3
4
5
6
7
8
9
{
"include": ["cypress/e2e/**/*.ts", "cypress/e2e/**/*.js"],
"compilerOptions": {
"allowJs": true,
"noEmit": true,
"types": ["cypress"]
},
"lib": ["es2015", "dom"]
}

The test.js file becomes test.ts with slightly simpler JSDoc comment

cypress/e2e/test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const Test = {
/**
* it provides the alias `@buttonText`.
* @param aliasName Default is `buttonText`
* @example
* Test.getText('myText')
* cy.get('@myText').should('be.a', 'string')
* @example
* Test.getText()
* cy.get('@buttonText').should('equal', 'Click me')
*/
getText(aliasName = 'buttonText') {
return cy.get('button').invoke('text').as(aliasName)
},
}

The JavaScript spec can be checked using the // @ts-check comment

cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
8
9
10
11
// @ts-check
import { Test } from './test'

it('has the right text', () => {
cy.visit('public/index.html')
Test.getText('myText')
// some time later
cy.get('@myText').then((s) => {
expect(s).to.be.a('string')
})
})

I changed the assertion .should('be.a', 'string') to a callback to show a problem: cy.get assumes it yields a jQuery object

The cy.get command yields a jQuery element by default

We can fix this either by switching the spec to TypeScript and writing cy.get<string>('@myText') or by adding a page object method to the Test object.

cypress/e2e/test.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
27
export const Test = {
/**
* it provides the alias `@buttonText`.
* @param aliasName Default is `buttonText`
* @example
* Test.getText('myText')
* cy.get('@myText').should('be.a', 'string')
* @example
* Test.getText()
* cy.get('@buttonText').should('equal', 'Click me')
*/
getText(aliasName = 'buttonText') {
return cy.get('button').invoke('text').as(aliasName)
},

/**
* Yields the previously set Cypress alias
* @param aliasName Alias without `@` prefix
* @example
* cy.getText('myText')
* // some time later
* cy.getAlias('myText').should('equal', 'Click me')
*/
getAlias(aliasName = 'buttonText') {
return cy.get<string>('@' + aliasName)
},
}

Let's use the Test.getAlias from the test JS spec

cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
8
9
10
11
// @ts-check
import { Test } from './test'

it('has the right text', () => {
cy.visit('public/index.html')
Test.getText('myText')
// some time later
Test.getAlias('myText').then((s) => {
expect(s).to.be.a('string')
})
})

IntelliSense popup shows the correct subject for Test.getAlias

The page objet method yields a string subject

TypeScript "knows" the s argument is a string.

Accessing the aliased value through the page object gives us the right type

Nice.