noboil

Testing

Unit tests, integration tests, and E2E testing strategies.

Setup

Install convex-test:

bun add -d convex-test

Set CONVEX_TEST_MODE=true when running tests:

{
  "scripts": {
    "test": "CONVEX_TEST_MODE=true bun with-env bun test"
  }
}

Create a test auth helper in convex/testAuth.ts:

import { makeTestAuth } from 'noboil/convex/test'
import { getAuthUserId } from '@convex-dev/auth/server'
import { mutation, query } from './_generated/server'

const t = makeTestAuth({ getAuthUserId, mutation, query })
export const {
  ensureTestUser,
  getTestUser,
  cleanupTestUsers,
  getAuthUserIdOrTest
} = t

All tests run against a local SpacetimeDB instance. Start it with Docker:

docker compose up -d

Wait for it to be healthy:

docker compose ps

Publish your module before running integration tests. Use a separate module name for tests so they don't interfere with your dev module:

spacetime publish my-app-test --module-path backend/spacetimedb/

Set test env vars before running helper-based tests:

export SPACETIMEDB_MODULE_NAME=my-app-test
export SPACETIMEDB_TEST_MODE=true

Optional deterministic token fallback when cookie auth is unavailable in Next.js server helpers:

export SPACETIMEDB_TEST_TOKEN=test-token-local

Unit tests

import { convexTest } from 'convex-test'
import { describe, expect, test } from 'bun:test'
import schema from './schema'
import { api } from './_generated/api'

const modules = {
  './_generated/api.js': async () => import('./_generated/api'),
  './_generated/server.js': async () => import('./_generated/server'),
  './blog.ts': async () => import('./blog')
}

describe('blog CRUD', () => {
  test('create and read a blog post', async () => {
    const ctx = convexTest(schema, modules)
    const userId = await ctx.run(async c =>
      c.db.insert('users', {
        email: 'test@example.com',
        emailVerificationTime: Date.now()
      })
    )
    const asUser = ctx.withIdentity({
      subject: userId,
      tokenIdentifier: `test|${userId}`
    })
    const postId = await asUser.mutation(api.blog.create, {
      title: 'Hello',
      content: 'World',
      category: 'tech',
      published: true
    })
    const post = await asUser.query(api.blog.read, { id: postId })
    expect(post?.title).toBe('Hello')
  })
})

Bun's built-in test runner works for testing pure logic, schema utilities, and server-side helpers:

import { describe, expect, it } from 'bun:test'
import { zodFromTable } from 'noboil/spacetimedb'
import { tables } from '../module_bindings'

describe('zodFromTable', () => {
  it('generates a schema from a table definition', () => {
    const schema = zodFromTable(tables.post.columns)
    const result = schema.safeParse({
      title: 'Hello',
      content: 'World',
      published: false
    })
    expect(result.success).toBe(true)
  })

  it('excludes auto-increment and identity fields by default', () => {
    const schema = zodFromTable(tables.post.columns)
    expect(Object.keys(schema.shape)).not.toContain('id')
    expect(Object.keys(schema.shape)).not.toContain('userId')
  })
})

Run unit tests:

bun test backend/spacetimedb/__tests__/

Integration tests

convex-test runs your Convex functions in a local in-memory environment. No network calls, no deployed backend needed.

describe('blog CRUD', () => {
  test('create and read a blog post', async () => {
    const ctx = convexTest(schema, modules)
    const userId = await ctx.run(async c =>
      c.db.insert('users', {
        email: 'test@example.com',
        emailVerificationTime: Date.now()
      })
    )
    const asUser = ctx.withIdentity({
      subject: userId,
      tokenIdentifier: `test|${userId}`
    })
    const postId = await asUser.mutation(api.blog.create, {
      title: 'Hello',
      content: 'World',
      category: 'tech',
      published: true
    })
    const post = await asUser.query(api.blog.read, { id: postId })
    expect(post?.title).toBe('Hello')
  })
})

Integration tests connect to a real SpacetimeDB instance and call reducers.

Test helper: connectAsTestUser

SpacetimeDB assigns a stable Identity when you reconnect with a saved token. Use this to create deterministic test users:

import { DbConnection } from '../module_bindings'

const TOKEN_CACHE = new Map<string, string>()

export const connectAsTestUser = async (
  name: string
): Promise<DbConnection> => {
  const savedToken = TOKEN_CACHE.get(name)

  return new Promise((resolve, reject) => {
    const builder = DbConnection.builder()
      .withUri('ws://localhost:4200')
      .withModuleName(process.env.SPACETIMEDB_MODULE_NAME ?? 'my-app-test')

    if (savedToken) {
      builder.withToken(savedToken)
    }

    builder
      .onConnect((conn, identity, token) => {
        TOKEN_CACHE.set(name, token)
        resolve(conn)
      })
      .onError(reject)
      .build()
  })
}

Writing integration tests

import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import { connectAsTestUser } from '../test-helpers/connect'
import { reducers, tables } from '../module_bindings'

describe('blog CRUD', () => {
  let conn: Awaited<ReturnType<typeof connectAsTestUser>>

  beforeEach(async () => {
    conn = await connectAsTestUser('alice')
  })

  afterEach(() => {
    conn.disconnect()
  })

  it('creates a post', async () => {
    await conn.reducers.create_post({
      title: 'Test post',
      content: 'Test content',
      published: false
    })

    await new Promise(resolve => setTimeout(resolve, 100))

    const posts = [...conn.db.post.iter()]
    const created = posts.find(p => p.title === 'Test post')
    expect(created).toBeDefined()
    expect(created?.published).toBe(false)
  })
})

Testing org-scoped endpoints

makeOrgTestCrud creates test helpers for org tables with membership and ACL checks:

import { makeOrgTestCrud } from 'noboil/convex/test'

export const wikiTest = makeOrgTestCrud({
  acl: true,
  mutation,
  query,
  table: 'wiki'
})
const orgId = await ctx.run(async c =>
  c.db.insert('org', {
    name: 'Acme',
    slug: 'acme',
    updatedAt: Date.now(),
    userId: ownerId
  })
)
const memberId = await ctx.run(async c =>
  c.db.insert('orgMember', {
    isAdmin: false,
    orgId,
    updatedAt: Date.now(),
    userId: memberUserId
  })
)

let threw = false
try {
  await asMember.mutation(api.wiki.update, {
    id: wikiId,
    orgId,
    title: 'Hacked'
  })
} catch (error) {
  threw = true
  expect(String(error)).toContain('EDITOR_REQUIRED')
}
expect(threw).toBe(true)

Test org-scoped endpoints by connecting as different users:

it('rejects update from non-member', async () => {
  const alice = await connectAsTestUser('alice')
  const bob = await connectAsTestUser('bob')

  await alice.reducers.create_org({ name: 'Acme', slug: 'acme' })
  await new Promise(resolve => setTimeout(resolve, 100))

  const orgs = [...alice.db.org.iter()]
  const org = orgs.find(o => o.slug === 'acme')
  expect(org).toBeDefined()

  await alice.reducers.create_project({ name: 'Project A', orgId: org!.id })
  await new Promise(resolve => setTimeout(resolve, 100))

  const projects = [...alice.db.project.iter()]
  const project = projects.find(p => p.name === 'Project A')
  expect(project).toBeDefined()

  await expect(
    bob.reducers.update_project({ id: project!.id, name: 'Hacked', orgId: org!.id })
  ).rejects.toThrow()

  alice.disconnect()
  bob.disconnect()
})

Testing auth

Test both authorization (wrong user) and authentication (no user) failures:

test('update fails on non-owned document', async () => {
  const ctx = convexTest(schema, modules)
  const owner = await ctx.run(async c =>
    c.db.insert('users', {
      email: 'owner@test.com',
      emailVerificationTime: Date.now()
    })
  )
  const other = await ctx.run(async c =>
    c.db.insert('users', {
      email: 'other@test.com',
      emailVerificationTime: Date.now()
    })
  )

  const asOwner = ctx.withIdentity({
    subject: owner,
    tokenIdentifier: `test|${owner}`
  })
  const asOther = ctx.withIdentity({
    subject: other,
    tokenIdentifier: `test|${other}`
  })

  const id = await asOwner.mutation(api.blog.create, {
    title: 'My Post',
    content: 'Content',
    category: 'tech',
    published: true
  })

  let threw = false
  try {
    await asOther.mutation(api.blog.update, { id, title: 'Hacked' })
  } catch (error) {
    threw = true
    expect(String(error)).toContain('NOT_FOUND')
  }
  expect(threw).toBe(true)
})

test('unauthenticated access throws', async () => {
  const ctx = convexTest(schema, modules)
  let threw = false
  try {
    await ctx.mutation(api.blog.create, {
      title: 'No Auth',
      content: 'Content',
      category: 'tech',
      published: true
    })
  } catch (error) {
    threw = true
    expect(String(error)).toContain('NOT_AUTHENTICATED')
  }
  expect(threw).toBe(true)
})

SpacetimeDB uses anonymous connections for local testing. Each connection gets a unique Identity. Reconnecting with the same token gives the same Identity.

const conn1 = await connectAsTestUser('alice')

conn1.disconnect()
const conn2 = await connectAsTestUser('alice')

For tests that need multiple users, call connectAsTestUser with different names:

const alice = await connectAsTestUser('alice')
const bob = await connectAsTestUser('bob')

Test that a non-owner can't update another user's post:

it('rejects update from non-owner', async () => {
  const alice = await connectAsTestUser('alice')
  await alice.reducers.create_post({
    title: 'Alice post',
    content: 'Content',
    published: false
  })
  await new Promise(resolve => setTimeout(resolve, 100))

  const posts = [...alice.db.post.iter()]
  const post = posts.find(p => p.title === 'Alice post')
  expect(post).toBeDefined()

  const bob = await connectAsTestUser('bob')
  await expect(
    bob.reducers.update_post({ id: post!.id, title: 'Hacked' })
  ).rejects.toThrow()

  alice.disconnect()
  bob.disconnect()
})

Testing soft delete and restore

Tables with softDelete: true don't delete documents — they set deletedAt. The restore endpoint reverses this.

test('soft delete and restore', async () => {
  const ctx = convexTest(schema, modules)
  const userId = await ctx.run(async c =>
    c.db.insert('users', {
      email: 'test@example.com',
      emailVerificationTime: Date.now()
    })
  )
  const asUser = ctx.withIdentity({
    subject: userId,
    tokenIdentifier: `test|${userId}`
  })

  const id = await asUser.mutation(api.wiki.create, {
    orgId,
    slug: 'test',
    status: 'draft',
    title: 'Test'
  })

  await asUser.mutation(api.wiki.rm, { id, orgId })

  const deleted = await asUser.query(api.wiki.read, { id, orgId })
  expect(deleted.deletedAt).toBeDefined()

  await asUser.mutation(api.wiki.restore, { id, orgId })

  const restored = await asUser.query(api.wiki.read, { id, orgId })
  expect(restored.deletedAt).toBeUndefined()
})
it('soft delete and restore', async () => {
  const conn = await connectAsTestUser('alice')

  await conn.reducers.create_wiki({
    title: 'Test wiki',
    slug: 'test',
    orgId: org.id
  })
  await new Promise(resolve => setTimeout(resolve, 100))

  const wikis = [...conn.db.wiki.iter()]
  const wiki = wikis.find(w => w.slug === 'test')
  expect(wiki).toBeDefined()

  await conn.reducers.rm_wiki({ id: wiki!.id, orgId: org.id })
  await new Promise(resolve => setTimeout(resolve, 100))

  const deleted = [...conn.db.wiki.iter()].find(w => w.id === wiki!.id)
  expect(deleted?.deletedAt).toBeDefined()

  await conn.reducers.restore_wiki({ id: wiki!.id, orgId: org.id })
  await new Promise(resolve => setTimeout(resolve, 100))

  const restored = [...conn.db.wiki.iter()].find(w => w.id === wiki!.id)
  expect(restored?.deletedAt).toBeUndefined()

  conn.disconnect()
})

Testing rate limiting

Rate limiting is skipped when CONVEX_TEST_MODE=true. To test rate limits, either unset the env var or test against a deployed backend.

test('rate limit blocks excessive requests', async () => {
  const ctx = convexTest(schema, modules)
  const userId = await ctx.run(async c =>
    c.db.insert('users', {
      email: 'test@example.com',
      emailVerificationTime: Date.now()
    })
  )
  const asUser = ctx.withIdentity({
    subject: userId,
    tokenIdentifier: `test|${userId}`
  })

  for (let i = 0; i < 10; i++) {
    await asUser.mutation(api.blog.create, {
      title: `Post ${String(i)}`,
      content: 'Content',
      category: 'tech',
      published: true
    })
  }

  let threw = false
  try {
    await asUser.mutation(api.blog.create, {
      title: 'One too many',
      content: 'Content',
      category: 'tech',
      published: true
    })
  } catch (error) {
    threw = true
    expect(String(error)).toContain('RATE_LIMITED')
  }
  expect(threw).toBe(true)
})

This test only works when CONVEX_TEST_MODE is NOT set. isTestMode() bypasses rate limits, so the 11th request will succeed in test mode.

Rate limiting tests run against the real SpacetimeDB instance:

it('rate limit blocks excessive requests', async () => {
  const conn = await connectAsTestUser('alice')

  for (let i = 0; i < 10; i++) {
    await conn.reducers.create_post({
      title: `Post ${String(i)}`,
      content: 'Content',
      published: false
    })
  }

  await expect(
    conn.reducers.create_post({
      title: 'One too many',
      content: 'Content',
      published: false
    })
  ).rejects.toThrow()

  conn.disconnect()
})

Search tests require the searchIndex to be defined in your schema. convex-test supports search indexes — results match the same behavior as production.

test('search returns matching results', async () => {
  const ctx = convexTest(schema, modules)
  const userId = await ctx.run(async c =>
    c.db.insert('users', {
      email: 'test@example.com',
      emailVerificationTime: Date.now()
    })
  )
  const asUser = ctx.withIdentity({
    subject: userId,
    tokenIdentifier: `test|${userId}`
  })

  await asUser.mutation(api.blog.create, {
    title: 'TypeScript Guide',
    content: 'Learn TypeScript basics',
    category: 'tech',
    published: true
  })
  await asUser.mutation(api.blog.create, {
    title: 'Cooking Tips',
    content: 'Best pasta recipes',
    category: 'life',
    published: true
  })

  const results = await asUser.query(api.blog.search, { query: 'TypeScript' })
  expect(results.length).toBe(1)
  expect(results[0]?.title).toBe('TypeScript Guide')
})

Search is client-side via useList. Test it by filtering the subscription data directly:

import { describe, expect, it } from 'bun:test'
import { zodFromTable } from 'noboil/spacetimedb'
import { useList } from 'noboil/spacetimedb/react'
import { tables } from '../module_bindings'

describe('useList search', () => {
  it('filters by search query', () => {
    const posts = [
      { id: 1, title: 'TypeScript Guide', content: 'Learn TypeScript', published: true },
      { id: 2, title: 'Cooking Tips', content: 'Best pasta recipes', published: true },
    ]

    const { data } = useList(posts, true, {
      search: { query: 'TypeScript', fields: ['title', 'content'] }
    })

    expect(data.length).toBe(1)
    expect(data[0]?.title).toBe('TypeScript Guide')
  })
})

Testing conflict detection

expectedUpdatedAt enables optimistic concurrency control.

test('concurrent edit triggers conflict', async () => {
  const ctx = convexTest(schema, modules)
  const userId = await ctx.run(async c =>
    c.db.insert('users', {
      email: 'test@example.com',
      emailVerificationTime: Date.now()
    })
  )
  const asUser = ctx.withIdentity({
    subject: userId,
    tokenIdentifier: `test|${userId}`
  })

  const id = await asUser.mutation(api.blog.create, {
    title: 'Original',
    content: 'Content',
    category: 'tech',
    published: true
  })
  const post = await asUser.query(api.blog.read, { id })
  const staleTimestamp = post?.updatedAt

  await asUser.mutation(api.blog.update, { id, title: 'Updated by user A' })

  let threw = false
  try {
    await asUser.mutation(api.blog.update, {
      id,
      title: 'Updated by user B',
      expectedUpdatedAt: staleTimestamp
    })
  } catch (error) {
    threw = true
    expect(String(error)).toContain('CONFLICT')
  }
  expect(threw).toBe(true)
})
it('concurrent edit triggers conflict', async () => {
  const conn = await connectAsTestUser('alice')

  await conn.reducers.create_post({
    title: 'Original',
    content: 'Content',
    published: false
  })
  await new Promise(resolve => setTimeout(resolve, 100))

  const posts = [...conn.db.post.iter()]
  const post = posts.find(p => p.title === 'Original')
  expect(post).toBeDefined()

  const staleUpdatedAt = post!.updatedAt

  await conn.reducers.update_post({ id: post!.id, title: 'Updated by user A' })
  await new Promise(resolve => setTimeout(resolve, 100))

  await expect(
    conn.reducers.update_post({
      id: post!.id,
      title: 'Updated by user B',
      expectedUpdatedAt: staleUpdatedAt
    })
  ).rejects.toThrow()

  conn.disconnect()
})

Testing log / kv / quota factories

The new factories follow the same convexTest / stdb integration patterns. See poll demo and backend/convex/convex/f.test.ts for full examples.

test('log: append + list returns rows in order', async () => {
  const ctx = t()
  const { asUser } = await createTestContext(ctx)
  const parent = 'poll-1'
  await asUser(0).mutation(api.vote.append, { parent, payload: { optionIdx: 0, voter: 'a' } })
  await asUser(0).mutation(api.vote.append, { parent, payload: { optionIdx: 1, voter: 'b' } })
  const { page } = await asUser(0).query(api.vote.list, {
    parent,
    paginationOpts: { cursor: null, numItems: 100 }
  })
  expect(page.length).toBe(2)
})
test('kv: set + restore round-trip', async () => {
  const ctx = t()
  const { asUser } = await createTestContext(ctx)
  await asUser(0).mutation(api.siteConfig.set, { key: 'banner', payload: { active: true, message: 'hi' } })
  await asUser(0).mutation(api.siteConfig.rm, { key: 'banner' })
  expect(await asUser(0).query(api.siteConfig.get, { key: 'banner' })).toBeNull()
  await asUser(0).mutation(api.siteConfig.restore, { key: 'banner' })
  const back = await asUser(0).query(api.siteConfig.get, { key: 'banner' })
  expect(back?.message).toBe('hi')
})
test('quota: exhausting limit returns allowed=false', async () => {
  const ctx = t()
  const { asUser } = await createTestContext(ctx)
  for (let i = 0; i < 30; i += 1) await asUser(0).mutation(api.pollVoteQuota.consume, { owner: 'p1' })
  const result = await asUser(0).mutation(api.pollVoteQuota.consume, { owner: 'p1' })
  expect(result.allowed).toBe(false)
})
test('vote (log) append + listed by parent', async () => {
  await withCtx(async ctx => {
    const [user] = ctx.users
    const parent = 'pollx'
    await callReducer(ctx, 'append_vote', { idempotency_key: none, option: 'a', parent }, user)
    const rows = await listTable(ctx, 'vote', user)
    const mine = rows.filter(r => getString(r, 'parent') === parent)
    expect(mine.length).toBeGreaterThanOrEqual(1)
  })
})
test('quota: consume + exhaust', async () => {
  await withCtx(async ctx => {
    const [user] = ctx.users
    for (let i = 0; i < 30; i += 1)
      await callReducer(ctx, 'consume_pollVoteQuota', { owner: 'p1' }, user)
    await expect(
      callReducer(ctx, 'consume_pollVoteQuota', { owner: 'p1' }, user)
    ).rejects.toThrow()
  })
})

E2E tests with Playwright

E2E tests run against the full stack.

import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  timeout: 10_000,
  use: {
    baseURL: 'http://localhost:4110'
  },
  webServer: {
    command: 'bun dev',
    url: 'http://localhost:4110',
    reuseExistingServer: !process.env.CI
  }
})

Deploy Convex before running tests:

CONVEX_TEST_MODE=true bun with-env convex dev --once
import { defineConfig } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  timeout: 10_000,
  use: {
    baseURL: 'http://localhost:4210'
  },
  webServer: {
    command: 'bun dev',
    url: 'http://localhost:4210',
    reuseExistingServer: !process.env.CI
  }
})

Start SpacetimeDB and publish the module before running tests:

docker compose up -d
SPACETIMEDB_TEST_MODE=true bun spacetime:publish

Writing E2E tests

import { expect, test } from '@playwright/test'

test('creates and displays a post', async ({ page }) => {
  await page.goto('/')

  await page.waitForSelector('[data-testid="post-list"]')

  await page.click('[data-testid="new-post-button"]')
  await page.fill('[name="title"]', 'E2E test post')
  await page.fill('[name="content"]', 'Test content')
  await page.click('[type="submit"]')

  await expect(page.getByText('E2E test post')).toBeVisible()
})

test('deletes a post', async ({ page }) => {
  await page.goto('/')
  await page.waitForSelector('[data-testid="post-list"]')

  const postTitle = `Delete test ${Date.now()}`
  await page.click('[data-testid="new-post-button"]')
  await page.fill('[name="title"]', postTitle)
  await page.fill('[name="content"]', 'To be deleted')
  await page.click('[type="submit"]')

  await expect(page.getByText(postTitle)).toBeVisible()

  await page.click(`[data-testid="delete-${postTitle}"]`)

  await expect(page.getByText(postTitle)).not.toBeVisible()
})

Running E2E tests

timeout 30 bun playwright test e2e/blog.test.ts --timeout=8000

bun test:e2e

Test isolation

convex-test creates a fresh in-memory database for each test context. Tests are isolated by default — no cleanup needed between tests.

SpacetimeDB doesn't have built-in per-test database reset. Options:

  1. Use unique identifiers: prefix test data with a timestamp or UUID so tests don't collide.
  2. Clean up in afterEach: call rm_* reducers to delete test data after each test.
  3. Use a fresh module: publish a new module name for each test run (slower but fully isolated).
const testId = Date.now()
await conn.reducers.create_post({
  title: `Test post ${testId}`,
  content: 'Content',
  published: false
})

For persistent tokens across runs, save them to a file:

import { readFileSync, writeFileSync } from 'fs'

const TOKEN_FILE = '.test-tokens.json'

const loadTokens = (): Record<string, string> => {
  try {
    return JSON.parse(readFileSync(TOKEN_FILE, 'utf-8')) as Record<string, string>
  } catch {
    return {}
  }
}

const saveTokens = (tokens: Record<string, string>) => {
  writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2))
}

Running all tests

bun test:all

This runs unit tests and E2E tests in parallel. All tests must pass before pushing.

Test inventory

Auto-generated breakdown of every *.test.ts in lib/noboil/. Counts come from static describe/test/it regex scans, so they reflect declared assertions, not runtime pass/fail.

2998 tests across 104 files (548 describe blocks)

Filedescribetest/it
src/__tests__/bin-smoke.test.ts316
src/__tests__/cli-utils.test.ts11
src/__tests__/doctor-fix.test.ts11
src/__tests__/help-commands.test.ts18
src/__tests__/init.test.ts11
src/__tests__/scaffold-ops.test.ts110
src/convex/__tests__/audit.test.ts12
src/convex/__tests__/budget.property.test.ts14
src/convex/__tests__/budget.synthetic.test.ts112
src/convex/__tests__/budget.test.ts26
src/convex/__tests__/builder.test.ts410
src/convex/__tests__/devtools.test.ts15
src/convex/__tests__/docs-gen.test.ts36
src/convex/__tests__/doctor.test.ts15
src/convex/__tests__/eslint-plugin.test.ts15
src/convex/__tests__/eslint-smoke.test.ts15
src/convex/__tests__/manifest.test.ts125
src/convex/__tests__/optimistic-store.test.tsx13
src/convex/__tests__/pure.test.ts162967
src/convex/__tests__/use-bulk-mutate.test.tsx15
src/convex/__tests__/use-form.test.tsx12
src/convex/__tests__/use-list.test.tsx25
src/convex/__tests__/use-mutate.test.ts14
src/convex/server/__tests__/helpers.test.ts410
src/convex/server/__tests__/middleware.test.ts49
src/convex/server/__tests__/schema-helpers.test.ts15
src/convex/server/__tests__/setup-hooks.test.ts13
src/convex/server/__tests__/test-harness.test.ts26
src/convex/tools/__tests__/dispatch.test.ts215
src/convex/tools/__tests__/error.test.ts09
src/convex/tools/__tests__/manifest.test.ts410
src/convex/tools/__tests__/parser.test.ts010
src/convex/tools/__tests__/step-sink.test.ts04
src/convex/tools/__tests__/to-dispatch-error.test.ts13
src/shared/__tests__/auth-helpers.test.ts210
src/shared/__tests__/binary.test.ts36
src/shared/__tests__/bounded-stream.test.ts38
src/shared/__tests__/cli.test.ts620
src/shared/__tests__/completions.test.ts15
src/shared/__tests__/config.test.ts24
src/shared/__tests__/crash-log.test.ts13
src/shared/__tests__/docs-gen.test.ts412
src/shared/__tests__/env-file.test.ts26
src/shared/__tests__/env-zod.test.ts26
src/shared/__tests__/error-toast.test.tsx18
src/shared/__tests__/eslint-factory.test.ts623
src/shared/__tests__/file-utils.test.ts614
src/shared/__tests__/fixtures.test.ts14
src/shared/__tests__/form-meta.test.ts619
src/shared/__tests__/form-use-form.test.tsx18
src/shared/__tests__/helpers.test.ts16
src/shared/__tests__/http-body.test.ts26
src/shared/__tests__/log.test.ts13
src/shared/__tests__/redact.test.ts19
src/shared/__tests__/retry.test.ts315
src/shared/__tests__/sanitize.test.ts418
src/shared/__tests__/security.test.ts310
src/shared/__tests__/small-utils.test.ts410
src/shared/__tests__/sse.test.ts110
src/shared/__tests__/state.test.ts13
src/shared/__tests__/test-utils.test.ts314
src/shared/__tests__/token-bucket.test.ts29
src/shared/__tests__/update-check.test.ts210
src/shared/__tests__/url.fuzz.test.ts15
src/shared/__tests__/url.test.ts623
src/shared/__tests__/use-bulk-mutate.test.tsx19
src/shared/__tests__/use-bulk-selection.test.tsx18
src/shared/__tests__/use-online-status.test.tsx14
src/shared/__tests__/use-optimistic.test.tsx15
src/shared/__tests__/use-soft-delete.test.tsx13
src/shared/__tests__/viz.test.ts34
src/shared/__tests__/zod.test.ts1149
src/spacetimedb/__tests__/check.test.ts119
src/spacetimedb/__tests__/docs-gen.test.ts14
src/spacetimedb/__tests__/doctor.test.ts13
src/spacetimedb/__tests__/eslint-plugin.test.ts16
src/spacetimedb/__tests__/migrate.test.ts16
src/spacetimedb/__tests__/pure.test.ts2031199
src/spacetimedb/react/__tests__/devtools.test.ts15
src/spacetimedb/react/__tests__/optimistic-store.test.tsx18
src/spacetimedb/react/__tests__/use-bulk-mutate.test.tsx15
src/spacetimedb/react/__tests__/use-list.test.tsx13
src/spacetimedb/react/__tests__/use-mutate.test.ts14
src/spacetimedb/server/__tests__/cache-crud.test.ts17
src/spacetimedb/server/__tests__/child.test.ts16
src/spacetimedb/server/__tests__/crud.test.ts15
src/spacetimedb/server/__tests__/file.test.ts18
src/spacetimedb/server/__tests__/helpers.test.ts117
src/spacetimedb/server/__tests__/kv.test.ts16
src/spacetimedb/server/__tests__/log.test.ts17
src/spacetimedb/server/__tests__/middleware.test.ts13
src/spacetimedb/server/__tests__/org-crud.test.ts19
src/spacetimedb/server/__tests__/org-invites.test.ts16
src/spacetimedb/server/__tests__/org-join.test.ts17
src/spacetimedb/server/__tests__/org-members.test.ts16
src/spacetimedb/server/__tests__/org.test.ts210
src/spacetimedb/server/__tests__/presence.test.ts15
src/spacetimedb/server/__tests__/quota.test.ts12
src/spacetimedb/server/__tests__/rls.test.ts111
src/spacetimedb/server/__tests__/schema-helpers.test.ts113
src/spacetimedb/server/__tests__/setup.test.ts114
src/spacetimedb/server/__tests__/singleton.test.ts14
src/spacetimedb/server/__tests__/stdb-tables.test.ts17
src/spacetimedb/server/__tests__/test.test.ts12

E2E coverage

Playwright test counts per demo app, scanned from each demo's e2e/*.test.ts. These run against a real backend.

Playwright E2E coverage across all 10 demo apps. 604 total tests in 56 files.

Democvx filescvx describecvx teststdb filesstdb describestdb test
blog52525252
chat26262626
movie10141014
org6012860128
poll141882141882
total2830228302

On this page