@elench/testkit 0.1.57 → 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.
@@ -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
+ }
@@ -1,3 +1,5 @@
1
+ import type { CoverageGraph } from "@elench/testkit-protocol";
2
+
1
3
  export type DiscoverySelectionType = "int" | "e2e" | "scenario" | "dal" | "load" | "pw";
2
4
  export type DiscoveryInternalType = "integration" | "e2e" | "scenario" | "dal" | "load";
3
5
  export type DiscoveryFramework = "k6" | "playwright";
@@ -86,6 +88,7 @@ export interface DiscoveryResult {
86
88
  services: DiscoveryService[];
87
89
  suites: DiscoverySuite[];
88
90
  files: DiscoveryFile[];
91
+ coverageGraph: CoverageGraph | null;
89
92
  diagnostics: DiscoveryDiagnostic[];
90
93
  summary: {
91
94
  services: number;
@@ -2,6 +2,7 @@ import path from "path";
2
2
  import { loadConfigContext, resolveProductDir } from "../config/index.mjs";
3
3
  import { discoverProject } from "../config/discovery.mjs";
4
4
  import { loadTestkitSetup } from "../config/setup-loader.mjs";
5
+ import { buildCoverageGraph } from "../coverage/index.mjs";
5
6
  import { historyFilePath, loadHistory, summarizeHistoryForFiles } from "../history/index.mjs";
6
7
  import {
7
8
  matchesSelectedTypes,
@@ -11,7 +12,7 @@ import {
11
12
  suiteSelectionType,
12
13
  } from "../runner/suite-selection.mjs";
13
14
 
14
- const DISCOVERY_SCHEMA_VERSION = 1;
15
+ const DISCOVERY_SCHEMA_VERSION = 3;
15
16
 
16
17
  export async function discoverTests(options = {}) {
17
18
  const productDir = resolveProductDir(process.cwd(), options.dir);
@@ -36,6 +37,7 @@ export async function discoverTests(options = {}) {
36
37
  services: [],
37
38
  suites: [],
38
39
  files: [],
40
+ coverageGraph: null,
39
41
  diagnostics: [...setupContext.diagnostics],
40
42
  summary: emptySummary(),
41
43
  history: {
@@ -45,7 +47,7 @@ export async function discoverTests(options = {}) {
45
47
  };
46
48
 
47
49
  if (!setupContext.setup) {
48
- return finalizeDiscoveryResult(baseResult);
50
+ return finalizeDiscoveryResult(baseResult, productDir);
49
51
  }
50
52
 
51
53
  const rawDiscovery = discoverProject(productDir, setupContext.setup.services || {}, {
@@ -86,6 +88,11 @@ export async function discoverTests(options = {}) {
86
88
  services: resolved.services,
87
89
  suites: resolved.suites,
88
90
  files: resolved.files,
91
+ coverageGraph: buildCoverageGraph({
92
+ productDir,
93
+ services: setupContext.setup.services || {},
94
+ discoveryFiles: rawDiscovery.files || [],
95
+ }),
89
96
  },
90
97
  productDir
91
98
  );
@@ -102,6 +109,11 @@ export async function discoverTests(options = {}) {
102
109
  services: rawOnly.services,
103
110
  suites: rawOnly.suites,
104
111
  files: rawOnly.files,
112
+ coverageGraph: buildCoverageGraph({
113
+ productDir,
114
+ services: setupContext.setup.services || {},
115
+ discoveryFiles: rawDiscovery.files || [],
116
+ }),
105
117
  },
106
118
  productDir
107
119
  );
@@ -106,6 +106,10 @@ export interface TestkitExecutionConfig {
106
106
  fileTimeoutSeconds?: number;
107
107
  }
108
108
 
109
+ export interface BrowserServiceConfig {
110
+ origins?: string[];
111
+ }
112
+
109
113
  export interface KnownFailureIssueValidationConfig {
110
114
  provider?: "github";
111
115
  mode?: "off" | "warn" | "error";
@@ -122,6 +126,7 @@ export interface ServiceConfig {
122
126
  env?: Record<string, string>;
123
127
  envFile?: string;
124
128
  envFiles?: string[];
129
+ browser?: BrowserServiceConfig;
125
130
  local?: false | {
126
131
  baseUrl: string;
127
132
  cwd?: string;
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@elench/testkit-bridge",
3
+ "version": "0.1.58",
4
+ "description": "Browser bridge helpers for testkit",
5
+ "type": "module",
6
+ "main": "./src/index.mjs",
7
+ "exports": {
8
+ ".": "./src/index.mjs"
9
+ },
10
+ "files": [
11
+ "src/"
12
+ ],
13
+ "dependencies": {
14
+ "@elench/testkit-protocol": "0.1.58"
15
+ },
16
+ "private": false
17
+ }