@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,241 @@
|
|
|
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
|
+
it("does not exclude app/coverage source routes while still allowing top-level coverage output to be ignored", () => {
|
|
210
|
+
const productDir = createProduct();
|
|
211
|
+
writeFile(productDir, "app/coverage/page.tsx", `export default function CoveragePage() { return null; }`);
|
|
212
|
+
writeFile(productDir, "coverage/page.tsx", `export default function OutputPage() { return null; }`);
|
|
213
|
+
|
|
214
|
+
const context = buildServiceCoverageContext(
|
|
215
|
+
productDir,
|
|
216
|
+
"web",
|
|
217
|
+
{
|
|
218
|
+
local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
|
|
219
|
+
discovery: { roots: ["app"] },
|
|
220
|
+
},
|
|
221
|
+
{ exclude: ["coverage"] }
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(context.pageByRoute.get("/coverage").node).toMatchObject({
|
|
225
|
+
filePath: "app/coverage/page.tsx",
|
|
226
|
+
});
|
|
227
|
+
expect(context.graph.nodes.some((node) => node.filePath === "coverage/page.tsx")).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
function createProduct() {
|
|
232
|
+
const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-coverage-"));
|
|
233
|
+
cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
|
|
234
|
+
return productDir;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function writeFile(productDir, relativePath, content = "export {};\n") {
|
|
238
|
+
const absolutePath = path.join(productDir, relativePath);
|
|
239
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
240
|
+
fs.writeFileSync(absolutePath, content);
|
|
241
|
+
}
|
package/lib/discovery/index.d.ts
CHANGED
|
@@ -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;
|
package/lib/discovery/index.mjs
CHANGED
|
@@ -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 =
|
|
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,11 +47,12 @@ 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 || {}, {
|
|
52
54
|
strict: filters.diagnosticsMode === "error",
|
|
55
|
+
discovery: setupContext.setup.discovery || {},
|
|
53
56
|
});
|
|
54
57
|
baseResult.diagnostics.push(...rawDiscovery.diagnostics);
|
|
55
58
|
validateRequestedService(filters.serviceFilter, setupContext.setup.services || {}, rawDiscovery);
|
|
@@ -86,6 +89,12 @@ export async function discoverTests(options = {}) {
|
|
|
86
89
|
services: resolved.services,
|
|
87
90
|
suites: resolved.suites,
|
|
88
91
|
files: resolved.files,
|
|
92
|
+
coverageGraph: buildCoverageGraph({
|
|
93
|
+
productDir,
|
|
94
|
+
repoDiscovery: setupContext.setup.discovery || {},
|
|
95
|
+
services: setupContext.setup.services || {},
|
|
96
|
+
discoveryFiles: rawDiscovery.files || [],
|
|
97
|
+
}),
|
|
89
98
|
},
|
|
90
99
|
productDir
|
|
91
100
|
);
|
|
@@ -102,6 +111,12 @@ export async function discoverTests(options = {}) {
|
|
|
102
111
|
services: rawOnly.services,
|
|
103
112
|
suites: rawOnly.suites,
|
|
104
113
|
files: rawOnly.files,
|
|
114
|
+
coverageGraph: buildCoverageGraph({
|
|
115
|
+
productDir,
|
|
116
|
+
repoDiscovery: setupContext.setup.discovery || {},
|
|
117
|
+
services: setupContext.setup.services || {},
|
|
118
|
+
discoveryFiles: rawDiscovery.files || [],
|
|
119
|
+
}),
|
|
105
120
|
},
|
|
106
121
|
productDir
|
|
107
122
|
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_DISCOVERY_EXCLUDES = Object.freeze([
|
|
5
|
+
".cache",
|
|
6
|
+
".git",
|
|
7
|
+
".hg",
|
|
8
|
+
".next",
|
|
9
|
+
".nuxt",
|
|
10
|
+
".playwright-browsers",
|
|
11
|
+
".svn",
|
|
12
|
+
".testkit",
|
|
13
|
+
".turbo",
|
|
14
|
+
"build",
|
|
15
|
+
"coverage",
|
|
16
|
+
"dist",
|
|
17
|
+
"node_modules",
|
|
18
|
+
"playwright-report",
|
|
19
|
+
"test-results",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function normalizeDiscoveryConfig(value, { allowRoots = true } = {}) {
|
|
23
|
+
if (value == null) return {};
|
|
24
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
25
|
+
throw new Error("testkit discovery config must be an object");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const normalized = {};
|
|
29
|
+
if (allowRoots && Object.prototype.hasOwnProperty.call(value, "roots")) {
|
|
30
|
+
normalized.roots = normalizeDiscoveryPathList(value.roots, "testkit discovery roots");
|
|
31
|
+
}
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(value, "exclude")) {
|
|
33
|
+
normalized.exclude = normalizeDiscoveryPathList(value.exclude, "testkit discovery exclude");
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function mergeDiscoveryConfigs(...configs) {
|
|
39
|
+
const merged = {};
|
|
40
|
+
for (const config of configs.filter(Boolean)) {
|
|
41
|
+
if (Array.isArray(config.roots)) {
|
|
42
|
+
merged.roots = [...config.roots];
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(config.exclude) && config.exclude.length > 0) {
|
|
45
|
+
merged.exclude = [...new Set([...(merged.exclude || []), ...config.exclude])];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveDiscoveryRoots(baseDir, roots, defaultRoot = ".") {
|
|
52
|
+
const normalizedRoots = Array.isArray(roots) && roots.length > 0 ? roots : [defaultRoot];
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const resolved = [];
|
|
55
|
+
|
|
56
|
+
for (const root of normalizedRoots) {
|
|
57
|
+
const normalizedRoot = normalizeDiscoveryPath(root);
|
|
58
|
+
if (!normalizedRoot) continue;
|
|
59
|
+
const absolutePath = path.resolve(baseDir, normalizedRoot === "." ? "." : normalizedRoot);
|
|
60
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) continue;
|
|
61
|
+
if (seen.has(absolutePath)) continue;
|
|
62
|
+
seen.add(absolutePath);
|
|
63
|
+
resolved.push(absolutePath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return resolved.sort((left, right) => left.localeCompare(right));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function shouldExcludeDiscoveryPath(relativePath, exclude = []) {
|
|
70
|
+
const normalizedPath = normalizeDiscoveryPath(relativePath);
|
|
71
|
+
if (!normalizedPath || normalizedPath === ".") return false;
|
|
72
|
+
return exclude.some((pattern) => matchesDiscoveryPathPattern(normalizedPath, pattern));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function normalizeDiscoveryPath(value) {
|
|
76
|
+
const normalized = String(value || "")
|
|
77
|
+
.trim()
|
|
78
|
+
.split(path.sep)
|
|
79
|
+
.join("/")
|
|
80
|
+
.replace(/^\.\/+/u, "")
|
|
81
|
+
.replace(/\/+/gu, "/")
|
|
82
|
+
.replace(/\/+$/u, "");
|
|
83
|
+
return normalized || ".";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeDiscoveryPathList(value, label) {
|
|
87
|
+
if (!Array.isArray(value)) {
|
|
88
|
+
throw new Error(`${label} must be an array of relative paths`);
|
|
89
|
+
}
|
|
90
|
+
const normalized = value
|
|
91
|
+
.map((entry) => {
|
|
92
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
93
|
+
throw new Error(`${label} entries must be non-empty strings`);
|
|
94
|
+
}
|
|
95
|
+
if (path.isAbsolute(entry)) {
|
|
96
|
+
throw new Error(`${label} entries must be relative paths`);
|
|
97
|
+
}
|
|
98
|
+
return normalizeDiscoveryPath(entry);
|
|
99
|
+
})
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
return [...new Set(normalized)];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function matchesDiscoveryPathPattern(relativePath, pattern) {
|
|
105
|
+
return relativePath === pattern || relativePath.startsWith(`${pattern}/`);
|
|
106
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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 {
|
|
6
|
+
mergeDiscoveryConfigs,
|
|
7
|
+
normalizeDiscoveryConfig,
|
|
8
|
+
resolveDiscoveryRoots,
|
|
9
|
+
shouldExcludeDiscoveryPath,
|
|
10
|
+
} from "./path-policy.mjs";
|
|
11
|
+
|
|
12
|
+
const cleanups = [];
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
while (cleanups.length > 0) {
|
|
16
|
+
cleanups.pop()();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("discovery path policy", () => {
|
|
21
|
+
it("normalizes roots and excludes as relative paths", () => {
|
|
22
|
+
expect(
|
|
23
|
+
normalizeDiscoveryConfig({
|
|
24
|
+
roots: ["./app/", "src/app"],
|
|
25
|
+
exclude: ["./coverage/", "dist"],
|
|
26
|
+
})
|
|
27
|
+
).toEqual({
|
|
28
|
+
roots: ["app", "src/app"],
|
|
29
|
+
exclude: ["coverage", "dist"],
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("merges excludes without losing later roots", () => {
|
|
34
|
+
expect(
|
|
35
|
+
mergeDiscoveryConfigs(
|
|
36
|
+
{ roots: ["src"], exclude: ["coverage"] },
|
|
37
|
+
{ exclude: ["dist"] },
|
|
38
|
+
{ roots: ["app"], exclude: ["test-results"] }
|
|
39
|
+
)
|
|
40
|
+
).toEqual({
|
|
41
|
+
roots: ["app"],
|
|
42
|
+
exclude: ["coverage", "dist", "test-results"],
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("treats exclude paths as path prefixes rather than bare directory names", () => {
|
|
47
|
+
expect(shouldExcludeDiscoveryPath("coverage", ["coverage"])).toBe(true);
|
|
48
|
+
expect(shouldExcludeDiscoveryPath("coverage/report/index.html", ["coverage"])).toBe(true);
|
|
49
|
+
expect(shouldExcludeDiscoveryPath("app/coverage", ["coverage"])).toBe(false);
|
|
50
|
+
expect(shouldExcludeDiscoveryPath("src/app/coverage", ["coverage"])).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("resolves only existing discovery roots", () => {
|
|
54
|
+
const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-discovery-roots-"));
|
|
55
|
+
cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
|
|
56
|
+
fs.mkdirSync(path.join(productDir, "app"), { recursive: true });
|
|
57
|
+
fs.mkdirSync(path.join(productDir, "src", "app"), { recursive: true });
|
|
58
|
+
|
|
59
|
+
const roots = resolveDiscoveryRoots(productDir, ["app", "missing", "src/app"]);
|
|
60
|
+
expect(roots).toEqual([
|
|
61
|
+
path.join(productDir, "app"),
|
|
62
|
+
path.join(productDir, "src", "app"),
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/lib/setup/index.d.ts
CHANGED
|
@@ -106,22 +106,30 @@ 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";
|
|
112
116
|
cacheTtlSeconds?: number;
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
export interface DiscoveryConfig {
|
|
120
|
+
roots?: string[];
|
|
121
|
+
exclude?: string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
export interface ServiceConfig {
|
|
116
125
|
database?: LocalDatabaseConfig;
|
|
117
126
|
databaseFrom?: string;
|
|
118
127
|
dependsOn?: string[];
|
|
119
|
-
discovery?:
|
|
120
|
-
roots?: string[];
|
|
121
|
-
};
|
|
128
|
+
discovery?: DiscoveryConfig;
|
|
122
129
|
env?: Record<string, string>;
|
|
123
130
|
envFile?: string;
|
|
124
131
|
envFiles?: string[];
|
|
132
|
+
browser?: BrowserServiceConfig;
|
|
125
133
|
local?: false | {
|
|
126
134
|
baseUrl: string;
|
|
127
135
|
cwd?: string;
|
|
@@ -137,6 +145,7 @@ export interface ServiceConfig {
|
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
export interface TestkitSetup {
|
|
148
|
+
discovery?: DiscoveryConfig;
|
|
140
149
|
execution?: TestkitExecutionConfig;
|
|
141
150
|
profiles?: {
|
|
142
151
|
http?: Record<string, HttpSuiteConfig>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elench/testkit-bridge",
|
|
3
|
+
"version": "0.1.59",
|
|
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.59"
|
|
15
|
+
},
|
|
16
|
+
"private": false
|
|
17
|
+
}
|