Your login page looks perfect on a 1440px desktop. Ship it. Two days later, a customer opens it on their phone and the login button is hidden behind the keyboard. The form overflows the screen. The logo is comically large. Responsive bugs are everywhere. And they are invisible if you only test at one viewport size.
import { test, expect } from '@playwright/test';
const viewports = [
{ name: 'mobile', width: 375, height: 667 }, // iPhone SE
{ name: 'tablet', width: 768, height: 1024 }, // iPad
{ name: 'desktop', width: 1440, height: 900 }, // Standard desktop
];
for (const vp of viewports) {
test(`login page at ${vp.name} (${vp.width}x${vp.height})`, async ({ page }) => {
await page.setViewportSize({ width: vp.width, height: vp.height });
await page.goto('/banking/login');
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot(`login-${vp.name}.png`, {
fullPage: true,
});
});
}Instead of loops in your test, you can configure viewport-specific projects in playwright.config.ts. This way, every test runs at every viewport automatically.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'Desktop Chrome',
use: {
...devices['Desktop Chrome'],
viewport: { width: 1440, height: 900 },
},
},
{
name: 'Mobile Safari',
use: {
...devices['iPhone 13'],
},
},
{
name: 'Tablet',
use: {
...devices['iPad (gen 7)'],
},
},
],
});import { test, expect } from '@playwright/test';
test.describe('Navigation at different widths', () => {
test('shows full nav at 1024px+', async ({ page }) => {
await page.setViewportSize({ width: 1024, height: 768 });
await page.goto('/banking');
const nav = page.getByRole('navigation');
await expect(nav).toHaveScreenshot('nav-desktop.png');
});
test('shows hamburger at 1023px', async ({ page }) => {
await page.setViewportSize({ width: 1023, height: 768 });
await page.goto('/banking');
const nav = page.getByRole('navigation');
await expect(nav).toHaveScreenshot('nav-mobile.png');
});
test('shows mobile menu when hamburger clicked', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/banking');
await page.getByRole('button', { name: /menu/i }).click();
await expect(page).toHaveScreenshot('nav-mobile-open.png');
});
});Test at the exact breakpoint boundaries. If your CSS switches at 768px, test at 767px (mobile) and 768px (tablet). That one pixel boundary is where layout bugs hide.
Q: How do you handle responsive visual testing in Playwright?
A: Two approaches. First, use page.setViewportSize() in individual tests to set specific widths like 375, 768, and 1440. Second, configure projects in playwright.config.ts with different devices like iPhone 13, iPad, Desktop Chrome. Each project runs all tests at its viewport. I test at breakpoint boundaries -- the exact pixel where the layout switches. I also test critical components like navigation separately at each width since they change the most between mobile and desktop.
Key Point: Test at multiple viewport sizes. Use projects or setViewportSize(). Focus on breakpoint boundaries where layout switches happen.