---
title: "Test email verification and login codes in Playwright · Proxy Mail QA"
description: "A Playwright test that signs up with a fresh inbox, waits for the verification code or link and finishes the flow, with a report on each pull request."
url: https://proxymail.qa/solve/playwright-email-tests
---

1. [Home](https://proxymail.qa/)
2. [What we solve](https://proxymail.qa/solve)

# Test sign-up emails in Playwright

To test email verification in Playwright, give each test a fresh address, sign up with it, then wait for the code or link and finish the flow. With the proxymail-qa package that's two calls: createAddress before the form, waitForCode or waitForLink after it. It runs on the Free plan, locally and in CI.

Updated October 17, 2026

## Set up

Install the [proxymail-qa](https://www.npmjs.com/package/proxymail-qa) package and put an [API key](https://proxymail.qa/docs/api-keys) in `PROXYMAIL_QA_API_KEY`, in your shell, a `.env` file kept out of Git, or your CI’s secrets. The Free plan is enough.

Terminal

```bash
npm install --save-dev proxymail-qa

```

## The test

Import `test` and `expect` from `proxymail-qa/playwright`: every test gets a `qa` fixture, a client that also names the test in each call for the report described below.

signup.spec.ts

```ts
import { test, expect } from 'proxymail-qa/playwright';

test('sign-up sends a working code', async ({ page, qa }) => {
  const { address, password } = await qa.createAddress({ persona: true, password: true });
  const submitted = new Date();

  await page.goto('/signup');
  await page.getByLabel('Email').fill(address);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign up' }).click();

  const { value } = await qa.waitForCode(address, { since: submitted });
  await page.getByLabel('Code').fill(value);
  await expect(page).toHaveURL(/welcome/);

  await qa.deleteAddress(address);
});

```

For a link instead of a code:

TypeScript

```ts
const { value: link } = await qa.waitForLink(address, { type: 'verify', since: submitted });
await page.goto(link);

```

`type` is `verify`, `magic_signin`, `invite`, `reset` or `any`. Match the labels and routes to your app rather than copying these.

## Check the email, not just the code

A passing sign-up can still hide a broken email. These catch the usual ones:

TypeScript

```ts
// Exactly one verification email, not two.
await new Promise((resolve) => setTimeout(resolve, 10_000)); // time for a duplicate
expect((await qa.countEmails(address, { since: submitted, type: 'link' })).count).toBe(1);

// Its link goes to staging, not localhost or production, and it passes SPF, DKIM and DMARC.
const result = await qa.checkEmail(address, { since: submitted, type: 'link', expectHost: 'staging.example.com' });
expect(result.failed).toEqual([]);

```

`checkEmail` also flags emails that don’t say when the code expires, that may not fit a phone, and, with `expectText` and `expectLanguage`, missing phrases or the wrong language. It returns verdicts, never the email.

## Run it in CI, with a report on the pull request

Add the [GitHub Action](https://proxymail.qa/docs/ci#a-report-on-the-pull-request) before your tests and the reporter to your Playwright config. The report on the pull request then lists which emails arrived and how fast, which test each came from, and the tests that failed with the step that failed.

.github/workflows/e2e.yml

```yaml
      - uses: aaquib90/proxymail-qa-action@v1
        with:
          api-key: ${{ secrets.PROXYMAIL_QA_API_KEY }}
      - run: npx playwright test
        env:
          PROXYMAIL_QA_API_KEY: ${{ secrets.PROXYMAIL_QA_API_KEY }}

```

playwright.config.ts

```ts
export default defineConfig({
  reporter: [['list'], ['proxymail-qa/playwright/reporter']],
});

```

Each report is compared with the last runs of the same name, so it also says when emails got slower, a test now gets two emails, or a test fails now and then.

More in the [Playwright docs](https://proxymail.qa/docs/playwright), [Node.js client](https://proxymail.qa/docs/node) and [Running in CI](https://proxymail.qa/docs/ci).

## Try it on your next sign-up test

Free plan, no credit card. Create an address and watch the code or link arrive.

[Create a free account](https://app.proxymail.qa/?mode=signup)

## Questions

### How do I get the verification code in a Playwright test?

Note the time just before submitting the form, then call qa.waitForCode(address, { since }). It waits up to 30 seconds for the email and returns the code on its own, so there's no email parsing in your test. For a link, waitForLink returns the URL to open with page.goto.

### Why does my test sometimes pick up an old code?

Without since, a wait can match an email from an earlier step or test run. Pass the time the form was submitted, and give each test its own address.

### What happens if the email never arrives?

The wait fails with a message saying why as far as Proxy Mail QA can tell: most often the app's email provider is in sandbox or test mode and only sends to verified addresses. A longer timeout won't help then; fix the sending side.

### Can tests run in parallel?

Yes. Give each test its own address, or one address per run with a plus tag per test (name+worker1@ arrives at name@, and the wait only matches its own tag). A wait counts as one request however long it holds.

## Related guides

- [Test sign-up and email verificationA fresh, private address for every sign-up test, with the verification link or code ready the moment it lands.](https://proxymail.qa/solve/email-verification-testing)
- [Test OTP and one-time login codesCodes pulled out of the email with a Copy button and a countdown when the email says how long they last.](https://proxymail.qa/solve/otp-code-testing)
- [Test magic links and passwordless sign-inSingle-use links that are never opened or prefetched on our side, so they still work when you click them.](https://proxymail.qa/solve/magic-link-testing)
- [Test password reset emailsRequest, receive and use reset links, and keep the new password saved next to the test account.](https://proxymail.qa/solve/password-reset-testing)
- [Stop sharing one Gmail inbox for QAPrivate, disposable addresses for every tester instead of one shared mailbox and a pile of plus-addresses.](https://proxymail.qa/solve/shared-test-inbox)
- [Take QA notes by voice with Wispr FlowDictate what each test account is for, its state and what you found, straight into its notes, in seconds.](https://proxymail.qa/solve/voice-notes-wispr-flow)
- [Verification link points to localhost:3000Why emails from staging link to localhost, how to fix it in Supabase, Firebase and other stacks, and how we flag it.](https://proxymail.qa/solve/localhost-links)
- [Test sign-up emails in CypressA complete Cypress test for sign-up links and codes, with the email waits running as Cypress tasks.](https://proxymail.qa/solve/cypress-email-tests)
- [Test sign-up emails with pytestA complete pytest test for sign-up codes and links, with Playwright for Python or Selenium and a one-file helper.](https://proxymail.qa/solve/pytest-email-tests)

[Built your app with an AI tool? Pick yours](https://proxymail.qa/built-with)
