Stable Cypress tests depend on stable selectors. CSS class selectors ('.submit-btn') break when designers rename classes. Text selectors (cy.contains('Submit')) break when copy is updated. data-cy attributes are the recommended approach — they are specifically for testing and survive both CSS refactoring and copy changes. Angular Testing Library commands (cy.findByRole, cy.findByLabelText) provide the most user-centric selectors, testing what users actually interact with.
Selectors, Interactions and Assertions
// ── Add data-cy attributes to Angular templates ───────────────────────────
// post-card.component.html:
// <article data-cy="post-card">
// <h2 data-cy="post-title">{{ post.title }}</h2>
// <button data-cy="read-more-btn">Read More</button>
// </article>
// ── Selector strategies (best to worst) ──────────────────────────────────
// BEST: data-cy attributes (testing-specific, stable)
cy.getByCy('post-card');
cy.get('[data-cy="submit-button"]');
// GOOD: ARIA roles and labels (accessible, user-facing)
cy.findByRole('button', { name: 'Publish Post' });
cy.findByLabelText('Post Title');
cy.findByPlaceholderText('Search posts...');
// OK: text content (user-visible but brittle to copy changes)
cy.contains('No posts found');
cy.contains('button', 'Cancel');
// BAD: CSS classes (breaks on refactor, implementation detail)
cy.get('.submit-btn');
cy.get('.mat-raised-button.primary');
// ── Chainable assertions ──────────────────────────────────────────────────
cy.getByCy('post-card')
.should('have.length', 10)
.first()
.should('be.visible')
.within(() => {
cy.getByCy('post-title').should('not.be.empty');
cy.getByCy('author-name').should('be.visible');
cy.getByCy('view-count').should('match', /\d+/);
});
// ── Common interactions ───────────────────────────────────────────────────
// Text input
cy.getByCy('title-input').clear().type('My New Post Title');
cy.getByCy('search-input').type('dotnet{enter}'); // {enter} = press Enter key
// Click
cy.getByCy('publish-button').click();
cy.getByCy('category-filter').click();
// Angular Material mat-select
cy.getByCy('status-select').click(); // open the dropdown
cy.get('.mat-option').contains('Published').click(); // select option
// Checkbox
cy.getByCy('featured-checkbox').check();
cy.getByCy('featured-checkbox').uncheck();
// File upload (drag and drop)
cy.getByCy('upload-zone').selectFile('cypress/fixtures/test-image.jpg', {
action: 'drag-drop',
});
// Keyboard navigation
cy.getByCy('search-input').type('{ctrl+a}{backspace}'); // select all and delete
// ── Waiting and intercepting ──────────────────────────────────────────────
// Wait for API call before asserting
cy.intercept('GET', '/api/posts*').as('getPosts');
cy.visit('/');
cy.wait('@getPosts').then(interception => {
expect(interception.response!.statusCode).toBe(200);
});
cy.getByCy('post-card').should('have.length.gt', 0);
// Stub API response
cy.intercept('GET', '/api/posts*', { fixture: 'posts.json' }).as('getPosts');
cy.visit('/');
cy.wait('@getPosts');
cy.getByCy('post-card').should('have.length', 5); // matches fixture data
cy.getByCy('post-card').should('have.length', 10) retries until the assertion passes or the timeout (defaultCommandTimeout) is reached. This built-in retry is Cypress’s solution to the async nature of web apps — elements may not appear immediately (Angular’s change detection, HTTP responses). You should almost never need explicit cy.wait(1000) time delays — use assertions that Cypress will retry until satisfied, or intercept network calls with cy.wait('@alias').cy.intercept() with cy.wait('@alias') instead of arbitrary cy.wait(milliseconds) delays. Waiting for a specific network request ensures the test proceeds exactly when the data is available — not after an arbitrary delay that may be too short (flaky) or too long (slow). cy.intercept('GET', '/api/posts*').as('getPosts'); cy.wait('@getPosts') waits for exactly one GET request matching the pattern and then continues immediately when it completes.cy.get('mat-select').click() may not work because the clickable element is a child div, not the mat-select host element. Inspect the rendered HTML in DevTools to find the actual interactive element, or use cy.findByRole('combobox') to find the accessible select role. The .mat-option class for option selection is relatively stable across Material versions, but always verify against the Material version in use.Common Mistakes
Mistake 1 — CSS class selectors that break on Material updates or refactoring
❌ Wrong — cy.get('.mdc-button--raised'); changes when Material updates internal class names.
✅ Correct — cy.findByRole('button', { name: 'Publish' }) or cy.getByCy('publish-btn').
Mistake 2 — Arbitrary cy.wait(milliseconds) for async operations
❌ Wrong — cy.wait(2000) after clicking submit; brittle (may be too short on slow CI), wasteful (always takes 2s even when data arrives in 100ms).
✅ Correct — cy.wait('@submitPost') after cy.intercept() setup; waits exactly until the request completes.