diff --git a/docs/app/core-concepts/best-practices.mdx b/docs/app/core-concepts/best-practices.mdx index 27b9f3e0f2..d74802d861 100644 --- a/docs/app/core-concepts/best-practices.mdx +++ b/docs/app/core-concepts/best-practices.mdx @@ -951,6 +951,193 @@ cy.wait('@getUsers') // <--- wait explicitly for this route to finish cy.get('table tr').should('have.length', 2) ``` +## Element Interaction Patterns and Command Chaining + +:::danger + + **Anti-Pattern:** Using `then()` +unnecessarily or re-querying the same element multiple times when chaining would +work. + +::: + +:::tip + + **Best Practice:** Use command chaining +when possible, and only use `then()` when you need to perform complex operations +or work with the actual DOM element. + +::: + +A common question arises when performing multiple actions on the same element: should you chain commands or use `then()` to wrap all actions? Let's examine the best approaches for common interaction patterns. + +### Multiple Actions on the Same Element + +When you need to perform several actions on the same element (like focus, clear, type, and blur), you have two main approaches. Remember that Cypress commands are retry-able, but actions like `.click()`, `.type()`, etc. should be at the end of command chains to maintain retry-ability: + +#### Option 1: Command Chaining (Recommended) + +```js +// RECOMMENDED: Chain commands with assertions before actions +cy.get('[data-cy="input-field"]') + .should('be.visible') + .should('not.be.disabled') + .focus() + +// Each action gets its own chain to ensure retry-ability +cy.get('[data-cy="input-field"]').clear() +cy.get('[data-cy="input-field"]') + .type('new value') + .should('have.value', 'new value') + +cy.get('[data-cy="input-field"]').blur() +``` + +#### Option 2: Using then() (Only when necessary) + +```js +// ONLY USE when you need to work with the actual DOM element +cy.get('[data-cy="input-field"]').then(($input) => { + cy.wrap($input).focus() + cy.wrap($input).clear() + cy.wrap($input).type('new value') + cy.wrap($input).blur() +}) +``` + +### Why Command Chaining is Usually Better + +**Performance Benefits:** + +- Cypress optimizes chained commands and doesn't re-query the element unnecessarily. +- The element reference is passed through the command chain automatically. +- No additional overhead from `cy.wrap()` calls. + +**Simplicity and Readability:** + +- More concise and easier to read. +- Follows Cypress's natural command pattern. +- Less nested code structure. + +**Automatic Retries:** + +- Each command in the chain automatically retries if the element becomes stale. +- Cypress handles DOM updates and element re-querying automatically. + +### When to Use `then()` + +Use `then()` only when you need to: + +1. **Access DOM element properties or methods directly:** + +```js +cy.get('[data-cy="input-field"]').then(($input) => { + const initialValue = $input.val() + // Do something with the initial value + if (initialValue !== '') { + cy.wrap($input).clear().type('new value') + } +}) +``` + +2. **Perform conditional logic based on element state:** + +```js +cy.get('[data-cy="toggle"]').then(($toggle) => { + if ($toggle.hasClass('active')) { + cy.wrap($toggle).click() + } +}) +``` + +3. **Work with multiple elements from the same query:** + +```js +cy.get('[data-cy="list-item"]').then(($items) => { + const count = $items.length + cy.log(`Found ${count} items`) + + // Work with specific items + cy.wrap($items.first()).should('contain', 'First item') + cy.wrap($items.last()).should('contain', 'Last item') +}) +``` + +### Common Misconceptions + +**"Using `then()` prevents detached DOM errors"** + +This is not accurate. Cypress automatically handles element re-querying in both chaining and `then()` scenarios. The framework is designed to handle DOM updates gracefully. + +**"Element is found only once with `then()`"** + +While `then()` captures the element at that moment, Cypress commands within `then()` (like those wrapped with `cy.wrap()`) still perform their own element queries and retries as needed. + +### Best Practice Examples + +```javascript +describe('form interaction', () => { + it('updates input field correctly', () => { + cy.get('[data-cy="email-input"]').as('emailInput') + cy.get('@emailInput').should('be.visible').focus() + cy.get('@emailInput').clear() + cy.get('@emailInput') + .type('user@example.com') + .should('have.value', 'user@example.com') + cy.get('@emailInput').blur() + }) +}) +``` + +#### Conditional Actions (When `then()` is appropriate) + +```js +describe('conditional interactions', () => { + it('handles different input states', () => { + cy.get('[data-cy="search-input"]').then(($input) => { + const currentValue = $input.val() + + if (currentValue) { + // Clear existing value and enter new one + cy.wrap($input).clear().type('new search term') + } else { + // Just enter the new value + cy.wrap($input).type('new search term') + } + }) + }) +}) +``` + +#### Working with Element Collections + +```javascript +describe('element collections', () => { + it('processes multiple items', () => { + cy.get('[data-cy="product-card"]').then(($cards) => { + // Process each card + $cards.each((index, card) => { + cy.wrap(card) + .should('be.visible') + .find('[data-cy="product-title"]') + .should('not.be.empty') + }) + }) + }) +}) +``` + +### Performance Considerations + +Command chaining is typically more performant because: + +- Cypress optimizes the command queue for chained operations. +- No additional `cy.wrap()` overhead. +- Automatic smart retries and element resolution. +- Better memory management with fewer closures. + +Use `then()` judiciously and only when the additional functionality it provides is necessary for your specific use case. + ## Running Tests Intelligently As your test suite grows and takes longer to run, you may find yourself hitting