Imagine you have a table like this one:
How would you confirm in your Cypress tests the table is showing the right values? Let's start with the column headings. Can you confirm the columns in order show the names, the dates, and the ages?
1 | beforeEach(() => { |
🎁 You can find the source code for this blog post in the repo bahmutov/sorted-table-example. I will also use custom queries like
cy.map
andcy.table
from my bahmutov/cypress-map plugin. You can practicecypress-map
commands in my course Cypress Plugins.
Retries
Great, the test works, but it is not very robust. For example, the cy.then(callback)
does not retry if the assertion expect(texts, 'headings').to.deep.equal(headings)
fails. So we really should (pun intended) use the cy.should(callback)
here - maybe the table is dynamic (as we will see later with sorting the values).
1 | <table id="people"> |
The application sets the headings row after one second
1 | // initialize the headings after a delay |
The cy.then(callback)
fails 😞 The text comes in after one second - the test has finished by then.
1 | const headings = ['Name', 'Date (YYYY-MM-DD)', 'Age'] |
The cy.should(callback)
on the other hand, succeeds ✅
1 | const headings = ['Name', 'Date (YYYY-MM-DD)', 'Age'] |
Tip: read Cypress retry-ability guide and study my Cypress assertions examples to learn more about cy.then
vs cy.should
.
Better test with cy.map query
In Cypress v12, the querying commands like cy.get
got their own "class" of commands, and it is a good thing. To simplify my own tests, I have written a library of extra queries cypress-map. Let's apply cy.map
from the plugin cypress-map
to the same test. We are mapping each found TD
element into its own innerText
property:
1 | const texts = Cypress._.map($td, 'innerText') |
Using cy.map we can simply write:
1 | // https://github.com/bahmutov/cypress-map |
Tip: confused about cy.map
and mapping a list of DOM elements into its property? Read my blog posts on point-free functional programming.
The 🔑 to understand how Cypress v12 queries are working is to realize that when the assertion fails, it goes back to the start of the entire chain of query commands and re-runs both cy.get
and cy.map
again and again.
Get the entire table with cy.table
What if we want to confirm all values in the table? We would map over rows and could map over the cells... It is a query too, since we are NOT changing the table, just getting all cell values. We could use the same logic to get a one dimensional array of cell texts:
1 | it('confirms the cells (cy.map)', () => { |
Ughh, we lost the "shape" of the table. We need 2D array of rows and columns. This is what cy.table does. By default it yields an array of arrays (each row is an array of cells).
1 | // https://github.com/bahmutov/cypress-map |
Super, we can confirm the initial table has the values we expect. Let's cy.get
the TABLE
element and grab all cells. The test will automatically retry until the page sets the headings.
1 | it('confirms the entire table with retries', () => { |
Slice a column from the table
Sometimes we don't want to confirm the entire table. Instead we want to extract a region or a column. For example, how would you confirm the "Age" column can be sorted when we press the button "Sort by date"? We need to be able to select the a region of the 2D table. Here is how we can define the axis X and Y in our table (if this is the full table including the THEAD
)
The command cy.table(x, y, w, h)
can select a region from the table starting at index x,y
(both zero-based indices), with w
columns and h
rows. Let's say we want to confirm the "ages" cells are NOT sorted by default. We can use chai-sorted assertion plugin to make our life easier.
1 | // https://github.com/bahmutov/cypress-map |
If we don't specify the h
argument, like in cy.table(2, 0, 1)
, then the entire h
will be used, so the entire column is returned.
Print the values
It makes sense to see the values between the steps, which we can do using the cy.print
query (also included in the cypress-map
library and described in my blog post A Better Cypress Log Command).
1 | it('confirms the sorted age column', () => { |
Note: cy.map
uses Lodash _.map
which flattens the 2D array [["20"],["37"],["25"],["30"]]
into [20,37,25,30]
. We could have flattened the initial array ourselves:
1 | it('confirms the sorted age column', () => { |
Sort the ages
Now let's click the "Sort by date" button and confirm the age column becomes sorted eventually.
1 | it.only('confirms the sorted age column', () => { |
Beautiful. Let's refactor the code a little bit to avoid duplication. Just move getting the ages column numbers into its own function returning the chain.
1 | const ages = () => |
Nice.
Get cells in the last two rows
Let's confirm a part of the table after sorting.
1 | it('confirms the name and dates of the last two sorted rows', () => { |
If we are looking at the table's body, the region we are interested starts at x = 0
(first column), y = 2
(third row), and extends 2x2.
1 | // sort the table by date |
What if we don't know the length of the table? Ok, let's grab the full table of the tbody
cells, which gives us an array of arrays. We can invoke the array methods using standard Cypress cy.invoke query. Let's grab the last two rows (after it was sorted using cy.wait
for now):
1 | // sort the table by date |
Now we have an array with 2 rows. Each row is an array of element texts. We want to call slice
again on each row. No worries, cypress-map
comes with cy.mapInvoke
that invokes a given method on every item in the current subject (which could be an array or a jQuery object). Now we can remove the cy.wait(2000)
and use an assertion to confirm the values are finally sorted using retries:
1 | it('confirms the name and dates of the last two sorted rows', () => { |
Nice, the values from the HTML table flow through our queries, again and again, until the assertion passes.
Ughh, now that I am looking at this, it would be nice if cy.table
supported the negative index arguments, so I could get the last two rows of the table like cy.table(0, -2)
? I guess, time to open a cypress-map issue.