- Use `cy.get('[data-cy=submit-btn]')` for element selection — never rely on CSS classes or positional selectors that break on style changes.
- Stub network requests with `cy.intercept('POST', '/api/login', { statusCode: 200, body: { token: 'fake' } })` to isolate tests from the backend.
- Write self-documenting tests with `cy.contains('button', 'Sign In').click()` chained assertions rather than custom command soup.
- Use `beforeEach(() => cy.login())` with a custom `cy.login()` command that calls the API directly instead of filling the login form in every test.
- Run `cypress run --spec 'cypress/e2e/checkout.cy.ts'` in CI to target specific feature suites.
- Select elements with `data-cy` attributes: `cy.get('[data-cy=email-input]').type('user@example.com')` — avoid coupling tests to CSS classes, IDs, or DOM structure.
- Intercept and stub HTTP calls with `cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')` then assert with `cy.wait('@getUsers').its('response.statusCode').should('eq', 200)`.
- Create custom commands for repeated workflows in `cypress/support/commands.ts`: `Cypress.Commands.add('login', (email, password) => { cy.request('POST', '/api/auth/login', { email, password }).its('body.token').then((t) => cy.setCookie('token', t)) })`.
- Use `cy.fixture('user.json')` to load test data and avoid hardcoded strings scattered across test files.
- Test error states explicitly: `cy.intercept('GET', '/api/profile', { statusCode: 500 }).as('profileError'); cy.wait('@profileError'); cy.get('[data-cy=error-banner]').should('be.visible')`.
- Avoid arbitrary `cy.wait(3000)` — use `cy.wait('@alias')` for network requests or `cy.get('[data-cy=spinner]').should('not.exist')` for DOM conditions.