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-cyattributes. 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: trueas 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
- Use data-cy attributes - Relying on CSS classes or IDs means your tests break when the design changes. Test-specific attributes dramatically improve maintainability.
- Use cy.within() for clear scoping - On pages with repeating structures, use within() to scope your commands and make your intent explicit.
- 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
- Cypress Best Practices - Selecting Elements
- cy.get() - Cypress Documentation
- cy.type() - Cypress Documentation
- cy.within() - Cypress Documentation
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.