
Tabla de Contenidos
- La Regla Fundamental
- Prefiere Consultas Accesibles Antes que Test IDs
- Ausencia Inmediata vs Desaparición Después de una Acción
- La Visibilidad No es Existencia
- Anti-Patrones Comunes y Soluciones
- Un Ejemplo Realista de Principio a Fin
- Casos Extremos que Debes Considerar
- Guía Rápida de Decisiones
- Configuración Mínima
- Conclusiones Clave
- Referencias
- Resumen
- Palabras Clave SEO
Cuando un elemento no debería estar presente, necesitas un test que falle solo cuando realmente existe—no cuando simplemente está oculto, fuera de pantalla o esperando una animación. Aquí te explicamos cómo hacer esto de forma confiable con React Testing Library (RTL).
La Regla Fundamental
El principio fundamental para probar la no existencia de elementos es directo:
- Usa una consulta
queryBy*(oqueryAllBy*) - Combínala con
not.toBeInTheDocument()de@testing-library/jest-dom
import { screen } from "@testing-library/react";
// Verifica ausencia (chequeo síncrono)
expect(screen.queryByTestId("role-icon")).not.toBeInTheDocument();
// Equivalente, pero menos expresivo:
expect(screen.queryByTestId("role-icon")).toBeNull();
¿Por qué no getBy*?
La familia de consultas getBy* lanza un error inmediatamente si no encuentra un elemento coincidente. Aunque este comportamiento es excelente cuando esperas que un elemento esté presente, hace imposibles las verificaciones de ausencia porque tu test falla antes de que el matcher pueda ejecutarse.
La diferencia clave: queryBy* devuelve null cuando no encuentra nada, permitiéndote hacer verificaciones sobre la ausencia. getBy* lanza un error, terminando el test inmediatamente.
Prefiere Consultas Accesibles Antes que Test IDs
Siempre favorece consultas que reflejen las capacidades reales del usuario y los patrones de accesibilidad. Esto hace que tus tests sean más resistentes y fomenta mejor HTML semántico.
// Prefiere este enfoque - prueba el comportamiento de cara al usuario:
expect(screen.queryByRole("dialog", { name: /profile/i }))
.not.toBeInTheDocument();
// Antes que este enfoque - prueba detalles de implementación:
expect(screen.queryByTestId("profile-modal"))
.not.toBeInTheDocument();
Las consultas accesibles como queryByRole, queryByLabelText y queryByText reducen la fragilidad y fomentan mejor semántica en tus componentes.
Consejo: Si específicamente necesitas verificar que algo no es accesible (pero aún puede estar montado con hidden o aria-hidden), usa:
expect(screen.queryByRole("dialog", { name: /profile/i })).not.toBeInTheDocument();Si necesitas incluir elementos ocultos en la búsqueda, pasa { hidden: true }.
Ausencia Inmediata vs Desaparición Después de una Acción
Existen dos escenarios distintos al probar la no existencia de elementos, y cada uno requiere un enfoque diferente.
1) Ausencia Inmediata (Síncrona)
Cuando nada desencadena un re-render y solo necesitas verificar que un elemento no está presente en el renderizado inicial:
render(<UserMenu />);
// El menú comienza cerrado - chequeo síncrono
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
Esta es una verificación síncrona directa. El elemento existe en el DOM inicial o no existe.
2) Desaparición Después de una Interacción (Asíncrona)
Para elementos que se desmontan después de un tiempo (por ejemplo, después de hacer clic en "Cerrar", presionar Esc, o que se complete una animación), usa waitForElementToBeRemoved (preferido) o waitFor.
import userEvent from "@testing-library/user-event";
import { waitForElementToBeRemoved, screen } from "@testing-library/react";
render(<UserMenu />);
// Abrir el menú
await userEvent.click(screen.getByRole("button", { name: /open menu/i }));
// Verificar que el menú aparece
expect(screen.getByRole("menu")).toBeInTheDocument();
// Cerrar el menú y esperar su eliminación
await userEvent.keyboard("{Escape}");
await waitForElementToBeRemoved(() => screen.queryByRole("menu"));
// Verificación final (opcional, pero explícita)
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
¿Por qué waitForElementToBeRemoved?
Esta utilidad vincula la espera al nodo DOM real, eliminando problemas de timing. Es más confiable que timeouts arbitrarios o intervalos de polling.
Si tu componente cambia clases CSS antes de desmontarse, waitFor también es un enfoque válido:
import { waitFor } from "@testing-library/react";
await userEvent.keyboard("{Escape}");
await waitFor(() => {
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
La Visibilidad No es Existencia
Es fundamental entender la distinción entre visibilidad y presencia en el DOM:
not.toBeVisible()→ el nodo existe en el DOM pero está oculto mediante CSS (display: none,visibility: hidden) o atributos ARIA (aria-hidden="true")not.toBeInTheDocument()→ el nodo no existe en absoluto en el DOM
// El elemento existe pero está oculto
expect(screen.getByTestId("modal")).not.toBeVisible();
// El elemento no existe en el DOM
expect(screen.queryByTestId("modal")).not.toBeInTheDocument();
Elige el matcher que represente con precisión tu intención. Si estás probando lógica de renderizado condicional, usa not.toBeInTheDocument(). Si estás probando comportamiento de mostrar/ocultar basado en CSS, usa not.toBeVisible().
Anti-Patrones Comunes y Soluciones
Aquí están los errores más frecuentes que cometen los desarrolladores al probar la no existencia, junto con los enfoques correctos:
1. ❌ Usar getBy* con negación
// INCORRECTO - Esto lanzará error antes de que se ejecute la verificación
expect(screen.getByTestId('x')).not.toBeInTheDocument();
✅ Solución: Usa consultas queryBy*
// CORRECTO - Devuelve null cuando no se encuentra
expect(screen.queryByTestId('x')).not.toBeInTheDocument();
2. ❌ Verificar la longitud del array
// INCORRECTO - Se lee como matemática de implementación, lanza error cuando los elementos no existen
expect(screen.getAllByRole('menu').length).toBe(0);
✅ Solución: Usa queryBy* con el matcher apropiado
// CORRECTO - Expresivo y maneja la ausencia de forma elegante
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
3. ❌ Usar findBy* con negación
// INCORRECTO - findBy* rechaza al agotar el timeout, haciendo los tests lentos y ambiguos
await expect(screen.findByRole('menu')).rejects.toThrow();
✅ Solución: Usa waitForElementToBeRemoved o waitFor
// CORRECTO - Espera explícitamente la desaparición
await waitForElementToBeRemoved(() => screen.queryByRole('menu'));
4. ❌ Verificar no existencia con toBeVisible()
// INCORRECTO - Prueba la visibilidad CSS, no la presencia en el DOM
expect(screen.queryByRole('menu')).not.toBeVisible();
✅ Solución: Usa not.toBeInTheDocument()
// CORRECTO - Prueba la presencia real en el DOM
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
Un Ejemplo Realista de Principio a Fin
Veamos un ejemplo completo que demuestra tanto la verificación de presencia como de ausencia:
// Componente (simplificado)
/**
* Componente de cabecera de personaje que muestra condicionalmente un icono de rol
* @param showRoleIcon - Bandera para controlar la visibilidad del icono de rol
*/
function CharacterMasthead({ showRoleIcon }: { showRoleIcon: boolean }) {
return (
<header data-testid="CharacterMasthead">
<h1>Champion</h1>
{showRoleIcon && <img alt="Role icon" data-testid="role-icon" />}
</header>
);
}
// Tests
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
/**
* Suite de pruebas para el componente CharacterMasthead
* Valida el renderizado condicional del icono de rol
*/
describe("CharacterMasthead", () => {
it("no renderiza el icono de rol cuando showRoleIcon=false", () => {
// Renderiza el componente con el icono deshabilitado
render(<CharacterMasthead showRoleIcon={false} />);
// Verifica que el icono no está presente en el DOM
expect(screen.queryByTestId("role-icon")).not.toBeInTheDocument();
});
it("renderiza el icono de rol cuando showRoleIcon=true", () => {
// Renderiza el componente con el icono habilitado
render(<CharacterMasthead showRoleIcon={true} />);
// Verifica que el icono está presente en el DOM (usando getBy ya que lo esperamos)
expect(screen.getByTestId("role-icon")).toBeInTheDocument();
});
});
Este ejemplo demuestra el uso complementario de queryBy* para ausencia y getBy* para presencia.
Casos Extremos que Debes Considerar
Las aplicaciones del mundo real a menudo tienen complejidades que requieren atención especial:
Portals (Modales, Tooltips)
Cuando los componentes se renderizan en portals, consulta desde document.body o usa la opción container:
// Para portals renderizados en document.body
expect(document.body.querySelector('[role="dialog"]')).not.toBeInTheDocument();
// O usa la opción baseElement en render
const { baseElement } = render(<ModalComponent />);
expect(within(baseElement).queryByRole("dialog")).not.toBeInTheDocument();
Si tu configuración renderiza portals en un contenedor personalizado, asegúrate de que la configuración de tu entorno de test coincida con esa configuración.
Transiciones y Animaciones
Los elementos pueden permanecer en el DOM mientras se animan hacia fuera. Considera verificar primero la visibilidad, luego esperar la eliminación:
// El elemento comienza visible
expect(screen.getByRole("dialog")).toBeVisible();
// Disparar el cierre
await userEvent.click(screen.getByRole("button", { name: /close/i }));
// Esperar a que la animación se complete y el elemento sea eliminado
await waitForElementToBeRemoved(() => screen.queryByRole("dialog"));
Alternativamente, si la librería usa estados ARIA durante las transiciones, verifica esos:
// Verificar si el elemento está marcado como oculto antes de la eliminación
expect(screen.getByRole("dialog", { hidden: true }))
.toHaveAttribute("aria-hidden", "true");
Múltiples Regiones
Usa within() para delimitar consultas y evitar coincidencias falsas cuando existen elementos similares en diferentes partes de la UI:
import { within } from "@testing-library/react";
const sidebar = screen.getByRole("complementary");
expect(within(sidebar).queryByRole("menu")).not.toBeInTheDocument();
Anuncios (Live Regions)
Al probar que un banner o alerta desaparece, prefiere waitForElementToBeRemoved en el nodo específico en lugar de timeouts arbitrarios:
// Verifica que la alerta aparece
const alert = screen.getByRole("alert");
expect(alert).toBeInTheDocument();
// Espera el cierre automático
await waitForElementToBeRemoved(alert);
Guía Rápida de Decisiones
Aquí tienes un diagrama de flujo simple para elegir el enfoque correcto:
Espero que esté ausente ahora mismo (síncrono):
expect(screen.queryByRole(...)).not.toBeInTheDocument()
Espero que desaparezca después de una acción (asíncrono):
await waitForElementToBeRemoved(() => screen.queryByRole(...))
Espero que esté oculto pero aún montado:
expect(screen.getByRole(..., { hidden: true })).not.toBeVisible()
Espero que sea inaccesible pero presente en el DOM:
expect(screen.getByTestId(...)).toHaveAttribute("aria-hidden", "true")
Configuración Mínima
Para comenzar con estos patrones, asegúrate de que tu configuración de tests incluya:
// jest.setup.ts
import "@testing-library/jest-dom";
// Si usas userEvent (recomendado):
import userEvent from "@testing-library/user-event";
// Nota: No mockees los timers globalmente a menos que tengas una razón específica.
// Mockear los timers puede ocultar bugs asíncronos y hacer los tests más difíciles de depurar.
En tu package.json, asegúrate de tener las dependencias necesarias:
{
"devDependencies": {
"@testing-library/react": "^14.0.0",
"@testing-library/jest-dom": "^6.1.0",
"@testing-library/user-event": "^14.5.0"
}
}
Conclusiones Clave
Aquí están los principios fundamentales para recordar:
-
Ausencia ⇒
queryBy*+not.toBeInTheDocument()- Este es el patrón estándar para verificar que un elemento no existe en el DOM. -
Desaparición ⇒
waitForElementToBeRemoved(owaitFor) - Usa utilidades asíncronas cuando pruebes que un elemento se elimina después de una interacción o timeout. -
Prefiere consultas accesibles; usa test IDs solo cuando sea necesario - Consultas como
queryByRoleyqueryByLabelTextpromueven mejor accesibilidad y tests más resistentes. -
No uses matchers de visibilidad para verificar no existencia - Entiende la diferencia entre elementos ocultos y elementos que no existen en el DOM.
-
Elige la consulta correcta para el trabajo -
getBy*para presencia esperada,queryBy*para ausencia esperada, yfindBy*para aparición asíncrona.
Si tus tests siguen estas reglas, tu suite reflejará con precisión el comportamiento visible para el usuario, fallará por las razones correctas, y permanecerá estable a medida que tu UI evolucione.
Referencias
- Documentación Oficial de React Testing Library
- Guía de Consultas de Testing Library
- Documentación de Matchers jest-dom
Resumen
Probar la no existencia de elementos en React Testing Library requiere un enfoque diferente al de probar la presencia. La clave es usar consultas queryBy* que devuelven null en lugar de lanzar errores, combinadas con el matcher not.toBeInTheDocument(). Para desapariciones asíncronas, usa waitForElementToBeRemoved para evitar tests inestables. Siempre prefiere consultas accesibles sobre test IDs, y entiende la distinción entre elementos ocultos y elementos que no existen en el DOM. Seguir estos patrones asegura que tus tests reflejen con precisión el comportamiento del usuario y permanezcan mantenibles a medida que tu aplicación evoluciona.
Palabras Clave SEO
react testing library, test element absence, queryby react testing, assert non-existence, react unit testing, jest-dom matchers, waitforelementtoberemoved, testing library best practices, react test patterns, not.tobeindocument, accessible queries testing, frontend testing guide