Learn Cypress in 10 DaysDay 3: Selectors and DOM Interaction

Day 3: Selectors and DOM Interaction

What You'll Learn Today

  • CSS selector fundamentals (ID, class, tag, attribute)
  • Using cy.get() in depth
  • Best practices with data-cy / data-testid attributes
  • DOM traversal commands (find, parent, children, siblings)
  • Filtering elements (first, last, eq)
  • Scoping with cy.within()
  • Click and text input DOM interactions

CSS Selector Fundamentals

Cypress uses CSS selectors to locate elements. Let's review the basics of CSS selectors.

flowchart TB
    subgraph Selectors["CSS Selector Types"]
        ID["#id\nID Selector"]
        CLASS[".class\nClass Selector"]
        TAG["tag\nTag Selector"]
        ATTR["[attr=val]\nAttribute Selector"]
    end
    style Selectors fill:#3b82f6,color:#fff
Selector Syntax Example Description
ID #id #login-btn Select element by unique ID
Class .class .btn-primary Select elements by class name
Tag tag button Select elements by HTML tag name
Attribute [attr=value] [type="submit"] Select elements by attribute value
Descendant parent child form input Select descendant elements within a parent
Direct child parent > child ul > li Select only direct children
Compound .class[attr] .btn[disabled] Combine multiple conditions
// ID selector
cy.get('#username')

// Class selector
cy.get('.submit-button')

// Tag selector
cy.get('h1')

// Attribute selectors
cy.get('[type="email"]')
cy.get('[name="password"]')

// Compound selectors
cy.get('input[type="text"]')
cy.get('button.primary[type="submit"]')

// Descendant selectors
cy.get('.form-group input')
cy.get('nav > ul > li')

Using cy.get() in Depth

cy.get() is the most frequently used command in Cypress. It accepts a CSS selector and returns matching elements.

// Basic usage
cy.get('button')           // All <button> elements
cy.get('.error-message')   // Elements with class="error-message"
cy.get('#submit')          // Element with id="submit"

// Custom timeout
cy.get('.loading', { timeout: 10000 }) // Wait up to 10 seconds

// When multiple elements are returned
cy.get('li')               // Get all <li> elements
cy.get('li').should('have.length', 5)  // Verify there are 5 <li> elements

cy.get() vs cy.contains()

// cy.get() - Select elements by CSS selector
cy.get('.nav-link')

// cy.contains() - Select elements by text content
cy.contains('Login')
cy.contains('button', 'Submit')  // Find a <button> containing "Submit"

// Regular expressions work too
cy.contains(/^Total: \d+ items$/)
Command Purpose Characteristics
cy.get() Select by CSS selector Structure-based selection
cy.contains() Select by text content User-perspective selection

Best Practices with data-cy / data-testid

CSS classes and IDs can change due to design updates or refactoring. Using test-specific attributes greatly improves test stability.

flowchart TB
    subgraph Bad["Selectors to Avoid"]
        B1["Tag name\n(button)"]
        B2["CSS class\n(.btn-primary)"]
        B3["ID\n(#main-btn)"]
    end
    subgraph Good["Recommended Selectors"]
        G1["data-cy\n[data-cy=submit]"]
        G2["data-testid\n[data-testid=submit]"]
    end
    Bad -->|"Prone to change"| FRAGILE["Fragile tests"]
    Good -->|"Resilient to change"| STABLE["Stable tests"]
    style Bad fill:#ef4444,color:#fff
    style Good fill:#22c55e,color:#fff
    style FRAGILE fill:#ef4444,color:#fff
    style STABLE fill:#22c55e,color:#fff

Adding data-cy Attributes to HTML

<!-- Recommended: Use test-specific attributes -->
<button data-cy="submit-btn" class="btn btn-primary">Submit</button>
<input data-cy="email-input" type="email" class="form-control" />
<div data-cy="error-message" class="alert alert-danger">An error occurred</div>

Using Them in Cypress

// Select elements by data-cy attribute
cy.get('[data-cy="submit-btn"]').click()
cy.get('[data-cy="email-input"]').type('user@example.com')
cy.get('[data-cy="error-message"]').should('be.visible')

Selector Strategy Priority

Priority Selector Reason
1 (Most recommended) [data-cy="..."] Test-specific. Unaffected by other changes
2 [data-testid="..."] Compatible with Testing Library
3 #id Unique, but may be used by JS or CSS
4 .class Can break with design changes
5 (Not recommended) tag Too broad and unreliable

The Cypress team officially recommends using data-cy attributes. This cleanly separates the concerns of test code and production code.


DOM Traversal Commands

Starting from an element obtained with cy.get(), let's explore commands for traversing the DOM tree.

flowchart TB
    subgraph DOM["DOM Tree"]
        PARENT["parent()\nParent element"]
        CURRENT["Current element"]
        CHILD1["children()\nChild 1"]
        CHILD2["children()\nChild 2"]
        SIBLING["siblings()\nSibling element"]
        FIND["find()\nDescendant element"]
    end
    PARENT --> CURRENT
    CURRENT --> CHILD1
    CURRENT --> CHILD2
    CURRENT --- SIBLING
    CHILD1 --> FIND
    style CURRENT fill:#3b82f6,color:#fff
    style PARENT fill:#8b5cf6,color:#fff
    style CHILD1 fill:#22c55e,color:#fff
    style CHILD2 fill:#22c55e,color:#fff
    style SIBLING fill:#f59e0b,color:#fff
    style FIND fill:#22c55e,color:#fff

cy.find() - Search Descendant Elements

// Difference from cy.get(): find() searches within the current element
cy.get('.user-card').find('.username')    // .username inside .user-card
cy.get('form').find('input[type="text"]') // text input inside form

// cy.get() searches from the entire document
cy.get('.username')  // Searches all of .username on the page

cy.parent() and cy.parents() - Navigate to Parent Elements

// Direct parent element
cy.get('.error-text').parent()

// Ancestor matching a condition
cy.get('.error-text').parents('.form-group')

// Get the closest ancestor (like closest())
cy.get('.error-text').closest('.card')

cy.children() - Get Child Elements

// All child elements
cy.get('ul.menu').children()

// Child elements matching a condition
cy.get('ul.menu').children('.active')

cy.siblings() - Get Sibling Elements

// All sibling elements
cy.get('.active-tab').siblings()

// Sibling elements matching a condition
cy.get('.active-tab').siblings('.disabled')

Filtering Elements

When multiple elements are returned, use these commands to narrow down to specific ones.

// First element
cy.get('li').first()

// Last element
cy.get('li').last()

// Nth element (0-based index)
cy.get('li').eq(0)    // 1st element
cy.get('li').eq(2)    // 3rd element
cy.get('li').eq(-1)   // Last element

// Filtering
cy.get('li').filter('.active')      // Only li elements with .active class
cy.get('li').not('.disabled')       // Only li elements without .disabled class
Command Description Example
first() First element cy.get('li').first()
last() Last element cy.get('li').last()
eq(index) Nth element cy.get('li').eq(2)
filter(selector) Elements matching a condition cy.get('li').filter('.done')
not(selector) Elements not matching a condition cy.get('li').not('.skip')

Scoping with cy.within()

cy.within() lets you scope commands to a specific element. This is especially useful on pages with repeating structures.

<div data-cy="login-form">
  <input name="email" />
  <input name="password" />
  <button>Login</button>
</div>

<div data-cy="register-form">
  <input name="email" />
  <input name="password" />
  <button>Register</button>
</div>
// Without within() - ambiguous which email field is targeted
cy.get('[name="email"]')  // Matches two elements

// Scoping with within()
cy.get('[data-cy="login-form"]').within(() => {
  cy.get('[name="email"]').type('user@example.com')
  cy.get('[name="password"]').type('secret123')
  cy.get('button').click()
})

// Operating on a different form
cy.get('[data-cy="register-form"]').within(() => {
  cy.get('[name="email"]').type('newuser@example.com')
  cy.get('[name="password"]').type('newpassword')
  cy.get('button').click()
})
flowchart TB
    subgraph Page["Entire Page"]
        subgraph Login["login-form Scope"]
            LE["email input"]
            LP["password input"]
            LB["Login button"]
        end
        subgraph Register["register-form Scope"]
            RE["email input"]
            RP["password input"]
            RB["Register button"]
        end
    end
    style Login fill:#3b82f6,color:#fff
    style Register fill:#8b5cf6,color:#fff

Click Interactions

Cypress provides three types of click interactions.

// Standard click
cy.get('[data-cy="submit-btn"]').click()

// Double click
cy.get('[data-cy="item"]').dblclick()

// Right click (context menu)
cy.get('[data-cy="file"]').rightclick()

click() Options

// Click at a specific position on the element
cy.get('.map').click('topLeft')
cy.get('.map').click('center')       // Default
cy.get('.map').click('bottomRight')

// Click at specific coordinates
cy.get('.canvas').click(100, 200)

// Click multiple elements in sequence
cy.get('.checkbox').click({ multiple: true })

// Click even if the element is covered (use with caution)
cy.get('.hidden-btn').click({ force: true })
Option Description Example
position Click position click('topLeft')
x, y Coordinate-based click(100, 200)
multiple Click all matching elements click({ multiple: true })
force Force click click({ force: true })

Do not use force: true as a quick fix when tests fail. Only use it when the UI element is genuinely obscured.


Text Input Interactions

cy.type() - Enter Text

// Basic input
cy.get('[data-cy="email"]').type('user@example.com')

// Special key input
cy.get('[data-cy="search"]').type('Cypress{enter}')  // Enter key
cy.get('[data-cy="name"]').type('{selectall}{backspace}')  // Select all and delete
cy.get('[data-cy="input"]').type('{ctrl+a}')  // Ctrl+A

// Adjust typing speed (default is 10ms)
cy.get('[data-cy="input"]').type('slow typing', { delay: 100 })

Special Keys Reference

Key Syntax Description
Enter {enter} Enter key
Tab {tab} Tab key (may require a plugin)
Escape {esc} Escape key
Backspace {backspace} Delete one character
Delete {del} Delete key
Select All {selectall} Select all text
Up Arrow {uparrow} Up arrow key
Down Arrow {downarrow} Down arrow key

cy.clear() - Clear Input

// Clear an input field
cy.get('[data-cy="email"]').clear()

// Clear and type a new value
cy.get('[data-cy="email"]').clear().type('new@example.com')

Other Input Interactions

// Select dropdown
cy.get('[data-cy="country"]').select('United States')
cy.get('[data-cy="country"]').select('us')       // Select by value

// Checkbox
cy.get('[data-cy="agree"]').check()
cy.get('[data-cy="agree"]').uncheck()

// Radio button
cy.get('[data-cy="plan"]').check('premium')       // Specify value

Practical Example: User Registration Form Test

Let's combine everything we've learned to write a practical test.

describe('User Registration Form', () => {
  beforeEach(() => {
    cy.visit('/register')
  })

  it('can fill in all fields and register', () => {
    cy.get('[data-cy="register-form"]').within(() => {
      // Text inputs
      cy.get('[data-cy="username"]').type('testuser')
      cy.get('[data-cy="email"]').type('test@example.com')
      cy.get('[data-cy="password"]').type('SecurePass123!')
      cy.get('[data-cy="password-confirm"]').type('SecurePass123!')

      // Select dropdown
      cy.get('[data-cy="country"]').select('United States')

      // Checkbox
      cy.get('[data-cy="terms"]').check()

      // Click submit button
      cy.get('[data-cy="submit"]').click()
    })

    // Verify success message
    cy.get('[data-cy="success-message"]')
      .should('be.visible')
      .and('contain', 'Registration successful')
  })

  it('shows errors when required fields are empty', () => {
    cy.get('[data-cy="register-form"]').within(() => {
      // Submit without filling in any fields
      cy.get('[data-cy="submit"]').click()

      // Verify error messages
      cy.get('[data-cy="error-username"]')
        .should('be.visible')
        .and('have.text', 'Username is required')

      cy.get('[data-cy="error-email"]')
        .should('be.visible')
    })
  })

  it('shows an error for invalid email format', () => {
    cy.get('[data-cy="register-form"]').within(() => {
      cy.get('[data-cy="email"]').type('invalid-email')
      cy.get('[data-cy="submit"]').click()

      cy.get('[data-cy="error-email"]')
        .should('contain', 'Please enter a valid email address')
    })
  })
})

Summary

Concept Description
CSS Selectors Identify elements using combinations of ID, class, tag, and attribute
data-cy attribute Achieve stable selectors with test-specific attributes
cy.get() The fundamental command for selecting DOM elements by CSS selector
DOM Traversal Navigate between elements with find, parent, children, siblings
Filtering Elements Narrow targets with first, last, eq, filter, not
cy.within() Scope interactions to a specific element
Click interactions Three types: click, dblclick, rightclick
Text input Perform input with type, clear, select, check/uncheck

Key Points

  1. Use data-cy attributes - Relying on CSS classes or IDs means your tests break when the design changes. Test-specific attributes dramatically improve maintainability.
  2. Use cy.within() for clear scoping - On pages with repeating structures, use within() to scope your commands and make your intent explicit.
  3. force: true is a last resort - When a test fails, investigate the UI issue first. Only use the force option when absolutely necessary.

Practice Exercises

Exercise 1: Basic

Write a Cypress test for the login form described by the following HTML.

<form id="login-form">
  <input data-cy="login-email" type="email" />
  <input data-cy="login-password" type="password" />
  <button data-cy="login-submit" type="submit">Login</button>
</form>

Exercise 2: Applied

On a product listing page, write a test that clicks the "Add to Cart" button on the third product card. Each product card has the .product-card class and contains an [data-cy="add-to-cart"] button.

Challenge Exercise

Create a test suite that meets the following requirements:

  • Enter a keyword in the search form and press Enter to search
  • Verify that 5 search results are displayed
  • Click the first result to navigate to the detail page
  • Click the "Favorite" button on the detail page
  • Verify that an "Added to favorites" message is displayed

References


Next Up: In Day 4, we'll learn about mastering assertions. You'll understand how to use should() and expect(), chain assertions, and leverage the retry mechanism to write reliable tests.