Check Data Using Page Objects And Higher Order Functions

Using cy-spok and other plugins to create data-checking page object assertions.

Testers and developers often use page objects to interact with their web applications via DOM elements. Let's create a page object for our TodoMVC app:

cypress/e2e/todo.ts
1
2
3
4
5
6
7
8
9
export const TodoMVC = {
addTodo(title: string) {
cy.get('.new-todo').type(`${title}{enter}`)
},

getTodos() {
return cy.get('.todo-list li')
},
}

All tests that add a todo item and check them can use the addTodo and getTodos methods

cypress/e2e/adding-spec.cy.ts
1
2
3
4
5
6
7
8
9
10
11
12
import { TodoMVC } from './todo'

describe('TodoMVC', function () {
beforeEach(() => {
cy.visit('/')
})

it('adds a new todo', () => {
cy.get('.new-todo').type('Feed the cat{enter}')
cy.get('.todo-list li').should('read', ['Feed the cat'])
})
})

Adds a new todo test

Note: the should read assertion comes from my cypress-map plugin.

🎁 The source code for this blog post can be found in the repo bahmutov/todomvc-po-with-assertions.

Data validation

What about data validation? For example, our application stores todo items in the browser's local storage. Let's confirm it.

The app stores its data in a local storage entry

1
2
3
4
5
6
7
8
9
10
11
12
13
it('stores todo in the local storage', () => {
cy.get('.new-todo').type('Feed the cat{enter}')
cy.get('.todo-list li').should('read', ['Feed the cat'])

cy.step('check local storage')
cy.window()
.its('localStorage')
.invoke('getItem', 'react-todos')
.apply(JSON.parse)
.should('have.length', 1)
.its(0)
.should('deep.include', { title: 'Feed the cat', completed: false })
})

Checking the object stored in the local storage

Note: the cy.step command comes from the cypress-plugin-steps plugin, cy.apply query is from my cypress-map plugin.

If we want to validate the id of the item, we need to grab the its value and use a regular expression

1
2
3
4
5
6
7
8
9
10
11
12
13
cy.step('check local storage')
cy.window()
.its('localStorage')
.invoke('getItem', 'react-todos')
.apply(JSON.parse)
.should('have.length', 1)
.its(0)
.should('deep.include', { title: 'Feed the cat', completed: false })
.its('id')
.should(
'match',
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
)

Validating the known properties plus the "id" value

cy-spok

Just like entering a todo item into the input field, checking the data is a very common operation in E2E tests. Thus it makes sense to create a page object utility method to easily check the data. I can use my cy-spok plugin in the page object to create a function to be used as cy.should(callback) argument.

cypress/e2e/todo.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// https://github.com/bahmutov/cy-spok
import spok from 'cy-spok'

export const TodoMVC = {
addTodo(title: string) {
cy.get('.new-todo').type(`${title}{enter}`)
},

getTodos() {
return cy.get('.todo-list li')
},

beTodoItem: spok({
title: 'Feed the cat',
completed: false,
id: spok.test(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
),
}),
}

The spok is a higher order function, since calling spok({ ... }) returns another function. The property beTodoItem: spok({ ... }) is a method to be used inside cy.should(callback). Let's check our local storage

1
2
3
4
5
6
7
8
9
10
11
12
13
it('adds a new todo (check the data)', () => {
TodoMVC.addTodo('Feed the cat')
TodoMVC.getTodos().should('read', ['Feed the cat'])

cy.step('check local storage')
cy.window()
.its('localStorage')
.invoke('getItem', 'react-todos')
.apply(JSON.parse)
.should('have.length', 1)
.its(0)
.should(TodoMVC.beTodoItem)
})

Page object with a should object callback

Checking the server response

Let's say our application receives the todo items from the backend. We could use the same callback property together with cy.intercept command

1
2
3
4
5
6
7
8
9
it('sends the todos on load', () => {
TodoMVC.addTodo('Feed the cat')
TodoMVC.getTodos().should('read', ['Feed the cat'])
cy.intercept('GET', '/todos').as('getData')
cy.reload()
cy.wait('@getData')
.its('response.body.0')
.should(TodoMVC.beTodoItem)
})

Checking HTML structure

Custom data checks inside the page object - how about custom HTML checks inside the page object? Let's say we want to validate all important fields inside the page. We could use a combination of cy.get and cy.within and all other HTML assertions:

1
2
3
4
5
6
7
8
9
10
11
12
13
it('adds a new todo and checks the DOM', () => {
cy.get('.todoapp').within(() => {
cy.get('header.header').within(() => {
cy.get('h1').should('have.text', 'todos')
cy.get('input.new-todo').should(
'have.attr',
'placeholder',
'What needs to be done?',
)
cy.get('input.new-todo').type('Feed the cat{enter}')
})
})
})

So we have the header inside the class="todoapp" element. The header contains the h1 element with the text "todos" and the input element having some certain attributes. Can we check the structure, attributes, and text easier?

Sure - by using the custom HTML assertion should look from the cypress-map plugin.

cypress/e2e/todo.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
export const TodoMVC = {
addTodo(title: string) {
cy.get('.new-todo').type(`${title}{enter}`)
},

getTodos() {
return cy.get('.todo-list li')
},

beTodoItem: spok({
title: 'Feed the cat',
completed: false,
id: spok.test(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
),
}),

html: `<section class="todoapp">
<div>
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" />
</header>
</div>
</section>`,
}

The html property simply lists the "important" elements and their important attributes. It is the small subset of the page, our page must have these HTML nodes. We can use this static string to check the page:

1
2
3
it('checks the DOM using page object', () => {
cy.get('.todoapp').should('look', TodoMVC.html)
})

Checking the page HTML subset

Nice!