@devness/coverit 0.1.0
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/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/agents/orchestrator.d.ts +21 -0
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/agents/orchestrator.js +235 -0
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/reporter.d.ts +13 -0
- package/dist/agents/reporter.d.ts.map +1 -0
- package/dist/agents/reporter.js +323 -0
- package/dist/agents/reporter.js.map +1 -0
- package/dist/ai/anthropic-provider.d.ts +19 -0
- package/dist/ai/anthropic-provider.d.ts.map +1 -0
- package/dist/ai/anthropic-provider.js +83 -0
- package/dist/ai/anthropic-provider.js.map +1 -0
- package/dist/ai/claude-cli-provider.d.ts +22 -0
- package/dist/ai/claude-cli-provider.d.ts.map +1 -0
- package/dist/ai/claude-cli-provider.js +197 -0
- package/dist/ai/claude-cli-provider.js.map +1 -0
- package/dist/ai/index.d.ts +15 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +16 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/ollama-provider.d.ts +17 -0
- package/dist/ai/ollama-provider.d.ts.map +1 -0
- package/dist/ai/ollama-provider.js +88 -0
- package/dist/ai/ollama-provider.js.map +1 -0
- package/dist/ai/openai-provider.d.ts +20 -0
- package/dist/ai/openai-provider.d.ts.map +1 -0
- package/dist/ai/openai-provider.js +74 -0
- package/dist/ai/openai-provider.js.map +1 -0
- package/dist/ai/prompts.d.ts +36 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +259 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/provider-factory.d.ts +26 -0
- package/dist/ai/provider-factory.d.ts.map +1 -0
- package/dist/ai/provider-factory.js +111 -0
- package/dist/ai/provider-factory.js.map +1 -0
- package/dist/ai/types.d.ts +37 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai/types.js +10 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/analysis/code-scanner.d.ts +9 -0
- package/dist/analysis/code-scanner.d.ts.map +1 -0
- package/dist/analysis/code-scanner.js +409 -0
- package/dist/analysis/code-scanner.js.map +1 -0
- package/dist/analysis/dependency-graph.d.ts +9 -0
- package/dist/analysis/dependency-graph.d.ts.map +1 -0
- package/dist/analysis/dependency-graph.js +149 -0
- package/dist/analysis/dependency-graph.js.map +1 -0
- package/dist/analysis/diff-analyzer.d.ts +9 -0
- package/dist/analysis/diff-analyzer.d.ts.map +1 -0
- package/dist/analysis/diff-analyzer.js +232 -0
- package/dist/analysis/diff-analyzer.js.map +1 -0
- package/dist/analysis/index.d.ts +5 -0
- package/dist/analysis/index.d.ts.map +1 -0
- package/dist/analysis/index.js +5 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/analysis/strategy-planner.d.ts +11 -0
- package/dist/analysis/strategy-planner.d.ts.map +1 -0
- package/dist/analysis/strategy-planner.js +384 -0
- package/dist/analysis/strategy-planner.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +288 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/executors/base-executor.d.ts +35 -0
- package/dist/executors/base-executor.d.ts.map +1 -0
- package/dist/executors/base-executor.js +138 -0
- package/dist/executors/base-executor.js.map +1 -0
- package/dist/executors/browser-runner.d.ts +24 -0
- package/dist/executors/browser-runner.d.ts.map +1 -0
- package/dist/executors/browser-runner.js +194 -0
- package/dist/executors/browser-runner.js.map +1 -0
- package/dist/executors/cloud-runner.d.ts +41 -0
- package/dist/executors/cloud-runner.d.ts.map +1 -0
- package/dist/executors/cloud-runner.js +153 -0
- package/dist/executors/cloud-runner.js.map +1 -0
- package/dist/executors/index.d.ts +12 -0
- package/dist/executors/index.d.ts.map +1 -0
- package/dist/executors/index.js +28 -0
- package/dist/executors/index.js.map +1 -0
- package/dist/executors/local-runner.d.ts +40 -0
- package/dist/executors/local-runner.d.ts.map +1 -0
- package/dist/executors/local-runner.js +395 -0
- package/dist/executors/local-runner.js.map +1 -0
- package/dist/executors/reporter.d.ts +6 -0
- package/dist/executors/reporter.d.ts.map +1 -0
- package/dist/executors/reporter.js +6 -0
- package/dist/executors/reporter.js.map +1 -0
- package/dist/executors/simulator-runner.d.ts +30 -0
- package/dist/executors/simulator-runner.d.ts.map +1 -0
- package/dist/executors/simulator-runner.js +339 -0
- package/dist/executors/simulator-runner.js.map +1 -0
- package/dist/generators/api-test.d.ts +22 -0
- package/dist/generators/api-test.d.ts.map +1 -0
- package/dist/generators/api-test.js +235 -0
- package/dist/generators/api-test.js.map +1 -0
- package/dist/generators/base-generator.d.ts +79 -0
- package/dist/generators/base-generator.d.ts.map +1 -0
- package/dist/generators/base-generator.js +234 -0
- package/dist/generators/base-generator.js.map +1 -0
- package/dist/generators/desktop-test.d.ts +22 -0
- package/dist/generators/desktop-test.d.ts.map +1 -0
- package/dist/generators/desktop-test.js +290 -0
- package/dist/generators/desktop-test.js.map +1 -0
- package/dist/generators/e2e-browser.d.ts +19 -0
- package/dist/generators/e2e-browser.d.ts.map +1 -0
- package/dist/generators/e2e-browser.js +233 -0
- package/dist/generators/e2e-browser.js.map +1 -0
- package/dist/generators/index.d.ts +21 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +66 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/mobile-test.d.ts +22 -0
- package/dist/generators/mobile-test.d.ts.map +1 -0
- package/dist/generators/mobile-test.js +286 -0
- package/dist/generators/mobile-test.js.map +1 -0
- package/dist/generators/unit-test.d.ts +23 -0
- package/dist/generators/unit-test.d.ts.map +1 -0
- package/dist/generators/unit-test.js +282 -0
- package/dist/generators/unit-test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/types/index.d.ts +295 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/framework-detector.d.ts +28 -0
- package/dist/utils/framework-detector.d.ts.map +1 -0
- package/dist/utils/framework-detector.js +184 -0
- package/dist/utils/framework-detector.js.map +1 -0
- package/dist/utils/git.d.ts +33 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +82 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +47 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CodeScanResult, GeneratorContext, GeneratorResult, TestFramework, TestType } from "../types/index.js";
|
|
2
|
+
import type { AIProvider } from "../ai/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Abstract base class for all test generators.
|
|
5
|
+
* Provides shared helpers for building test file content strings,
|
|
6
|
+
* deduplicating against existing tests, and producing consistent output.
|
|
7
|
+
*
|
|
8
|
+
* When an AIProvider is attached, generators attempt LLM-powered test
|
|
9
|
+
* generation first, falling back to AST-based templates on failure or
|
|
10
|
+
* when no provider is available.
|
|
11
|
+
*/
|
|
12
|
+
export declare abstract class BaseGenerator {
|
|
13
|
+
protected aiProvider: AIProvider | null;
|
|
14
|
+
abstract generate(context: GeneratorContext): Promise<GeneratorResult>;
|
|
15
|
+
setAIProvider(provider: AIProvider): void;
|
|
16
|
+
/**
|
|
17
|
+
* Attempts LLM-powered test generation using the attached AI provider.
|
|
18
|
+
* Returns the generated test file content as a string, or null if:
|
|
19
|
+
* - No AI provider is configured
|
|
20
|
+
* - The provider call fails for any reason
|
|
21
|
+
*
|
|
22
|
+
* Callers should fall back to template-based generation when this returns null.
|
|
23
|
+
*/
|
|
24
|
+
protected generateWithAI(params: {
|
|
25
|
+
sourceCode: string;
|
|
26
|
+
scanResult: CodeScanResult;
|
|
27
|
+
testType: TestType;
|
|
28
|
+
framework: TestFramework;
|
|
29
|
+
existingTests?: string[];
|
|
30
|
+
}): Promise<string | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Extracts raw code from an LLM response that may be wrapped in
|
|
33
|
+
* markdown code fences (```typescript ... ``` or ``` ... ```).
|
|
34
|
+
*/
|
|
35
|
+
private extractCodeFromResponse;
|
|
36
|
+
/**
|
|
37
|
+
* Counts the number of test cases in generated code by looking for
|
|
38
|
+
* `it(`, `test(`, and `test.only(` invocations.
|
|
39
|
+
*/
|
|
40
|
+
protected countTestCases(code: string): number;
|
|
41
|
+
protected fileHeader(): string;
|
|
42
|
+
protected buildImports(framework: TestFramework): string;
|
|
43
|
+
protected buildDescribeBlock(name: string, tests: string[]): string;
|
|
44
|
+
protected buildTestCase(name: string, body: string): string;
|
|
45
|
+
protected buildAsyncTestCase(name: string, body: string): string;
|
|
46
|
+
protected generateTestFileName(sourceFile: string, type: TestType): string;
|
|
47
|
+
/**
|
|
48
|
+
* Reads all test file contents from a directory so generators
|
|
49
|
+
* can skip creating tests that already exist.
|
|
50
|
+
*/
|
|
51
|
+
protected readExistingTests(dir: string): Promise<string[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Returns true if an existing test already covers the given target name
|
|
54
|
+
* (function, class, endpoint, etc.) by scanning existing test contents.
|
|
55
|
+
*/
|
|
56
|
+
protected isAlreadyTested(targetName: string, existingTests: string[]): boolean;
|
|
57
|
+
protected indent(text: string, spaces: number): string;
|
|
58
|
+
protected escapeString(str: string): string;
|
|
59
|
+
private escapeRegExp;
|
|
60
|
+
/**
|
|
61
|
+
* Generates a mock variable name from a module path.
|
|
62
|
+
* e.g. "../services/user.service" -> "mockUserService"
|
|
63
|
+
*/
|
|
64
|
+
protected mockNameFromModule(modulePath: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Generates a sample value for a given TypeScript type string.
|
|
67
|
+
* Used to create meaningful test data.
|
|
68
|
+
*/
|
|
69
|
+
protected sampleValueForType(type: string | null): string;
|
|
70
|
+
/**
|
|
71
|
+
* Builds a full test file string from its parts.
|
|
72
|
+
*/
|
|
73
|
+
protected assembleTestFile(parts: {
|
|
74
|
+
header?: string;
|
|
75
|
+
imports: string[];
|
|
76
|
+
body: string[];
|
|
77
|
+
}): string;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=base-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-generator.d.ts","sourceRoot":"","sources":["../../src/generators/base-generator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,QAAQ,EACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD;;;;;;;;GAQG;AACH,8BAAsB,aAAa;IACjC,SAAS,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAQ;IAE/C,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAEtE,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI;IAIzC;;;;;;;OAOG;cACa,cAAc,CAAC,MAAM,EAAE;QACrC,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,cAAc,CAAC;QAC3B,QAAQ,EAAE,QAAQ,CAAC;QACnB,SAAS,EAAE,aAAa,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;KAC1B,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA6B1B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAO/B;;;OAGG;IACH,SAAS,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAQ9C,SAAS,CAAC,UAAU,IAAI,MAAM;IAM9B,SAAS,CAAC,YAAY,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM;IAqBxD,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM;IAKnE,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAK3D,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAOhE,SAAS,CAAC,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM;IAsB1E;;;OAGG;cACa,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA2BjE;;;OAGG;IACH,SAAS,CAAC,eAAe,CACvB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO;IAUV,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAQtD,SAAS,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAI3C,OAAO,CAAC,YAAY;IAIpB;;;OAGG;IACH,SAAS,CAAC,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAWxD;;;OAGG;IACH,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM;IAuBzD;;OAEG;IACH,SAAS,CAAC,gBAAgB,CAAC,KAAK,EAAE;QAChC,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GAAG,MAAM;CAWX"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Generated by coverit — https://coverit.dev
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
4
|
+
import { buildTestGenerationPrompt } from "../ai/prompts.js";
|
|
5
|
+
/**
|
|
6
|
+
* Abstract base class for all test generators.
|
|
7
|
+
* Provides shared helpers for building test file content strings,
|
|
8
|
+
* deduplicating against existing tests, and producing consistent output.
|
|
9
|
+
*
|
|
10
|
+
* When an AIProvider is attached, generators attempt LLM-powered test
|
|
11
|
+
* generation first, falling back to AST-based templates on failure or
|
|
12
|
+
* when no provider is available.
|
|
13
|
+
*/
|
|
14
|
+
export class BaseGenerator {
|
|
15
|
+
aiProvider = null;
|
|
16
|
+
setAIProvider(provider) {
|
|
17
|
+
this.aiProvider = provider;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Attempts LLM-powered test generation using the attached AI provider.
|
|
21
|
+
* Returns the generated test file content as a string, or null if:
|
|
22
|
+
* - No AI provider is configured
|
|
23
|
+
* - The provider call fails for any reason
|
|
24
|
+
*
|
|
25
|
+
* Callers should fall back to template-based generation when this returns null.
|
|
26
|
+
*/
|
|
27
|
+
async generateWithAI(params) {
|
|
28
|
+
if (!this.aiProvider)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const messages = buildTestGenerationPrompt({
|
|
32
|
+
sourceCode: params.sourceCode,
|
|
33
|
+
scanResult: params.scanResult,
|
|
34
|
+
testType: params.testType,
|
|
35
|
+
framework: params.framework,
|
|
36
|
+
existingTests: params.existingTests,
|
|
37
|
+
});
|
|
38
|
+
const response = await this.aiProvider.generate(messages, {
|
|
39
|
+
temperature: 0.2,
|
|
40
|
+
maxTokens: 4096,
|
|
41
|
+
});
|
|
42
|
+
const content = response.content.trim();
|
|
43
|
+
if (!content)
|
|
44
|
+
return null;
|
|
45
|
+
// Strip markdown fences if the model wrapped its output in them
|
|
46
|
+
return this.extractCodeFromResponse(content);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
+
console.error(`[coverit] AI generation failed: ${msg}`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extracts raw code from an LLM response that may be wrapped in
|
|
56
|
+
* markdown code fences (```typescript ... ``` or ``` ... ```).
|
|
57
|
+
*/
|
|
58
|
+
extractCodeFromResponse(raw) {
|
|
59
|
+
const fenceMatch = raw.match(/^```(?:typescript|ts|javascript|js|tsx|jsx)?\s*\n([\s\S]*?)\n```\s*$/);
|
|
60
|
+
return fenceMatch?.[1] ? fenceMatch[1].trim() : raw;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Counts the number of test cases in generated code by looking for
|
|
64
|
+
* `it(`, `test(`, and `test.only(` invocations.
|
|
65
|
+
*/
|
|
66
|
+
countTestCases(code) {
|
|
67
|
+
const itCount = (code.match(/\bit\s*\(/g) || []).length;
|
|
68
|
+
const testCount = (code.match(/\btest\s*\(/g) || []).length;
|
|
69
|
+
return Math.max(itCount + testCount, 1);
|
|
70
|
+
}
|
|
71
|
+
// ── File header ────────────────────────────────────────────
|
|
72
|
+
fileHeader() {
|
|
73
|
+
return "// Generated by coverit — https://coverit.dev";
|
|
74
|
+
}
|
|
75
|
+
// ── Import builders ────────────────────────────────────────
|
|
76
|
+
buildImports(framework) {
|
|
77
|
+
switch (framework) {
|
|
78
|
+
case "vitest":
|
|
79
|
+
return "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';";
|
|
80
|
+
case "jest":
|
|
81
|
+
// Jest globals are available without explicit import in most configs,
|
|
82
|
+
// but we import jest for mock utilities.
|
|
83
|
+
return "import { jest } from '@jest/globals';";
|
|
84
|
+
case "playwright":
|
|
85
|
+
return "import { test, expect } from '@playwright/test';";
|
|
86
|
+
case "detox":
|
|
87
|
+
return [
|
|
88
|
+
"import { device, element, by, expect as detoxExpect } from 'detox';",
|
|
89
|
+
].join("\n");
|
|
90
|
+
default:
|
|
91
|
+
return "// No framework-specific imports";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ── Block builders ─────────────────────────────────────────
|
|
95
|
+
buildDescribeBlock(name, tests) {
|
|
96
|
+
const body = tests.map((t) => this.indent(t, 2)).join("\n\n");
|
|
97
|
+
return `describe('${this.escapeString(name)}', () => {\n${body}\n});`;
|
|
98
|
+
}
|
|
99
|
+
buildTestCase(name, body) {
|
|
100
|
+
const indentedBody = this.indent(body, 2);
|
|
101
|
+
return `it('${this.escapeString(name)}', () => {\n${indentedBody}\n});`;
|
|
102
|
+
}
|
|
103
|
+
buildAsyncTestCase(name, body) {
|
|
104
|
+
const indentedBody = this.indent(body, 2);
|
|
105
|
+
return `it('${this.escapeString(name)}', async () => {\n${indentedBody}\n});`;
|
|
106
|
+
}
|
|
107
|
+
// ── File naming ────────────────────────────────────────────
|
|
108
|
+
generateTestFileName(sourceFile, type) {
|
|
109
|
+
const dir = dirname(sourceFile);
|
|
110
|
+
const ext = extname(sourceFile);
|
|
111
|
+
const name = basename(sourceFile, ext);
|
|
112
|
+
const suffixMap = {
|
|
113
|
+
unit: "test",
|
|
114
|
+
integration: "integration.test",
|
|
115
|
+
api: "api.test",
|
|
116
|
+
"e2e-browser": "e2e.test",
|
|
117
|
+
"e2e-mobile": "mobile.test",
|
|
118
|
+
"e2e-desktop": "desktop.test",
|
|
119
|
+
snapshot: "snap.test",
|
|
120
|
+
performance: "perf.test",
|
|
121
|
+
};
|
|
122
|
+
const suffix = suffixMap[type] ?? "test";
|
|
123
|
+
return join(dir, `${name}.${suffix}${ext}`);
|
|
124
|
+
}
|
|
125
|
+
// ── Existing test deduplication ────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Reads all test file contents from a directory so generators
|
|
128
|
+
* can skip creating tests that already exist.
|
|
129
|
+
*/
|
|
130
|
+
async readExistingTests(dir) {
|
|
131
|
+
try {
|
|
132
|
+
const entries = await readdir(dir, { recursive: true });
|
|
133
|
+
const testFiles = entries.filter((e) => typeof e === "string" &&
|
|
134
|
+
(e.endsWith(".test.ts") ||
|
|
135
|
+
e.endsWith(".test.tsx") ||
|
|
136
|
+
e.endsWith(".spec.ts") ||
|
|
137
|
+
e.endsWith(".spec.tsx")));
|
|
138
|
+
const contents = [];
|
|
139
|
+
for (const file of testFiles) {
|
|
140
|
+
try {
|
|
141
|
+
const content = await readFile(join(dir, file), "utf-8");
|
|
142
|
+
contents.push(content);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Silently skip unreadable files
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return contents;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Returns true if an existing test already covers the given target name
|
|
156
|
+
* (function, class, endpoint, etc.) by scanning existing test contents.
|
|
157
|
+
*/
|
|
158
|
+
isAlreadyTested(targetName, existingTests) {
|
|
159
|
+
const pattern = new RegExp(`(?:describe|it|test)\\s*\\(\\s*['"\`].*${this.escapeRegExp(targetName)}`, "i");
|
|
160
|
+
return existingTests.some((content) => pattern.test(content));
|
|
161
|
+
}
|
|
162
|
+
// ── Utility helpers ────────────────────────────────────────
|
|
163
|
+
indent(text, spaces) {
|
|
164
|
+
const pad = " ".repeat(spaces);
|
|
165
|
+
return text
|
|
166
|
+
.split("\n")
|
|
167
|
+
.map((line) => (line.trim() ? `${pad}${line}` : line))
|
|
168
|
+
.join("\n");
|
|
169
|
+
}
|
|
170
|
+
escapeString(str) {
|
|
171
|
+
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
172
|
+
}
|
|
173
|
+
escapeRegExp(str) {
|
|
174
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Generates a mock variable name from a module path.
|
|
178
|
+
* e.g. "../services/user.service" -> "mockUserService"
|
|
179
|
+
*/
|
|
180
|
+
mockNameFromModule(modulePath) {
|
|
181
|
+
const name = basename(modulePath, extname(modulePath));
|
|
182
|
+
const camel = name
|
|
183
|
+
.split(/[-.]/)
|
|
184
|
+
.map((part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))
|
|
185
|
+
.join("");
|
|
186
|
+
return `mock${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Generates a sample value for a given TypeScript type string.
|
|
190
|
+
* Used to create meaningful test data.
|
|
191
|
+
*/
|
|
192
|
+
sampleValueForType(type) {
|
|
193
|
+
if (!type)
|
|
194
|
+
return "'test-value'";
|
|
195
|
+
const normalized = type.trim().toLowerCase();
|
|
196
|
+
if (normalized === "string")
|
|
197
|
+
return "'test-string'";
|
|
198
|
+
if (normalized === "number")
|
|
199
|
+
return "42";
|
|
200
|
+
if (normalized === "boolean")
|
|
201
|
+
return "true";
|
|
202
|
+
if (normalized === "void" || normalized === "undefined")
|
|
203
|
+
return "undefined";
|
|
204
|
+
if (normalized === "null")
|
|
205
|
+
return "null";
|
|
206
|
+
if (normalized.endsWith("[]"))
|
|
207
|
+
return "[]";
|
|
208
|
+
if (normalized.startsWith("promise<")) {
|
|
209
|
+
const inner = type.slice(8, -1);
|
|
210
|
+
return this.sampleValueForType(inner);
|
|
211
|
+
}
|
|
212
|
+
if (normalized === "date")
|
|
213
|
+
return "new Date('2025-01-01')";
|
|
214
|
+
if (normalized === "record<string, any>" || normalized === "object")
|
|
215
|
+
return "{}";
|
|
216
|
+
// Default to an empty object for complex types
|
|
217
|
+
return `{} as ${type}`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Builds a full test file string from its parts.
|
|
221
|
+
*/
|
|
222
|
+
assembleTestFile(parts) {
|
|
223
|
+
const sections = [
|
|
224
|
+
parts.header ?? this.fileHeader(),
|
|
225
|
+
"",
|
|
226
|
+
...parts.imports,
|
|
227
|
+
"",
|
|
228
|
+
...parts.body,
|
|
229
|
+
"", // trailing newline
|
|
230
|
+
];
|
|
231
|
+
return sections.join("\n");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=base-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-generator.js","sourceRoot":"","sources":["../../src/generators/base-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GeneratorContext, GeneratorResult } from "../types/index.js";
|
|
2
|
+
import { BaseGenerator } from "./base-generator.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generates tests for Tauri desktop applications:
|
|
5
|
+
* - IPC command invoke tests (mocking @tauri-apps/api/core)
|
|
6
|
+
* - React component tests for Tauri frontends
|
|
7
|
+
*/
|
|
8
|
+
export declare class DesktopTestGenerator extends BaseGenerator {
|
|
9
|
+
generate(context: GeneratorContext): Promise<GeneratorResult>;
|
|
10
|
+
private buildDesktopImports;
|
|
11
|
+
private generateCommandTests;
|
|
12
|
+
private generateTauriComponentTests;
|
|
13
|
+
private generateStandardComponentTests;
|
|
14
|
+
private detectTauriCommands;
|
|
15
|
+
private getTauriCommandFunctions;
|
|
16
|
+
private isTauriIntegrated;
|
|
17
|
+
private buildCommandPayload;
|
|
18
|
+
private buildDefaultProps;
|
|
19
|
+
private toSnakeCase;
|
|
20
|
+
private relativeImportPath;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=desktop-test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"desktop-test.d.ts","sourceRoot":"","sources":["../../src/generators/desktop-test.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAKV,gBAAgB,EAChB,eAAe,EAIhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;IAC/C,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAuInE,OAAO,CAAC,mBAAmB;IA2D3B,OAAO,CAAC,oBAAoB;IA0D5B,OAAO,CAAC,2BAA2B;IAqDnC,OAAO,CAAC,8BAA8B;IA+DtC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,kBAAkB;CAK3B"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Generated by coverit — https://coverit.dev
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
import { BaseGenerator } from "./base-generator.js";
|
|
5
|
+
/**
|
|
6
|
+
* Generates tests for Tauri desktop applications:
|
|
7
|
+
* - IPC command invoke tests (mocking @tauri-apps/api/core)
|
|
8
|
+
* - React component tests for Tauri frontends
|
|
9
|
+
*/
|
|
10
|
+
export class DesktopTestGenerator extends BaseGenerator {
|
|
11
|
+
async generate(context) {
|
|
12
|
+
const { plan, project, scanResults, existingTests } = context;
|
|
13
|
+
const tests = [];
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const skipped = [];
|
|
16
|
+
for (const scan of scanResults) {
|
|
17
|
+
if (!plan.target.files.includes(scan.file))
|
|
18
|
+
continue;
|
|
19
|
+
// ── AI-powered generation (preferred path) ──────────────
|
|
20
|
+
if (this.aiProvider) {
|
|
21
|
+
try {
|
|
22
|
+
const sourceCode = await readFile(join(project.root, scan.file), "utf-8");
|
|
23
|
+
const aiResult = await this.generateWithAI({
|
|
24
|
+
sourceCode,
|
|
25
|
+
scanResult: scan,
|
|
26
|
+
testType: "e2e-desktop",
|
|
27
|
+
framework: project.testFramework,
|
|
28
|
+
existingTests,
|
|
29
|
+
});
|
|
30
|
+
if (aiResult) {
|
|
31
|
+
const testFileName = this.generateTestFileName(scan.file, "e2e-desktop");
|
|
32
|
+
tests.push({
|
|
33
|
+
planId: plan.id,
|
|
34
|
+
filePath: testFileName,
|
|
35
|
+
content: aiResult,
|
|
36
|
+
testType: "e2e-desktop",
|
|
37
|
+
testCount: this.countTestCases(aiResult),
|
|
38
|
+
framework: project.testFramework === "playwright" ||
|
|
39
|
+
project.testFramework === "detox"
|
|
40
|
+
? "vitest"
|
|
41
|
+
: project.testFramework,
|
|
42
|
+
});
|
|
43
|
+
continue; // Skip template fallback for this file
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Fall through to template-based generation
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ── Template-based generation (fallback) ────────────────
|
|
51
|
+
const hasComponents = scan.components.length > 0;
|
|
52
|
+
const hasTauriCommands = this.detectTauriCommands(scan);
|
|
53
|
+
if (!hasComponents && !hasTauriCommands) {
|
|
54
|
+
skipped.push({
|
|
55
|
+
target: scan.file,
|
|
56
|
+
reason: "No Tauri components or commands detected",
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const testFileName = this.generateTestFileName(scan.file, "e2e-desktop");
|
|
61
|
+
const fw = project.testFramework;
|
|
62
|
+
const imports = this.buildDesktopImports(scan, fw, testFileName, hasTauriCommands);
|
|
63
|
+
const bodyBlocks = [];
|
|
64
|
+
let testCount = 0;
|
|
65
|
+
// Tauri IPC command tests
|
|
66
|
+
if (hasTauriCommands) {
|
|
67
|
+
const commandFns = this.getTauriCommandFunctions(scan);
|
|
68
|
+
for (const fn of commandFns) {
|
|
69
|
+
if (this.isAlreadyTested(`invoke:${fn.name}`, existingTests)) {
|
|
70
|
+
skipped.push({
|
|
71
|
+
target: `invoke:${fn.name}`,
|
|
72
|
+
reason: "Already has command tests",
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const cases = this.generateCommandTests(fn, fw);
|
|
77
|
+
bodyBlocks.push(this.buildDescribeBlock(`Tauri command: ${fn.name}`, cases));
|
|
78
|
+
testCount += cases.length;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// React component tests
|
|
82
|
+
for (const component of scan.components) {
|
|
83
|
+
if (this.isAlreadyTested(component.name, existingTests)) {
|
|
84
|
+
skipped.push({
|
|
85
|
+
target: component.name,
|
|
86
|
+
reason: "Already has desktop component tests",
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const isTauriComponent = this.isTauriIntegrated(component, scan);
|
|
91
|
+
const cases = isTauriComponent
|
|
92
|
+
? this.generateTauriComponentTests(component, scan, fw)
|
|
93
|
+
: this.generateStandardComponentTests(component, fw);
|
|
94
|
+
bodyBlocks.push(this.buildDescribeBlock(component.name, cases));
|
|
95
|
+
testCount += cases.length;
|
|
96
|
+
}
|
|
97
|
+
if (testCount === 0)
|
|
98
|
+
continue;
|
|
99
|
+
const content = this.assembleTestFile({
|
|
100
|
+
imports,
|
|
101
|
+
body: bodyBlocks,
|
|
102
|
+
});
|
|
103
|
+
tests.push({
|
|
104
|
+
planId: plan.id,
|
|
105
|
+
filePath: testFileName,
|
|
106
|
+
content,
|
|
107
|
+
testType: "e2e-desktop",
|
|
108
|
+
testCount,
|
|
109
|
+
framework: fw === "playwright" || fw === "detox" ? "vitest" : fw,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return { tests, warnings, skipped };
|
|
113
|
+
}
|
|
114
|
+
// ── Import generation ──────────────────────────────────────
|
|
115
|
+
buildDesktopImports(scan, fw, testFilePath, hasTauriCommands) {
|
|
116
|
+
const lines = [this.buildImports(fw)];
|
|
117
|
+
const mockFn = fw === "vitest" ? "vi" : "jest";
|
|
118
|
+
// Mock Tauri core if IPC commands are used
|
|
119
|
+
if (hasTauriCommands) {
|
|
120
|
+
lines.push(`${mockFn === "vi" ? "vi.mock" : "jest.mock"}('@tauri-apps/api/core', () => ({`, ` invoke: ${mockFn}.fn(),`, `}));`, "", `import { invoke } from '@tauri-apps/api/core';`);
|
|
121
|
+
}
|
|
122
|
+
// Import testing library for React component tests
|
|
123
|
+
if (scan.components.length > 0) {
|
|
124
|
+
lines.push("import { render, screen, fireEvent, waitFor } from '@testing-library/react';");
|
|
125
|
+
}
|
|
126
|
+
// Import source components/functions
|
|
127
|
+
const sourcePath = this.relativeImportPath(testFilePath, scan.file);
|
|
128
|
+
const exportedNames = scan.exports
|
|
129
|
+
.filter((e) => e.kind === "function" || e.kind === "class")
|
|
130
|
+
.map((e) => e.name);
|
|
131
|
+
const componentNames = scan.components.map((c) => c.name);
|
|
132
|
+
const allNames = [...new Set([...exportedNames, ...componentNames])];
|
|
133
|
+
if (allNames.length > 0) {
|
|
134
|
+
const defaultExport = scan.exports.find((e) => e.isDefault);
|
|
135
|
+
const namedExports = allNames.filter((n) => n !== defaultExport?.name);
|
|
136
|
+
if (defaultExport && namedExports.length > 0) {
|
|
137
|
+
lines.push(`import ${defaultExport.name}, { ${namedExports.join(", ")} } from '${sourcePath}';`);
|
|
138
|
+
}
|
|
139
|
+
else if (defaultExport) {
|
|
140
|
+
lines.push(`import ${defaultExport.name} from '${sourcePath}';`);
|
|
141
|
+
}
|
|
142
|
+
else if (namedExports.length > 0) {
|
|
143
|
+
lines.push(`import { ${namedExports.join(", ")} } from '${sourcePath}';`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return lines;
|
|
147
|
+
}
|
|
148
|
+
// ── Tauri command tests ────────────────────────────────────
|
|
149
|
+
generateCommandTests(fn, fw) {
|
|
150
|
+
const cases = [];
|
|
151
|
+
const mockFn = fw === "vitest" ? "vi" : "jest";
|
|
152
|
+
const mockedInvoke = `${mockFn === "vi" ? "vi.mocked" : "jest.mocked"}(invoke)`;
|
|
153
|
+
// Build sample payload from function params
|
|
154
|
+
const payload = this.buildCommandPayload(fn.params);
|
|
155
|
+
const payloadStr = payload ? `, ${payload}` : "";
|
|
156
|
+
const returnSample = this.sampleValueForType(fn.returnType);
|
|
157
|
+
// Success case
|
|
158
|
+
cases.push(this.buildAsyncTestCase("should invoke command successfully", [
|
|
159
|
+
`${mockedInvoke}.mockResolvedValueOnce(${returnSample});`,
|
|
160
|
+
`const result = await invoke('${this.toSnakeCase(fn.name)}'${payloadStr});`,
|
|
161
|
+
`expect(invoke).toHaveBeenCalledWith('${this.toSnakeCase(fn.name)}'${payloadStr});`,
|
|
162
|
+
`expect(result).toEqual(${returnSample});`,
|
|
163
|
+
].join("\n")));
|
|
164
|
+
// Error case
|
|
165
|
+
cases.push(this.buildAsyncTestCase("should handle command error", [
|
|
166
|
+
`${mockedInvoke}.mockRejectedValueOnce(new Error('Command failed'));`,
|
|
167
|
+
`await expect(invoke('${this.toSnakeCase(fn.name)}'${payloadStr})).rejects.toThrow('Command failed');`,
|
|
168
|
+
].join("\n")));
|
|
169
|
+
// Payload validation (if params exist)
|
|
170
|
+
if (fn.params.length > 0) {
|
|
171
|
+
cases.push(this.buildAsyncTestCase("should send correct payload", [
|
|
172
|
+
`${mockedInvoke}.mockResolvedValueOnce(undefined);`,
|
|
173
|
+
`await invoke('${this.toSnakeCase(fn.name)}'${payloadStr});`,
|
|
174
|
+
`expect(invoke).toHaveBeenCalledTimes(1);`,
|
|
175
|
+
`const calledPayload = ${mockedInvoke}.mock.calls[0][1];`,
|
|
176
|
+
`expect(calledPayload).toBeDefined();`,
|
|
177
|
+
].join("\n")));
|
|
178
|
+
}
|
|
179
|
+
return cases;
|
|
180
|
+
}
|
|
181
|
+
// ── Tauri-integrated component tests ───────────────────────
|
|
182
|
+
generateTauriComponentTests(component, _scan, fw) {
|
|
183
|
+
const cases = [];
|
|
184
|
+
const mockFn = fw === "vitest" ? "vi" : "jest";
|
|
185
|
+
const mockedInvoke = `${mockFn === "vi" ? "vi.mocked" : "jest.mocked"}(invoke)`;
|
|
186
|
+
const propStr = this.buildDefaultProps(component.props, fw);
|
|
187
|
+
// Rendering test
|
|
188
|
+
cases.push(this.buildTestCase("should render without crashing", [
|
|
189
|
+
`const { container } = render(<${component.name}${propStr} />);`,
|
|
190
|
+
`expect(container).toBeTruthy();`,
|
|
191
|
+
].join("\n")));
|
|
192
|
+
// Test that Tauri commands are invoked on mount/interaction
|
|
193
|
+
cases.push(this.buildAsyncTestCase("should invoke Tauri commands", [
|
|
194
|
+
`${mockedInvoke}.mockResolvedValue({});`,
|
|
195
|
+
`render(<${component.name}${propStr} />);`,
|
|
196
|
+
`await waitFor(() => {`,
|
|
197
|
+
` expect(invoke).toHaveBeenCalled();`,
|
|
198
|
+
`});`,
|
|
199
|
+
].join("\n")));
|
|
200
|
+
// Reset mocks
|
|
201
|
+
cases.push(this.buildTestCase("should clean up on unmount", [
|
|
202
|
+
`${mockedInvoke}.mockResolvedValue({});`,
|
|
203
|
+
`const { unmount } = render(<${component.name}${propStr} />);`,
|
|
204
|
+
`unmount();`,
|
|
205
|
+
`// Component should unmount without errors`,
|
|
206
|
+
].join("\n")));
|
|
207
|
+
return cases;
|
|
208
|
+
}
|
|
209
|
+
// ── Standard React component tests ─────────────────────────
|
|
210
|
+
generateStandardComponentTests(component, fw) {
|
|
211
|
+
const cases = [];
|
|
212
|
+
const propStr = this.buildDefaultProps(component.props, fw);
|
|
213
|
+
// Basic render
|
|
214
|
+
cases.push(this.buildTestCase("should render without crashing", [
|
|
215
|
+
`const { container } = render(<${component.name}${propStr} />);`,
|
|
216
|
+
`expect(container).toBeTruthy();`,
|
|
217
|
+
].join("\n")));
|
|
218
|
+
// Event handlers
|
|
219
|
+
const eventProps = component.props.filter((p) => /^on[A-Z]/.test(p.name));
|
|
220
|
+
for (const prop of eventProps) {
|
|
221
|
+
const mockName = `mock${prop.name.charAt(0).toUpperCase()}${prop.name.slice(1)}`;
|
|
222
|
+
const mockInit = fw === "vitest" ? "vi.fn()" : "jest.fn()";
|
|
223
|
+
cases.push(this.buildTestCase(`should handle ${prop.name}`, [
|
|
224
|
+
`const ${mockName} = ${mockInit};`,
|
|
225
|
+
`render(<${component.name}${propStr} ${prop.name}={${mockName}} />);`,
|
|
226
|
+
`const target = screen.getByRole('button');`,
|
|
227
|
+
`fireEvent.click(target);`,
|
|
228
|
+
`expect(${mockName}).toHaveBeenCalled();`,
|
|
229
|
+
].join("\n")));
|
|
230
|
+
}
|
|
231
|
+
// Conditional rendering for boolean props
|
|
232
|
+
const boolProps = component.props.filter((p) => p.type?.toLowerCase() === "boolean");
|
|
233
|
+
for (const prop of boolProps) {
|
|
234
|
+
cases.push(this.buildTestCase(`should render correctly when ${prop.name} is false`, [
|
|
235
|
+
`const { container } = render(<${component.name}${propStr} ${prop.name}={false} />);`,
|
|
236
|
+
`expect(container).toBeTruthy();`,
|
|
237
|
+
].join("\n")));
|
|
238
|
+
}
|
|
239
|
+
return cases;
|
|
240
|
+
}
|
|
241
|
+
// ── Detection helpers ──────────────────────────────────────
|
|
242
|
+
detectTauriCommands(scan) {
|
|
243
|
+
return scan.imports.some((m) => m.source === "@tauri-apps/api/core" ||
|
|
244
|
+
m.source === "@tauri-apps/api" ||
|
|
245
|
+
m.source.startsWith("@tauri-apps/"));
|
|
246
|
+
}
|
|
247
|
+
getTauriCommandFunctions(scan) {
|
|
248
|
+
// Functions that likely invoke Tauri commands are exported async functions
|
|
249
|
+
// in files that import from @tauri-apps
|
|
250
|
+
return scan.functions.filter((f) => f.isExported && f.isAsync);
|
|
251
|
+
}
|
|
252
|
+
isTauriIntegrated(_component, scan) {
|
|
253
|
+
return this.detectTauriCommands(scan);
|
|
254
|
+
}
|
|
255
|
+
// ── Utility helpers ────────────────────────────────────────
|
|
256
|
+
buildCommandPayload(params) {
|
|
257
|
+
if (params.length === 0)
|
|
258
|
+
return null;
|
|
259
|
+
const fields = params
|
|
260
|
+
.map((p) => `${p.name}: ${this.sampleValueForType(p.type)}`)
|
|
261
|
+
.join(", ");
|
|
262
|
+
return `{ ${fields} }`;
|
|
263
|
+
}
|
|
264
|
+
buildDefaultProps(props, fw) {
|
|
265
|
+
const required = props.filter((p) => !p.isOptional);
|
|
266
|
+
if (required.length === 0)
|
|
267
|
+
return "";
|
|
268
|
+
const mockInit = fw === "vitest" ? "vi.fn()" : "jest.fn()";
|
|
269
|
+
const assignments = required
|
|
270
|
+
.map((p) => {
|
|
271
|
+
if (/^on[A-Z]/.test(p.name))
|
|
272
|
+
return `${p.name}={${mockInit}}`;
|
|
273
|
+
return `${p.name}={${this.sampleValueForType(p.type)}}`;
|
|
274
|
+
})
|
|
275
|
+
.join(" ");
|
|
276
|
+
return ` ${assignments}`;
|
|
277
|
+
}
|
|
278
|
+
toSnakeCase(name) {
|
|
279
|
+
return name
|
|
280
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
281
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
282
|
+
.toLowerCase();
|
|
283
|
+
}
|
|
284
|
+
relativeImportPath(from, to) {
|
|
285
|
+
const rel = relative(dirname(from), to);
|
|
286
|
+
const withoutExt = rel.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
287
|
+
return rel.startsWith(".") ? withoutExt : `./${withoutExt}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=desktop-test.js.map
|