TestWatchStart free
Docs menu

Test fixtures: inboxes and test data

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.

by Vasyl Harastei

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.

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 withWhat happens
TESTWATCH_EMAIL_NOT_RECEIVEDNo 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_MISCONFIGUREDThe 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_UNAVAILABLEThe 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 (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 (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
  }
}