<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Silviu Technology</title>
    <description>The latest articles on DEV Community by Silviu Technology (@silviutech).</description>
    <link>https://dev.to/silviutech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4013497%2F7d1c82f4-cd78-412d-908d-981280990b57.png</url>
      <title>DEV Community: Silviu Technology</title>
      <link>https://dev.to/silviutech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/silviutech"/>
    <language>en</language>
    <item>
      <title>Agentes LLM: simula login social sin falsos positivos</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 29 Sep 2026 23:23:37 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-simula-login-social-sin-falsos-positivos-4d1f</link>
      <guid>https://dev.to/silviutech/agentes-llm-simula-login-social-sin-falsos-positivos-4d1f</guid>
      <description>&lt;p&gt;Cuando un agente LLM prueba un signup con login social, “el botón respondió” no significa que el flujo sea correcto. Puede haber usado una identidad vieja, seguir una sesión de navegador equivocada o confirmar una cuenta distinta de la que el test esperaba. El resultado parece verde, pero la evidencia es debil.&lt;/p&gt;

&lt;p&gt;En workflows de desarrollo con agentes, trato la identidad de prueba como una dependencia explícita, no como un detalle del navegador. El agente puede elegir el siguiente paso y explicar un fallo, pero un contrato pequeño debe demostrar qué identidad se usó, qué estado cambió y qué señales fueron observadas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un agente puede completar el flujo equivocado
&lt;/h2&gt;

&lt;p&gt;Un login social tiene más estado que un formulario de email y contraseña. Hay una sesión del proveedor, cookies del navegador, consentimiento, redirecciones y, a veces, un email de confirmación. Si todo eso vive en el mismo perfil, un agente puede “pasar” porque heredó una sesión que ya estaba autenticada.&lt;/p&gt;

&lt;p&gt;Los falsos positivos más comunes son estos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El navegador conserva cookies de una cuenta anterior.&lt;/li&gt;
&lt;li&gt;El proveedor redirige al entorno equivocado.&lt;/li&gt;
&lt;li&gt;El email asociado a la identidad no coincide con el usuario creado.&lt;/li&gt;
&lt;li&gt;El agente lee un mensaje antiguo y lo toma por la confirmación actual.&lt;/li&gt;
&lt;li&gt;Un reintento crea otra cuenta, aunque el resumen solo diga &lt;code&gt;ok&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La solución no es pedirle al modelo que sea mas cuidadoso. Es dividir el flujo en hechos verificables. Como en otros sistemas con correo, conviene &lt;a href="https://dev.to/alexcarteruk/kubernetes-correos-utiles-en-cambios-de-red-36c8"&gt;pensar los correos de prueba como una señal operativa&lt;/a&gt;, con una edad, un identificador y un contexto de ejecución.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué significa simular un login social
&lt;/h2&gt;

&lt;p&gt;Simular no quiere decir falsificar una cuenta real ni probar contra producción. Quiere decir construir una identidad sintética controlada en staging, con permisos mínimos y un ciclo de vida corto. El agente debe recibir una referencia como &lt;code&gt;identity-qa-17&lt;/code&gt;, no una contraseña pegada en el prompt.&lt;/p&gt;

&lt;p&gt;El mapa mental es sencillo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;identidad sintética -&amp;gt; sesión aislada -&amp;gt; login social -&amp;gt; signup
          |                |                |             |
       contrato       cookies nuevas    redirect host   evento de dominio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada flecha tiene una comprobación. Si la sesión no es nueva, el test se detiene. Si el &lt;code&gt;redirect_uri&lt;/code&gt; no apunta a staging, se marca como fallo de seguridad. Si el evento de dominio no contiene la misma referencia de identidad, no se afirma que el signup terminó.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato de identidad sintética
&lt;/h2&gt;

&lt;p&gt;Un fixture útil puede describirse con pocos campos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"identity_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"identity-qa-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider_subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider-subject-91"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email_ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inbox-qa-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"staging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_started_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-30T06:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-30T06:15:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;provider_subject&lt;/code&gt; es un identificador de prueba, no un dato personal. &lt;code&gt;email_ref&lt;/code&gt; apunta al fixture de correo sin escribir el contenido completo en cada log. El contrato tambien necesita expiración: una identidad de staging que sobrevive semanas termina pareciéndose demasiado a una cuenta compartida.&lt;/p&gt;

&lt;p&gt;Para flujos que envían un código o un enlace, el adaptador de correo debería devolver solo candidatos recientes, su &lt;code&gt;message_id&lt;/code&gt; y metadatos sanitizados. La comparación del asunto, del host y de la antigüedad debe ser código determinista. Un LLM puede interpretar “el proveedor rechazó el consentimiento”, pero no debe inventar que una identidad fue verificada.&lt;/p&gt;

&lt;p&gt;Cuando el signup necesita reenvíos, es útil &lt;a href="https://dev.to/silviutech/fastapi-reenvios-seguros-en-signup-5f56"&gt;diseñar reenvíos seguros en un signup&lt;/a&gt; con el mismo &lt;code&gt;run_id&lt;/code&gt;. Así un reintento queda relacionado con el intento original y no se confunde con un nuevo usuario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación por checkpoints
&lt;/h2&gt;

&lt;p&gt;Un workflow manejado por un agente puede tener cinco checkpoints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Preparar:&lt;/strong&gt; crear &lt;code&gt;run_id&lt;/code&gt;, fixture de identidad, inbox lógico y expectativas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aislar:&lt;/strong&gt; abrir un contexto de navegador limpio y comprobar que no hay cookies previas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actuar:&lt;/strong&gt; ejecutar el login social y capturar el host de cada redirección.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observar:&lt;/strong&gt; consultar los eventos de autenticación y el mensaje de confirmación esperado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verificar:&lt;/strong&gt; comparar referencias, timestamps y estado final antes de emitir el recibo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El recibo debería decir &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;redirect_mismatch&lt;/code&gt; o &lt;code&gt;ambiguous&lt;/code&gt;. “Ambiguous” es un resultado valido: obliga a investigar en vez de convertir señales incompletas en éxito. En CI, guardaría el recibo y un snapshot pequeño; guardar todo el cuerpo del email hace el debugging mas costoso y aumenta la retención de datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs y límites
&lt;/h2&gt;

&lt;p&gt;La identidad sintética reduce contaminación entre tests, pero añade una fábrica de fixtures y una política de expiración. Un proveedor social real puede cambiar su pantalla o imponer límites, por lo que no conviene depender de una prueba end-to-end para cada commit.&lt;/p&gt;

&lt;p&gt;Mi separación práctica es usar tres capas: pruebas de contrato para el adaptador, pruebas de integración con un proveedor controlado y pocas pruebas end-to-end nocturnas. El agente participa en la preparación y en el diagnóstico; las aserciones críticas permanecen en código. Es menos flexible, pero mucho mas fácil de auditar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para el siguiente workflow
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿La identidad tiene &lt;code&gt;identity_ref&lt;/code&gt; y fecha de expiración?&lt;/li&gt;
&lt;li&gt;¿El navegador empieza sin cookies ni sesiones heredadas?&lt;/li&gt;
&lt;li&gt;¿Cada redirección comprueba el host esperado?&lt;/li&gt;
&lt;li&gt;¿El email se vincula por &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;message_id&lt;/code&gt; y antigüedad?&lt;/li&gt;
&lt;li&gt;¿Un reintento conserva la relación con el intento original?&lt;/li&gt;
&lt;li&gt;¿Los logs evitan secretos, cuerpos completos y datos personales?&lt;/li&gt;
&lt;li&gt;¿El resultado puede ser &lt;code&gt;ambiguous&lt;/code&gt; sin forzar un falso éxito?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un agente LLM es muy útil para coordinar pasos y resumir evidencia. El límite sano es que no sea la fuente de verdad sobre la identidad, el correo o el estado de autenticación. Si el sistema deja ese contrato claro, hasta una nota con &lt;code&gt;temp mailid&lt;/code&gt; o &lt;code&gt;dummy e mail&lt;/code&gt; se puede rastrear sin convertirla en una cuenta permanente. Y el resultado deja de ser “parece que funcionó” para convertirse en una señal que otro proceso puede revisar.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: reintentos seguros para emails de prueba</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 29 Sep 2026 02:23:21 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-reintentos-seguros-para-emails-de-prueba-6cj</link>
      <guid>https://dev.to/silviutech/fastapi-reintentos-seguros-para-emails-de-prueba-6cj</guid>
      <description>&lt;p&gt;Los flujos de email de prueba suelen fallar por una razón poco visible: un timeout no significa que la operación no ocurrió. Un cliente puede repetir la llamada, un worker puede procesar el mismo evento dos veces y un test puede hacer polling mientras el estado todavía cambia.&lt;/p&gt;

&lt;p&gt;En proyectos Python suelo tratar este problema como un contrato de API, no como un simple &lt;code&gt;try/except&lt;/code&gt;. La idea es que cada reintento tenga una respuesta predecible, que el backend no duplique el trabajo y que los logs permitan entender lo que pasó. Este patrón funciona especialmente bien con FastAPI y es fácil de adaptar a automatización de CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un reintento no es una nueva operación
&lt;/h2&gt;

&lt;p&gt;Imagina un endpoint que crea un buzón de prueba y espera un mensaje:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /test-mailboxes
{
  "purpose": "signup"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si la respuesta tarda más que el timeout del cliente, repetir el &lt;code&gt;POST&lt;/code&gt; puede crear dos recursos. El test quizá solo observa uno y deja el segundo consumiendo espacio. En una ejecución puntual parece un fallo de red, pero despues se convierte en datos huérfanos y resultados difíciles de comparar.&lt;/p&gt;

&lt;p&gt;La solución empieza con una distinción sencilla: el &lt;code&gt;request_id&lt;/code&gt; identifica la petición, mientras que la &lt;code&gt;Idempotency-Key&lt;/code&gt; identifica la operación que se puede repetir. La segunda debe persistirse durante el tiempo suficiente para que el cliente reciba el resultado original.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define estados antes de escribir el endpoint
&lt;/h2&gt;

&lt;p&gt;Para un flujo de email de prueba, unos estados pequeños son más útiles que una cadena de mensajes libres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created -&amp;gt; waiting_for_message -&amp;gt; received
                         \-&amp;gt; expired
                         \-&amp;gt; failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada transición debería guardar &lt;code&gt;updated_at&lt;/code&gt;, un motivo corto y un identificador de correlación. No guardes el mensaje completo en el log: con el asunto, el &lt;code&gt;message_id&lt;/code&gt; hash y el tamaño suele bastar para investigar.&lt;/p&gt;

&lt;p&gt;También conviene separar el estado de la solicitud del estado del mensaje. Una solicitud puede estar &lt;code&gt;completed&lt;/code&gt; aunque el mensaje recibido sea un rebote, por ejemplo. Esa separación evita que un reintento cambie silenciosamente el significado del resultado.&lt;/p&gt;

&lt;p&gt;Si el producto necesita generar una dirección para pruebas, una búsqueda como &lt;code&gt;temp mail generator&lt;/code&gt; puede llevar a opciones distintas; en el código importa más definir la duración, el aislamiento y la limpieza del recurso. Para pruebas manuales, &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;obtener un correo temporal&lt;/a&gt; puede ser útil, pero no lo uses para datos personales ni como dependencia oculta de una suite automatizada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una clave de idempotencia en FastAPI
&lt;/h2&gt;

&lt;p&gt;El handler puede recibir la clave desde una cabecera y delegar la decisión a una capa de servicio. El ejemplo es pequeño a propósito:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/test-mailboxes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_mailbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Falta Idempotency-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mailbox_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción, &lt;code&gt;find_request&lt;/code&gt; y &lt;code&gt;save_request&lt;/code&gt; deben participar en una garantía real de unicidad. Un diccionario en memoria sirve para una demo, pero no para varios workers. Usa una restricción única en PostgreSQL o Redis con una política de expiración y considera qué ocurre si el proceso muere después de crear el buzón pero antes de guardar la respuesta.&lt;/p&gt;

&lt;p&gt;La clave también debe estar ligada a una huella de la petición. Si el mismo cliente reutiliza la clave con otro &lt;code&gt;purpose&lt;/code&gt;, responde &lt;code&gt;409 Conflict&lt;/code&gt;. Aceptarlo como si nada produce bugs muy raros de reproducir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling y webhooks con evidencia útil
&lt;/h2&gt;

&lt;p&gt;El polling necesita límites claros: intervalo, deadline y estado terminal. Un test que pregunta indefinidamente oculta una regresión. Una implementación simple puede usar backoff y detenerse cuando &lt;code&gt;received&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; o &lt;code&gt;failed&lt;/code&gt; aparece.&lt;/p&gt;

&lt;p&gt;Para el equipo frontend, es útil &lt;a href="https://dev.to/silviutech/react-valida-email-sin-respuestas-viejas-33od"&gt;validar emails sin respuestas viejas&lt;/a&gt; y mostrar el &lt;code&gt;correlation_id&lt;/code&gt; cuando algo termina. Si el trabajo tarda, una cola con estados visibles también ayuda a &lt;a href="https://dev.to/silviutech/fastapi-colas-visibles-para-emails-largos-5dgd"&gt;hacer visibles las colas de emails largos&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Un webhook debe ser idempotente por &lt;code&gt;event_id&lt;/code&gt;. Antes de ejecutar una acción, guarda ese identificador con una restricción única. Si llega de nuevo, devuelve &lt;code&gt;200&lt;/code&gt; y no repitas el efecto. La respuesta exitosa significa “evento conocido y procesado”, no necesariamente “se hizo trabajo nuevo”. Es un matiz pequeño, pero ahorra alertas falsas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rápidas sobre reintentos
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Cuánto tiempo guardo una clave?
&lt;/h3&gt;

&lt;p&gt;Durante la ventana máxima de reintento del cliente, más un margen. Para una suite de CI, una hora puede ser suficiente; elige según tu flujo y documentalo. No existe un número universal.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Un timeout debe devolver error siempre?
&lt;/h3&gt;

&lt;p&gt;No necesariamente. El cliente puede recibir &lt;code&gt;202 Accepted&lt;/code&gt; con un estado consultable. Si ya existe una respuesta final asociada a la clave, devuelve ese mismo resultado para que el consumidor no tenga que adivinar.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo pruebo que la idempotencia funciona?
&lt;/h3&gt;

&lt;p&gt;Envía dos peticiones concurrentes con la misma clave y comprueba que solo exista un recurso. Después repite con el mismo valor y un cuerpo diferente: debe fallar de forma explícita.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Hay una clave de idempotencia para cada operación repetible.&lt;/li&gt;
&lt;li&gt;[ ] La base de datos impone unicidad, incluso con varios workers.&lt;/li&gt;
&lt;li&gt;[ ] Los estados terminales y sus motivos están documentados.&lt;/li&gt;
&lt;li&gt;[ ] Polling tiene deadline y no confunde un mensaje viejo con uno nuevo.&lt;/li&gt;
&lt;li&gt;[ ] Webhooks deduplican por &lt;code&gt;event_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Los logs no contienen tokens, direcciones privadas ni cuerpos completos.&lt;/li&gt;
&lt;li&gt;[ ] La limpieza de buzones de prueba tiene un TTL verificable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este diseño no elimina todos los fallos de red. Hace algo más valioso: convierte los reintentos en comportamiento definido. Con estados pequeños, claves persistentes y evidencia suficiente, FastAPI puede mantener los tests rápidos sin volverlos frágiles.&lt;/p&gt;

&lt;p&gt;En documentación y búsquedas también aparecerán expresiones como &lt;code&gt;temp org mail&lt;/code&gt; o &lt;code&gt;dummy e mail&lt;/code&gt;; tratarlas como entradas imperfectas, no como estados válidos del dominio, ayuda a que el sistema sea más tolerante sin contaminar sus datos internos.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>FastAPI: contratos de tiempo para emails de prueba</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:23:48 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-contratos-de-tiempo-para-emails-de-prueba-60</link>
      <guid>https://dev.to/silviutech/fastapi-contratos-de-tiempo-para-emails-de-prueba-60</guid>
      <description>&lt;h1&gt;
  
  
  FastAPI: contratos de tiempo para emails de prueba
&lt;/h1&gt;

&lt;p&gt;Cuando una prueba de registro depende de un email, el fallo no siempre está en FastAPI. Puede que el mensaje tarde, que el buzón de prueba todavía no esté listo o que el worker de CI haya perdido un evento. El problema aparece cuando la prueba solo sabe decir “espera un poco más”.&lt;/p&gt;

&lt;p&gt;En mis automatizaciones de backend me resulta más útil tratar la llegada del email como un pequeño contrato: estados claros, un presupuesto de tiempo y una evidencia que permita repetir el diagnóstico. Así el flujo deja de ser un &lt;code&gt;sleep(10)&lt;/code&gt; misterioso y se convierte en una pieza que podemos observar.&lt;/p&gt;

&lt;p&gt;Este patrón sirve para pruebas de verificación, upgrades y webhooks. También combina bien con una cuenta aislada —por ejemplo, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;burner email generator&lt;/a&gt;— siempre que el buzón se use solo para datos de prueba y no para información real.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: esperar no es una estrategia
&lt;/h2&gt;

&lt;p&gt;Un polling ingenuo suele verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tiene tres defectos. No distingue entre “todavía no llegó” y “la API ya falló”, tarda lo mismo cuando el mensaje llega en un segundo y oculta cuántos intentos se hicieron. En un portátil puede parecer suficiente; bajo carga, ese &lt;code&gt;sleep&lt;/code&gt; se multiplica por cada worker y el diagnóstico se vuelve confuso.&lt;/p&gt;

&lt;p&gt;Además, un correo temporal o una bandeja efímera no debe ser la única fuente de verdad. La aplicación debería exponer un &lt;code&gt;request_id&lt;/code&gt; o un identificador de evento que conecte el signup, el envío y la lectura del mensaje. Es un detalle chico, pero ahorra bastante tiempo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define un contrato de estados
&lt;/h2&gt;

&lt;p&gt;Antes de escribir el endpoint, define estados que una prueba pueda interpretar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;queued&lt;/code&gt;: se aceptó la solicitud y el email está en cola.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sent&lt;/code&gt;: el proveedor de correo confirmó el envío.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;received&lt;/code&gt;: el fixture encontró el mensaje esperado.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expired&lt;/code&gt;: se terminó el presupuesto de espera.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;failed&lt;/code&gt;: ocurrió un error no recuperable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El cliente de pruebas no debería inferir un estado mirando texto libre. Un esquema pequeño con &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;request_id&lt;/code&gt;, &lt;code&gt;attempts&lt;/code&gt; y &lt;code&gt;last_checked_at&lt;/code&gt; es suficiente para comenzar. Para evitar carreras, haz que &lt;code&gt;received&lt;/code&gt; sea idempotente: leer dos veces el mismo email no debe crear dos verificaciones.&lt;/p&gt;

&lt;p&gt;Si ya estás trabajando con SaaS, vale la pena &lt;a href="https://dev.to/hannahdev56/como-probar-upgrades-por-email-sin-ruido-en-tu-saas"&gt;probar upgrades por email sin ruido&lt;/a&gt;. La misma separación entre evento, buzón y resultado evita que una prueba de pago dependa accidentalmente de un mensaje viejo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementa polling con un presupuesto de tiempo
&lt;/h2&gt;

&lt;p&gt;Un helper sencillo puede recibir un deadline, no solo una cantidad fija de intentos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wait_for_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="n"&gt;Awaitable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="n"&gt;timeout_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;interval_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;timeout_seconds&lt;/span&gt;
    &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email not received after &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;time.monotonic()&lt;/code&gt; es importante: un cambio en el reloj del sistema no debe alargar la prueba. En un sistema real también limitaría el número máximo de mensajes revisados y filtraría por &lt;code&gt;request_id&lt;/code&gt;, destinatario y una marca de tiempo reciente. Buscar solo por asunto es una receta para falsos positivos, aunque el asunto se vea correcto.&lt;/p&gt;

&lt;p&gt;Un backoff corto puede reducir llamadas cuando la bandeja tarda, pero no lo uses para tapar una cola saturada. Registra el intervalo, el número de intentos y el motivo del último resultado. Esas métricas cuentan más que otro retry ciego.&lt;/p&gt;

&lt;h2&gt;
  
  
  Haz que FastAPI y CI dejen evidencia
&lt;/h2&gt;

&lt;p&gt;El endpoint puede devolver el estado actual sin bloquear el worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/test-emails/{request_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;email_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;load_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_checked_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_checked_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El job de CI puede consultar este endpoint y guardar el JSON de la última respuesta como artefacto. Si el test falla, conviene conservar el &lt;code&gt;request_id&lt;/code&gt;, pero nunca el contenido completo del email si podría contener tokens. Redacta enlaces de verificación y limita los logs; privacidad importa incluso en fixtures.&lt;/p&gt;

&lt;p&gt;Para evitar que una prueba antigua contamine la siguiente, crea un identificador único por ejecución y elimina el mensaje después de una prueba exitosa o vencida. La limpieza debe ocurrir en un &lt;code&gt;finally&lt;/code&gt;, y debe tolerar que el recurso ya no exista. Esta parte suele olvidarse en el primer borrador, luego el buzón crece y las pruebas empiezan a leer resultados equivocados.&lt;/p&gt;

&lt;p&gt;Una guía relacionada sobre &lt;a href="https://dev.to/silviutech/como-probar-emails-transaccionales-en-fastapi-sin-mezclar-bandejas-ni-eventos-5f0m"&gt;emails transaccionales sin mezclar bandejas&lt;/a&gt; ayuda a pensar en esta frontera como una decisión de arquitectura, no como un truco de QA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A: dudas habituales
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo hacer polling o esperar un webhook?
&lt;/h3&gt;

&lt;p&gt;Usa webhook cuando el entorno sea estable y puedas verificar la firma del evento. Mantén polling como fallback para pruebas locales y para diagnosticar entregas tardías. Ambos caminos deben actualizar el mismo contrato de estados.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué timeout es correcto?
&lt;/h3&gt;

&lt;p&gt;Mide primero el tiempo normal de tu proveedor y añade margen pequeño. Un timeout de 30 segundos puede ser razonable para CI, pero no es una constante universal. Si cada ejecución necesita 5 minutos, revisa la cola antes de subir el número.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Puedo reutilizar un buzón?
&lt;/h3&gt;

&lt;p&gt;Sí, si cada mensaje tiene un identificador único y hay limpieza verificable. Reutilizarlo sin aislamiento crea una prueba que pasa por accidente. Incluso &lt;code&gt;tem email&lt;/code&gt; o &lt;code&gt;tempail&lt;/code&gt; que aparecen en una búsqueda manual no deben entrar como criterio de coincidencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Genera un &lt;code&gt;request_id&lt;/code&gt; único por prueba.&lt;/li&gt;
&lt;li&gt;Modela &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;received&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; y &lt;code&gt;failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Usa un deadline monotónico y limita los intentos.&lt;/li&gt;
&lt;li&gt;Filtra por identificador, destinatario y fecha, no solo por asunto.&lt;/li&gt;
&lt;li&gt;Guarda estados y métricas, pero redacta tokens del email.&lt;/li&gt;
&lt;li&gt;Limpia fixtures en &lt;code&gt;finally&lt;/code&gt; y acepta recursos ausentes.&lt;/li&gt;
&lt;li&gt;Conserva un JSON pequeño cuando CI falle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con este contrato, FastAPI no necesita adivinar si el correo llegará. La prueba tiene un límite, el equipo tiene evidencia y una demora real deja de parecer un fallo aleatorio.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: validación de email sin saltos de layout</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:24:07 +0000</pubDate>
      <link>https://dev.to/silviutech/react-validacion-de-email-sin-saltos-de-layout-9b0</link>
      <guid>https://dev.to/silviutech/react-validacion-de-email-sin-saltos-de-layout-9b0</guid>
      <description>&lt;p&gt;Un formulario de registro puede sentirse lento aunque la red responda rápido. Basta con que el mensaje de validación aparezca debajo del campo y empuje todo el contenido para que el botón cambie de posición justo cuando el usuario intenta pulsarlo. Ese pequeño salto afecta la confianza y complica la navegación con teclado.&lt;/p&gt;

&lt;p&gt;Al trabajar con formularios en React, me resulta útil tratar la validación como un problema de diseño de estados, no solo como una expresión regular. Un usuario puede ver el campo vacío, un aviso local, una comprobación en progreso, un error del servidor o una confirmación. Si cada estado tiene una geometría previsible, la interfaz se siente más estable y el rendimiento percibido mejora.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: validar cambia el tamaño de la interfaz
&lt;/h2&gt;

&lt;p&gt;El caso típico es un &lt;code&gt;div&lt;/code&gt; que solo se renderiza cuando el email es inválido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Escribe un email válido.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El código es correcto, pero el párrafo no ocupa espacio antes de que exista el error. Cuando aparece, mueve los controles siguientes. El usuario entiende que algo pasó, pero tambien puede perder el foco visual o hacer clic en otro elemento.&lt;/p&gt;

&lt;p&gt;La solución no es ocultar el mensaje. Los errores deben ser visibles, estar asociados al campo y explicar cómo continuar. El objetivo es reservar una zona estable para ese mensaje y cambiar su contenido sin cambiar innecesariamente la composición.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserva espacio para cada estado
&lt;/h2&gt;

&lt;p&gt;Una opción sencilla es dar una altura mínima al contenedor del mensaje. El valor exacto depende de la tipografía y del diseño, pero debe cubrir una o dos líneas en móvil:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fieldMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.35rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b42318&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.fieldMessage&lt;/span&gt;&lt;span class="nd"&gt;:empty&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;visibility: hidden&lt;/code&gt; conserva el espacio sin presentar texto a la vista. Para un estado vacío es una elección más segura que &lt;code&gt;display: none&lt;/code&gt; cuando la estabilidad visual importa. Si el mensaje puede ocupar más de una línea, usa una altura mínima generosa y prueba la versión con zoom del navegador.&lt;/p&gt;

&lt;p&gt;También conviene reservar espacio para un indicador de estado. Un formulario que cambia directamente de “comprobar” a “email no disponible” puede parpadear. Una zona con texto corto y estable resulta mas facil de leer que un spinner sin explicación.&lt;/p&gt;

&lt;h2&gt;
  
  
  React y CSS para mensajes previsibles
&lt;/h2&gt;

&lt;p&gt;El componente puede mantener un estado explícito y una relación clara entre el &lt;code&gt;label&lt;/code&gt;, el input y el mensaje:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmailState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;hasError&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email-message"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email-message"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"fieldMessage"&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El mensaje debe cambiar cuando cambia el estado, no como efecto secundario de un render inesperado. &lt;code&gt;aria-live="polite"&lt;/code&gt; permite anunciar el resultado sin interrumpir cada pulsación. Para errores críticos puede ser necesario otro nivel de urgencia, pero no conviene convertir cada validación local en una alarma.&lt;/p&gt;

&lt;p&gt;En validaciones asíncronas, cancela o ignora respuestas antiguas. Una respuesta lenta para &lt;code&gt;ana@ejemplo.com&lt;/code&gt; no debe reemplazar el resultado más nuevo de &lt;code&gt;ana@ejemplo.org&lt;/code&gt;. Este detalle evita mensajes contradictorios y reduce el trabajo visual que el usuario tiene que interpretar. Un registro de eventos pequeño también ayuda: aquí sirven &lt;a href="https://dev.to/silviutech/fastapi-logs-utiles-para-colas-de-email"&gt;logs útiles para una cola de email&lt;/a&gt; para separar una respuesta tardía de un error real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesibilidad sin perder rendimiento
&lt;/h2&gt;

&lt;p&gt;La estabilidad de layout y la accesibilidad se refuerzan. Un mensaje que aparece lejos del campo es dificil de relacionar; un mensaje que mueve el botón puede causar activaciones accidentales. Mantén el texto cerca del control, pero evita que el contenido cambiante fuerce toda la página a recolocarse.&lt;/p&gt;

&lt;p&gt;Hay cuatro comprobaciones rápidas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navega el formulario solo con teclado y confirma que el foco no salta.&lt;/li&gt;
&lt;li&gt;Comprueba que &lt;code&gt;aria-describedby&lt;/code&gt; apunta a un nodo que siempre existe.&lt;/li&gt;
&lt;li&gt;Prueba mensajes largos con aumento de texto y una pantalla estrecha.&lt;/li&gt;
&lt;li&gt;Respeta &lt;code&gt;prefers-reduced-motion&lt;/code&gt; si usas transiciones en el mensaje.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En un formulario que acepta una dirección de prueba o un generador de correo falso, la interfaz debe seguir explicando el estado sin asumir que el usuario ve el color. El color ayuda, pero el texto, el icono con nombre accesible y &lt;code&gt;aria-invalid&lt;/code&gt; llevan la información principal. Para revisar patrones de foco y avisos, consulta estos &lt;a href="https://dev.to/silviutech/react-warnings-de-correo-sin-robar-el-foco-7k1"&gt;warnings de React sin robar el foco&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Si necesitas probar una confirmación de entrega, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;correo temporal&lt;/a&gt; puede separar el escenario de pruebas de una bandeja personal. El test debe verificar también el estado de la UI: mensaje visible, foco conservado y botón habilitado o deshabilitado según la regla. No basta con comprobar que llegó un email.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A: dudas habituales
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo validar mientras el usuario escribe?
&lt;/h3&gt;

&lt;p&gt;No siempre. La validación local de formato puede ocurrir después de que el campo pierda el foco o cuando hay una pausa breve. Validar en cada tecla suele producir demasiado ruido. La disponibilidad o confirmación remota debería tener una acción explícita y un estado de carga entendible.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Una altura fija siempre evita los saltos?
&lt;/h3&gt;

&lt;p&gt;No. El texto puede envolver en pantallas pequeñas, cambiar con la localización o crecer por preferencias de accesibilidad. Usa &lt;code&gt;min-height&lt;/code&gt;, prueba varios anchos y permite que el contenedor crezca cuando haga falta. Es mejor un movimiento ocasional y comprensible que texto cortado.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago con un dato de prueba como &lt;code&gt;temp mailid&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Trátalo como un caso negativo de datos, no como una dirección válida ni como un enlace. El estado del formulario debe explicar el formato esperado sin exponer detalles internos del test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;El mensaje de validación tiene una región reservada.&lt;/li&gt;
&lt;li&gt;El campo conserva foco y usa &lt;code&gt;aria-invalid&lt;/code&gt; cuando corresponde.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aria-describedby&lt;/code&gt; apunta a un elemento estable.&lt;/li&gt;
&lt;li&gt;Los estados local, remoto y de error tienen textos distintos.&lt;/li&gt;
&lt;li&gt;Las respuestas asíncronas antiguas no pisan el resultado actual.&lt;/li&gt;
&lt;li&gt;El diseño funciona con zoom, móvil y mensajes de dos líneas.&lt;/li&gt;
&lt;li&gt;Las pruebas revisan la interfaz además del correo recibido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una validación bien diseñada no solo rechaza entradas incorrectas. Hace visible qué está pasando, mantiene el formulario tranquilo y deja que React, CSS y las ayudas de accesibilidad trabajen en la misma dirección.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>css</category>
      <category>performance</category>
    </item>
    <item>
      <title>Playwright Email Tests Need a Fixture State Model</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 27 Sep 2026 20:23:45 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-a-fixture-state-model-1dpo</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-a-fixture-state-model-1dpo</guid>
      <description>&lt;p&gt;Email verification tests often look simple in a test plan: create an account, open the message, click the link, and assert that the user is verified. The hard part is usually not the click. It is deciding what the mailbox currently means.&lt;/p&gt;

&lt;p&gt;An empty inbox can mean that the application has not sent anything, the provider is still processing the message, the test is reading the wrong mailbox, or the message was already consumed by another test. If a Playwright test treats all four situations as “not found,” the failure report becomes guesswork.&lt;/p&gt;

&lt;p&gt;In my QA work, an email fixture became much more useful after I gave its lifecycle explicit states. A fixture should explain what it has done, what evidence it has observed, and what cleanup remains. That small model makes a use and throw email address safer to use in automation and makes a temp mail mail workflow easier to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an email fixture needs explicit states
&lt;/h2&gt;

&lt;p&gt;Most flaky email tests mix setup, waiting, parsing, and assertions in one long flow. When the assertion fails, it is hard to tell whether the signup request failed or the test simply polled too early.&lt;/p&gt;

&lt;p&gt;The fixture dont need to expose every provider detail. It does need a stable vocabulary. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MailboxState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;waiting_for_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message_received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link_extracted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;consumed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cleaned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These states separate facts from expectations. &lt;code&gt;waiting_for_message&lt;/code&gt; says that the application has submitted the action and the test is waiting. &lt;code&gt;message_received&lt;/code&gt; says that a message matching the test correlation rule was observed. &lt;code&gt;expired&lt;/code&gt; says the allowed wait ended without evidence. Those are very different debugging paths.&lt;/p&gt;

&lt;p&gt;Without this distinction, teams often increase the timeout until the suite appears more reliable. That can hide a real delivery regression and make every CI run slower. A state model gives the QA engineer a reason for each wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the mailbox lifecycle
&lt;/h2&gt;

&lt;p&gt;A practical fixture can expose four operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create&lt;/strong&gt; a unique mailbox or address for the test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait&lt;/strong&gt; for a message using a bounded polling policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract&lt;/strong&gt; a link or code while preserving a safe receipt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean&lt;/strong&gt; the mailbox and local test data in a finally block.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important detail is that creation and cleanup are part of the contract. If creation fails, the test should not start a browser flow that can never finish. If extraction succeeds, the fixture should record a message ID or a hash of the subject, not dump the complete message into the CI log.&lt;/p&gt;

&lt;p&gt;This is also where naming helps. A test that uses a string like &lt;code&gt;tempail mail&lt;/code&gt; as malformed input should label it as an intentional negative case. It should not be confused with the generated address used by the happy path. Test data that looks almost correct are valuable for validation, but only when the report says why it was used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poll for evidence, not hope
&lt;/h2&gt;

&lt;p&gt;Polling should answer one question at a time. First ask whether a matching message exists. Then validate that the message is recent enough and belongs to this test. Only after those checks should the test parse a link.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForVerificationLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/verify/i&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;extractVerificationLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verification message was not observed before timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is intentionally boring. A bounded loop is easier to inspect than a chain of arbitrary sleeps. In a real implementation, &lt;code&gt;find&lt;/code&gt; should also use a correlation value, such as a test-specific recipient or signup identifier. Subject-only matching can pass against an old message.&lt;/p&gt;

&lt;p&gt;When a timeout occurs, attach a compact receipt: mailbox identifier, creation time, poll count, last provider status, and the final matching decision. Do not attach the full address, token, or raw HTML by default. A receipt should act like &lt;a href="https://dev.to/jasonmills94/docker-releases-need-an-aws-receipt-contract-1l1b"&gt;a reviewable deployment receipt&lt;/a&gt;: enough evidence to explain what happened, without becoming another source of secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Playwright tests isolated
&lt;/h2&gt;

&lt;p&gt;Parallel workers make isolation non-optional. Each worker should get a distinct mailbox identity and a correlation value that cannot be accidentally reused. Avoid a global inbox shared by the entire project. It saves setup time, but it makes old messages and cross-test consumption almost impossible to reason about.&lt;/p&gt;

&lt;p&gt;The fixture should also own the browser action that triggers delivery only when that ownership is useful. In some suites, the page flow belongs in the test and the mailbox belongs in a helper. Either arrangement works if the boundary is clear. What causes trouble is a helper that silently creates a mailbox, retries the signup, and consumes the message before the test can report which attempt succeeded.&lt;/p&gt;

&lt;p&gt;For negative tests, create the invalid input explicitly. For example, &lt;code&gt;temp gamil com&lt;/code&gt; can verify client-side validation, while a real generated address verifies delivery. These cases should be seperate fixtures or clearly named scenarios. Mixing them in one parametrized test can produce a report that looks like a provider failure when it is really an input failure.&lt;/p&gt;

&lt;p&gt;Cleanup needs to run even after a failed assertion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verifies a new account by email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signupWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForVerificationLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mailbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cleanup step is easy to forgets when the happy path is the only path being run locally. CI will eventually exercise a timeout, a provider error, or a browser crash, so cleanup should be structural rather than a final assertion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure-analysis checklist
&lt;/h2&gt;

&lt;p&gt;When an email test fails, check the states in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did mailbox creation return a unique identity?&lt;/li&gt;
&lt;li&gt;Did the application submit the message request successfully?&lt;/li&gt;
&lt;li&gt;Did the provider accept the message, or was it rejected?&lt;/li&gt;
&lt;li&gt;Did the poll use the correct mailbox and correlation value?&lt;/li&gt;
&lt;li&gt;Was the message newer than the fixture creation time?&lt;/li&gt;
&lt;li&gt;Did link extraction reject an expired or malformed URL?&lt;/li&gt;
&lt;li&gt;Did cleanup run after the failure?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This checklist prevents a common mistake: retrying the browser flow before proving that the mailbox read was correct. Retries can create duplicate messages and make the original failure less visible. The fixture should collect these facts seperately so the final report can show where the workflow stopped.&lt;/p&gt;

&lt;p&gt;For additional context, &lt;a href="https://dev.to/bitheirstake/safer-invite-email-debugging-notes-5ao6"&gt;safer invite email debugging&lt;/a&gt; is a useful adjacent pattern: preserve enough delivery evidence for diagnosis while keeping sensitive values out of routine logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Reliable Playwright email tests are less about finding a magic polling interval and more about making the mailbox lifecycle observable. Give the fixture states, bounded waits, correlation rules, and guaranteed cleanup. Then a failure can say “message was never observed for this test” instead of simply “expected verified, received unverified.”&lt;/p&gt;

&lt;p&gt;That difference is small in code but large in QA feedback. It helps a team fix the application, the provider integration, or the test itself with much less guessing.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Playwright Email Tests Need a Failure Taxonomy</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 27 Sep 2026 05:23:59 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-a-failure-taxonomy-575n</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-a-failure-taxonomy-575n</guid>
      <description>&lt;p&gt;Email verification tests often fail in a way that looks random: the form submits, the inbox stays empty, or the assertion finds an old message instead of the new one. In a QA suite, “email failed” is not a useful diagnosis. The first job is to identify which boundary failed.&lt;/p&gt;

&lt;p&gt;This article presents a small failure taxonomy for Playwright tests that use a burner email address or another isolated test mailbox. The goal is not to make every test pass by adding longer waits. It is to make each failure explain itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why email test failures are hard to classify
&lt;/h2&gt;

&lt;p&gt;An email verification flow crosses several systems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser submits the signup form.&lt;/li&gt;
&lt;li&gt;The application creates a verification record.&lt;/li&gt;
&lt;li&gt;A worker or provider sends the message.&lt;/li&gt;
&lt;li&gt;The test mailbox receives and exposes it.&lt;/li&gt;
&lt;li&gt;The browser opens the link and completes verification.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One failed assertion can hide any of these causes. A selector timeout might actually be a backend error. An empty inbox might be a polling problem. A successful click might still verify an expired token.&lt;/p&gt;

&lt;p&gt;The test become much easier to maintain when the failure output names the boundary instead of only saying that an expectation timed out.&lt;/p&gt;

&lt;h2&gt;
  
  
  A four-part failure taxonomy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Product failures
&lt;/h3&gt;

&lt;p&gt;The application did not create the expected verification event, used the wrong recipient, or generated a link that the server rejects. These failures should be visible in the application response, network log, or database fixture—not inferred from an inbox timeout.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Delivery failures
&lt;/h3&gt;

&lt;p&gt;The application says it sent a message, but the message never reaches the test mailbox. This may be a provider issue, a blocked domain, or a fixture configured for the wrong account. Keep delivery evidence separate from browser evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Observation failures
&lt;/h3&gt;

&lt;p&gt;The message exists, but the test checks too early, reads a stale message, or filters the subject too strictly. This is where deterministic polling and a unique test identifier help. A fixed &lt;code&gt;waitForTimeout&lt;/code&gt; can hide the issue for a while, but it does not solve it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Interaction failures
&lt;/h3&gt;

&lt;p&gt;The message is correct, yet the browser cannot use it. The link may open in a new page, the verification UI may have changed, or the token may be expired. Playwright traces are especially useful here because they show the action, URL, and page state together.&lt;/p&gt;

&lt;p&gt;This separation also gives the team a better vocabulary during triage. “Delivery is green, observation is red” is a useful next action. “Email test is flaky” is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a diagnostic Playwright fixture
&lt;/h2&gt;

&lt;p&gt;Give every test a unique recipient and correlation value. Then return evidence from the mailbox helper, rather than returning only a string URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MailEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;verificationUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extend&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;mailEvidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MailEvidence&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;mailEvidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pollMailbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is the &lt;code&gt;marker&lt;/code&gt;, not the exact helper name. It can be a test ID in the subject, a unique local part, or a server-side correlation ID. Without it, the test may pass against an old message, and that is a very expensive false positive.&lt;/p&gt;

&lt;p&gt;For a mailbox provider, use a &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail so&lt;/a&gt; address only where the test data is non-sensitive and the provider fits the environment’s privacy rules. Never place real customer data in a disposable test inbox. The phrase “tempail mail” may appear in search notes or old fixture documentation, but it should never become the actual contract for a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the evidence in the right order
&lt;/h2&gt;

&lt;p&gt;When a test fails, inspect evidence in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Form response:&lt;/strong&gt; Did the signup request return the expected status and recipient?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application event:&lt;/strong&gt; Did the service record an email request with the test marker?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailbox result:&lt;/strong&gt; Was a new message received, and what was its ID and timestamp?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification request:&lt;/strong&gt; Did the link return the expected response?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser trace:&lt;/strong&gt; Did the UI render the correct success state?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This order prevents a browser timeout from becoming a week-long investigation. It also works well with CI artifacts: save the test ID, message ID, and trace together. If you need retry-safe server behavior, the idea of &lt;a href="https://dev.to/kevindev27/postgresql-idempotency-for-rest-apis-under-retries"&gt;idempotent behavior under retries&lt;/a&gt; is a useful companion pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CI checklist for reliable email tests
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Create a new mailbox identity per test or per isolated worker.&lt;/li&gt;
&lt;li&gt;Put a unique marker in every email request.&lt;/li&gt;
&lt;li&gt;Poll for a matching message, not merely the newest message.&lt;/li&gt;
&lt;li&gt;Record message ID, subject, received time, and the selected link.&lt;/li&gt;
&lt;li&gt;Keep application, delivery, and browser evidence as separate fields.&lt;/li&gt;
&lt;li&gt;Capture a Playwright trace on failure and retain it with the mailbox evidence.&lt;/li&gt;
&lt;li&gt;Make cleanup explicit, even when the test fails halfway through.&lt;/li&gt;
&lt;li&gt;Check that retries do not accept a message from the first attempt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An email test is more trustworthy when it leaves a small, reviewable receipt. That is closely related to &lt;a href="https://dev.to/mrdapperx/email-as-a-deployment-contract"&gt;treating email as a deployment contract&lt;/a&gt;: define what was requested, what was observed, and what was finally verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaways
&lt;/h2&gt;

&lt;p&gt;Reliable Playwright email tests are mostly an observability problem. Classify the failure before changing the timeout. Use unique markers, return structured mailbox evidence, and preserve a trace that connects the UI action to the backend event.&lt;/p&gt;

&lt;p&gt;The slightly imperfect test that explains its failure is more valuable than the green test that could be reading yesterday’s message. And if a fixture note says “tempail,” review it before copying the term into a new automation contract.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Agentes LLM: contratos de herramientas que fallan bien</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 26 Sep 2026 23:23:38 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-contratos-de-herramientas-que-fallan-bien-1k7l</link>
      <guid>https://dev.to/silviutech/agentes-llm-contratos-de-herramientas-que-fallan-bien-1k7l</guid>
      <description>&lt;p&gt;Cuando un agente LLM falla, solemos culpar al prompt. A veces el prompt está bien: el problema es que la herramienta devuelve una mezcla de texto, estados implícitos y errores que el modelo debe adivinar.&lt;/p&gt;

&lt;p&gt;En un sistema que usa herramientas, el prompt es la intención y el contrato es la frontera. Si esa frontera es ambigua, el agente puede repetir una acción, ocultar un fallo o afirmar que terminó cuando solo inició una operación.&lt;/p&gt;

&lt;p&gt;Este patrón sirve para herramientas de email, búsquedas, despliegues o cualquier integración externa. La idea no es hacer al agente más listo. Es reducir las decisiones que tiene que inventar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es el prompt, es el contrato
&lt;/h2&gt;

&lt;p&gt;Imagina una herramienta llamada &lt;code&gt;send_verification_email&lt;/code&gt;. Una versión débil devuelve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Qué significa &lt;code&gt;ok&lt;/code&gt;? ¿El proveedor aceptó el mensaje? ¿El email llegó? ¿El usuario ya verificó su cuenta? Son hechos diferentes, pero el agente puede tratarlos como uno solo.&lt;/p&gt;

&lt;p&gt;Un contrato útil separa la intención del resultado observable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"op_4821"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"next_check_after_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El modelo no tiene que interpretar una frase optimista. Puede decidir el siguiente paso a partir de campos con significado estable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseña la herramienta como una frontera
&lt;/h2&gt;

&lt;p&gt;Pienso en la herramienta como tres cajas conectadas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entrada:&lt;/strong&gt; argumentos validados, límites de tamaño y un identificador de correlación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ejecución:&lt;/strong&gt; el adaptador que habla con el proveedor y convierte sus respuestas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salida:&lt;/strong&gt; un pequeño conjunto de estados que el agente puede usar sin conocer detalles internos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La caja intermedia es importante. No expongas al LLM cada código extraño del proveedor. Convierte &lt;code&gt;429&lt;/code&gt;, timeout y respuesta incompleta en estados de dominio como &lt;code&gt;rate_limited&lt;/code&gt;, &lt;code&gt;temporarily_unavailable&lt;/code&gt; y &lt;code&gt;unknown_result&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;También conviene declarar qué no hace la herramienta. &lt;code&gt;accepted&lt;/code&gt; no significa &lt;code&gt;delivered&lt;/code&gt;; &lt;code&gt;delivered&lt;/code&gt; no significa &lt;code&gt;opened&lt;/code&gt;. Esta precisión parece un detalle, pero evita decisiones de negocio equivocadas.&lt;/p&gt;

&lt;p&gt;Si la operación necesita una cola, la trazabilidad debe sobrevivir entre la petición y el worker. Un buen ejemplo práctico es este enfoque de &lt;a href="https://dev.to/silviutech/fastapi-colas-de-email-con-trazas-utiles-1fc6"&gt;colas de email con trazas útiles&lt;/a&gt;, donde la operación se puede seguir sin depender del texto que genere el modelo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una salida estructurada y sus estados
&lt;/h2&gt;

&lt;p&gt;Un contrato mínimo puede usar estados explícitos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;started -&amp;gt; accepted -&amp;gt; observed -&amp;gt; completed
             |            |
             v            v
        retryable     expired
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No todos los estados tienen que ser visibles para el usuario, pero sí deben ser distinguibles para el orquestador. &lt;code&gt;started&lt;/code&gt; permite evitar un duplicado si la red corta la respuesta. &lt;code&gt;accepted&lt;/code&gt; confirma que el proveedor tomó la petición. &lt;code&gt;observed&lt;/code&gt; indica que encontramos evidencia posterior. &lt;code&gt;expired&lt;/code&gt; define una acción de recuperación, no un fallo misterioso.&lt;/p&gt;

&lt;p&gt;Añade idempotencia donde una repetición pueda tener coste. La clave puede combinar &lt;code&gt;user_id&lt;/code&gt;, propósito y una ventana temporal. Si el agente recibe un timeout, debe poder consultar el &lt;code&gt;operation_id&lt;/code&gt; antes de enviar otra vez.&lt;/p&gt;

&lt;p&gt;Esto es Prompt Engineering aplicado a arquitectura: el prompt explica cuándo consultar y cuándo detenerse, mientras el contrato hace imposible confundir una aceptación con una confirmación.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas con fixtures de email
&lt;/h2&gt;

&lt;p&gt;Las pruebas de agentes necesitan entradas reproducibles. Un fixture de email puede representar un inbox vacío, un mensaje retrasado, un enlace expirado y dos mensajes con el mismo asunto.&lt;/p&gt;

&lt;p&gt;No uses una dirección real del equipo. Genera datos aislados y registra solo el identificador del fixture. Incluso una dirección de prueba con un nombre torcido como &lt;code&gt;temp gamil com&lt;/code&gt; puede ser útil para comprobar que la interfaz no “corrige” silenciosamente datos del usuario. &lt;code&gt;temp org mail&lt;/code&gt; puede servir como caso de búsqueda que no debe producir coincidencias mágicas.&lt;/p&gt;

&lt;p&gt;Un caso de prueba legible podría verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wait_for_verification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fixture"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"delayed_message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"first_result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"waiting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"second_result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"observed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_polls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prueba también el límite: el agente no debe hacer polling para siempre. Después de &lt;code&gt;max_polls&lt;/code&gt;, devuelve una explicación y una acción concreta. Es más útil decir “no hay evidencia todavía; vuelve a intentar” que inventar que el email llegó.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observabilidad sin inundar el contexto
&lt;/h2&gt;

&lt;p&gt;El agente necesita un resumen corto; el sistema necesita evidencia completa. Separa ambos canales.&lt;/p&gt;

&lt;p&gt;En el contexto del modelo guarda &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;operation_id&lt;/code&gt;, &lt;code&gt;retryable&lt;/code&gt; y una razón breve. En logs conserva timestamps, latencia, proveedor y códigos normalizados. Evita guardar el contenido completo del email si no es necesario.&lt;/p&gt;

&lt;p&gt;Los operadores también necesitan señales limpias. Las prácticas de &lt;a href="https://dev.to/alexcarteruk/sre-correos-de-incidentes-sin-ruido-operativo-5bc6"&gt;correos de incidentes sin ruido operativo&lt;/a&gt; ayudan a aplicar la misma regla: un evento accionable debe ser distinto de un simple cambio de estado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A: errores, reintentos y prompts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo devolver texto además de JSON?
&lt;/h3&gt;

&lt;p&gt;Para el orquestador, devuelve una estructura estable. Puedes generar una explicación legible en otra capa. Mezclar ambas cosas hace que el contrato dependa de cómo redacte el modelo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo debe reintentar el agente?
&lt;/h3&gt;

&lt;p&gt;Solo cuando &lt;code&gt;retryable&lt;/code&gt; sea verdadero y exista presupuesto de intentos. Un timeout con resultado desconocido requiere consultar el estado antes de repetir, porque la primera operación pudo completarse.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Un mejor prompt elimina estos problemas?
&lt;/h3&gt;

&lt;p&gt;No. Un prompt claro reduce malentendidos, pero no puede convertir una respuesta ambigua del proveedor en evidencia. La frontera de la herramienta debe expresar esa evidencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Puntos de implementación
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Escribe primero los estados y las transiciones, después el prompt.&lt;/li&gt;
&lt;li&gt;Define un esquema de salida pequeño y versionable.&lt;/li&gt;
&lt;li&gt;Normaliza errores externos en categorías de dominio.&lt;/li&gt;
&lt;li&gt;Usa &lt;code&gt;operation_id&lt;/code&gt; e idempotencia para proteger los reintentos.&lt;/li&gt;
&lt;li&gt;Construye fixtures para retrasos, duplicados y expiraciones.&lt;/li&gt;
&lt;li&gt;Mantén el resumen del LLM corto y la evidencia operativa fuera del contexto.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El resultado es un agente menos teatral y más predecible. Todavía puede equivocarse, claro, pero sus errores tienen forma, límites y un camino de recuperación. Esa es una propiedad de arquitectura mucho más valiosa que un prompt ingenioso.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>promptengineering</category>
      <category>devtools</category>
    </item>
    <item>
      <title>FastAPI: webhooks que sobreviven a un timeout</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 26 Sep 2026 11:23:38 +0000</pubDate>
      <link>https://dev.to/silviutech/fastapi-webhooks-que-sobreviven-a-un-timeout-2lg2</link>
      <guid>https://dev.to/silviutech/fastapi-webhooks-que-sobreviven-a-un-timeout-2lg2</guid>
      <description>&lt;p&gt;Un webhook puede funcionar durante semanas y fallar justo cuando un proveedor tarda un poco más en responder. El cliente ve un timeout y reintenta; nuestro endpoint quizá ya guardó el evento y empezó a enviar una notificación. Si no hemos diseñado ese caso, una sola acción termina duplicada.&lt;/p&gt;

&lt;p&gt;En proyectos Python prefiero separar dos preguntas: ¿recibimos el evento? y ¿terminamos su procesamiento? FastAPI hace sencilla la primera parte, pero la segunda necesita un contrato explícito. Este patrón pequeño ayuda a construir APIs más predecibles sin añadir una plataforma enorme desde el primer día.&lt;/p&gt;

&lt;h2&gt;
  
  
  El timeout no significa que el webhook falló
&lt;/h2&gt;

&lt;p&gt;Un timeout solo dice que el cliente no recibió una respuesta a tiempo. No prueba que el servidor no haya hecho nada. La petición pudo llegar, pasar la validación y quedar en una cola mientras la conexión se cerraba.&lt;/p&gt;

&lt;p&gt;Por eso un endpoint de webhook no debería hacer todo el trabajo antes de responder. Validar la firma, comprobar el esquema y registrar una clave de idempotencia suele ser suficiente para devolver &lt;code&gt;202 Accepted&lt;/code&gt;. El procesamiento pesado puede continuar en una tarea o worker.&lt;/p&gt;

&lt;p&gt;La secuencia queda así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El proveedor envía el evento con un identificador único.&lt;/li&gt;
&lt;li&gt;FastAPI valida autenticación, formato y tamaño.&lt;/li&gt;
&lt;li&gt;La aplicación registra el identificador si todavía no existe.&lt;/li&gt;
&lt;li&gt;El endpoint responde rápido con &lt;code&gt;202&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Un worker procesa el evento y deja un resultado observable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Es una diferencia facil de pasar por alto: el código HTTP describe la aceptación, no la entrega final del email ni el estado del negocio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato pequeño para el endpoint
&lt;/h2&gt;

&lt;p&gt;Un esquema claro evita que cada integración invente su propia interpretación. Este ejemplo no implementa un sistema de colas, pero muestra qué debe quedar definido en la frontera:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;occurred_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El handler puede devolver un resultado mínimo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/hooks/provider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_202_ACCEPTED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebhookEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;event_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_if_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La operación &lt;code&gt;insert_if_new&lt;/code&gt; debe ser atómica, respaldada por un índice único sobre &lt;code&gt;event_id&lt;/code&gt;. Un diccionario en memoria puede servir para una demo, pero no para varias réplicas. Si dos peticiones llegan casi al mismo tiempo, ambas deben observar el mismo contrato.&lt;/p&gt;

&lt;p&gt;También conviene documentar qué ocurre con un evento repetido. Devolver &lt;code&gt;202&lt;/code&gt; con &lt;code&gt;duplicate: true&lt;/code&gt; permite que el proveedor deje de reintentar, mientras el sistema mantiene una métrica separada para investigar la causa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotencia antes de reintentar
&lt;/h2&gt;

&lt;p&gt;El reintento es sano cuando el efecto también lo es. Antes de configurar tres reintentos exponenciales, decide cuál es la unidad que no puede duplicarse: un email enviado, una factura creada o una transición de estado.&lt;/p&gt;

&lt;p&gt;Para una notificación, guardaría una clave compuesta parecida a &lt;code&gt;event_id + template_version + recipient&lt;/code&gt;. El worker consulta esa clave dentro de una transacción y solo entonces crea la tarea de envío. Si el proveedor vuelve a llamar, la API responde con el mismo resultado lógico, aunque el cuerpo HTTP tenga pequeñas diferencias.&lt;/p&gt;

&lt;p&gt;No hay que confundir esto con ignorar errores. Si el primer procesamiento quedó a medias, el registro debe tener un estado como &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt; o &lt;code&gt;failed&lt;/code&gt;, además de un contador de intentos. Un estado &lt;code&gt;unknown&lt;/code&gt; es útil cuando el worker perdió la conexión justo despues de pedir el envío y no puede afirmar si el proveedor aceptó la operación.&lt;/p&gt;

&lt;p&gt;Para revisar la parte de notificaciones, me gusta mantener separadas las &lt;a href="https://dev.to/hannahdev56/saas-emails-de-prueba-que-no-rompen-tus-metricas"&gt;emails de prueba que no rompen tus métricas&lt;/a&gt;. La automatización de pruebas no debe modificar datos de producción ni depender de una bandeja personal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas con un buzón aislado
&lt;/h2&gt;

&lt;p&gt;Una prueba de integración útil provoca un evento, espera el recibo y comprueba el identificador de correlación. Un buzón temporal como &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt; puede aislar ese flujo cuando la prueba necesita observar una notificación real sin tocar una cuenta del equipo.&lt;/p&gt;

&lt;p&gt;El test debe usar un &lt;code&gt;run_id&lt;/code&gt; nuevo y filtrar mensajes por tiempo, destinatario y una cabecera de correlación. Buscar solo por asunto es una trampa: el mensaje de una ejecución anterior puede parecer válido. En algunas notas de QA aparece la cadena temp mailid, pero no debería formar parte de la lógica de búsqueda.&lt;/p&gt;

&lt;p&gt;Una aserción conceptual sería:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Run-Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;welcome-v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuando la prueba falla, guarda el &lt;code&gt;run_id&lt;/code&gt;, el &lt;code&gt;event_id&lt;/code&gt;, la hora de envío, los estados observados y el motivo de cada mensaje descartado. Eso produce una evidencia mas pequeña y util que un log completo. Si usas contratos para automatizaciones, también puede servir &lt;a href="https://dev.to/silviutech/llms-contratos-minimos-para-revisar-prompts"&gt;revisar una automatización con contratos mínimos&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist de implementación
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Validar firma, tamaño y esquema antes de aceptar el evento.&lt;/li&gt;
&lt;li&gt;Responder &lt;code&gt;202&lt;/code&gt; solo cuando el evento quedó registrado de forma durable.&lt;/li&gt;
&lt;li&gt;Crear una restricción única para la clave de idempotencia.&lt;/li&gt;
&lt;li&gt;Separar recepción, procesamiento y envío de notificaciones.&lt;/li&gt;
&lt;li&gt;Medir duplicados, latencia, reintentos y estados &lt;code&gt;unknown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Probar un timeout del cliente y verificar que no aparece un segundo email.&lt;/li&gt;
&lt;li&gt;Limpiar los buzones y artefactos de prueba despues de cada ejecución.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un detalle mas: el timeout del cliente debe estar cubierto en las pruebas, no solo el timeout del worker. Son fallos distintos y requieren diagnósticos distintos. Si el endpoint acepta dos veces, el problema es de idempotencia; si acepta una vez pero el email no sale, hay que mirar el worker o el proveedor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas rápidas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo devolver &lt;code&gt;200&lt;/code&gt; cuando el trabajo aún no termina?
&lt;/h3&gt;

&lt;p&gt;No necesariamente. &lt;code&gt;202 Accepted&lt;/code&gt; comunica mejor que el evento fue aceptado para procesamiento posterior. Elige &lt;code&gt;200&lt;/code&gt; solo si la operación terminó y puedes afirmar su resultado.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Un retry siempre debe crear un nuevo registro?
&lt;/h3&gt;

&lt;p&gt;No. El reintento puede crear un intento de procesamiento, pero el evento de negocio debe conservar su identidad. Separar ambos identificadores hace el historial mucho mas claro.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué pasa si el proveedor no envía un identificador?
&lt;/h3&gt;

&lt;p&gt;Genera una huella con campos estables solo como último recurso y documenta la limitación. Es mejor pedir un identificador oficial: deduplicar por asunto, fecha o contenido parcial falla facilmente con eventos legítimos parecidos.&lt;/p&gt;

&lt;p&gt;El objetivo no es evitar todos los timeouts. Es hacer que un timeout sea una señal diagnostica, no una invitación a duplicar efectos. Con un contrato pequeño, una clave idempotente y una prueba de integración aislada, los webhooks de FastAPI se vuelven bastante menos misteriosos.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: estados de email sin perder el contexto</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 26 Sep 2026 05:23:35 +0000</pubDate>
      <link>https://dev.to/silviutech/react-estados-de-email-sin-perder-el-contexto-52ac</link>
      <guid>https://dev.to/silviutech/react-estados-de-email-sin-perder-el-contexto-52ac</guid>
      <description>&lt;p&gt;Un formulario de registro no termina cuando el usuario pulsa &lt;strong&gt;Crear cuenta&lt;/strong&gt;. En ese momento empieza una parte delicada: la verificación del email. Si la pantalla cambia demasiado, el botón desaparece o el mensaje de éxito tarda en llegar, la persona no sabe si debe esperar, corregir la dirección o volver a intentarlo.&lt;/p&gt;

&lt;p&gt;En interfaces React suelo tratar la verificación como un pequeño flujo de estados, no como un simple &lt;code&gt;isLoading&lt;/code&gt;. Este cambio hace que el producto sea más facil de entender, mejora la accesibilidad y evita trabajo visual innecesario. También permite probar cada escenario sin depender de una bandeja real.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: el email también tiene estados
&lt;/h2&gt;

&lt;p&gt;Un flujo de email puede pasar por estados bastante diferentes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;idle&lt;/code&gt;: todavía no se ha enviado nada.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sending&lt;/code&gt;: la aplicación está enviando la solicitud.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sent&lt;/code&gt;: el mensaje fue aceptado y esperamos al usuario.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verified&lt;/code&gt;: la cuenta ya está confirmada.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt;: algo falló y hay una acción posible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cuando todos ellos se representan con el mismo spinner, se pierde contexto. Un spinner dice que hay trabajo, pero no dice qué trabajo, durante cuanto tiempo conviene esperar ni que puede hacer la persona después.&lt;/p&gt;

&lt;p&gt;Un correo temporal desechable puede ser útil en pruebas de interfaz, pero el componente debe comportarse igual con cualquier dirección permitida. No conviene diseñar la pantalla alrededor de una bandeja concreta: conviene diseñarla alrededor de una intención observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modelar un estado visible y estable
&lt;/h2&gt;

&lt;p&gt;El estado de la interfaz debería responder tres preguntas: ¿qué pasó?, ¿qué está ocurriendo? y ¿cuál es el siguiente paso? Una forma sencilla de expresarlo es con una unión discriminada en TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmailState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El campo &lt;code&gt;kind&lt;/code&gt; evita combinar por accidente &lt;code&gt;loading: true&lt;/code&gt; con un mensaje viejo de error. Es una diferencia pequeña, pero ayuda mucho cuando el formulario crece. La interfaz no deberia adivinar el estado a partir de varios booleanos.&lt;/p&gt;

&lt;p&gt;Para mantener el contexto, deja el email visible después del envío y cambia sólo las partes necesarias. Un mensaje como “Enviamos un enlace a &lt;a href="mailto:ana@example.com"&gt;ana@example.com&lt;/a&gt;” es más útil que “Revisa tu correo”, porque confirma la acción y la dirección que el usuario debe revisar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un componente React pequeño, pero honesto
&lt;/h2&gt;

&lt;p&gt;El componente no necesita saber cómo se transporta el email. Sólo necesita recibir el estado y emitir acciones claras:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VerificationStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;onResend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onResend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Enviando el correo de verificación…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email-status-title"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email-status-title"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Revisa tu bandeja&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Enviamos un enlace a &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onResend&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Reenviar correo
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email verificado. Ya puedes continuar.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onResend&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Intentar de nuevo&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El botón de reenvío no debería aparecer como un enlace ambiguo. Su nombre comunica la acción y su tipo explícito evita que un cambio posterior lo convierta en un submit accidental. Para los casos donde el mensaje tarda, conserva el contenido del formulario; perderlo después de un clic fallido es una fricción que no aporta nada.&lt;/p&gt;

&lt;p&gt;Para flujos con reenvíos, también es útil &lt;a href="https://dev.to/silviutech/react-reenvios-de-verificacion-sin-ansiedad-4km0"&gt;mostrar reenvíos de verificación con menos fricción&lt;/a&gt; y &lt;a href="https://dev.to/hannahdev56/saas-registra-intentos-de-email-sin-perder-contexto-1dfm"&gt;registrar intentos de email sin perder contexto&lt;/a&gt;. La observabilidad del producto y la claridad de la UI se refuerzan mutuamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesibilidad y CSS sin saltos
&lt;/h2&gt;

&lt;p&gt;Un estado dinámico debe anunciarse sin robar el foco. &lt;code&gt;role="status"&lt;/code&gt; funciona bien para una actualización informativa; &lt;code&gt;role="alert"&lt;/code&gt; es mejor para un error que necesita atención inmediata. No uses ambos para el mismo texto, porque el lector de pantalla podria anunciarlo dos veces.&lt;/p&gt;

&lt;p&gt;También evita reservar alturas completamente distintas para cada mensaje. Un bloque con &lt;code&gt;min-height&lt;/code&gt;, un ancho razonable y un texto que pueda envolver reduce los saltos de layout. El botón debe conservar un foco visible y no depender sólo del color para indicar que está deshabilitado.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.verification-status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;34rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.verification-status&lt;/span&gt; &lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="nd"&gt;:focus-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;currentColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En móvil, el mensaje puede ocupar dos líneas mas. Eso no es un fallo: es mejor un bloque flexible que truncar la dirección o esconder el siguiente paso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo probar el flujo
&lt;/h2&gt;

&lt;p&gt;Prueba el componente con una tabla de estados, no sólo con un test de clics:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El estado &lt;code&gt;idle&lt;/code&gt; no muestra un mensaje vacío.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sending&lt;/code&gt; anuncia la actividad y conserva la dirección.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sent&lt;/code&gt; muestra una confirmación específica y un reenvío accesible.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt; explica una acción recuperable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verified&lt;/code&gt; no deja visible un botón que ya no hace falta.&lt;/li&gt;
&lt;li&gt;El foco de teclado sigue un orden comprensible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En las pruebas de integración, simula una respuesta lenta y un reintento. Una dirección de prueba como “tempail mail” puede aparecer en datos de QA, pero no la conviertas en lógica especial del componente. El objetivo es comprobar que la interfaz sigue siendo clara cuando el correo no llega enseguida.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿Cada estado tiene un mensaje y una acción comprensibles?&lt;/li&gt;
&lt;li&gt;¿El usuario conserva el email y el resto del contexto?&lt;/li&gt;
&lt;li&gt;¿Los cambios se anuncian correctamente a tecnologías asistivas?&lt;/li&gt;
&lt;li&gt;¿El CSS evita saltos sin fijar una altura frágil?&lt;/li&gt;
&lt;li&gt;¿Los tests cubren carga lenta, error y reenvío?&lt;/li&gt;
&lt;li&gt;¿El botón de reenvío tiene límites para no generar acciones repetidas?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un flujo de verificación bien diseñado no necesita más adornos. Necesita estados honestos, texto útil y una estructura que aguante una respuesta lenta. En React, modelar esas decisiones desde el principio suele costar menos que reparar después una pantalla que hace que cada email parezca un problema distinto.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>css</category>
      <category>performance</category>
    </item>
    <item>
      <title>Playwright Email Polling Needs Fresh Evidence</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 26 Sep 2026 02:25:03 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-polling-needs-fresh-evidence-2m8i</link>
      <guid>https://dev.to/silviutech/playwright-email-polling-needs-fresh-evidence-2m8i</guid>
      <description>&lt;p&gt;Email assertions are often the last step in an otherwise healthy Playwright flow. The page submits, the API returns 202, and then the test waits for a verification message. When that wait fails, the report may only say that a timeout was reached.&lt;/p&gt;

&lt;p&gt;That message is not enough to diagnose the problem. The test might have used the wrong throwaway email address, matched an earlier message, or started polling before the delivery job existed. A retry can pass while hiding the original failure.&lt;/p&gt;

&lt;p&gt;This post defines a small evidence contract for email polling. It does not claim one best throwaway email; it makes each Playwright attempt explain what it saw, accepted, and where the wait ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a passing retry can still be a bad test
&lt;/h2&gt;

&lt;p&gt;Suppose a signup test creates an inbox and submits a form, then asks for the latest message. The first attempt times out during slow delivery; the retry sees a quick message and passes.&lt;/p&gt;

&lt;p&gt;The retry did not prove that the first attempt was a transient delay. It may have used a different throwaway email address, worker, or old message with the right subject. Without attempt-level evidence, the team debates whether the product or test is flaky. That takes alot longer than reading a structured failure.&lt;/p&gt;

&lt;p&gt;I want each attempt to record four boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the inbox or alias assigned to the run&lt;/li&gt;
&lt;li&gt;the time the product action was triggered&lt;/li&gt;
&lt;li&gt;the messages observed after that time&lt;/li&gt;
&lt;li&gt;the reason the selected message belonged to this test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For related operational thinking, &lt;a href="https://dev.to/jasonmills94/cloudwatch-alarm-emails-need-deploy-evidence-4ee1"&gt;deploy evidence for email alerts&lt;/a&gt; is a useful reminder that an email result needs context around the event that produced it. For authentication flows, &lt;a href="https://dev.to/sophiax99/oauth-email-verification-needs-a-real-threat-model-1pe2"&gt;a threat model for verification email&lt;/a&gt; adds another helpful question: what exactly does possession of the message prove?&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence contract for email polling
&lt;/h2&gt;

&lt;p&gt;The contract can be a plain object saved as a test attachment. Keep it small enough to scan in CI, but detailed enough to distinguish delivery, matching, and application failures.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmailPollEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;acceptedMessageId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;triggeredAt&lt;/code&gt; value is important. Filtering by message time is safer than always taking the newest message, especialy when a shared test environment receives unrelated mail. The acceptance reason matters too: “matched subject” is weaker than “matched subject and contained the run-specific token.”&lt;/p&gt;

&lt;p&gt;If your test notes contain searches such as &lt;code&gt;temp gamil com&lt;/code&gt; or &lt;code&gt;tem email&lt;/code&gt;, keep those as plain text in diagnostics. They can be useful clues about copied setup instructions, but they should not become link anchors or supported integration names.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playwright helper with bounded polling
&lt;/h2&gt;

&lt;p&gt;The helper below uses a deadline instead of an unlimited loop. It also stores every candidate decision, so a timeout leaves a useful receipt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;runToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triggeredAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailPollEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;triggeredAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Verify your email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subject did not match&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run token was missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subject and run token matched&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acceptedMessageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evidence&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few deliberate choices here. The run token is stronger than a subject-only match. The deadline prevents a stuck provider from hanging the worker. Retaining rejected candidates improves observabilty without dumping full message bodies into CI.&lt;/p&gt;

&lt;p&gt;In a real suite, I would also deduplicate candidates by message id. Otherwise one message returned by several polls can make the evidence look noisier than it is. A small helper is worth the few extra lines, mabye more than increasing the timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to classify failures
&lt;/h2&gt;

&lt;p&gt;When the helper fails, start with the evidence rather than rerunning immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No candidates after the trigger time:&lt;/strong&gt; investigate delivery, the selected inbox, and whether the request created a send job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Candidates with the wrong subject:&lt;/strong&gt; investigate templates, locale, and environment configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct subject but missing run token:&lt;/strong&gt; investigate message routing or whether parallel tests share an inbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accepted message but a later assertion fails:&lt;/strong&gt; investigate the link, token, or application state after email delivery.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps a Playwright timeout from becoming a generic “email issue.” The failure is more specific, and retries can be compared instead of replacing the first attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  A repeatable CI checklist
&lt;/h2&gt;

&lt;p&gt;Before calling an email test reliable, I check the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every attempt has an isolated inbox or a unique run token&lt;/li&gt;
&lt;li&gt;polling begins after the triggering action&lt;/li&gt;
&lt;li&gt;message time is checked against the attempt boundary&lt;/li&gt;
&lt;li&gt;subject matching is combined with ownership evidence&lt;/li&gt;
&lt;li&gt;the helper has a hard deadline&lt;/li&gt;
&lt;li&gt;candidate decisions are attached to the test report&lt;/li&gt;
&lt;li&gt;retry attempts preserve earlier evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the environment requires a temporary address, document its privacy and retention limits as part of the fixture contract. A &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;free throwaway email&lt;/a&gt; service may suit a low-risk staging check, but production-like data and long-lived accounts deserve a controlled test mailbox. Make that boundary obvious in setup code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should every email test use a unique inbox?
&lt;/h3&gt;

&lt;p&gt;Ideally, each parallel attempt gets an isolated inbox or a unique address alias. If that is not possible, a run token plus strict time filtering is the minimum I would accept.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a longer timeout the right fix for slow delivery?
&lt;/h3&gt;

&lt;p&gt;Only when the evidence shows normal delivery is slower than the current deadline. A longer wait does not fix a wrong inbox, stale-message match, or missing ownership check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I attach the full email body to CI?
&lt;/h3&gt;

&lt;p&gt;Usually no. Store the message id, subject, timestamps, decision, and a redacted diagnostic excerpt. Full bodies can contain personal data and are rarely needed to prove the polling decision.&lt;/p&gt;

&lt;p&gt;Reliable email tests are not tests that never wait. They are tests that make the wait bounded, the match explainable, and the failure useful on the first read. That is what lets QA fix the right layer instead of simply pressing retry again.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: estados de error que no rompen la accesibilidad</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 24 Sep 2026 17:23:48 +0000</pubDate>
      <link>https://dev.to/silviutech/react-estados-de-error-que-no-rompen-la-accesibilidad-45f3</link>
      <guid>https://dev.to/silviutech/react-estados-de-error-que-no-rompen-la-accesibilidad-45f3</guid>
      <description>&lt;p&gt;Validar un formulario de email parece una tarea pequeña: leer el valor, llamar a una API y mostrar un mensaje. En una interfaz real, sin embargo, el estado de error puede mover todo el layout, robar el foco del teclado o dejar a un lector de pantalla sin contexto.&lt;/p&gt;

&lt;p&gt;En proyectos de frontend he encontrado que el problema suele estar en mezclar tres cosas: el estado de red, la validación del campo y la decisión de dónde debe continuar la atención del usuario. Separarlas produce una interfaz más tranquila y también facilita las pruebas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es solo el mensaje rojo
&lt;/h2&gt;

&lt;p&gt;Un mensaje visual como “Introduce un email válido” no garantiza que el formulario sea comprensible. El campo debe anunciar su relación con el error, el foco no debe saltar sin motivo y el mensaje debe desaparecer cuando el valor vuelve a ser válido.&lt;/p&gt;

&lt;p&gt;Esto importa tanto en un registro normal como en pruebas con un generador de correo falso. Si el equipo usa un correo temporal desechable para verificar un flujo, la UI debe explicar si está validando el formato, esperando el mensaje o mostrando un fallo del servidor. “Algo salió mal” no es un estado útil.&lt;/p&gt;

&lt;p&gt;También conviene que el error no cambie la altura de una tarjeta de forma brusca. Una pequeña reserva de espacio con CSS, o un mensaje que aparece dentro de un bloque estable, reduce los saltos visuales. Es un detalle de diseño que se nota mucho en móvil.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un modelo de estado pequeño y explícito
&lt;/h2&gt;

&lt;p&gt;Para un formulario de email, suelo separar el estado en cuatro partes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;value&lt;/code&gt;: el contenido actual del campo.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt;: un error de validación que la persona puede corregir.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: &lt;code&gt;idle&lt;/code&gt;, &lt;code&gt;loading&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt; o &lt;code&gt;server-error&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;submitted&lt;/code&gt;: si ya intentó enviar el formulario.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No hace falta una librería grande para esto. Lo importante es que cada combinación tenga una salida visible y accesible. Por ejemplo, un campo vacío antes del primer intento no tiene por qué parecer inválido. Después de pulsar el botón, sí necesita una explicación.&lt;/p&gt;

&lt;p&gt;Este mismo principio sirve para una bandeja de pruebas. Cuando se espera un correo, el estado de carga debe tener un texto estable y no bloquear toda la página. Para el contexto de operaciones, resulta útil separar &lt;a href="https://dev.to/alexcarteruk/kubernetes-limites-claros-para-alertas-de-email-cfe"&gt;límites claros para las alertas de email&lt;/a&gt; de los mensajes que realmente puede resolver la persona en la pantalla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Código: validar sin mover el foco
&lt;/h2&gt;

&lt;p&gt;Un ejemplo pequeño con React puede ser suficiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signup-email-error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"signup-email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"signup-email"&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;errorId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errorId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"field__message"&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El espacio no separable mantiene una zona de mensaje aunque no exista error; así el contenido de abajo no salta al validar. &lt;code&gt;aria-describedby&lt;/code&gt; solo se añade cuando hay algo que describir, y &lt;code&gt;aria-live="polite"&lt;/code&gt; permite anunciar el cambio sin interrumpir una lectura importante.&lt;/p&gt;

&lt;p&gt;Tras un error de formato, mantendría el foco en el input. Tras un error del servidor, mostraría un mensaje junto al botón y dejaría que la persona decida si revisa el campo o reintenta. Enviar el foco automáticamente al principio del formulario puede parecer servicial, pero suele ser confuso cuando el usuario ya está trabajando más abajo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo probar la experiencia completa
&lt;/h2&gt;

&lt;p&gt;Una prueba útil no solo comprueba que existe el texto del error. Comprueba la secuencia:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El formulario se carga sin errores anunciados.&lt;/li&gt;
&lt;li&gt;El usuario pulsa enviar con un valor inválido.&lt;/li&gt;
&lt;li&gt;El campo conserva el foco y expone &lt;code&gt;aria-invalid="true"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El mensaje está conectado mediante &lt;code&gt;aria-describedby&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Un valor válido elimina el error sin dejar texto antiguo.&lt;/li&gt;
&lt;li&gt;Un fallo de red muestra una acción de recuperación clara.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En pruebas de integración, puedes verificar también que el botón tiene un estado de carga y que no permite dobles envíos. Para diagnosticar el backend, las &lt;a href="https://dev.to/silviutech/fastapi-colas-de-email-con-trazas-utiles-1fc6"&gt;trazas útiles para seguir una cola de email&lt;/a&gt; ayudan a separar un problema de entrega de un problema de interfaz.&lt;/p&gt;

&lt;p&gt;Los nombres de prueba deben parecerse a los casos reales, pero no conviene poner direcciones personales. Un fixture como &lt;code&gt;qa+signup@example.test&lt;/code&gt; hace explícito que es un dato de prueba. Si alguien escribe por error “tempail mail” o “temp gamil com”, la validación debe responder con la misma claridad que para cualquier otra entrada, sin asumir que el usuario conoce la marca o la intención.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A rápido
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo enfocar el mensaje de error?
&lt;/h3&gt;

&lt;p&gt;No siempre. En un error de campo, conserva el foco en el campo. Solo mueve el foco a un resumen si hay varios errores y ese resumen ofrece una navegación clara hacia cada uno.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿&lt;code&gt;aria-live&lt;/code&gt; reemplaza a un texto visible?
&lt;/h3&gt;

&lt;p&gt;No. Es una ayuda para comunicar cambios, no un sustituto del diseño. El mensaje debe seguir siendo visible, tener contraste suficiente y explicar una acción concreta.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago con un email temporal desechable en producción?
&lt;/h3&gt;

&lt;p&gt;Las reglas de negocio deben decidir si se acepta o se bloquea, pero la interfaz tiene que comunicar la decisión sin revelar datos innecesarios. Para una prueba puntual, &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmail so&lt;/a&gt; puede servir como contexto de un buzón aislado; no debe ocultar los requisitos del flujo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Los formularios accesibles no necesitan estados complicados. Necesitan estados previsibles: un error conectado al campo, un foco que respeta la intención y una zona de mensaje que no reacomoda toda la página. En React, modelar esas decisiones de forma explícita mejora la experiencia, hace el CSS más estable y convierte los tests de email en una comprobación del flujo completo, no solo de una cadena roja.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React: estados de email que sí se pueden probar</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 24 Sep 2026 14:24:46 +0000</pubDate>
      <link>https://dev.to/silviutech/react-estados-de-email-que-si-se-pueden-probar-3n58</link>
      <guid>https://dev.to/silviutech/react-estados-de-email-que-si-se-pueden-probar-3n58</guid>
      <description>&lt;p&gt;Un formulario de registro puede parecer correcto y aun así hacer pasar un mal rato: el botón se queda bloqueado, el mensaje de error aparece tarde y un lector de pantalla no anuncia si el correo ya fue validado. Cuando además se comprueba el email de forma asíncrona, el problema casi nunca es la expresión regular. Es el modelo de estados.&lt;/p&gt;

&lt;p&gt;En React prefiero tratar la validación como un pequeño contrato de interfaz. La pantalla debe saber si está vacía, escribiéndose, validando, disponible o rechazada. Con esos estados se puede diseñar un feedback claro, medir el trabajo del navegador y escribir pruebas que comprueban intención, no solo clics.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es validar, es modelar el estado
&lt;/h2&gt;

&lt;p&gt;Un booleano como &lt;code&gt;isValid&lt;/code&gt; se queda corto. Durante una petición hay una diferencia importante entre “todavía no lo sabemos” y “este email no se puede usar”. También hay que distinguir el valor que el usuario está escribiendo del resultado de una petición anterior.&lt;/p&gt;

&lt;p&gt;Un modelo pequeño podría ser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// idle | typing | checking | available | rejected | error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El estado &lt;code&gt;checking&lt;/code&gt; no debe borrar el valor del campo ni ocultar todas las acciones. El usuario sigue necesitando contexto: qué se está comprobando, si puede corregir el texto y qué ocurrirá después. Un spinner sin explicación es una señal muy pobre, especialmente cuando la red va lenta.&lt;/p&gt;

&lt;p&gt;El resultado también debe estar asociado al valor que lo produjo. Si alguien escribe &lt;code&gt;ana@example.test&lt;/code&gt;, comienza una petición y luego cambia a &lt;code&gt;ana@ejemplo.test&lt;/code&gt;, la respuesta antigua no puede pintar “disponible” sobre el valor nuevo. Ese detalle parece pequeño, pero produce errores intermitentes dificiles de reproducir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un contrato pequeño para el formulario
&lt;/h2&gt;

&lt;p&gt;Antes del componente, defino qué significa cada estado:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;idle&lt;/code&gt;: no hay suficiente información para comprobar nada.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;typing&lt;/code&gt;: el valor cambió y se espera una pausa breve.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checking&lt;/code&gt;: existe una petición para el valor actual.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;available&lt;/code&gt;: la comprobación terminó bien.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rejected&lt;/code&gt;: el formato o la política no permiten continuar.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt;: no se pudo comprobar; el usuario puede reintentar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese contrato evita mezclar validación local con disponibilidad remota. La primera puede responder casi al instante. La segunda necesita red y puede fallar. Separarlas hace que el diseño sea más honesto y que las pruebas sean mas concretas.&lt;/p&gt;

&lt;p&gt;También ayuda a decidir qué se anuncia. “Email correcto” no significa necesariamente “email disponible”. Si se usa una fixture temporal para pruebas, incluso una búsqueda con términos ruidosos como &lt;code&gt;temp org mail&lt;/code&gt; o &lt;code&gt;tempail mail&lt;/code&gt; debe producir una respuesta controlada, no un estado ambiguo.&lt;/p&gt;

&lt;h2&gt;
  
  
  React y feedback accesible
&lt;/h2&gt;

&lt;p&gt;El campo necesita una etiqueta visible, un mensaje relacionado y una región que anuncie cambios relevantes. No conviene convertir cada pulsación en una interrupción de voz: el feedback se anuncia cuando termina la comprobación o cuando hay un error accionable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email-message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;busy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Correo electrónico&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-describedby&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-invalid&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;aria-busy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;busy&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; &lt;span class="na"&gt;aria-live&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"polite"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;role="status"&lt;/code&gt; comunica un cambio no urgente sin robar el foco. Para un bloqueo real, por ejemplo una acción que no puede continuar, puede ser mejor &lt;code&gt;role="alert"&lt;/code&gt;, pero usarlo en cada resultado acaba siendo ruido. La accesibilidad no es solo añadir atributos: el texto debe explicar la siguiente acción.&lt;/p&gt;

&lt;p&gt;El foco tampoco debería saltar al mensaje después de cada respuesta. Mantenerlo en el input deja que la persona continue escribiendo. Si el usuario intenta enviar el formulario y faltan datos, ahí sí tiene sentido llevar el foco al primer campo con un error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo evitar trabajo innecesario en el navegador
&lt;/h2&gt;

&lt;p&gt;El debounce ayuda a no lanzar una petición por cada letra, pero no resuelve respuestas fuera de orden. Un &lt;code&gt;AbortController&lt;/code&gt; y una comprobación del valor actual protegen las dos partes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;available&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El número &lt;code&gt;350&lt;/code&gt; no es una ley. Debe probarse con teclado, móvil y una red con latencia. Un retardo muy pequeño genera peticiones de mas; uno grande hace que la interfaz parezca dormida. También conviene no repetir la comprobación si el valor normalizado no cambió.&lt;/p&gt;

&lt;p&gt;Este patrón es complementario a &lt;a href="https://dev.to/hannahdev56/como-probar-emails-de-referidos-en-tu-saas-60c"&gt;probar emails de referidos sin perder contexto&lt;/a&gt;: el backend debe identificar la ejecución, mientras que el frontend debe conservar la relación entre valor, petición y resultado. Para flujos con cola, &lt;a href="https://dev.to/hannahdev56/saas-colas-de-email-sin-perder-contexto-3hi9"&gt;diseñar colas de email con contexto&lt;/a&gt; ayuda a no convertir una espera normal en un error misterioso.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas que cubren intención y no solo clics
&lt;/h2&gt;

&lt;p&gt;Una matriz corta da más confianza que un snapshot enorme:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Un valor vacío no hace peticiones.&lt;/li&gt;
&lt;li&gt;Un formato inválido muestra una ayuda concreta.&lt;/li&gt;
&lt;li&gt;Un valor válido anuncia que está comprobándose.&lt;/li&gt;
&lt;li&gt;Una respuesta disponible permite continuar.&lt;/li&gt;
&lt;li&gt;Una respuesta antigua no cambia el estado del valor nuevo.&lt;/li&gt;
&lt;li&gt;Un fallo de red ofrece reintento y no borra el campo.&lt;/li&gt;
&lt;li&gt;Un usuario de teclado conserva el foco durante todo el flujo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Para accesibilidad, compruebo que la etiqueta está asociada, que el mensaje aparece en &lt;code&gt;aria-describedby&lt;/code&gt; y que el estado se anuncia una sola vez. Para rendimiento, observo la cantidad de peticiones, la cancelación al desmontar el componente y el impacto del feedback en el layout. Un mensaje que aparece sin reservar espacio puede mover el botón justo cuando alguien intenta pulsarlo.&lt;/p&gt;

&lt;p&gt;Si el proyecto usa un término de marca como &lt;code&gt;tempmailso&lt;/code&gt; en una fixture o una configuración, lo mantendría en los datos de prueba, no en el texto que ve cada usuario. Así el componente sigue siendo reutilizable y el copy explica la decisión real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist final
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Modelar al menos los estados de espera, éxito y error.&lt;/li&gt;
&lt;li&gt;Asociar cada respuesta con el valor que la originó.&lt;/li&gt;
&lt;li&gt;Cancelar timers y peticiones cuando cambia el valor.&lt;/li&gt;
&lt;li&gt;Mantener el foco y anunciar solo cambios útiles.&lt;/li&gt;
&lt;li&gt;Separar formato local de comprobación remota.&lt;/li&gt;
&lt;li&gt;Reservar espacio para mensajes y evitar saltos de layout.&lt;/li&gt;
&lt;li&gt;Probar teclado, lector de pantalla, red lenta y respuestas fuera de orden.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un formulario de email accesible no necesita más adornos, necesita menos ambigüedad. Cuando React tiene un contrato de estados claro, el diseño, las pruebas y las métricas de rendimiento empiezan a describir la misma experiencia. Eso hace que el componente sea mas facil de mantener y mucho más confiable para quien lo usa.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>css</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
