Sorting the Table

July 27, 2020

•

By Gleb Bahmutov

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.

Clicking on the "Make" and "Price" column headers sorts the records

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.

Ag-Grid Table widget markup

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.

The table loads

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.

Sorting cards by price, from lowest to Porsche

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.

Every price element has col-id="price" attribute

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.

The list of cells is NOT sorted, even though we see a sorted table

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.

Ag-Grid markup related to sorting

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
      })
    })
  })
})
Each price cell with the sorted row index

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 sort test passes

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.