Cypress TodoMVC Questions Answered

Answers to some questions about TodoMVC tests from my Cypress workshop.

Recently I have conducted a Cypress Level Up Online Workshop April 2023, and this blog post answers some of the questions I have received from the participants. Hope these blog post provides good answers.

📺 You can watch these questions answered in the video Cypress TodoMVC Questions Answered.

The Todo item selector

Q: How do you determine to use the selector ".todo-list li" in the test below?

1
2
3
4
5
6
7
8
it ('add 2 items', function(){
cy.get('.new-todo')
.type('todo A{enter}')
.type('todo B{enter}')
// assert that the new Todo item has been added added to the list
// li = command which combines lists
cy.get('.todo-list li').should('have.length', 2)
})

This is a good question especially considering what the Cypress Selector Playground suggests when you try to pick the first "todo A" item from the list

Todo list item suggested selector

The Selector Playground is definitely broken when you are trying to pick a list of elements, thus I use my personal judgement. I want to pick all Todo elements on the page. First, I read the Cypress best practices for selecting elements, then I inspect the HTML markup available in my application:

1
2
3
4
5
6
7
8
9
<header>...</header>
<section class="main">
<ul class="todo-list">
<li class="todo">...</li>
<li class="todo">...</li>
...
</ul>
</section>
<footer>...</footer>

I want to select the Todo items in the list. I won't just use the selector cy.get('li') since there could be several lists on the page. I want the list of todos, thus I pick the li elements that are children of the <ul class="todo-list"> element. To select the elements inside a parent element we separate the parent selector .todo-list from the child item selector li using the space character, so that gives me .todo-list li.

This is far from perfect. I would advise keeping the selectors consistent. In .todo-list li we use the class name "todo-list" and the element name "li". I should have probably used class names for both to keep it consistent:

1
2
cy.get('.todo-list .todo')
.should('have.length', 2)

I say picking good selectors comes with experience and reading the best practices guides. You can also learn all available ways to pick elements in Cypress by reading my Cypress Querying Examples page.

Asserting the number shown

Q: How do I capture the "1" in "1 item left" and how do I log it? Why does this attempt not work?

1
2
3
4
5
// verify filters
it ('verify filters', function(){
cy.get('class="todo-count"')
.should('equal', 1)
})

The TodoMVC app filters

The test runner error

First, let's fix the error. The selector class="todo-count" in the command cy.get('class="todo-count"') is incorrect. We could select by the class name as written in the HTML <span class="todo-count">...</span> using an attribute selector with square brackets [name=value]. In this case it would be something like cy.get('[class=todo-count]'). But a much better solution would be to use the . CSS selector when you want to use the class name:

1
2
3
4
5
// verify filters
it ('verify filters', function(){
cy.get('.todo-count')
.should('equal', 1)
})

Tip: you can find examples of all querying commands and selectors on my Cypress Querying Examples page.

Now the assertion part. When you use cy.get and other querying commands, you get a jQuery object. Can a jQuery object equal 1? No. Only a number can equal another number 1. You probably want to confirm that the text inside the found element is "1" (notice the type - everything on the page is a string). You could write the assertion like this:

1
2
3
4
5
// verify filters
it ('verify filters', function(){
cy.get('.todo-count')
.should('include.text', '1')
})

Even better is to limit ourselves exactly to the element with the number:

1
2
3
4
5
// verify filters
it('verify filters', function () {
cy.get('.new-todo').type('todo A{enter}')
cy.get('.todo-count [data-cy=remaining-count]').should('have.text', '1')
})

Confirm the count of one

Confirm text from multiple items

Q: How do I capture both items using one line of code?

1
2
- jan
- feb

So we want to get all todos on the page, extract their text content, and confirm these strings are "jan" and "feb". There are a couple of ways to write it. Let's use should(callback) assertion

1
2
3
4
5
6
cy.get('.todo-list .todo')
.should('have.length', 2)
.should($li => {
expect($li[0]).to.have.text('jan')
expect($li[1]).to.have.text('feb')
})

Ok, let's write it in a shorter way, since we want a one-liner. We want to extract text from each element and confirm the array of strings. We can write the following:

1
2
3
4
cy.get('.todo-list .todo')
.should('have.length', 2)
.then($li => Cypress._.map($li, 'innerText'))
.should('deep.equal', ['jan', 'feb'])

Unfortunately, cy.then is not querying command, thus we are forced to confirm the length 2 before using cy.then. Ok, no big deal, we can bring in my cypress-map plugin and really shorten this test:

1
2
3
4
// https://github.com/bahmutov/cypress-map
import 'cypress-map'

cy.get('.todo-list .todo').map('innerText').should('deep.equal', ['jan', 'feb'])

Checking the text from found items

Tip: for more about cypress-map and Cypress query commands, read my blog post Cypress V12 Is A Big Deal.

Confirming the text

Q: Why does the .and('have.string', 'first item') fail?

1
2
3
4
5
cy.get('.new-todo').type('first item{enter}')
cy.contains('li.todo', 'first item')
.should('be.visible')
.and('have.length', 1)
.and('have.string', 'first item')

Cypress comes with Chai.js, Chai-jQuery, and Sinon-Chai assertion libraries. Each assertion tries to confirm the value / type of the subject. The assertion have.string expects the subject value to be a string, but the subject yielded by the cy.contains command is a jQuery object

jQuery object is not a string

So what do we need to use to confirm the element's text? Look at the assertions on the Assertions example page.

The assertions Cypress examples page

Let's rewrite the test. We can use include.text assertion

1
2
3
4
5
cy.get('.new-todo').type('first item{enter}')
cy.contains('li.todo', 'first item')
.should('be.visible')
.and('have.length', 1)
.and('include.text', 'first item')

Include text assertion works

If you look at the cy.contains command documentation page, notice it always returns just a single item, and that item includes the given text. Thus the same test can be written simply:

1
2
cy.get('.new-todo').type('first item{enter}')
cy.contains('li.todo', 'first item').should('be.visible')