Todos los navegadores en iOS, incluyendo Chrome, corren sobre WebKit internamente: un bug de CSS en Safari afecta a todos los navegadores de iOS, y un test que pasa en Chrome de Android no dice nada sobre el comportamiento en iOS. La mayoría de los equipos web cubren el 80% del valor del testing mobile con la emulación de dispositivos integrada en Playwright sin necesidad de un laboratorio de dispositivos físicos, ya que configura el tamaño del viewport, el user agent, el pixel ratio y el soporte touch en una sola línea de configuración. Esta guía cubre la verificación de layouts responsivos, el throttling de red vía CDP, los bugs de tamaño de tap targets y zoom en inputs de iOS que sistemáticamente escapan al testing en desktop, y cuándo vale el costo de servicios de dispositivos reales como BrowserStack.

Desafíos del testing mobile

Fragmentación de dispositivos

Android corre en miles de dispositivos diferentes con distintos tamaños de pantalla, versiones de OS y hardware. iOS es más controlado pero igual tiene múltiples tamaños de pantalla y versiones.

Interacciones táctiles

Los usuarios mobile hacen tap, swipe, pinch y rotan la pantalla. Los tests de clic con mouse en desktop no cubren estas interacciones.

Condiciones de red

Los usuarios mobile frecuentemente tienen conexiones lentas e inestables. 3G, 4G, wifi intermitente: tu aplicación necesita manejarlas bien.

Limitaciones de rendimiento

Los dispositivos móviles tienen menos RAM, CPUs más lentos y baterías más pequeñas que los desktops. Lo que es rápido en desktop puede ser lento en mobile.

Diferencias de plataforma

iOS y Android manejan las cosas de forma diferente: notificaciones, deep links, compartir archivos, acceso a cámara, notificaciones push.

El problema de WebKit en iOS

TODOS los navegadores en iOS usan WebKit internamente. Chrome en iPhone no es Chrome: es el motor de Safari con la interfaz de Chrome. Los bugs de WebKit afectan a todos los navegadores de iOS.

Tipos de testing mobile

Testing web responsivo

Testear tu app web en viewports móviles, sin necesidad de instalar nada.

// Playwright: testear viewport mobile
test('la página de login funciona en mobile', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 });  // iPhone SE
  await page.goto('/login');
  
  // Debe aparecer la UI específica para mobile
  await expect(page.getByTestId('mobile-header')).toBeVisible();
  await expect(page.getByTestId('desktop-nav')).not.toBeVisible();
  
  // La funcionalidad principal funciona
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'PassValida1');
  await page.tap('[data-testid="submit"]');  // tap en lugar de click
  
  await expect(page).toHaveURL('/dashboard');
});

Testing de apps nativas

Apps instaladas desde App Store o Google Play. Requieren Appium, Detox (React Native) o XCUITest/Espresso directamente.

Testing de Progressive Web Apps (PWA)

Apps web que se comportan como nativas: instalables, funcionan offline, envían notificaciones push.

Playwright para testing web mobile

La emulación de dispositivos de Playwright cubre los escenarios de testing mobile más importantes para apps web:

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

export default defineConfig({
  projects: [
    // Desktop
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    
    // Mobile
    { name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
    { name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
    { name: 'tablet', use: { ...devices['iPad Pro'] } },
  ],
});

Dispositivos disponibles (ver npx playwright show-devices): iPhone 14, iPhone 14 Pro, iPhone SE, Pixel 7, Galaxy S21, iPad Pro y iPad Mini.

La emulación de dispositivos configura:

  • Tamaño del viewport
  • String del user agent
  • Device pixel ratio
  • Soporte de eventos touch

Eventos touch vs. eventos mouse

Los usuarios mobile hacen tap, no click. El click() de Playwright funciona para los dos, pero a veces necesitas touch explícito:

test('el carrusel responde al swipe', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await page.goto('/productos');
  
  const carrusel = page.getByTestId('product-carousel');
  const box = await carrusel.boundingBox();
  
  if (box) {
    // Simular swipe a la izquierda (el dedo se mueve de derecha a izquierda)
    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();
  }
  
  // Verificar que el slide siguiente es visible
  await expect(page.getByTestId('slide-2')).toBeVisible();
});

Testear layouts responsivos

Verifica que la UI se adapta correctamente en diferentes breakpoints:

const viewports = [
  { name: 'mobile', width: 375, height: 667 },
  { name: 'mobile-grande', width: 414, height: 896 },
  { name: 'tablet', width: 768, height: 1024 },
  { name: 'desktop', width: 1280, height: 720 },
];

for (const viewport of viewports) {
  test(`la navegación se renderiza correctamente en ${viewport.name}`, async ({ page }) => {
    await page.setViewportSize({ width: viewport.width, height: viewport.height });
    await page.goto('/');
    
    if (viewport.width < 768) {
      // Mobile: menú hamburguesa
      await expect(page.getByTestId('hamburger')).toBeVisible();
      await expect(page.getByTestId('desktop-nav')).not.toBeVisible();
    } else {
      // Desktop/tablet: navegación completa
      await expect(page.getByTestId('hamburger')).not.toBeVisible();
      await expect(page.getByTestId('desktop-nav')).toBeVisible();
    }
  });
}

Throttling de red

Testea cómo se comporta tu app en redes mobile lentas:

test('la app carga en 3G lento', async ({ page, context }) => {
  // Aplicar throttling a velocidades 3G
  const cdpSession = await context.newCDPSession(page);
  await cdpSession.send('Network.emulateNetworkConditions', {
    offline: false,
    downloadThroughput: 780 * 1024 / 8,  // 780 kbps descarga 3G
    uploadThroughput: 330 * 1024 / 8,    // 330 kbps subida 3G
    latency: 100,                          // 100ms latencia
  });
  
  const tiempoInicio = Date.now();
  await page.goto('/');
  const tiempoCarga = Date.now() - tiempoInicio;
  
  // La página debe cargar en menos de 10 segundos en 3G
  expect(tiempoCarga).toBeLessThan(10000);
  
  await expect(page.getByTestId('main-content')).toBeVisible();
});

test('la app funciona offline (PWA)', async ({ page, context }) => {
  await page.goto('/');
  
  // Desconectar
  await context.setOffline(true);
  
  await page.reload();
  
  // La PWA debe mostrar contenido en caché o página offline
  await expect(page.getByTestId('offline-message')).toBeVisible();
  // O
  await expect(page.getByTestId('main-content')).toBeVisible();  // Contenido en caché
  
  // Volver a conectar
  await context.setOffline(false);
});

Testing en dispositivos reales con BrowserStack

La emulación cubre la mayoría de los escenarios. Para dispositivos reales, las opciones principales son BrowserStack (dispositivos iOS y Android reales en la nube), Sauce Labs (oferta similar) y Firebase Test Lab (el servicio de dispositivos de Google, enfocado en Android).

// Config de Playwright para dispositivos reales en BrowserStack
export default defineConfig({
  projects: [
    {
      name: 'iphone-14-real',
      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',
            }
          }))}`,
        },
      },
    },
  ],
});

Bugs comunes específicos de mobile que testear

Tamaño de tap targets

Botones demasiado pequeños para tocar de forma confiable (menos de 44×44px en iOS, 48×48dp en Android):

test('todos los elementos interactivos son suficientemente grandes', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('/');
  
  const botones = await page.locator('button, a, [role="button"]').all();
  
  for (const boton of botones) {
    const box = await boton.boundingBox();
    if (box) {
      expect(box.width, `Botón demasiado angosto: ${await boton.textContent()}`).toBeGreaterThanOrEqual(44);
      expect(box.height, `Botón demasiado bajo: ${await boton.textContent()}`).toBeGreaterThanOrEqual(44);
    }
  }
});

Scroll horizontal

El contenido no debería requerir scroll horizontal en mobile:

test('no hay scroll horizontal en 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);  // Sin desbordamiento horizontal
});

Zoom en inputs

En iOS, si un input tiene font-size menor a 16px, iOS hace zoom automáticamente. Usa font-size: 16px como mínimo.

Checklist de testing mobile

Antes de cada release:

  • [ ] Flujos principales testeados en iPhone (Safari) y Android (Chrome)
  • [ ] Layout responsivo verificado en 375px, 768px y 1280px
  • [ ] Tap targets de al menos 44×44px
  • [ ] Sin scroll horizontal en viewports mobile
  • [ ] Los formularios funcionan con teclados móviles
  • [ ] Estados de carga visibles en conexiones lentas
  • [ ] Las imágenes tienen tamaños apropiados para mobile (no imágenes del tamaño de desktop)
  • [ ] El texto es legible sin hacer zoom (mínimo 16px)

Resumen

| Tipo de testing | Herramienta | Cuándo usar |

|----------------|-------------|-------------|

| Web responsivo | Viewport de Playwright | Cada sprint |

| Mobile cross-browser | Presets de dispositivos de Playwright | Antes del release |

| Throttling de red | Chrome DevTools Protocol en Playwright | Features con mucho uso mobile |

| Dispositivos reales | BrowserStack, Sauce Labs | Antes de releases mayores |

| Apps nativas | Appium, Detox | Proyecto de app nativa separado |

La mayoría de las apps web necesitan testing web responsivo (el 80% del valor del testing mobile) más verificaciones ocasionales en dispositivos reales para el comportamiento de Safari en iOS. Guarda los device farms para la validación pre-release y bugs conocidos de plataformas específicas.

→ See also: Emulación Móvil en Playwright: Pruebas Responsivas y Táctiles | Estrategias de Testing Cruzado de Navegadores: Cuándo y Cómo Probar Múltiples Navegadores | Testing de Accesibilidad con Playwright: Verificaciones a11y Automatizadas