Email verification tests rarely fail because the browser cannot click a button. More often, the test receives a message it did not expect, checks the right message at the wrong time, or leaves too little evidence to explain the failure. Then a retry passes and everyone moves on, even though the test is still fragile.
I treat a temporary inbox as part of the test fixture, not as an invisible service. Whether the suite calls it temp mail so, a throwaway email address, or simply an inbox, the run should record how that inbox was selected and which messages were observed. This small change makes QA conversations much calmer.
The failure is often an evidence problem
A typical flow looks straightforward:
- Create an inbox for the test.
- Submit the signup form.
- Poll for the verification email.
- Open the link and assert the account is verified.
The trouble starts when step three only asks for the newest email. A previous message can win the race. A shared inbox can contain another test's message. A provider can return a cached response. If the assertion reports only timeout, the team cannot tell which case happened.
This is similar to connecting alerts to one rollout: an event is useful only when it has enough context to identify the change that caused it. Email assertions need the same discipline.
Define the email test contract
Before writing the locator, define a contract for the message. Mine usually includes:
- an inbox address owned by the current test run
- the timestamp immediately before form submission
- an expected recipient or inbox identifier
- a subject or sender rule
- a message ID that has not been consumed by an earlier attempt
- a maximum polling window
The timestamp matters. “Find the latest message” is not the same as “find a matching message created after this action.” The latter gives the test a clear boundary and avoids accepting stale mail.
It is also worth separating provider errors from product errors. If the inbox API returns a 502, mark the infrastructure step as inconclusive. If the message arrives but has the wrong recipient, that is a product or integration failure. Mixing both into one red assertion makes the dashboard less useful.
Some teams keep notes with strings like temp gamil com while investigating. That kind of typo is harmless in a private note, but it is a good reminder to log the exact fixture value and source returned by the inbox service.
Capture evidence during polling
Do not collect diagnostics only after the timeout. Capture a compact event for every poll:
type MailObservation = {
checkedAt: string;
messageIds: string[];
matchingSubjects: string[];
providerStatus: number;
};
Keep the evidence attached to the test attempt. On failure, print the inbox identifier, trigger time, poll count, and observed message IDs. Do not print verification tokens or full message bodies; those are secrets and they do not improve diagnosis.
A helper can then make the ownership rule explicit:
async function waitForVerification(inbox: string, after: Date) {
return poll(async () => {
const messages = await mailClient.list({ inbox, after });
const candidate = messages.find((message) =>
message.to === inbox && message.subject.includes("Verify")
);
recordObservation(inbox, messages);
return candidate;
}, { timeout: 30_000, interval: 1_000 });
}
The exact client does not matter as much as the boundaries. Pass the inbox into the helper, pass the start time into the query, and record what the helper saw. A retry should create a new evidence segment instead of overwriting the first attempt.
A failure triage checklist
When the test fails, answer these questions in order:
- Did the browser submit successfully, and what request ID did it receive?
- Was the inbox unique to this test or shared with a worker?
- Did polling begin after submission and use the correct time boundary?
- Did the provider return messages, an empty list, or an error?
- Did the matching message belong to this inbox and run?
- Did the verification link point to the expected environment?
If the first retry passes, compare the evidence from both attempts. That often reveals a race between account creation and mail delivery, a stale message, or a selector that finished before the backend had committed its state.
Caching can help reduce provider load, but it must preserve these distinctions; caching email checks without hiding signup risk is a useful design constraint here.
Final thoughts
Reliable Playwright email tests are less about longer timeouts and more about explainable fixtures. Give every inbox an owner, every poll a time boundary, and every retry its own evidence. The occasional awkward phrase in a test note is fine; an unexplained green retry is not. Once failures tell a complete story, flaky email checks become ordinary QA work instead of detective fiction.
Top comments (0)