# Test fixtures: inboxes and test data

Source: https://testwatch.dev/docs/fixtures
Updated: 2026-10-17

Every generated test imports from ../fixtures. Besides test and expect, it gives tests real email inboxes and values you can find and delete later. All of it works on every plan.

Tests import these from `../fixtures`: `test`, `expect`, `inbox`, `waitForEmail`, `unique`, `testEmail`, `track`, `otp` (the current code for an authenticator 2FA login), `file` (a file from the app’s Files library, for uploads) and `readDownload` (the name, size, type and text of a download). Generated tests use them, and they keep working in an [exported suite](https://testwatch.dev/docs/export).

## Test email inboxes

TestWatch receives mail for the addresses it gives your tests, so a test can follow a verify link, a magic link or a password reset, or type an emailed code.

```
import { test, expect, inbox, unique } from '../fixtures';

test('Sign up and verify the email', async ({ page }) => {
  const box = inbox('signup');
  await page.goto('/signup');
  await page.getByLabel('Company').fill(unique('company'));
  await page.getByLabel('Email').fill(box.address);
  await page.getByRole('button', { name: 'Create account' }).click();

  const mail = await box.waitForEmail({ subject: /verify/i });
  await page.goto(mail.link!);
  await expect(page.getByText('Email verified')).toBeVisible();
});
```

### `inbox(label?)`

Returns `{ address, label, waitForEmail }`. Every call gives a fresh address that starts with `tw-` and the run tag, so a re-run doesn’t collide with a user an earlier run created. Type `address` into the form. `box.waitForEmail(...)` reads that address.

### `waitForEmail({ subject, from, to })`

- `subject` and `from` are a string (a case-insensitive substring) or a regular expression. All three are optional.
- Without `to`, it reads the latest `inbox()` address of this test, or else the test account’s own TestWatch inbox, if the account was created with one.
- Only mail that arrived after the address was created (or, for an account inbox, after this test attempt started) counts. Among the matches the newest wins, so a resent email replaces the first one. An email it returned is not returned again, so a second call gets the next one.
- It waits up to 45 seconds. There is no timeout option.

It returns:

```
{
  subject: string,
  from: string,
  to: string,
  text: string,
  html: string,
  links: string[],
  link: string | null,     // the action link: verify, reset, sign-in
  code: string | null,     // a one-time code, when the email has one
  receivedAt: Date,
}
```

When `link` has several candidates, a link to the app’s own host is preferred.

### When no email comes

| Error starts with | What happens |
| --- | --- |
| `TESTWATCH_EMAIL_NOT_RECEIVED` | No matching email within 45 seconds. A normal failure that TestWatch checks: if other mail arrived (say, the subject changed), it proposes a fix to the matcher; if nothing arrived, it reports a real bug, because the app didn’t send the email. |
| `TESTWATCH_INBOX_MISCONFIGURED` | The test or its account is set up wrong, for example the account’s inbox was removed. The message says what to change. Shown as failed; no fix is proposed. |
| `TESTWATCH_INBOX_UNAVAILABLE` | The inbox couldn’t be read for the whole wait. Recorded as an error, not a bug in your app. |

Each email a test read appears on the result’s Technical tab, with links and codes masked.

## Clean test data

Tests that create things leave them behind. The helpers below make every value a run creates easy to find and delete.

```
import { test, expect, unique, testEmail, track } from '../fixtures';

test('Create an invoice', async ({ page }) => {
  await page.getByLabel('Customer name').fill(unique('customer'));   // tw-r7k2m9x4qa-x3f9ab
  await page.getByLabel('Billing email').fill(testEmail('billing'));  // tw-r7k2m9x4qa-p2k8sd@example.com
  await page.getByRole('button', { name: 'Create invoice' }).click();
  const number = track(await page.getByTestId('invoice-number').innerText(), 'invoice');
  await expect(page.getByText(number)).toBeVisible();
});
```

- `unique(label?, { compact? })` returns `tw-<runTag>-<6 random>`, for any field that must be unique: a username, a company or product name. With `compact: true` it uses letters and digits only (`tw<runTag><6 random>`) for fields that reject `-`.
- `testEmail(label?)` returns `tw-<runTag>-<6 random>@example.com`, or the app’s own test email domain if you set one. It isn’t meant to receive mail. A test that has to read the email uses `inbox()`.
- `track(value, label?)` records an id your app created, such as an order number read off the page, and returns it unchanged.

The run tag is `r`, `g` or `h` (a run, a generation or a fix check) plus 9 characters, the same for every test and retry in that job. Inbox addresses carry it too, so deleting by the prefix also catches signups. The label is only recorded; it isn’t part of the value. A result’s Technical tab lists what its test created.

## The cleanup webhook

Set one per app on **CI & deploys → Test data cleanup**: an https URL that receives what each job created. You get a `whsec_` signing secret, shown once. Requests are signed like [webhook channels](https://testwatch.dev/docs/deploy-hooks-and-webhooks#verify) (Standard Webhooks).

- One POST when a run closes, when a generation ends and when a fix check ends, only if that job created something.
- Delete by the exact `prefix`, or by the listed values. Never delete by a bare `tw-`.
- A body holds up to 2,000 items and 256 KB. Past that, `truncated` is `true` and the full list is at [`GET /v1/runs/:id/test-data`](https://testwatch.dev/docs/api#test-data) (an API key, Starter and up).
- 429, 5xx and network errors are retried for about 30 minutes. After 5 other failures in a row the hook is turned off, with the reason shown.

```
{
  "type": "test_data.created",
  "timestamp": "2026-10-12T09:00:00.000Z",
  "data": {
    "workspaceId": "...",
    "app": { "id": "...", "name": "Shop" },
    "source": "run",
    "sourceId": "...",
    "runTag": "r7k2m9x4qa",
    "prefix": "tw-r7k2m9x4qa",
    "targetUrl": "https://staging.shop.example",
    "run": { "id": "...", "trigger": "scheduled", "environment": "Staging", "url": "https://app.testwatch.dev/..." },
    "generation": null,
    "heal": null,
    "items": [
      { "kind": "unique", "value": "tw-r7k2m9x4qa-x3f9ab", "label": "company", "testCaseId": "...",
        "testName": "Create company", "resultId": "...", "attempt": 1, "createdAt": "..." }
    ],
    "totalItems": 1,
    "truncated": false
  }
}
```
