Saltar al contenido principal
Sábado 31 Enero - Taller en vivo: Herramientas reales para analizar accesibilidad.¡Apúntate ya!
Volver a la página del blog

¿Cómo automatizar tests con lectores de pantalla?

Tiempo de lectura: 9 minutos
Autor
Carlos Garrido Marín
Fecha de publicación
18 / 01 / 2026

Estás desarrollando un formulario de registro. Has puesto todos los aria-label, los role correctos, las imágenes tienen alt. Pasas axe-core: cero errores. Lighthouse te da un 100 en accesibilidad.

Entonces activas VoiceOver y empiezas a navegar. El lector anuncia "botón" sin más contexto. Luego "editar texto" sin saber qué campo es. Después "enlace" a secas. El formulario que parecía accesible, realmente no lo es.

El problema es que las herramientas de análisis estático verifican la presencia de atributos, pero no verifican la experiencia. Comprueban que existe un aria-label, pero no si ese label tiene sentido en el contexto de navegación real. Comprueban que hay un alt, pero no si ese alt aporta información útil cuando el lector de pantalla lo anuncia.

Probar manualmente con lectores de pantalla lleva tiempo. Y en equipos con entregas continuas, ese tiempo no siempre existe. El resultado es que se hace una prueba manual al principio del proyecto, se documentan los problemas, y después... nadie vuelve a probarlo.

Guidepup permite automatizar estas pruebas. Es una librería de JavaScript que controla VoiceOver y NVDA programáticamente, permitiendo escribir tests que verifican exactamente lo que un usuario escucharía al navegar por tu web.

Aunque esto no reemplaza las pruebas manuales, si sirven para un apoyo adicional.

Qué es Guidepup y qué problema resuelve

Guidepup es un driver de lectores de pantalla. Esto significa que puedes controlarlo desde código: iniciar el lector de pantalla, navegar entre elementos, capturar lo que se anuncia, y hacer assertions sobre ello.

Esto permite aplicar el concepto de shift-left a las pruebas con lectores de pantalla. Como comentamos en el artículo sobre DesignOps, shift-left significa mover la accesibilidad hacia fases más tempranas del desarrollo. En lugar de esperar a QA (Quality Assurance) o a una auditoría externa para descubrir que un formulario no funciona con el lector, se detecta en el momento en que escribes el código y pruebas. Los errores que se detectan antes son más baratos de corregir, y con Guidepup puedes detectarlos en cada pull request.

De momento, soporta:

  • VoiceOver en macOS (Monterey, Ventura, Sonoma)
  • NVDA en Windows (10, Server 2019/2022/2025)
  • Un Virtual Screen Reader para tests unitarios sin necesidad de lector real

La API es consistente entre VoiceOver y NVDA, debido a la fachada que implementa. Esto crea consistencia para que funcione en ambas plataformas con cambios mínimos.

Configuración del entorno

Los lectores de pantalla tienen acceso a información sensible del sistema. Por eso, los sistemas operativos restringen qué aplicaciones pueden controlarlos. Antes de usar Guidepup, necesitas configurar estos permisos.

El paquete @guidepup/setup automatiza este proceso:

npx @guidepup/setup

Lenguaje del código:Bash

En macOS, esto habilita el control de accesibilidad para el terminal. En Windows, configura los permisos necesarios para NVDA.

Después, instala Guidepup en tu proyecto:

npm install @guidepup/guidepup

Lenguaje del código:Bash

Primer script: navegación básica

Vamos a ver qué hace Guidepup con un script mínimo. Este código inicia VoiceOver, navega al siguiente elemento, y muestra lo que se anunció:

import { voiceOver } from "@guidepup/guidepup";

(async () => {
  await voiceOver.start();
  await voiceOver.next();

  console.log(await voiceOver.lastSpokenPhrase());

  await voiceOver.stop();
})();

Lenguaje del código:TypeScript

Para NVDA, el código es prácticamente idéntico:

import { nvda } from "@guidepup/guidepup";

(async () => {
  await nvda.start();
  await nvda.next();

  console.log(await nvda.lastSpokenPhrase());

  await nvda.stop();
})();

Lenguaje del código:TypeScript

Navegación como un usuario real

Los usuarios de lectores de pantalla no navegan de elemento en elemento secuencialmente. Usan atajos para saltar entre tipos de elementos: encabezados, controles de formulario, landmarks, enlaces.

Como vimos en el artículo sobre cómo navegan los usuarios de lectores de pantalla, un usuario puede configurar el lector para saltar directamente a lo que le interesa. Si tu página tiene 15 encabezados, el usuario puede recorrerlos todos en segundos para hacerse una idea de la estructura.

Guidepup permite simular estos patrones:

import { voiceOver } from "@guidepup/guidepup";

(async () => {
  await voiceOver.start();

  // Saltar al siguiente encabezado
  // (equivalente a VO + Command + H)
  await voiceOver.perform(
    voiceOver.keyboardCommands.findNextHeading,
  );
  console.log(await voiceOver.itemText());

  // Saltar al siguiente control de formulario
  await voiceOver.perform(
    voiceOver.keyboardCommands.findNextControl,
  );
  console.log(await voiceOver.lastSpokenPhrase());

  await voiceOver.stop();
})();

Lenguaje del código:TypeScript

Esto permite escribir tests que verifican lo que un usuario escucharía al usar los atajos reales del lector de pantalla.

Integración con Playwright

El módulo @guidepup/playwright integra Guidepup con Playwright para tests end-to-end. Proporciona un wrapper voiceOverTest que inicia y detiene VoiceOver automáticamente, y expone el objeto voiceOver junto con page.

Instalación:

npm install @guidepup/playwright @playwright/test
npx playwright install webkit

Lenguaje del código:Bash

Configuración en playwright.config.ts:

import { screenReaderConfig } from "@guidepup/playwright";
import {
  devices,
  PlaywrightTestConfig,
} from "@playwright/test";

const config: PlaywrightTestConfig = {
  ...screenReaderConfig,
  reportSlowTests: null,
  timeout: 3 * 60 * 1000,
  retries: 2,
  projects: [
    {
      name: "webkit",
      use: {
        ...devices["Desktop Safari"],
        headless: false,
      },
    },
  ],
};

export default config;

Lenguaje del código:TypeScript

Nombre del archivo:playwright.config.ts

Hay tres cosas importantes aquí:

  • screenReaderConfig configura un solo worker. VoiceOver solo puede controlar una instancia del navegador a la vez.
  • timeout de 3 minutos. Los tests con lectores de pantalla son más lentos. El lector necesita tiempo para procesar y anunciar el contenido.
  • headless: false. VoiceOver necesita un navegador visible.

Un test de ejemplo:

import { voiceOverTest as test } from "@guidepup/playwright";
import { expect } from "@playwright/test";

test("El encabezado principal es correcto", async ({
  page,
  voiceOver,
}) => {
  await page.goto("https://github.com/guidepup/guidepup", {
    waitUntil: "load",
  });

  await expect(
    page.locator('header[role="banner"]'),
  ).toBeVisible();
  await voiceOver.navigateToWebContent();

  // Navegar por encabezados hasta encontrar el h1
  while (
    (await voiceOver.itemText()) !==
    "Guidepup heading level 1"
  ) {
    await voiceOver.perform(
      voiceOver.keyboardCommands.findNextHeading,
    );
  }

  // Verificar que llegamos al encabezado correcto
  expect(await voiceOver.itemText()).toBe(
    "Guidepup heading level 1",
  );
});

Lenguaje del código:TypeScript

Virtual Screen Reader para tests unitarios

Los tests con lectores de pantalla reales son lentos y requieren el entorno configurado. No siempre es práctico ejecutarlos en cada commit.

El Virtual Screen Reader de Guidepup simula la funcionalidad de un lector de pantalla sobre el DOM, sin necesidad de VoiceOver ni NVDA. Esto, es una gran ventaja, pero hay que tener en cuenta que el Virtual Screen Reader no es completamente idéntico a un lector de pantalla y en algunos casos puede ser diferente el resultado.

npm install -D @guidepup/virtual-screen-reader

Lenguaje del código:Bash

Ejemplo con Jest:

import { virtual } from "@guidepup/virtual-screen-reader";

test("El input anuncia su label y placeholder", async () => {
  document.body.innerHTML = `
    <label id="search-label">Buscar productos</label>
    <input 
      type="text" 
      aria-labelledby="search-label" 
      placeholder="Escribe aquí..."
    />
  `;

  await virtual.start({ container: document.body });

  // Navegar al input
  await virtual.next();
  await virtual.next();

  expect(await virtual.lastSpokenPhrase()).toBe(
    "textbox, Buscar productos, placeholder Escribe aquí...",
  );

  await virtual.stop();
});

Lenguaje del código:TypeScript

El Virtual Screen Reader es útil para ejecutar pruebas rápidas en CI sin configurar especial para instalar el driver del lector y para verificar funcionalidades más básicas que no requieran una complejidad elevada. Podríamos decir que es el equivalente al jsdom en su respectivo campo.

Pero tiene limitaciones. Los lectores de pantalla reales tienen comportamientos específicos que el simulador no replica exactamente. El propio equipo de Guidepup lo dice menciona en su documentación:

"Este paquete no debería reemplazar sino complementar tus pruebas con lectores de pantalla. No hay sustituto para probar con lectores de pantalla reales y con usuarios reales."

Ejemplo: Verificando la estructura de encabezados

Un problema común de accesibilidad es la estructura de encabezados incorrecta: saltar de h1 a h3, tener múltiples h1, o no tener h1 en absoluto.

Podemos verificarlo con Guidepup:

import { voiceOverTest as test } from "@guidepup/playwright";
import { expect } from "@playwright/test";

test("La estructura de encabezados es correcta", async ({
  page,
  voiceOver,
}) => {
  await page.goto("/articulo");
  await voiceOver.navigateToWebContent();

  const headings: string[] = [];
  let previousItem = "";

  // Recorrer todos los encabezados
  while (true) {
    await voiceOver.perform(
      voiceOver.keyboardCommands.findNextHeading,
    );
    const currentItem = await voiceOver.itemText();

    if (currentItem === previousItem) break;

    headings.push(currentItem);
    previousItem = currentItem;
  }

  // Verificar que existe exactamente un h1
  const h1Count = headings.filter((h) =>
    h.includes("heading level 1"),
  ).length;
  expect(h1Count).toBe(1);

  // Verificar que no hay saltos en la jerarquía
  const levels = headings.map((h) => {
    const match = h.match(/heading level (\d)/);
    return match ? parseInt(match[1]) : 0;
  });

  for (let i = 1; i < levels.length; i++) {
    const jump = levels[i] - levels[i - 1];
    // No saltar más de un nivel
    expect(jump).toBeLessThanOrEqual(1);
  }
});

Lenguaje del código:TypeScript

Ejemplo: Verificando formularios

Los formularios son uno de los componentes donde más fallos de accesibilidad ocurren. Tener etiquetas de campos no asociados, campos sin nombre accesible, errores no enlazados bien al campo que pertenencen, etc.

Para entender qué esperamos que anuncie el lector de pantalla, primero vamos a revisar el HTML del formulario que vamos a probar:

<form>
  <div>
    <label for="email">Correo electrónico</label>
    <input
      type="email"
      id="email"
      aria-describedby="email-hint"
      aria-required="true"
    />
    <span id="email-hint"
      >Usaremos este correo para enviarte
      actualizaciones</span
    >
  </div>

  <div>
    <label for="password">Contraseña</label>
    <input
      type="password"
      id="password"
      aria-required="true"
      aria-describedby="password-error"
      aria-invalid="true"
    />
    <span id="password-error" role="alert"
      >La contraseña debe tener al menos 8 caracteres</span
    >
  </div>
</form>

Lenguaje del código:HTML

Antes de hacer el test, hay varias relaciones ARIA en este formulario que tenemos que comprender:

  • for asociado al id del input: Asocia el <label> con el <input>. Cuando el lector llega al input, anuncia el texto del label.
  • aria-describedby: Añade una descripción adicional. El lector anuncia primero el label, luego el tipo de campo, y después la descripción.
  • aria-required="true": Indica que el campo es obligatorio. VoiceOver lo anuncia como "required".
  • aria-invalid="true": Indica que el campo tiene un error. El lector anuncia "invalid data" o similar.

Con Guidepup podemos verificar que todas estas relaciones funcionan correctamente:

import { voiceOverTest as test } from "@guidepup/playwright";
import { expect } from "@playwright/test";

test("Los campos del formulario tienen labels y descripciones", async ({
  page,
  voiceOver,
}) => {
  await page.goto("/registro");
  await voiceOver.navigateToWebContent();

  // Navegar al primer campo
  await voiceOver.perform(
    voiceOver.keyboardCommands.findNextControl,
  );
  const emailField = await voiceOver.lastSpokenPhrase();

  // Verificar que anuncia:
  // label + tipo + required + descripción
  expect(emailField).toContain("Correo electrónico");
  expect(emailField).toContain("text field");
  expect(emailField).toContain("required");
  expect(emailField).toContain("Usaremos este correo");

  // Navegar al campo de contraseña
  await voiceOver.perform(
    voiceOver.keyboardCommands.findNextControl,
  );
  const passwordField = await voiceOver.lastSpokenPhrase();

  // Verificar que anuncia:
  // label + tipo + required + invalid + mensaje de error
  expect(passwordField).toContain("Contraseña");
  expect(passwordField).toContain("secure text field");
  expect(passwordField).toContain("required");
  // aria-invalid="true"
  expect(passwordField).toContain("invalid");
  // aria-describedby apunta al error
  expect(passwordField).toContain("al menos 8 caracteres");
});

Lenguaje del código:TypeScript

Este test verifica que las relaciones ARIA funcionan en conjunto. Un test con axe-core comprobaría que el aria-describedby apunta a un ID que existe, pero no comprobaría que el lector de pantalla anuncia el texto en el contexto actual.

Integración en CI/CD

Guidepup ofrece una GitHub Action para configurar el entorno:

name: Tests de accesibilidad

on: [push, pull_request]

jobs:
  a11y-tests:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "24"

      - name: Install dependencies
        run: npm ci

      - name: Configurar entorno para lectores de pantalla
        uses: guidepup/setup-action

      - name: Ejecutar tests
        run: npm run test:a11y

Lenguaje del código:YAML

Para NVDA, usa windows-latest en lugar de macos-latest.

El ecosistema completo de Guidepup

Guidepup es un conjunto de paquetes, que, según tus necesidades puedes utilizar y adaptar: 

PaquetePropósito
@guidepup/guidepupLibrería principal para VoiceOver y NVDA
@guidepup/playwrightIntegración con Playwright
@guidepup/virtual-screen-readerSimulador para tests unitarios
@guidepup/jestMatchers de Jest
@guidepup/setupCLI para configurar el entorno
guidepup/setup-actionGitHub Action para CI/CD

Alternativas todavía en desarrollo

Guidepup no es la única opción. El ecosistema de automatización de tecnologías asistivas está en evolución, y hay varias herramientas que vale la pena conocer:

  • at-driver: Este es el estándar que está desarrollando el W3C para automatización de tecnologías asistivas. La premia es que cualquier herramienta de testeo pueda comunicarse con cualquier tecnología asistiva usando un protocolo común. Todavía está en desarrollo activo, pero cuando madure probablemente será la referencia del sector. Si te interesa el futuro de este espacio, vale la pena seguir su evolución.
  • nvda-at-automation: Si tu entorno es exclusivamente Windows y solo necesitas probar con NVDA, esta librería ofrece automatización específica para ese lector. Fue desarrollada por Prime Access Consulting, una consultora especializada en accesibilidad. La diferencia es que está muy pegada a las APIs internas de NVDA, lo que da resultados más precisos para este lector.

Conclusión

Volvamos al formulario del principio. Ese que pasaba axe-core con cero errores pero que en VoiceOver anunciaba "botón" sin contexto, "editar texto" sin saber qué campo era.

Guidepup permite detectar esos problemas antes de que lleguen a producción. No porque analice el código, sino porque escucha lo mismo que escucharía un usuario real. La diferencia entre "el atributo aria-label existe" y "el lector de pantalla anuncia algo útil" es exactamente lo que Guidepup da.

¿Qué estrategia seguir para integrarlo?, bueno, una estrategia práctica para integrarlo podría ser:

  • Virtual Screen Reader en cada pull request: Tests unitarios rápidos que verifican que los atributos ARIA producen los anuncios esperados. Se ejecutan en segundos y no requieren configuración especial.
  • VoiceOver/NVDA en merges a main en release: Tests end-to-end con lectores para los flujos críticos (login, checkout, formularios principales). Se ejecutan en minutos, pero capturan comportamientos que el simulador no detecta.
  • Pruebas manuales con usuarios en releases: Ningún test automatizado sustituye a un usuario real navegando tu aplicación. Guidepup detecta regresiones, pero no va a encontrar problemas de experiencia que solo una persona puede identificar.

El código de este artículo está disponible en el repositorio de ejemplos de Guidepup. Si quieres probarlo, empieza por el Virtual Screen Reader: no necesita configuración especial y te da una idea inmediata de lo que Guidepup puede hacer.

Seguir aprendiendo

Si quieres profundizar en testing de accesibilidad más allá de este artículo, en weAAAre tenemos recursos específicos:

Escrito por:

Únete a nuestra comunidad para aprender y estar al día sobre accesibilidad digital.

Recibirás recursos y artículos semanalmente para que puedas aprender, compartir y ser parte de la comunidad de accesibilidad digital.
¿Quieres colaborar en nuestra Newsletter? Escríbenos a hola@weaaare.com