|
24 | 24 | // mocks.fn(name, returnValue?) -- async mock, returns Promise.resolve(returnValue) |
25 | 25 | // mocks.fnSync(name, returnValue?) -- sync mock, returns returnValue (or a valid hex string) |
26 | 26 | // mocks.trackedObject(name) -- Proxy that records all method calls |
| 27 | +// |
| 28 | +// Note: optional/nullable fields are excluded from automatic testing because |
| 29 | +// they can't be given two distinct non-undefined values without knowing the |
| 30 | +// context (e.g. a refine may reject them). Use the overrides parameter to |
| 31 | +// test optional fields that matter: |
| 32 | +// |
| 33 | +// await assertSchemaCoverage(schema, execute, mocks, { |
| 34 | +// 'optionalField': (base) => ({ ...base, optionalField: 'some valid value' }), |
| 35 | +// }); |
27 | 36 |
|
28 | 37 | import { vi } from 'vitest'; |
29 | 38 | import { type ZodType, type z } from 'zod'; |
@@ -82,8 +91,9 @@ const _mocks = vi.hoisted(() => { |
82 | 91 | export const mocks = _mocks; |
83 | 92 |
|
84 | 93 | // -- Module mocks ------------------------------------------------------------ |
85 | | -// vi.mock is statically analyzed and hoisted in any file that imports this |
86 | | -// module, so these take effect before imports in the consuming test file. |
| 94 | +// vi.mock calls are processed by vitest's module transform regardless of |
| 95 | +// which file they appear in, so these mocks are active when the consuming |
| 96 | +// test file's imports resolve. |
87 | 97 |
|
88 | 98 | vi.mock('./viemTransforms', () => ({ |
89 | 99 | toPublicClient: (rpcUrl: string, chain: unknown) => |
@@ -176,7 +186,7 @@ vi.mock('../createRollup', () => ({ |
176 | 186 | createRollup: _mocks.fn('createRollup', { coreContracts: {} }), |
177 | 187 | })); |
178 | 188 | vi.mock('../setValidKeyset', () => ({ setValidKeyset: _mocks.fn('setValidKeyset') })); |
179 | | -vi.mock('../utils/generateChainId', () => ({ generateChainId: () => 999999 })); |
| 189 | +vi.mock('../utils/generateChainId', () => ({ generateChainId: _mocks.fnSync('generateChainId', 999999) })); |
180 | 190 | vi.mock('../upgradeExecutorPrepareAddExecutorTransactionRequest', () => ({ |
181 | 191 | upgradeExecutorPrepareAddExecutorTransactionRequest: _mocks.fn('addExecutor'), |
182 | 192 | })); |
@@ -371,6 +381,16 @@ export async function assertSchemaCoverage<T extends ZodType>( |
371 | 381 | overrides?: Record<string, (base: z.input<T>) => z.input<T>>, |
372 | 382 | ): Promise<void> { |
373 | 383 | const leaves = getSchemaLeaves(schema); |
| 384 | + const testableLeaves = leaves.filter((l) => { |
| 385 | + const t = getDefType(l.schema); |
| 386 | + return t !== 'literal' && t !== 'null'; |
| 387 | + }); |
| 388 | + if (testableLeaves.length === 0) { |
| 389 | + throw new Error( |
| 390 | + 'assertSchemaCoverage found 0 testable fields. ' + |
| 391 | + 'The schema may be empty or getSchemaLeaves may not support a type it uses.', |
| 392 | + ); |
| 393 | + } |
374 | 394 |
|
375 | 395 | resetCounter(); |
376 | 396 | const valuesA = new Map<string, unknown>(); |
|
0 commit comments