Best practices
Choose stable locators, wait for outcomes and keep test data separate as your suite grows.
Use these patterns as you add tests that share an app, account or backend. Check each flow's outcome, wait for conditions and prepare the data it needs so another test cannot change its result.
Check the outcome Link to Check the outcome
Check the saved result after clicking Save, because the click alone does not prove what the app kept. This test catches a save that drops a character by comparing the displayed title with the value it entered.
import { expect, test } from '@rehearsal-labs/retest'test('saves a task', async ({ page }) => { await page.goto('/') await page.getByTestId('task-title').fill('Release checklist') await page.getByTestId('save-task').click() await expect(page.getByTestId('saved-task')).toHaveText('Release checklist') await expect(page.getByTestId('task-title')).toHaveValue('Release checklist')})The first check verifies the saved result, and the second verifies what remains in the field. Keeping them separate makes a failure name the fact that changed. For independent state checks, expect.soft can record a mismatch and continue; the test still fails.
Choose stable locators Link to Choose stable locators
Use test ids for controls whose labels change with language or copy. Role and name locators are useful when the accessible name is part of what you want to test. Keep a locator specific enough to identify one control.
import { chrome, defineConfig } from '@rehearsal-labs/retest'const config = defineConfig({ apps: { web: chrome({ baseUrl: 'http://127.0.0.1:4173' }) }, testIds: ['task-title', 'save-task', 'saved-task'],})export default configdeclare module '@rehearsal-labs/retest' { interface Register { config: typeof config }}Registering this config makes a misspelt test id a TypeScript error. The list affects type checks only, so run the generated typecheck:e2e script as well as Retest. Locators covers scoping and ambiguous matches.
Wait for a condition Link to Wait for a condition
Wait for the result you need instead of guessing how long the app takes. Locator and page checks already repeat their reads until they pass or reach a deadline. For server state, use expect.poll with a function that only reads.
const before = await savedTasks()await page.getByTestId('task-title').fill('Release checklist')await page.getByTestId('save-task').click()await expect.poll(savedTasks, { timeout: 5000 }).toBe(before + 1)Here savedTasks is your function that reads the API's save count. Polling it cannot submit the form again. If other tests change that count, give every saving test the same lock, or give the test its own data.
Prepare data and sign-in Link to Prepare data and sign-in
A fresh browser context separates cookies and browser storage, but does not reset the database. Create records the test owns, and use the id the app assigns when you need to find them again. A random title helps avoid collisions, but does not isolate a shared account or counter.
For browser tests, save sign-in with test.setup and restore its state when the test checks something after login. Keep a separate test without that state for the sign-in form itself. Give passwords through config secrets and secret(), rather than writing values in test source.
Use an AI check for a judgement about wording or an image that an exact matcher cannot express. Keep exact facts, such as titles, totals and addresses, in expect. A screenshot cannot prove that the server saved a record; check server state when that fact matters.
Keep the run complete Link to Keep the run complete
Await every action and asynchronous check. Keep setup inside tests or hooks because Retest loads test files more than once, and finish callbacks before a test ends. Tests in one file share a process, so an unfinished timer can affect the next test.
Remove test.only before CI; Retest refuses it there unless explicitly allowed. Investigate a non-zero exit instead of treating an undecided or incomplete run as a pass. Retest never retries a test automatically, so a later successful run does not erase the first result.