How to Test Magic Link (Passwordless) Login
Magic-link login is lovely for users and awkward for testers: there’s no password to type, just a link that arrives by email and logs you in when clicked. Here’s how to test passwordless login end-to-end by capturing that link from a disposable inbox.
How magic-link auth works
The flow: user enters their email → app emails a one-time link containing a signed token → user clicks it → app verifies the token and starts a session. No password anywhere. The test challenge is the same as the magic: the only way in is the emailed link.
Capture the link
const inbox = await createInbox(`magic-${Date.now()}`)
// request the magic link
await page.goto('https://staging.myapp.com/login')
await page.getByLabel('Email').fill(inbox.address)
await page.getByRole('button', { name: 'Send magic link' }).click()
// wait for the email and grab the link
const email = await waitForEmail(inbox._id, 'subject=' + encodeURIComponent('Sign in'))
const magicLink = email.primaryLinkprimaryLink is the best-guess call-to-action — the sign-in URL. (The createInbox/waitForEmail helpers come from the CI/CD guide.)
Click it to log in
Navigate to the link in the same browser context and assert you land authenticated:
await page.goto(magicLink)
await expect(page.getByText('Dashboard')).toBeVisible()Because magic links create a session, opening the link in your test’s browser logs that browser in — exactly what a real user gets.
Same-browser vs. cross-device
Some apps tie the link to the session that requested it (same-device); others let you open it anywhere. If yours is same-session, request and open the link in the samePlaywright context (as above). If it’s device-agnostic, open it in a fresh context to simulate clicking from a phone.
Heads up — link prefetching: Email clients and security scanners sometimes pre-fetch links, which can burn a single-use token before the user clicks. If you see flaky “link already used” failures in production, that’s often the cause. In tests, just make sure nothing fetches the link before you do.
Pure-API version
To skip the browser, pull the token from the link and exchange it via your API:
const token = new URL(email.primaryLink).searchParams.get('token')
const res = await fetch('https://staging.myapp.com/api/auth/magic', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
})
expect(res.status).toBe(200) // session cookie / JWT in the responseRelated reading
Try it in two minutes
Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.