Clean up the data your tests create
Every scheduled run signs up users, creates companies and places orders in your staging app. After a month, staging is mostly test leftovers. TestWatch marks what each run creates and tells your backend, so you can delete exactly that.
The test
import { test, expect, unique, testEmail, track } from '../fixtures';
test('Create a company and place an order', async ({ page }) => {
await test.step('Create a company', async () => {
await page.getByRole('link', { name: 'New company' }).click();
await page.getByRole('textbox', { name: 'Company name' }).fill(unique('company'));
await page.getByRole('textbox', { name: 'Billing email' }).fill(testEmail('billing'));
await page.getByRole('button', { name: 'Create company' }).click();
});
await test.step('Place an order', async () => {
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByText('Order placed')).toBeVisible();
track(await page.getByTestId('order-number').innerText(), 'order');
});
});
A test like this one. unique() and testEmail() make the values; track() records an id your app made, like an order number.
How it works
Every run gets a run tag, so its values look like tw-r7k2m9x4qa-x3f9ab. The prefix is the same for every test, retry and verify pass of one run, and the end is fresh on each call, so a retry never hits “name already taken”. unique('x', { compact: true }) drops the dashes for fields that refuse them. testEmail() addresses never receive mail; when the test must read the email, inbox() gives an address with the same prefix.
Each value is recorded with its test, its attempt and a label. That covers scheduled and CI runs, and also the verify runs while TestWatch writes or fixes a test, because those create data too. Records are kept for 30 days. Values the agent types while exploring are recorded only when they carry the run’s tag, so a real login it typed isn’t listed.
The cleanup webhook
Add one webhook per app under CI & deploys, then Test data cleanup. After a run closes, or a generation or fix ends, and only if it created something, TestWatch sends one test_data.created POST. It carries the prefix and the list of values. It is signed with the Standard Webhooks headers, so any Standard Webhooks library can check it, or a few lines of code:
// Example for YOUR backend (Node + Express). Not TestWatch code.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const key = Buffer.from(process.env.TESTWATCH_CLEANUP_SECRET!.replace(/^whsec_/, ''), 'base64');
function verified(req: express.Request, body: string): boolean {
const id = req.header('webhook-id') ?? '';
const ts = req.header('webhook-timestamp') ?? '';
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const want = createHmac('sha256', key).update(`${id}.${ts}.${body}`).digest();
return (req.header('webhook-signature') ?? '').split(' ').some((s) => {
const got = Buffer.from(s.replace(/^v1,/, ''), 'base64');
return got.length === want.length && timingSafeEqual(got, want);
});
}
const app = express();
app.post('/hooks/testwatch-cleanup', express.text({ type: '*/*' }), async (req, res) => {
if (!verified(req, req.body)) return res.sendStatus(401);
const event = JSON.parse(req.body);
if (event.type === 'test_data.created') {
const { prefix, items } = event.data; // prefix = "tw-<runTag>", never a bare "tw-"
await deleteTestData(prefix, items.map((i: { value: string }) => i.value)); // yours
}
res.sendStatus(204);
});
An example for your backend. deleteTestData is your own code; TestWatch doesn’t delete anything in your app itself.
Each item says what kind of value it is (a unique value, an email, an inbox address or a tracked id), with its label, test name and attempt, so your handler can choose what to delete. The webhook-id header stays the same on retries, so you can skip an event you already handled. The settings card can send a signed test event and lists the last deliveries.
When delivery fails
A 429, a 5xx or a network error is retried for about 30 minutes. After five other failures in a row the webhook is turned off and the dashboard says why. A long list is cut at 2,000 items and marked truncated; the full list of a run is on the API (Starter and above).
What breaks it
- Delete by the exact prefix or the listed values. Never purge by a bare
tw-: that also hits other runs. - Compact values have no dash after
tw, so a prefix match with the dash misses them. Use the listed values. - Older tests that build values from the clock report nothing until they’re regenerated.
The helpers and the webhook are on every plan, including Free.
Related
- Email verification
- Preview deploys
- Export your suite
- Docs: Deploy hooks and webhooks
- Docs: Test fixtures: inboxes and test data
Free: 5 tests, 500 runs a month, 2 apps and one daily schedule. Unlimited teammates, no card.