General
Playwright email verification can pass for the wrong reason: six checks for a trustworthy test
Mgr. Ing. Michal Šefara, PhD. DEV Community 周榜
2 views
We deliberately made an email-verification test finish on the wrong account.
The page said Email verified. Playwright passed.
Then we added an assertion for which account had been verified. The same scenario failed.
This was a controlled browser experiment with synthetic accounts and mocked email responses. It illustrates a mistake that is easy to miss: testing the success message without testing the identity behind it.
Disclosure: I am a technical co-founder of gettemp.email. The example uses our MIT-licensed connector; its hosted Developer REST API requires a paid plan.
The false pass
Our test submitted current-run@example.test. We made the verification page identify previous-run@example.test while still showing the expected heading. Both addresses are fixtures.
Assertion against that page
Result
The “Email verified” heading is visible
PASS
The verified email equals this run's inbox address
FAIL — correctly catches the mismatch
A stale message from a shared mailbox is one way to reach the wrong account. Our experiment injects the mismatch directly; it does not claim to reproduce a real stale-email incident.
The important change is just the second assertion:
await expect(page.getByRole('heading', { name: 'Email verified' })).toBeVisible();
await expect(page.getByTestId('verified-email')).toHaveText(inbox.address);
Here, verified-email means the identity your application reports after server-side verification. If the page merely echoes the submitted address, assert the verified account through your test API instead. Two UI checks are useful only when they check the right state.
Keep each attempt tied to its own inbox
Start with a fresh inbox for every attempt, including retries. Submit that address, select the expected message, follow only the application's verification link, and assert the resulting identity. Then delete the inbox.
The full example below adds those boundaries. It also gives polling 45 seconds, caps each HTTP request at five seconds, and leaves a 90-second budget for the whole test. Adjust the application-specific selectors and timing for your sender.
Open the complete Playwright example and setup
Use Node.js 22+, a GetTemp account API key with paid Developer REST access, and a test application you own or are authorized to test. Your application sends the email; GetTemp receives it.
npm install --save-dev @playwright/test@1.55.0
npm install --save-dev github:sefara/gettemp-email-testing#v0.2.0
npx playwright install chromium
This is an early GitHub release. The example uses its direct client export.
Supply secrets through your CI secret store or current shell. These are placeholders:
export GETTEMP_API_KEY='replace-with-your-account-api-key'
export TARGET_APP_URL='https://staging.your-app.example'
Save as email-verification.spec.js in your Playwright test directory. Replace the signup fields, subject, /verify path, and final identity assertion with your application's equivalents.
import { test, expect } from '@playwright/test';
import { GetTempClient, verificationUrl } from '@gettemp-email/testing';
test.use({ trace: 'off', video: 'off', screenshot: 'off' });
test('verifies a new account by email', async ({ page }) => {
test.setTimeout(90_000);
const target = new URL(process.env.TARGET_APP_URL);
if (target.protocol !== 'https:' || target.username || target.password) {
throw new Error('TARGET_APP_URL must use HTTPS without credentials.');
}
const client = new GetTempClient({
fetch: (url, init = {}) =>
fetch(url, {
...init,
signal: AbortSignal.any([init.signal, AbortSignal.timeout(5_000)].filter(Boolean)),
}),
});
await client.withInbox(
async (inbox) => {
await page.goto(new URL('/signup', target).href);
await page.getByLabel('Email address').fill(inbox.address);
await page.getByRole('button', { name: 'Create account' }).click();
await expect(page.getByText('Check your inbox')).toBeVisible();
const summary = await client.waitForMessage(inbox, {
subjectIncludes: 'Verify',
timeoutMs: 45_000,
intervalMs: 1_000,
signal: AbortSignal.timeout(45_000),
});
const message = await client.readMessage(inbox, summary.id);
const href = verificationUrl(message, {
expectedHostname: target.hostname,
expectedPath: '/verify',
});
const destination = new URL(href);
if (
destination.origin !== target.origin ||
destination.pathname !== '/verify' ||
destination.username ||
destination.password
) {
throw new Error('Verification destination did not match the application.');
}
await page.goto(href);
if (new URL(page.url()).origin !== target.origin) {
throw new Error('Verification redirected outside the expected origin.');
}
await expect(page.getByRole('heading', { name: 'Email verified' })).toBeVisible();
// Replace this with your app's server-backed account identity/status assertion.
await expect(page.getByTestId('verified-email')).toHaveText(inbox.address);
},
{ ttlMinutes: 5 },
);
});
Run npx playwright test email-verification.spec.js.
The client wraps the inbox lifecycle in try/finally; a delete returning 404 means already absent. A short TTL covers cases where a killed worker or network failure prevents cleanup. Add your application's test-account teardown separately.
Six checks worth keeping
Isolate each attempt. Fresh inbox on each test and retry; assert the resulting account identity.
Separate address from access. GetTemp requires both the reusable account API key and a short-lived inbox capability for list/read/delete. Knowing the address does not grant access.
Match the expected message. Filter by subject and, where useful, sender or a per-run marker. Sender labels alone do not authenticate a message.
Bound the wait. Use a polling deadline, a request timeout, and an overall test budget.
Validate the destination before navigation. Require one matching HTTPS link and the expected origin/path. Initial URL checks do not constrain redirects; enforce your application's redirect policy when needed.
Clean up and protect debug artifacts. Delete the inbox and test account. Restrict trace access: traces can contain addresses, URLs and network data. Share a separate redacted result.
What we actually tested
The browser checks covered the false pass above, the corrected happy path, wrong-account detection, foreign and ambiguous links, quota failure, failed deletion, a stalled request, and a missing message. All nine scenarios produced the expected outcome. These were tests of the example with synthetic responses, not a production deliverability measurement.
For separate delivery evidence, our controlled Playwright observation records a local signup application sending mail through the public MX and reading the first-party production inbox. It did not exercise this customer REST snippet. That distinction matters when deciding what a green result tells you.
Do you need a hosted inbox?
If you control SMTP in development, a local catcher such as Mailpit is often enough. A hosted inbox helps when the public delivery path itself is part of the test.
Keep most template and token-generation checks in faster unit or integration tests, with a few complete email journeys in E2E.
You can inspect the connector source and the full integration guide. But the first useful change to an existing suite is smaller: make the verification page report the wrong account and see whether your test still passes.
Revision note
Updated 14 September 2026. Corrected credential lifetime, trace handling, setup and evidence scope; then shortened the article around a reproduced wrong-account scenario. The example uses the direct client because the v0.2.0 /playwright adapter has a response-body handling defect.
AI disclosure: AI assisted with writing and code. We checked the example in real Chromium with synthetic browser fixtures and mocked inbox responses.
Read original: https://dev.to/sefara/playwright-email-verification-can-pass-for-the-wrong-reason-six-checks-for-a-trustworthy-test-20nd
← Previous
I connected an authentic fruit fly brain to Polymarket order books via WebGL
Next →
Giving an AI Agent a Real Sandbox: Filesystem and Network Jail, in Java
Related
Your Cache Is Part of the Security Model
General
0
DEV Community 周榜
Why Most Medicine Reminders Fail (And How We Built One That Refuses to Be Ignored)
General
0
DEV Community 周榜
How to find LM Studio plugins and MCP servers
General
0
DEV Community 周榜
Stop Returning “Access Denied”
General
2
DEV Community 周榜
Comments0
No comments yet — be the first