Write a test

Write tests with suites, steps, hooks and rows of data, and know the rules each test follows.

A test file ends in .retest.ts. Each test() gets a fresh page for the app it uses, and it passes when its checks pass.

tests/tasks.retest.ts
import { expect, test } from '@rehearsal-labs/retest'test('saves a task', async ({ page }) => {  await page.goto('/')  await page.getByLabel('Title').fill('Release checklist')  await page.getByRole('button', { name: 'Save' }).click()  await expect(page.getByTestId('saved-task')).toHaveText('Release checklist')})

Test options Link to Test options

test(name, options, fn) takes these options. All of them are optional, and so is the object itself.

OptionWhat it does
appsThe apps the test uses. The function then gets one page for each, by name, instead of page.
tagsTags that --tag selects the test by.
stateA saved sign-in state to start from. With several apps, one per app, such as { web: 'signed-in' }.
locksShared state outside the page that the test needs to itself.
timeoutThe test's own budget, in milliseconds.

Test names are unique in a file. Secrets and sign-in covers state, and Run tests in parallel covers locks.

Suites Link to Suites

test.describe(name, options, fn) groups tests. Its options pass down to every test inside, and its function gets a test that knows them. Blocks can nest.

TypeScript
test.describe('tasks', { tags: ['smoke'] }, (test) => {  test('saves a task', async ({ page }) => {    // ...  })})

The block's name joins the test's id, as in tests/tasks.retest.ts > tasks > saves a task. Reports, --grep and retest inspect all use that id.

Hooks Link to Hooks

test.beforeEach(fn) and test.afterEach(fn) run around each test in their file or block.

TypeScript
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()  })})
  • beforeEach hooks run outermost first, and afterEach hooks innermost first.
  • A beforeEach that fails skips the hooks after it and the test itself.
  • Every afterEach runs, even after a failure. Its own failure is added beside the test's first failure and never replaces it.

Rows of data Link to Rows of data

test.for(rows) declares one test for each row. Each $key in the name takes the row's value, and the function gets the row after the page.

TypeScript
test.for([{ title: 'Release checklist' }, { title: 'Groceries' }])('saves "$title"', async ({ page }, { title }) => {  await page.getByLabel('Title').fill(title)  await page.getByRole('button', { name: 'Save' }).click()  await expect(page.getByTestId('saved-task')).toHaveText(title)})

Two rows that make the same name stop the file from loading. To run one row, add its number after the line, as in tests/tasks.retest.ts:22#2.

Steps Link to Steps

test.step(name, fn) runs part of a test as a named step in the report. It returns what fn returns.

TypeScript
const title = await test.step('create the task', async () => {  await page.getByLabel('Title').fill('Release checklist')  await page.getByRole('button', { name: 'Save' }).click()  return 'Release checklist'})

Skip and only Link to Skip and only

  • test.skip(name, options, fn) declares a test that does not run. test.describe.skip does the same for a block. A skipped test is reported as skipped, never as a pass.
  • test.only(name, options, fn) and test.describe.only keep only the tests they mark. The report warns that the run checked less than the suite.
  • When the CI variable is set, a run with test.only stops before any test runs. Add --allow-only to run it anyway.

A run whose every chosen test is skipped checked nothing, so it exits with 2.

Rules every test follows Link to Rules every test follows

  • Await every action and every check. An app takes one command at a time.
  • A test fails when it makes no assertion, or when a check it did not await failed.
  • A test also fails when work it started is still running as it returns.
  • Each file runs in a process of its own, and the tests of one file run one after another.
  • A file is loaded more than once: to plan the run, then to run it. Code at the top of a file runs each time.
  • A test that runs out of time ends its file's process. The file's remaining tests do not run.

Tests in one process share module state. An error from a timer an earlier test started fails whichever test is running, and its message says so.