<?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: BeilunYang</title>
    <description>The latest articles on DEV Community by BeilunYang (@beilunyang).</description>
    <link>https://dev.to/beilunyang</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%2F937469%2F2ad494de-970b-4a2d-9a12-ee8678e6e7b3.jpeg</url>
      <title>DEV Community: BeilunYang</title>
      <link>https://dev.to/beilunyang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/beilunyang"/>
    <language>en</language>
    <item>
      <title>Your AI agent can fill in a signup form. It can't read the verification email.</title>
      <dc:creator>BeilunYang</dc:creator>
      <pubDate>Mon, 28 Sep 2026 16:46:48 +0000</pubDate>
      <link>https://dev.to/beilunyang/your-ai-agent-can-fill-in-a-signup-form-it-cant-read-the-verification-email-54o0</link>
      <guid>https://dev.to/beilunyang/your-ai-agent-can-fill-in-a-signup-form-it-cant-read-the-verification-email-54o0</guid>
      <description>&lt;p&gt;Ask Claude, Cursor or Codex to sign up for something and it will happily fill in the form. Then it hits the wall every signup flow has: &lt;strong&gt;"We've sent you a code. Check your inbox."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent has no inbox. So it stops, and asks you to go and copy a six-digit number out of your email. The one step that actually proves the flow works is the one it can't do.&lt;/p&gt;

&lt;p&gt;This post is about closing that gap with an MCP server — and about a couple of design decisions that matter more than they look when the caller is an LLM rather than a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we want the agent to do
&lt;/h2&gt;

&lt;p&gt;Four steps, end to end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a throwaway mailbox and get its address.&lt;/li&gt;
&lt;li&gt;Type that address into the signup form.&lt;/li&gt;
&lt;li&gt;Wait for the verification email to arrive.&lt;/li&gt;
&lt;li&gt;Read it, pull out the code, and delete the mailbox.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 2 and 4 the agent already handles. Steps 1 and 3 need tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugging in an inbox
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://moemail.app/en/mcp" rel="noopener noreferrer"&gt;MoeMail&lt;/a&gt; is an open-source temporary email service, and it ships an MCP server as an npm package, &lt;a href="https://www.npmjs.com/package/@moemail/mcp" rel="noopener noreferrer"&gt;&lt;code&gt;@moemail/mcp&lt;/code&gt;&lt;/a&gt;. It runs locally over stdio via &lt;code&gt;npx&lt;/code&gt;, so there is nothing to deploy or keep running.&lt;/p&gt;

&lt;p&gt;For Claude Desktop, Cursor and Windsurf, the config block is identical:&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;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moemail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@moemail/mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MOEMAIL_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mk_••••••••••••"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code takes the same thing as one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add moemail &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MOEMAIL_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mk_•••••••••••• &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @moemail/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server registers eight tools: &lt;code&gt;create_email&lt;/code&gt;, &lt;code&gt;wait_for_email&lt;/code&gt;, &lt;code&gt;list_messages&lt;/code&gt;, &lt;code&gt;read_message&lt;/code&gt;, &lt;code&gt;list_emails&lt;/code&gt;, &lt;code&gt;send_email&lt;/code&gt;, &lt;code&gt;delete_message&lt;/code&gt; and &lt;code&gt;delete_email&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a session actually looks like
&lt;/h2&gt;

&lt;p&gt;You say: &lt;em&gt;"Sign up for the beta at example.com with a throwaway address and tell me the code."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The agent creates a mailbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create_email { "expiry": "1h" }
→ { "id": "k3f…", "address": "x7q2m9@moemail.app", "expiresAt": "…T15:04:05.000Z" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It types &lt;code&gt;x7q2m9@moemail.app&lt;/code&gt; into the form and submits. Then it waits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wait_for_email { "emailId": "k3f…", "timeoutSec": 90 }
→ { "status": "received", "elapsedSec": 12,
    "message": { "messageId": "m81…", "from": "no-reply@example.com",
                 "subject": "Your code is 482913", "receivedAt": "…" } }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the message, finds &lt;code&gt;482913&lt;/code&gt;, finishes the signup, and calls &lt;code&gt;delete_email&lt;/code&gt; so nothing is left behind. Four tool calls, plus however many polls the mail takes to land.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design decision that makes it work: timeouts are not errors
&lt;/h2&gt;

&lt;p&gt;The interesting tool is &lt;code&gt;wait_for_email&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A naive "check inbox" tool returns immediately: empty, or not. That leaves the polling loop to the model, and models are bad at patience. They check once, see nothing, and either give up or invent a reason the email never came.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;wait_for_email&lt;/code&gt; &lt;strong&gt;blocks for up to 90 seconds&lt;/strong&gt;, polling the inbox for the agent, and when nothing arrives it does not throw. It returns:&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;"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;That one choice changes the agent's behaviour. An error reads as "something is broken, stop". A &lt;code&gt;timeout&lt;/code&gt; status reads as "not yet", and the natural next move is to call the tool again — which is exactly right, because slow transactional mail is normal and not a failure.&lt;/p&gt;

&lt;p&gt;If you are writing tools for agents, this generalises: &lt;strong&gt;return expected, recoverable states as data, not as exceptions.&lt;/strong&gt; Save errors for things the agent should actually stop on, like a bad API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same thing from a shell
&lt;/h2&gt;

&lt;p&gt;Not every agent lives in a chat window. For CI jobs and shell-driven agents there is a CLI, &lt;a href="https://www.npmjs.com/package/@moemail/cli" rel="noopener noreferrer"&gt;&lt;code&gt;@moemail/cli&lt;/code&gt;&lt;/a&gt;, where every command takes &lt;code&gt;--json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @moemail/cli
moemail config &lt;span class="nb"&gt;set &lt;/span&gt;api-key mk_••••••••••••

moemail create &lt;span class="nt"&gt;--expiry&lt;/span&gt; 1h &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="c"&gt;# {"id":"k3f…","address":"x7q2m9@moemail.app","expiresAt":"…"}&lt;/span&gt;

moemail &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--email-id&lt;/span&gt; k3f… &lt;span class="nt"&gt;--timeout&lt;/span&gt; 120 &lt;span class="nt"&gt;--json&lt;/span&gt;
moemail &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;--email-id&lt;/span&gt; k3f… &lt;span class="nt"&gt;--message-id&lt;/span&gt; m81… &lt;span class="nt"&gt;--json&lt;/span&gt;
moemail delete &lt;span class="nt"&gt;--email-id&lt;/span&gt; k3f…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exit codes are deliberate: &lt;code&gt;1&lt;/code&gt; is a runtime error (including a wait that timed out), &lt;code&gt;2&lt;/code&gt; is a configuration or auth error. A script can tell "no mail yet" apart from "wrong key" without parsing anything.&lt;/p&gt;

&lt;p&gt;The CLI can also install a skill file, so Claude Code or Codex already knows the create → wait → read → delete loop before you ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;moemail skill &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# auto-detects Claude Code and Codex&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this is genuinely useful
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing your own signup and onboarding flows.&lt;/strong&gt; Have an agent walk through registration on your staging environment after each deploy and confirm the verification email arrives and the code works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-driven QA&lt;/strong&gt; of password reset, magic links and email-change confirmations — the flows that are tedious to click through by hand and easy to break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trying out a tool without handing over your real address&lt;/strong&gt;, where the service's terms allow it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it isn't
&lt;/h2&gt;

&lt;p&gt;It is worth saying plainly: a throwaway inbox for an agent is not a licence to mass-create accounts, farm free trials, or get around a platform's limits. Plenty of services forbid that in their terms and actively detect it. Point this at systems you own or are allowed to test, and respect the rules of the ones you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost and self-hosting
&lt;/h2&gt;

&lt;p&gt;The MCP server and CLI are MIT-licensed and free to install. Calling the MoeMail API needs a key with quota, and &lt;strong&gt;API access is a paid add-on&lt;/strong&gt; — the default plan has no free API calls. Budget for polling: every poll &lt;code&gt;wait_for_email&lt;/code&gt; makes is one API call, so a typical round trip is four calls plus the polls. The web inbox itself is free.&lt;/p&gt;

&lt;p&gt;If you would rather not depend on a hosted service at all, MoeMail is &lt;a href="https://github.com/beilunyang/moemail" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and runs on Cloudflare Workers. Point the packages at your own instance with &lt;code&gt;MOEMAIL_API_URL&lt;/code&gt;; they default to moemail.app.&lt;/p&gt;




&lt;p&gt;Full client configs (including Codex) and the complete tool list are on the &lt;a href="https://moemail.app/en/mcp" rel="noopener noreferrer"&gt;MCP page&lt;/a&gt;. The source for both packages lives in the &lt;a href="https://github.com/beilunyang/moemail" rel="noopener noreferrer"&gt;repository&lt;/a&gt; under &lt;code&gt;packages/mcp&lt;/code&gt; and &lt;code&gt;packages/cli&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Playwright Email Testing: A Real End-to-End Tutorial (No Mocks)</title>
      <dc:creator>BeilunYang</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:31:06 +0000</pubDate>
      <link>https://dev.to/beilunyang/playwright-email-testing-a-real-end-to-end-tutorial-no-mocks-24d8</link>
      <guid>https://dev.to/beilunyang/playwright-email-testing-a-real-end-to-end-tutorial-no-mocks-24d8</guid>
      <description>&lt;p&gt;Most "email testing" advice ends at stubbing the send call. You assert that your&lt;br&gt;
app &lt;em&gt;tried&lt;/em&gt; to send a message, and the test goes green. That leaves the&lt;br&gt;
interesting half untested: whether the message actually left your infrastructure,&lt;br&gt;
whether the template rendered, and whether the six-digit code inside it matches&lt;br&gt;
the one your backend is willing to accept.&lt;/p&gt;

&lt;p&gt;This walks through the other approach — driving a real signup flow in&lt;br&gt;
&lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;, letting a real email get delivered to a&lt;br&gt;
real inbox, then reading it back over an API and typing the code into the page.&lt;br&gt;
No mail server to run, no shared QA mailbox to clean up.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;A verification-email test has four moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;an address that is unique to this test run,&lt;/li&gt;
&lt;li&gt;the browser flow that triggers the send,&lt;/li&gt;
&lt;li&gt;a way to read the message that arrives,&lt;/li&gt;
&lt;li&gt;code extraction and the assertion.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 3 are the ones people get wrong, and they get them wrong in the same&lt;br&gt;
way: by sharing one mailbox across the suite. The moment two tests run in&lt;br&gt;
parallel, one of them reads the other's email. So the rule is &lt;strong&gt;one inbox per&lt;br&gt;
test&lt;/strong&gt;, provisioned on the fly and thrown away afterwards.&lt;/p&gt;
&lt;h2&gt;
  
  
  The inbox helper
&lt;/h2&gt;

&lt;p&gt;Any disposable-inbox API with a REST interface works here. I'll use&lt;br&gt;
&lt;a href="https://moemail.app" rel="noopener noreferrer"&gt;MoeMail&lt;/a&gt;'s because it's open source and the free tier is&lt;br&gt;
enough for a CI suite — the shape is the same anywhere, so swap the base URL and&lt;br&gt;
the auth header if you use something else.&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="c1"&gt;// inbox.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://moemail.app/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt; &lt;span class="o"&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;MAIL_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Inbox&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;email&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;export&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;createInbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ttlMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="nx"&gt;_600_000&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;Inbox&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;res&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;fetch&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;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/emails/generate`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Omit `name` and a random local part is generated for you — which is&lt;/span&gt;
    &lt;span class="c1"&gt;// exactly what you want, so parallel tests can never collide.&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="na"&gt;expiryTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;moemail.app&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;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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;`create inbox failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&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="c1"&gt;// { id, email }&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;type&lt;/span&gt; &lt;span class="nx"&gt;Message&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="na"&gt;from_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&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="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;received_at&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="k"&gt;export&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;waitForMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;inboxId&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="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_500&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="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;Message&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="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;res&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;fetch&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;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/emails/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inboxId&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;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KEY&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;`poll failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;messages&lt;/span&gt; &lt;span class="p"&gt;}&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;res&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;if &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;length&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&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 email arrived within &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&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;Two details worth keeping. &lt;code&gt;expiryTime&lt;/code&gt; is in &lt;strong&gt;milliseconds&lt;/strong&gt;, and the poll loop&lt;br&gt;
is bounded by a deadline rather than a fixed retry count — a fixed count silently&lt;br&gt;
becomes a different timeout the moment you change the interval.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wiring it into a Playwright fixture
&lt;/h2&gt;

&lt;p&gt;Doing this in the test body works, but it puts transport concerns in front of&lt;br&gt;
behaviour. A fixture hides it, and gives every test a fresh address without&lt;br&gt;
anyone having to remember:&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="c1"&gt;// fixtures.ts&lt;/span&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createInbox&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;Inbox&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./inbox&lt;/span&gt;&lt;span class="dl"&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;inbox&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="o"&gt;&amp;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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createInbox&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the test reads like the user story it represents:&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;waitForMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./inbox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&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;a new user can verify their email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inbox&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="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;inbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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;Sign up&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;msg&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;inbox&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b\d{6}\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`no 6-digit code in: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;msg&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="nf"&gt;toBeDefined&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;Verification code&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;code&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;expect&lt;/code&gt; on the extraction itself. &lt;code&gt;match(...)[0]&lt;/code&gt; throws&lt;br&gt;
&lt;code&gt;Cannot read properties of null&lt;/code&gt; when the regex misses, and you get a stack trace&lt;br&gt;
pointing at the regex instead of the actual failure, which is that the email said&lt;br&gt;
something you didn't expect. Asserting with the subject line in the message turns&lt;br&gt;
ten minutes of confusion into one glance at the report.&lt;/p&gt;
&lt;h2&gt;
  
  
  Magic links instead of codes
&lt;/h2&gt;

&lt;p&gt;Same fixture, different extraction — pull the URL out and navigate straight to it:&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/https:&lt;/span&gt;&lt;span class="se"&gt;\/\/[^\s&lt;/span&gt;&lt;span class="sr"&gt;"&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;verify&lt;/span&gt;&lt;span class="se"&gt;\?[^\s&lt;/span&gt;&lt;span class="sr"&gt;"&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;expect&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;toBeDefined&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your template only puts the link in the HTML part, read &lt;code&gt;msg.html&lt;/code&gt; instead of&lt;br&gt;
&lt;code&gt;msg.content&lt;/code&gt; — plain-text alternatives get out of sync with the HTML more often&lt;br&gt;
than anyone expects, and testing the part users actually click is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it survive CI
&lt;/h2&gt;

&lt;p&gt;A few things that turned flaky runs into boring ones for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never share an address across tests.&lt;/strong&gt; The fixture already handles it; the
failure mode when you don't is a test that passes alone and fails in a suite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer a webhook over polling if latency matters.&lt;/strong&gt; Polling is simpler and
fine for a handful of tests; a push means you react the instant mail lands
instead of waiting out an interval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the timeout tight but honest.&lt;/strong&gt; 30s covers real delivery. If you need
90s, you have a delivery problem that a longer timeout is hiding, not fixing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the API key in CI secrets&lt;/strong&gt;, not in &lt;code&gt;playwright.config.ts&lt;/code&gt;. Obvious, and
still the most common thing I see committed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The same recipe elsewhere
&lt;/h2&gt;

&lt;p&gt;Nothing above is Playwright-specific except the fixture wiring. The two helpers&lt;br&gt;
drop into Cypress or Selenium unchanged — only the way you register them differs.&lt;/p&gt;

&lt;p&gt;If you're currently paying for a hosted inbox service, this is also roughly the&lt;br&gt;
whole migration: the fixture is the seam, and only the transport behind it&lt;br&gt;
changes. I wrote up how the options compare —&lt;br&gt;
&lt;a href="https://moemail.app/en/email-testing/mailosaur-alternative" rel="noopener noreferrer"&gt;Mailosaur and MailSlurp alternatives that receive real email&lt;/a&gt;&lt;br&gt;
— including which of them can't actually accept mail from the public internet,&lt;br&gt;
which is the distinction most comparison posts skip.&lt;/p&gt;

&lt;p&gt;Full API reference is in the &lt;a href="https://docs.moemail.app/api.html" rel="noopener noreferrer"&gt;OpenAPI docs&lt;/a&gt;,&lt;br&gt;
and the server itself is on&lt;br&gt;
&lt;a href="https://github.com/beilunyang/moemail" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; under MIT if you'd rather&lt;br&gt;
self-host it and point the same tests at your own domain.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>qa</category>
      <category>node</category>
    </item>
  </channel>
</rss>
