Все браузеры на iOS, включая Chrome, работают на движке WebKit: CSS-баг в Safari затрагивает все iOS-браузеры, а тест который прошёл на Android Chrome ничего не говорит о поведении на iOS. Большинство команд закрывают 80% ценности мобильного тестирования встроенной эмуляцией устройств Playwright без физических девайсов: она выставляет размер вьюпорта, user agent, pixel ratio и поддержку тач-событий одной строкой конфига. Гайд охватывает проверку адаптивной вёрстки, эмуляцию сети через CDP, баги с размером тач-целей и автозумом iOS, а также когда реальные устройства через BrowserStack действительно оправданы.
Сложности мобильного тестирования
Фрагментация устройств: Android работает на тысячах разных девайсов с разными размерами экранов, версиями ОС и железом. iOS контролируется жёстче, но тоже имеет несколько поколений размеров и версий. Тач-взаимодействия: Мобильные пользователи тапают, свайпают, пинчат и поворачивают экран. Тесты с кликами мышью это не покрывают. Сетевые условия: Мобильные пользователи часто сидят на медленном и нестабильном соединении. 3G, 4G, плохой вайфай: приложение должно с этим справляться. Ограничения производительности: В мобильных устройствах меньше RAM, медленнее процессор, меньше батарея. То что быстро на десктопе, может тормозить на телефоне. Платформенные различия: iOS и Android по-разному обрабатывают уведомления, диплинки, шаринг файлов, доступ к камере, пуш-уведомления. Проблема WebKit на iOS: ВСЕ браузеры на iOS используют WebKit. Chrome на iPhone не является Chrome: это движок Safari в обёртке с логотипом Chrome. Баги в WebKit затрагивают все iOS-браузеры без исключения.Виды мобильного тестирования
Тестирование адаптивного веба
Проверка веб-приложения в мобильных вьюпортах, без установки приложения.
// Playwright — тест мобильного вьюпорта
test('login page works on mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
await page.goto('/login');
// Мобильный UI должен отображаться
await expect(page.getByTestId('mobile-header')).toBeVisible();
await expect(page.getByTestId('desktop-nav')).not.toBeVisible();
// Основная функциональность работает
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'ValidPass1');
await page.tap('[data-testid="submit"]'); // tap вместо click
await expect(page).toHaveURL('/dashboard');
});Тестирование нативных приложений
Приложения из App Store или Google Play. Требуют Appium, Detox (React Native) или напрямую XCUITest/Espresso.
Тестирование PWA
Веб-приложения которые ведут себя как нативные: устанавливаются, работают офлайн, отправляют пуш-уведомления.
Playwright для тестирования мобильного веба
Эмуляция устройств в Playwright покрывает большинство мобильных сценариев для веб-приложений:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Десктоп
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
// Мобайл
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
{ name: 'tablet', use: { ...devices['iPad Pro'] } },
],
});Доступные устройства (см. npx playwright show-devices): iPhone 14, iPhone 14 Pro, iPhone SE, Pixel 7, Galaxy S21, iPad Pro, iPad Mini.
Эмуляция устройства выставляет:
- Размер вьюпорта
- User agent строку
- Device pixel ratio
- Поддержку тач-событий
Тач-события и события мыши
Мобильные пользователи тапают, а не кликают. click() в Playwright работает для обоих случаев, но иногда нужны явные тач-события:
test('swipe carousel works', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/products');
const carousel = page.getByTestId('product-carousel');
const box = await carousel.boundingBox();
if (box) {
// Симулируем свайп влево (палец движется справа налево)
await page.touchscreen.tap(box.x + box.width - 50, box.y + box.height / 2);
await page.mouse.move(box.x + box.width - 50, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + 50, box.y + box.height / 2, { steps: 20 });
await page.mouse.up();
}
// Проверяем что следующий слайд виден
await expect(page.getByTestId('slide-2')).toBeVisible();
});Проверка адаптивной вёрстки
Проверяем что UI корректно перестраивается на разных брейкпоинтах:
const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'mobile-large', width: 414, height: 896 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 720 },
];
for (const viewport of viewports) {
test(`navigation renders correctly at ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/');
if (viewport.width < 768) {
// Мобайл: гамбургер-меню
await expect(page.getByTestId('hamburger')).toBeVisible();
await expect(page.getByTestId('desktop-nav')).not.toBeVisible();
} else {
// Десктоп/планшет: полная навигация
await expect(page.getByTestId('hamburger')).not.toBeVisible();
await expect(page.getByTestId('desktop-nav')).toBeVisible();
}
});
}Эмуляция медленной сети
Проверяем как приложение ведёт себя на медленном мобильном соединении:
test('app loads on slow 3G', async ({ page, context }) => {
// Эмулируем скорости 3G
const cdpSession = await context.newCDPSession(page);
await cdpSession.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: 780 * 1024 / 8, // 780 kbps загрузка
uploadThroughput: 330 * 1024 / 8, // 330 kbps отдача
latency: 100, // 100ms задержка
});
const startTime = Date.now();
await page.goto('/');
const loadTime = Date.now() - startTime;
// Страница должна загрузиться за 10 секунд на 3G
expect(loadTime).toBeLessThan(10000);
// Состояние загрузки должно отображаться во время ожидания
await expect(page.getByTestId('main-content')).toBeVisible();
});
test('app works offline (PWA)', async ({ page, context }) => {
await page.goto('/');
// Отключаем сеть
await context.setOffline(true);
await page.reload();
// PWA должен показать закешированный контент или офлайн-страницу
await expect(page.getByTestId('offline-message')).toBeVisible();
// ИЛИ
await expect(page.getByTestId('main-content')).toBeVisible(); // Закешированный контент
// Восстанавливаем сеть
await context.setOffline(false);
});Тестирование на реальных устройствах через BrowserStack
Эмуляция закрывает большинство сценариев. Когда нужны реальные устройства, есть несколько вариантов: BrowserStack даёт реальные iOS и Android устройства в облаке, Sauce Labs предоставляет аналогичный сервис, а Firebase Test Lab это девайс-ферма от Google с фокусом на Android.
// Конфиг Playwright для реальных устройств BrowserStack
export default defineConfig({
projects: [
{
name: 'real-iphone-14',
use: {
connectOptions: {
wsEndpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify({
'browserstack.user': process.env.BS_USER,
'browserstack.key': process.env.BS_KEY,
'browserName': 'safari',
'bstack:options': {
deviceName: 'iPhone 14',
osVersion: '16',
}
}))}`,
},
},
},
],
});Типичные мобильные баги
Размер тач-целей: Кнопки слишком маленькие для надёжного нажатия (менее 44×44px на iOS, 48×48dp на Android).test('all interactive elements are large enough to tap', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
const buttons = await page.locator('button, a, [role="button"]').all();
for (const button of buttons) {
const box = await button.boundingBox();
if (box) {
expect(box.width, `Button too narrow: ${await button.textContent()}`).toBeGreaterThanOrEqual(44);
expect(box.height, `Button too short: ${await button.textContent()}`).toBeGreaterThanOrEqual(44);
}
}
});test('no horizontal scrolling on mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
expect(scrollWidth).toBe(clientWidth); // Нет горизонтального overflow
});font-size меньше 16px, iOS автоматически зумирует страницу. Минимум 16px.
Чеклист мобильного тестирования
Перед каждым релизом:
- [ ] Основные флоу проверены на iPhone (Safari) и Android (Chrome)
- [ ] Адаптивная вёрстка проверена на ширинах 375px, 768px, 1280px
- [ ] Тач-цели не менее 44×44px
- [ ] Горизонтальный скролл отсутствует на мобильных вьюпортах
- [ ] Формы работают с мобильными клавиатурами
- [ ] Состояния загрузки отображаются на медленных соединениях
- [ ] Изображения оптимизированы для мобиля (не десктопные размеры)
- [ ] Текст читается без зума (минимум 16px)
Итоговая таблица
| Тип тестирования | Инструмент | Когда применять |
|-----------------|-----------|----------------|
| Адаптивный веб | Вьюпорт Playwright | Каждый спринт |
| Кросс-браузер мобайл | Пресеты устройств Playwright | Перед релизом |
| Эмуляция сети | Chrome DevTools Protocol в Playwright | Мобильные фичи |
| Реальные устройства | BrowserStack, Sauce Labs | Перед крупными релизами |
| Нативные приложения | Appium, Detox | Отдельный нативный проект |
Большинству веб-приложений хватает тестирования адаптивного веба (80% ценности мобильного тестирования) плюс периодические прогоны на реальных устройствах для проверки специфики iOS Safari. Реальные девайс-фермы стоит подключать перед крупными релизами и для воспроизведения платформенных багов.
→ See also: Мобильная эмуляция в Playwright: тестирование адаптивности и сенсорного ввода | Стратегии кросс-браузерного тестирования: когда и как тестировать в разных браузерах | Тестирование доступности с Playwright: автоматизированные a11y проверки