feather-testing-convex 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,353 @@
1
+ # feather-testing-convex
2
+
3
+ React provider that adapts [convex-test](https://www.npmjs.com/package/convex-test)'s one-shot query/mutation client for use with Convex's `ConvexProvider`, so `useQuery` and `useMutation` work in tests against an in-memory backend.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i convex convex-test react
9
+ npm i -D feather-testing-convex
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ Three files to get from zero to a working test:
15
+
16
+ **1. `vitest.config.ts`**
17
+
18
+ ```typescript
19
+ import { defineConfig } from "vitest/config";
20
+ import react from "@vitejs/plugin-react";
21
+
22
+ export default defineConfig({
23
+ plugins: [react()],
24
+ test: {
25
+ environment: "jsdom",
26
+ environmentMatchGlobs: [["convex/**", "edge-runtime"]],
27
+ server: { deps: { inline: ["convex-test"] } },
28
+ globals: true,
29
+ setupFiles: ["./src/test-setup.ts"],
30
+ },
31
+ });
32
+ ```
33
+
34
+ **2. `convex/test.setup.ts`**
35
+
36
+ ```typescript
37
+ /// <reference types="vite/client" />
38
+ import { createConvexTest, renderWithConvex } from "feather-testing-convex";
39
+ import schema from "./schema";
40
+
41
+ export const modules = import.meta.glob("./**/!(*.*.*)*.*s");
42
+ export const test = createConvexTest(schema, modules);
43
+ export { renderWithConvex };
44
+ ```
45
+
46
+ **3. `src/test-setup.ts`**
47
+
48
+ ```typescript
49
+ import "@testing-library/jest-dom/vitest";
50
+ ```
51
+
52
+ **First test:**
53
+
54
+ ```tsx
55
+ import { describe, expect } from "vitest";
56
+ import { screen } from "@testing-library/react";
57
+ import { test, renderWithConvex } from "../../convex/test.setup";
58
+ import { TodoList } from "./TodoList";
59
+
60
+ describe("TodoList", () => {
61
+ test("shows seeded data", async ({ client, seed }) => {
62
+ await seed("todos", { text: "Buy milk", completed: false });
63
+ renderWithConvex(<TodoList />, client);
64
+ expect(await screen.findByText("Buy milk")).toBeInTheDocument();
65
+ });
66
+ });
67
+ ```
68
+
69
+ For auth testing, see [Auth Testing](#auth-testing) below.
70
+
71
+ ## Usage
72
+
73
+ > For the recommended setup, see [Quick Start](#quick-start) above. This section shows the low-level API.
74
+
75
+ 1. Create a convex-test client: `convexTest(schema, modules)`.
76
+ 2. Wrap your component with `ConvexTestProvider` and pass the client:
77
+
78
+ ```tsx
79
+ import { convexTest } from "convex-test";
80
+ import { ConvexTestProvider } from "feather-testing-convex";
81
+ import schema from "./convex/schema";
82
+ import { modules } from "./convex/test.setup";
83
+
84
+ const testClient = convexTest(schema, modules);
85
+
86
+ render(
87
+ <ConvexTestProvider client={testClient}>
88
+ <YourComponent />
89
+ </ConvexTestProvider>
90
+ );
91
+ ```
92
+
93
+ ## Query reactivity
94
+
95
+ This adapter runs each query **once** (when the component mounts). The UI does not re-render after a mutation in the same test. Assert backend state via `client.query(api.your.list, {})`, or re-mount to run the query again.
96
+
97
+ ## Helper Functions
98
+
99
+ Reduce test boilerplate from ~15 lines to ~2 lines with `createConvexTest`.
100
+
101
+ ### Setup
102
+
103
+ Create a test setup file in your Convex directory:
104
+
105
+ ```typescript
106
+ // convex/test.setup.ts
107
+ import { createConvexTest, renderWithConvex } from "feather-testing-convex";
108
+ import schema from "./schema";
109
+
110
+ export const modules = import.meta.glob("./**/!(*.*.*)*.*s");
111
+ export const test = createConvexTest(schema, modules);
112
+ export { renderWithConvex };
113
+ ```
114
+
115
+ ### Usage
116
+
117
+ ```typescript
118
+ // src/components/TodoList.test.tsx
119
+ import { test, renderWithConvex } from "../../convex/test.setup";
120
+ import { expect } from "vitest";
121
+ import { api } from "../../convex/_generated/api";
122
+
123
+ test("creates a todo", async ({ client, seed }) => {
124
+ await seed("todos", { text: "Buy milk", completed: false });
125
+
126
+ const todos = await client.query(api.todos.list, {});
127
+ expect(todos).toHaveLength(1);
128
+ });
129
+ ```
130
+
131
+ ### Fixtures
132
+
133
+ | Fixture | Description |
134
+ |---------|-------------|
135
+ | `testClient` | Raw convex-test client (unauthenticated) |
136
+ | `userId` | ID of an auto-created user (string) |
137
+ | `client` | Authenticated client for the auto-created user |
138
+ | `seed(table, data)` | Insert a document. Auto-fills `userId` unless `data` includes an explicit `userId` (explicit wins). Returns the document ID. |
139
+ | `createUser()` | Create another user, return authenticated client with `.userId` property. |
140
+
141
+ ### Multi-user Test Example
142
+
143
+ ```typescript
144
+ test("users only see their own todos", async ({ client, seed, createUser }) => {
145
+ // Alice creates a todo
146
+ await client.mutation(api.todos.create, { text: "Alice's todo" });
147
+
148
+ // Bob creates a todo (seed with explicit userId)
149
+ const bob = await createUser();
150
+ await seed("todos", { text: "Bob's todo", completed: false, userId: bob.userId });
151
+
152
+ // Each user only sees their own
153
+ const aliceTodos = await client.query(api.todos.list, {});
154
+ expect(aliceTodos).toHaveLength(1);
155
+
156
+ const bobTodos = await bob.query(api.todos.list, {});
157
+ expect(bobTodos).toHaveLength(1);
158
+ expect(bobTodos[0].text).toBe("Bob's todo");
159
+ });
160
+ ```
161
+
162
+ ### Configuration
163
+
164
+ ```typescript
165
+ // Custom users table name
166
+ export const test = createConvexTest(schema, modules, {
167
+ usersTable: "profiles",
168
+ });
169
+ ```
170
+
171
+ ### Additional Helpers
172
+
173
+ - `wrapWithConvex(children, client)` — JSX wrapper for custom rendering
174
+ - `renderWithConvex(ui, client)` — Testing Library render with Convex provider
175
+
176
+ ## Auth Testing
177
+
178
+ Test components that use `<Authenticated>`, `<Unauthenticated>`, `useConvexAuth()`, and `useAuthActions()` — without mocking.
179
+
180
+ ### Prerequisites
181
+
182
+ ```bash
183
+ npm i -D @convex-dev/auth
184
+ ```
185
+
186
+ Add the vitest plugin to resolve an internal `@convex-dev/auth` import ([upstream fix requested](https://github.com/get-convex/convex-auth/issues/281)):
187
+
188
+ ```typescript
189
+ // vitest.config.ts
190
+ import { convexTestProviderPlugin } from "feather-testing-convex/vitest-plugin";
191
+
192
+ export default defineConfig({
193
+ plugins: [
194
+ react(),
195
+ convexTestProviderPlugin(), // resolves @convex-dev/auth internal import
196
+ ],
197
+ // ... rest of config unchanged
198
+ });
199
+ ```
200
+
201
+ ### Usage
202
+
203
+ ```tsx
204
+ import { renderWithConvexAuth } from "feather-testing-convex";
205
+
206
+ // Authenticated (default) — <Authenticated> children render
207
+ renderWithConvexAuth(<App />, client);
208
+
209
+ // Unauthenticated — <Unauthenticated> children render
210
+ renderWithConvexAuth(<App />, client, { authenticated: false });
211
+ ```
212
+
213
+ `renderWithConvexAuth` wraps your component with both auth state (so `<Authenticated>`, `<Unauthenticated>`, and `useConvexAuth()` work) and auth actions context (so `useAuthActions()` works). Calling `signIn()` sets auth to true; calling `signOut()` sets auth to false — the view re-renders accordingly.
214
+
215
+ ### Complete auth test example
216
+
217
+ ```tsx
218
+ import { test, renderWithConvexAuth } from "../../convex/test.setup";
219
+ import { screen } from "@testing-library/react";
220
+ import userEvent from "@testing-library/user-event";
221
+ import { expect } from "vitest";
222
+
223
+ test("sign out toggles the view", async ({ client }) => {
224
+ const user = userEvent.setup();
225
+ renderWithConvexAuth(<App />, client);
226
+
227
+ await user.click(screen.getByRole("button", { name: /sign out/i }));
228
+ expect(await screen.findByText("Please sign in")).toBeInTheDocument();
229
+ });
230
+ ```
231
+
232
+ ### Sign-in error simulation
233
+
234
+ ```tsx
235
+ renderWithConvexAuth(<App />, client, {
236
+ authenticated: false,
237
+ signInError: new Error("Invalid credentials"),
238
+ });
239
+ ```
240
+
241
+ ### Direct `ConvexTestAuthProvider` (custom wrapping)
242
+
243
+ ```tsx
244
+ import { ConvexTestAuthProvider } from "feather-testing-convex";
245
+
246
+ <ConvexTestAuthProvider client={client} authenticated={true}>
247
+ <YourComponent />
248
+ </ConvexTestAuthProvider>
249
+ ```
250
+
251
+ ## Vitest Configuration Reference
252
+
253
+ ### Minimal config (no auth)
254
+
255
+ ```typescript
256
+ import { defineConfig } from "vitest/config";
257
+ import react from "@vitejs/plugin-react";
258
+
259
+ export default defineConfig({
260
+ plugins: [react()],
261
+ test: {
262
+ environment: "jsdom",
263
+ environmentMatchGlobs: [["convex/**", "edge-runtime"]],
264
+ server: { deps: { inline: ["convex-test"] } },
265
+ globals: true,
266
+ setupFiles: ["./src/test-setup.ts"],
267
+ },
268
+ });
269
+ ```
270
+
271
+ ### With auth testing
272
+
273
+ ```typescript
274
+ import { defineConfig } from "vitest/config";
275
+ import react from "@vitejs/plugin-react";
276
+ import { convexTestProviderPlugin } from "feather-testing-convex/vitest-plugin";
277
+
278
+ export default defineConfig({
279
+ plugins: [react(), convexTestProviderPlugin()],
280
+ test: {
281
+ environment: "jsdom",
282
+ environmentMatchGlobs: [["convex/**", "edge-runtime"]],
283
+ server: { deps: { inline: ["convex-test"] } },
284
+ globals: true,
285
+ setupFiles: ["./src/test-setup.ts"],
286
+ },
287
+ });
288
+ ```
289
+
290
+ ### Config options explained
291
+
292
+ | Option | Why |
293
+ |--------|-----|
294
+ | `react()` | JSX transform for test files |
295
+ | `environment: "jsdom"` | DOM APIs for React component tests |
296
+ | `environmentMatchGlobs` | Convex functions run in edge runtime, not jsdom |
297
+ | `server.deps.inline: ["convex-test"]` | convex-test must be inlined for Vitest to resolve it |
298
+ | `setupFiles` | Load jest-dom matchers (`toBeInTheDocument()`, etc.) |
299
+ | `convexTestProviderPlugin()` | Resolves `@convex-dev/auth` internal import (auth testing only) |
300
+
301
+ ## Agent Skills
302
+
303
+ Install skills for AI coding agents via [skills.sh](https://skills.sh):
304
+
305
+ ```bash
306
+ npx skills add siraj-samsudeen/feather-testing-convex
307
+ ```
308
+
309
+ This installs three skills: `setup-convex-testing`, `add-convex-auth-testing`, and `convex-test-patterns`.
310
+
311
+ ## Limitations
312
+
313
+ ### One-shot query execution (non-reactive)
314
+
315
+ Queries resolve **once** at component mount. After a mutation, the UI does not automatically re-render with updated data — this adapter does not simulate Convex's reactive subscription model.
316
+
317
+ To verify backend state after a mutation, query directly:
318
+
319
+ ```tsx
320
+ await user.click(screen.getByRole("button", { name: "Add" }));
321
+ const items = await client.query(api.items.list, {});
322
+ expect(items).toHaveLength(1);
323
+ ```
324
+
325
+ To see updated data in the UI, unmount and remount the component (or call `rerender`).
326
+
327
+ ### Nested `runQuery`/`runMutation` lose auth context
328
+
329
+ When a Convex function calls `ctx.runQuery()` or `ctx.runMutation()`, the nested call does not inherit the caller's auth identity. This is an [upstream limitation in convex-test](https://github.com/get-convex/convex-test) ([issue #50](https://github.com/get-convex/convex-test/issues/50)), not in this package.
330
+
331
+ **Root cause:** In `convex-test`, the `queryFromPath` and `mutationFromPath` handlers spread `{ ...ctx, auth }` but do not override `ctx.runQuery`/`ctx.runMutation` with auth-aware versions. Actions (`actionFromPath`) already do this correctly.
332
+
333
+ **Workarounds:**
334
+
335
+ 1. **Pass userId as an explicit argument** (recommended) — create `internalQuery`/`internalMutation` variants that accept `userId` instead of reading `ctx.auth.getUserIdentity()` inside nested calls.
336
+ 2. **Use `patch-package`** — apply a 2-line fix to `node_modules/convex-test/dist/index.js`:
337
+ - In `queryFromPath`: change `{ ...ctx, auth }` to `{ ...ctx, auth, runQuery: byType.query }`
338
+ - In `runTransaction`: change `{ ...ctx, auth, ...extraCtx }` to `{ ...ctx, auth, runQuery: byType.query, runMutation: byType.mutation, ...extraCtx }`
339
+ 3. **Use actions for orchestration** — actions already propagate auth correctly to nested calls (note: different transactional semantics).
340
+
341
+ ## Types
342
+
343
+ The `client` prop accepts any object with `query(ref, args)` and `mutation(ref, args)` returning promises. The result of `convexTest(schema, modules)` (and `.withIdentity(...)`) satisfies this.
344
+
345
+ ## Contributing
346
+
347
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow.
348
+
349
+ AI agents: See [CLAUDE.md](CLAUDE.md) for quick reference.
350
+
351
+ ## Versioning
352
+
353
+ Releases follow [semantic versioning](https://semver.org/). See [CHANGELOG.md](./CHANGELOG.md) for release history.
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ConvexTestClient } from "./ConvexTestProvider.js";
3
+ /**
4
+ * Wraps children with both auth state (ConvexProviderWithAuth) and auth actions
5
+ * (ConvexAuthActionsContext), so components using <Authenticated>, <Unauthenticated>,
6
+ * useConvexAuth(), and useAuthActions() all work in tests.
7
+ *
8
+ * signIn/signOut are pure React state toggles — no backend calls.
9
+ * For auth-dependent queries, use `client.withIdentity()` to inject identity
10
+ * so `getAuthUserId(ctx)` returns a valid user ID.
11
+ */
12
+ export declare function ConvexTestAuthProvider({ client, children, authenticated, signInError, }: {
13
+ client: ConvexTestClient;
14
+ children: ReactNode;
15
+ authenticated?: boolean;
16
+ /** When set, signIn() rejects with this error instead of toggling state. */
17
+ signInError?: Error;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=ConvexTestAuthProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConvexTestAuthProvider.d.ts","sourceRoot":"","sources":["../src/ConvexTestAuthProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAKjD,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEpF;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,QAAQ,EACR,aAAoB,EACpB,WAAW,GACZ,EAAE;IACD,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,KAAK,CAAC;CACrB,2CAqBA"}
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // TODO: Replace with public export when @convex-dev/auth provides one.
3
+ // Track: https://github.com/get-convex/convex-auth — request exported TestAuthProvider or context.
4
+ import { useState } from "react";
5
+ // @ts-expect-error — internal path not in package exports; works at runtime via bundler
6
+ import { ConvexAuthActionsContext } from "@convex-dev/auth/dist/react/client.js";
7
+ import { ConvexTestProvider } from "./ConvexTestProvider.js";
8
+ /**
9
+ * Wraps children with both auth state (ConvexProviderWithAuth) and auth actions
10
+ * (ConvexAuthActionsContext), so components using <Authenticated>, <Unauthenticated>,
11
+ * useConvexAuth(), and useAuthActions() all work in tests.
12
+ *
13
+ * signIn/signOut are pure React state toggles — no backend calls.
14
+ * For auth-dependent queries, use `client.withIdentity()` to inject identity
15
+ * so `getAuthUserId(ctx)` returns a valid user ID.
16
+ */
17
+ export function ConvexTestAuthProvider({ client, children, authenticated = true, signInError, }) {
18
+ const [isAuth, setIsAuth] = useState(authenticated);
19
+ const actions = {
20
+ signIn: async () => {
21
+ if (signInError)
22
+ throw signInError;
23
+ setIsAuth(true);
24
+ return { signingIn: false };
25
+ },
26
+ signOut: async () => {
27
+ setIsAuth(false);
28
+ },
29
+ };
30
+ return (_jsx(ConvexTestProvider, { client: client, authenticated: isAuth, children: _jsx(ConvexAuthActionsContext.Provider, { value: actions, children: children }) }));
31
+ }
@@ -0,0 +1,23 @@
1
+ import { type ReactNode } from "react";
2
+ /** Minimal client shape: one-shot query, mutation, action, plus identity injection. Matches convex-test client. */
3
+ export interface ConvexTestClient {
4
+ query: (query: unknown, args: unknown) => Promise<unknown>;
5
+ mutation: (mutation: unknown, args: unknown) => Promise<unknown>;
6
+ action: (action: unknown, args: unknown) => Promise<unknown>;
7
+ run: (fn: (ctx: any) => Promise<any>) => Promise<any>;
8
+ withIdentity: (identity: Record<string, unknown>) => ConvexTestClient;
9
+ }
10
+ /**
11
+ * Wraps children in ConvexProvider with a fake client that adapts convex-test's
12
+ * one-shot query/mutation API to the reactive watchQuery API the real ConvexProvider expects.
13
+ * Lets useQuery/useMutation run in tests against an in-memory backend.
14
+ *
15
+ * When `authenticated` is provided, uses ConvexProviderWithAuth instead of ConvexProvider,
16
+ * enabling <Authenticated>, <Unauthenticated>, and useConvexAuth() in tested components.
17
+ */
18
+ export declare function ConvexTestProvider({ client, children, authenticated, }: {
19
+ client: ConvexTestClient;
20
+ children: ReactNode;
21
+ authenticated?: boolean;
22
+ }): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=ConvexTestProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConvexTestProvider.d.ts","sourceRoot":"","sources":["../src/ConvexTestProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrE,mHAAmH;AACnH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACtD,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,gBAAgB,CAAC;CACvE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,QAAQ,EACR,aAAa,GACd,EAAE;IACD,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,2CA2EA"}
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useMemo, useRef } from "react";
3
+ import { ConvexProvider, ConvexProviderWithAuth } from "convex/react";
4
+ /**
5
+ * Wraps children in ConvexProvider with a fake client that adapts convex-test's
6
+ * one-shot query/mutation API to the reactive watchQuery API the real ConvexProvider expects.
7
+ * Lets useQuery/useMutation run in tests against an in-memory backend.
8
+ *
9
+ * When `authenticated` is provided, uses ConvexProviderWithAuth instead of ConvexProvider,
10
+ * enabling <Authenticated>, <Unauthenticated>, and useConvexAuth() in tested components.
11
+ */
12
+ export function ConvexTestProvider({ client, children, authenticated, }) {
13
+ // Two-level cache: query reference identity → serialized args → result.
14
+ // Avoids collisions between different query functions whose proxies all
15
+ // stringify to "{}" (see issue #2). Approach from PR #3.
16
+ // useRef so cache survives re-renders (e.g. auth state toggle).
17
+ const cache = useRef(new Map());
18
+ // Ref for authenticated so fakeClient.setAuth reads the latest value
19
+ // without needing authenticated in the useMemo dependency array.
20
+ const authenticatedRef = useRef(authenticated);
21
+ authenticatedRef.current = authenticated;
22
+ // Stable reference — only changes when the client prop itself changes.
23
+ // ConvexProviderWithAuth puts client in useEffect deps; an unstable
24
+ // reference would trigger setAuth/clearAuth cycles every render.
25
+ const fakeClient = useMemo(() => ({
26
+ watchQuery: (query, args) => {
27
+ let queryCache = cache.current.get(query);
28
+ if (!queryCache) {
29
+ queryCache = new Map();
30
+ cache.current.set(query, queryCache);
31
+ }
32
+ const argsKey = JSON.stringify(args ?? {});
33
+ let subscriber = null;
34
+ client.query(query, args ?? {}).then((result) => {
35
+ queryCache.set(argsKey, result);
36
+ subscriber?.();
37
+ });
38
+ return {
39
+ localQueryResult: () => queryCache.get(argsKey),
40
+ onUpdate: (cb) => {
41
+ subscriber = cb;
42
+ return () => { subscriber = null; };
43
+ },
44
+ };
45
+ },
46
+ mutation: (mutation, args) => {
47
+ return client.mutation(mutation, args ?? {});
48
+ },
49
+ // Synchronous — real client calls onChange async, but ConvexProviderWithAuth
50
+ // calls setAuth inside useEffect (committed state), so sync is safe here.
51
+ setAuth: (_fetchToken, onChange) => {
52
+ onChange(authenticatedRef.current ?? false);
53
+ },
54
+ clearAuth: () => { },
55
+ }), [client]);
56
+ // Stable fetchAccessToken — ConvexProviderWithAuth puts this in useEffect
57
+ // deps. Unstable reference would trigger setAuth/clearAuth every render,
58
+ // pausing/resuming websockets. Matches Convex's own Auth0 provider pattern.
59
+ const fetchAccessToken = useCallback(async () => null, []);
60
+ // Only changes when authenticated changes — which is when we actually
61
+ // want ConvexProviderWithAuth to re-run its auth effects.
62
+ const useAuth = useCallback(() => ({
63
+ isLoading: false,
64
+ isAuthenticated: authenticated ?? false,
65
+ fetchAccessToken,
66
+ }), [authenticated, fetchAccessToken]);
67
+ if (authenticated === undefined) {
68
+ return (_jsx(ConvexProvider, { client: fakeClient, children: children }));
69
+ }
70
+ return (_jsx(ConvexProviderWithAuth, { client: fakeClient, useAuth: useAuth, children: children }));
71
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode, ReactElement } from "react";
2
+ interface CreateConvexTestOptions {
3
+ usersTable?: string;
4
+ }
5
+ export declare function createConvexTest(schema: any, modules: any, options?: CreateConvexTestOptions): import("vitest").TestAPI<{
6
+ client: any;
7
+ testClient: any;
8
+ userId: string;
9
+ seed: (table: string, data: Record<string, unknown>) => Promise<string>;
10
+ createUser: () => Promise<any & {
11
+ userId: string;
12
+ }>;
13
+ }>;
14
+ export declare function wrapWithConvex(children: ReactNode, client: unknown): import("react/jsx-runtime").JSX.Element;
15
+ export declare function renderWithConvex(ui: ReactElement, client: unknown): import("@testing-library/react").RenderResult<typeof import("@testing-library/dom/types/queries.js"), HTMLElement, HTMLElement>;
16
+ export declare function renderWithConvexAuth(ui: ReactElement, client: unknown, options?: {
17
+ authenticated?: boolean;
18
+ signInError?: Error;
19
+ }): import("@testing-library/react").RenderResult<typeof import("@testing-library/dom/types/queries.js"), HTMLElement, HTMLElement>;
20
+ export {};
21
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAErD,UAAU,uBAAuB;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAaD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,GAAG,EACX,OAAO,EAAE,GAAG,EACZ,OAAO,GAAE,uBAA4B;;;;kBAVvB,MAAM,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC;sBACrD,OAAO,CAAC,GAAG,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;GA4CpD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,2CAElE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,mIAIjE;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,OAAO,EACf,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,mIAa3D"}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { test as baseTest } from "vitest";
3
+ import { convexTest } from "convex-test";
4
+ import { ConvexTestProvider } from "./ConvexTestProvider.js";
5
+ import { ConvexTestAuthProvider } from "./ConvexTestAuthProvider.js";
6
+ import { render } from "@testing-library/react";
7
+ const dbInsert = (client, table, data) => client.run((ctx) => ctx.db.insert(table, data));
8
+ export function createConvexTest(schema, modules, options = {}) {
9
+ const usersTable = options.usersTable ?? "users";
10
+ return baseTest.extend({
11
+ testClient: async ({}, use) => {
12
+ const client = convexTest(schema, modules);
13
+ await use(client);
14
+ },
15
+ userId: async ({ testClient }, use) => {
16
+ const id = await dbInsert(testClient, usersTable, {});
17
+ await use(id);
18
+ },
19
+ client: async ({ testClient, userId }, use) => {
20
+ const authenticated = testClient.withIdentity({ subject: userId });
21
+ await use(authenticated);
22
+ },
23
+ seed: async ({ testClient, userId }, use) => {
24
+ const seedFn = (table, data) => dbInsert(testClient, table, { userId, ...data });
25
+ await use(seedFn);
26
+ },
27
+ createUser: async ({ testClient }, use) => {
28
+ const createUserFn = async () => {
29
+ const newUserId = await dbInsert(testClient, usersTable, {});
30
+ const userClient = testClient.withIdentity({ subject: newUserId });
31
+ return Object.assign(userClient, { userId: newUserId });
32
+ };
33
+ await use(createUserFn);
34
+ },
35
+ });
36
+ }
37
+ export function wrapWithConvex(children, client) {
38
+ return _jsx(ConvexTestProvider, { client: client, children: children });
39
+ }
40
+ export function renderWithConvex(ui, client) {
41
+ return render(ui, {
42
+ wrapper: ({ children }) => wrapWithConvex(children, client),
43
+ });
44
+ }
45
+ export function renderWithConvexAuth(ui, client, options) {
46
+ return render(ui, {
47
+ wrapper: ({ children }) => (_jsx(ConvexTestAuthProvider, { client: client, authenticated: options?.authenticated ?? true, signInError: options?.signInError, children: children })),
48
+ });
49
+ }
@@ -0,0 +1,4 @@
1
+ export { ConvexTestProvider, type ConvexTestClient } from "./ConvexTestProvider.js";
2
+ export { ConvexTestAuthProvider } from "./ConvexTestAuthProvider.js";
3
+ export { createConvexTest, wrapWithConvex, renderWithConvex, renderWithConvexAuth, } from "./helpers.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { ConvexTestProvider } from "./ConvexTestProvider.js";
2
+ export { ConvexTestAuthProvider } from "./ConvexTestAuthProvider.js";
3
+ export { createConvexTest, wrapWithConvex, renderWithConvex, renderWithConvexAuth, } from "./helpers.js";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Vite plugin that resolves the internal @convex-dev/auth import used by
3
+ * ConvexTestAuthProvider. Add this to your vitest.config.ts plugins array
4
+ * so you don't need a manual resolve.alias entry.
5
+ *
6
+ * Background: ConvexTestAuthProvider imports ConvexAuthActionsContext from
7
+ * @convex-dev/auth/dist/react/client.js — an internal path not in the
8
+ * package's exports field. Vite enforces exports strictly and blocks it.
9
+ * This plugin adds a resolve.alias so Vite can find the file.
10
+ *
11
+ * Upstream fix requested: https://github.com/get-convex/convex-auth/issues/281
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { convexTestProviderPlugin } from "feather-testing-convex/vitest-plugin";
16
+ *
17
+ * export default defineConfig({
18
+ * plugins: [convexTestProviderPlugin()],
19
+ * });
20
+ * ```
21
+ */
22
+ export declare function convexTestProviderPlugin(): {
23
+ name: string;
24
+ config(): {
25
+ resolve: {
26
+ alias: {
27
+ "@convex-dev/auth/dist/react/client.js": string;
28
+ };
29
+ };
30
+ };
31
+ };
32
+ //# sourceMappingURL=vitest-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest-plugin.d.ts","sourceRoot":"","sources":["../src/vitest-plugin.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,wBAAwB;;;;;;;;;EA2BvC"}
@@ -0,0 +1,47 @@
1
+ import { createRequire } from "node:module";
2
+ import { resolve } from "node:path";
3
+ /**
4
+ * Vite plugin that resolves the internal @convex-dev/auth import used by
5
+ * ConvexTestAuthProvider. Add this to your vitest.config.ts plugins array
6
+ * so you don't need a manual resolve.alias entry.
7
+ *
8
+ * Background: ConvexTestAuthProvider imports ConvexAuthActionsContext from
9
+ * @convex-dev/auth/dist/react/client.js — an internal path not in the
10
+ * package's exports field. Vite enforces exports strictly and blocks it.
11
+ * This plugin adds a resolve.alias so Vite can find the file.
12
+ *
13
+ * Upstream fix requested: https://github.com/get-convex/convex-auth/issues/281
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { convexTestProviderPlugin } from "feather-testing-convex/vitest-plugin";
18
+ *
19
+ * export default defineConfig({
20
+ * plugins: [convexTestProviderPlugin()],
21
+ * });
22
+ * ```
23
+ */
24
+ export function convexTestProviderPlugin() {
25
+ // require.resolve("@convex-dev/auth/dist/react/client.js") throws
26
+ // ERR_PACKAGE_PATH_NOT_EXPORTED because the path isn't in exports.
27
+ // Resolve from the consumer's project root (not from this plugin's
28
+ // location) so the alias points to the same copy that useAuthActions()
29
+ // uses — avoiding React Context duplication.
30
+ const require = createRequire(resolve(process.cwd(), "package.json"));
31
+ const mainEntry = require.resolve("@convex-dev/auth/react");
32
+ const pkgName = "@convex-dev/auth";
33
+ const authRoot = mainEntry.slice(0, mainEntry.indexOf(pkgName) + pkgName.length);
34
+ const resolved = resolve(authRoot, "dist/react/client.js");
35
+ return {
36
+ name: "feather-testing-convex",
37
+ config() {
38
+ return {
39
+ resolve: {
40
+ alias: {
41
+ "@convex-dev/auth/dist/react/client.js": resolved,
42
+ },
43
+ },
44
+ };
45
+ },
46
+ };
47
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "feather-testing-convex",
3
+ "version": "0.4.0",
4
+ "description": "React provider that adapts convex-test's one-shot client for use with ConvexProvider, so useQuery/useMutation work in tests.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "convex",
8
+ "testing",
9
+ "react",
10
+ "provider",
11
+ "convex-test"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/siraj-samsudeen/feather-testing-convex.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/siraj-samsudeen/feather-testing-convex/issues"
19
+ },
20
+ "homepage": "https://github.com/siraj-samsudeen/feather-testing-convex#readme",
21
+ "type": "module",
22
+ "main": "dist/index.js",
23
+ "module": "dist/index.js",
24
+ "types": "dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./vitest-plugin": {
32
+ "types": "./dist/vitest-plugin.d.ts",
33
+ "import": "./dist/vitest-plugin.js",
34
+ "default": "./dist/vitest-plugin.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "peerDependencies": {
41
+ "@convex-dev/auth": ">=0.0.80",
42
+ "@testing-library/react": ">=14.0.0",
43
+ "convex": ">=1.0.0",
44
+ "convex-test": ">=0.0.1",
45
+ "react": ">=18.0.0",
46
+ "vitest": ">=1.0.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@testing-library/react": {
50
+ "optional": true
51
+ },
52
+ "@convex-dev/auth": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "scripts": {
57
+ "build": "tsc",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest"
60
+ },
61
+ "devDependencies": {
62
+ "@convex-dev/auth": "^0.0.90",
63
+ "@testing-library/dom": "^10.4.1",
64
+ "@testing-library/jest-dom": "^6.9.1",
65
+ "@testing-library/react": "^16.3.2",
66
+ "@testing-library/user-event": "^14.6.1",
67
+ "@types/node": "^25.2.3",
68
+ "@types/react": "^19.2.10",
69
+ "@vitejs/plugin-react": "^5.1.3",
70
+ "convex": "^1.31.7",
71
+ "convex-test": "^0.0.41",
72
+ "jsdom": "^28.0.0",
73
+ "react": "^19.2.4",
74
+ "typescript": "~5.9.3",
75
+ "vitest": "^4.0.18"
76
+ }
77
+ }