@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.
- package/dist/analyzer.d.ts +5 -0
- package/dist/analyzer.js +7 -2
- package/dist/analyzer.js.map +1 -1
- package/dist/checker.d.ts +14 -1
- package/dist/checker.js +109 -8
- package/dist/checker.js.map +1 -1
- package/dist/cli.js +64 -2
- package/dist/cli.js.map +1 -1
- package/dist/configManager.d.ts +11 -0
- package/dist/configManager.js +55 -1
- package/dist/configManager.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 +1 -1
- package/dist/generator.js +149 -59
- 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/ignoreManager.d.ts +28 -0
- package/dist/ignoreManager.js +126 -0
- package/dist/ignoreManager.js.map +1 -0
- package/dist/mockGenerator.d.ts +1 -1
- package/dist/mockGenerator.js +36 -5
- 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/sharedMockRegistry.d.ts +27 -0
- package/dist/sharedMockRegistry.js +223 -0
- package/dist/sharedMockRegistry.js.map +1 -0
- package/dist/watcher.d.ts +1 -1
- package/dist/watcher.js +31 -13
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
|
@@ -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"}
|
package/dist/generator.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
455
|
-
lines.push(
|
|
456
|
-
lines.push(
|
|
457
|
-
lines.push(
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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(`
|
|
618
|
+
mockSetupLines.push(` ${adapter.mockResolvedValue(meta.name, mockVal)}`);
|
|
602
619
|
}
|
|
603
620
|
else {
|
|
604
|
-
mockSetupLines.push(`
|
|
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
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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();
|