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.
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.
| Option | What it does |
|---|---|
apps | The apps the test uses. The function then gets one page for each, by name, instead of page. |
tags | Tags that --tag selects the test by. |
state | A saved sign-in state to start from. With several apps, one per app, such as { web: 'signed-in' }. |
locks | Shared state outside the page that the test needs to itself. |
timeout | The 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.
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.
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() })})beforeEachhooks run outermost first, andafterEachhooks innermost first.- A
beforeEachthat fails skips the hooks after it and the test itself. - Every
afterEachruns, 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.
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.
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.skipdoes the same for a block. A skipped test is reported as skipped, never as a pass.test.only(name, options, fn)andtest.describe.onlykeep only the tests they mark. The report warns that the run checked less than the suite.- When the
CIvariable is set, a run withtest.onlystops before any test runs. Add--allow-onlyto 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.