-
Notifications
You must be signed in to change notification settings - Fork 120
publish zeroForTest as potential alternative to #5225
#5392
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d63a3a6
0d73c52
0472362
d8e5680
8ad83a1
ec390c0
e470d02
998065b
50dddfc
11c1598
480478e
8dcc9f6
2d2f6d9
389db1b
59cd735
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import {expect, test, vi, beforeEach, afterEach} from 'vitest'; | ||
| import {zeroForTest} from '@rocicorp/zero/testing'; | ||
| import {builder, schema} from '../shared/schema.ts'; | ||
| import {mutators} from '../shared/mutators.ts'; | ||
| import {defineMutator, defineMutators} from '@rocicorp/zero'; | ||
| import {z} from 'zod/mini'; | ||
|
|
||
| const userSchema = z.object({ | ||
| id: z.string(), | ||
| login: z.string(), | ||
| name: z.string(), | ||
| role: z.union([z.literal('user'), z.literal('crew')]), | ||
| }); | ||
|
|
||
| // Use the merge overload to inherit context type from the base mutators | ||
| const mutatorsForTest = defineMutators(mutators, { | ||
| user: { | ||
| create: defineMutator(userSchema, async ({tx, args}) => { | ||
| await tx.mutate.user.insert({ | ||
| id: args.id, | ||
| login: args.login, | ||
| name: args.name, | ||
| role: args.role, | ||
| avatar: '', | ||
| }); | ||
| }), | ||
| }, | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| vi.setSystemTime(new Date('2025-01-15T12:00:00Z')); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| test('local mutate', async () => { | ||
| const zero = zeroForTest({ | ||
| cacheURL: null, | ||
| kvStore: 'mem', | ||
| schema, | ||
| mutators: mutatorsForTest, | ||
| userID: 'user-1', | ||
| // oxlint-disable-next-line no-explicit-any | ||
| context: {sub: 'user-1', role: 'user'} as any, | ||
| }); | ||
|
|
||
| await zero.mutate( | ||
| mutatorsForTest.user.create({ | ||
| id: 'user-1', | ||
| login: 'holden', | ||
| name: 'James Holden', | ||
| role: 'user', | ||
| }), | ||
| ).client; | ||
|
|
||
| await zero.mutate( | ||
| mutatorsForTest.issue.create({ | ||
| id: 'issue-1', | ||
| title: 'Test Issue', | ||
| description: 'This is a test issue', | ||
| projectID: 'project-1', | ||
| created: Date.now(), | ||
| modified: Date.now(), | ||
| }), | ||
| ).client; | ||
|
|
||
| const issues = await zero.run(builder.issue); | ||
|
|
||
| expect(issues).toMatchInlineSnapshot(` | ||
| [ | ||
| { | ||
| "assigneeID": null, | ||
| "created": 1736942400000, | ||
| "creatorID": "user-1", | ||
| "description": "This is a test issue", | ||
| "id": "issue-1", | ||
| "modified": 1736942400000, | ||
| "open": true, | ||
| "projectID": "project-1", | ||
| "shortID": null, | ||
| "title": "Test Issue", | ||
| "visibility": "public", | ||
| Symbol(rc): 1, | ||
| }, | ||
| ] | ||
| `); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export {zeroForTest} from '../../zero-client/src/client/test-utils.ts'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,11 @@ const define = { | |
| 'process.env.DISABLE_MUTATION_RECOVERY': 'true', | ||
| }; | ||
|
|
||
| const testingDefine = { | ||
| ...define, | ||
| TESTING: 'true', | ||
| }; | ||
|
|
||
| // Vite config helper functions | ||
| async function getPackageJSON() { | ||
| const content = await readFile(resolve('package.json'), 'utf-8'); | ||
|
|
@@ -53,10 +58,14 @@ function extractOutPath(path: string): string | undefined { | |
| function extractEntries( | ||
| entries: Record<string, unknown>, | ||
| getEntryName: (key: string, outPath: string) => string, | ||
| excludeKeys: Set<string> = new Set(), | ||
| ): Record<string, string> { | ||
| const entryPoints: Record<string, string> = {}; | ||
|
|
||
| for (const [key, value] of Object.entries(entries)) { | ||
| if (excludeKeys.has(key)) { | ||
| continue; | ||
| } | ||
| const path = | ||
| typeof value === 'string' ? value : (value as {default?: string}).default; | ||
|
|
||
|
|
@@ -94,18 +103,37 @@ function getWorkerEntryPoints(): Record<string, string> { | |
| return entryPoints; | ||
| } | ||
|
|
||
| // Entry points that need TESTING=true | ||
| const testingExports = new Set(['./testing']); | ||
|
|
||
| async function getAllEntryPoints(): Promise<Record<string, string>> { | ||
| const packageJSON = await getPackageJSON(); | ||
|
|
||
| return { | ||
| ...extractEntries(packageJSON.exports ?? {}, (key, outPath) => | ||
| key === '.' ? 'zero/src/zero' : outPath, | ||
| ...extractEntries( | ||
| packageJSON.exports ?? {}, | ||
| (key, outPath) => (key === '.' ? 'zero/src/zero' : outPath), | ||
| testingExports, | ||
| ), | ||
| ...extractEntries(packageJSON.bin ?? {}, (_, outPath) => outPath), | ||
| ...getWorkerEntryPoints(), | ||
| }; | ||
| } | ||
|
|
||
| async function getTestingEntryPoints(): Promise<Record<string, string>> { | ||
| const packageJSON = await getPackageJSON(); | ||
|
|
||
| return extractEntries( | ||
| packageJSON.exports ?? {}, | ||
| (key, outPath) => (key === '.' ? 'zero/src/zero' : outPath), | ||
| new Set( | ||
| Object.keys(packageJSON.exports ?? {}).filter( | ||
| k => !testingExports.has(k), | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| const baseConfig: InlineConfig = { | ||
| configFile: false, | ||
| logLevel: 'warn', | ||
|
|
@@ -143,6 +171,58 @@ async function getViteConfig(): Promise<InlineConfig> { | |
| }; | ||
| } | ||
|
|
||
| // Modules that should be kept external in the testing build to share | ||
| // symbol instances with the main bundle | ||
| const testingExternalModules = ['query-internals']; | ||
|
Comment on lines
+174
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is correct? Don't we want to share everything? It is a bit strange because we do not want to get copies of the code but the code needs have have TESTING true. I'm really not sure how to square this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should get rid of the TESTING flag and use import.meta.env or process.env?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we should get rid of the TESTING flag and use import.meta.env or process.env? |
||
|
|
||
| async function getTestingViteConfig(): Promise<InlineConfig> { | ||
| return { | ||
| ...baseConfig, | ||
| define: testingDefine, | ||
| build: { | ||
| ...baseConfig.build, | ||
| rollupOptions: { | ||
| external: (id: string) => { | ||
| // Check standard externals | ||
| if (external.some(ext => id === ext || id.startsWith(ext + '/'))) { | ||
| return true; | ||
| } | ||
| // Keep certain modules external so testing bundle shares symbols | ||
| // with the main bundle (e.g., queryInternalsTag) | ||
| return testingExternalModules.some(mod => id.includes(mod)); | ||
| }, | ||
| input: await getTestingEntryPoints(), | ||
| output: { | ||
| format: 'es', | ||
| entryFileNames: '[name].js', | ||
| chunkFileNames: 'chunks/[name]-[hash].js', | ||
| // Don't preserve modules for testing - bundle dependencies so they | ||
| // get the TESTING=true define | ||
| preserveModules: false, | ||
| // Rewrite external module paths to point to the built output | ||
| paths: (id: string) => { | ||
| for (const mod of testingExternalModules) { | ||
| if (id.includes(mod)) { | ||
| // Convert absolute path to relative path from testing.js output | ||
| // testing.js is at out/zero/src/testing.js | ||
| // query-internals.js is at out/zql/src/query/query-internals.js | ||
| // Correct relative path: ../../zql/src/query/query-internals.js | ||
| // Vite prepends ../../ for output at zero/src/, so we just return | ||
| // the package-relative path | ||
| const match = id.match(/packages\/([^/]+\/src\/.+)\.ts$/); | ||
| if (match) { | ||
| return `${match[1]}.js`; | ||
| } | ||
| } | ||
| } | ||
| return id; | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| // Bundle size dashboard config: single entry, no code splitting, minified | ||
| // Uses esbuild's dropLabels to strip BUNDLE_SIZE labeled code blocks | ||
| const bundleSizeConfig: InlineConfig = { | ||
|
|
@@ -233,8 +313,11 @@ async function build() { | |
| // Watch mode: run vite and tsc in watch mode | ||
| const viteConfig = await getViteConfig(); | ||
| viteConfig.build = {...viteConfig.build, watch: {}}; | ||
| const testingConfig = await getTestingViteConfig(); | ||
| testingConfig.build = {...testingConfig.build, watch: {}}; | ||
| await Promise.all([ | ||
| runViteBuild(viteConfig, 'vite build (watch)'), | ||
| runViteBuild(testingConfig, 'vite build (testing watch)'), | ||
| exec( | ||
| 'tsc -p tsconfig.client.json --watch --preserveWatchOutput', | ||
| 'client dts (watch)', | ||
|
|
@@ -247,8 +330,10 @@ async function build() { | |
| } else { | ||
| // Normal build: use inline vite config + type declarations | ||
| const viteConfig = await getViteConfig(); | ||
| const testingConfig = await getTestingViteConfig(); | ||
| await Promise.all([ | ||
| runViteBuild(viteConfig, 'vite build'), | ||
| runViteBuild(testingConfig, 'vite build (testing)'), | ||
| exec('tsc -p tsconfig.client.json', 'client dts'), | ||
| exec('tsc -p tsconfig.server.json', 'server dts'), | ||
| ]); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
example of how you could use
zeroForTest