@elench/testkit 0.1.56 → 0.1.58
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/README.md +59 -0
- package/lib/cli/commands/browser/serve.mjs +112 -0
- package/lib/cli/commands/discover.mjs +80 -0
- package/lib/cli/entrypoint.mjs +5 -0
- package/lib/cli/presentation/colors.mjs +32 -0
- package/lib/cli/presentation/discovery-reporter.mjs +166 -0
- package/lib/config/discovery.mjs +106 -45
- package/lib/config/discovery.test.mjs +78 -3
- package/lib/config/index.mjs +50 -3
- package/lib/coverage/index.mjs +774 -0
- package/lib/coverage/index.test.mjs +220 -0
- package/lib/discovery/index.d.ts +124 -0
- package/lib/discovery/index.mjs +552 -0
- package/lib/discovery/index.test.mjs +182 -0
- package/lib/history/index.d.ts +46 -0
- package/lib/history/index.mjs +166 -0
- package/lib/history/index.test.mjs +115 -0
- package/lib/package.test.mjs +5 -0
- package/lib/runner/orchestrator.mjs +7 -0
- package/lib/setup/index.d.ts +5 -0
- package/node_modules/@elench/testkit-bridge/package.json +17 -0
- package/node_modules/@elench/testkit-bridge/src/index.mjs +391 -0
- package/node_modules/@elench/testkit-bridge/src/index.test.mjs +183 -0
- package/node_modules/@elench/testkit-protocol/package.json +18 -0
- package/node_modules/@elench/testkit-protocol/src/index.d.ts +204 -0
- package/node_modules/@elench/testkit-protocol/src/index.mjs +245 -0
- package/node_modules/@elench/testkit-protocol/src/index.test.mjs +154 -0
- package/package.json +15 -2
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { buildCoverageGraph, buildServiceCoverageContext } from "./index.mjs";
|
|
6
|
+
|
|
7
|
+
const cleanups = [];
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
while (cleanups.length > 0) {
|
|
11
|
+
cleanups.pop()();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("coverage graph builder", () => {
|
|
16
|
+
it("builds a Next page -> request -> API route -> server capability graph", () => {
|
|
17
|
+
const productDir = createProduct();
|
|
18
|
+
writeFile(
|
|
19
|
+
productDir,
|
|
20
|
+
"testkit.setup.ts",
|
|
21
|
+
`
|
|
22
|
+
import { defineTestkitSetup, nextService } from "@elench/testkit/setup";
|
|
23
|
+
|
|
24
|
+
export default defineTestkitSetup({
|
|
25
|
+
services: {
|
|
26
|
+
web: nextService({
|
|
27
|
+
cwd: ".",
|
|
28
|
+
start: "node server.js",
|
|
29
|
+
baseUrl: "http://127.0.0.1:3000",
|
|
30
|
+
readyUrl: "http://127.0.0.1:3000"
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
`
|
|
35
|
+
);
|
|
36
|
+
writeFile(
|
|
37
|
+
productDir,
|
|
38
|
+
"src/app/campaigns/page.tsx",
|
|
39
|
+
`
|
|
40
|
+
"use client";
|
|
41
|
+
import { getJson } from "@/client/http/http";
|
|
42
|
+
|
|
43
|
+
export default async function CampaignsPage() {
|
|
44
|
+
await getJson("/api/campaigns");
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
);
|
|
49
|
+
writeFile(productDir, "src/client/http/http.ts", `export function getJson() { return null; }`);
|
|
50
|
+
writeFile(
|
|
51
|
+
productDir,
|
|
52
|
+
"src/app/api/campaigns/route.ts",
|
|
53
|
+
`
|
|
54
|
+
import { listCampaigns, createCampaign } from "@/backend/server/campaigns";
|
|
55
|
+
|
|
56
|
+
export async function GET() {
|
|
57
|
+
return listCampaigns();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function POST() {
|
|
61
|
+
return createCampaign();
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
);
|
|
65
|
+
writeFile(productDir, "src/backend/server/campaigns/index.ts", `export async function listCampaigns() {}\nexport async function createCampaign() {}`);
|
|
66
|
+
|
|
67
|
+
const context = buildServiceCoverageContext(productDir, "web", {
|
|
68
|
+
local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(context.pageByRoute.get("/campaigns").node).toMatchObject({
|
|
72
|
+
kind: "page_view",
|
|
73
|
+
route: "/campaigns",
|
|
74
|
+
filePath: "src/app/campaigns/page.tsx",
|
|
75
|
+
});
|
|
76
|
+
expect(context.apiRouteByKey.get("GET:/api/campaigns").node).toMatchObject({
|
|
77
|
+
kind: "api_route",
|
|
78
|
+
route: "/campaigns",
|
|
79
|
+
method: "GET",
|
|
80
|
+
path: "/api/campaigns",
|
|
81
|
+
filePath: "src/app/api/campaigns/route.ts",
|
|
82
|
+
});
|
|
83
|
+
expect(context.graph.edges).toEqual(
|
|
84
|
+
expect.arrayContaining([
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
kind: "requests",
|
|
87
|
+
from: "page_view:web:/campaigns",
|
|
88
|
+
}),
|
|
89
|
+
expect.objectContaining({
|
|
90
|
+
kind: "handles",
|
|
91
|
+
to: "api_route:web:GET:/api/campaigns",
|
|
92
|
+
}),
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
kind: "delegates_to",
|
|
95
|
+
from: "api_route:web:GET:/api/campaigns",
|
|
96
|
+
}),
|
|
97
|
+
])
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("discovers server action links from pages", () => {
|
|
102
|
+
const productDir = createProduct();
|
|
103
|
+
writeFile(
|
|
104
|
+
productDir,
|
|
105
|
+
"src/app/settings/page.tsx",
|
|
106
|
+
`
|
|
107
|
+
import { saveSettings } from "./actions";
|
|
108
|
+
|
|
109
|
+
export default function SettingsPage() {
|
|
110
|
+
void saveSettings;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
`
|
|
114
|
+
);
|
|
115
|
+
writeFile(
|
|
116
|
+
productDir,
|
|
117
|
+
"src/app/settings/actions.ts",
|
|
118
|
+
`
|
|
119
|
+
"use server";
|
|
120
|
+
import { updateSettings } from "@/backend/server/settings";
|
|
121
|
+
|
|
122
|
+
export async function saveSettings() {
|
|
123
|
+
return updateSettings();
|
|
124
|
+
}
|
|
125
|
+
`
|
|
126
|
+
);
|
|
127
|
+
writeFile(productDir, "src/backend/server/settings.ts", `export async function updateSettings() {}`);
|
|
128
|
+
|
|
129
|
+
const context = buildServiceCoverageContext(productDir, "web", {
|
|
130
|
+
local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(context.serverActionByExportKey.get("src/app/settings/actions.ts#saveSettings").node).toMatchObject({
|
|
134
|
+
kind: "server_action",
|
|
135
|
+
label: "saveSettings",
|
|
136
|
+
});
|
|
137
|
+
expect(context.graph.edges).toEqual(
|
|
138
|
+
expect.arrayContaining([
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
kind: "triggers",
|
|
141
|
+
from: "page_view:web:/settings",
|
|
142
|
+
to: "server_action:web:src/app/settings/actions.ts#saveSettings",
|
|
143
|
+
}),
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
kind: "delegates_to",
|
|
146
|
+
from: "server_action:web:src/app/settings/actions.ts#saveSettings",
|
|
147
|
+
}),
|
|
148
|
+
])
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("attaches convention-based test evidence to page and route nodes", () => {
|
|
153
|
+
const productDir = createProduct();
|
|
154
|
+
writeFile(productDir, "src/app/campaigns/page.tsx", `export default function CampaignsPage() { return null; }`);
|
|
155
|
+
writeFile(productDir, "src/app/api/campaigns/route.ts", `export async function GET() { return Response.json({ ok: true }); }`);
|
|
156
|
+
|
|
157
|
+
const graph = buildCoverageGraph({
|
|
158
|
+
productDir,
|
|
159
|
+
services: {
|
|
160
|
+
web: {
|
|
161
|
+
local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
discoveryFiles: [
|
|
165
|
+
{
|
|
166
|
+
serviceName: "web",
|
|
167
|
+
type: "e2e",
|
|
168
|
+
framework: "playwright",
|
|
169
|
+
suiteName: "campaigns",
|
|
170
|
+
filePath: "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
serviceName: "web",
|
|
174
|
+
type: "integration",
|
|
175
|
+
framework: "k6",
|
|
176
|
+
suiteName: "campaigns",
|
|
177
|
+
filePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(graph.evidence).toEqual(
|
|
183
|
+
expect.arrayContaining([
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
testFilePath: "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
|
|
186
|
+
coveredNodeIds: ["page_view:web:/campaigns"],
|
|
187
|
+
}),
|
|
188
|
+
expect.objectContaining({
|
|
189
|
+
testFilePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
|
|
190
|
+
coveredNodeIds: ["api_route:web:GET:/api/campaigns"],
|
|
191
|
+
}),
|
|
192
|
+
])
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("normalizes dynamic routes", () => {
|
|
197
|
+
const productDir = createProduct();
|
|
198
|
+
writeFile(productDir, "src/app/projects/[projectId]/page.tsx", `export default function ProjectPage() { return null; }`);
|
|
199
|
+
writeFile(productDir, "src/app/api/projects/[projectId]/route.ts", `export async function GET() { return Response.json({ ok: true }); }`);
|
|
200
|
+
|
|
201
|
+
const context = buildServiceCoverageContext(productDir, "web", {
|
|
202
|
+
local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(context.pageByRoute.get("/projects/[projectId]").node).toBeTruthy();
|
|
206
|
+
expect(context.apiRouteByKey.get("GET:/api/projects/[projectId]").node).toBeTruthy();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
function createProduct() {
|
|
211
|
+
const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-coverage-"));
|
|
212
|
+
cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
|
|
213
|
+
return productDir;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function writeFile(productDir, relativePath, content = "export {};\n") {
|
|
217
|
+
const absolutePath = path.join(productDir, relativePath);
|
|
218
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
219
|
+
fs.writeFileSync(absolutePath, content);
|
|
220
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { CoverageGraph } from "@elench/testkit-protocol";
|
|
2
|
+
|
|
3
|
+
export type DiscoverySelectionType = "int" | "e2e" | "scenario" | "dal" | "load" | "pw";
|
|
4
|
+
export type DiscoveryInternalType = "integration" | "e2e" | "scenario" | "dal" | "load";
|
|
5
|
+
export type DiscoveryFramework = "k6" | "playwright";
|
|
6
|
+
|
|
7
|
+
export interface DiscoveryDiagnostic {
|
|
8
|
+
code: string;
|
|
9
|
+
severity: "error" | "warning";
|
|
10
|
+
message: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
serviceNames?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DiscoveryService {
|
|
16
|
+
name: string;
|
|
17
|
+
discovered: boolean;
|
|
18
|
+
localCwd: string;
|
|
19
|
+
dependsOn: string[];
|
|
20
|
+
suiteCount: number;
|
|
21
|
+
fileCount: number;
|
|
22
|
+
activeFileCount: number;
|
|
23
|
+
skippedFileCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DiscoverySuite {
|
|
27
|
+
id: string;
|
|
28
|
+
service: string;
|
|
29
|
+
name: string;
|
|
30
|
+
displayName: string;
|
|
31
|
+
selectionType: DiscoverySelectionType;
|
|
32
|
+
internalType: DiscoveryInternalType;
|
|
33
|
+
framework: DiscoveryFramework;
|
|
34
|
+
groupLabel: string;
|
|
35
|
+
fileCount: number;
|
|
36
|
+
activeFileCount: number;
|
|
37
|
+
skippedFileCount: number;
|
|
38
|
+
dependsOn: string[];
|
|
39
|
+
locks: string[];
|
|
40
|
+
filePaths: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DiscoveryHistorySummary {
|
|
44
|
+
firstSeenAt?: string | null;
|
|
45
|
+
lastSeenAt?: string | null;
|
|
46
|
+
lastRunAt?: string | null;
|
|
47
|
+
runCount?: number;
|
|
48
|
+
passCount?: number;
|
|
49
|
+
failCount?: number;
|
|
50
|
+
skipCount?: number;
|
|
51
|
+
avgDurationMs?: number;
|
|
52
|
+
lastStatus?: "passed" | "failed" | "skipped" | "not_run" | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DiscoveryFile {
|
|
56
|
+
id: string;
|
|
57
|
+
path: string;
|
|
58
|
+
displayName: string;
|
|
59
|
+
service: string;
|
|
60
|
+
suiteName: string;
|
|
61
|
+
groupLabel: string;
|
|
62
|
+
selectionType: DiscoverySelectionType;
|
|
63
|
+
internalType: DiscoveryInternalType;
|
|
64
|
+
framework: DiscoveryFramework;
|
|
65
|
+
skipped: boolean;
|
|
66
|
+
skipReason: string | null;
|
|
67
|
+
locks: string[];
|
|
68
|
+
dependsOn: string[];
|
|
69
|
+
history?: DiscoveryHistorySummary;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DiscoveryResult {
|
|
73
|
+
schemaVersion: number;
|
|
74
|
+
source: "testkit-discovery";
|
|
75
|
+
product: {
|
|
76
|
+
name: string;
|
|
77
|
+
directory: string;
|
|
78
|
+
};
|
|
79
|
+
setupFile: string | null;
|
|
80
|
+
filters: {
|
|
81
|
+
service: string | null;
|
|
82
|
+
types: DiscoverySelectionType[] | ["all"];
|
|
83
|
+
suiteSelectors: string[];
|
|
84
|
+
fileNames: string[];
|
|
85
|
+
runnableOnly: boolean;
|
|
86
|
+
diagnostics: "error" | "report";
|
|
87
|
+
};
|
|
88
|
+
services: DiscoveryService[];
|
|
89
|
+
suites: DiscoverySuite[];
|
|
90
|
+
files: DiscoveryFile[];
|
|
91
|
+
coverageGraph: CoverageGraph | null;
|
|
92
|
+
diagnostics: DiscoveryDiagnostic[];
|
|
93
|
+
summary: {
|
|
94
|
+
services: number;
|
|
95
|
+
suites: number;
|
|
96
|
+
files: number;
|
|
97
|
+
activeFiles: number;
|
|
98
|
+
skippedFiles: number;
|
|
99
|
+
diagnostics: {
|
|
100
|
+
errors: number;
|
|
101
|
+
warnings: number;
|
|
102
|
+
};
|
|
103
|
+
byService: Record<string, number>;
|
|
104
|
+
byType: Record<string, number>;
|
|
105
|
+
};
|
|
106
|
+
history: {
|
|
107
|
+
available: boolean;
|
|
108
|
+
path?: string;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface DiscoverTestsOptions {
|
|
113
|
+
dir?: string;
|
|
114
|
+
service?: string;
|
|
115
|
+
type?: string | string[];
|
|
116
|
+
suite?: string | string[];
|
|
117
|
+
file?: string | string[];
|
|
118
|
+
runnableOnly?: boolean;
|
|
119
|
+
diagnostics?: "error" | "report";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export declare function discoverTests(options?: DiscoverTestsOptions): Promise<DiscoveryResult>;
|
|
123
|
+
export declare function formatSelectionTypeLabel(type: DiscoverySelectionType): string;
|
|
124
|
+
export declare function formatDisplayName(value: string): string;
|