When a client shows you a bug that only happens on mobile — this is how you reproduce it without picking up a phone. Playwright can fake any device, locale, timezone, geolocation, or color scheme.
Device profiles
One device profile sets all browser behavior at once
Playwright ships with a built-in registry of ~60 device profiles — iPhone models, Pixel phones, iPad variants, desktop browsers. Each profile sets userAgent, viewport, deviceScaleFactor, hasTouch and isMobile in one spread. I use this constantly when testing responsive layouts or touch interactions.
In playwright.config.ts you add a project per device. In the test file the page is already sized and touch-enabled — no extra setup.
// Тест запускається з тими самими параметрами що в профіліtest('mobile nav shows hamburger menu', async ({ page }) => {
await page.goto('/dashboard')
// При viewport iPhone 13 desktop nav прихований, hamburger видимийawaitexpect(page.getByRole('button', { name: 'Menu' })).toBeVisible()
awaitexpect(page.getByRole('navigation')).not.toBeVisible()
})
Override viewport per test
The device profile sets a default viewport, but you can override it for a specific test or describe block. Useful when you need to test a specific breakpoint without creating a whole new project.
ts
// Для всього файлуtest.use({ viewport: { width: 1440, height: 900 } })
// Або для конкретного describetest.describe('tablet layout', () => {
test.use({ viewport: { width: 768, height: 1024 } })
test('sidebar collapses at tablet width', async ({ page }) => {
await page.goto('/dashboard')
awaitexpect(page.getByRole('complementary')).toHaveAttribute('data-collapsed', 'true')
})
})
// Або прямо в тестіtest('wide screen shows split view', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 })
await page.goto('/orders')
awaitexpect(page.getByTestId('split-view')).toBeVisible()
})
Locale and timezone
Date formatting, number separators, currency symbols — all of these depend on locale. If your app shows orders with dates, and you're testing a German customer, the date should be 14.05.2026, not 05/14/2026. Playwright lets you emulate any locale and timezone at the context level.
Note: this affects only what the browser reports — navigator.language, Intl API, and timezone for JS date operations. It doesn't change the test runner's timezone.
ts
// playwright.config.ts — глобально для всіх тестівexportdefault defineConfig({
use: {
locale: 'de-DE',
timezoneId: 'Europe/Berlin',
},
})
ts
// Або для окремого тестуtest.use({
locale: 'uk-UA',
timezoneId: 'Europe/Kyiv',
})
test('order date shows in Ukrainian format', async ({ page }) => {
await page.goto('/orders')
// Дата має бути у форматі ДД.ММ.РРРРawaitexpect(page.getByTestId('order-date').first()).toContainText(/d{2}.d{2}.d{4}/)
})
Browser permissions
If your app asks for notifications, camera, or geolocation access — by default the browser blocks it with a permission dialog that Playwright can't click through. You need to grant the permission programmatically before the page even asks for it.
ts
// Глобально в конфізіexportdefault defineConfig({
use: {
permissions: ['notifications', 'geolocation'],
},
})
// Або в тесті через contexttest('notification opt-in flow works', async ({ page, context }) => {
await context.grantPermissions(['notifications'])
await page.goto('/settings/notifications')
await page.getByRole('button', { name: 'Enable notifications' }).click()
// Діалогу браузера немає — дозвіл вже виданийawaitexpect(page.getByText('Notifications enabled')).toBeVisible()
})
// Скинути всі дозволиawait context.clearPermissions()
Geolocation
If the app shows location-based content — store locators, delivery zones, region-specific pricing — you need to fake the user's position. Set geolocation in the config or override it mid-test.
ts
test.use({
geolocation: { latitude: 50.4501, longitude: 30.5234 }, // Kyiv
permissions: ['geolocation'],
})
test('shows Kyiv delivery zone', async ({ page }) => {
await page.goto('/delivery-zones')
awaitexpect(page.getByText('Доставка по Києву')).toBeVisible()
})
test('location-based store finder', async ({ page, context }) => {
await page.goto('/stores')
// Змінити позицію прямо в тестіawait context.setGeolocation({ latitude: 48.4647, longitude: 35.0462 }) // Dniproawait page.getByRole('button', { name: 'Find stores near me' }).click()
awaitexpect(page.getByText('Дніпро')).toBeVisible()
})
Dark mode and color scheme
If your app supports dark mode via prefers-color-scheme, tests run in light mode by default. To test dark mode components, set colorScheme: 'dark' — either globally or for a specific test.
ts
test.describe('dark mode', () => {
test.use({ colorScheme: 'dark' })
test('dashboard looks right in dark mode', async ({ page }) => {
await page.goto('/dashboard')
// Перевіряємо що dark mode клас є на bodyawaitexpect(page.locator('body')).toHaveClass(/dark/)
})
})
// Або переключати в тесті через emulateMediatest('color scheme toggle works', async ({ page }) => {
await page.goto('/dashboard')
await page.emulateMedia({ colorScheme: 'dark' })
awaitexpect(page.locator('[data-theme]')).toHaveAttribute('data-theme', 'dark')
await page.emulateMedia({ colorScheme: 'light' })
awaitexpect(page.locator('[data-theme]')).toHaveAttribute('data-theme', 'light')
})
Offline mode
Set offline: true to simulate a dropped connection. Useful for testing error states — what does the app show when the API is unreachable? Better to test this with emulation than to actually kill the server.