---
title: Testing
description: Tests of your whole app, written as Playwright writes them and run by Sluurp itself, in the admin or headless, with coverage.
section: Development
order: 1
---

# Testing

<p class="lead">Put tests in your app's <code>tests/</code> folder and Sluurp runs them. You can run them in the admin, where you watch each one drive your app, or headless with <code>sluurp test</code> for CI. There's no Node and no Playwright to install; Sluurp uses the Chrome or Edge already on your machine.</p>

## A test

```tsx title="tests/notes.test.tsx"
import { expect, test } from "sluurp/test";

test("a note is added", async ({ page }) => {
  await page.goto("/notes");
  await page.getByLabel("Title").fill("Milk");
  await page.getByRole("button", { name: "Add" }).click();
  await expect(page.getByText("Milk")).toBeVisible();
});

test("only a signed-in person may list notes", async ({ request, admin }) => {
  expect((await request.get("/api/collections/notes/records")).status).toBe(403);
  expect((await admin.get("/api/collections/notes/records")).ok).toBe(true);
});
```

Each test gets three things:

- `page`: your app in a fresh frame, driven as a person would. It has `goto`, `reload` and `locator`, plus `getByRole`, `getByText`, `getByLabel`, `getByPlaceholder` and `getByTestId`. What those return has `click`, `fill`, `press`, `check`, `hover`, `textContent` and more.
- `request`: your API, as someone who isn't signed in.
- `admin`: your API as a superuser, for setting data up.

Every action waits until its element is on the page and visible. Inside an island, it also waits until the island has woken up.

`expect` on a locator or the page retries until the check holds, for up to 5 seconds, as in Playwright. The checks are `toBeVisible`, `toBeHidden`, `toHaveText`, `toContainText`, `toHaveCount`, `toHaveValue`, `toHaveAttribute`, `toBeChecked`, `toBeEnabled`, `toBeDisabled`, `toHaveURL` and `toHaveTitle`. On any other value, `expect` checks once. `expect.poll(() => value)` asks again until the check holds.

Group tests with `test.describe`, set things up with `test.beforeEach`, `test.afterEach`, `test.beforeAll` and `test.afterAll`, and narrow a run with `test.only` and `test.skip`.

## In the admin

**Tests** in the admin's sidebar lists your test files and runs them. Each test's frame is shown while it runs, and you can choose a result to see its steps and what failed. Turn on **Auto-run** and a test file you save runs again at once, without the page reloading. A change to the app itself runs every test again.

## Headless

```sh title="Terminal"
sluurp test --public app
```

Your app is served on a fresh database, and its tests run in a headless Chrome, Edge or Chromium. Results are printed as they come, and a failed test makes the command exit with code 1, for CI. Use `--grep` to run only the tests whose name contains a word, and `--browser-path` or `SLUURP_BROWSER` to name the browser.

## Coverage

```sh title="Terminal"
sluurp test --public app --coverage
```

This reports which lines of your app's code the tests ran in the browser, file by file. It also writes `coverage/lcov.info`, which coverage services and editors can read. Pages drawn on the server run on the server, so they aren't measured in the browser.

## Components on their own

A component's stories are tests too: each story's `play` runs in the UI kit's Test tab. See [UI kit](/docs/ui-kit).
