Passwordless login feels elegant until an end-to-end test clicks a marketing footer link and marks the suite green. Magic links bundle SMTP delivery, URL tokens, cookie state, and one-time-use rules. A flaky test usually means the automation skipped validation and trusted whatever string looked like a URL.

Set the scene
You request "Email me a login link." The app sends HTML with a primary button, a plaintext fallback, an unsubscribe URL, and a support article link. Your test needs the callback that sets the session—not the Help Center.
Consumer disposable mail sites talk about throwaway email and burner addresses to keep a real mailbox clean. Your staging test should still use an inbox you control with saved access, not a public inbox anyone can open. Relay's privacy guide explains what disposable addresses do and do not hide.
Validation checklist before navigation
| Check | Question to answer |
|---|---|
| Scheme | Is it https (or the expected custom scheme)? |
| Host | Does it match staging.example.test, not a CDN typo? |
| Path | Is it the documented callback path? |
| Query | Is a token present without logging it? |
| Freshness | Did this message arrive after the click that triggered it? |
Relay's auth-artifact endpoint can return candidate links; your test must still apply the table above. Never treat familiarity of the sender domain as proof the link is safe.
Step-by-step Playwright pattern
- Create a fresh inbox and stash its ID in the fixture.
- Navigate to the login screen and request a magic link for that address.
- Poll Relay until a message matches sender and subject rules.
- Fetch auth artifacts and select the link whose host equals
process.env.EXPECTED_AUTH_HOST. - Open the link with
page.gotoinside a new context when parallel tests share a browser. - Assert authenticated UI state—not merely HTTP 200.
const inbox = await relay.inboxes.create();
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Send link" }).click();
const message = await relay.messages.wait(inbox.id, { timeoutMs: 60_000 });
const { links } = await relay.messages.auth(message.id);
const target = links.find((url) => new URL(url).host === expectedHost);
if (!target) throw new Error("No auth link for expected host");
await page.goto(target);
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
Compare with OTP flows in OTP best practices when a product sends both link and code.
Expiry and reuse scenarios
Strong suites cover:
- Fresh link succeeds within the advertised window.
- Expired link shows the documented error.
- Second click after success follows your product's single-use policy.
- Resend generates a new message; the test targets the newest match.
Document whether older tokens should invalidate. Tests should encode that contract instead of assuming.
Keep tokens out of artifacts
Mask query strings in screenshots. Disable full network HAR uploads for login callbacks unless your security team approves storage. Print only a redacted path such as /auth/callback?… in CI logs.
FAQ
Should agents open magic links automatically?
Only with explicit approval and host validation. Prefer returning the candidate URL to a human or a hardened browser step.
Can I combine magic-link and OTP tests in one spec?
Use separate fixtures. Mixed messages make extraction ambiguous.
What about the Chrome extension?
It copies one safe HTTPS candidate; it does not navigate. See the extension guide for manual flows and /extension for install links.
How does this relate to Mailinator-style QA tools?
Mailinator markets sign-up, reset, and 2FA testing with Playwright. Relay follows the same testing intent with scoped inboxes and MCP; see Relay vs Mailinator.
Coordinate with product security reviews
Passwordless flows often include device binding, IP throttling, or risk checks your test environment disables. Document which controls are off in staging so a passing test does not imply production behavior. When security enables a new check, add a negative case that proves the check blocks a reused or expired token.
If marketing email shares the same template infrastructure, add a negative assertion that the test ignores unsubscribe and footer links. That prevents future template edits from breaking login coverage silently.
Observability without token leaks
Structured CI output can include a mail_wait_ms field and a boolean auth_link_found without printing the token. Dashboard those fields to spot SMTP slowdowns before developers blame Playwright locators. Pair with the email test data guide when artifacts must be retained for audits.