# Write a test

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

Source: https://rehearsal.dev/retest/essentials/write-a-test

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.

```ts filename="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

`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](https://rehearsal.dev/retest/essentials/secrets-and-sign-in.md) covers `state`, and [Run tests in parallel](https://rehearsal.dev/retest/scaling-up/parallel-runs.md) covers `locks`.

## 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.

```ts
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

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

```ts
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

`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.

```ts
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

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

```ts
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

- `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

- 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.

> [!NOTE]
> 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.
