@gnapi/cotester 1.2.4 → 1.2.6
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/dist/analyzer.d.ts +5 -0
- package/dist/analyzer.js +7 -2
- package/dist/analyzer.js.map +1 -1
- package/dist/auditLogger.d.ts +46 -0
- package/dist/auditLogger.js +107 -0
- package/dist/auditLogger.js.map +1 -0
- package/dist/checker.d.ts +3 -1
- package/dist/checker.js +1 -0
- package/dist/checker.js.map +1 -1
- package/dist/cli.js +39 -0
- package/dist/cli.js.map +1 -1
- package/dist/configManager.d.ts +27 -0
- package/dist/configManager.js +87 -17
- package/dist/configManager.js.map +1 -1
- package/dist/fileWorker.js +6 -1
- package/dist/fileWorker.js.map +1 -1
- package/dist/frameworkAdapter.d.ts +51 -0
- package/dist/frameworkAdapter.js +77 -0
- package/dist/frameworkAdapter.js.map +1 -0
- package/dist/generator.d.ts +24 -1
- package/dist/generator.js +167 -69
- package/dist/generator.js.map +1 -1
- package/dist/hooksInstaller.d.ts +1 -0
- package/dist/hooksInstaller.js +246 -0
- package/dist/hooksInstaller.js.map +1 -0
- package/dist/importRepairer.d.ts +22 -0
- package/dist/importRepairer.js +226 -0
- package/dist/importRepairer.js.map +1 -0
- package/dist/interfaceShapeResolver.js +8 -3
- package/dist/interfaceShapeResolver.js.map +1 -1
- package/dist/migrator.d.ts +49 -0
- package/dist/migrator.js +335 -0
- package/dist/migrator.js.map +1 -0
- package/dist/mockGenerator.js +25 -1
- package/dist/mockGenerator.js.map +1 -1
- package/dist/reporter.d.ts +10 -0
- package/dist/reporter.js +270 -0
- package/dist/reporter.js.map +1 -0
- package/dist/scenarioEngine.js +4 -1
- package/dist/scenarioEngine.js.map +1 -1
- package/dist/sensitiveValueDetector.d.ts +62 -0
- package/dist/sensitiveValueDetector.js +147 -0
- package/dist/sensitiveValueDetector.js.map +1 -0
- package/dist/sharedMockRegistry.d.ts +27 -0
- package/dist/sharedMockRegistry.js +223 -0
- package/dist/sharedMockRegistry.js.map +1 -0
- package/dist/watcher.js +10 -1
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
package/dist/generator.js
CHANGED
|
@@ -33,18 +33,27 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.VERSION_MARKER = exports.CURRENT_FILE_VERSION = exports.TESTGEN_MARKER = void 0;
|
|
36
37
|
exports.generateTestFile = generateTestFile;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const formatter_1 = require("./formatter");
|
|
40
41
|
const utils_1 = require("./utils");
|
|
41
42
|
const ormMockGenerator_1 = require("./ormMockGenerator");
|
|
43
|
+
const frameworkAdapter_1 = require("./frameworkAdapter");
|
|
42
44
|
/**
|
|
43
45
|
* Marker placed as the very first line of every generated test file.
|
|
44
46
|
* Its presence lets cotester know the file was scaffolded (not hand-written),
|
|
45
47
|
* but function-level merging works regardless of whether the marker is present.
|
|
46
48
|
*/
|
|
47
|
-
|
|
49
|
+
exports.TESTGEN_MARKER = "// @cotester-generated";
|
|
50
|
+
/**
|
|
51
|
+
* Format version written into the second line of every generated test file.
|
|
52
|
+
* Used by `cotester migrate` to detect files produced by older versions and
|
|
53
|
+
* upgrade them in-place. Increment this when the generated format changes.
|
|
54
|
+
*/
|
|
55
|
+
exports.CURRENT_FILE_VERSION = 2;
|
|
56
|
+
exports.VERSION_MARKER = `// @cotester-version: ${exports.CURRENT_FILE_VERSION}`;
|
|
48
57
|
/**
|
|
49
58
|
* Generate (or merge into) a test file for the given source file.
|
|
50
59
|
*
|
|
@@ -58,34 +67,37 @@ const TESTGEN_MARKER = "// @cotester-generated";
|
|
|
58
67
|
* Individual functions/methods that already have a describe block are never
|
|
59
68
|
* touched, preserving any hand-written assertions inside them.
|
|
60
69
|
*/
|
|
61
|
-
async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, testDir, mockResult, tableThreshold = 0, dryRun = false) {
|
|
70
|
+
async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, testDir, mockResult, tableThreshold = 0, dryRun = false, framework = "jest", snapshotForComplexTypes = false) {
|
|
62
71
|
if (scenarios.length === 0) {
|
|
63
72
|
return null;
|
|
64
73
|
}
|
|
74
|
+
const adapter = (0, frameworkAdapter_1.createAdapter)(framework);
|
|
65
75
|
const absSrcRoot = path.resolve(projectRoot, srcDir);
|
|
66
76
|
const testFilePath = (0, utils_1.mirrorPath)(srcFilePath, absSrcRoot, path.join(projectRoot, testDir), ".test.ts");
|
|
67
77
|
// ── Dry-run: print generated content without touching the filesystem ──
|
|
68
78
|
if (dryRun) {
|
|
69
|
-
const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
|
|
79
|
+
const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
|
|
70
80
|
const formatted = await (0, formatter_1.formatCode)(code, projectRoot);
|
|
71
81
|
const relTest = path.relative(projectRoot, testFilePath).replace(/\\/g, "/");
|
|
72
82
|
process.stdout.write(`\n${"─".repeat(60)}\n`);
|
|
73
83
|
process.stdout.write(`// [dry-run] Test file: ${relTest}\n`);
|
|
74
84
|
process.stdout.write(`${"─".repeat(60)}\n`);
|
|
75
85
|
process.stdout.write(formatted);
|
|
76
|
-
|
|
86
|
+
const allNames = scenarios.map((s) => s.functionName);
|
|
87
|
+
return { testFilePath, added: allNames, merged: [], skipped: [] };
|
|
77
88
|
}
|
|
78
89
|
(0, utils_1.ensureDir)(path.dirname(testFilePath));
|
|
79
90
|
// ── Brand-new file → generate everything ─────────────────────────────
|
|
80
91
|
if (!fs.existsSync(testFilePath)) {
|
|
81
|
-
const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
|
|
92
|
+
const code = buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
|
|
82
93
|
const formatted = await (0, formatter_1.formatCode)(code, projectRoot);
|
|
83
94
|
fs.writeFileSync(testFilePath, formatted, "utf-8");
|
|
84
95
|
(0, utils_1.log)(`Test file → ${path.relative(projectRoot, testFilePath)}`, "success");
|
|
85
|
-
|
|
96
|
+
const allNames = scenarios.map((s) => s.functionName);
|
|
97
|
+
return { testFilePath, added: allNames, merged: [], skipped: [] };
|
|
86
98
|
}
|
|
87
99
|
// ── File exists → function-level merge ───────────────────────────────
|
|
88
|
-
return mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold);
|
|
100
|
+
return mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold, adapter, snapshotForComplexTypes);
|
|
89
101
|
}
|
|
90
102
|
/**
|
|
91
103
|
* Merge missing function/method describe blocks into an already-existing test file.
|
|
@@ -98,11 +110,12 @@ async function generateTestFile(srcFilePath, scenarios, projectRoot, srcDir, tes
|
|
|
98
110
|
* 5. For entire new classes → append a full class describe block.
|
|
99
111
|
* 6. For any new dependencies (jest.mock) not already in the file → prepend them.
|
|
100
112
|
*/
|
|
101
|
-
async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0) {
|
|
113
|
+
async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
102
114
|
let content = fs.readFileSync(testFilePath, "utf-8");
|
|
103
115
|
const existingDescribes = detectExistingDescribes(content);
|
|
104
116
|
// ── Feature 6: signature-change detection for already-covered describes ──
|
|
105
117
|
const existingSigs = parseExistingSignatures(content);
|
|
118
|
+
const sigChangedNames = new Set();
|
|
106
119
|
for (const s of scenarios) {
|
|
107
120
|
const key = s.functionName;
|
|
108
121
|
if (!existingDescribes.has(key))
|
|
@@ -118,6 +131,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
|
|
|
118
131
|
(0, utils_1.log)(` Signature changed for ${key} — injecting update warning.`, "warn");
|
|
119
132
|
content = updateSignatureComment(content, key, newSig);
|
|
120
133
|
content = injectSignatureWarning(content, key, oldSig, newSig);
|
|
134
|
+
sigChangedNames.add(key);
|
|
121
135
|
}
|
|
122
136
|
}
|
|
123
137
|
// ── Classify scenarios ───────────────────────────────────────────────
|
|
@@ -140,7 +154,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
|
|
|
140
154
|
for (const [className, methods] of classMap) {
|
|
141
155
|
if (!existingDescribes.has(className)) {
|
|
142
156
|
// Entire class is new → append full class describe block
|
|
143
|
-
toAppend.push(buildClassDescribe(className, methods, mockResult, tableThreshold));
|
|
157
|
+
toAppend.push(buildClassDescribe(className, methods, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
|
|
144
158
|
newScenarios.push(...methods);
|
|
145
159
|
}
|
|
146
160
|
else {
|
|
@@ -148,7 +162,7 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
|
|
|
148
162
|
const missingMethods = methods.filter(m => !existingDescribes.has(m.functionName));
|
|
149
163
|
if (missingMethods.length > 0) {
|
|
150
164
|
const methodBlocks = missingMethods
|
|
151
|
-
.map(m => indentBlock(buildMethodDescribe(m, mockResult, tableThreshold), 1))
|
|
165
|
+
.map(m => indentBlock(buildMethodDescribe(m, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 1))
|
|
152
166
|
.join("\n\n");
|
|
153
167
|
content = injectIntoClassDescribe(content, className, methodBlocks);
|
|
154
168
|
newScenarios.push(...missingMethods);
|
|
@@ -157,23 +171,29 @@ async function mergeIntoExistingTestFile(scenarios, srcFilePath, testFilePath, m
|
|
|
157
171
|
}
|
|
158
172
|
// ── Standalone missing functions ─────────────────────────────────────
|
|
159
173
|
for (const fn of missingStandalone) {
|
|
160
|
-
toAppend.push(buildFunctionDescribe(fn, mockResult, tableThreshold));
|
|
161
|
-
}
|
|
174
|
+
toAppend.push(buildFunctionDescribe(fn, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
|
|
175
|
+
}
|
|
176
|
+
// ── Build audit lists ────────────────────────────────────────────────
|
|
177
|
+
const addedNames = newScenarios.map((s) => s.functionName);
|
|
178
|
+
const mergedNames = Array.from(sigChangedNames);
|
|
179
|
+
const skippedNames = scenarios
|
|
180
|
+
.filter((s) => existingDescribes.has(s.functionName) &&
|
|
181
|
+
!sigChangedNames.has(s.functionName))
|
|
182
|
+
.map((s) => s.functionName);
|
|
162
183
|
if (toAppend.length === 0 && newScenarios.length === 0) {
|
|
163
184
|
(0, utils_1.log)(` ${path.relative(projectRoot, testFilePath)} — all functions already covered.`, "info");
|
|
164
|
-
return testFilePath;
|
|
185
|
+
return { testFilePath, added: [], merged: mergedNames, skipped: skippedNames };
|
|
165
186
|
}
|
|
166
187
|
// ── Append standalone / new-class blocks ─────────────────────────────
|
|
167
188
|
if (toAppend.length > 0) {
|
|
168
189
|
content = content.trimEnd() + "\n\n" + toAppend.join("\n\n") + "\n";
|
|
169
190
|
}
|
|
170
|
-
// ── Prepend missing
|
|
171
|
-
content = addMissingMocks(content, newScenarios, srcFilePath, testFilePath);
|
|
191
|
+
// ── Prepend missing mock() / import lines ────────────────────────────
|
|
192
|
+
content = addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapter);
|
|
172
193
|
const formatted = await (0, formatter_1.formatCode)(content, projectRoot);
|
|
173
194
|
fs.writeFileSync(testFilePath, formatted, "utf-8");
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return testFilePath;
|
|
195
|
+
(0, utils_1.log)(`Test file updated → ${path.relative(projectRoot, testFilePath)} (+${addedNames.length} function(s))`, "success");
|
|
196
|
+
return { testFilePath, added: addedNames, merged: mergedNames, skipped: skippedNames };
|
|
177
197
|
}
|
|
178
198
|
// ─── Merge helpers ────────────────────────────────────────────────────────────
|
|
179
199
|
/**
|
|
@@ -282,17 +302,16 @@ function injectIntoClassDescribe(content, className, newBlocks) {
|
|
|
282
302
|
* are already mocked in the file. If not, prepend the missing jest.mock() calls
|
|
283
303
|
* and import statements before the first existing import line.
|
|
284
304
|
*/
|
|
285
|
-
function addMissingMocks(content, newScenarios, srcFilePath, testFilePath) {
|
|
305
|
+
function addMissingMocks(content, newScenarios, srcFilePath, testFilePath, adapter = (0, frameworkAdapter_1.createAdapter)("jest")) {
|
|
286
306
|
const allDeps = collectAllDependencies(newScenarios);
|
|
287
307
|
const mockLines = [];
|
|
288
308
|
const importLines = [];
|
|
289
309
|
for (const dep of allDeps) {
|
|
290
310
|
const depAbsPath = resolveDepPath(srcFilePath, dep.modulePath);
|
|
291
311
|
const depImportPath = computeRelativeImport(depAbsPath, testFilePath);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
mockLines.push(`jest.mock('${depImportPath}', () => ({\n${mockFns}\n}));`);
|
|
312
|
+
const checkStr = adapter.mockModuleCheck(depImportPath);
|
|
313
|
+
if (!content.includes(checkStr) && !content.includes(checkStr.replace("'", '"'))) {
|
|
314
|
+
mockLines.push(adapter.mockModule(depImportPath, dep.importedNames));
|
|
296
315
|
}
|
|
297
316
|
const names = dep.importedNames.filter(n => n !== "default");
|
|
298
317
|
if (names.length > 0) {
|
|
@@ -316,24 +335,26 @@ function escapeForRegex(str) {
|
|
|
316
335
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
317
336
|
}
|
|
318
337
|
// ─── Build the full test source ──────────────────────────────────────────────
|
|
319
|
-
function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0) {
|
|
338
|
+
function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, projectRoot, srcDir, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
320
339
|
const lines = [];
|
|
321
340
|
// Marker — presence of this line tells cotester the file is auto-generated
|
|
322
341
|
// and safe to regenerate. Remove it to protect hand-written edits.
|
|
323
|
-
lines.push(TESTGEN_MARKER);
|
|
342
|
+
lines.push(exports.TESTGEN_MARKER);
|
|
343
|
+
lines.push(exports.VERSION_MARKER);
|
|
324
344
|
lines.push("");
|
|
345
|
+
// ── Framework import (Vitest only; empty for Jest) ───────────────
|
|
346
|
+
const fwImport = adapter.frameworkImport();
|
|
347
|
+
if (fwImport) {
|
|
348
|
+
lines.push(fwImport);
|
|
349
|
+
lines.push("");
|
|
350
|
+
}
|
|
325
351
|
// ── Collect all unique dependencies across scenarios ─────────────
|
|
326
352
|
const allDeps = collectAllDependencies(scenarios);
|
|
327
|
-
// ──
|
|
353
|
+
// ── mock() calls (must appear before imports) ────────────────────
|
|
328
354
|
for (const dep of allDeps) {
|
|
329
355
|
const depAbsPath = resolveDepPath(srcFilePath, dep.modulePath);
|
|
330
356
|
const depImportPath = computeRelativeImport(depAbsPath, testFilePath);
|
|
331
|
-
|
|
332
|
-
.map((n) => ` ${n}: jest.fn(),`)
|
|
333
|
-
.join("\n");
|
|
334
|
-
lines.push(`jest.mock('${depImportPath}', () => ({`);
|
|
335
|
-
lines.push(mockFns);
|
|
336
|
-
lines.push(`}));`);
|
|
357
|
+
lines.push(adapter.mockModule(depImportPath, dep.importedNames));
|
|
337
358
|
lines.push("");
|
|
338
359
|
}
|
|
339
360
|
// ── Import the source module ────────────────────────────────────
|
|
@@ -382,18 +403,18 @@ function buildTestSource(scenarios, srcFilePath, testFilePath, mockResult, proje
|
|
|
382
403
|
}
|
|
383
404
|
// ── Standalone functions ────────────────────────────────────────
|
|
384
405
|
for (const fn of standaloneFns) {
|
|
385
|
-
lines.push(buildFunctionDescribe(fn, mockResult, tableThreshold));
|
|
406
|
+
lines.push(buildFunctionDescribe(fn, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
|
|
386
407
|
lines.push("");
|
|
387
408
|
}
|
|
388
409
|
// ── Class methods ───────────────────────────────────────────────
|
|
389
410
|
for (const [className, methods] of classMap) {
|
|
390
|
-
lines.push(buildClassDescribe(className, methods, mockResult, tableThreshold));
|
|
411
|
+
lines.push(buildClassDescribe(className, methods, mockResult, tableThreshold, adapter, snapshotForComplexTypes));
|
|
391
412
|
lines.push("");
|
|
392
413
|
}
|
|
393
414
|
return lines.join("\n");
|
|
394
415
|
}
|
|
395
416
|
// ─── Class describe block with constructor DI ────────────────────────────────
|
|
396
|
-
function buildClassDescribe(className, methods, mockResult, tableThreshold = 0) {
|
|
417
|
+
function buildClassDescribe(className, methods, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
397
418
|
const lines = [];
|
|
398
419
|
lines.push(`describe(${JSON.stringify(className)}, () => {`);
|
|
399
420
|
lines.push(` let instance: ${className};`);
|
|
@@ -406,19 +427,21 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
|
|
|
406
427
|
// If this constructor param matches the ORM variable, use ORM mocks
|
|
407
428
|
if (ormUsage && (param.name === ormUsage.variableName || param.type.toLowerCase().includes(ormUsage.kind))) {
|
|
408
429
|
const ormMocks = (0, ormMockGenerator_1.generateOrmMocks)(ormUsage);
|
|
409
|
-
//
|
|
410
|
-
if (ormMocks.mockVarNames.length > 0) {
|
|
430
|
+
// Only alias if the file-level ORM var has a DIFFERENT name from the ctor param mock name
|
|
431
|
+
if (ormMocks.mockVarNames.length > 0 && ormMocks.mockVarNames[0] !== mockName) {
|
|
411
432
|
mockDeclarations.push(` const ${mockName} = ${ormMocks.mockVarNames[0]}; // ORM mock`);
|
|
412
433
|
}
|
|
413
|
-
else {
|
|
434
|
+
else if (ormMocks.mockVarNames.length === 0) {
|
|
414
435
|
mockDeclarations.push(` const ${mockName} = { } as any; // TODO: add mock methods for ${param.type}`);
|
|
415
436
|
}
|
|
437
|
+
// else: same name — variable already declared at file level; no re-declaration needed
|
|
416
438
|
}
|
|
417
439
|
else {
|
|
418
440
|
// Generate smart mock based on type name
|
|
419
441
|
const typeLower = param.type.toLowerCase();
|
|
420
442
|
if (typeLower.includes("repository") || typeLower.includes("repo")) {
|
|
421
|
-
|
|
443
|
+
const fn = adapter.mockFn();
|
|
444
|
+
mockDeclarations.push(` const ${mockName} = { find: ${fn}, findById: ${fn}, save: ${fn}, create: ${fn}, update: ${fn}, delete: ${fn} } as any;`);
|
|
422
445
|
}
|
|
423
446
|
else if (typeLower.includes("service")) {
|
|
424
447
|
mockDeclarations.push(` const ${mockName} = { } as any; // TODO: mock ${param.type} methods`);
|
|
@@ -436,7 +459,7 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
|
|
|
436
459
|
}
|
|
437
460
|
lines.push("");
|
|
438
461
|
lines.push(" beforeEach(() => {");
|
|
439
|
-
lines.push(
|
|
462
|
+
lines.push(` ${adapter.clearAllMocks()}`);
|
|
440
463
|
if (ctorParams.length > 0) {
|
|
441
464
|
const args = ctorParams
|
|
442
465
|
.map((p) => `mock${capitalize(p.name)}`)
|
|
@@ -457,66 +480,66 @@ function buildClassDescribe(className, methods, mockResult, tableThreshold = 0)
|
|
|
457
480
|
lines.push(" params,");
|
|
458
481
|
lines.push(" query,");
|
|
459
482
|
lines.push(" headers: { 'Content-Type': 'application/json' },");
|
|
460
|
-
lines.push(
|
|
483
|
+
lines.push(` get: ${adapter.mockFn()},`);
|
|
461
484
|
lines.push(" } as any);");
|
|
462
485
|
lines.push("");
|
|
463
486
|
lines.push(" const mockResponse = () => {");
|
|
464
487
|
lines.push(" const res: any = {};");
|
|
465
|
-
lines.push(
|
|
466
|
-
lines.push(
|
|
467
|
-
lines.push(
|
|
468
|
-
lines.push(
|
|
488
|
+
lines.push(` res.status = ${adapter.mockFnReturnValue("res")};`);
|
|
489
|
+
lines.push(` res.json = ${adapter.mockFnReturnValue("res")};`);
|
|
490
|
+
lines.push(` res.send = ${adapter.mockFnReturnValue("res")};`);
|
|
491
|
+
lines.push(` res.set = ${adapter.mockFnReturnValue("res")};`);
|
|
469
492
|
lines.push(" return res;");
|
|
470
493
|
lines.push(" };");
|
|
471
494
|
lines.push("");
|
|
472
|
-
lines.push(
|
|
495
|
+
lines.push(` const mockNext = ${adapter.mockFn()};`);
|
|
473
496
|
}
|
|
474
497
|
lines.push("");
|
|
475
498
|
for (const method of methods) {
|
|
476
|
-
lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold), 1));
|
|
499
|
+
lines.push(indentBlock(buildMethodDescribe(method, mockResult, tableThreshold, adapter, snapshotForComplexTypes), 1));
|
|
477
500
|
lines.push("");
|
|
478
501
|
}
|
|
479
502
|
lines.push("});");
|
|
480
503
|
return lines.join("\n");
|
|
481
504
|
}
|
|
482
505
|
// ─── describe block for a standalone function ────────────────────────────────
|
|
483
|
-
function buildFunctionDescribe(fn, mockResult, tableThreshold = 0) {
|
|
506
|
+
function buildFunctionDescribe(fn, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
484
507
|
const lines = [];
|
|
485
508
|
lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
|
|
486
509
|
lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
|
|
487
510
|
// Add beforeEach with clearAllMocks if this function has dependencies
|
|
488
511
|
if (fn.dependencies.length > 0) {
|
|
489
512
|
lines.push(" beforeEach(() => {");
|
|
490
|
-
lines.push(
|
|
513
|
+
lines.push(` ${adapter.clearAllMocks()}`);
|
|
491
514
|
lines.push(" });");
|
|
492
515
|
lines.push("");
|
|
493
516
|
}
|
|
494
517
|
const constName = mockResult?.constantNames.get(fn.functionName);
|
|
495
518
|
const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
|
|
496
519
|
if (tableCases.length > 0) {
|
|
497
|
-
lines.push(indentBlock(buildTableBlock(fn, tableCases), 1));
|
|
520
|
+
lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
|
|
498
521
|
lines.push("");
|
|
499
522
|
}
|
|
500
523
|
for (const c of individualCases) {
|
|
501
|
-
lines.push(indentBlock(buildTestBlock(fn, c, constName), 1));
|
|
524
|
+
lines.push(indentBlock(buildTestBlock(fn, c, constName, adapter, snapshotForComplexTypes), 1));
|
|
502
525
|
lines.push("");
|
|
503
526
|
}
|
|
504
527
|
lines.push("});");
|
|
505
528
|
return lines.join("\n");
|
|
506
529
|
}
|
|
507
530
|
// ─── describe block for a class method ───────────────────────────────────────
|
|
508
|
-
function buildMethodDescribe(fn, mockResult, tableThreshold = 0) {
|
|
531
|
+
function buildMethodDescribe(fn, mockResult, tableThreshold = 0, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
509
532
|
const lines = [];
|
|
510
533
|
lines.push(`// @testgen-sig:${buildSignatureFingerprint(fn)}`);
|
|
511
534
|
lines.push(`describe(${JSON.stringify(fn.functionName)}, () => {`);
|
|
512
535
|
const constName = mockResult?.constantNames.get(fn.functionName);
|
|
513
536
|
const { tableCases, individualCases } = partitionCases(fn.cases, fn.parameters.length, tableThreshold);
|
|
514
537
|
if (tableCases.length > 0) {
|
|
515
|
-
lines.push(indentBlock(buildTableBlock(fn, tableCases), 1));
|
|
538
|
+
lines.push(indentBlock(buildTableBlock(fn, tableCases, snapshotForComplexTypes), 1));
|
|
516
539
|
lines.push("");
|
|
517
540
|
}
|
|
518
541
|
for (const c of individualCases) {
|
|
519
|
-
lines.push(indentBlock(buildTestBlock(fn, c, constName), 1));
|
|
542
|
+
lines.push(indentBlock(buildTestBlock(fn, c, constName, adapter, snapshotForComplexTypes), 1));
|
|
520
543
|
lines.push("");
|
|
521
544
|
}
|
|
522
545
|
lines.push("});");
|
|
@@ -550,7 +573,7 @@ function partitionCases(cases, paramCount, tableThreshold) {
|
|
|
550
573
|
* Emit a `test.each([...])('%s', ...)` block for a group of standard cases.
|
|
551
574
|
* Each row is `[label, ...argLiterals]` so the Jest title shows the scenario name.
|
|
552
575
|
*/
|
|
553
|
-
function buildTableBlock(scenario, tableCases) {
|
|
576
|
+
function buildTableBlock(scenario, tableCases, snapshotForComplexTypes = false) {
|
|
554
577
|
const lines = [];
|
|
555
578
|
const isAsync = scenario.isAsync;
|
|
556
579
|
const isMethod = !!scenario.className;
|
|
@@ -574,7 +597,7 @@ function buildTableBlock(scenario, tableCases) {
|
|
|
574
597
|
lines.push(``);
|
|
575
598
|
// Assert
|
|
576
599
|
lines.push(` // Assert`);
|
|
577
|
-
const expectLines = buildExpectStubs(scenario.returnType, "");
|
|
600
|
+
const expectLines = buildExpectStubs(scenario.returnType, "", snapshotForComplexTypes);
|
|
578
601
|
for (const e of expectLines) {
|
|
579
602
|
lines.push(` ${e}`);
|
|
580
603
|
}
|
|
@@ -582,18 +605,18 @@ function buildTableBlock(scenario, tableCases) {
|
|
|
582
605
|
return lines.join("\n");
|
|
583
606
|
}
|
|
584
607
|
// ─── Individual test block ───────────────────────────────────────────────────
|
|
585
|
-
function buildTestBlock(scenario, testCase, mockConstName) {
|
|
608
|
+
function buildTestBlock(scenario, testCase, mockConstName, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
586
609
|
switch (testCase.kind) {
|
|
587
610
|
case "throws":
|
|
588
611
|
return buildThrowsBlock(scenario, testCase, mockConstName);
|
|
589
612
|
case "rejects":
|
|
590
613
|
return buildRejectsBlock(scenario, testCase, mockConstName);
|
|
591
614
|
default:
|
|
592
|
-
return buildStandardBlock(scenario, testCase, mockConstName);
|
|
615
|
+
return buildStandardBlock(scenario, testCase, mockConstName, adapter, snapshotForComplexTypes);
|
|
593
616
|
}
|
|
594
617
|
}
|
|
595
618
|
// ─── Standard test block (Arrange / Act / Assert) ────────────────────────────
|
|
596
|
-
function buildStandardBlock(scenario, testCase, mockConstName) {
|
|
619
|
+
function buildStandardBlock(scenario, testCase, mockConstName, adapter = (0, frameworkAdapter_1.createAdapter)("jest"), snapshotForComplexTypes = false) {
|
|
597
620
|
const lines = [];
|
|
598
621
|
const fnName = scenario.functionName;
|
|
599
622
|
const isAsync = scenario.isAsync;
|
|
@@ -603,21 +626,29 @@ function buildStandardBlock(scenario, testCase, mockConstName) {
|
|
|
603
626
|
// ── Arrange ──────────────────────────────────────────────────────
|
|
604
627
|
const caseKey = camelCase(testCase.label);
|
|
605
628
|
if (isDependencyTest) {
|
|
606
|
-
//
|
|
629
|
+
// Arrange: first declare input params from the first mock set's literals
|
|
630
|
+
const hasParams = testCase.paramNames.length > 0 && testCase.argLiterals.length > 0;
|
|
631
|
+
// Then set up mock return values for only the functions actually in importedFunctionMeta
|
|
607
632
|
const mockSetupLines = [];
|
|
608
633
|
for (const dep of scenario.dependencies) {
|
|
609
634
|
for (const meta of dep.importedFunctionMeta) {
|
|
610
635
|
const mockVal = buildMockReturnValue(meta.returnType);
|
|
611
636
|
if (meta.isAsync) {
|
|
612
|
-
mockSetupLines.push(`
|
|
637
|
+
mockSetupLines.push(` ${adapter.mockResolvedValue(meta.name, mockVal)}`);
|
|
613
638
|
}
|
|
614
639
|
else {
|
|
615
|
-
mockSetupLines.push(`
|
|
640
|
+
mockSetupLines.push(` ${adapter.mockReturnValue(meta.name, mockVal)}`);
|
|
616
641
|
}
|
|
617
642
|
}
|
|
618
643
|
}
|
|
619
|
-
if (mockSetupLines.length > 0) {
|
|
644
|
+
if (hasParams || mockSetupLines.length > 0) {
|
|
620
645
|
lines.push(" // Arrange");
|
|
646
|
+
// Declare params so the function call compiles
|
|
647
|
+
if (hasParams) {
|
|
648
|
+
for (let i = 0; i < testCase.paramNames.length; i++) {
|
|
649
|
+
lines.push(` const ${testCase.paramNames[i]} = ${testCase.argLiterals[i] ?? "undefined"};`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
621
652
|
for (const l of mockSetupLines)
|
|
622
653
|
lines.push(l);
|
|
623
654
|
lines.push("");
|
|
@@ -655,17 +686,28 @@ function buildStandardBlock(scenario, testCase, mockConstName) {
|
|
|
655
686
|
// ── Assert ──────────────────────────────────────────────────────
|
|
656
687
|
lines.push(" // Assert");
|
|
657
688
|
if (isDependencyTest) {
|
|
658
|
-
// Verify the
|
|
689
|
+
// Verify only functions that the analyzer confirmed are called (have meta)
|
|
690
|
+
// This avoids asserting on imports the function never uses
|
|
659
691
|
for (const dep of scenario.dependencies) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
692
|
+
const metaList = dep.importedFunctionMeta.length > 0
|
|
693
|
+
? dep.importedFunctionMeta
|
|
694
|
+
: dep.importedNames
|
|
695
|
+
.filter(n => n !== "default")
|
|
696
|
+
.map(n => ({ name: n, returnType: "unknown", isAsync: false, params: [] }));
|
|
697
|
+
for (const meta of metaList) {
|
|
698
|
+
const depParams = meta.params ?? [];
|
|
699
|
+
if (depParams.length > 0) {
|
|
700
|
+
const callWithArgs = buildCalledWithArgs(depParams, testCase.paramNames, testCase.argLiterals);
|
|
701
|
+
lines.push(` expect(${meta.name}).toHaveBeenCalledWith(${callWithArgs});`);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
lines.push(` expect(${meta.name}).toHaveBeenCalled();`);
|
|
663
705
|
}
|
|
664
706
|
}
|
|
665
707
|
}
|
|
666
708
|
}
|
|
667
709
|
else {
|
|
668
|
-
const expectLines = buildExpectStubs(scenario.returnType, testCase.label);
|
|
710
|
+
const expectLines = buildExpectStubs(scenario.returnType, testCase.label, snapshotForComplexTypes);
|
|
669
711
|
for (const e of expectLines) {
|
|
670
712
|
lines.push(` ${e}`);
|
|
671
713
|
}
|
|
@@ -774,7 +816,22 @@ function buildMockReturnValue(returnType) {
|
|
|
774
816
|
return "{ id: 1 }";
|
|
775
817
|
}
|
|
776
818
|
// ─── Expect stubs based on return type ───────────────────────────────────────
|
|
777
|
-
|
|
819
|
+
/** Primitive/built-in type names that do NOT warrant snapshot assertions. */
|
|
820
|
+
const SNAPSHOT_SKIP_TYPES = new Set([
|
|
821
|
+
"number", "string", "boolean", "void", "undefined", "null",
|
|
822
|
+
"any", "never", "unknown", "object", "date", "record",
|
|
823
|
+
]);
|
|
824
|
+
function isComplexReturnType(unwrapped) {
|
|
825
|
+
const base = unwrapped.replace(/\[\]$/, "").replace(/<[^>]+>$/, "").trim();
|
|
826
|
+
if (SNAPSHOT_SKIP_TYPES.has(base))
|
|
827
|
+
return false;
|
|
828
|
+
if (base.startsWith("array") || base.startsWith("map") || base.startsWith("set") || base.startsWith("record"))
|
|
829
|
+
return false;
|
|
830
|
+
if (base.endsWith("[]"))
|
|
831
|
+
return false;
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
function buildExpectStubs(returnType, scenarioLabel, snapshotForComplexTypes = false) {
|
|
778
835
|
const t = returnType.trim().toLowerCase();
|
|
779
836
|
const unwrapped = unwrapType(t);
|
|
780
837
|
const stubs = [];
|
|
@@ -813,6 +870,9 @@ function buildExpectStubs(returnType, scenarioLabel) {
|
|
|
813
870
|
}
|
|
814
871
|
else {
|
|
815
872
|
stubs.push("expect(result).not.toBeNull();");
|
|
873
|
+
if (snapshotForComplexTypes && isComplexReturnType(unwrapped)) {
|
|
874
|
+
stubs.push("expect(result).toMatchSnapshot(); // Update snapshot after first run");
|
|
875
|
+
}
|
|
816
876
|
}
|
|
817
877
|
return stubs;
|
|
818
878
|
}
|
|
@@ -822,6 +882,44 @@ function unwrapType(t) {
|
|
|
822
882
|
return promiseMatch[1].trim();
|
|
823
883
|
return t;
|
|
824
884
|
}
|
|
885
|
+
// ─── toHaveBeenCalledWith argument builder ────────────────────────────────────
|
|
886
|
+
/**
|
|
887
|
+
* Build the argument list for `toHaveBeenCalledWith(...)`.
|
|
888
|
+
*
|
|
889
|
+
* Strategy (per dep function parameter):
|
|
890
|
+
* 1. If a SUT param with the same name and compatible type exists in the test's
|
|
891
|
+
* arranged args, use that variable name (it was declared in Arrange).
|
|
892
|
+
* 2. Otherwise emit an `expect.any(Constructor)` / `expect.anything()` matcher.
|
|
893
|
+
*/
|
|
894
|
+
function buildCalledWithArgs(depParams, sutParamNames, sutArgLiterals) {
|
|
895
|
+
if (depParams.length === 0)
|
|
896
|
+
return "";
|
|
897
|
+
const args = depParams.map((dp) => {
|
|
898
|
+
// 1. Name match: SUT has a param with the same name
|
|
899
|
+
const nameIdx = sutParamNames.indexOf(dp.name);
|
|
900
|
+
if (nameIdx !== -1)
|
|
901
|
+
return dp.name;
|
|
902
|
+
// 2. Positional fallback: same position in param list
|
|
903
|
+
const posIdx = depParams.indexOf(dp);
|
|
904
|
+
if (posIdx < sutParamNames.length)
|
|
905
|
+
return sutParamNames[posIdx];
|
|
906
|
+
// 3. Type-based matcher
|
|
907
|
+
return typeToMatcher(dp.type);
|
|
908
|
+
});
|
|
909
|
+
return args.join(", ");
|
|
910
|
+
}
|
|
911
|
+
function typeToMatcher(typeStr) {
|
|
912
|
+
const t = typeStr.trim().toLowerCase().replace(/^promise<(.+)>$/i, "$1");
|
|
913
|
+
if (t === "number")
|
|
914
|
+
return "expect.any(Number)";
|
|
915
|
+
if (t === "string")
|
|
916
|
+
return "expect.any(String)";
|
|
917
|
+
if (t === "boolean")
|
|
918
|
+
return "expect.any(Boolean)";
|
|
919
|
+
if (t.endsWith("[]") || t.startsWith("array"))
|
|
920
|
+
return "expect.any(Array)";
|
|
921
|
+
return "expect.anything()";
|
|
922
|
+
}
|
|
825
923
|
// ─── Dependency helpers ──────────────────────────────────────────────────────
|
|
826
924
|
function collectAllDependencies(scenarios) {
|
|
827
925
|
const depMap = new Map();
|