This recipe shows how to verify that an Ag-Grid table is sorted correctly. The source code can be found in the cypress-example-recipes repository under the "Testing the DOM" list.
The application itself is a single HTML file
<head>
<title>Ag-Grid Basic Example</title>
<script src="https://unpkg.com/ag-grid-community/dist/ag-grid-community.min.js"></script>
<script src="main.js"></script>
</head>
<body>
<h1>Ag-Grid Table</h1>
<div id="myGrid" style="height: 200px; width:700px;" class="ag-theme-alpine"></div>
</body>
The main.js
file creates a "smart" table widget using vanilla JavaScript
const columnDefs = [
{ headerName: 'Make', field: 'make', sortable: true },
{ headerName: 'Model', field: 'model', sortable: false },
{ headerName: 'Price', field: 'price', sortable: true },
]
// specify the data
const rowData = [
{ make: 'Toyota', model: 'Celica', price: 35000 },
{ make: 'Ford', model: 'Mondeo', price: 32000 },
{ make: 'Porsche', model: 'Boxter', price: 72000 },
]
// let the grid know which columns and what data to use
const gridOptions = {
columnDefs,
rowData,
}
// setup the grid after the page has finished loading
document.addEventListener('DOMContentLoaded', function () {
const gridDiv = document.querySelector('#myGrid')
new agGrid.Grid(gridDiv, gridOptions)
})
The table is sortable by car make and price.
Display test
Let's write a test that verifies the sorting order. First, we need to make sure the table loads and the three rows are displayed. We can inspect the rendered DOM elements to find the selectors.
We are only interested in the displayed rows built with the HTML <div role="row" class="ag-row" ...>
markup.
// cypress/integration/spec.js
/// <reference types="cypress" />
describe('Sorting table', () => {
it('sorts', () => {
cy.visit('index.html')
cy.get('#myGrid') // table
.get('[role=rowgroup] .ag-row')
.should('have.length', 3) // non-header rows
})
})
The test passes when the table loads and the elements are found.
During the test we will always query the table, thus we can avoid using #myGrid
and [role=rowgroup]
selectors again and again by limiting the part of the document we are interested in using .within command.
cy.get('#myGrid') // table
.within(() => {
// limit query commands to the found element
cy.get('[role=rowgroup] .ag-row')
.should('have.length', 3) // non-header rows
})
Sorting
Let's sort the rows by price. Because sorting happens so fast, I have inserted .wait(1000)
into this test. In real life, the waits are usually unnecessary.
describe('Sorting table', () => {
it('sorts', () => {
cy.visit('index.html')
cy.get('#myGrid') // table
.within(() => {
cy.get('[role=rowgroup] .ag-row')
.should('have.length', 3) // non-header rows
cy.log('**sort by price**').wait(1000)
cy.contains('.ag-header-cell-label', 'Price').click()
// check ↑ is visible
cy.contains('.ag-header-cell-label', 'Price')
.find('[ref=eSortAsc]').should('be.visible')
})
})
})
The test passes and we can see the table has been sorted by price.
To really check the sorted table, we need to extract the price cells, convert them from strings to numbers, then check if they have indeed been sorted. Conveniently, every cell in Ag-Grid has an attribute with the property name.
Let's grab these cells and check if they have been sorted from min to max.
/// <reference types="cypress" />
// Lodash is bundled with Cypress
// https://on.cypress.io/bundled-tools
const { _ } = Cypress
describe('Sorting table', () => {
it('sorts', () => {
cy.visit('index.html')
cy.get('#myGrid') // table
.within(() => {
cy.get('[role=rowgroup] .ag-row')
.should('have.length', 3) // non-header rows
cy.log('**sort by price**').wait(1000)
cy.contains('.ag-header-cell-label', 'Price').click()
// check ↑ is visible
cy.contains('.ag-header-cell-label', 'Price')
.find('[ref=eSortAsc]').should('be.visible')
// verify the prices in the column are indeed in sorted order
const toStrings = (cells$) => _.map(cells$, 'textContent')
const toNumbers = (prices) => _.map(prices, Number)
cy.get('[col-id=price].ag-cell')
.then(toStrings)
.then(toNumbers)
.then((prices) => {
// confirm prices are sorted
// by sorting them ourselves
// and comparing with the input list
const sorted = _.sortBy(prices)
expect(prices, 'cells are sorted 📈').to.deep.equal(sorted)
})
})
})
})
We get the list of cells, extract textContent
from every cell, call Number
to convert string to a number, then assert that the sorted list of prices is the same as the list of extracted prices.
Tip: you can extend Chai assertions used in Cypress with additional matchers, making complex checks simpler. See the recipe "Adding Chai Assertions" in cypress-example-recipes.
// explicit assertion
expect(justPrices).to.be.sorted()
// or by returning the list of prices
.then((cells$) => {
...
return _.map(sorted, 'price')
})
.should('be.sorted')
The test runs ... and fails.
We can see that the table has been sorted (the column header indicates it, and the rows do display 32000, 35000, and 70000), so what is going wrong?
Study the Markup
To answer the question "why is the test failing?", we need to look at the HTML markup again.
The Ag-Grid is fast because it does not actually move anything in the DOM. Notice that row with index zero still has the price "35000" cell, and the row with index one has the lowest price "32000". The Ag-Grid instead of changing the order of rows, "simply" changes how they are displayed. It sets the individual style on each row using translateY
property to move the row on the screen (see the text underlined with purple). Thus the lowest priced car has translateY(0px)
(top row), the second car has translateY(42px)
and the most expensive car has translateY(84px)
value, putting it to the third row.
Fixed Test
Luckily for us, the grid still adds an attribute with the sorted index to each row: row-index=...
. We need to get the cells, convert the price text to a number, AND attach the row index attribute from the parent element. Let's first make sure the list of objects comes out correctly, I will use console.table
to print it.
// Lodash is bundled with Cypress
// https://on.cypress.io/bundled-tools
const { _ } = Cypress
describe('Sorting table', () => {
it('sorts', () => {
cy.visit('index.html')
cy.get('#myGrid') // table
.within(() => {
cy.get('[role=rowgroup] .ag-row')
.should('have.length', 3) // non-header rows
cy.log('**sort by price**')
cy.contains('.ag-header-cell-label', 'Price').click()
// check ↑ is visible
cy.contains('.ag-header-cell-label', 'Price')
.find('[ref=eSortAsc]').should('be.visible')
// verify the prices in the column are indeed in sorted order
const cellsToPriceObjects = (cells$) => {
return _.map(cells$, (cell$) => {
return {
price: Number(cell$.textContent),
rowIndex: Number(cell$.parentElement.attributes['row-index'].value),
}
})
}
cy.get('[col-id=price].ag-cell')
.then(cellsToPriceObjects)
.then((prices) => {
console.table(prices)
// TODO confirm prices are sorted
})
})
})
})
Great, now we can sort the prices using the rowIndex
property using Lodash method sortBy
, extract the price property, and confirm the list is sorted.
cy.get('[col-id=price].ag-cell')
.then(cellsToPriceObjects)
.then((prices) => {
console.table(prices)
// confirm prices are sorted
// by sorting them ourselves
// and comparing with the input list
const sorted = _.sortBy(prices, 'rowIndex')
// extract just the price numbers and check if they are sorted
const justPrices = _.map(sorted, 'price')
const sortedPrices = _.sortBy(justPrices)
expect(justPrices, 'cells are sorted 📈').to.deep.equal(sortedPrices)
})
The test is green. In general, we want to keep end-to-end tests independent of the implementation details. In this case, we had to inspect how the table widget renders itself in the DOM to write the test correctly; which to me still is testing the output of the code, and not the implementation details.