ScreenshotNeo

BlogHow-to

How to Make Cypress Recognize List Elements

Use Cypress CSS selectors, scoped queries, text filters, stable data attributes, and retry-safe patterns to test every kind of list item.

By the ScreenshotNeo team30 September 20265 min read

How to Make Cypress Recognize List Elements

Direct answer: Cypress recognizes list elements through normal CSS selectors. Use cy.get('ul li') for every descendant list item, cy.get('ul > li') for direct children, cy.get('#list').find('li') for one list, and cy.contains('li', 'Banana') for one item by visible text. For selectors that survive copy and styling changes, add a dedicated attribute such as data-cy.

1. Select list items with CSS

cy.get() starts at the application document and yields every element matching its selector. Cypress documents list selection with selectors such as .list > li. Queries retry until the elements and chained assertions exist. See the cy.get() documentation.

A CSS query narrows a page to the list elements the test should inspect.
A CSS query narrows a page to the list elements the test should inspect.
describe('lists', () => {
  it('selects list items', () => {
    cy.visit('/shopping')
    cy.get('ul li').should('have.length', 3)
    cy.get('ul > li').should('be.visible')
  })
})

Descendants versus direct children

Need Selector Matches
Every item under a list ul li Nested items too
Only top-level items ul > li Immediate children
Items with a class li.todo Matching list items

2. Scope the query to one list

Chain .find() from a command that yields a DOM subject. It searches descendants of that subject. Cypress describes this behavior in the cy.find() documentation.

cy.get('#shopping-list').find('li').should('have.length', 4)

cy.get('[data-cy=primary-list]').within(() => {
  cy.get('li').should('have.length.at.least', 1)
})

.find() cannot be called directly from cy; it needs a parent subject.

3. Prefer stable test attributes

Cypress recommends dedicated data-* selectors because text and classes often change during redesigns or translation.

<ul data-cy="todo-list">
  <li data-cy="todo-item" data-id="42">Buy fruit</li>
  <li data-cy="todo-item" data-id="43">Ship order</li>
</ul>
cy.get('[data-cy=todo-item]').should('have.length', 2)
cy.get('[data-cy=todo-item][data-id="42"]').should('contain.text', 'Buy fruit')

4. Find an item by text

cy.contains() accepts a selector and text, preventing an unrelated ancestor from becoming the match:

cy.contains('li', 'Banana').should('be.visible').click()
cy.contains('li', /^Banana$/).should('exist')

cy.contains() yields at most one element. Matching is substring-based and case-sensitive; Cypress collapses runs of whitespace except in <pre>. Use an anchored regular expression for an exact match. See the cy.contains() documentation.

Find every item containing text

cy.get('li').filter(':contains("Banana")').should('have.length', 2)

The jQuery :contains() filter is case-sensitive. See cy.filter().

5. Select the first item correctly

To select the first child from each list, use :first-child:

Direct-child and descendant selectors produce different collections.
Direct-child and descendant selectors produce different collections.
cy.get('ul li:first-child').should('have.length', 2)
cy.get('#shopping-list li').first().should('be.visible')
cy.get('#shopping-list li').eq(2).should('contain.text', 'Milk')

jQuery’s :first returns only the first result overall, so it is different from :first-child.

6. Iterate over a collection

Use .each() when every current item needs an assertion or action:

cy.get('ul > li').each(($li) => {
  cy.wrap($li).should('be.visible').and('not.have.text', '')
})

.each() is not a query and does not retry its callback. If rendering replaces nodes, re-query by a stable key:

cy.get('[data-cy=todo-item]').each(($li) => {
  const id = $li.attr('data-id')
  cy.get(`[data-cy=todo-item][data-id="${id}"]`).click()
})

See cy.each().

7. Shadow DOM and iframes

For list items inside a shadow root, enable traversal:

cy.get('todo-list', { includeShadowDom: true }).find('li', { includeShadowDom: true })

cy.get() does not descend into iframe documents. Switch to the iframe document with an iframe helper or explicit DOM access, then query its body.

8. Common failures and fixes

Symptom Cause Fix
find is not a function .find() was called from cy. Start with cy.get('ul').find('li').
Only one text match cy.contains() yields one element. Use cy.get('li').filter(':contains("text")').
Wrong ancestor matched Text exists in a parent too. Use cy.contains('li', 'text').
Only one first item found :first selects one overall. Use ul li:first-child.
Selector breaks after copy changes It depends on text or styling classes. Add a data-cy attribute.
Text assertion fails Whitespace, case, or locale differs. Inspect rendered text; use a scoped regex or attribute.
Detached element in .each() The app replaced nodes. Re-query before acting.
No items in iframe Queries do not cross iframe boundaries. Query the iframe document.
Shadow items missing Shadow traversal is disabled. Pass includeShadowDom: true.

9. Reliability and performance

  • Use the narrowest selector, such as #shopping-list > li.
  • Assert counts when the count is part of the behavior.
  • Let Cypress retry queries instead of adding arbitrary waits.
  • After mutations, re-query current DOM elements.
  • Use text for user-facing behavior and data attributes for stable identity.
  • Scope or filter collections before expensive assertions.

See Cypress’s core concepts for retrying, querying, and internationalization guidance.

10. Complete example

describe('todo list', () => {
  beforeEach(() => cy.visit('/todos'))

  it('recognizes list elements', () => {
    cy.get('[data-cy=todo-list] > li')
      .should('have.length', 3)
      .each(($item) => cy.wrap($item).should('be.visible'))

    cy.contains('[data-cy=todo-item]', 'Buy fruit')
      .should('exist').click()

    cy.get('[data-cy=todo-item]')
      .filter(':contains("Buy")').should('have.length', 1)
  })
})

11. Or skip the browser setup

If you need a rendered list image or PDF rather than an interactive end-to-end test, ScreenshotNeo captures a page with one GET request. Before capture it accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off.

Only clean shots are billed. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and X-Page-Verdict and X-Billed headers identify the result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API docs:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/todos -o todos.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/todos"}, timeout=90)
open("todos.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/todos' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

1,000 screenshots each month are free with no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Should I use ul li or ul > li?

Use ul li when nested items belong in the result. Use ul > li for top-level entries only.

Can Cypress select by index?

Yes. Select the collection, then call .eq(index) or .first().

How do I assert every item has text?

Use .each() with cy.wrap($item), or assert the collection’s combined text.

Why does a translated test fail?

Visible text changes by locale. Use a stable data-cy attribute for identity and text assertions only for localized behavior.