Overview
Abyss components using the MediaQuery component render all breakpoint variants (mobile, tablet, desktop) in the DOM and use CSS to control visibility. This approach improves performance and SSR compatibility but requires specific testing strategies.
The change
Before
Components conditionally rendered elements based on viewport size:
{ isMobile ? <MobileNav /> : <DesktopNav />;}This meant that only one element existed in the DOM at any given time.
After
Components render all variants and use CSS to control display:
<MediaQuery smallerThan="md"><MobileNav /></MediaQuery><MediaQuery largerThan="md"><DesktopNav /></MediaQuery>Now, both elements always exist in the DOM and CSS determines which is visible.
Impact on tests
This change means that test locators may match multiple elements (mobile and desktop variants) instead of just one. Tests must be updated to filter by visibility to ensure they interact with the correct variant.
Note: The below sandbox examples in this section are for Playwright. Other framework examples can be seen further down on the page.
1. Multiple elements matched
Symptom:
Error: Multiple elements found for selector .breadcrumb-linkExpected 3, found 6Cause: Test locator matches both mobile and desktop elements.
Fix: Filter by visibility
// ❌ Matches both mobile and desktopconst links = await page.locator('.breadcrumb-link').all();
// ✅ Only matches visible elementsconst links = await page .locator('.breadcrumb-link') .filter({ visible: true }) .all();2. Interacting with hidden elements
Symptom:
Error: Element is not visibleCause: Test is targeting the hidden variant instead of the visible one.
Fix: Always filter by visibility
// ❌ Might target hidden elementawait page.locator('[data-testid="nav-menu"]').first().click();
// ✅ Targets visible elementawait page .locator('[data-testid="nav-menu"]') .filter({ visible: true }) .click();Testing patterns
Playwright
// Single elementawait page .getByRole('link', { name: 'Home' }) .filter({ visible: true }) .click();
// Multiple elementsconst visibleLinks = await page .getByRole('link') .filter({ visible: true }) .all();
// Count visible elementsconst count = await page .locator('.breadcrumb') .filter({ visible: true }) .count();
expect(count).toBe(3);React Testing Library
// Filter by visibility helperfunction isVisible(element: HTMLElement): boolean { return ( element.offsetParent !== null && window.getComputedStyle(element).display !== 'none' && window.getComputedStyle(element).visibility !== 'hidden' );}
// Use the helperconst visibleLinks = screen.getAllByRole('link').filter(isVisible);Cypress
// Filter visible elementscy.get('.breadcrumb-link').filter(':visible').should('have.length', 3);
// Ensure element is visible before interactioncy.get('[data-testid="mobile-nav"]').should('be.visible').click();Page object model (POM) pattern
Update page object models to include visibility filters by default:
export class BreadcrumbsPage { constructor(private page: Page) {}
// ✅ Visibility filter built into getter get breadcrumbLinks() { return this.page.locator('.breadcrumb-link').filter({ visible: true }); }
async clickBreadcrumb(text: string) { await this.breadcrumbLinks.filter({ hasText: text }).click(); }
async getBreadcrumbCount() { return await this.breadcrumbLinks.count(); }}Viewport testing
When testing responsive behavior, set explicit viewports:
test.describe('Mobile view', () => { test.use({ viewport: { width: 375, height: 667 } });
test('shows correct navigation', async ({ page }) => { // Mobile nav should be visible await expect( page.locator('[data-testid="mobile-nav"]').filter({ visible: true }) ).toBeVisible();
// Desktop nav should not be visible await expect( page.locator('[data-testid="desktop-nav"]').filter({ visible: true }) ).toHaveCount(0); });});
test.describe('Desktop view', () => { test.use({ viewport: { width: 1280, height: 720 } });
test('shows correct navigation', async ({ page }) => { // Desktop nav should be visible await expect( page.locator('[data-testid="desktop-nav"]').filter({ visible: true }) ).toBeVisible();
// Mobile nav should not be visible await expect( page.locator('[data-testid="mobile-nav"]').filter({ visible: true }) ).toHaveCount(0); });});Affected components
These components render multiple responsive variants:
| Component | What's Duplicated | Filter Required |
|---|---|---|
| Alert | Layout and actions | Yes |
| Breadcrumbs | Mobile shows subset | Yes |
| EmphasisBanner | Layout | Yes |
| Footer | Link columns | Yes |
| Header | Navigation menus | Yes |
| PageBodyIntro | Content layout | Yes |
| StepTracker | Display format | Yes |
| Carousel | Navigation buttons | Yes |
Best practices
- Always filter by visibility when targeting responsive elements
- Set explicit viewports in tests for predictable behavior
- Test both variants in separate test cases (mobile and desktop)
- Use semantic selectors (roles, labels) over class names
- Validate only one variant is visible at any viewport
- Build visibility filters into page object models for reusability
Why this approach?
The CSS-based approach provides several benefits:
- Better SSR/hydration: No mismatches between server and client
- Improved performance: CSS-based visibility is faster than JS re-renders
- Consistency: Follows modern React patterns (CSS over JS for styling)
- Accessibility: Screen readers handle visibility correctly