Volver al Centro de conocimiento
Primeros pasos

Playwright strict mode violation: corregir duplicados

Corrige strict mode violation en Playwright: inspecciona coincidencias duplicadas, delimita el componente y verifica el resultado. Incluye una prueba ejecutable.

BotBrowser Team

Documentación

Quieres la documentación estructurada de Primeros pasos?

Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.

Un error strict mode violation de Playwright significa que una operación que necesita un elemento ha encontrado varios. Cuando el error indica resolved to 2 elements, inspecciona ambas coincidencias y delimita el localizador al componente previsto antes de cambiar los tiempos de espera. exact: true elimina las coincidencias parciales de nombre; no distingue dos controles con el mismo nombre. BotBrowser admite una configuración declarada del entorno del navegador para reproducir diferencias de diseño; no decide cuál de los controles duplicados pretendía pulsar una prueba.

TL;DR

  • Comprueba el número de coincidencias del localizador y todos los nodos encontrados en el paso que falla; una captura puede ocultar un duplicado fuera de la pantalla u oculto.
  • Delimita la búsqueda mediante un límite de la aplicación, como un diálogo o registro identificado de forma única, y después usa un nombre accesible exacto o un ID de prueba estable.
  • Distingue getByText() de getByRole(): los localizadores por rol excluyen por defecto los elementos ocultos, mientras que el texto oculto de la prueba sigue coincidiendo.
  • No uses una pausa más larga, force: true ni .first() como arreglo general de una identidad ambigua. Verifica el resultado de la aplicación después del clic.

Contenido

Lee el error antes de cambiar el localizador

La regla de unicidad de Playwright se aplica a operaciones dirigidas a un solo elemento. Un click() necesita un destino; count() describe deliberadamente una colección. Varias coincidencias son válidas en una aserción sobre una lista, por lo que reducir todos los localizadores a un elemento también rompería pruebas legítimas de listas. Registra tanto la operación que falla como el selector.

ObservaciónPrimera comprobaciónOrientación de la corrección
resolved to 2 elementsAmbos nodos coincidentes y los componentes que los contienenAsigna un localizador único al componente previsto
Save también coincide con Save draftModo de coincidencia del texto o nombre accesibleUsa exact: true cuando el contrato exige el nombre completo
Dos controles llamados SaveDiálogo, formulario, fila o estado de la aplicaciónAñade un ámbito; el nombre exacto no basta para distinguirlos
Ninguna coincidencia en el paso que fallaPágina actual, marco y condición de disponibilidadLocaliza el documento correcto y espera su propia señal
Una coincidencia, pero el clic agota el tiempoVisibilidad, estabilidad, estado habilitado y recepción de eventosInvestiga las condiciones de interacción una vez establecida la identidad

Los alias de localizador del error ayudan a identificar los candidatos; no son instrucciones para pegar el selector generado más corto. Si las navegaciones de escritorio y móvil contienen el mismo enlace, elige el punto de referencia de navegación previsto. Si dos filas de productos contienen Buy, elige la fila mediante la identidad de su registro. El arranque del navegador y la preparación de la extracción corresponden a la configuración de scraping con Playwright; arrancar correctamente el navegador no determina la intención de un selector.

Conserva el contexto del fallo

Antes de editar un selector, conserva el mensaje de error, la revisión de la prueba, la versión del paquete Playwright, la categoría de la URL final y la cadena de localizadores usada por esa acción. Una cadena como page.getByRole('dialog').getByText('Save') contiene más información que su último fragmento de texto. Copiar solo Save en un depurador puede producir un conjunto de coincidencias diferente y llevar a una corrección equivocada. Conserva también la selección del marco: un localizador dentro de frameLocator() consulta un documento diferente del que consulta un localizador de página.

Usa el número del error como la observación de la operación fallida. Un count() de diagnóstico recogido segundos después es una observación nueva; si los recuentos difieren, eso demuestra que el DOM está cambiando, no que el error fuera incorrecto. Captura la traza y la transición pertinente de la aplicación en lugar de recargar una y otra vez hasta que quede un solo nodo. Un error constante con dos controles permanentes necesita una corrección de identidad; los recuentos duplicados intermitentes necesitan además una explicación de la transición de estado.

Reproduce dos coincidencias en una página local

En un proyecto Node vacío, instala playwright, instala su Chromium con npx playwright install chromium y guarda lo siguiente como strict.mjs. La prueba no contacta con ningún sitio externo. Captura el error de unicidad previsto y después demuestra que un clic con un ámbito delimitado modificó el diálogo correcto.

import assert from 'node:assert/strict';
import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  const page = await browser.newPage();
  await page.setContent(`
    <button style="display:none">Save</button>
    <div role="dialog" aria-label="Edit profile">
      <button onclick="this.parentElement.dataset.saved='yes'">Save</button>
    </div>
  `);
  const ambiguous = page.getByText('Save', { exact: true });
  assert.equal(await ambiguous.count(), 2);
  await assert.rejects(() => ambiguous.click({ timeout: 1500 }), /strict mode violation/);
  const dialog = page.getByRole('dialog', {
    name: 'Edit profile',
    exact: true,
  });
  const save = dialog.getByRole('button', { name: 'Save', exact: true });
  assert.equal(await save.count(), 1);
  await save.click();
  assert.equal(await dialog.getAttribute('data-saved'), 'yes');
  console.log('textMatches=2 scopedMatches=1 saved=yes');
} finally {
  await browser.close();
}

Ejecuta node strict.mjs. La última línea esperada es textMatches=2 scopedMatches=1 saved=yes; el primer clic se rechaza y no se ejecuta. Quita assert.rejects() y espera directamente el clic ambiguo para ver el error strict mode violation sin capturar. El botón oculto hace que el DOM difiera deliberadamente de lo que sugiere una captura de pantalla.

La prueba ampliada del repositorio se ejecutó el 2026-11-11 con Playwright 1.58.0, Chrome estándar 157.0.8103.51 y macOS arm64. Se superaron siete aserciones, incluidas la exclusión de roles ocultos, los diálogos duplicados y una unión or(). La evidencia es una prueba sintética de localizadores, no una ejecución de BotBrowser ni una demostración sobre los tiempos de una aplicación. El arranque portable anterior usa el Chromium instalado por Playwright; la ejecución registrada usó un ejecutable explícito de Chrome estándar.

Dos coincidencias de texto Save causan un error de unicidad; un ámbito único del diálogo Edit profile produce un botón Save y un resultado de guardado verificado.

Inspecciona los candidatos y corrige su ámbito

Coloca el diagnóstico inmediatamente antes de la operación que falla. Captura solo los atributos necesarios para distinguir los candidatos, porque las exportaciones completas del DOM pueden incluir datos de cuentas. Esta instantánea aporta evidencia útil en ese momento, pero no garantiza que el DOM permanezca sin cambios.

const target = page.getByText('Save', { exact: true });
console.log(await target.count());
console.table(
  await target.evaluateAll(nodes =>
    nodes.map(node => ({
      tag: node.tagName,
      id: node.id,
      testId: node.getAttribute('data-testid'),
      dialog: node.closest('[role="dialog"]')?.getAttribute('aria-label'),
      rects: node.getClientRects().length,
    }))
  )
);

rects es una pista de diagnóstico, no un cálculo completo de visibilidad o accesibilidad de Playwright. Un nodo puede tener geometría y estar cubierto, y la visibilidad CSS puede diferir de su presencia en el árbol de accesibilidad. La referencia del localizador por rol especifica includeHidden; exact controla el nombre accesible. En la prueba, un localizador por rol predeterminado encuentra un botón Save, mientras que includeHidden: true encuentra dos. No generalices el comportamiento de los localizadores de texto a todos los motores de localización.

Prefiere un ámbito único de diálogo, formulario o registro que exprese por qué el destino es el correcto. Un data-testid estable resulta adecuado cuando la aplicación no expone una distinción semántica útil. Tanto getByRole('dialog', { name: 'Edit profile', exact: true }) como su descendiente Save deben identificar el componente previsto. Si la aplicación renderiza dos diálogos con el mismo nombre, el descendiente delimitado puede seguir coincidiendo dos veces; corrige el estado de la aplicación o añade una identidad real en lugar de otro selector por posición.

Para Save y Save draft, la coincidencia exacta basta cuando la etiqueta deseada realmente es Save. Para dos etiquetas Save idénticas, no cambia nada. Un filtro de visibilidad puede servir cuando la visibilidad sea la distinción prevista y la versión fijada de Playwright admita ese filtro; seguirá fallando si ambas copias están visibles. Valida esta condición en diseños adaptables en lugar de tratar visible como sinónimo de correcto.

Los registros repetidos necesitan una identidad de registro

En una tabla, identifica el registro antes de identificar su acción. Si la aplicación expone deliberadamente un ID de prueba para la fila, el localizador puede expresar ese contrato sin depender de un precio, un índice de fila ni una celda ajena que casualmente contiene las mismas palabras. El siguiente es un bloque de prueba ilustrativo para una tabla de pedidos propia, no otra salida de la prueba local Save. Requiere @playwright/test y una aplicación que renderice la fila y la confirmación indicadas.

const row = page.getByTestId('order-1042');
await expect(row).toHaveCount(1);
const archive = row.getByRole('button', { name: 'Archive', exact: true });
await expect(archive).toHaveCount(1);
await archive.click();
await expect(page.getByTestId('archived-order-1042')).toBeVisible();

La invariante importante es la identidad del pedido, no que el destino ocupe casualmente la primera fila. La ordenación, los filtros y un registro recién recibido pueden mover el mismo pedido a otra posición. Si una tabla virtualizada elimina del DOM las filas fuera de la pantalla, una fila ausente es un problema distinto de carga o navegación. No cambies .getByTestId('order-1042') por .first() para hacer desaparecer esa ausencia; incorpora el registro previsto a la colección renderizada mediante los controles normales de la aplicación.

Cuando no existe un ID de prueba, filter({ has: ... }) puede expresar una relación con un descendiente identificado de forma única. El localizador interior se evalúa en relación con el elemento candidato; no es una consulta independiente de página para cualquier aparición de ese registro. Verifica el número de filas filtradas, porque un número de registro impreso en un pie o en una fila de detalle anidada puede crear otra coincidencia. Prefiere una etiqueta accesible o un contrato de prueba deliberado a una cadena frágil de ancestros ligada a los contenedores CSS actuales.

Distingue los marcos de los elementos duplicados de la página

Un iframe no pasa a formar parte del documento padre solo porque aparezca en la captura. Por tanto, un localizador de página con cero coincidencias puede estar correctamente delimitado, pero al documento equivocado. Selecciona primero el marco previsto, por ejemplo con page.frameLocator('iframe[title="Checkout"]'), y después localiza su control. A la inversa, dos elementos de marco que coincidan con ese selector constituyen otra ambigüedad; añadir un nombre de botón único dentro de ambos no hace única la elección del marco.

Registra la identidad estable del marco en la aplicación en lugar de usar el primer marco devuelto por una enumeración. Los anuncios, las analíticas y las vistas previas incrustadas pueden cambiar el orden de los marcos entre ejecuciones. Para una integración de prueba propia entre orígenes, usa los localizadores de marcos que admite Playwright; no supongas que una consulta DOM en el padre puede recorrer el documento incrustado. Así se distingue un destino ausente por el límite entre documentos de dos destinos dentro del mismo documento.

Comprueba el nombre accesible y no solo el texto visible

El name de un localizador por rol es el nombre accesible calculado, que puede proceder de aria-label, aria-labelledby o del etiquetado nativo en lugar del texto dibujado dentro del botón. Dos iconos visibles pueden compartir el nombre Save aunque sus símbolos difieran. A la inversa, un botón que muestra Save puede tener un nombre accesible como Save profile. Inspecciona la información de accesibilidad del control en las herramientas del navegador antes de cambiar exact; ampliar el patrón de nombre puede añadir candidatos en lugar de recuperar el previsto.

Si un aria-labelledby incorrecto apunta varios controles a la misma etiqueta, corrige el etiquetado del componente cuando la aplicación sea tuya. Un selector exclusivo de prueba puede desbloquear el diagnóstico, pero no corrige los nombres confusos para quienes usan tecnologías de asistencia. Conserva una aserción de accesibilidad independiente si ese etiquetado forma parte del contrato del producto.

Separa la unicidad del estado listo y las carreras

Playwright comprueba las condiciones de interacción antes del clic, entre ellas un destino único, visibilidad, estabilidad, recepción de eventos y estado habilitado. La espera automática no define cuál de dos controles válidos corresponde a una tarea. Ni los tiempos de espera más largos ni las pausas fijas expresan esa elección. force: true relaja algunas comprobaciones de interacción; no resuelve la ambigüedad semántica de dos elementos coincidentes.

Durante una transición de componente pueden coexistir brevemente la copia antigua y la nueva. Si la aplicación tiene una señal de finalización, usa una aserción con reintentos automáticos como await expect(dialog).toHaveCount(1) de @playwright/test, después pulsa el descendiente previsto y comprueba el resultado de negocio. Un count() aislado es una instantánea y no reintenta como esa aserción. Ni siquiera una aserción seguida de un clic es un bloqueo atómico; los localizadores se resuelven de nuevo, por lo que un duplicado nuevo puede hacer fallar correctamente la acción posterior.

La investigación con trazas de Playwright aporta instantáneas de acciones y una cronología para decidir si los duplicados eran permanentes o transitorios. Conserva la traza de la ejecución que realmente falló; abrir el DOM actual después del fallo puede no mostrar el duplicado en absoluto. La desaparición de un indicador de carga solo sirve si la aplicación garantiza que significa que el componente pertinente ya se ha estabilizado.

a.or(b) combina alternativas; no es un operador de prioridad. Si ambas ramas coinciden, una acción de un solo elemento puede lanzar un error de unicidad. Cuando puedan aparecer dos estados distintos, espera una condición adecuada, inspecciona el estado real de la aplicación y actúa sobre su localizador específico. .first() o .nth() son razonables solo cuando el propio orden es el requisito probado, como al seleccionar el primer resultado de una clasificación. Con botones duplicados sin relación pueden pulsar silenciosamente el control equivocado después de un cambio de diseño.

Espera una transición identificada y después verifica su resultado

Supón que cerrar un editor inicia una animación y se abre otro antes de retirar el antiguo del DOM. Un localizador de diálogo con nombre puede coincidir brevemente con ambas copias. El contrato más sólido de la aplicación es un estado explícito, como un único editor activo para un registro concreto. Si el producto garantiza en cambio que el diálogo antiguo se elimina antes de que el nuevo permita interactuar, espera a que desaparezca el diálogo antiguo específico de ese registro y comprueba la identidad del nuevo. Esperar a que desaparezcan todos los diálogos sería incorrecto cuando el destino mantiene uno abierto deliberadamente.

Los reintentos deben conservar estas distinciones. Un reintento que pulsa dos veces puede enviar un formulario dos veces si el primer clic funcionó, pero su aserción de resultado se ejecutó demasiado pronto. Mantén un identificador único de operación o un marcador de finalización de la aplicación cuando el flujo lo requiera. No ignores una excepción de unicidad para continuar hasta un aviso de éxito sin relación: ese aviso puede corresponder a una acción anterior. La prueba Save verifica un atributo local data-saved del diálogo delimitado precisamente para que un nodo ajeno no pueda cumplir su condición de éxito.

Después de reparar un selector, prueba un contraejemplo además del recorrido esperado: renderiza un segundo registro con la misma etiqueta de botón, reordena los registros y conserva un componente inactivo en el DOM. El localizador reparado debe seguir seleccionando el registro previsto o fallar claramente si esa identidad se duplica. Son comprobaciones específicas de la aplicación, no afirmaciones de que superar una prueba sintética cubra todos los frameworks o transiciones. Los dos diálogos con el mismo nombre de la prueba ampliada demuestran intencionadamente ese último límite de fallo.

Mantén las diferencias de entorno en su propia capa

Un punto de ruptura puede renderizar tanto el marcado de escritorio como el móvil, y un cambio de fuente o de ventana gráfica puede cambiar el diseño de diálogo que aparece. Reproduce el caso con la misma versión de navegador, ventana gráfica, configuración regional y estado de aplicación antes de atribuir distintos candidatos al comportamiento del navegador. Un perfil de navegador nuevo no repara un selector que coincide deliberadamente con dos filas de cuentas.

BotBrowser admite configuraciones declaradas de pantalla, fuentes y entorno del navegador descritas en la referencia de configuración de perfiles. No proporciona la identidad de los componentes de una aplicación, no elimina un duplicado oculto ni garantiza que un clic produzca el resultado de negocio. Mantén esta comparación de entorno después del diagnóstico del localizador y del estado de la aplicación; las siete comprobaciones de la prueba local no demuestran ningún resultado de huella digital ni de servicio remoto.

Preguntas frecuentes

¿Es el modo estricto de JavaScript?

No. Una directiva 'use strict' controla el comportamiento del lenguaje JavaScript. La unicidad de los localizadores de Playwright controla cuántos elementos acepta una operación de destino único. Desactivar el modo estricto de JavaScript no cambia el conjunto de coincidencias del localizador.

¿Se aplica el mismo diagnóstico a Playwright para Python?

Sí; la decisión sobre el localizador es la misma, aunque Python usa nombres como get_by_role() y exact=True. Conserva el ámbito del componente al traducir el código. Las excepciones de elementos de Java Selenium pertenecen a otra API y no deben clasificarse a partir de una cadena de error de Playwright.

¿Por qué count funciona mientras click falla?

count() describe una colección y permite más de un resultado. Un clic necesita un único elemento previsto. Por tanto, un recuento de dos es una observación correcta de la ambigüedad, no una prueba de que la acción siguiente vaya a aceptarse.

Fuentes

Conclusión práctica

Ante un error strict mode violation de Playwright, conserva el localizador que falla, inspecciona el conjunto completo de coincidencias y expresa el componente previsto en el selector. Ejecuta primero la prueba local, después sustituye su límite de diálogo por la identidad real de la aplicación y comprueba el resultado del clic. Investiga la disponibilidad o el entorno del navegador solo cuando el destino previsto ya no sea ambiguo.

#Playwright#Strict Mode Violation#Localizadores#Depuración De Pruebas

Este artículo forma parte de la guía: Primeros pasos con automatización de navegador: mapa inicial

Ejecuta la misma comprobación en BotBrowser

Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.