Skip to main content

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.

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

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
}
// 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).

Escape hatches

When the fixture is too high-level, import the underlying helpers directly:
HelperSignature
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

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

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

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

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

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