QA Engineer Skills 2026QA-2026Responsive Design Testing

Responsive Design Testing

Beyond Screenshot Comparison

Traditional responsive testing took screenshots at different widths and compared them visually. This approach fails because it cannot distinguish between "the layout adapted correctly to a smaller width" and "the layout broke at a smaller width." Both look different from the desktop screenshot.

AI agents and modern testing tools can reason about functional correctness rather than pixel-perfect matching. The question is not "does it look the same?" but "does it work correctly at every width?"


Viewport Breakpoint Configuration

Define your test viewports to cover the critical breakpoints where layouts change:

// playwright.config.ts -- define viewport breakpoints
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
    projects: [
        // Mobile portrait
        { name: 'mobile-portrait', use: { viewport: { width: 375, height: 812 } } },
        // Mobile landscape
        { name: 'mobile-landscape', use: { viewport: { width: 812, height: 375 } } },
        // Small tablet
        { name: 'tablet-portrait', use: { viewport: { width: 768, height: 1024 } } },
        // Tablet landscape
        { name: 'tablet-landscape', use: { viewport: { width: 1024, height: 768 } } },
        // Desktop
        { name: 'desktop', use: { viewport: { width: 1440, height: 900 } } },
        // Ultrawide
        { name: 'ultrawide', use: { viewport: { width: 2560, height: 1080 } } },
        // Foldable inner display (Samsung Galaxy Fold open)
        { name: 'foldable-open', use: { viewport: { width: 1812, height: 2176 } } },
        // Foldable outer display
        { name: 'foldable-closed', use: { viewport: { width: 832, height: 2268 } } },
    ],
});

Choosing Breakpoints Strategically

Do not just test at CSS breakpoint values. Test at the transition points where layouts are most likely to break:

Width What to Test Why
320px Smallest viable width iPhone SE, small Android phones
374px Just below common mobile breakpoint Catches off-by-one in media queries
375px Standard mobile breakpoint iPhone 12-15 mini/standard width
414px Larger phones iPhone Pro Max, Galaxy S series
767px Just below tablet breakpoint Catches tablet layout triggering issues
768px Tablet breakpoint iPad portrait
1023px Just below desktop breakpoint Catches desktop layout issues
1024px Desktop breakpoint iPad landscape, small laptops
1440px Standard desktop Most common desktop resolution
1920px Full HD desktop Content stretching issues

AI Agent Responsive Testing Pattern

When an AI agent tests responsive layouts, it can reason about functional correctness rather than visual similarity:

## Responsive Test: Navigation Menu

### At mobile width (375px):
1. Navigate to the homepage
2. Verify the hamburger menu icon is visible
3. Verify the full navigation links are NOT visible
4. Click the hamburger menu
5. Verify the navigation drawer slides in
6. Verify all primary links are accessible
7. Take a screenshot

### At desktop width (1440px):
1. Navigate to the homepage
2. Verify the full navigation bar is visible with all links
3. Verify the hamburger menu icon is NOT visible
4. Verify navigation links are horizontally laid out
5. Take a screenshot

The AI agent can then compare behavior across viewports and flag inconsistencies like missing menu items, overlapping elements, or truncated text that a static screenshot comparison might miss.


Dynamic Viewport Resizing Test

Test that layouts remain stable during continuous resizing (important for foldable devices and desktop window resizing):

// tests/responsive/layout-stability.spec.ts
import { test, expect } from '@playwright/test';

test('layout remains stable during viewport resize', async ({ page }) => {
    await page.goto('/dashboard');

    const widths = [320, 375, 414, 768, 1024, 1280, 1440, 1920];

    for (const width of widths) {
        await page.setViewportSize({ width, height: 800 });
        // Wait for CSS transitions to complete
        await page.waitForTimeout(300);

        // Verify no horizontal overflow (no horizontal scrollbar)
        const bodyWidth = await page.evaluate(() => document.body.scrollWidth);
        const viewportWidth = await page.evaluate(() => window.innerWidth);
        expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1); // +1 for rounding

        // Verify primary content is visible
        await expect(page.locator('[data-testid="main-content"]')).toBeVisible();

        // Verify no text truncation on critical elements
        const heading = page.locator('h1').first();
        const box = await heading.boundingBox();
        if (box) {
            expect(box.width).toBeGreaterThan(50); // Not crushed to nothing
        }
    }
});

test('navigation adapts between mobile and desktop', async ({ page }) => {
    await page.goto('/');

    // Desktop: full nav visible
    await page.setViewportSize({ width: 1440, height: 900 });
    await expect(page.locator('[data-testid="desktop-nav"]')).toBeVisible();
    await expect(page.locator('[data-testid="mobile-menu-btn"]')).not.toBeVisible();

    // Mobile: hamburger menu visible
    await page.setViewportSize({ width: 375, height: 812 });
    await page.waitForTimeout(300);
    await expect(page.locator('[data-testid="mobile-menu-btn"]')).toBeVisible();
    await expect(page.locator('[data-testid="desktop-nav"]')).not.toBeVisible();

    // Open mobile menu
    await page.click('[data-testid="mobile-menu-btn"]');
    await expect(page.locator('[data-testid="mobile-drawer"]')).toBeVisible();

    // Verify all navigation items are present in mobile menu
    const mobileLinks = await page.locator('[data-testid="mobile-drawer"] a').allTextContents();
    expect(mobileLinks).toContain('Home');
    expect(mobileLinks).toContain('Products');
    expect(mobileLinks).toContain('Cart');
    expect(mobileLinks).toContain('Account');
});

Common Responsive Bugs to Test For

Horizontal Overflow

The most common responsive bug: content wider than the viewport causes a horizontal scrollbar.

test('no horizontal overflow on any page', async ({ page }) => {
    const pages = ['/', '/products', '/cart', '/checkout', '/account'];
    const widths = [320, 375, 768, 1024, 1440];

    for (const pagePath of pages) {
        for (const width of widths) {
            await page.setViewportSize({ width, height: 800 });
            await page.goto(pagePath);

            const hasOverflow = await page.evaluate(() => {
                return document.body.scrollWidth > window.innerWidth;
            });

            expect(hasOverflow).toBe(false);
        }
    }
});

Touch Target Overlap

On mobile, elements that are spaced fine on desktop may overlap or be too close together:

test('interactive elements have adequate spacing on mobile', async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 812 });
    await page.goto('/');

    const buttons = await page.locator('button, a, [role="button"]').all();
    const rects = await Promise.all(buttons.map(b => b.boundingBox()));

    for (let i = 0; i < rects.length; i++) {
        for (let j = i + 1; j < rects.length; j++) {
            const a = rects[i];
            const b = rects[j];
            if (!a || !b) continue;

            // Check if elements overlap
            const overlapsX = a.x < b.x + b.width && a.x + a.width > b.x;
            const overlapsY = a.y < b.y + b.height && a.y + a.height > b.y;

            if (overlapsX && overlapsY) {
                // Calculate overlap area
                const overlapWidth = Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x);
                const overlapHeight = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
                const overlapArea = overlapWidth * overlapHeight;

                expect(overlapArea).toBeLessThan(50);
            }
        }
    }
});

Image Scaling

test('images do not overflow their containers', async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 812 });
    await page.goto('/products');

    const images = await page.locator('img').all();
    for (const img of images) {
        const box = await img.boundingBox();
        if (box) {
            expect(box.width).toBeLessThanOrEqual(375);
        }
    }
});

Responsive Testing in CI

# .github/workflows/responsive-tests.yml
name: Responsive Layout Tests
on: [pull_request]

jobs:
  responsive:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        project: [mobile-portrait, tablet-portrait, desktop, foldable-open]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --project=${{ matrix.project }}
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: responsive-report-${{ matrix.project }}
          path: playwright-report/

Responsive testing is not optional for any public-facing application. The investment in automated viewport testing pays for itself by catching layout bugs that would otherwise require manual QA on physical devices.