> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deep.space/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing reference

> The Playwright fixture and account helpers from `deepspace/testing`.

```ts theme={null}
import {
  test, expect,
  loadAllTestAccounts, pickTestAccounts, findTestAccountByName,
  ensureStorageState, newSignedInContext,
  getStatePathForEmail, readCachedState,
} from 'deepspace/testing'

import type {
  MultiplayerUser, UsersFixture, TestAccount, EnsureStorageStateOptions,
} from 'deepspace/testing'
```

Imported only inside Playwright spec files.

## `test` and `expect`

Re-exports from Playwright with the `users` fixture pre-installed:

```ts theme={null}
import { test, expect } from 'deepspace/testing'

test('A sends, B sees', async ({ users }) => {
  const [alice, bob] = await users(2)
  // ...
})
```

The fixture caches `storageState` per account, so each test account signs in once per machine - not once per test.

**Requires `baseURL`** in `tests/playwright.config.ts`. The scaffold sets this; tests error with `users fixture requires a baseURL` if it's missing.

## `users(N | string[], options?)` - the fixture

```ts theme={null}
type UsersFixture = (
  selector: number | string[],
  options?: { label?: string },
) => Promise<MultiplayerUser[]>

interface MultiplayerUser {
  context: BrowserContext
  page: Page
  email: string
  name: string
  /** Test account user ID, if known from the accounts registry. */
  userId?: string
}
```

```ts theme={null}
// First N accounts by createdAt
const [a, b] = await users(2)

// Specific accounts by name
const [alice, bob] = await users(['Alice', 'Bob'])

// First N filtered by label
const [team] = await users(1, { label: 'team-fixture' })
```

Contexts auto-close when the test finishes - no manual cleanup needed for contexts. You still need to clean up records you create during the test (see [Testing guide → Test data cleanup](/guides/testing#test-data-cleanup)).

## Escape hatches

When the fixture is too high-level, import the underlying helpers directly:

| Helper                                                    | Signature                                                                                       |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `loadAllTestAccounts()`                                   | `() => TestAccount[]` - every cached account (sync)                                             |
| `pickTestAccounts(n, opts?)`                              | `(n: number, opts?: { label?: string }) => TestAccount[]` (sync); throws if not enough accounts |
| `findTestAccountByName(name)`                             | `(name: string) => TestAccount` (sync); throws if not found                                     |
| `ensureStorageState(browser, account, baseURL, options?)` | Sign in once and return the cached `storageState` path                                          |
| `newSignedInContext(browser, account, baseURL, options?)` | One-liner for a signed-in `BrowserContext`                                                      |
| `getStatePathForEmail(email)`                             | Direct cache-path lookup for an email                                                           |
| `readCachedState(email)`                                  | Direct cache read by email                                                                      |

All three account loaders read from `~/.deepspace/test-accounts.json` synchronously - they don't return promises.

### `TestAccount`

```ts theme={null}
interface TestAccount {
  email: string
  password: string
  name?: string
  label?: string | null
  id?: string
  userId?: string
  createdAt?: number
}
```

Loaded from `~/.deepspace/test-accounts.json`, populated by `npx deepspace test-accounts create`. `createdAt` is a numeric epoch millisecond timestamp, not an ISO string.

### `EnsureStorageStateOptions`

```ts theme={null}
interface EnsureStorageStateOptions {
  /** Max age of a cached state file before we re-sign in. Default 7 days. */
  maxAgeMs?: number
  /** Force a fresh sign-in even if the cache is fresh. */
  force?: boolean
}
```

### `ensureStorageState` / `newSignedInContext` signatures

```ts theme={null}
function ensureStorageState(
  browser: Browser,
  account: { email: string; password: string },
  baseURL: string,
  options?: EnsureStorageStateOptions,
): Promise<string>

function newSignedInContext(
  browser: Browser,
  account: { email: string; password: string },
  baseURL: string,
  options?: EnsureStorageStateOptions,
): Promise<BrowserContext>
```

`browser` is always the first argument, then the account record, then `baseURL`, then options.

## Patterns

### Multiplayer test

```ts theme={null}
import { test, expect } from 'deepspace/testing'

test('shared state syncs', async ({ users }) => {
  const [a, b] = await users(2)
  await a.page.goto('/board')
  await b.page.goto('/board')

  await a.page.getByTestId('add-card-btn').click()
  await a.page.getByTestId('card-title-input').fill('Hello')
  await a.page.getByTestId('save-card-btn').click()

  await expect(b.page.getByText('Hello')).toBeVisible()
})
```

### Reusing a signed-in context outside the fixture

```ts theme={null}
import { test } from '@playwright/test'
import { ensureStorageState, loadAllTestAccounts } from 'deepspace/testing'

test('custom flow', async ({ browser }) => {
  const accounts = loadAllTestAccounts()
  const statePath = await ensureStorageState(browser, accounts[0], 'http://localhost:5173')
  const context = await browser.newContext({ storageState: statePath })
  // ... use context.newPage(), etc.
  await context.close()
})
```

## See also

* [Testing guide](/guides/testing) - full workflow, test extension checklist, debugging
* [CLI test command](/cli-reference/commands#test) - runner flags
* [CLI test-accounts command](/cli-reference/commands#test-accounts) - provisioning the account pool
