Testing
Unit tests, integration tests, and E2E testing strategies.
Setup
Install convex-test:
bun add -d convex-testSet 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
} = tAll tests run against a local SpacetimeDB instance. Start it with Docker:
docker compose up -dWait for it to be healthy:
docker compose psPublish 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=trueOptional deterministic token fallback when cookie auth is unavailable in Next.js server helpers:
export SPACETIMEDB_TEST_TOKEN=test-token-localUnit 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()
})Testing search
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 --onceimport { 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:publishWriting 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:e2eTest 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:
- Use unique identifiers: prefix test data with a timestamp or UUID so tests don't collide.
- Clean up in afterEach: call
rm_*reducers to delete test data after each test. - 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:allThis 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)
| File | describe | test/it |
|---|---|---|
src/__tests__/bin-smoke.test.ts | 3 | 16 |
src/__tests__/cli-utils.test.ts | 1 | 1 |
src/__tests__/doctor-fix.test.ts | 1 | 1 |
src/__tests__/help-commands.test.ts | 1 | 8 |
src/__tests__/init.test.ts | 1 | 1 |
src/__tests__/scaffold-ops.test.ts | 1 | 10 |
src/convex/__tests__/audit.test.ts | 1 | 2 |
src/convex/__tests__/budget.property.test.ts | 1 | 4 |
src/convex/__tests__/budget.synthetic.test.ts | 1 | 12 |
src/convex/__tests__/budget.test.ts | 2 | 6 |
src/convex/__tests__/builder.test.ts | 4 | 10 |
src/convex/__tests__/devtools.test.ts | 1 | 5 |
src/convex/__tests__/docs-gen.test.ts | 3 | 6 |
src/convex/__tests__/doctor.test.ts | 1 | 5 |
src/convex/__tests__/eslint-plugin.test.ts | 1 | 5 |
src/convex/__tests__/eslint-smoke.test.ts | 1 | 5 |
src/convex/__tests__/manifest.test.ts | 1 | 25 |
src/convex/__tests__/optimistic-store.test.tsx | 1 | 3 |
src/convex/__tests__/pure.test.ts | 162 | 967 |
src/convex/__tests__/use-bulk-mutate.test.tsx | 1 | 5 |
src/convex/__tests__/use-form.test.tsx | 1 | 2 |
src/convex/__tests__/use-list.test.tsx | 2 | 5 |
src/convex/__tests__/use-mutate.test.ts | 1 | 4 |
src/convex/server/__tests__/helpers.test.ts | 4 | 10 |
src/convex/server/__tests__/middleware.test.ts | 4 | 9 |
src/convex/server/__tests__/schema-helpers.test.ts | 1 | 5 |
src/convex/server/__tests__/setup-hooks.test.ts | 1 | 3 |
src/convex/server/__tests__/test-harness.test.ts | 2 | 6 |
src/convex/tools/__tests__/dispatch.test.ts | 2 | 15 |
src/convex/tools/__tests__/error.test.ts | 0 | 9 |
src/convex/tools/__tests__/manifest.test.ts | 4 | 10 |
src/convex/tools/__tests__/parser.test.ts | 0 | 10 |
src/convex/tools/__tests__/step-sink.test.ts | 0 | 4 |
src/convex/tools/__tests__/to-dispatch-error.test.ts | 1 | 3 |
src/shared/__tests__/auth-helpers.test.ts | 2 | 10 |
src/shared/__tests__/binary.test.ts | 3 | 6 |
src/shared/__tests__/bounded-stream.test.ts | 3 | 8 |
src/shared/__tests__/cli.test.ts | 6 | 20 |
src/shared/__tests__/completions.test.ts | 1 | 5 |
src/shared/__tests__/config.test.ts | 2 | 4 |
src/shared/__tests__/crash-log.test.ts | 1 | 3 |
src/shared/__tests__/docs-gen.test.ts | 4 | 12 |
src/shared/__tests__/env-file.test.ts | 2 | 6 |
src/shared/__tests__/env-zod.test.ts | 2 | 6 |
src/shared/__tests__/error-toast.test.tsx | 1 | 8 |
src/shared/__tests__/eslint-factory.test.ts | 6 | 23 |
src/shared/__tests__/file-utils.test.ts | 6 | 14 |
src/shared/__tests__/fixtures.test.ts | 1 | 4 |
src/shared/__tests__/form-meta.test.ts | 6 | 19 |
src/shared/__tests__/form-use-form.test.tsx | 1 | 8 |
src/shared/__tests__/helpers.test.ts | 1 | 6 |
src/shared/__tests__/http-body.test.ts | 2 | 6 |
src/shared/__tests__/log.test.ts | 1 | 3 |
src/shared/__tests__/redact.test.ts | 1 | 9 |
src/shared/__tests__/retry.test.ts | 3 | 15 |
src/shared/__tests__/sanitize.test.ts | 4 | 18 |
src/shared/__tests__/security.test.ts | 3 | 10 |
src/shared/__tests__/small-utils.test.ts | 4 | 10 |
src/shared/__tests__/sse.test.ts | 1 | 10 |
src/shared/__tests__/state.test.ts | 1 | 3 |
src/shared/__tests__/test-utils.test.ts | 3 | 14 |
src/shared/__tests__/token-bucket.test.ts | 2 | 9 |
src/shared/__tests__/update-check.test.ts | 2 | 10 |
src/shared/__tests__/url.fuzz.test.ts | 1 | 5 |
src/shared/__tests__/url.test.ts | 6 | 23 |
src/shared/__tests__/use-bulk-mutate.test.tsx | 1 | 9 |
src/shared/__tests__/use-bulk-selection.test.tsx | 1 | 8 |
src/shared/__tests__/use-online-status.test.tsx | 1 | 4 |
src/shared/__tests__/use-optimistic.test.tsx | 1 | 5 |
src/shared/__tests__/use-soft-delete.test.tsx | 1 | 3 |
src/shared/__tests__/viz.test.ts | 3 | 4 |
src/shared/__tests__/zod.test.ts | 11 | 49 |
src/spacetimedb/__tests__/check.test.ts | 1 | 19 |
src/spacetimedb/__tests__/docs-gen.test.ts | 1 | 4 |
src/spacetimedb/__tests__/doctor.test.ts | 1 | 3 |
src/spacetimedb/__tests__/eslint-plugin.test.ts | 1 | 6 |
src/spacetimedb/__tests__/migrate.test.ts | 1 | 6 |
src/spacetimedb/__tests__/pure.test.ts | 203 | 1199 |
src/spacetimedb/react/__tests__/devtools.test.ts | 1 | 5 |
src/spacetimedb/react/__tests__/optimistic-store.test.tsx | 1 | 8 |
src/spacetimedb/react/__tests__/use-bulk-mutate.test.tsx | 1 | 5 |
src/spacetimedb/react/__tests__/use-list.test.tsx | 1 | 3 |
src/spacetimedb/react/__tests__/use-mutate.test.ts | 1 | 4 |
src/spacetimedb/server/__tests__/cache-crud.test.ts | 1 | 7 |
src/spacetimedb/server/__tests__/child.test.ts | 1 | 6 |
src/spacetimedb/server/__tests__/crud.test.ts | 1 | 5 |
src/spacetimedb/server/__tests__/file.test.ts | 1 | 8 |
src/spacetimedb/server/__tests__/helpers.test.ts | 1 | 17 |
src/spacetimedb/server/__tests__/kv.test.ts | 1 | 6 |
src/spacetimedb/server/__tests__/log.test.ts | 1 | 7 |
src/spacetimedb/server/__tests__/middleware.test.ts | 1 | 3 |
src/spacetimedb/server/__tests__/org-crud.test.ts | 1 | 9 |
src/spacetimedb/server/__tests__/org-invites.test.ts | 1 | 6 |
src/spacetimedb/server/__tests__/org-join.test.ts | 1 | 7 |
src/spacetimedb/server/__tests__/org-members.test.ts | 1 | 6 |
src/spacetimedb/server/__tests__/org.test.ts | 2 | 10 |
src/spacetimedb/server/__tests__/presence.test.ts | 1 | 5 |
src/spacetimedb/server/__tests__/quota.test.ts | 1 | 2 |
src/spacetimedb/server/__tests__/rls.test.ts | 1 | 11 |
src/spacetimedb/server/__tests__/schema-helpers.test.ts | 1 | 13 |
src/spacetimedb/server/__tests__/setup.test.ts | 1 | 14 |
src/spacetimedb/server/__tests__/singleton.test.ts | 1 | 4 |
src/spacetimedb/server/__tests__/stdb-tables.test.ts | 1 | 7 |
src/spacetimedb/server/__tests__/test.test.ts | 1 | 2 |
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.
| Demo | cvx files | cvx describe | cvx test | stdb files | stdb describe | stdb test |
|---|---|---|---|---|---|---|
blog | 5 | 2 | 52 | 5 | 2 | 52 |
chat | 2 | 6 | 26 | 2 | 6 | 26 |
movie | 1 | 0 | 14 | 1 | 0 | 14 |
org | 6 | 0 | 128 | 6 | 0 | 128 |
poll | 14 | 18 | 82 | 14 | 18 | 82 |
| total | 28 | — | 302 | 28 | — | 302 |