@elench/testkit 0.1.59 → 0.1.60

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.
@@ -84,7 +84,7 @@ export function buildCoverageGraph({ productDir, repoDiscovery = {}, services =
84
84
  framework: entry.framework,
85
85
  testFilePath: entry.filePath,
86
86
  coveredNodeIds,
87
- details: buildEvidenceDetails(coveredNodeIds, graph),
87
+ details: buildEvidenceDetails(coveredNodeIds, graph, entry, context),
88
88
  });
89
89
  }
90
90
 
@@ -238,7 +238,7 @@ function inferCoveredNodeIdsForTest(entry, context) {
238
238
  return [...coveredNodeIds].sort();
239
239
  }
240
240
 
241
- function buildEvidenceDetails(coveredNodeIds, graph) {
241
+ function buildEvidenceDetails(coveredNodeIds, graph, entry, context) {
242
242
  const routes = new Set();
243
243
  const requestPaths = new Set();
244
244
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
@@ -255,9 +255,39 @@ function buildEvidenceDetails(coveredNodeIds, graph) {
255
255
  if (requestPaths.size > 0) {
256
256
  details.requestPaths = [...requestPaths].sort();
257
257
  }
258
+ const targets = extractPlaywrightTargets(entry, context);
259
+ if (targets.length > 0) {
260
+ details.targets = targets;
261
+ }
258
262
  return Object.keys(details).length > 0 ? details : undefined;
259
263
  }
260
264
 
265
+ function extractPlaywrightTargets(entry, context) {
266
+ if (!entry || entry.framework !== "playwright" || !context?.serviceRoot) return [];
267
+ const absolutePath = path.join(context.serviceRoot, entry.filePath);
268
+ if (!fs.existsSync(absolutePath)) return [];
269
+ const content = fs.readFileSync(absolutePath, "utf8");
270
+ const targets = [];
271
+
272
+ for (const match of content.matchAll(/\bgetByTestId\(\s*["'`]([^"'`]+)["'`]\s*\)/gu)) {
273
+ targets.push({
274
+ kind: "testId",
275
+ value: match[1],
276
+ confidence: "high",
277
+ });
278
+ }
279
+
280
+ for (const match of content.matchAll(/\blocator\(\s*["'`]\[data-testid=(?:"|')([^"'`\]]+)(?:"|')\]["'`]\s*\)/gu)) {
281
+ targets.push({
282
+ kind: "testId",
283
+ value: match[1],
284
+ confidence: "medium",
285
+ });
286
+ }
287
+
288
+ return dedupeTargets(targets);
289
+ }
290
+
261
291
  function discoverPageViews(serviceName, serviceRoot, nextAppRoot, exclude = []) {
262
292
  const pageFiles = walkFiles(nextAppRoot, { baseDir: serviceRoot, exclude }).filter(
263
293
  (filePath) => filePath.endsWith("/page.tsx") || filePath.endsWith("/page.ts")
@@ -765,6 +795,16 @@ function dedupeNodes(nodes) {
765
795
  });
766
796
  }
767
797
 
798
+ function dedupeTargets(targets) {
799
+ const seen = new Set();
800
+ return targets.filter((target) => {
801
+ const key = `${target.kind}:${target.value}`;
802
+ if (seen.has(key)) return false;
803
+ seen.add(key);
804
+ return true;
805
+ });
806
+ }
807
+
768
808
  function dedupeBackendImports(entries) {
769
809
  const seen = new Set();
770
810
  return entries.filter((entry) => {
@@ -153,6 +153,18 @@ describe("coverage graph builder", () => {
153
153
  const productDir = createProduct();
154
154
  writeFile(productDir, "src/app/campaigns/page.tsx", `export default function CampaignsPage() { return null; }`);
155
155
  writeFile(productDir, "src/app/api/campaigns/route.ts", `export async function GET() { return Response.json({ ok: true }); }`);
156
+ writeFile(
157
+ productDir,
158
+ "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
159
+ `
160
+ import { expect, test } from "@playwright/test";
161
+
162
+ test("campaigns route", async ({ page }) => {
163
+ await page.goto("/campaigns");
164
+ await expect(page.getByTestId("campaign-save-button")).toBeVisible();
165
+ });
166
+ `
167
+ );
156
168
 
157
169
  const graph = buildCoverageGraph({
158
170
  productDir,
@@ -184,6 +196,15 @@ describe("coverage graph builder", () => {
184
196
  expect.objectContaining({
185
197
  testFilePath: "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
186
198
  coveredNodeIds: ["page_view:web:/campaigns"],
199
+ details: expect.objectContaining({
200
+ route: "/campaigns",
201
+ targets: [
202
+ expect.objectContaining({
203
+ kind: "testId",
204
+ value: "campaign-save-button",
205
+ }),
206
+ ],
207
+ }),
187
208
  }),
188
209
  expect.objectContaining({
189
210
  testFilePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -11,7 +11,7 @@
11
11
  "src/"
12
12
  ],
13
13
  "dependencies": {
14
- "@elench/testkit-protocol": "0.1.59"
14
+ "@elench/testkit-protocol": "0.1.60"
15
15
  },
16
16
  "private": false
17
17
  }
@@ -277,7 +277,7 @@ function buildGraphProjection(context, page, matchedServiceName) {
277
277
  label: pageNode.label,
278
278
  service: pageNode.service,
279
279
  route: pageNode.route || null,
280
- targets: pageNode.target ? [pageNode.target] : [],
280
+ targets: collectTargetsForEvidence(relevantEvidence, pageNode.target),
281
281
  supportingTests,
282
282
  viaNodes,
283
283
  confidence: supportingTests.some((entry) => entry.type === "pw") ? "high" : "medium",
@@ -298,7 +298,7 @@ function buildGraphProjection(context, page, matchedServiceName) {
298
298
  label: supporting?.label || pageNode.label,
299
299
  service: pageNode.service,
300
300
  route: pageNode.route || null,
301
- targets: pageNode.target ? [pageNode.target] : [],
301
+ targets: collectTargetsForEvidence([entry], pageNode.target),
302
302
  failedTests: supporting ? [{ ...supporting, error: failed?.error || supporting.error || null, status: "failed" }] : [],
303
303
  viaNodes: collectViaNodes([entry], reachableNodeIds, nodeById, pageNode.id),
304
304
  };
@@ -351,6 +351,23 @@ function collectViaNodes(evidenceEntries, reachableNodeIds, nodeById, pageNodeId
351
351
  return [...nodes.values()].sort((left, right) => left.id.localeCompare(right.id));
352
352
  }
353
353
 
354
+ function collectTargetsForEvidence(evidenceEntries, pageTarget) {
355
+ const targets = [];
356
+ if (pageTarget) targets.push(pageTarget);
357
+ for (const entry of evidenceEntries || []) {
358
+ for (const target of entry?.details?.targets || []) {
359
+ targets.push(target);
360
+ }
361
+ }
362
+ const seen = new Set();
363
+ return targets.filter((target) => {
364
+ const key = `${target.kind}:${target.value}`;
365
+ if (seen.has(key)) return false;
366
+ seen.add(key);
367
+ return true;
368
+ });
369
+ }
370
+
354
371
  function buildSupportingTestRef(evidence, discoveryByFile, runArtifact) {
355
372
  const discovery = discoveryByFile.get(evidence.testFilePath);
356
373
  const runFile = findRunFileResult(runArtifact, evidence.testFilePath);
@@ -90,7 +90,10 @@ const context = {
90
90
  framework: "playwright",
91
91
  testFilePath: "app/coverage/__testkit__/coverage.pw.testkit.ts",
92
92
  coveredNodeIds: ["page_view:web:/coverage"],
93
- details: { route: "/coverage" },
93
+ details: {
94
+ route: "/coverage",
95
+ targets: [{ kind: "testId", value: "coverage-refresh-button", confidence: "high" }],
96
+ },
94
97
  },
95
98
  {
96
99
  id: "evidence:web:app/api/coverage/__testkit__/coverage.int.testkit.ts",
@@ -157,6 +160,7 @@ describe("testkit bridge", () => {
157
160
  failures: [
158
161
  {
159
162
  label: "Coverage",
163
+ targets: [{ kind: "testId", value: "coverage-refresh-button", confidence: "high" }],
160
164
  failedTests: [
161
165
  {
162
166
  filePath: "app/coverage/__testkit__/coverage.pw.testkit.ts",
@@ -168,6 +172,7 @@ describe("testkit bridge", () => {
168
172
  coverage: [
169
173
  {
170
174
  label: "Coverage",
175
+ targets: [{ kind: "testId", value: "coverage-refresh-button", confidence: "high" }],
171
176
  supportingTests: [
172
177
  {
173
178
  filePath: "app/coverage/__testkit__/coverage.pw.testkit.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -82,6 +82,7 @@ export interface CoverageEvidence {
82
82
  details?: {
83
83
  requestPaths?: string[];
84
84
  route?: string;
85
+ targets?: BrowserTarget[];
85
86
  };
86
87
  }
87
88
 
@@ -208,10 +208,12 @@ function normalizeEvidenceDetails(value) {
208
208
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
209
209
  const requestPaths = normalizeStringArray(value.requestPaths);
210
210
  const route = normalizeOptionalString(value.route);
211
- if (requestPaths.length === 0 && !route) return null;
211
+ const targets = Array.isArray(value.targets) ? value.targets.map(normalizeBrowserTarget).filter(Boolean) : [];
212
+ if (requestPaths.length === 0 && !route && targets.length === 0) return null;
212
213
  return {
213
214
  ...(requestPaths.length > 0 ? { requestPaths } : {}),
214
215
  ...(route ? { route } : {}),
216
+ ...(targets.length > 0 ? { targets } : {}),
215
217
  };
216
218
  }
217
219
 
@@ -74,6 +74,13 @@ describe("testkit browser protocol", () => {
74
74
  details: {
75
75
  route: "/coverage",
76
76
  requestPaths: ["/api/coverage"],
77
+ targets: [
78
+ {
79
+ kind: "testId",
80
+ value: "coverage-refresh-button",
81
+ confidence: "high",
82
+ },
83
+ ],
77
84
  },
78
85
  })
79
86
  ).toEqual({
@@ -89,6 +96,13 @@ describe("testkit browser protocol", () => {
89
96
  details: {
90
97
  route: "/coverage",
91
98
  requestPaths: ["/api/coverage"],
99
+ targets: [
100
+ {
101
+ kind: "testId",
102
+ value: "coverage-refresh-button",
103
+ confidence: "high",
104
+ },
105
+ ],
92
106
  },
93
107
  });
94
108
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -59,8 +59,8 @@
59
59
  "vitest": "^3.2.4"
60
60
  },
61
61
  "dependencies": {
62
- "@elench/testkit-bridge": "0.1.59",
63
- "@elench/testkit-protocol": "0.1.59",
62
+ "@elench/testkit-bridge": "0.1.60",
63
+ "@elench/testkit-protocol": "0.1.60",
64
64
  "@babel/code-frame": "^7.29.0",
65
65
  "@oclif/core": "^4.10.6",
66
66
  "esbuild": "^0.25.11",