Testing an online chainsaw store using Cypress.io

Drive-by testing www.chainsawsdirect.com using Cypress.io test runner.

Source code for this blog post is in https://github.com/bahmutov/testing-chainsawsdirect repo.

I don't know what kind of chainsaw Cypress.io is (maybe it is an open source chainsaw ?!) but I do know that Cypress can quickly test an online chainsaw store like www.chainsawsdirect.com.

First test

I have installed Cypress with npm install -D cypress and created cypress.json file with base url pointing at the domain I want to test: www.chainsawsdirect.com

cypress.json
1
2
3
{
"baseUrl": "https://www.chainsawsdirect.com"
}

My first test is simple - it selects a type of the product and checks if the page shows at least a few matching ones.

cypress/integration/first.js
1
2
3
4
5
6
7
/// <reference types="Cypress" />
it('has gas chainsaws', () => {
cy.visit('/')
cy.get('#style1').select('Chain Saws - Gas')
// make sure there are at least double digit number of products
cy.contains('.plcount', /^\d\d Products$/).should('be.visible')
})

The test runs and passes

Has gas chainsaws test

If I hover over any command in the Command Log (left side of the GUI), it will show the view of the application at that moment. For example, I can hover over "CONTAINS ..." command and see that the right text on the page is highlighted - our test is asserting the right thing.

Highlights number of gas chainsaws found

Note: the red XHR calls in the Command Log are the calls to the ad tracking service I have blacklisted from cypress.json file.

cypress.json
1
2
3
4
{
"baseUrl": "https://www.chainsawsdirect.com",
"blacklistHosts": "wrs.adrsp.net"
}

Test organization

Following the Writing and organizing test guide, I can refactor my spec file to avoid code duplication. For example, we can visit the page before each test:

1
2
3
4
5
6
7
8
9
/// <reference types="Cypress" />
beforeEach(() => {
cy.visit('/')
})

it('has gas chainsaws', () => {
cy.get('#style1').select('Chain Saws - Gas')
cy.contains('.plcount', /^\d\d Products$/).should('be.visible')
})

Prices test

Hmm, $500 for a gas chainsaw is kind of steep. I would like to buy a reasonably priced saw, so let's test that we can see all gas chainsaws sorted by price from low to high. Cypress Selector Playground helps me find the selector command for the price widget:

Selector Playground suggests the command to use

We can sort found products and get all DOM elements with the prices.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
context('Chainsaw Direct', () => {
beforeEach(() => {
cy.visit('/')
})

it('has reasonably priced gas chainsaws', () => {
cy.get('#style1').select('Chain Saws - Gas')
cy.contains('.plcount', /^\d\d Products$/).should('be.visible')

cy.get('#sort_value').select('Price: Low to High')
cy.get('.regPrice').should('have.length.gt', 0)
})
})

Prices low to high

Great, there are 22 products currently in the store, and they seem arranged from low price to higher prices. But are we sure that the products are really sorted? The low price $154.99 is nice, but I would like to

  • grab all elements with prices like $154.99, $159.99, ...
  • convert to numbers
  • assert that the list of numbers follows the increasing order

The built-in Chai assertions that come with Cypress do not include "array should be sorted" assertion, but a quick NPM search finds a package that seems to do what I need.

Find chai-sorted

Extending Cypress Chai object with additional assertions is show in "Adding Chai Assertions" recipe and is simple. Just add these two lines to cypress/support/index.js file.

cypress/support/index.js
1
2
import chaiSorted from 'chai-sorted'
chai.use(chaiSorted)

Update the test to parse elements' text content and add assertion

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
context('Chainsaw Direct', () => {
beforeEach(() => {
cy.visit('/')
})

it('has reasonably priced gas chainsaws', () => {
cy.get('#style1').select('Chain Saws - Gas')
cy.contains('.plcount', /^\d\d Products$/).should('be.visible')

cy.get('#sort_value').select('Price: Low to High')
cy.get('.regPrice')
.should('have.length.gt', 0)
.then($prices => {
// remove "$" from prices and convert to strings
const prices = $prices
.toArray()
.map($el => parseFloat($el.innerText.substr(1)))
// assertion comes from chai-sorted
expect(prices).to.be.sorted()
})
})
})

Beautiful, there are 22 prices, and they are displayed sorted in ascending order.

Prices are in ascending order

Search test

I can see that there is a search box that shows different results as I type.

Search XHR calls

I can inspect each XHR call to find out how the API returns the search results. Let's get it to show "Cypress Test Runner" as the one and only search result 😁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
it('finds Cypress among the saws', () => {
// stub API calls to the search endpoint
cy.server()
cy.route('/sayt.php?q=Cypr*', {
suggestions: [
{
value: 'Cypress Test Runner',
data: 1,
exactMatch: 1
}
]
})

// by stubbing search XHRs we can return a single
// result when typing "Cypress" into the search box
cy.get('#searchText').type('Cypress', { delay: 100 })
cy.get('.autocomplete-suggestion')
.should('have.length', 1)
.first()
.should('have.text', 'Cypress Test Runner')
})

The test runs and the test results are showing the synthetic test result we have returned from the stubbed XHR.

Prices are in ascending order

All good.

Conclusions

Cypress test runner is a quick and enjoyable way to write and run end-to-end tests for any website. Well, for almost any website - we have difficulties with websites that use iframes, shadow DOM or multiple domains. But aside from that - if you need to cut some nice looking tests, try Cypress.