Playwright email verification can pass for the wrong reason: six checks for a trustworthy test

Playwright email verification can pass for the wrong reason: six checks for a trustworthy test

A Playwright test can reach an "Email verified" screen and still prove the wrong thing. The test may have read yesterday's message from a shared mailbox. A parallel run may have produced the email. The first link in the HTML may have been a tracking or unsubscribe URL. The verification link may even point to the wrong environment. That is why a green browser assertion is not enough. A trustworthy email-verification test must connect one authorized signup, one isolated inbox, one expected message, one allowed destination, and one cleanup result into the same bounded journey. Disclosure: I am a technical co-founder of gettemp.email. The example uses our MIT-licensed connector and a paid service plan with Developer REST access. The testing principles also apply to other inbox providers. The deceptively green version This deliberately incomplete pseudocode illustrates a missing correlation check: const messages = await inbox.listMessages(sharedAddress); const message = messages[0]; const [firstUrl] = message.html.match(/https?:\/\/[^"'<>\s]+/g) ?? []; await page.goto(firstUrl); await expect(page.getByText('Email verified')).toBeVisible(); Enter fullscreen mode Exit fullscreen mode It does not establish that the message belongs to this test run. It trusts list order, treats any URL as a verification URL, and leaves the shared mailbox behind for the next run. A retry can therefore turn stale state into a false pass if the final screen does not identify the account being verified. Six checks that make the result trustworthy 1. Isolate the inbox per test run Create a fresh, short-lived inbox for each test attempt, including retries. Parallel workers then avoid competing for the same mailbox. Also assert the verified identity or account state in your application; a generic success heading alone is weaker evidence. 2. Separate the address from read authority An email address is routing information, not a credential. The GetTemp developer flow uses an account API key for quota and account access, plus a short-lived, inbox-scoped capability. The account key is reusable until revoked or expired, even if its value is displayed only once when created. The inbox capability is also reused for list, read, and delete operations during that inbox's lifetime. Both credentials are required for those Developer REST operations; a leaked address alone does not authorize reading. 3. Select the expected message Do not silently accept the first message returned. Match the expected subject and, where your application exposes one, a sender or non-secret correlation marker. Inbox isolation is the primary boundary; message matching is an additional assertion, not a substitute for isolation. 4. Poll with a deadline Email delivery is asynchronous. Poll at a fixed interval with a deadline, and also bound the HTTP requests: checking a clock between requests cannot interrupt a stalled request. Leave time in the overall test budget for signup, verification, and cleanup. The example below uses a 45-second polling deadline, five-second request timeouts, and a 90-second test budget. Tune those values to your sender. 5. Validate the destination before navigation Extracting the first href is unsafe and unreliable. Require HTTPS, compare the exact hostname with the application under test, and add an expected path when the host contains multiple verification routes. Reject missing or ambiguous matches. A check on the initial link does not restrict HTTP redirects: use an application-specific redirect policy if needed and check the final destination. 6. Clean up and preserve a content-free receipt Attempt inbox deletion in a finally block so it also runs after ordinary assertion failures. A network outage or killed worker can still prevent cleanup; use a short TTL as a fallback. Remove the test account from the target application through its own teardown mechanism too. Treat Playwright traces as sensitive artifacts: they can contain entered addresses, navigation URLs, DOM snapshots, and network data. Restrict access and retention, and review them before sharing. Generate a separate public receipt containing only stage results, timestamps, and error categories. Exclude mailbox addresses, OTPs, message content, credentials, and verification URLs from that receipt. Playwright's trace documentation describes what is captured. A Playwright example using the direct client This is an early release (v0.2.0) installed from GitHub. It requires Node.js 22 or newer for this recipe, an account API key, a plan with Developer REST access, and an authorized application that sends the verification email. The open-source connector itself is MIT-licensed; it does not make the hosted Developer REST service free. The repository also contains a Python/pytest example. The corrected example uses the package's direct GetTempClient with native fetch. The v0.2.0 /playwright adapter has a response-body handling defect; avoid that adapter in this version. This lifecycle helper is not a custom Playwright fixture defined with test.extend(). 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 Enter fullscreen mode Exit fullscreen mode Review the source before placing it in CI: github.com/sefara/gettemp-email-testing. Store credentials in your shell or CI secret store, never in the test file or a committed .env file: export GETTEMP_API_KEY='replace-with-your-account-api-key' export TARGET_APP_URL='https://staging.your-app.example' Enter fullscreen mode Exit fullscreen mode Save the following as email-verification.spec.js in your Playwright test directory. Adapt the signup fields, subject, verification path, and final account assertion to your application. The example uses an absolute signup URL, so it does not depend on a separate baseURL setting. Trace, video, and screenshots are disabled for this example; use protected debugging artifacts if you enable them. Review any custom reporter or external CI capture separately. 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 }, ); }); Enter fullscreen mode Exit fullscreen mode Run it with npx playwright test email-verification.spec.js. The inbox lifecycle wrapper attempts deletion after the callback, including after an exception; a 404 is treated as already absent. It does not remove the application's test account or generate a public receipt for you. HTTP errors, including quota errors, fail the attempt instead of triggering unlimited retries. The subject match is deliberately minimal. Where your sender supports it, add a sender filter and a per-attempt correlation marker. The link helper uses GetTemp's verification-likely classification and requires one unique matching link. Adjust your template-specific checks rather than assuming the classification proves the sender's identity or the link's semantics. When to use a hosted inbox Situation Better starting point You control the sender and can redirect SMTP in development A local SMTP catcher such as Mailpit You need to exercise an external sender's real public delivery path A publicly reachable, isolated test inbox You need one manual visual check A browser inbox may be sufficient Parallel CI must verify the complete public delivery path An API-controlled inbox with explicit cleanup A hosted inbox is not automatically better. Use it when the public receive path is part of the behavior you need to verify. Keep most template and token-generation cases at faster unit or integration layers, with only a small number of complete email journeys in E2E. What the passing result proves — and what it does not A passing run supports only the assertions it executes. To establish that the intended account was verified, the final identity/status check must reflect your server's state. The snippet alone cannot establish a deliverability rate, SLA, or correctness of every email template, and a successful DELETE response is not an independent audit of physical data erasure. There are three different levels of evidence here: The corrected snippet is checked in a browser against a synthetic application and mocked inbox responses. That checks example execution and failure paths, not live email delivery. The controlled Playwright observation used a local application, direct SMTP, and the first-party production inbox interface. It did not exercise this package through the customer Developer REST API. The MCP booking observation received Microsoft Bookings mail through MCP. Talenta develops gettemp.email, so this is an owner-authorized observation on a related company's site, not an independent endorsement or a production test of this REST snippet or the pytest connector. The public observation records exclude addresses, credentials, message content, and booking details. Related documentation: Complete Playwright guide, runnable files, and machine-readable proof MIT-licensed Playwright and pytest connector Developer REST documentation MCP setup for authorized AI agents Use this pattern only against systems you own or are explicitly authorized to test. A temporary inbox is the wrong tool for banking, purchases, recovery accounts, or any identity that must remain available after the test. Correction — 14 September 2026: clarified credential lifetime, trace handling, prerequisites, helper terminology, timeouts, and evidence scope. Replaced the affected v0.2.0 Playwright adapter with the direct client in the example. AI disclosure: AI assisted with drafting and code. The revised example was checked against the tagged implementation and exercised with synthetic browser fixtures; the production observations have the separate scopes described above.

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.