@gnapi/cotester 1.2.3 → 1.2.5

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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Abstracts test-framework-specific syntax so the generator can emit
3
+ * either Jest or Vitest output without conditional logic scattered everywhere.
4
+ */
5
+ export interface TestFrameworkAdapter {
6
+ /** Top-level import line(s) required by the framework (empty for Jest). */
7
+ frameworkImport(): string;
8
+ /** Produce `jest.mock(...)` / `vi.mock(...)` call for a module. */
9
+ mockModule(importPath: string, importedNames: string[]): string;
10
+ /** Check string used to detect an existing mock in a file. */
11
+ mockModuleCheck(importPath: string): string;
12
+ /** `jest.fn()` / `vi.fn()` */
13
+ mockFn(): string;
14
+ /** `jest.clearAllMocks()` / `vi.clearAllMocks()` */
15
+ clearAllMocks(): string;
16
+ /**
17
+ * Cast expression prefix for setting up a mock return value on a named import.
18
+ * Jest: `(name as jest.Mock)`
19
+ * Vitest: `vi.mocked(name)`
20
+ */
21
+ mockCast(name: string): string;
22
+ /** `<cast>.mockResolvedValue(val)` */
23
+ mockResolvedValue(name: string, val: string): string;
24
+ /** `<cast>.mockReturnValue(val)` */
25
+ mockReturnValue(name: string, val: string): string;
26
+ /** `jest.fn().mockReturnValue(val)` / `vi.fn().mockReturnValue(val)` — for inline chained stubs */
27
+ mockFnReturnValue(val: string): string;
28
+ }
29
+ export declare class JestAdapter implements TestFrameworkAdapter {
30
+ frameworkImport(): string;
31
+ mockModule(importPath: string, importedNames: string[]): string;
32
+ mockModuleCheck(importPath: string): string;
33
+ mockFn(): string;
34
+ clearAllMocks(): string;
35
+ mockCast(name: string): string;
36
+ mockResolvedValue(name: string, val: string): string;
37
+ mockReturnValue(name: string, val: string): string;
38
+ mockFnReturnValue(val: string): string;
39
+ }
40
+ export declare class VitestAdapter implements TestFrameworkAdapter {
41
+ frameworkImport(): string;
42
+ mockModule(importPath: string, importedNames: string[]): string;
43
+ mockModuleCheck(importPath: string): string;
44
+ mockFn(): string;
45
+ clearAllMocks(): string;
46
+ mockCast(name: string): string;
47
+ mockResolvedValue(name: string, val: string): string;
48
+ mockReturnValue(name: string, val: string): string;
49
+ mockFnReturnValue(val: string): string;
50
+ }
51
+ export declare function createAdapter(framework?: "jest" | "vitest"): TestFrameworkAdapter;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * Abstracts test-framework-specific syntax so the generator can emit
4
+ * either Jest or Vitest output without conditional logic scattered everywhere.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.VitestAdapter = exports.JestAdapter = void 0;
8
+ exports.createAdapter = createAdapter;
9
+ // ── Jest adapter ──────────────────────────────────────────────────────────────
10
+ class JestAdapter {
11
+ frameworkImport() {
12
+ return "";
13
+ }
14
+ mockModule(importPath, importedNames) {
15
+ const fns = importedNames.map((n) => ` ${n}: jest.fn(),`).join("\n");
16
+ return `jest.mock('${importPath}', () => ({\n${fns}\n}));`;
17
+ }
18
+ mockModuleCheck(importPath) {
19
+ return `jest.mock('${importPath}'`;
20
+ }
21
+ mockFn() {
22
+ return "jest.fn()";
23
+ }
24
+ clearAllMocks() {
25
+ return "jest.clearAllMocks();";
26
+ }
27
+ mockCast(name) {
28
+ return `(${name} as jest.Mock)`;
29
+ }
30
+ mockResolvedValue(name, val) {
31
+ return `${this.mockCast(name)}.mockResolvedValue(${val});`;
32
+ }
33
+ mockReturnValue(name, val) {
34
+ return `${this.mockCast(name)}.mockReturnValue(${val});`;
35
+ }
36
+ mockFnReturnValue(val) {
37
+ return `jest.fn().mockReturnValue(${val})`;
38
+ }
39
+ }
40
+ exports.JestAdapter = JestAdapter;
41
+ // ── Vitest adapter ────────────────────────────────────────────────────────────
42
+ class VitestAdapter {
43
+ frameworkImport() {
44
+ return "import { describe, test, expect, vi, beforeEach } from 'vitest';";
45
+ }
46
+ mockModule(importPath, importedNames) {
47
+ const fns = importedNames.map((n) => ` ${n}: vi.fn(),`).join("\n");
48
+ return `vi.mock('${importPath}', () => ({\n${fns}\n}));`;
49
+ }
50
+ mockModuleCheck(importPath) {
51
+ return `vi.mock('${importPath}'`;
52
+ }
53
+ mockFn() {
54
+ return "vi.fn()";
55
+ }
56
+ clearAllMocks() {
57
+ return "vi.clearAllMocks();";
58
+ }
59
+ mockCast(name) {
60
+ return `vi.mocked(${name})`;
61
+ }
62
+ mockResolvedValue(name, val) {
63
+ return `${this.mockCast(name)}.mockResolvedValue(${val});`;
64
+ }
65
+ mockReturnValue(name, val) {
66
+ return `${this.mockCast(name)}.mockReturnValue(${val});`;
67
+ }
68
+ mockFnReturnValue(val) {
69
+ return `vi.fn().mockReturnValue(${val})`;
70
+ }
71
+ }
72
+ exports.VitestAdapter = VitestAdapter;
73
+ // ── Factory ───────────────────────────────────────────────────────────────────
74
+ function createAdapter(framework = "jest") {
75
+ return framework === "vitest" ? new VitestAdapter() : new JestAdapter();
76
+ }
77
+ //# sourceMappingURL=frameworkAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frameworkAdapter.js","sourceRoot":"","sources":["../src/frameworkAdapter.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAuHH,sCAEC;AAtFD,iFAAiF;AAEjF,MAAa,WAAW;IACpB,eAAe;QACX,OAAO,EAAE,CAAC;IACd,CAAC;IAED,UAAU,CAAC,UAAkB,EAAE,aAAuB;QAClD,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtE,OAAO,cAAc,UAAU,gBAAgB,GAAG,QAAQ,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,UAAkB;QAC9B,OAAO,cAAc,UAAU,GAAG,CAAC;IACvC,CAAC;IAED,MAAM;QACF,OAAO,WAAW,CAAC;IACvB,CAAC;IAED,aAAa;QACT,OAAO,uBAAuB,CAAC;IACnC,CAAC;IAED,QAAQ,CAAC,IAAY;QACjB,OAAO,IAAI,IAAI,gBAAgB,CAAC;IACpC,CAAC;IAED,iBAAiB,CAAC,IAAY,EAAE,GAAW;QACvC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,GAAW;QACrC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7D,CAAC;IAED,iBAAiB,CAAC,GAAW;QACzB,OAAO,6BAA6B,GAAG,GAAG,CAAC;IAC/C,CAAC;CACJ;AArCD,kCAqCC;AAED,iFAAiF;AAEjF,MAAa,aAAa;IACtB,eAAe;QACX,OAAO,kEAAkE,CAAC;IAC9E,CAAC;IAED,UAAU,CAAC,UAAkB,EAAE,aAAuB;QAClD,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpE,OAAO,YAAY,UAAU,gBAAgB,GAAG,QAAQ,CAAC;IAC7D,CAAC;IAED,eAAe,CAAC,UAAkB;QAC9B,OAAO,YAAY,UAAU,GAAG,CAAC;IACrC,CAAC;IAED,MAAM;QACF,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,aAAa;QACT,OAAO,qBAAqB,CAAC;IACjC,CAAC;IAED,QAAQ,CAAC,IAAY;QACjB,OAAO,aAAa,IAAI,GAAG,CAAC;IAChC,CAAC;IAED,iBAAiB,CAAC,IAAY,EAAE,GAAW;QACvC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB,GAAG,IAAI,CAAC;IAC/D,CAAC;IAED,eAAe,CAAC,IAAY,EAAE,GAAW;QACrC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7D,CAAC;IAED,iBAAiB,CAAC,GAAW;QACzB,OAAO,2BAA2B,GAAG,GAAG,CAAC;IAC7C,CAAC;CACJ;AArCD,sCAqCC;AAED,iFAAiF;AAEjF,SAAgB,aAAa,CAAC,YAA+B,MAAM;IAC/D,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC;AAC5E,CAAC"}
@@ -13,4 +13,4 @@ import { MockFileResult } from "./mockGenerator";
13
13
  * Individual functions/methods that already have a describe block are never
14
14
  * touched, preserving any hand-written assertions inside them.
15
15
  */
16
- export declare function generateTestFile(srcFilePath: string, scenarios: TestScenario[], projectRoot: string, srcDir: string, testDir: string, mockResult: MockFileResult | null, tableThreshold?: number): Promise<string | null>;
16
+ export declare function generateTestFile(srcFilePath: string, scenarios: TestScenario[], projectRoot: string, srcDir: string, testDir: string, mockResult: MockFileResult | null, tableThreshold?: number, dryRun?: boolean, framework?: "jest" | "vitest", snapshotForComplexTypes?: boolean): Promise<string | null>;
package/dist/generator.js CHANGED
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
39
39
  const formatter_1 = require("./formatter");
40
40
  const utils_1 = require("./utils");
41
41
  const ormMockGenerator_1 = require("./ormMockGenerator");
42
+ const frameworkAdapter_1 = require("./frameworkAdapter");
42
43
  /**
43
44
  * Marker placed as the very first line of every generated test file.
44
45
  * Its presence lets cotester know the file was scaffolded (not hand-written),
@@ -58,23 +59,35 @@ const TESTGEN_MARKER = "// @cotester-generated";
58
59
  * Individual functions/methods that already have a describe block are never
59
60
  * touched, preserving any hand-written assertions inside them.
60
61
  */
61
- async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, testDir, mockResult, tableThreshold = 0) {
62
+ async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, testDir, mockResult, tableThreshold = 0, dryRun = false, framework = "jest", snapshotForComplexTypes = false) {
62
63
  if (scenarios.length === 0) {
63
64
  return null;
64
65
  }
66
+ const adapter = (0, frameworkAdapter_1.createAdapter)(framework);
65
67
  const absSrcRoot = path.resolve(projectRoot, srcDir);
66
68
  const testFilePath = (0, utils_1.mirrorPath)(srcFilePath, absSrcRoot, path.join(projectRoot, testDir), ".test.ts");
69
+ // ── Dry-run: print generated content without touching the filesystem ──
70
+ if (dryRun) {
71
+ const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
72
+ const formatted = await (0, formatter_1.formatCode)(code, projectRoot);
73
+ const relTest = path.relative(projectRoot, testFilePath).replace(/\\/g, "/");
74
+ process.stdout.write(`\n${"─".repeat(60)}\n`);
75
+ process.stdout.write(`// [dry-run] Test file: ${relTest}\n`);
76
+ process.stdout.write(`${"─".repeat(60)}\n`);
77
+ process.stdout.write(formatted);
78
+ return testFilePath;
79
+ }
67
80
  (0, utils_1.ensureDir)(path.dirname(testFilePath));
68
81
  // ── Brand-new file → generate everything ─────────────────────────────
69
82
  if (!fs.existsSync(testFilePath)) {
70
- const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
83
+ const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
71
84
  const formatted = await (0, formatter_1.formatCode)(code, projectRoot);
72
85
  fs.writeFileSync(testFilePath, formatted, "utf-8");
73
86
  (0, utils_1.log)(`Test file → ${path.relative(projectRoot, testFilePath)}`, "success");
74
87
  return testFilePath;
75
88
  }
76
89
  // ── File exists → function-level merge ───────────────────────────────
77
- return mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
90
+ return mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
78
91
  }
79
92
  /**
80
93
  * Merge missing function/method describe blocks into an already-existing test file.
@@ -87,7 +100,7 @@ async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, tes
87
100
  * 5. For entire new classes → append a full class describe block.
88
101
  * 6. For any new dependencies (jest.mock) not already in the file → prepend them.
89
102
  */
90
- async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0) {
103
+ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
91
104
  let content = fs.readFileSync(testFilePath, "utf-8");
92
105
  const existingDescribes = detectExistingDescribes(content);
93
106
  // ── Feature 6: signature-change detection for already-covered describes ──
@@ -129,7 +142,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
129
142
  for (const [className, methods] of classMap) {
130
143
  if (!existingDescribes.has(className)) {
131
144
  // Entire class is new → append full class describe block
132
- toAppend.push(buildClassDescribe(className, methods, mockResult, tableThreshold));
145
+ toAppend.push(buildClassDescribe(className, methods, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
133
146
  newScenarios.push(...methods);
134
147
  }
135
148
  else {
@@ -137,7 +150,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
137
150
  const missingMethods = methods.filter(m => !existingDescribes.has(m.functionName));
138
151
  if (missingMethods.length > 0) {
139
152
  const methodBlocks = missingMethods
140
- .map(m => indentBlock(buildMethodDescribe(m, mockResult, tableThreshold), 1))
153
+ .map(m => indentBlock(buildMethodDescribe(m, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 1))
141
154
  .join("\n\n");
142
155
  content = injectIntoClassDescribe(content, className, methodBlocks);
143
156
  newScenarios.push(...missingMethods);
@@ -146,7 +159,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
146
159
  }
147
160
  // ── Standalone missing functions ─────────────────────────────────────
148
161
  for (const fn of missingStandalone) {
149
- toAppend.push(buildFunctionDescribe(fn, mockResult, tableThreshold));
162
+ toAppend.push(buildFunctionDescribe(fn, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
150
163
  }
151
164
  if (toAppend.length === 0 && newScenarios.length === 0) {
152
165
  (0, utils_1.log)(` ${path.relative(projectRoot, testFilePath)} — all functions already covered.`, "info");
@@ -156,8 +169,8 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
156
169
  if (toAppend.length > 0) {
157
170
  content = content.trimEnd() + "\n\n" + toAppend.join("\n\n") + "\n";
158
171
  }
159
- // ── Prepend missing jest.mock() / import lines ───────────────────────
160
- content = addMissingMocks(content, newScenarios, srcFilePath, testFilePath);
172
+ // ── Prepend missing mock() / import lines ────────────────────────────
173
+ content = addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapter);
161
174
  const formatted = await (0, formatter_1.formatCode)(content, projectRoot);
162
175
  fs.writeFileSync(testFilePath, formatted, "utf-8");
163
176
  const addedCount = newScenarios.length;
@@ -271,17 +284,16 @@ function injectIntoClassDescribe(content, className, newBlocks) {
271
284
  * are already mocked in the file. If not, prepend the missing jest.mock() calls
272
285
  * and import statements before the first existing import line.
273
286
  */
274
- function addMissingMocks(content, newScenarios, srcFilePath, testFilePath) {
287
+ function addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapter = (0, frameworkAdapter_1.createAdapter)("jest")) {
275
288
  const allDeps = collectAllDependencies(newScenarios);
276
289
  const mockLines = [];
277
290
  const importLines = [];
278
291
  for (const dep of allDeps) {
279
292
  const depAbsPath = resolveDepPath(srcFilePath, dep.modulePath);
280
293
  const depImportPath = computeRelativeImport(depAbsPath, testFilePath);
281
- if (!content.includes(`jest.mock('${depImportPath}'`) &&
282
- !content.includes(`jest.mock("${depImportPath}"`)) {
283
- const mockFns = dep.importedNames.map(n => ` ${n}: jest.fn(),`).join("\n");
284
- mockLines.push(`jest.mock('${depImportPath}', () => ({\n${mockFns}\n}));`);
294
+ const checkStr = adapter.mockModuleCheck(depImportPath);
295
+ if (!content.includes(checkStr) && !content.includes(checkStr.replace("'", '"'))) {
296
+ mockLines.push(adapter.mockModule(depImportPath, dep.importedNames));
285
297
  }
286
298
  const names = dep.importedNames.filter(n => n !== "default");
287
299
  if (names.length > 0) {
@@ -305,24 +317,25 @@ function escapeForRegex(str) {
305
317
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
306
318
  }
307
319
  // ─── Build the full test source ──────────────────────────────────────────────
308
- function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0) {
320
+ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
309
321
  const lines = [];
310
322
  // Marker — presence of this line tells cotester the file is auto-generated
311
323
  // and safe to regenerate. Remove it to protect hand-written edits.
312
324
  lines.push(TESTGEN_MARKER);
313
325
  lines.push("");
326
+ // ── Framework import (Vitest only; empty for Jest) ───────────────
327
+ const fwImport = adapter.frameworkImport();
328
+ if (fwImport) {
329
+ lines.push(fwImport);
330
+ lines.push("");
331
+ }
314
332
  // ── Collect all unique dependencies across scenarios ─────────────
315
333
  const allDeps = collectAllDependencies(scenarios);
316
- // ── jest.mock() calls (must appear before imports) ───────────────
334
+ // ── mock() calls (must appear before imports) ────────────────────
317
335
  for (const dep of allDeps) {
318
336
  const depAbsPath = resolveDepPath(srcFilePath, dep.modulePath);
319
337
  const depImportPath = computeRelativeImport(depAbsPath, testFilePath);
320
- const mockFns = dep.importedNames
321
- .map((n) => ` ${n}: jest.fn(),`)
322
- .join("\n");
323
- lines.push(`jest.mock('${depImportPath}', () => ({`);
324
- lines.push(mockFns);
325
- lines.push(`}));`);
338
+ lines.push(adapter.mockModule(depImportPath, dep.importedNames));
326
339
  lines.push("");
327
340
  }
328
341
  // ── Import the source module ────────────────────────────────────
@@ -371,18 +384,18 @@ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, proje
371
384
  }
372
385
  // ── Standalone functions ────────────────────────────────────────
373
386
  for (const fn of standaloneFns) {
374
- lines.push(buildFunctionDescribe(fn, mockResult, tableThreshold));
387
+ lines.push(buildFunctionDescribe(fn, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
375
388
  lines.push("");
376
389
  }
377
390
  // ── Class methods ───────────────────────────────────────────────
378
391
  for (const [className, methods] of classMap) {
379
- lines.push(buildClassDescribe(className, methods, mockResult, tableThreshold));
392
+ lines.push(buildClassDescribe(className, methods, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
380
393
  lines.push("");
381
394
  }
382
395
  return lines.join("\n");
383
396
  }
384
397
  // ─── Class describe block with constructor DI ────────────────────────────────
385
- function buildClassDescribe(className, methods, mockResult, tableThreshold = 0) {
398
+ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
386
399
  const lines = [];
387
400
  lines.push(`describe(${JSON.stringify(className)}, () => {`);
388
401
  lines.push(` let instance: ${className};`);
@@ -395,19 +408,21 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
395
408
  // If this constructor param matches the ORM variable, use ORM mocks
396
409
  if (ormUsage && (param.name === ormUsage.variableName || param.type.toLowerCase().includes(ormUsage.kind))) {
397
410
  const ormMocks = (0, ormMockGenerator_1.generateOrmMocks)(ormUsage);
398
- // Add ORM mock if not already declared at file level
399
- if (ormMocks.mockVarNames.length > 0) {
411
+ // Only alias if the file-level ORM var has a DIFFERENT name from the ctor param mock name
412
+ if (ormMocks.mockVarNames.length > 0 && ormMocks.mockVarNames[0] !== mockName) {
400
413
  mockDeclarations.push(` const ${mockName} = ${ormMocks.mockVarNames[0]}; // ORM mock`);
401
414
  }
402
- else {
415
+ else if (ormMocks.mockVarNames.length === 0) {
403
416
  mockDeclarations.push(` const ${mockName} = { } as any; // TODO: add mock methods for ${param.type}`);
404
417
  }
418
+ // else: same name — variable already declared at file level; no re-declaration needed
405
419
  }
406
420
  else {
407
421
  // Generate smart mock based on type name
408
422
  const typeLower = param.type.toLowerCase();
409
423
  if (typeLower.includes("repository") || typeLower.includes("repo")) {
410
- mockDeclarations.push(` const ${mockName} = { find: jest.fn(), findById: jest.fn(), save: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn() } as any;`);
424
+ const fn = adapter.mockFn();
425
+ mockDeclarations.push(` const ${mockName} = { find: ${fn}, findById: ${fn}, save: ${fn}, create: ${fn}, update: ${fn}, delete: ${fn} } as any;`);
411
426
  }
412
427
  else if (typeLower.includes("service")) {
413
428
  mockDeclarations.push(` const ${mockName} = { } as any; // TODO: mock ${param.type} methods`);
@@ -425,7 +440,7 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
425
440
  }
426
441
  lines.push("");
427
442
  lines.push(" beforeEach(() => {");
428
- lines.push(" jest.clearAllMocks();");
443
+ lines.push(` ${adapter.clearAllMocks()}`);
429
444
  if (ctorParams.length > 0) {
430
445
  const args = ctorParams
431
446
  .map((p) => `mock${capitalize(p.name)}`)
@@ -446,66 +461,66 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
446
461
  lines.push(" params,");
447
462
  lines.push(" query,");
448
463
  lines.push(" headers: { 'Content-Type': 'application/json' },");
449
- lines.push(" get: jest.fn(),");
464
+ lines.push(` get: ${adapter.mockFn()},`);
450
465
  lines.push(" } as any);");
451
466
  lines.push("");
452
467
  lines.push(" const mockResponse = () => {");
453
468
  lines.push(" const res: any = {};");
454
- lines.push(" res.status = jest.fn().mockReturnValue(res);");
455
- lines.push(" res.json = jest.fn().mockReturnValue(res);");
456
- lines.push(" res.send = jest.fn().mockReturnValue(res);");
457
- lines.push(" res.set = jest.fn().mockReturnValue(res);");
469
+ lines.push(` res.status = ${adapter.mockFnReturnValue("res")};`);
470
+ lines.push(` res.json = ${adapter.mockFnReturnValue("res")};`);
471
+ lines.push(` res.send = ${adapter.mockFnReturnValue("res")};`);
472
+ lines.push(` res.set = ${adapter.mockFnReturnValue("res")};`);
458
473
  lines.push(" return res;");
459
474
  lines.push(" };");
460
475
  lines.push("");
461
- lines.push(" const mockNext = jest.fn();");
476
+ lines.push(` const mockNext = ${adapter.mockFn()};`);
462
477
  }
463
478
  lines.push("");
464
479
  for (const method of methods) {
465
- lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold), 1));
480
+ lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 1));
466
481
  lines.push("");
467
482
  }
468
483
  lines.push("});");
469
484
  return lines.join("\n");
470
485
  }
471
486
  // ─── describe block for a standalone function ────────────────────────────────
472
- function buildFunctionDescribe(fn, mockResult, tableThreshold = 0) {
487
+ function buildFunctionDescribe(fn, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
473
488
  const lines = [];
474
489
  lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
475
490
  lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
476
491
  // Add beforeEach with clearAllMocks if this function has dependencies
477
492
  if (fn.dependencies.length > 0) {
478
493
  lines.push(" beforeEach(() => {");
479
- lines.push(" jest.clearAllMocks();");
494
+ lines.push(` ${adapter.clearAllMocks()}`);
480
495
  lines.push(" });");
481
496
  lines.push("");
482
497
  }
483
498
  const constName = mockResult?.constantNames.get(fn.functionName);
484
499
  const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
485
500
  if (tableCases.length > 0) {
486
- lines.push(indentBlock(buildTableBlock(fn, tableCases), 1));
501
+ lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
487
502
  lines.push("");
488
503
  }
489
504
  for (const c of individualCases) {
490
- lines.push(indentBlock(buildTestBlock(fn, c, constName), 1));
505
+ lines.push(indentBlock(buildTestBlock(fn, c, constName, adapter, snapshotForComplexTypes), 1));
491
506
  lines.push("");
492
507
  }
493
508
  lines.push("});");
494
509
  return lines.join("\n");
495
510
  }
496
511
  // ─── describe block for a class method ───────────────────────────────────────
497
- function buildMethodDescribe(fn, mockResult, tableThreshold = 0) {
512
+ function buildMethodDescribe(fn, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
498
513
  const lines = [];
499
514
  lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
500
515
  lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
501
516
  const constName = mockResult?.constantNames.get(fn.functionName);
502
517
  const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
503
518
  if (tableCases.length > 0) {
504
- lines.push(indentBlock(buildTableBlock(fn, tableCases), 1));
519
+ lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
505
520
  lines.push("");
506
521
  }
507
522
  for (const c of individualCases) {
508
- lines.push(indentBlock(buildTestBlock(fn, c, constName), 1));
523
+ lines.push(indentBlock(buildTestBlock(fn, c, constName, adapter, snapshotForComplexTypes), 1));
509
524
  lines.push("");
510
525
  }
511
526
  lines.push("});");
@@ -539,7 +554,7 @@ function partitionCases(cases, paramCount, tableThreshold) {
539
554
  * Emit a `test.each([...])('%s', ...)` block for a group of standard cases.
540
555
  * Each row is `[label, ...argLiterals]` so the Jest title shows the scenario name.
541
556
  */
542
- function buildTableBlock(scenario, tableCases) {
557
+ function buildTableBlock(scenario, tableCases, snapshotForComplexTypes = false) {
543
558
  const lines = [];
544
559
  const isAsync = scenario.isAsync;
545
560
  const isMethod = !!scenario.className;
@@ -563,7 +578,7 @@ function buildTableBlock(scenario, tableCases) {
563
578
  lines.push(``);
564
579
  // Assert
565
580
  lines.push(` // Assert`);
566
- const expectLines = buildExpectStubs(scenario.returnType, "");
581
+ const expectLines = buildExpectStubs(scenario.returnType, "", snapshotForComplexTypes);
567
582
  for (const e of expectLines) {
568
583
  lines.push(` ${e}`);
569
584
  }
@@ -571,18 +586,18 @@ function buildTableBlock(scenario, tableCases) {
571
586
  return lines.join("\n");
572
587
  }
573
588
  // ─── Individual test block ───────────────────────────────────────────────────
574
- function buildTestBlock(scenario, testCase, mockConstName) {
589
+ function buildTestBlock(scenario, testCase, mockConstName, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
575
590
  switch (testCase.kind) {
576
591
  case "throws":
577
592
  return buildThrowsBlock(scenario, testCase, mockConstName);
578
593
  case "rejects":
579
594
  return buildRejectsBlock(scenario, testCase, mockConstName);
580
595
  default:
581
- return buildStandardBlock(scenario, testCase, mockConstName);
596
+ return buildStandardBlock(scenario, testCase, mockConstName, adapter, snapshotForComplexTypes);
582
597
  }
583
598
  }
584
599
  // ─── Standard test block (Arrange / Act / Assert) ────────────────────────────
585
- function buildStandardBlock(scenario, testCase, mockConstName) {
600
+ function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
586
601
  const lines = [];
587
602
  const fnName = scenario.functionName;
588
603
  const isAsync = scenario.isAsync;
@@ -592,21 +607,29 @@ function buildStandardBlock(scenario, testCase, mockConstName) {
592
607
  // ── Arrange ──────────────────────────────────────────────────────
593
608
  const caseKey = camelCase(testCase.label);
594
609
  if (isDependencyTest) {
595
- // Emit mockReturnValue / mockResolvedValue so the SUT doesn't receive undefined
610
+ // Arrange: first declare input params from the first mock set's literals
611
+ const hasParams = testCase.paramNames.length > 0 && testCase.argLiterals.length > 0;
612
+ // Then set up mock return values for only the functions actually in importedFunctionMeta
596
613
  const mockSetupLines = [];
597
614
  for (const dep of scenario.dependencies) {
598
615
  for (const meta of dep.importedFunctionMeta) {
599
616
  const mockVal = buildMockReturnValue(meta.returnType);
600
617
  if (meta.isAsync) {
601
- mockSetupLines.push(` (${meta.name} as jest.Mock).mockResolvedValue(${mockVal});`);
618
+ mockSetupLines.push(` ${adapter.mockResolvedValue(meta.name, mockVal)}`);
602
619
  }
603
620
  else {
604
- mockSetupLines.push(` (${meta.name} as jest.Mock).mockReturnValue(${mockVal});`);
621
+ mockSetupLines.push(` ${adapter.mockReturnValue(meta.name, mockVal)}`);
605
622
  }
606
623
  }
607
624
  }
608
- if (mockSetupLines.length > 0) {
625
+ if (hasParams || mockSetupLines.length > 0) {
609
626
  lines.push(" // Arrange");
627
+ // Declare params so the function call compiles
628
+ if (hasParams) {
629
+ for (let i = 0; i < testCase.paramNames.length; i++) {
630
+ lines.push(` const ${testCase.paramNames[i]} = ${testCase.argLiterals[i] ?? "undefined"};`);
631
+ }
632
+ }
610
633
  for (const l of mockSetupLines)
611
634
  lines.push(l);
612
635
  lines.push("");
@@ -644,17 +667,28 @@ function buildStandardBlock(scenario, testCase, mockConstName) {
644
667
  // ── Assert ──────────────────────────────────────────────────────
645
668
  lines.push(" // Assert");
646
669
  if (isDependencyTest) {
647
- // Verify the mocked functions were called
670
+ // Verify only functions that the analyzer confirmed are called (have meta)
671
+ // This avoids asserting on imports the function never uses
648
672
  for (const dep of scenario.dependencies) {
649
- for (const impName of dep.importedNames) {
650
- if (impName !== "default") {
651
- lines.push(` expect(${impName}).toHaveBeenCalled();`);
673
+ const metaList = dep.importedFunctionMeta.length > 0
674
+ ? dep.importedFunctionMeta
675
+ : dep.importedNames
676
+ .filter(n => n !== "default")
677
+ .map(n => ({ name: n, returnType: "unknown", isAsync: false, params: [] }));
678
+ for (const meta of metaList) {
679
+ const depParams = meta.params ?? [];
680
+ if (depParams.length > 0) {
681
+ const callWithArgs = buildCalledWithArgs(depParams, testCase.paramNames, testCase.argLiterals);
682
+ lines.push(` expect(${meta.name}).toHaveBeenCalledWith(${callWithArgs});`);
683
+ }
684
+ else {
685
+ lines.push(` expect(${meta.name}).toHaveBeenCalled();`);
652
686
  }
653
687
  }
654
688
  }
655
689
  }
656
690
  else {
657
- const expectLines = buildExpectStubs(scenario.returnType, testCase.label);
691
+ const expectLines = buildExpectStubs(scenario.returnType, testCase.label, snapshotForComplexTypes);
658
692
  for (const e of expectLines) {
659
693
  lines.push(` ${e}`);
660
694
  }
@@ -763,7 +797,22 @@ function buildMockReturnValue(returnType) {
763
797
  return "{ id: 1 }";
764
798
  }
765
799
  // ─── Expect stubs based on return type ───────────────────────────────────────
766
- function buildExpectStubs(returnType, scenarioLabel) {
800
+ /** Primitive/built-in type names that do NOT warrant snapshot assertions. */
801
+ const SNAPSHOT_SKIP_TYPES = new Set([
802
+ "number", "string", "boolean", "void", "undefined", "null",
803
+ "any", "never", "unknown", "object", "date", "record",
804
+ ]);
805
+ function isComplexReturnType(unwrapped) {
806
+ const base = unwrapped.replace(/\[\]$/, "").replace(/<[^>]+>$/, "").trim();
807
+ if (SNAPSHOT_SKIP_TYPES.has(base))
808
+ return false;
809
+ if (base.startsWith("array") || base.startsWith("map") || base.startsWith("set") || base.startsWith("record"))
810
+ return false;
811
+ if (base.endsWith("[]"))
812
+ return false;
813
+ return true;
814
+ }
815
+ function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = false) {
767
816
  const t = returnType.trim().toLowerCase();
768
817
  const unwrapped = unwrapType(t);
769
818
  const stubs = [];
@@ -802,6 +851,9 @@ function buildExpectStubs(returnType, scenarioLabel) {
802
851
  }
803
852
  else {
804
853
  stubs.push("expect(result).not.toBeNull();");
854
+ if (snapshotForComplexTypes && isComplexReturnType(unwrapped)) {
855
+ stubs.push("expect(result).toMatchSnapshot(); // Update snapshot after first run");
856
+ }
805
857
  }
806
858
  return stubs;
807
859
  }
@@ -811,6 +863,44 @@ function unwrapType(t) {
811
863
  return promiseMatch[1].trim();
812
864
  return t;
813
865
  }
866
+ // ─── toHaveBeenCalledWith argument builder ────────────────────────────────────
867
+ /**
868
+ * Build the argument list for `toHaveBeenCalledWith(...)`.
869
+ *
870
+ * Strategy (per dep function parameter):
871
+ * 1. If a SUT param with the same name and compatible type exists in the test's
872
+ * arranged args, use that variable name (it was declared in Arrange).
873
+ * 2. Otherwise emit an `expect.any(Constructor)` / `expect.anything()` matcher.
874
+ */
875
+ function buildCalledWithArgs(depParams, sutParamNames, sutArgLiterals) {
876
+ if (depParams.length === 0)
877
+ return "";
878
+ const args = depParams.map((dp) => {
879
+ // 1. Name match: SUT has a param with the same name
880
+ const nameIdx = sutParamNames.indexOf(dp.name);
881
+ if (nameIdx !== -1)
882
+ return dp.name;
883
+ // 2. Positional fallback: same position in param list
884
+ const posIdx = depParams.indexOf(dp);
885
+ if (posIdx < sutParamNames.length)
886
+ return sutParamNames[posIdx];
887
+ // 3. Type-based matcher
888
+ return typeToMatcher(dp.type);
889
+ });
890
+ return args.join(", ");
891
+ }
892
+ function typeToMatcher(typeStr) {
893
+ const t = typeStr.trim().toLowerCase().replace(/^promise<(.+)>$/i, "$1");
894
+ if (t === "number")
895
+ return "expect.any(Number)";
896
+ if (t === "string")
897
+ return "expect.any(String)";
898
+ if (t === "boolean")
899
+ return "expect.any(Boolean)";
900
+ if (t.endsWith("[]") || t.startsWith("array"))
901
+ return "expect.any(Array)";
902
+ return "expect.anything()";
903
+ }
814
904
  // ─── Dependency helpers ──────────────────────────────────────────────────────
815
905
  function collectAllDependencies(scenarios) {
816
906
  const depMap = new Map();