Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm test
- run: pnpm build
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ Always follow the guidelines in this file, unless explicitly told otherwise by t

- Before pushing:
- Check background process output for Convex backend errors.
- Run `pnpm lint`
- Run `pnpm lint` and `pnpm test`
- Review with `git diff origin/main` (or whatever branch makes sense)
- Manual UI testing: use the **dev-browser** skill (see the `dev-browser-clerk-testing` skill for signing in, resizing for mobile/tablet/desktop, and reading console output)
- Test account: `claude+clerk_test@example.com`, code `424242`. Use slowly/pressSequentially to trigger auto distribution
- Convex in tests: Use `ConvexTestingHelper` for queries/mutations
- Backend tests: vitest + [convex-test](https://docs.convex.dev/testing/convex-test) (`pnpm test`, or `pnpm test:watch`). See `convex/users.test.ts` for the pattern (`convexTest(schema, modules)`, `t.withIdentity` for auth)
- Test cleanup: Use `testingMutation` from `convex/testingFunctions.ts` for cleanup functions - prevents accidental production use
- If you run into an issue you don't know how to fix, look for relevant documentation or a reference implementation

Expand Down
12 changes: 12 additions & 0 deletions convex/test.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference types="vite/client" />

// Lets convexTest find the function modules (automatic discovery breaks under
// vitest 4). https://docs.convex.dev/testing/convex-test - but its
// `!(*.*.*)` extglob matches nothing in Vite 6+ (tinyglobby), hence negations.
export const modules = import.meta.glob([
"./**/*.ts",
"./_generated/*.js",
"!./**/*.test.ts",
"!./**/*.d.ts",
"!./test.setup.ts",
]);
70 changes: 70 additions & 0 deletions convex/users.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { convexTest } from "convex-test";
import { ConvexError } from "convex/values";
import { expect, test } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";
import { modules } from "./test.setup";

// https://docs.convex.dev/testing/convex-test

test("getCurrentUser returns null when not authenticated", async () => {
const t = convexTest(schema, modules);

expect(await t.query(api.users.getCurrentUser, {})).toBeNull();
});

test("getCurrentUser returns the user after ensureUser", async () => {
const t = convexTest(schema, modules);
const asSarah = t.withIdentity({ subject: "clerk|sarah", name: "Sarah" });

await asSarah.mutation(api.users.ensureUser, {});

expect(await asSarah.query(api.users.getCurrentUser, {})).toMatchObject({
clerkId: "clerk|sarah",
name: "Sarah",
});
});

test("ensureUser upserts: creates once, updates name on change", async () => {
const t = convexTest(schema, modules);
const asSarah = t.withIdentity({ subject: "clerk|sarah", name: "Sarah" });

const created = await asSarah.mutation(api.users.ensureUser, {});
const unchanged = await asSarah.mutation(api.users.ensureUser, {});
expect(unchanged?._id).toEqual(created?._id);

const asRenamedSarah = t.withIdentity({
subject: "clerk|sarah",
name: "Sarah Connor",
});
const renamed = await asRenamedSarah.mutation(api.users.ensureUser, {});
expect(renamed).toMatchObject({ _id: created?._id, name: "Sarah Connor" });

const users = await asSarah.query(api.users.listUsers, {});
expect(users).toHaveLength(1);
});

test("listUsers throws for unauthenticated callers", async () => {
const t = convexTest(schema, modules);

await expect(t.query(api.users.listUsers, {})).rejects.toThrowError(
ConvexError,
);
});

test("listUsers throws until the user row is provisioned", async () => {
const t = convexTest(schema, modules);
const asSarah = t.withIdentity({ subject: "clerk|sarah", name: "Sarah" });

// Authenticated with Clerk, but ensureUser hasn't run yet
// (the first-login provisioning window)
await expect(asSarah.query(api.users.listUsers, {})).rejects.toThrowError(
ConvexError,
);

await asSarah.mutation(api.users.ensureUser, {});

expect(await asSarah.query(api.users.listUsers, {})).toMatchObject([
{ clerkId: "clerk|sarah", name: "Sarah" },
]);
});
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default tseslint.config(
"postcss.config.js",
"tailwind.config.js",
"vite.config.ts",
"vitest.config.ts",
],
},
{
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"dev:backend": "convex dev",
"build": "tsc -b && vite build",
"lint": "tsc -b && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"test": "vitest run",
"test:watch": "vitest",
"format": "prettier --write .",
"preview": "vite preview"
},
Expand All @@ -35,6 +37,7 @@
},
"devDependencies": {
"@convex-dev/eslint-plugin": "^2.0.0",
"@edge-runtime/vm": "^5.0.0",
"@eslint/js": "^10.0.1",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.3.0",
Expand All @@ -44,6 +47,7 @@
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"convex-helpers": "^0.1.118",
"convex-test": "^0.0.54",
"dotenv": "^17.4.2",
"eslint": "^10.4.0",
"eslint-plugin-react-hooks": "^7.1.1",
Expand All @@ -55,6 +59,7 @@
"typescript": "~5.9.3",
"typescript-eslint": "^8.60.0",
"vite": "^8.0.14",
"vitest": "^4.1.10",
"zod": "^4.4.3"
},
"packageManager": "pnpm@11.5.0",
Expand Down
Loading
Loading