<?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>Playwright Email Tests Need a Clock Skew Matrix</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:23:20 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-a-clock-skew-matrix-2594</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-a-clock-skew-matrix-2594</guid>
      <description>&lt;p&gt;Email tests often fail in a confusing way: the application says a verification message was sent, but Playwright cannot find it yet. A retry may pass seconds later. That looks like a random browser problem, while the real defect is usually an unstated timing contract between the app, the mailbox, and the test runner.&lt;/p&gt;

&lt;p&gt;I treat this as a clock problem, even when no system clock is wrong. Each part of the flow has a different notion of “now”: the API records a send time, a queue adds delay, the mailbox exposes a message later, and the CI worker starts polling on its own schedule. A clock skew matrix makes those boundaries visible and gives QA a repeatable way to test them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a passing email test can still be flaky
&lt;/h2&gt;

&lt;p&gt;A basic test usually looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a test user.&lt;/li&gt;
&lt;li&gt;Submit the signup form.&lt;/li&gt;
&lt;li&gt;Wait for the newest email.&lt;/li&gt;
&lt;li&gt;Click the verification link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The dangerous assumption is step three. “Newest” does not mean “created by this run,” and “not visible yet” does not mean “not sent.” A shared inbox can contain an older message, while a dedicated inbox can still be delayed by a queue. The failure become especially hard to reproduce when the test runs in parallel.&lt;/p&gt;

&lt;p&gt;The same issue appears in flows described with search phrases such as &lt;strong&gt;temp mail so&lt;/strong&gt;. The mailbox provider is only one participant in the contract. It cannot tell your test which request owns a message unless the application adds a correlation marker or the test uses an isolated mailbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a clock skew matrix
&lt;/h2&gt;

&lt;p&gt;Before changing timeout values, list the timestamps that matter. A small matrix is more useful than a single global wait:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;Evidence to record&lt;/th&gt;
&lt;th&gt;Expected relationship&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request accepted&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requested_at&lt;/code&gt;, request ID&lt;/td&gt;
&lt;td&gt;Before message creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message queued&lt;/td&gt;
&lt;td&gt;delivery event or provider ID&lt;/td&gt;
&lt;td&gt;At or after request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message visible&lt;/td&gt;
&lt;td&gt;mailbox timestamp, message ID&lt;/td&gt;
&lt;td&gt;After queue time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Link consumed&lt;/td&gt;
&lt;td&gt;token-use event&lt;/td&gt;
&lt;td&gt;After message visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test assertion&lt;/td&gt;
&lt;td&gt;Playwright step time&lt;/td&gt;
&lt;td&gt;After the required event&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then run the scenario with deliberate conditions: a fast delivery, a normal delivery, a delayed delivery, and a message that belongs to another run. The goal is not to make every test wait longer. Prove that it waits for the right evidence and rejects a message outside its ownership window.&lt;/p&gt;

&lt;p&gt;For example, define a message as eligible only when its provider ID matches the request, its timestamp is after &lt;code&gt;requested_at&lt;/code&gt;, and its subject or body contains a non-secret run marker. A body marker is more safer than selecting by subject alone, but it should never contain a token or personal data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn timing into a Playwright contract
&lt;/h2&gt;

&lt;p&gt;Keep the polling helper explicit about its deadline and ownership rules. Do not hide a five-minute retry loop inside a generic &lt;code&gt;waitForEmail&lt;/code&gt; function:&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;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;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;mailboxId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestedAt&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="nx"&gt;runId&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="na"&gt;intervalMs&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="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;receivedAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeDefined&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;runId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runId&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verificationUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper should return evidence that explains its decision: message ID, observed timestamp, polling attempts, and the final reason for success or timeout. Redact the URL before logging it. The checks is easier to review when each field has one clear meaning. If a test uses a temporary mailbox for isolation, keep its lease tied to the run and clean it up in a &lt;code&gt;finally&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;For teams testing invitation flows, these &lt;a href="https://dev.to/ryanlee91/version-invite-emails-across-react-and-node-4ci6"&gt;versioned invite email flows&lt;/a&gt; are a useful companion pattern: the message content and the application state should agree on which version was requested. In CI, &lt;a href="https://dev.to/pong1965/contract-test-api-emails-in-github-actions-45f8"&gt;contract-testing email APIs in GitHub Actions&lt;/a&gt; can catch provider or API changes before they become browser failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose failures with evidence
&lt;/h2&gt;

&lt;p&gt;When the test fails, classify the first missing boundary rather than immediately increasing the timeout:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No request ID:&lt;/strong&gt; the browser or API step failed before delivery began.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request exists, no queue event:&lt;/strong&gt; inspect the application or worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue event exists, no mailbox message:&lt;/strong&gt; inspect provider delivery and lease ownership.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message exists, wrong run marker:&lt;/strong&gt; the inbox is shared or the correlation logic is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message is correct, link is rejected:&lt;/strong&gt; inspect origin, expiry, or token consumption.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This classification keeps a QA report actionable. “Email missing” is a symptom; “message visible 42 seconds after queue event” is a testable observation. The result are much easier to compare across local runs and CI. A single late worker can makes the whole flow look random if those timestamps are not saved.&lt;/p&gt;

&lt;p&gt;Also record the runner timezone and timestamp format. A string like &lt;code&gt;10/11/2026&lt;/code&gt; is ambiguous, and a mailbox timestamp rounded to minutes can look older than it is. Use ISO 8601 values in evidence, even if the human-facing report uses a friendly format. A temp mailid label must not change ownership.&lt;/p&gt;

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

&lt;p&gt;For each job, generate a unique run ID, create or reserve an isolated mailbox, and store only the minimum metadata needed to diagnose delivery. Submit the request, capture &lt;code&gt;requested_at&lt;/code&gt; from the same trusted source, then poll for a matching message. These records is enough for most triage without keeping message bodies forever. After the browser assertion, verify that a second attempt cannot consume the same link.&lt;/p&gt;

&lt;p&gt;Keep the matrix in the test report. A delayed message is not automatically a product failure if it stays inside the documented service-level boundary, but it is valuable reliability data. If delivery exceeds the boundary, fail with the queue and mailbox evidence attached. This is better than stretching the timeout until every slow run passes.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Give every email test a unique run ID.&lt;/li&gt;
&lt;li&gt;Match messages by ownership, not by “newest.”&lt;/li&gt;
&lt;li&gt;Record request, queue, visibility, and consumption timestamps.&lt;/li&gt;
&lt;li&gt;Test fast, normal, delayed, and wrong-run messages.&lt;/li&gt;
&lt;li&gt;Bound Playwright polling with a clear deadline.&lt;/li&gt;
&lt;li&gt;Redact links and tokens from logs and traces.&lt;/li&gt;
&lt;li&gt;Separate delivery failures from browser assertion failures.&lt;/li&gt;
&lt;li&gt;Clean up mailbox leases after each run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should I increase the Playwright timeout?
&lt;/h3&gt;

&lt;p&gt;Only after measuring the delivery boundary. A longer timeout can hide queue regressions and make feedback slower. First make the poll ownership-aware, then choose a deadline that reflects the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a dedicated mailbox enough?
&lt;/h3&gt;

&lt;p&gt;It reduces cross-run contamination, but it does not prove that the message belongs to the current request. Keep a run marker and timestamp checks as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best first diagnostic?
&lt;/h3&gt;

&lt;p&gt;Compare &lt;code&gt;requested_at&lt;/code&gt;, queue time, mailbox visibility time, and assertion time for one failed run. That sequence usually identifies the missing contract faster than rerunning the browser test.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>React: filtros accesibles para un inbox temporal</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:22:55 +0000</pubDate>
      <link>https://dev.to/silviutech/react-filtros-accesibles-para-un-inbox-temporal-2c0p</link>
      <guid>https://dev.to/silviutech/react-filtros-accesibles-para-un-inbox-temporal-2c0p</guid>
      <description>&lt;h1&gt;
  
  
  React: filtros accesibles para un inbox temporal
&lt;/h1&gt;

&lt;p&gt;Un inbox temporal de pruebas suele empezar como una lista sencilla: asunto, remitente y hora. Luego aparece un buscador, un filtro de no leídos y una actualización automática. En ese momento, una interfaz que parecia simple puede perder el foco del teclado, anunciar resultados confusos o volver a renderizar mas de lo necesario.&lt;/p&gt;

&lt;p&gt;El filtro no es solo una mejora visual. Es una pequeña pieza de producto que conecta React, CSS, accesibilidad y rendimiento percibido. Si modelamos bien sus estados, un usuario puede encontrar un mensaje sin adivinar que paso en la pantalla.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué un filtro también es una decisión de accesibilidad
&lt;/h2&gt;

&lt;p&gt;Un campo de búsqueda debe tener una etiqueta visible o un nombre accesible claro. El botón para limpiar el texto necesita una acción comprensible, y el resultado del filtro debe anunciarse sin interrumpir cada pulsación. Un &lt;code&gt;aria-live="polite"&lt;/code&gt; para el resumen suele ser suficiente: “3 mensajes encontrados”.&lt;/p&gt;

&lt;p&gt;También conviene separar dos cosas que a veces se mezclan: el estado del filtro y el estado de la bandeja. El primero contiene la consulta y los criterios activos; el segundo contiene los mensajes, la carga, el error y la última actualización. Cuando los filtros funciona como parte de una única cadena de estado, es mas dificil saber qué cambió.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modela los estados antes de pintar la lista
&lt;/h2&gt;

&lt;p&gt;Para un componente de React, una forma práctica es definir primero estos estados:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cargando:&lt;/strong&gt; todavía no hay datos para filtrar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lista disponible:&lt;/strong&gt; existen mensajes y se pueden aplicar criterios.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin coincidencias:&lt;/strong&gt; la consulta es válida, pero no encuentra resultados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error:&lt;/strong&gt; la fuente de datos no respondió o devolvió un resultado inválido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El estado “sin coincidencias” no debería parecer un error. Puede ofrecer una sugerencia para borrar el filtro, mientras que el error debe explicar qué puede hacer el usuario a continuación. Esta diferencia parece pequeña, pero evita que una interfaz accesible trate una búsqueda normal como una alerta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un filtro React pequeño y medible
&lt;/h2&gt;

&lt;p&gt;La lógica puede permanecer cerca de los datos, pero no dentro de cada fila. Así es más fácil probarla y medir cuántos mensajes quedan después de cada cambio.&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;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onlyUnread&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOnlyUnread&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;visibleMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleLowerCase&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="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;matchesText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="s2"&gt;`&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="s2"&gt; &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;sender&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&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;normalized&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;matchesText&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;onlyUnread&lt;/span&gt; &lt;span class="o"&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;read&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;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onlyUnread&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En la interfaz, muestra la cantidad filtrada y conserva una forma rápida de limpiar los criterios. Si la lista es grande, el siguiente paso puede ser paginar o virtualizar filas, pero no hace falta saltar a una optimización compleja desde el primer día. Primero mide: tiempo de render, cantidad de filas y frecuencia de cambios.&lt;/p&gt;

&lt;p&gt;Una métrica útil es el tiempo entre la pulsación y la actualización visible. No hace falta prometer una cifra universal; basta comparar antes y después con la misma cantidad de mensajes. Para ideas sobre cómo &lt;a href="https://dev.to/silviutech/react-mide-la-ux-de-un-inbox-de-prueba-5bj8"&gt;medir la UX de un inbox de prueba&lt;/a&gt;, conviene observar también el estado que ve el usuario, no solo el tiempo de la petición.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS y foco: que la interfaz no se mueva bajo el usuario
&lt;/h2&gt;

&lt;p&gt;El campo de búsqueda y el botón de limpiar deben conservar un foco visible con contraste suficiente. No uses el color como única señal para distinguir un mensaje leído. Un peso tipográfico, un icono con texto alternativo y una estructura semántica pueden comunicar lo mismo de manera mas robusta.&lt;/p&gt;

&lt;p&gt;Cuando cambia la lista, evita que el panel cambie de altura de forma brusca. Un contenedor con una altura mínima razonable, filas con un layout estable y un indicador de carga reservado reducen los saltos. Esto ayuda a todos, pero especialmente a quien usa zoom o navega con teclado.&lt;/p&gt;

&lt;p&gt;El orden de foco merece una prueba aparte. Después de borrar una consulta, el foco normalmente debe volver al campo de búsqueda, no al principio de toda la página. Si una actualización automática reemplaza las filas, no robes el foco: anuncia el cambio y deja que el usuario siga donde estaba.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruebas con datos de correo temporal
&lt;/h2&gt;

&lt;p&gt;Los datos de prueba deben parecerse a los casos reales: asuntos largos, remitentes con nombres distintos, mensajes no leídos y una bandeja vacía. Incluye además cadenas que un equipo puede escribir en fixtures, como &lt;code&gt;dummy e mail&lt;/code&gt; o &lt;code&gt;temp gamil com&lt;/code&gt;, pero no las uses como instrucciones para una persona final.&lt;/p&gt;

&lt;p&gt;Para escenarios de registro y verificación, una &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;cuenta de correo desechable&lt;/a&gt; puede aislar el flujo del correo personal. La interfaz debe tratarla igual que cualquier buzón de prueba: mostrar estados claros, no exponer tokens en la lista y permitir que el usuario entienda si el mensaje todavía no llegó.&lt;/p&gt;

&lt;p&gt;Si el inbox depende de un backend que produce mensajes de forma asíncrona, una cola observable evita que el frontend adivine cuánto esperar. La idea de &lt;a href="https://dev.to/silviutech/fastapi-cola-visible-para-correos-async-419m"&gt;una cola visible para correos asíncronos&lt;/a&gt; también sirve para diseñar una UI que diferencie “esperando” de “vacío”. Es facil olvidar esta diferencia cuando solo se prueba el caso feliz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist antes de publicar el componente
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿El campo tiene etiqueta visible y nombre accesible?&lt;/li&gt;
&lt;li&gt;¿La cantidad de resultados se comunica sin ruido?&lt;/li&gt;
&lt;li&gt;¿El foco sigue una ruta predecible después de filtrar y limpiar?&lt;/li&gt;
&lt;li&gt;¿Los estados de carga, vacío y error son distintos?&lt;/li&gt;
&lt;li&gt;¿La lista mantiene una altura y un orden visual estables?&lt;/li&gt;
&lt;li&gt;¿Se probó con teclado, zoom y mensajes largos?&lt;/li&gt;
&lt;li&gt;¿La actualización automática respeta el foco actual?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  ¿Debo filtrar en el servidor?
&lt;/h3&gt;

&lt;p&gt;Para una bandeja pequeña, filtrar en el cliente es simple y suficiente. Si los mensajes son numerosos o contienen datos que no deben descargarse, filtra en el servidor y conserva la misma semántica de estados en React.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Es mejor un botón o un filtro instantáneo?
&lt;/h3&gt;

&lt;p&gt;El filtro instantáneo puede sentirse rápido, pero un botón “Aplicar” resulta útil en consultas costosas o en formularios complejos. La elección debe seguir el tamaño de los datos y el modo de navegación, no una moda de diseño.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué pasa si no hay mensajes?
&lt;/h3&gt;

&lt;p&gt;Explica si la bandeja todavía está cargando, si está vacía o si el filtro no encontró coincidencias. Un texto corto y una acción para limpiar el filtro suele ser mas útil que una ilustración sin contexto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;Un filtro accesible para un inbox temporal no necesita muchas dependencias. Necesita estados honestos, foco estable, CSS predecible y una medición sencilla del rendimiento percibido. Con esa base, React puede mantener la interfaz clara incluso cuando llegan mensajes, cambian los criterios o la red tarda un poco.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>css</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Playwright Email Tests Need a Failure Triage Map</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:22:57 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-a-failure-triage-map-22c5</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-a-failure-triage-map-22c5</guid>
      <description>&lt;p&gt;Email verification tests often fail in a confusing way: the UI says “check your inbox,” the message exists somewhere, and the test still times out. The usual reaction is to add another wait. That can make one run pass while making the real failure harder to see.&lt;/p&gt;

&lt;p&gt;This is a more useful approach: treat the mailbox as a boundary with its own evidence, then classify the failure before changing the timeout. The same workflow works whether the test uses a local fake inbox, a service such as tempmailso, or a controlled test mailbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a green test can still hide an email failure
&lt;/h2&gt;

&lt;p&gt;An email verification flow has at least three systems involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application creates a verification event.&lt;/li&gt;
&lt;li&gt;A mail provider accepts, delays, or rejects the message.&lt;/li&gt;
&lt;li&gt;The test client reads the mailbox and follows the link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An end-to-end test can fail in any of those places. A locator timeout on &lt;code&gt;a[href*="verify"]&lt;/code&gt; does not tell us which one. It only tells us that the final assertion did not become true in time.&lt;/p&gt;

&lt;p&gt;The first useful question is not “how long should I wait?” It is “what was the last fact we proved?” For QA work, that small change in wording is important. It turns a vague timeout into a sequence of observable claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  A triage map for the mailbox boundary
&lt;/h2&gt;

&lt;p&gt;I use four states in the test report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not requested:&lt;/strong&gt; the application never recorded the send action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Requested, not received:&lt;/strong&gt; the application emitted an event, but no message is visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Received, not usable:&lt;/strong&gt; a message exists, but it has the wrong recipient, token, or link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usable, not completed:&lt;/strong&gt; the test opened the right link, but the application rejected it or the UI did not settle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This map prevents a common mistake: blaming Playwright for a delivery problem. It also makes ownership clearer. The first state belongs to the application flow, the second usually belongs to delivery or the mailbox fixture, and the last two need a closer look at token and UI behavior.&lt;/p&gt;

&lt;p&gt;For another way to make mailbox state explicit, see &lt;a href="https://dev.to/silviutech/playwright-mail-tests-need-a-consumption-ledger-5d4g"&gt;a consumption ledger for Playwright mail tests&lt;/a&gt;. A ledger is especially handy when parallel tests share an inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build evidence before adding waits
&lt;/h2&gt;

&lt;p&gt;Give each test a correlation value and carry it through the request, the message subject, and the assertion. Do not log the full verification token; a short hash or message ID is enough. This keeps debugging useful without leaking credentials into CI logs.&lt;/p&gt;

&lt;p&gt;The test should capture four timestamps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;registration submitted&lt;/li&gt;
&lt;li&gt;send event observed&lt;/li&gt;
&lt;li&gt;matching message observed&lt;/li&gt;
&lt;li&gt;verification response completed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gaps are more informative than one large timeout. If the send event is missing, increasing mailbox polling will not help. If the message arrives quickly but the link request fails, the defect is probably in token state or routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playwright implementation
&lt;/h2&gt;

&lt;p&gt;Here is a small polling helper. It waits for a message matching a correlation value, but it also records the reason for each unsuccessful poll. The helper does not hide the failure behind a sleep.&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;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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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;Mail&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;text&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForMatchingMail&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;Page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mailboxUrl&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;correlation&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;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&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="nx"&gt;Mail&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastCount&lt;/span&gt; &lt;span class="o"&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;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;response&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mailboxUrl&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toBeTruthy&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="k"&gt;as&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;lastCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&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="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="o"&gt;=&amp;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;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="nx"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;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;correlation&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;match&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;match&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;waitForTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&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="s2"&gt;`No mail for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; mailbox contained &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; message(s)`&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;In a real suite, replace the final error with a structured attachment containing the correlation value, counts, and timestamps. Keep the polling interval modest and make the timeout a documented contract. A timeout chosen by feeling is hard to maintain.&lt;/p&gt;

&lt;p&gt;The mailbox should also be isolated per test or per worker. A shared inbox makes an old message look like a new success, specially when subjects are similar. A unique recipient or correlation value is usually more reliable than deleting messages after each test.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the failure points somewhere else
&lt;/h2&gt;

&lt;p&gt;If no send event exists, inspect the registration response and server logs. If the send event exists but no message arrives, inspect the fixture, provider response, and mailbox retention. If the message is present but has a wrong token, compare the token identifier with the database record. If the link works but the page remains pending, inspect the UI request and its final response.&lt;/p&gt;

&lt;p&gt;This is also where &lt;a href="https://dev.to/ryanlee91/react-form-tests-need-a-mailbox-boundary-1ojn"&gt;a mailbox boundary in form tests&lt;/a&gt; is a useful design idea: keep message retrieval separate from form assertions. The separation makes the test easier to rerun and the failure more clear.&lt;/p&gt;

&lt;p&gt;One small wording trap is worth mentioning. Teams sometimes call a test address a “tem email” in notes or fixture names. Keep that phrase as plain text if it is part of your existing vocabulary, but use precise terms in APIs and test names so nobody mistakes it for a product or protocol.&lt;/p&gt;

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

&lt;p&gt;Before changing a timeout, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the recipient unique for this test or worker?&lt;/li&gt;
&lt;li&gt;Is the send event correlated with the test run?&lt;/li&gt;
&lt;li&gt;Did the mailbox API return successfully on every poll?&lt;/li&gt;
&lt;li&gt;Did the matching message arrive, or did an unrelated message match?&lt;/li&gt;
&lt;li&gt;Is the verification URL complete and from the expected host?&lt;/li&gt;
&lt;li&gt;Do the timestamps identify a slow component?&lt;/li&gt;
&lt;li&gt;Does a trace or attachment show the last confirmed state?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a test fails, attach the triage state to the report. That one label is often more valuable than another screenshot. It tells the next person where to look first, and it makes flaky-test trends easier to measure over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions that come up in review
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should every poll be a separate assertion?
&lt;/h3&gt;

&lt;p&gt;No. Polls are observations. Assert the final contract and record intermediate facts for diagnosis. Turning every empty poll into a failure creates noisy reports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a longer timeout ever the right fix?
&lt;/h3&gt;

&lt;p&gt;Yes, when delivery latency is known, bounded, and visible in the evidence. First measure the latency, then set a budget with a reason. A longer wait should not compensate for missing correlation or a shared mailbox.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should the test parse HTML or click the first link?
&lt;/h3&gt;

&lt;p&gt;Parse the message and validate the URL before navigation. Select the link associated with the expected action, and assert the destination host. The first link is not always the verification link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Reliable Playwright email tests are less about perfect waiting and more about proving state in order. Correlate the run, observe the mailbox boundary, classify the failure, and only then adjust timing. The result is a test that explains what went wrong instead of merely reporting that a selector was late.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Playwright Email Tests Need a Cleanup Contract</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 22 Oct 2026 23:22:56 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-a-cleanup-contract-2ebh</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-a-cleanup-contract-2ebh</guid>
      <description>&lt;p&gt;Email-based end-to-end tests often look flaky for the wrong reason. A signup test creates a user, waits for a verification message, and sometimes finds an email that belongs to a previous run. The test may pass with the wrong message, fail because the inbox is full of old messages, or become slower as more CI jobs share the same mailbox.&lt;/p&gt;

&lt;p&gt;In my QA work, I treat the mailbox as a test resource with a lifecycle, not as a passive place where messages happen to arrive. Search notes may call the workflow “temp mail so” or “tempmailso”, but the useful engineering question is simpler: which test owns this mailbox, and when is that ownership finished?&lt;/p&gt;

&lt;h2&gt;
  
  
  The flaky symptom is often a cleanup failure
&lt;/h2&gt;

&lt;p&gt;Consider two Playwright workers running the same registration scenario. Both use an address based on &lt;code&gt;signup-test@example.invalid&lt;/code&gt;, and both ask for the newest message. If worker A receives its message after worker B has already polled, the result depends on timing. A retry can hide the problem for one run and make the next run harder to understand.&lt;/p&gt;

&lt;p&gt;The failure is more usefull when the test reports the mailbox state at each boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mailbox created or leased;&lt;/li&gt;
&lt;li&gt;test identifier assigned;&lt;/li&gt;
&lt;li&gt;message requested and received;&lt;/li&gt;
&lt;li&gt;verification completed;&lt;/li&gt;
&lt;li&gt;mailbox cleared or released.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those events, a “verification email not found” error combines delivery, filtering, and cleanup into one vague symptom. Increasing the timeout only gives stale state more time to interfere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every test mailbox an ownership contract
&lt;/h2&gt;

&lt;p&gt;A reliable fixture should answer four questions before the browser opens the signup page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who owns it?&lt;/strong&gt; Use a run ID and worker ID, such as &lt;code&gt;ci-1842-w3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What can be read?&lt;/strong&gt; Filter by recipient, subject, test ID, and a time lower bound.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How long can it live?&lt;/strong&gt; Set a lease expiry even when the test crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who cleans it?&lt;/strong&gt; The test should release it, while a scheduled reaper handles abandoned leases.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The address itself is not enough. A message from an older run can still have a matching subject. Include a unique token in the request and assert that the received message contains it. Keep the token in the test artifact, but avoid storing the complete message when a message ID, timestamp, and hash are enough for diagnosis.&lt;/p&gt;

&lt;p&gt;This ownership model is also useful when CI is paused or deployments are frozen. Defining &lt;a href="https://dev.to/jasonmills94/eks-freeze-windows-need-alert-boundaries-hc0"&gt;alert boundaries for CI freezes&lt;/a&gt; makes it clearer whether a delayed test is a product failure or an environment that is intentionally not moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make cleanup observable in Playwright
&lt;/h2&gt;

&lt;p&gt;Use a fixture that records its state and always attempts release in teardown. The exact API will vary, but the shape can be straightforward:&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="s1"&gt;verifies a new account&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;testInfo&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;mailbox&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;mailboxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerIndex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI_RUN_ID&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local-run&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;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="s1"&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="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&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="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;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create account&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&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;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;token&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;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;after&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;leasedAt&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;20&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;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;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContainText&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 your account&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;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;release&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="s1"&gt;test-teardown&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block is important, but it is not the whole solution. If the browser process is killed, teardown may never run. The mailbox service needs a lease expiry and a reaper, and the CI job should publish counts for active, released, and expired leases. A small, visible contract is more reliable than a cleanup promise hidden in a helper.&lt;/p&gt;

&lt;p&gt;For Kubernetes-backed runners, also define the scope of a fixture namespace. A &lt;a href="https://dev.to/jasonmills94/kubernetes-email-fixtures-need-a-namespace-budget-10ia"&gt;namespace budget for email fixtures&lt;/a&gt; helps prevent parallel jobs from quietly sharing resources that were designed for one worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose stale messages before increasing timeouts
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ownership:&lt;/strong&gt; Does the message recipient belong to this run and worker?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Age:&lt;/strong&gt; Was it received after the mailbox lease began?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token:&lt;/strong&gt; Does it contain the unique scenario token?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State:&lt;/strong&gt; Was the mailbox previously released, reused, or expired?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery:&lt;/strong&gt; Did the application emit the event before the test started polling?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the message is old, classify the result as &lt;code&gt;stale-message&lt;/code&gt;, not &lt;code&gt;email-timeout&lt;/code&gt;. If the token is wrong, classify it as &lt;code&gt;cross-run-collision&lt;/code&gt;. If no event was emitted, the email adapter is not the first place to debug. This makes failures actionable even when the test cannot reproduce locally.&lt;/p&gt;

&lt;p&gt;Small wording mistakes in test output can make triage harder: &lt;code&gt;enviroment&lt;/code&gt;, &lt;code&gt;seperately&lt;/code&gt;, or &lt;code&gt;occured&lt;/code&gt; may look harmless, but consistent state names matter more than perfect prose. Keep diagnostic fields stable, and make the cleanup result availble in the final report. Teams may also search for malformed terms such as &lt;code&gt;temp org mail&lt;/code&gt; or &lt;code&gt;tempail mail&lt;/code&gt;; map those terms in documentation, but do not let them become logic or selectors.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Lease one mailbox per test or controlled worker, never one shared inbox for the whole suite.&lt;/li&gt;
&lt;li&gt;Add a run token and a lease timestamp to every message query.&lt;/li&gt;
&lt;li&gt;Filter by recipient, token, and time before asserting message content.&lt;/li&gt;
&lt;li&gt;Release in teardown and expire abandoned leases server-side.&lt;/li&gt;
&lt;li&gt;Report &lt;code&gt;leased&lt;/code&gt;, &lt;code&gt;released&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt;, &lt;code&gt;stale&lt;/code&gt;, and &lt;code&gt;unmatched&lt;/code&gt; counts.&lt;/li&gt;
&lt;li&gt;Keep message IDs and hashes for debugging instead of unnecessary message bodies.&lt;/li&gt;
&lt;li&gt;Classify the failure before changing a timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A cleanup contract turns email testing from a timing gamble into a diagnosable workflow. The browser still waits for a real message, but Playwright can now prove whether that message belongs to the current test, whether the fixture was cleaned up, and which boundary needs attention.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>automation</category>
      <category>qa</category>
    </item>
    <item>
      <title>Playwright Email Tests Need an Evidence Ledger</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Thu, 22 Oct 2026 17:23:15 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-an-evidence-ledger-381n</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-an-evidence-ledger-381n</guid>
      <description>&lt;p&gt;Email verification tests often fail in the least helpful way: a button click times out, the CI log says nothing about the message, and the whole run is marked red. The next person reruns it. Then it passes.&lt;/p&gt;

&lt;p&gt;I have found that the real problem is not always delivery speed. It is missing evidence. A browser test usually records what the page did, while the mailbox adapter knows what happened to the message. If those observations are not connected, a failure becomes guesswork.&lt;/p&gt;

&lt;p&gt;An evidence ledger is a small, structured record for each email test. It does not need to be a new service. It can be a JSON object attached to the test result, with timestamps, identifiers, and safe status values. The point is to answer one question quickly: which boundary failed?&lt;/p&gt;

&lt;h2&gt;
  
  
  What an evidence ledger adds to an email test
&lt;/h2&gt;

&lt;p&gt;An ordinary test might say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected "Email verified" to be visible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is useful, but incomplete. The same assertion can fail because the form request was rejected, the email was sent to the wrong address, the inbox poll used an expired lease, or the verification code was extracted from an old message.&lt;/p&gt;

&lt;p&gt;For a verification flow, I record events such as:&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;EmailEvidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;testId&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;mailboxId&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;addressHash&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;requestedAt&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;deliveredAt&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;codeReadAt&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;verifiedAt&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;messageId&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;finalState&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="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivery-timeout&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;ui-error&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;cleanup-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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The address itself should usually stay out of shared artifacts. A hash or provider-issued mailbox ID is enough to correlate records without leaking test data. Keep the ledger boring and predictable; a stable schema is easier to query than a large provider response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate browser, delivery, and mailbox evidence
&lt;/h2&gt;

&lt;p&gt;A reliable test has at least three boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Browser boundary:&lt;/strong&gt; did the user submit the form and see the expected state?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery boundary:&lt;/strong&gt; did the application accept and send the message?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mailbox boundary:&lt;/strong&gt; did the test inbox receive the right message before its lease expired?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These boundaries should not all be represented by one five-minute timeout. A browser assertion can fail immediately when the UI shows an API error. A mailbox wait can have its own bounded timeout. The final report can then say “delivery-timeout” instead of simply “test failed.”&lt;/p&gt;

&lt;p&gt;This is also where idempotency matters. A retry may create a second message, so the adapter should identify the request or verification attempt before selecting a code. A separate guide on &lt;a href="https://dev.to/kevindev27/idempotency-keys-for-verification-email-apis-1o5i"&gt;idempotency boundaries for verification emails&lt;/a&gt; is useful background for this part of the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Playwright fixture
&lt;/h2&gt;

&lt;p&gt;I prefer to hide mailbox setup behind a fixture, while keeping the evidence object visible to the test. The test remains readable and the cleanup path stays consistent:&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="s2"&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="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;mailbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestMailbox&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailEvidence&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;mailbox&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mailbox&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;createTestMailbox&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="nf"&gt;use&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="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;dispose&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="na"&gt;evidence&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="nx"&gt;testInfo&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="na"&gt;testId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mailboxId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not-created-yet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;addressHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;not-created-yet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;finalState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ui-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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production code, the fixture would fill the IDs after creating the mailbox. The important design choice is that &lt;code&gt;dispose&lt;/code&gt; is guaranteed by &lt;code&gt;finally&lt;/code&gt;. Cleanup should not depend on the last assertion passing. Otherwise a failed run can leave messages around and make the next run look successful for the wrong reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn failures into useful artifacts
&lt;/h2&gt;

&lt;p&gt;Attach the ledger as a test artifact only after removing message bodies, tokens, and private addresses. Include a screenshot or trace for the browser side, but do not assume a trace explains delivery. The ledger should carry the cross-boundary facts:&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;await&lt;/span&gt; &lt;span class="nx"&gt;testInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email-evidence.json&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="na"&gt;body&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="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&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;A good failure report lets a QA engineer classify the issue in under a minute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ui-error&lt;/code&gt;: inspect the page response and visible error state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delivery-timeout&lt;/code&gt;: inspect the application mail queue and request ID.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mailbox-mismatch&lt;/code&gt;: inspect address ownership and message filtering.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cleanup-error&lt;/code&gt;: quarantine the mailbox and fix test isolation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For cleanup specifically, &lt;a href="https://dev.to/bitheirstake/temporary-inbox-tests-need-deletion-proof-3flb-temp-slug-2710229?preview=9152b7ee64c3479fee91f6b480017ca87ed59e4cebb850a22f8f412aab4040b7f1714580b71d82c0ef2ec06ed996a3ec2f64f420f4f2e723695f1f0a"&gt;deletion proof for temporary inbox tests&lt;/a&gt; is a good reminder that disposal is an observable outcome, not just a call in &lt;code&gt;afterEach&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid false confidence in cleanup
&lt;/h2&gt;

&lt;p&gt;There are a few easy mistakes here. Do not mark a mailbox clean merely because the delete request returned 200; verify the provider's final state when the system supports it. Do not reuse a static address across parallel workers. Do not select the newest message without checking the test's correlation value.&lt;/p&gt;

&lt;p&gt;Also watch for small naming errors in test configuration. A typo like &lt;code&gt;tamp mail com&lt;/code&gt; or &lt;code&gt;tempail mail&lt;/code&gt; in a fixture map can send a test down a fallback path that looks like a delivery problem. The spelling is not important to the product, but the failure classification is.&lt;/p&gt;

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

&lt;p&gt;Before calling an email test trustworthy, check that it can answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does every worker receive an isolated mailbox?&lt;/li&gt;
&lt;li&gt;Is the verification request tied to a correlation ID?&lt;/li&gt;
&lt;li&gt;Are UI and delivery timeouts reported separately?&lt;/li&gt;
&lt;li&gt;Are message bodies and addresses removed from artifacts?&lt;/li&gt;
&lt;li&gt;Does cleanup run after assertion failures and timeouts?&lt;/li&gt;
&lt;li&gt;Can a failed run identify the mailbox without exposing it?&lt;/li&gt;
&lt;li&gt;Is the test result useful without rerunning the job?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ledger does not make a slow provider fast, and it cannot prevent every flaky dependency. It does something more practical: it turns an ambiguous Playwright failure into a bounded diagnosis. Once the evidence is visible, the team can fix the correct boundary instead of adding another arbitrary wait.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Playwright Email Tests Need Ownership Checks</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 21 Oct 2026 11:23:07 +0000</pubDate>
      <link>https://dev.to/silviutech/playwright-email-tests-need-ownership-checks-33e6</link>
      <guid>https://dev.to/silviutech/playwright-email-tests-need-ownership-checks-33e6</guid>
      <description>&lt;p&gt;An email test can pass for the wrong reason. The subject looks right, the link opens, and the build turns green. Later, someone discovers that the test read a message left by an earlier retry or by a different worker.&lt;/p&gt;

&lt;p&gt;This is one of those failures that feels random until you ask a more precise question: &lt;strong&gt;how did the test prove that this message belongs to this run?&lt;/strong&gt; A free throwaway email inbox can be useful for end-to-end testing, but an address by itself is not ownership evidence. The assertion needs a small contract around the message.&lt;/p&gt;

&lt;p&gt;I have seen teams focus on the inbox provider first, especially when somebody searches for &lt;code&gt;tp mail so&lt;/code&gt; or a free throwaway email during tool research. The provider matters, but the test design matters more. A stable inbox with weak matching rules is still a flaky test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a matching subject is not enough
&lt;/h2&gt;

&lt;p&gt;A common helper looks roughly like this:&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;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;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForMessage&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="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="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;message&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="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="s2"&gt;/verify&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;It is easy to read and often works on a quiet developer machine. In CI, the same subject can appear more than once. A retry may leave an old message in the inbox. Parallel tests may share an address by accident. A product team may also reuse the subject for a resend flow.&lt;/p&gt;

&lt;p&gt;The result is a false positive: the test checks a real verification message, but not necessarily the message created by the action under test. The failure appears much later, when a token is expired or a user reports that the wrong account was verified.&lt;/p&gt;

&lt;p&gt;Another subtle problem is a timing assumption. A helper that selects the newest message can still choose an older message if the inbox API returns events out of order. “Newest” is a sorting rule, not proof of causality. The test needs an ownership signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ownership proof a test should require
&lt;/h2&gt;

&lt;p&gt;Before clicking the signup button, create a correlation value that is unique to the test attempt. Put it somewhere the email can carry it, such as a user-specific local part, a supported header, or a value in the request metadata. Then require more than one matching condition.&lt;/p&gt;

&lt;p&gt;For a verification flow, my minimum contract is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The message arrived after the test recorded its trigger time.&lt;/li&gt;
&lt;li&gt;The recipient identifies the current test account.&lt;/li&gt;
&lt;li&gt;The subject or message type is correct.&lt;/li&gt;
&lt;li&gt;The body contains the current correlation value.&lt;/li&gt;
&lt;li&gt;The verification URL belongs to the expected environment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not every mail system exposes all five fields. That is fine. The important part is to choose the strongest available signals and record which ones were checked. If the body cannot contain a correlation value, use a unique recipient alias or a request identifier exposed by the application.&lt;/p&gt;

&lt;p&gt;For example, an address like &lt;code&gt;qa-checkout-7f3c@example.test&lt;/code&gt; gives a parallel worker its own destination. The random suffix is not a secret; it is simply a way to make accidental cross-test reads less likely. The test still needs to check the recipient and message time.&lt;/p&gt;

&lt;p&gt;If your backend already emits verification events, it is worth keeping &lt;a href="https://dev.to/kevindev27/replay-safe-email-verification-receipts-lia"&gt;replay-safe verification receipts&lt;/a&gt; beside the browser evidence. That makes it easier to tell whether the browser missed a message or the application created the wrong one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Playwright helper with explicit correlation
&lt;/h2&gt;

&lt;p&gt;Put the matching rules in one helper so each spec does not invent its own version. The helper below keeps a small evidence record and rejects messages that only look similar.&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;MailMessage&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;to&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;text&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;links&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="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;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;triggerAt&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;acceptedId&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;rejected&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;waitForOwnedVerification&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="nx"&gt;MailClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;recipient&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;correlation&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;triggerAt&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="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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MailEvidence&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="na"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&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;inbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;triggerAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;triggerAt&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;rejected&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;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;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&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;45&lt;/span&gt;&lt;span class="nx"&gt;_000&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;receivedAt&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="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="k"&gt;if &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;lt;&lt;/span&gt; &lt;span class="nx"&gt;triggerAt&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;rejected&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="s2"&gt;`&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="s2"&gt;: before trigger`&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;recipient&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;rejected&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="s2"&gt;`&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="s2"&gt;: wrong recipient`&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;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;rejected&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="s2"&gt;`&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="s2"&gt;: wrong subject`&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;correlation&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;rejected&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="s2"&gt;`&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="s2"&gt;: correlation missing`&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;acceptedId&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;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="s2"&gt;`No owned verification message found: &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="s2"&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 example assumes &lt;code&gt;poll&lt;/code&gt; returns messages within the timeout. Your client may use a callback or locator instead. That implementation detail is less important than the order of checks and the rejection reasons.&lt;/p&gt;

&lt;p&gt;Do not log the full email body or verification token in a shared CI report. Keep the message id, recipient hash, timestamps, and rejection reason. A small, safe record is enough to explain most failures without creating a new privacy problem. For API-driven systems, &lt;a href="https://dev.to/kevindev27/postgresql-audits-for-email-change-apis-3nl"&gt;auditing email-change API events&lt;/a&gt; is a useful companion pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to triage a rejected message
&lt;/h2&gt;

&lt;p&gt;When the helper fails, read the evidence in this order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before trigger:&lt;/strong&gt; the inbox had stale mail, or the clock used by the test is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong recipient:&lt;/strong&gt; an alias was reused, or the mail client returned messages from a wider scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong subject:&lt;/strong&gt; the product sent a different template, or the test observed an unrelated notification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation missing:&lt;/strong&gt; the application dropped the request id, used the wrong template, or the test checked the wrong field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No messages:&lt;/strong&gt; investigate the application event, worker, provider, and inbox separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns “email timeout” into a useful diagnosis. It also helps reveal a dependancy on message ordering that was never part of the intended contract. In practise, the first rejected message often tells you more than the final timeout.&lt;/p&gt;

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

&lt;p&gt;Before merging an email test, check that it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates or leases an isolated recipient for the attempt&lt;/li&gt;
&lt;li&gt;records the trigger time immediately before the product action&lt;/li&gt;
&lt;li&gt;matches recipient, type, correlation, and environment where possible&lt;/li&gt;
&lt;li&gt;rejects stale messages instead of silently choosing the latest one&lt;/li&gt;
&lt;li&gt;records safe evidence for each rejected candidate&lt;/li&gt;
&lt;li&gt;behaves consistently when the test is retried&lt;/li&gt;
&lt;li&gt;cleans up the inbox or marks the lease as finished&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a teammate types &lt;code&gt;tem email&lt;/code&gt; into a bug note, treat it as a signal to inspect the evidence, not as a new test rule. The typo is harmless; an undocumented matching assumption is not.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Should I delete every old message before a test?
&lt;/h3&gt;

&lt;p&gt;Not necessarily. Deleting old messages can hide useful evidence and introduce another race. It is usually safer to keep the inbox, record a trigger boundary, and reject anything that cannot prove ownership.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a unique email address enough?
&lt;/h3&gt;

&lt;p&gt;It is a strong start, especially for parallel workers, but it does not prove that the message belongs to the current action. A worker can still read a delayed message from an earlier attempt. Add a timestamp and correlation check when the system supports it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should retries use the same correlation value?
&lt;/h3&gt;

&lt;p&gt;Usually no. A retry should have its own attempt id, while the test report can connect both attempts to the same scenario id. This keeps the evidence honest and makes it clear whether the first attempt produced a late message.&lt;/p&gt;

&lt;p&gt;Ownership checks add a little code, but they remove a lot of guesswork. Once every email assertion explains why its message belongs to the current run, Playwright failures become much more actionable.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>playwright</category>
      <category>qa</category>
      <category>automation</category>
    </item>
    <item>
      <title>Agentes LLM: snapshots para reanudar email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 21 Oct 2026 08:23:05 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-snapshots-para-reanudar-email-3p7b</link>
      <guid>https://dev.to/silviutech/agentes-llm-snapshots-para-reanudar-email-3p7b</guid>
      <description>&lt;p&gt;Un agente LLM puede completar una verificación de email en una sola conversación. El problema aparece cuando la ejecución dura más que el contexto disponible, una herramienta tarda demasiado o el proceso se reinicia. Si el agente solo conserva el último mensaje, no sabe si debe volver a consultar el inbox, pedir otro código o cerrar la ejecución.&lt;/p&gt;

&lt;p&gt;La solución no es guardar toda la conversación. Es crear un &lt;strong&gt;snapshot operativo&lt;/strong&gt;: una representación pequeña, explícita y durable del estado que importa para reanudar. Este patrón hace que una automatización sea mas predecible y reduce los reintentos que producen evidencia duplicada.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: el contexto del agente no es un almacenamiento
&lt;/h2&gt;

&lt;p&gt;El contexto de un modelo sirve para razonar, pero no es una base de datos. Puede resumirse, truncarse o cambiar cuando interviene otra herramienta. Además, la narración del agente suele mezclar intención y resultado: “voy a consultar el inbox” no significa que la consulta haya terminado.&lt;/p&gt;

&lt;p&gt;En un flujo típico hay varios puntos de riesgo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Se reserva una cuenta de correo desechable para una ejecución.&lt;/li&gt;
&lt;li&gt;La aplicación solicita un mensaje de verificación.&lt;/li&gt;
&lt;li&gt;Una herramienta consulta el buzón y devuelve un resultado parcial.&lt;/li&gt;
&lt;li&gt;El agente se reinicia antes de confirmar el código.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tras el reinicio, una política ingenua vuelve al paso dos. Puede enviar otro mensaje, consumir otro código o asociar la respuesta a la ejecución equivocada. Una &lt;a href="https://dev.to/alexcarteruk/kubernetes-senales-utiles-para-cronjobs-fragiles-59n"&gt;señal útil para un CronJob frágil&lt;/a&gt; no es suficiente si el propio workflow no conserva su estado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué debe contener un snapshot de email
&lt;/h2&gt;

&lt;p&gt;Un snapshot debería responder seis preguntas sin leer el historial completo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Qué &lt;code&gt;run_id&lt;/code&gt; está activo?&lt;/li&gt;
&lt;li&gt;¿Qué buzón y destinatario pertenecen a esa ejecución?&lt;/li&gt;
&lt;li&gt;¿Qué acción fue confirmada por última vez?&lt;/li&gt;
&lt;li&gt;¿Cuál es la próxima acción permitida?&lt;/li&gt;
&lt;li&gt;¿Cuántos intentos quedan y cuándo expira la reserva?&lt;/li&gt;
&lt;li&gt;¿Qué evidencia ya fue aceptada?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Una forma compacta podría ser:&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;"run_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;"checkout-1842"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mailbox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"leased"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verification"&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;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"poll_inbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lease_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-10-21T16: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;"accepted_message_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&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 snapshot no debe guardar el cuerpo completo del email si no hace falta. Basta con un identificador, un hash o los metadatos necesarios para volver a obtener la evidencia. Para un caso de prueba, una &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;dirección desechable&lt;/a&gt; puede ser parte del aislamiento, pero la decisión de reanudar debe vivir en el contrato de la ejecución.&lt;/p&gt;

&lt;p&gt;También conviene conservar los nombres defectuosos que llegan desde integraciones, por ejemplo &lt;code&gt;temp mailid&lt;/code&gt;. Sirven para investigar el origen de un fallo, aunque no deben convertirse en una regla para aceptar un mensaje.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar la reanudación como una máquina de estados
&lt;/h2&gt;

&lt;p&gt;El snapshot es más útil cuando representa una máquina de estados pequeña. El flujo puede expresarse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LEASED -&amp;gt; REQUESTED -&amp;gt; WAITING -&amp;gt; RECEIVED -&amp;gt; VERIFIED
                      \-&amp;gt; EXPIRED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada transición debe tener una condición observable. &lt;code&gt;REQUESTED&lt;/code&gt; significa que la solicitud fue aceptada por la aplicación, no que el email ya existe. &lt;code&gt;RECEIVED&lt;/code&gt; significa que hay un mensaje que coincide con &lt;code&gt;run_id&lt;/code&gt;, destinatario y tipo de evento. &lt;code&gt;VERIFIED&lt;/code&gt; solo ocurre después de validar el código.&lt;/p&gt;

&lt;p&gt;Al reanudar, el agente no interpreta una historia ambigua. Lee &lt;code&gt;next_action&lt;/code&gt; y ejecuta una operación acotada. Si el estado es &lt;code&gt;WAITING&lt;/code&gt;, puede hacer una consulta con el presupuesto restante. Si es &lt;code&gt;RECEIVED&lt;/code&gt;, debe validar la evidencia antes de consultar de nuevo. Si es &lt;code&gt;EXPIRED&lt;/code&gt;, tiene que cerrar la ejecución o reservar otro buzón, nunca reutilizar el anterior por comodidad.&lt;/p&gt;

&lt;p&gt;Esta separación también ayuda a &lt;a href="https://dev.to/alexcarteruk/sre-aislar-fallos-de-email-en-kubernetes-3470"&gt;aislar fallos de email en Kubernetes&lt;/a&gt;: el orquestador puede reiniciar un worker sin convertir el reinicio en una nueva intención de negocio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación mínima con eventos idempotentes
&lt;/h2&gt;

&lt;p&gt;Guardar el snapshot no alcanza. Cada cambio debe registrarse como un evento con versión:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;append(run_id, version=7, type="message_received", message_id="m-93")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El servidor rechaza una escritura si la versión esperada ya no coincide. Así, dos workers que despiertan casi al mismo tiempo no pueden confirmar mensajes distintos para el mismo paso. La operación de confirmación debe ser idempotente: repetir la misma solicitud devuelve el mismo resultado, no consume evidencia dos veces.&lt;/p&gt;

&lt;p&gt;En la práctica, separaría tres responsabilidades:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Planificador&lt;/strong&gt;: decide la próxima acción y sus límites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ejecutor&lt;/strong&gt;: llama a la herramienta de email y devuelve hechos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Almacenamiento&lt;/strong&gt;: persiste snapshots y eventos con control de versión.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El modelo puede escoger entre acciones permitidas, pero no debería inventar una transición. Un contrato de tool claro reduce la tentación de hacer otro &lt;code&gt;poll&lt;/code&gt; cuando ya no quedan intentos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs y métricas
&lt;/h2&gt;

&lt;p&gt;Snapshots pequeños son fáciles de inspeccionar, pero exigen diseñar el esquema. Guardar el historial completo facilita auditorías, aunque aumenta el riesgo de conservar datos sensibles y hace más lenta la reanudación. En la mayoría de flujos de testing, un snapshot más un ledger de eventos es un equilibrio razonable.&lt;/p&gt;

&lt;p&gt;Mediría como mínimo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;porcentaje de ejecuciones reanudadas con éxito;&lt;/li&gt;
&lt;li&gt;consultas de inbox por ejecución;&lt;/li&gt;
&lt;li&gt;transiciones rechazadas por versión obsoleta;&lt;/li&gt;
&lt;li&gt;tiempo desde &lt;code&gt;REQUESTED&lt;/code&gt; hasta &lt;code&gt;RECEIVED&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;mensajes descartados por pertenecer a otro &lt;code&gt;run_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si las consultas repetidas suben después de cada reinicio, el snapshot no está expresando una próxima acción suficiente. Si aumentan las transiciones obsoletas, falta coordinación entre workers o el lease es demasiado largo.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  ¿Debo guardar el prompt completo en el snapshot?
&lt;/h3&gt;

&lt;p&gt;No por defecto. Guarda la versión del prompt, los parámetros que cambian el comportamiento y las decisiones confirmadas. El prompt completo puede contener datos que no necesitas para reanudar.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué pasa si el mensaje llega mientras el agente está reiniciando?
&lt;/h3&gt;

&lt;p&gt;El mensaje debe quedar disponible como evento pendiente. En la siguiente ejecución, una consulta por &lt;code&gt;run_id&lt;/code&gt; y &lt;code&gt;message_id&lt;/code&gt; puede mover el estado a &lt;code&gt;RECEIVED&lt;/code&gt; sin enviar otro email.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Un snapshot reemplaza la observabilidad?
&lt;/h3&gt;

&lt;p&gt;No. Es la entrada mínima para continuar. Los logs y métricas explican por qué se produjo cada transición y permiten detectar una política de reintentos mal ajustada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para llevarlo a producción
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;¿Cada ejecución tiene un &lt;code&gt;run_id&lt;/code&gt; y un lease con expiración?&lt;/li&gt;
&lt;li&gt;¿El snapshot declara una sola próxima acción permitida?&lt;/li&gt;
&lt;li&gt;¿Las transiciones tienen versión y son idempotentes?&lt;/li&gt;
&lt;li&gt;¿Se valida la propiedad del mensaje antes de aceptar evidencia?&lt;/li&gt;
&lt;li&gt;¿El reinicio puede continuar sin reenviar una solicitud?&lt;/li&gt;
&lt;li&gt;¿Se evita guardar contenido de email que no es necesario?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un agente no necesita recordar toda la conversación para comportarse bien. Necesita un estado pequeño que pueda verificar, actualizar y reanudar. Ese contrato convierte la automatización de email en un sistema recuperable: el modelo razona sobre el siguiente paso, mientras la infraestructura conserva lo que no se puede perder.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Agentes LLM: un cortocircuito para email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:22:56 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-un-cortocircuito-para-email-5pc</link>
      <guid>https://dev.to/silviutech/agentes-llm-un-cortocircuito-para-email-5pc</guid>
      <description>&lt;p&gt;Cuando un agente LLM puede crear una cuenta, solicitar un código y leer un mensaje, una falla de email no debería provocar diez reintentos iguales. En sistemas de automatización, insistir no es lo mismo que recuperarse.&lt;/p&gt;

&lt;p&gt;La idea de este diseño es sencilla: envolver la herramienta de email en un &lt;strong&gt;cortocircuito&lt;/strong&gt;. Si el proveedor está lento, el inbox no pertenece a la ejecución o el mismo mensaje ya fue procesado, la herramienta debe pasar a un estado limitado y explicarle al agente qué hacer despues.&lt;/p&gt;

&lt;p&gt;Esto es especialmente útil cuando se trabaja con un &lt;strong&gt;use and throw email&lt;/strong&gt; para pruebas. El buzón puede ser temporal, pero las decisiones del agente deben ser trazables.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un agente puede insistir demasiado
&lt;/h2&gt;

&lt;p&gt;Un flujo típico se ve así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El agente crea un usuario de prueba.&lt;/li&gt;
&lt;li&gt;Solicita un email de confirmación.&lt;/li&gt;
&lt;li&gt;Consulta el inbox cada pocos segundos.&lt;/li&gt;
&lt;li&gt;Si no encuentra nada, repite la consulta o vuelve a enviar el mensaje.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El cuarto paso parece razonable hasta que se combinan varias ejecuciones. Una consulta lenta se confunde con un mensaje perdido; un reenvío crea dos códigos; y un agente con contexto incompleto puede seguir preguntando indefinidamente. El coste aparece en tiempo, llamadas y datos de prueba que quedan vivos mas de lo necesario.&lt;/p&gt;

&lt;p&gt;Este límite también ayuda a &lt;a href="https://dev.to/hannahdev56/saas-como-probar-onboarding-sin-emails-falsos-23gf"&gt;probar el onboarding sin emails falsos&lt;/a&gt;: el flujo de producto y el contrato de la evidencia dejan de depender de una única espera optimista.&lt;/p&gt;

&lt;h2&gt;
  
  
  El cortocircuito como contrato de herramienta
&lt;/h2&gt;

&lt;p&gt;Un circuito no debe ser solo un contador global. Tiene que estar ligado a una ejecución, a un buzón y a una operación concreta. Una representación útil es:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLOSED -&amp;gt; OPEN -&amp;gt; HALF_OPEN -&amp;gt; CLOSED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CLOSED&lt;/strong&gt;: se permiten lecturas, pero con un presupuesto de intentos y tiempo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OPEN&lt;/strong&gt;: se bloquean nuevas consultas durante una ventana de enfriamiento.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HALF_OPEN&lt;/strong&gt;: se permite una prueba controlada para comprobar si el servicio responde.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El agente no necesita conocer toda la implementación. La herramienta puede devolver &lt;code&gt;retryable&lt;/code&gt;, &lt;code&gt;blocked&lt;/code&gt; o &lt;code&gt;completed&lt;/code&gt;, junto con &lt;code&gt;next_action&lt;/code&gt;. Así el modelo recibe una decisión pequeña y verificable, en vez de interpretar un error ambiguo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar los estados y las señales
&lt;/h2&gt;

&lt;p&gt;Conviene separar tres tipos de fallo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Proveedor no disponible&lt;/strong&gt;: el inbox no responde o supera el timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidencia no encontrada&lt;/strong&gt;: el proveedor responde, pero no hay un mensaje que corresponda al &lt;code&gt;run_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidencia inválida&lt;/strong&gt;: hay un mensaje, pero el código ya fue consumido, expiró o pertenece a otra ejecución.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Los tres casos pueden parecer “no hay email”, pero requieren acciones distintas. El primero permite una espera corta. El segundo debe volver a revisar el estado del flujo. El tercero normalmente debe cerrar la ejecución y pedir un nuevo buzón.&lt;/p&gt;

&lt;p&gt;Para evitar carreras, la consulta debe incluir una marca de propiedad: &lt;code&gt;run_id&lt;/code&gt;, destinatario, tipo de evento y un límite de edad. Si el mensaje no coincide, no se convierte en evidencia solo porque sea el más reciente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación mínima
&lt;/h2&gt;

&lt;p&gt;Un contrato pequeño para una herramienta de agentes puede ser suficiente:&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;"retryable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attempt"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"remaining_attempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lease_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-10-21T02:30: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;"next_action"&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_then_poll"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&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_timeout"&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 servidor conserva el contador y el vencimiento. No conviene pedirle al agente que recuerde cuantos intentos lleva: el contexto puede resumirse, cambiar de modelo o perder una llamada anterior.&lt;/p&gt;

&lt;p&gt;Para cada &lt;code&gt;run_id&lt;/code&gt;, guardaría como mínimo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;momento de reserva del buzón;&lt;/li&gt;
&lt;li&gt;última consulta y su latencia;&lt;/li&gt;
&lt;li&gt;identificador del mensaje aceptado;&lt;/li&gt;
&lt;li&gt;razón del último cambio de estado;&lt;/li&gt;
&lt;li&gt;fecha de liberación de la evidencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si una integración usa nombres de prueba poco consistentes, pueden aparecer cadenas como &lt;strong&gt;tem email&lt;/strong&gt; o &lt;strong&gt;tempail&lt;/strong&gt; en los logs de entrada. Se deben conservar para diagnóstico, pero no usarlas como señal de éxito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué medir y cuándo reabrir el circuito
&lt;/h2&gt;

&lt;p&gt;Las métricas importantes no son solo el porcentaje de flujos exitosos. También mediría consultas por ejecución, tiempo hasta la primera evidencia, porcentaje de mensajes descartados y cantidad de circuitos abiertos por causa.&lt;/p&gt;

&lt;p&gt;El estado &lt;code&gt;HALF_OPEN&lt;/code&gt; debería probar una operación barata y segura. Si esa prueba responde, se cierra el circuito y se permite continuar. Si vuelve a fallar, se extiende el enfriamiento. No conviene reabrirlo por cada respuesta del agente, por que eso elimina el límite que acabamos de diseñar.&lt;/p&gt;

&lt;p&gt;Para un servicio como FastAPI, &lt;a href="https://dev.to/silviutech/fastapi-valida-emails-por-entorno-preview-40nd"&gt;validar emails por entorno preview&lt;/a&gt; puede ser el siguiente paso: separar la política de preview, CI y producción evita que los límites de un entorno se copien sin pensar.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;¿Cada buzón tiene un &lt;code&gt;run_id&lt;/code&gt; y una expiración?&lt;/li&gt;
&lt;li&gt;¿El presupuesto de consultas vive en el servidor?&lt;/li&gt;
&lt;li&gt;¿Se diferencian timeout, ausencia y evidencia inválida?&lt;/li&gt;
&lt;li&gt;¿La herramienta devuelve una acción siguiente explícita?&lt;/li&gt;
&lt;li&gt;¿El agente sabe cuándo dejar de insistir?&lt;/li&gt;
&lt;li&gt;¿Los logs permiten reconstruir la decisión sin guardar contenido innecesario?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un cortocircuito bien definido no hace que el email sea más rápido. Hace que la automatización falle de forma limitada, explicable y recuperable. Para agentes LLM, esa diferencia suele valer mas que otro reintento automático.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Agentes LLM: una cola de señales de email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Tue, 20 Oct 2026 02:23:11 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-una-cola-de-senales-de-email-4hm4</link>
      <guid>https://dev.to/silviutech/agentes-llm-una-cola-de-senales-de-email-4hm4</guid>
      <description>&lt;p&gt;Un agente LLM que crea una cuenta de prueba y espera un email parece sencillo. En la práctica, la espera es una frontera entre dos sistemas: el agente decide qué hacer y el proveedor de correo decide cuándo aparece una señal.&lt;/p&gt;

&lt;p&gt;Cuando esa frontera no está diseñada, el agente suele repetir la misma consulta, gastar llamadas de herramienta y terminar con un diagnóstico poco útil: “no llegó el email”. La solución que prefiero es tratar cada mensaje esperado como una señal con contrato, caducidad y evidencia.&lt;/p&gt;

&lt;p&gt;La idea no es construir otro framework de agentes. Es añadir una pequeña cola de señales entre el workflow y la bandeja de prueba. Esa cola hace visible el estado y limita el coste de la incertidumbre.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un agente que espera sin saber qué espera
&lt;/h2&gt;

&lt;p&gt;Un flujo ingenuo se parece a esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crear_usuario()
repetir:
    revisar_bandeja()
    si hay_email: continuar()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El pseudocódigo es corto, pero mezcla varias preguntas distintas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Qué usuario y qué mensaje estamos buscando?&lt;/li&gt;
&lt;li&gt;¿Cuánto tiempo puede esperar el agente?&lt;/li&gt;
&lt;li&gt;¿Cómo distingue un email nuevo de uno de una ejecución anterior?&lt;/li&gt;
&lt;li&gt;¿Qué hace si la bandeja responde con un error temporal?&lt;/li&gt;
&lt;li&gt;¿Qué evidencia se conserva al terminar?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sin respuestas explícitas, cada reintento parece razonable por separado y el conjunto se vuelve caro. Tambien resulta dificil saber si falló el proveedor, el filtro de búsqueda o la propia instrucción del agente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseñar la cola de señales
&lt;/h2&gt;

&lt;p&gt;Una señal útil tiene un identificador y una condición de éxito concreta. Para un email de verificación, usaría un registro parecido a este:&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;"signal_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;"sig_01J9MAIL42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_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;"run_20261020_01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verification_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-user-42@example.invalid"&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_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;"Confirma tu cuenta"&lt;/span&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;"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;"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-10-20T02:35: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;El agente no necesita leer toda la bandeja. Pide una operación estrecha: buscar la señal &lt;code&gt;sig_01J9MAIL42&lt;/code&gt;, con un asunto esperado y una ventana temporal. El adaptador de correo devuelve uno de cuatro estados: &lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;found&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; o &lt;code&gt;provider_error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Esta separación es importante. &lt;code&gt;waiting&lt;/code&gt; significa que todavía no hay evidencia; &lt;code&gt;provider_error&lt;/code&gt; significa que la consulta no fue confiable. Si ambos se convierten en “no encontrado”, el agente aprende a insistir justo cuando debería cambiar de estrategia.&lt;/p&gt;

&lt;p&gt;Para una prueba que usa un buzón aislado, una opción como &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;tempmailso&lt;/a&gt; puede encajar como adaptador externo, siempre que el equipo revise sus condiciones, privacidad y límites antes de incluirla en un pipeline. El proveedor es reemplazable; el contrato de señales debería permanecer estable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación con estados e idempotencia
&lt;/h2&gt;

&lt;p&gt;El worker que consulta el correo debe ser idempotente. Si el agente repite la llamada por un timeout, no debería crear otra señal ni consumir la primera evidencia como si fuera un mensaje distinto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signal = queue.get(signal_id)

si signal.status en [found, expired]:
    devolver signal.status

resultado = mailbox.search(
    recipient=signal.recipient,
    subject=signal.expected_subject,
    after=signal.created_at
)

si resultado.matches:
    queue.mark_found(signal_id, resultado.message_id)
si resultado.provider_error:
    queue.record_error(signal_id, resultado.code)
si no:
    queue.record_check(signal_id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El campo &lt;code&gt;message_id&lt;/code&gt; evita contar dos veces el mismo email. También guardaría la hora de cada consulta, la cantidad de coincidencias y un código de error resumido, pero no el cuerpo completo del mensaje. Esa evidencia basta para depurar sin convertir los logs en un almacén de datos personales.&lt;/p&gt;

&lt;p&gt;En búsquedas y conversaciones técnicas aparecen variantes como &lt;code&gt;tem email&lt;/code&gt; y &lt;code&gt;temp org mail&lt;/code&gt;. Pueden servir para analizar consultas reales, pero no deberían entrar en nombres de estados ni en reglas de coincidencia: son señales de texto, no contratos del sistema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Presupuesto de herramientas y reintentos
&lt;/h2&gt;

&lt;p&gt;Un agente necesita un presupuesto visible. Por ejemplo, puede tener diez consultas de bandeja o dos minutos, lo que ocurra primero. Cada consulta debe indicar el motivo: primera comprobación, espera después de un webhook o recuperación de un error temporal.&lt;/p&gt;

&lt;p&gt;No usaría intervalos fijos demasiado agresivos. Un backoff pequeño con un límite superior reduce presión sobre el proveedor. Si el flujo tiene una señal de webhook, el agente puede dormir hasta que ocurra un evento y usar la consulta solo como recuperación. En ese diseño, los &lt;a href="https://dev.to/silviutech/llms-contratos-cortos-para-tool-use-m3e"&gt;contratos cortos para usar herramientas&lt;/a&gt; ayudan a que cada llamada tenga entradas y salidas verificables.&lt;/p&gt;

&lt;p&gt;La cola también permite separar errores recuperables de errores finales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;waiting&lt;/code&gt;: no hay coincidencia todavía; esperar según el presupuesto.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider_error&lt;/code&gt;: reintentar con backoff y registrar el código.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expired&lt;/code&gt;: detener el workflow y crear un diagnóstico.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;found&lt;/code&gt;: pasar un identificador de evidencia al siguiente paso.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El agente no deberia inventar que el mensaje llegó solo porque una llamada anterior creó al usuario. Creación de cuenta y llegada del email son eventos distintos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo evaluar el workflow
&lt;/h2&gt;

&lt;p&gt;Probaría el diseño con una tabla de escenarios, no solo con el caso feliz. Cada fila debe indicar el estado esperado, el número máximo de llamadas y la evidencia que queda:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Escenario&lt;/th&gt;
&lt;th&gt;Resultado esperado&lt;/th&gt;
&lt;th&gt;Evidencia&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Email llega en la primera consulta&lt;/td&gt;
&lt;td&gt;&lt;code&gt;found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;message_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email llega después de dos esperas&lt;/td&gt;
&lt;td&gt;&lt;code&gt;found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;historial de checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No llega antes del límite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expired&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expires_at&lt;/code&gt; y última consulta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proveedor responde 503&lt;/td&gt;
&lt;td&gt;&lt;code&gt;provider_error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;código y reintentos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email de otra ejecución&lt;/td&gt;
&lt;td&gt;&lt;code&gt;waiting&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;filtro por &lt;code&gt;run_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La evaluación debe comprobar también que el agente explica el siguiente paso. Un resultado &lt;code&gt;expired&lt;/code&gt; debería producir una recomendación accionable, no otro “revisa la bandeja”. Para escenarios Kubernetes, conviene combinar esta matriz con &lt;a href="https://dev.to/alexcarteruk/kubernetes-pruebas-sinteticas-sin-ruido-en-alertas-196l"&gt;pruebas sintéticas sin ruido en las alertas&lt;/a&gt;, de modo que una demora externa no se convierta automáticamente en una alerta operativa.&lt;/p&gt;

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

&lt;p&gt;Antes de conectar el adaptador de correo, revisaría estos puntos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada señal tiene &lt;code&gt;signal_id&lt;/code&gt;, &lt;code&gt;run_id&lt;/code&gt;, caducidad y condición de éxito.&lt;/li&gt;
&lt;li&gt;La consulta es idempotente y filtra por ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;waiting&lt;/code&gt;, &lt;code&gt;provider_error&lt;/code&gt; y &lt;code&gt;expired&lt;/code&gt; no se confunden.&lt;/li&gt;
&lt;li&gt;Existe un presupuesto de tiempo y llamadas de herramienta.&lt;/li&gt;
&lt;li&gt;Los logs guardan evidencia mínima, sin contenido privado innecesario.&lt;/li&gt;
&lt;li&gt;El workflow puede reanudar una señal sin crear otra.&lt;/li&gt;
&lt;li&gt;Los tests cubren llegada tardía, duplicados y errores del proveedor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El valor de este diseño está en hacer pequeño el contrato, no en añadir más autonomía. Un agente que sabe qué señal espera, cuánto puede esperar y qué evidencia debe devolver es más fácil de evaluar y bastante más barato de operar. La bandeja cambia; el contrato permanece.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Agentes LLM: un contrato de evidencia para email</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Mon, 19 Oct 2026 08:23:07 +0000</pubDate>
      <link>https://dev.to/silviutech/agentes-llm-un-contrato-de-evidencia-para-email-lma</link>
      <guid>https://dev.to/silviutech/agentes-llm-un-contrato-de-evidencia-para-email-lma</guid>
      <description>&lt;h1&gt;
  
  
  Agentes LLM: un contrato de evidencia para email
&lt;/h1&gt;

&lt;p&gt;Un agente LLM puede completar una tarea de email y aun así dejar un sistema dificil de revisar. Dice que abrió un buzón, encontró un mensaje y siguió un enlace, pero el registro solo contiene “tarea completada”. Cuando el flujo falla en CI, el equipo tiene que adivinar qué vio el agente, qué herramienta llamó y si el mensaje era nuevo.&lt;/p&gt;

&lt;p&gt;La solución no es guardar toda la conversación. Es definir un contrato de evidencia pequeño: cada paso debe producir una señal verificable, con un estado, un identificador y un límite de retención. Esto hace que una automatización sea más fácil de depurar sin convertir el buzón temporal en un archivo de datos.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: un agente puede acertar sin dejar pruebas
&lt;/h2&gt;

&lt;p&gt;Un flujo típico de automatización de email se parece a esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crear identidad -&amp;gt; reservar buzón -&amp;gt; esperar mensaje -&amp;gt; extraer enlace -&amp;gt; validar resultado
       |                |                |                 |                |
     estado          lease_id       message_id        evidence_id       outcome
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada flecha es una frontera. El agente puede usar una herramienta para reservar la dirección, otra para leer mensajes y otra para comprobar el resultado en la aplicación. Si solo se conserva la respuesta final del modelo, desaparece la diferencia entre “no llegó el mensaje”, “llegó tarde” y “se leyó un mensaje viejo”.&lt;/p&gt;

&lt;p&gt;También hay un riesgo de diseño: dejar que el agente reintente sin entender el estado. Un reintento puede crear otra cuenta, consumir un segundo buzón o repetir una acción que ya era efectiva. Para APIs, ayuda pensar en &lt;a href="https://dev.to/silviutech/fastapi-pruebas-de-api-que-sobreviven-al-reintento-95b"&gt;pruebas de API que sobreviven al reintento&lt;/a&gt;, pero un agente necesita además mostrar qué evidencia usó para decidir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diseña el contrato de evidencia
&lt;/h2&gt;

&lt;p&gt;El contrato debe ser legible por una persona y validable por código. Para cada llamada de herramienta, registra como mínimo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;step&lt;/code&gt;: nombre estable del paso, por ejemplo &lt;code&gt;read_verification_email&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attempt&lt;/code&gt;: número de intento dentro de la ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;state&lt;/code&gt;: &lt;code&gt;started&lt;/code&gt;, &lt;code&gt;observed&lt;/code&gt;, &lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;expired&lt;/code&gt; o &lt;code&gt;failed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;correlation_id&lt;/code&gt;: identificador común de la ejecución, nunca un token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evidence_id&lt;/code&gt;: referencia al dato observado, como un ID de mensaje con el cuerpo redacted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expires_at&lt;/code&gt;: momento en que la evidencia deja de ser válida.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La palabra “observed” es importante. Significa que el agente vio algo, no que ese algo demuestre el objetivo. Solo &lt;code&gt;accepted&lt;/code&gt; debería permitir avanzar a un paso que cambie estado en la aplicación.&lt;/p&gt;

&lt;p&gt;Por ejemplo, ver un email con el asunto correcto no prueba que pertenezca a la ejecución actual. El agente necesita comparar el &lt;code&gt;correlation_id&lt;/code&gt;, una marca temporal y, cuando sea posible, un nonce de prueba. Si falta una señal, el resultado correcto es “evidencia insuficiente”, no otra llamada ciega.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reintentos, estados y límites del buzón
&lt;/h2&gt;

&lt;p&gt;Los modelos LLM son buenos proponiendo el siguiente paso, pero no deberían ser la fuente de verdad del estado. Guarda la máquina de estados fuera del prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reserved -&amp;gt; waiting -&amp;gt; observed -&amp;gt; accepted -&amp;gt; consumed
     \         |          |
      -&amp;gt; expired       -&amp;gt; rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un reintento desde &lt;code&gt;waiting&lt;/code&gt; puede volver a leer el buzón. Un reintento desde &lt;code&gt;accepted&lt;/code&gt; debería reutilizar la evidencia existente o detenerse. Un reintento desde &lt;code&gt;consumed&lt;/code&gt; casi siempre es un error de diseño. Esta distinción reduce coste y evita que una automatización cree actividad duplicada.&lt;/p&gt;

&lt;p&gt;El buzón debe tener un lease, no una vida indefinida. Define cuánto puede esperar, cuántos mensajes puede leer y qué ocurre al expirar. Si el agente encuentra un mensaje después de &lt;code&gt;expires_at&lt;/code&gt;, puede registrarlo como señal tardía, pero no debe aceptarlo automáticamente. Es un detalle pequeño que se olvida facil.&lt;/p&gt;

&lt;p&gt;Los términos de búsqueda también pueden ser ruidosos. “tempmailso” puede aparecer como contexto de un flujo, y una consulta mal escrita como “temp mailid” no debe convertirse en una instrucción de seguridad. Las palabras ayudan a encontrar documentación; el contrato de evidencia decide si una acción es válida.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un manifiesto mínimo de ejecución
&lt;/h2&gt;

&lt;p&gt;Un manifiesto compacto puede vivir junto al resultado de cada run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;correlation_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run_2026_10_19_0822&lt;/span&gt;
  &lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email-verifier&lt;/span&gt;
  &lt;span class="na"&gt;max_attempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;mailbox&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lease_minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;max_messages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;message_id&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;received_at&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;correlation_id&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;retain_minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accept_only_from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;observed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;on_expired&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stop_with_diagnostic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El manifiesto no necesita conocer el razonamiento privado del modelo. Expone las restricciones que sí deben ser auditables: qué se puede leer, cuánto tiempo, cuántos intentos y qué transición está permitida. Si el agente propone saltarse una condición, el ejecutor la rechaza.&lt;/p&gt;

&lt;p&gt;Para la interfaz de un panel de pruebas, los estados también deben ser visibles. Un mensaje de error que desaparece cuando cambia el layout complica la revisión; el patrón de &lt;a href="https://dev.to/silviutech/react-errores-inline-sin-mover-el-formulario-b84"&gt;errores inline sin mover el formulario&lt;/a&gt; es aplicable aquí: conserva la señal cerca de la acción y no escondas el motivo del fallo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Puntos de control antes de publicar
&lt;/h2&gt;

&lt;p&gt;Antes de poner el flujo en CI, comprueba:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cada herramienta devuelve un estado estructurado, no solo texto libre.&lt;/li&gt;
&lt;li&gt;Un mensaje antiguo no puede satisfacer una espera nueva.&lt;/li&gt;
&lt;li&gt;Los reintentos son seguros para cada estado, o se detienen.&lt;/li&gt;
&lt;li&gt;Los tokens, enlaces completos y cuerpos de email no aparecen en logs.&lt;/li&gt;
&lt;li&gt;La evidencia tiene una expiración y un ID correlacionable.&lt;/li&gt;
&lt;li&gt;El resultado final explica por qué fue aceptado o rechazado.&lt;/li&gt;
&lt;li&gt;El cleanup se ejecuta también después de un fallo parcial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El objetivo no es hacer que el agente sea menos flexible. Es separar la flexibilidad del modelo de las garantías del sistema. Los modelos LLM pueden elegir herramientas y proponer recuperaciones; la máquina de estados, el lease del buzón y el contrato de evidencia ponen los límites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preguntas frecuentes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Debo guardar la respuesta completa del agente?
&lt;/h3&gt;

&lt;p&gt;No como requisito del flujo. Guarda eventos estructurados y referencias redacted. La conversación puede conservarse con una política aparte para depuración, pero no debe ser necesaria para demostrar que un email fue validado.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Qué hago cuando no hay evidencia suficiente?
&lt;/h3&gt;

&lt;p&gt;Marca el paso como &lt;code&gt;rejected&lt;/code&gt; o &lt;code&gt;expired&lt;/code&gt;, incluye el &lt;code&gt;correlation_id&lt;/code&gt; y termina con un diagnóstico accionable. Reintentar es razonable solo si la causa y el límite de intentos están definidos.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Por qué no dejar que el agente decida todo?
&lt;/h3&gt;

&lt;p&gt;Porque el agente optimiza una respuesta probable, mientras que el sistema necesita invariantes. Un contrato corto permite aprovechar su capacidad de adaptación sin confiarle la autoridad sobre credenciales, retención o cambios irreversibles.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>automation</category>
      <category>devtools</category>
    </item>
    <item>
      <title>React: un inbox de prueba que explica cada espera</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:22:51 +0000</pubDate>
      <link>https://dev.to/silviutech/react-un-inbox-de-prueba-que-explica-cada-espera-69h</link>
      <guid>https://dev.to/silviutech/react-un-inbox-de-prueba-que-explica-cada-espera-69h</guid>
      <description>&lt;p&gt;Un inbox de prueba en una app React no debería limitarse a mostrar un spinner. Cuando esperamos un correo de verificación, la interfaz necesita explicar que está pasando, que puede hacer la persona y cuando conviene volver a intentar. Este enfoque reduce la incertidumbre y hace que un flujo de pruebas con correo temporal sea mucho más fácil de depurar.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema: una espera que parece un fallo
&lt;/h2&gt;

&lt;p&gt;En mis flujos de frontend, el fallo más costoso no siempre está en la API. A veces el endpoint responde bien, pero la pantalla deja al usuario ante un botón desactivado y un indicador que gira sin contexto. Después de unos segundos, la pregunta es inevitable: ¿el email no llegó, la bandeja está vacía o la aplicación se rompió?&lt;/p&gt;

&lt;p&gt;Un inbox de prueba tiene varios momentos distintos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;todavía no se ha solicitado el mensaje;&lt;/li&gt;
&lt;li&gt;la solicitud está en curso;&lt;/li&gt;
&lt;li&gt;la bandeja está esperando un mensaje nuevo;&lt;/li&gt;
&lt;li&gt;el mensaje llegó y puede abrirse;&lt;/li&gt;
&lt;li&gt;la espera expiró o devolvió un error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Me gusta tratar cada momento como un estado de producto, no como un detalle interno de la petición. Así se puede probar un &lt;code&gt;fake email generator&lt;/code&gt;, un buzón local o una integración real sin cambiar la expectativa de la interfaz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modelar el inbox como estados visibles
&lt;/h2&gt;

&lt;p&gt;Un modelo pequeño evita que los componentes acumulen condiciones contradictorias. Por ejemplo, &lt;code&gt;waiting&lt;/code&gt; no significa lo mismo que &lt;code&gt;empty&lt;/code&gt;: en el primero hay una consulta activa; en el segundo, la consulta terminó y no encontró nada.&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;InboxState&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;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;startedAt&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="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;empty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;checkedAt&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="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;ready&lt;/span&gt;&lt;span class="dl"&gt;"&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;number&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;Con esta separación, el texto de la pantalla puede ser preciso. “Buscando el correo” comunica una acción en progreso; “Aún no hay mensajes” comunica un resultado. Parece una diferencia pequeña, pero ayuda a que el equipo de QA y quien desarrolla hablen del mismo escenario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un componente React con feedback útil
&lt;/h2&gt;

&lt;p&gt;El componente no necesita conocer todos los detalles del proveedor de correo. Solo recibe el estado y expone una acción que se pueda repetir de forma segura.&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;TestInbox&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;onRefresh&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&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;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;El inbox está listo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Buscando el correo de prueba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Aún no hay mensajes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Correo recibido&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&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;No pudimos consultar el inbox&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;"inbox-title"&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="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;"inbox-title"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Inbox de prueba&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;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;label&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="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;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;waiting&lt;/span&gt;&lt;span class="dl"&gt;"&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;progress&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Esperando correo"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&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;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;ready&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;subject&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="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&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;empty&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&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;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;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;onRefresh&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Volver a consultar&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;)&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aria-live="polite"&lt;/code&gt; anuncia los cambios sin interrumpir una lectura. El botón sigue siendo un botón real, con foco visible y un nombre que explica la acción. Si el chequeo tarda, mantengo el texto “Buscando” cerca del indicador; un spinner por sí solo no ayuda a una persona que navega con lector de pantalla.&lt;/p&gt;

&lt;p&gt;También conviene mostrar el tiempo de la última consulta. No hace falta prometer una entrega instantánea: una frase como “Última revisión hace unos segundos” da contexto y evita clics repetidos. En pruebas con &lt;code&gt;tp mail so&lt;/code&gt; o con otra fuente, ese dato permite distinguir una demora del proveedor de un problema de renderizado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesibilidad y rendimiento en la misma revisión
&lt;/h2&gt;

&lt;p&gt;La accesibilidad no es una capa para añadir al final. Un estado explícito mejora el rendimiento percibido porque la persona entiende la espera antes de decidir si abandona o pulsa otra vez. En mi revisión de CSS compruebo tres cosas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El layout no salta cuando aparece el botón o el asunto del mensaje. Reservo espacio para esos elementos y evito cambios bruscos.&lt;/li&gt;
&lt;li&gt;El foco no desaparece al actualizar la lista. Si se abre un detalle, lo devuelvo a un encabezado útil o al control que inició la acción.&lt;/li&gt;
&lt;li&gt;La animación es opcional. Con &lt;code&gt;prefers-reduced-motion&lt;/code&gt;, desactivo el giro y mantengo un indicador textual.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.inbox-spinner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&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;En móvil, reviso que el mensaje de estado y el botón queden juntos, sin obligar a desplazarse para entender el resultado. Es un ajuste simple, pero se nota mucho en una interacion corta de verificación.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para probar el flujo
&lt;/h2&gt;

&lt;p&gt;Antes de dar por terminado el componente, pruebo estos casos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;solicitud lenta: el estado cambia rápido y explica la espera;&lt;/li&gt;
&lt;li&gt;respuesta vacía: no se presenta como un error del sistema;&lt;/li&gt;
&lt;li&gt;mensaje recibido: el asunto y la hora son legibles;&lt;/li&gt;
&lt;li&gt;error de red: existe un camino claro para reintentar;&lt;/li&gt;
&lt;li&gt;doble clic: no se generan consultas duplicadas sin control;&lt;/li&gt;
&lt;li&gt;lector de pantalla: cada transición tiene una etiqueta entendible;&lt;/li&gt;
&lt;li&gt;pantalla estrecha: el contenido no se desborda ni se superpone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para investigar mensajes de activación en un producto SaaS, puede servir &lt;a href="https://dev.to/hannahdev56/como-revisar-emails-de-activacion-en-saas-1kjf"&gt;revisar emails de activación en un SaaS&lt;/a&gt;. Y si la validación vive dentro de un formulario, recomiendo &lt;a href="https://dev.to/silviutech/react-valida-emails-sin-friccion-visual-4pk5"&gt;validar emails sin fricción visual&lt;/a&gt; antes de añadir más lógica al inbox.&lt;/p&gt;

&lt;p&gt;Hay un detalle de documentación que suele olvidarse: registrar los nombres usados por el equipo. Un test puede buscar “tepm mail com” o “tempail” por una razón histórica, pero esos terminos no deberian convertirse en etiquetas visibles ni en enlaces. Los dejo como alias de fixture, con una nota breve, para que nadie confunda un typo de prueba con una funcionalidad.&lt;/p&gt;

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

&lt;p&gt;Un inbox de prueba útil no intenta ocultar la espera. La hace legible, medible y recuperable. En React, un modelo de estados pequeño, un mensaje accesible y un layout estable suelen aportar más que otra animación o un botón de reintento colocado sin contexto.&lt;/p&gt;

&lt;p&gt;Cuando el frontend explica si está esperando, vacío, listo o bloqueado, el flujo de verificación deja de sentirse misterioso. Y esa claridad sirve tanto para la persona que prueba la app como para quien debe diagnosticarla a las nueve de la mañana, aun con poca paciencia y menos contexto.&lt;/p&gt;

</description>
      <category>react</category>
      <category>a11y</category>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>React: estados accesibles para email de prueba</title>
      <dc:creator>Silviu Technology</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:22:45 +0000</pubDate>
      <link>https://dev.to/silviutech/react-estados-accesibles-para-email-de-prueba-34ef</link>
      <guid>https://dev.to/silviutech/react-estados-accesibles-para-email-de-prueba-34ef</guid>
      <description>&lt;p&gt;Una pantalla de email de prueba parece sencilla: un botón envía el mensaje, un inbox muestra el resultado y una alerta confirma lo ocurrido. En la practica, esa pequeña interfaz mezcla asincronía, estados visuales, lectores de pantalla y cambios de layout que pueden hacer sentir lenta la aplicación.&lt;/p&gt;

&lt;p&gt;La solución no es agregar más spinners. Es modelar con claridad qué sabe la interfaz en cada momento. Un usuario debería distinguir entre “todavía estamos esperando”, “llegó un mensaje”, “la búsqueda expiró” y “algo falló”. Si todos esos casos usan el mismo texto, el flujo se vuelve confuso aunque el backend funcione bien.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema no es solo esperar un email
&lt;/h2&gt;

&lt;p&gt;Un inbox temporal o de prueba suele tener este recorrido:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;El usuario solicita un email.&lt;/li&gt;
&lt;li&gt;React muestra que la solicitud está en progreso.&lt;/li&gt;
&lt;li&gt;El servidor acepta el evento, pero el mensaje todavía no es visible.&lt;/li&gt;
&lt;li&gt;El mensaje aparece y puede abrirse o copiarse.&lt;/li&gt;
&lt;li&gt;La espera termina por timeout o por un error recuperable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cada paso necesita una señal distinta. “Cargando” no explica si el mensaje fue enviado, si el inbox está consultando o si la conexión se perdió. En pruebas de frontend, también conviene separar el estado del request del estado del mensaje: una respuesta HTTP correcta no garantiza que el email ya pueda leerse.&lt;/p&gt;

&lt;p&gt;Este enfoque se parece a diseñar &lt;a href="https://dev.to/hannahdev56/saas-alertas-de-trial-que-si-orientan-3i2p"&gt;alertas de trial que sí orientan&lt;/a&gt;: el copy debe decir qué pasó y cuál es el siguiente paso, no solo mostrar un color verde.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modela los estados antes de escribir JSX
&lt;/h2&gt;

&lt;p&gt;Un union type pequeño evita que el componente dependa de varios booleanos que pueden entrar en combinaciones imposibles:&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;InboxState&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="s1"&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="s1"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;startedAt&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="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="s1"&gt;received&lt;/span&gt;&lt;span class="dl"&gt;'&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="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="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;retryable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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="s1"&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;Con &lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;hasMessage&lt;/code&gt;, &lt;code&gt;hasError&lt;/code&gt; y &lt;code&gt;isExpired&lt;/code&gt; es facil terminar con una alerta de éxito y error al mismo tiempo. Un estado discriminado obliga a que la vista elija una sola lectura principal. También hace más simple probar las transiciones: idle → waiting → received, o waiting → expired.&lt;/p&gt;

&lt;p&gt;El texto debe ser concreto. “Esperando confirmación” ayuda más que “Procesando”. Para un timeout, “No vimos el email en 60 segundos; puedes volver a intentar” ofrece contexto y una acción. Si el usuario usa un lector de pantalla, esa diferencia es todavía más importante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un componente React con layout estable
&lt;/h2&gt;

&lt;p&gt;El feedback no debería empujar el botón ni mover el inbox cuando cambia. Reserva una región con altura mínima y usa &lt;code&gt;aria-live&lt;/code&gt; solo para cambios relevantes:&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;InboxStatus&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="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;InboxState&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Listo para comprobar el inbox.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;waiting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Esperando el email de prueba.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;El email ya está disponible.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;La espera terminó; puedes reintentar.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No pudimos consultar el inbox.&lt;/span&gt;&lt;span class="dl"&gt;'&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="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;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;"status"&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;text&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;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;min-block-size&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="py"&gt;margin-block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.75rem&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--text-muted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.status&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;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--focus-ring&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;&lt;code&gt;role="status"&lt;/code&gt; anuncia el cambio sin interrumpir cada acción del usuario. No conviene poner un contador que se actualiza cada segundo en esa misma región: un lector de pantalla podria repetirlo demasiado. Muestra el tiempo restante en otra parte, con una etiqueta visible y un ritmo de actualización razonable.&lt;/p&gt;

&lt;p&gt;Un botón deshabilitado durante la espera debe conservar una explicación. El texto “Comprobando…” es mejor que dejar solo un icono. Para conexiones lentas, evita reemplazar todo el contenido; conserva el contexto del inbox y actualiza la parte que cambió.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesibilidad y rendimiento en la misma revisión
&lt;/h2&gt;

&lt;p&gt;La accesibilidad no es un paso posterior al rendimiento. Un árbol pequeño, mensajes estables y menos renders suelen beneficiar a ambos. Calcula el copy del estado en el componente y evita recrear listas grandes del inbox cuando solo cambió la etiqueta.&lt;/p&gt;

&lt;p&gt;También reviso estos puntos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El contraste del estado secundario sigue siendo legible en modo oscuro.&lt;/li&gt;
&lt;li&gt;El foco no desaparece cuando llega el mensaje.&lt;/li&gt;
&lt;li&gt;El título del email y el timestamp tienen nombres accesibles.&lt;/li&gt;
&lt;li&gt;La respuesta de error no depende solo de rojo o verde.&lt;/li&gt;
&lt;li&gt;El polling se cancela al desmontar el componente.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt; evita animaciones innecesarias.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para un flujo de CI, los &lt;a href="https://dev.to/alexcarteruk/kubernetes-limites-de-confianza-para-emails-ci-4hnp"&gt;límites de confianza para emails de CI&lt;/a&gt; también sirven como recordatorio de que cada ejecución debe tener un contexto claro. En la UI, eso significa mostrar el inbox o identificador de la ejecución, sin exponer datos que no corresponden al usuario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist para una interfaz de email de prueba
&lt;/h2&gt;

&lt;p&gt;Antes de dar por terminado el componente, comprueba:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;¿Existe un estado explícito para espera, éxito, expiración y error?&lt;/li&gt;
&lt;li&gt;¿El layout mantiene su espacio cuando aparece el feedback?&lt;/li&gt;
&lt;li&gt;¿El mensaje importante se anuncia una sola vez?&lt;/li&gt;
&lt;li&gt;¿El foco y el teclado siguen funcionando después de un reintento?&lt;/li&gt;
&lt;li&gt;¿El polling termina cuando el componente se desmonta?&lt;/li&gt;
&lt;li&gt;¿El inbox se identifica con un nombre entendible, y no como “dummy e mail”?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si el flujo necesita un buzón aislado para una comprobación manual, un &lt;a href="https://tempmailso.com" rel="noopener noreferrer"&gt;temp mail&lt;/a&gt; puede ser parte del entorno de prueba. El enlace no arregla la UX por sí solo: la interfaz aún debe explicar quién está esperando, qué llegó y qué puede hacer la persona después.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;Una buena interfaz de email de prueba convierte la asincronía en información útil. En React, un modelo de estados claro, una región de feedback estable y una revisión conjunta de accesibilidad y rendimiento suelen ser suficientes para evitar gran parte de la fricción.&lt;/p&gt;

&lt;p&gt;No hace falta llenar la pantalla de indicadores. Hace falta elegir palabras precisas, respetar el foco y mantener el contexto cuando el servidor tarda. Ese detalle pequeño se nota mucho cuando el flujo se usa en una prueba real, con una conexión lenta o con tecnologías de asistencia.&lt;/p&gt;

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