How to Test Lists of Items in Cypress
Test Cypress lists by checking their count, content, and order with retryable queries. See fixture patterns, async loading, troubleshooting, and runnable examples.
Use a stable selector for the repeated elements and assert the expected count with Cypress’s retryable .should('have.length', n). Then verify text and order if those are part of the requirement. Keeping the query and assertions in a Cypress chain lets the test wait for an asynchronously rendered list; a .then() callback runs once and is not a wait mechanism.
1. Select list items with a stable selector
Prefer a dedicated testing attribute such as data-cy over a styling class. Styling changes should not require rewriting tests.
<ul>
<li data-cy="todo-item">Buy milk</li>
<li data-cy="todo-item">Walk the dog</li>
<li data-cy="todo-item">Write JavaScript</li>
</ul>
Query the repeated elements with cy.get('[data-cy="todo-item"]'). Scope the selector to a particular list when the page has multiple collections, for example cy.get('[data-cy="active-todos"] [data-cy="todo-item"]').
2. Assert the expected item count
cy.get('[data-cy="todo-item"]').should('have.length', 3)
The linked query and assertion retry while Cypress waits for the matching list to reach three items, up to the applicable command timeout. Use the exact count when the expected behavior defines one. If the requirement is a range or minimum, assert that condition explicitly rather than choosing an arbitrary exact count.
Prefer a positive assertion describing the correct state. A negative assertion like “not length 4” could pass if the list is empty or otherwise broken. Cypress discusses this risk in its Assertions documentation.
3. Check content and order
For a few meaningful positions, use .first() and .eq(index):
cy.get('[data-cy="todo-item"]').first().should('contain', 'Buy milk')
cy.get('[data-cy="todo-item"]').eq(1).should('contain', 'Walk the dog')
cy.get('[data-cy="todo-item"]').eq(2).should('contain', 'Write JavaScript')
For a short list, a single retryable callback can check count and ordered contents together:
cy.get('[data-cy="todo-item"]').should(($items) => {
expect($items).to.have.length(3)
expect($items.eq(0)).to.contain('Buy milk')
expect($items.eq(1)).to.contain('Walk the dog')
expect($items.eq(2)).to.contain('Write JavaScript')
})
For longer lists, map rendered text into a plain array and compare the sequence:
const expected = ['Buy milk', 'Walk the dog', 'Write JavaScript']
cy.get('[data-cy="todo-item"]').should(($items) => {
const actual = $items.map((_, item) => item.innerText.trim()).get()
expect(actual).to.deep.equal(expected)
})
This verifies the number of rows, their text, and their order. Cypress may call a .should(callback) callback repeatedly, so keep it limited to assertions and pure inspection. Do not click, mutate data, or trigger other side effects inside it.
4. Wait for a list that loads asynchronously
Keep the collection query and expected state in a retryable chain. Cypress retries linked queries and .should() assertions until they pass or time out.
// Good: the query and assertion can be retried as the list renders.
cy.get('[data-cy="todo-item"]').should('have.length', 3)
// Avoid for an asynchronously filling list: .then() runs once.
cy.get('[data-cy="todo-item"]').then(($items) => {
expect($items).to.have.length(3)
})
If loading is tied to a request, wait for that request and still assert the rendered result. Intercept before the page action that triggers the request:
cy.intercept('GET', '/api/todos').as('getTodos')
cy.visit('/todos')
cy.wait('@getTodos')
cy.get('[data-cy="todo-item"]').should('have.length', 3)
The request wait makes the network event explicit; the DOM assertion confirms the user-visible list actually rendered. If the application renders progressively, use the DOM assertion as the readiness condition rather than a fixed sleep.
5. Compare rows against fixture data
Fixtures provide stable expected records and can also be used to stub responses. For a fixture at cypress/fixtures/users.json:
[
{ "email": "ada@example.com" },
{ "email": "grace@example.com" }
]
cy.fixture('users.json').then((users) => {
cy.get('[data-cy="user-row"]').should('have.length', users.length)
cy.get('[data-cy="user-email"]')
.each(($email, index) => {
expect($email).to.have.text(users[index].email)
})
})
For a fully controlled page, stub the endpoint with the fixture before visiting:
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
cy.fixture('users.json').then((users) => {
cy.get('[data-cy="user-row"]').should('have.length', users.length)
})
Cypress notes that a TypeScript type parameter on cy.fixture<User[]>() describes the yielded value but does not validate that the JSON file conforms to that type. Fixtures are read within a test; they do not create test blocks. To generate one test per known record, import the data when defining the tests. See the Cypress fixture documentation and test organization guide.
6. Choose the assertion pattern
| Need | Pattern | What it checks |
|---|---|---|
| Confirm exact collection size | .should('have.length', n) |
Expected cardinality, with retrying |
| Check a few positions | .first() or .eq(i) with a text assertion |
Specific row at a specific position |
| Check the whole sequence | Map text and deep-compare with an array | Membership, content, and order |
| Check known records | Fixture plus DOM assertions | Stable expected data independent of a changing backend |
| Generate cases from records | Import data while defining tests | One test block per known record |
7. Run the same checks for each item
When each row must satisfy the same condition, use .each() for per-item assertions. Keep the collection-level count assertion too, so a missing row cannot go unnoticed.
cy.get('[data-cy="user-row"]')
.should('have.length', 2)
.each(($row) => {
expect($row).to.be.visible
expect($row.find('[data-cy="user-email"]').text()).to.match(/@example\.com$/)
})
For large fixture files or Node-side file processing, Cypress recommends using cy.task() for server-side work and returning only the data the test needs. Avoid loading or transforming a large dataset in the browser test when a small derived result is sufficient.
8. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Count assertion times out | Wrong selector, wrong expected count, failed request, or the list never reaches the asserted state | Confirm the selector in the rendered DOM; inspect the triggering request; assert the real expected count |
| Assertion passes with the wrong list | A weak negative assertion, such as “not length 4” | Assert the exact intended length or a precise range, then check content |
| Test sees too few items once | A one-shot .then() check runs before rendering finishes |
Use a retryable .should() assertion in the query chain |
| Correct count, wrong rows or order | The test checks cardinality only | Compare each meaningful position or deep-compare the complete text array |
| Test breaks after CSS changes | Selector relies on a presentation class | Add a stable data-cy attribute and query it |
| Callback causes duplicate actions | A side effect is inside a retryable .should(callback) |
Keep callbacks pure; perform actions in separate Cypress commands |
| Fixture-backed test is flaky | Fixture stub is registered after the page request, or expected records differ from the response | Register cy.intercept() before cy.visit() and use the same fixture for expected values |
9. Performance and reliability
- Use a focused selector scoped to the relevant list to reduce ambiguity and avoid querying unrelated elements.
- Assert the smallest meaningful state: count plus the content/order requirements that protect the feature. Avoid repeatedly re-querying the entire page for every field when a row-scoped check is clearer.
- Prefer request interception and deterministic fixtures when the test is about rendering known records. This avoids relying on changing backend data.
- Avoid arbitrary waits where a retryable DOM condition or a request alias describes readiness more directly.
- For large datasets, test representative rendering behavior and use server-side processing through
cy.task()when file work is substantial.
Cypress retries linked queries from the top of the query chain and retries assertions until they pass or time out. See its retry-ability guide, cy.get() reference, cy.should() reference, and introduction for the documented behaviors behind these patterns.
10. Or skip the browser setup
If your workflow also needs screenshots of the page or list for review, bug reports, or agent workflows, ScreenshotNeo is a website screenshot API and MCP server. It complements Cypress assertions; a screenshot does not replace checking the DOM.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up free for 1,000 screenshots a month, with no card required.
FAQ
Can I assert that a list has at least one item?
Yes. Use an assertion that expresses the actual requirement, such as a positive minimum-length check, and separately verify expected content if item identity matters.
Does a fixture create one Cypress test per record?
No. A fixture supplies data inside a running test. Import data while defining the suite if you need to create separate test blocks for each record.
Should I check the exact text or just that it contains a phrase?
Use exact text when the whole label is part of the contract. Use a containment assertion when surrounding text may legitimately vary.


