@elench/testkit 0.1.57 → 0.1.59
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/lib/cli/commands/browser/serve.mjs +112 -0
- package/lib/cli/entrypoint.mjs +4 -0
- package/lib/config/discovery.mjs +61 -32
- package/lib/config/discovery.test.mjs +65 -2
- package/lib/config/index.mjs +58 -1
- package/lib/coverage/index.mjs +776 -0
- package/lib/coverage/index.test.mjs +241 -0
- package/lib/discovery/index.d.ts +3 -0
- package/lib/discovery/index.mjs +17 -2
- package/lib/discovery/path-policy.mjs +106 -0
- package/lib/discovery/path-policy.test.mjs +65 -0
- package/lib/setup/index.d.ts +12 -3
- 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 +11 -2
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import {
|
|
4
|
+
TESTKIT_BROWSER_PROTOCOL_VERSION,
|
|
5
|
+
createBridgeErrorResponse,
|
|
6
|
+
} from "@elench/testkit-protocol";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const BRIDGE_PACKAGE_VERSION = require("../package.json").version;
|
|
10
|
+
|
|
11
|
+
export function createBrowserBridge(adapter) {
|
|
12
|
+
if (!adapter || typeof adapter.loadProductContext !== "function") {
|
|
13
|
+
throw new Error("createBrowserBridge(adapter) requires a loadProductContext function");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
async handleRequest(request, response) {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
20
|
+
const context = await adapter.loadProductContext();
|
|
21
|
+
|
|
22
|
+
if (request.method === "OPTIONS") {
|
|
23
|
+
response.statusCode = 204;
|
|
24
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
25
|
+
response.setHeader("access-control-allow-methods", "GET, OPTIONS");
|
|
26
|
+
response.setHeader("access-control-allow-headers", "content-type");
|
|
27
|
+
response.end();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (request.method !== "GET") {
|
|
32
|
+
return writeJson(response, 405, createBridgeErrorResponse("method_not_allowed", "Only GET is supported"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (url.pathname === "/health") {
|
|
36
|
+
return writeJson(response, 200, buildHealthResponse(context));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (url.pathname === "/match") {
|
|
40
|
+
const pageUrl = url.searchParams.get("url");
|
|
41
|
+
if (!pageUrl) {
|
|
42
|
+
return writeJson(response, 400, createBridgeErrorResponse("missing_url", 'Expected query parameter "url"'));
|
|
43
|
+
}
|
|
44
|
+
return writeJson(response, 200, buildMatchResponse(context, pageUrl));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (url.pathname === "/page" || url.pathname === "/failures" || url.pathname === "/coverage") {
|
|
48
|
+
const pageUrl = url.searchParams.get("url");
|
|
49
|
+
if (!pageUrl) {
|
|
50
|
+
return writeJson(response, 400, createBridgeErrorResponse("missing_url", 'Expected query parameter "url"'));
|
|
51
|
+
}
|
|
52
|
+
const payload = buildPageOverlayResponse(context, pageUrl);
|
|
53
|
+
if (url.pathname === "/failures") {
|
|
54
|
+
return writeJson(response, 200, {
|
|
55
|
+
protocolVersion: payload.protocolVersion,
|
|
56
|
+
page: payload.page,
|
|
57
|
+
match: payload.match,
|
|
58
|
+
run: payload.run,
|
|
59
|
+
summary: payload.summary,
|
|
60
|
+
failures: payload.failures,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (url.pathname === "/coverage") {
|
|
64
|
+
return writeJson(response, 200, {
|
|
65
|
+
protocolVersion: payload.protocolVersion,
|
|
66
|
+
page: payload.page,
|
|
67
|
+
match: payload.match,
|
|
68
|
+
run: payload.run,
|
|
69
|
+
summary: payload.summary,
|
|
70
|
+
coverage: payload.coverage,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return writeJson(response, 200, payload);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return writeJson(response, 404, createBridgeErrorResponse("not_found", `No bridge endpoint for ${url.pathname}`));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return writeJson(
|
|
79
|
+
response,
|
|
80
|
+
500,
|
|
81
|
+
createBridgeErrorResponse("internal_error", error instanceof Error ? error.message : String(error))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function startBrowserBridgeServer(adapter, options = {}) {
|
|
89
|
+
const bridge = createBrowserBridge(adapter);
|
|
90
|
+
const host = options.host || "127.0.0.1";
|
|
91
|
+
const port = Number(options.port || 3847);
|
|
92
|
+
const server = http.createServer((request, response) => {
|
|
93
|
+
bridge.handleRequest(request, response);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
server.once("error", reject);
|
|
98
|
+
server.listen(port, host, () => {
|
|
99
|
+
server.removeListener("error", reject);
|
|
100
|
+
resolve({
|
|
101
|
+
server,
|
|
102
|
+
host,
|
|
103
|
+
port,
|
|
104
|
+
url: `http://${host}:${port}`,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildHealthResponse(context) {
|
|
111
|
+
return {
|
|
112
|
+
protocolVersion: TESTKIT_BROWSER_PROTOCOL_VERSION,
|
|
113
|
+
bridgeVersion: BRIDGE_PACKAGE_VERSION,
|
|
114
|
+
status: "ok",
|
|
115
|
+
product: context.product,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildMatchResponse(context, pageUrl) {
|
|
120
|
+
const page = normalizePage(pageUrl);
|
|
121
|
+
const service = matchServiceForPage(context.services || [], page);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
protocolVersion: TESTKIT_BROWSER_PROTOCOL_VERSION,
|
|
125
|
+
url: page.url,
|
|
126
|
+
origin: page.origin,
|
|
127
|
+
route: page.route,
|
|
128
|
+
matched: Boolean(service),
|
|
129
|
+
product: context.product,
|
|
130
|
+
service: service
|
|
131
|
+
? {
|
|
132
|
+
name: service.name,
|
|
133
|
+
baseUrl: service.baseUrl,
|
|
134
|
+
origin: service.origin,
|
|
135
|
+
}
|
|
136
|
+
: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildPageOverlayResponse(context, pageUrl) {
|
|
141
|
+
const page = normalizePage(pageUrl);
|
|
142
|
+
const match = buildMatchResponse(context, page.url);
|
|
143
|
+
const projection = buildGraphProjection(context, page, match.service?.name || null);
|
|
144
|
+
const relatedCoverageCount = projection.coverage.length;
|
|
145
|
+
const relatedFailureCount = projection.failures.length;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
protocolVersion: TESTKIT_BROWSER_PROTOCOL_VERSION,
|
|
149
|
+
page,
|
|
150
|
+
match,
|
|
151
|
+
run: {
|
|
152
|
+
artifactAvailable: Boolean(context.runArtifact),
|
|
153
|
+
generatedAt: context.runArtifact?.generatedAt || null,
|
|
154
|
+
status: context.runArtifact?.run?.status || null,
|
|
155
|
+
},
|
|
156
|
+
summary: {
|
|
157
|
+
failureState: context.runArtifact
|
|
158
|
+
? relatedFailureCount > 0
|
|
159
|
+
? "failing"
|
|
160
|
+
: "healthy"
|
|
161
|
+
: "unavailable",
|
|
162
|
+
coverageState: relatedCoverageCount > 0 ? "covered" : "missing",
|
|
163
|
+
relatedFailureCount,
|
|
164
|
+
relatedCoverageCount,
|
|
165
|
+
},
|
|
166
|
+
failures: projection.failures,
|
|
167
|
+
coverage: projection.coverage,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function collectFailedFiles(runArtifact) {
|
|
172
|
+
if (!runArtifact || !Array.isArray(runArtifact.services)) return [];
|
|
173
|
+
return runArtifact.services.flatMap((service) =>
|
|
174
|
+
(service.suites || []).flatMap((suite) =>
|
|
175
|
+
(suite.files || [])
|
|
176
|
+
.filter((file) => file.status === "failed")
|
|
177
|
+
.map((file) => ({
|
|
178
|
+
service: service.name,
|
|
179
|
+
suite: suite.name,
|
|
180
|
+
type: suite.type,
|
|
181
|
+
framework: suite.framework,
|
|
182
|
+
filePath: file.path,
|
|
183
|
+
error: file.error || null,
|
|
184
|
+
failureDetails: Array.isArray(file.failureDetails) ? file.failureDetails : [],
|
|
185
|
+
}))
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function matchServiceForPage(services, page) {
|
|
191
|
+
return [...services]
|
|
192
|
+
.filter((service) => matchesServiceBaseUrl(service.baseUrl, page))
|
|
193
|
+
.sort((left, right) => normalizeBaseUrl(right.baseUrl).length - normalizeBaseUrl(left.baseUrl).length)[0] || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function matchesServiceBaseUrl(baseUrl, page) {
|
|
197
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
198
|
+
if (!normalized) return false;
|
|
199
|
+
if (!page.url.startsWith(normalized)) return false;
|
|
200
|
+
const parsed = new URL(normalized);
|
|
201
|
+
const basePath = normalizeRoute(parsed.pathname || "/");
|
|
202
|
+
return basePath === "/" || page.route === basePath || page.route.startsWith(`${basePath}/`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizePage(value) {
|
|
206
|
+
const parsed = new URL(String(value));
|
|
207
|
+
return {
|
|
208
|
+
url: parsed.href,
|
|
209
|
+
origin: parsed.origin,
|
|
210
|
+
route: normalizeRoute(parsed.pathname || "/"),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeBaseUrl(value) {
|
|
215
|
+
if (typeof value !== "string" || value.trim().length === 0) return null;
|
|
216
|
+
try {
|
|
217
|
+
const parsed = new URL(value);
|
|
218
|
+
const normalizedPath = normalizeRoute(parsed.pathname || "/");
|
|
219
|
+
return `${parsed.origin}${normalizedPath === "/" ? "" : normalizedPath}`;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeRoute(value) {
|
|
226
|
+
const trimmed = String(value || "/").trim();
|
|
227
|
+
if (!trimmed || trimmed === "/") return "/";
|
|
228
|
+
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
229
|
+
return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/u, "") : withLeadingSlash;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function writeJson(response, statusCode, payload) {
|
|
233
|
+
response.statusCode = statusCode;
|
|
234
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
235
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
236
|
+
response.setHeader("access-control-allow-methods", "GET, OPTIONS");
|
|
237
|
+
response.setHeader("access-control-allow-headers", "content-type");
|
|
238
|
+
response.end(`${JSON.stringify(payload, null, 2)}\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildGraphProjection(context, page, matchedServiceName) {
|
|
242
|
+
const graph = context.discovery?.coverageGraph || null;
|
|
243
|
+
if (!graph || !matchedServiceName) {
|
|
244
|
+
return { coverage: [], failures: [] };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
|
|
248
|
+
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
249
|
+
const evidence = Array.isArray(graph.evidence) ? graph.evidence : [];
|
|
250
|
+
const discoveryFiles = Array.isArray(context.discovery?.files) ? context.discovery.files : [];
|
|
251
|
+
const discoveryByFile = new Map(discoveryFiles.map((entry) => [entry.path, entry]));
|
|
252
|
+
const nodeById = new Map(nodes.map((node) => [node.id, node]));
|
|
253
|
+
const outgoing = buildOutgoingEdgeMap(edges);
|
|
254
|
+
const pageNode = nodes.find(
|
|
255
|
+
(node) =>
|
|
256
|
+
node.kind === "page_view" && node.service === matchedServiceName && normalizeRoute(node.route || "/") === page.route
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (!pageNode) {
|
|
260
|
+
return { coverage: [], failures: [] };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const reachableNodeIds = collectReachableNodeIds(pageNode.id, outgoing);
|
|
264
|
+
const relevantEvidence = evidence.filter((entry) =>
|
|
265
|
+
intersects(entry.coveredNodeIds || [], reachableNodeIds)
|
|
266
|
+
);
|
|
267
|
+
const supportingTests = relevantEvidence
|
|
268
|
+
.map((entry) => buildSupportingTestRef(entry, discoveryByFile, context.runArtifact))
|
|
269
|
+
.filter(Boolean);
|
|
270
|
+
|
|
271
|
+
const viaNodes = collectViaNodes(relevantEvidence, reachableNodeIds, nodeById, pageNode.id);
|
|
272
|
+
const coverage = supportingTests.length > 0
|
|
273
|
+
? [
|
|
274
|
+
{
|
|
275
|
+
id: pageNode.id,
|
|
276
|
+
kind: pageNode.kind,
|
|
277
|
+
label: pageNode.label,
|
|
278
|
+
service: pageNode.service,
|
|
279
|
+
route: pageNode.route || null,
|
|
280
|
+
targets: pageNode.target ? [pageNode.target] : [],
|
|
281
|
+
supportingTests,
|
|
282
|
+
viaNodes,
|
|
283
|
+
confidence: supportingTests.some((entry) => entry.type === "pw") ? "high" : "medium",
|
|
284
|
+
},
|
|
285
|
+
]
|
|
286
|
+
: [];
|
|
287
|
+
|
|
288
|
+
const failedFiles = collectFailedFiles(context.runArtifact);
|
|
289
|
+
const failureByFile = new Map(failedFiles.map((entry) => [entry.filePath, entry]));
|
|
290
|
+
const failures = relevantEvidence
|
|
291
|
+
.filter((entry) => failureByFile.has(entry.testFilePath))
|
|
292
|
+
.map((entry) => {
|
|
293
|
+
const failed = failureByFile.get(entry.testFilePath);
|
|
294
|
+
const supporting = buildSupportingTestRef(entry, discoveryByFile, context.runArtifact);
|
|
295
|
+
return {
|
|
296
|
+
id: `failure:${entry.testFilePath}`,
|
|
297
|
+
kind: pageNode.kind,
|
|
298
|
+
label: supporting?.label || pageNode.label,
|
|
299
|
+
service: pageNode.service,
|
|
300
|
+
route: pageNode.route || null,
|
|
301
|
+
targets: pageNode.target ? [pageNode.target] : [],
|
|
302
|
+
failedTests: supporting ? [{ ...supporting, error: failed?.error || supporting.error || null, status: "failed" }] : [],
|
|
303
|
+
viaNodes: collectViaNodes([entry], reachableNodeIds, nodeById, pageNode.id),
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return { coverage, failures };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function buildOutgoingEdgeMap(edges) {
|
|
311
|
+
const map = new Map();
|
|
312
|
+
for (const edge of edges || []) {
|
|
313
|
+
const entries = map.get(edge.from) || [];
|
|
314
|
+
entries.push(edge.to);
|
|
315
|
+
map.set(edge.from, entries);
|
|
316
|
+
}
|
|
317
|
+
return map;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function collectReachableNodeIds(startNodeId, outgoing) {
|
|
321
|
+
const visited = new Set([startNodeId]);
|
|
322
|
+
const queue = [startNodeId];
|
|
323
|
+
while (queue.length > 0) {
|
|
324
|
+
const current = queue.shift();
|
|
325
|
+
for (const next of outgoing.get(current) || []) {
|
|
326
|
+
if (visited.has(next)) continue;
|
|
327
|
+
visited.add(next);
|
|
328
|
+
queue.push(next);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return visited;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function collectViaNodes(evidenceEntries, reachableNodeIds, nodeById, pageNodeId) {
|
|
335
|
+
const nodes = new Map();
|
|
336
|
+
for (const entry of evidenceEntries) {
|
|
337
|
+
for (const nodeId of entry.coveredNodeIds || []) {
|
|
338
|
+
if (!reachableNodeIds.has(nodeId) || nodeId === pageNodeId) continue;
|
|
339
|
+
const node = nodeById.get(nodeId);
|
|
340
|
+
if (!node) continue;
|
|
341
|
+
nodes.set(nodeId, {
|
|
342
|
+
id: node.id,
|
|
343
|
+
kind: node.kind,
|
|
344
|
+
label: node.label,
|
|
345
|
+
...(node.route ? { route: node.route } : {}),
|
|
346
|
+
...(node.method ? { method: node.method } : {}),
|
|
347
|
+
...(node.path ? { path: node.path } : {}),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return [...nodes.values()].sort((left, right) => left.id.localeCompare(right.id));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildSupportingTestRef(evidence, discoveryByFile, runArtifact) {
|
|
355
|
+
const discovery = discoveryByFile.get(evidence.testFilePath);
|
|
356
|
+
const runFile = findRunFileResult(runArtifact, evidence.testFilePath);
|
|
357
|
+
return {
|
|
358
|
+
service: evidence.service,
|
|
359
|
+
suite: evidence.suiteName,
|
|
360
|
+
type: evidence.selectionType,
|
|
361
|
+
framework: evidence.framework,
|
|
362
|
+
filePath: evidence.testFilePath,
|
|
363
|
+
label: discovery?.displayName || pathBaseName(evidence.testFilePath),
|
|
364
|
+
...(runFile?.status ? { status: runFile.status } : {}),
|
|
365
|
+
...(runFile?.error ? { error: runFile.error } : {}),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function findRunFileResult(runArtifact, targetPath) {
|
|
370
|
+
if (!runArtifact || !Array.isArray(runArtifact.services)) return null;
|
|
371
|
+
for (const service of runArtifact.services) {
|
|
372
|
+
for (const suite of service.suites || []) {
|
|
373
|
+
for (const file of suite.files || []) {
|
|
374
|
+
if (file.path === targetPath) return file;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function intersects(values, setLike) {
|
|
382
|
+
for (const value of values || []) {
|
|
383
|
+
if (setLike.has(value)) return true;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function pathBaseName(filePath) {
|
|
389
|
+
const parts = String(filePath || "").split("/");
|
|
390
|
+
return parts[parts.length - 1] || filePath;
|
|
391
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildMatchResponse, buildPageOverlayResponse } from "./index.mjs";
|
|
3
|
+
|
|
4
|
+
const context = {
|
|
5
|
+
product: {
|
|
6
|
+
name: "web",
|
|
7
|
+
directory: "/tmp/web",
|
|
8
|
+
},
|
|
9
|
+
services: [
|
|
10
|
+
{
|
|
11
|
+
name: "web",
|
|
12
|
+
baseUrl: "http://localhost:3000",
|
|
13
|
+
origin: "http://localhost:3000",
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
discovery: {
|
|
17
|
+
files: [
|
|
18
|
+
{
|
|
19
|
+
path: "app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
20
|
+
displayName: "Coverage",
|
|
21
|
+
service: "web",
|
|
22
|
+
suiteName: "coverage",
|
|
23
|
+
selectionType: "pw",
|
|
24
|
+
framework: "playwright",
|
|
25
|
+
skipped: false,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
path: "app/api/coverage/__testkit__/coverage.int.testkit.ts",
|
|
29
|
+
displayName: "Coverage API",
|
|
30
|
+
service: "web",
|
|
31
|
+
suiteName: "coverage",
|
|
32
|
+
selectionType: "int",
|
|
33
|
+
framework: "k6",
|
|
34
|
+
skipped: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
coverageGraph: {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
nodes: [
|
|
40
|
+
{
|
|
41
|
+
id: "page_view:web:/coverage",
|
|
42
|
+
kind: "page_view",
|
|
43
|
+
service: "web",
|
|
44
|
+
label: "Coverage",
|
|
45
|
+
route: "/coverage",
|
|
46
|
+
filePath: "app/coverage/page.tsx",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "api_route:web:GET:/api/coverage",
|
|
50
|
+
kind: "api_route",
|
|
51
|
+
service: "web",
|
|
52
|
+
label: "GET /api/coverage",
|
|
53
|
+
route: "/coverage",
|
|
54
|
+
method: "GET",
|
|
55
|
+
path: "/api/coverage",
|
|
56
|
+
filePath: "app/api/coverage/route.ts",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "test_file:web:app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
60
|
+
kind: "test_file",
|
|
61
|
+
service: "web",
|
|
62
|
+
label: "coverage.pw.testkit.ts",
|
|
63
|
+
filePath: "app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "test_file:web:app/api/coverage/__testkit__/coverage.int.testkit.ts",
|
|
67
|
+
kind: "test_file",
|
|
68
|
+
service: "web",
|
|
69
|
+
label: "coverage.int.testkit.ts",
|
|
70
|
+
filePath: "app/api/coverage/__testkit__/coverage.int.testkit.ts",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
edges: [
|
|
74
|
+
{
|
|
75
|
+
id: "requests:page_view:web:/coverage:api_route:web:GET:/api/coverage",
|
|
76
|
+
kind: "requests",
|
|
77
|
+
from: "page_view:web:/coverage",
|
|
78
|
+
to: "api_route:web:GET:/api/coverage",
|
|
79
|
+
confidence: "high",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
evidence: [
|
|
83
|
+
{
|
|
84
|
+
id: "evidence:web:app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
85
|
+
source: "convention",
|
|
86
|
+
confidence: "high",
|
|
87
|
+
service: "web",
|
|
88
|
+
suiteName: "coverage",
|
|
89
|
+
selectionType: "pw",
|
|
90
|
+
framework: "playwright",
|
|
91
|
+
testFilePath: "app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
92
|
+
coveredNodeIds: ["page_view:web:/coverage"],
|
|
93
|
+
details: { route: "/coverage" },
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "evidence:web:app/api/coverage/__testkit__/coverage.int.testkit.ts",
|
|
97
|
+
source: "convention",
|
|
98
|
+
confidence: "high",
|
|
99
|
+
service: "web",
|
|
100
|
+
suiteName: "coverage",
|
|
101
|
+
selectionType: "int",
|
|
102
|
+
framework: "k6",
|
|
103
|
+
testFilePath: "app/api/coverage/__testkit__/coverage.int.testkit.ts",
|
|
104
|
+
coveredNodeIds: ["api_route:web:GET:/api/coverage"],
|
|
105
|
+
details: { requestPaths: ["/api/coverage"] },
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
runArtifact: {
|
|
111
|
+
generatedAt: "2026-01-01T00:00:00.000Z",
|
|
112
|
+
run: {
|
|
113
|
+
status: "failed",
|
|
114
|
+
},
|
|
115
|
+
services: [
|
|
116
|
+
{
|
|
117
|
+
name: "web",
|
|
118
|
+
suites: [
|
|
119
|
+
{
|
|
120
|
+
name: "coverage",
|
|
121
|
+
type: "pw",
|
|
122
|
+
framework: "playwright",
|
|
123
|
+
files: [
|
|
124
|
+
{
|
|
125
|
+
path: "app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
126
|
+
status: "failed",
|
|
127
|
+
error: "Expected coverage heading",
|
|
128
|
+
failureDetails: [],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
describe("testkit bridge", () => {
|
|
139
|
+
it("matches pages against service base URLs", () => {
|
|
140
|
+
expect(buildMatchResponse(context, "http://localhost:3000/coverage")).toMatchObject({
|
|
141
|
+
matched: true,
|
|
142
|
+
route: "/coverage",
|
|
143
|
+
service: {
|
|
144
|
+
name: "web",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("builds page overlay responses from graph coverage and run artifacts", () => {
|
|
150
|
+
expect(buildPageOverlayResponse(context, "http://localhost:3000/coverage")).toMatchObject({
|
|
151
|
+
summary: {
|
|
152
|
+
failureState: "failing",
|
|
153
|
+
coverageState: "covered",
|
|
154
|
+
relatedFailureCount: 1,
|
|
155
|
+
relatedCoverageCount: 1,
|
|
156
|
+
},
|
|
157
|
+
failures: [
|
|
158
|
+
{
|
|
159
|
+
label: "Coverage",
|
|
160
|
+
failedTests: [
|
|
161
|
+
{
|
|
162
|
+
filePath: "app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
163
|
+
type: "pw",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
coverage: [
|
|
169
|
+
{
|
|
170
|
+
label: "Coverage",
|
|
171
|
+
supportingTests: [
|
|
172
|
+
{
|
|
173
|
+
filePath: "app/coverage/__testkit__/coverage.pw.testkit.ts",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
filePath: "app/api/coverage/__testkit__/coverage.int.testkit.ts",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elench/testkit-protocol",
|
|
3
|
+
"version": "0.1.59",
|
|
4
|
+
"description": "Shared browser protocol for testkit bridge and extension consumers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.mjs",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"default": "./src/index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/"
|
|
16
|
+
],
|
|
17
|
+
"private": false
|
|
18
|
+
}
|