Writing tests
Write a test that checks an outcome, then organise related tests with groups, hooks and rows of data.
Write one test for a complete user flow, such as saving a task and seeing its title. As the suite grows, use groups, hooks and data rows to organise related tests.
Write a test Link to Write a test
Put your tests in files ending in .retest.ts and import test and expect from Retest. The test below uses the task app from Retest's examples, with its address set in Configuration.
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('saved-task')).toBeVisible()})page is the default app from your config. After opening the home page, the test enters and saves a title, then checks the saved text and visibility. toHaveText can read hidden elements, so toBeVisible checks that the saved task is visible too.
Each test gets fresh browser contexts and pages, so cookies and browser storage from another test do not carry over. This does not reset your server's data. If tests share an account or a server counter, see Parallel runs.
Await the work Link to Await the work
Await each action and each asynchronous check so they finish in the order you wrote them. An app takes one command at a time, so starting two actions together on the same page fails the test.
An unawaited action or check fails the test even if it would succeed. Asynchronous checks start only when awaited, so leaving out await can leave the app unchecked. A test also fails if it makes no assertions or returns with work still running.
Group related tests Link to Group related tests
Use test.describe when several tests cover the same part of your app. Its callback receives a test that inherits the group's options, and hooks on that test apply to the tests inside the group.
import { expect, test } from '@rehearsal-labs/retest'test.describe('tasks', (test) => { test.beforeEach(async ({ page }) => { await page.goto('/') await expect(page.getByRole('heading', { name: 'Tasks' })).toBeVisible() }) test.afterEach(async ({ page }) => { await expect(page.getByText('Could not save')).toBeHidden() }) test('saves a task', async ({ page }) => { await page.getByTestId('task-title').fill('Release checklist') await page.getByTestId('save-task').click() await expect(page.getByTestId('saved-task')).toHaveText('Release checklist') })})beforeEach opens the app and checks that the task screen is ready before each test. afterEach checks for a save error after an ordinary failure too, but does not run when the whole test runs out of time. A failed beforeEach prevents the body from running; an afterEach failure stays beside the original failure rather than replacing it.
Groups can nest. Setup hooks run from the outer group inward, while teardown hooks run from the inner group outward. Reports identify the test as tests/tasks.retest.ts > tasks > saves a task; --grep searches only the title path, tasks > saves a task, without the file name.
Test several values Link to Test several values
When the same behaviour needs several inputs, test.for declares a separate test for each row. Put $title in the name to include that row's title in the report.
test.for([ { title: 'Release checklist' }, { title: 'Groceries' },])('saves "$title"', async ({ page }, { title }) => { await page.goto('/') await page.getByTestId('task-title').fill(title) await page.getByTestId('save-task').click() await expect(page.getByTestId('saved-task')).toHaveText(title)})The callback receives the usual context first and the row second. Each row gets its own result and fresh page. Choose values that produce distinct names, because two rows with the same name stop the file from loading.
Name a part of a test Link to Name a part of a test
For a longer flow, test.step gives a sequence of actions a name in the report. It returns the callback's result, so you can use a value from the step in a later check.
const title = await test.step('Create the task', async () => { await page.getByTestId('task-title').fill('Release checklist') await page.getByTestId('save-task').click() return 'Release checklist'})await expect(page.getByTestId('saved-task')).toHaveText(title)This belongs inside a test after opening the page. Naming the step helps you find it in the timeline, but you still need the check afterwards to prove its outcome.
Set test options Link to Set test options
An options object between the name and callback can add tags or change the test's timeout. If your config lists allowed tags, include smoke there for this example.
test('saves a task', { tags: ['smoke'], timeout: 30_000 }, 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')})Use apps when a test needs named app handles, state to restore a saved sign-in, and locks to protect shared data. The Test API lists all options and which ones groups inherit.
test.skip declares a test that is reported as skipped, while test.only focuses a local run on the tests it marks. A run in continuous integration refuses only unless you pass --allow-only. To choose tests without editing them, use the filters in Running tests.