@elench/testkit 0.1.34 → 0.1.36

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.
@@ -10,11 +10,13 @@ describe("runner reporting", () => {
10
10
  skipped: false,
11
11
  suiteCount: 1,
12
12
  completedSuiteCount: 1,
13
+ skippedSuiteCount: 0,
13
14
  failedSuiteCount: 0,
14
15
  totalFileCount: 3,
15
16
  completedFileCount: 3,
16
17
  passedFileCount: 3,
17
18
  failedFileCount: 0,
19
+ skippedFileCount: 0,
18
20
  notRunFileCount: 0,
19
21
  durationMs: 1200,
20
22
  totalTaskDurationMs: 2400,
@@ -28,11 +30,13 @@ describe("runner reporting", () => {
28
30
  skipped: true,
29
31
  suiteCount: 0,
30
32
  completedSuiteCount: 0,
33
+ skippedSuiteCount: 0,
31
34
  failedSuiteCount: 0,
32
35
  totalFileCount: 0,
33
36
  completedFileCount: 0,
34
37
  passedFileCount: 0,
35
38
  failedFileCount: 0,
39
+ skippedFileCount: 0,
36
40
  notRunFileCount: 0,
37
41
  durationMs: 0,
38
42
  totalTaskDurationMs: 0,
@@ -49,10 +53,9 @@ describe("runner reporting", () => {
49
53
  finishedAt: 4000,
50
54
  requestedJobs: 2,
51
55
  workerCount: 1,
52
- suiteType: "all",
53
- suiteNames: [],
56
+ typeValues: ["all"],
57
+ suiteSelectors: [],
54
58
  fileNames: [],
55
- framework: "all",
56
59
  shard: null,
57
60
  serviceFilter: null,
58
61
  metadata: {
@@ -71,11 +74,25 @@ describe("runner reporting", () => {
71
74
  });
72
75
 
73
76
  expect(artifact.product.name).toBe("my-product");
74
- expect(artifact.summary.services.total).toBe(1);
77
+ expect(artifact.schemaVersion).toBe(3);
78
+ expect(artifact.summary.services).toEqual({
79
+ total: 1,
80
+ passed: 1,
81
+ failed: 0,
82
+ skipped: 0,
83
+ });
84
+ expect(artifact.summary.suites).toEqual({
85
+ total: 1,
86
+ completed: 1,
87
+ passed: 1,
88
+ failed: 0,
89
+ skipped: 0,
90
+ });
75
91
  expect(artifact.summary.files).toEqual({
76
92
  total: 3,
77
93
  passed: 3,
78
94
  failed: 0,
95
+ skipped: 0,
79
96
  notRun: 0,
80
97
  });
81
98
  expect(artifact.services[0].durationMs).toBe(1200);
@@ -93,20 +110,23 @@ describe("runner reporting", () => {
93
110
  suites: [
94
111
  {
95
112
  name: "health",
96
- type: "integration",
113
+ type: "int",
97
114
  framework: "k6",
98
115
  files: [
99
116
  { path: "tests/api/integration/a.int.testkit.ts", status: "passed" },
100
- { path: "tests/api/integration/b.int.testkit.ts", status: "failed" },
117
+ {
118
+ path: "tests/api/integration/b.int.testkit.ts",
119
+ status: "skipped",
120
+ reason: "Billing is stubbed",
121
+ },
101
122
  ],
102
123
  },
103
124
  ],
104
125
  },
105
126
  ],
106
- suiteType: "int",
107
- suiteNames: ["health"],
127
+ typeValues: ["int"],
128
+ suiteSelectors: [{ kind: "plain", name: "health", raw: "health" }],
108
129
  fileNames: ["tests/api/integration/b.int.testkit.ts"],
109
- framework: "default",
110
130
  shard: null,
111
131
  serviceFilter: "api",
112
132
  metadata: {
@@ -119,7 +139,7 @@ describe("runner reporting", () => {
119
139
  });
120
140
 
121
141
  expect(status).toEqual({
122
- schemaVersion: 1,
142
+ schemaVersion: 3,
123
143
  source: "testkit",
124
144
  notice: "Generated file. Do not edit manually.",
125
145
  product: {
@@ -131,10 +151,9 @@ describe("runner reporting", () => {
131
151
  },
132
152
  testkitVersion: "0.1.20",
133
153
  scope: {
134
- suiteType: "int",
135
- suiteNames: ["health"],
154
+ types: ["int"],
155
+ suiteSelectors: ["health"],
136
156
  fileNames: ["tests/api/integration/b.int.testkit.ts"],
137
- framework: "default",
138
157
  shard: null,
139
158
  serviceFilter: "api",
140
159
  isFullRun: false,
@@ -144,26 +163,29 @@ describe("runner reporting", () => {
144
163
  total: 1,
145
164
  passed: 0,
146
165
  failed: 1,
166
+ skipped: 0,
147
167
  },
148
168
  tests: {
149
169
  total: 2,
150
170
  passed: 1,
151
- failed: 1,
171
+ failed: 0,
172
+ skipped: 1,
152
173
  notRun: 0,
153
174
  },
154
175
  },
155
176
  tests: [
156
177
  {
157
178
  service: "api",
158
- type: "integration",
179
+ type: "int",
159
180
  path: "tests/api/integration/a.int.testkit.ts",
160
181
  status: "passed",
161
182
  },
162
183
  {
163
184
  service: "api",
164
- type: "integration",
185
+ type: "int",
165
186
  path: "tests/api/integration/b.int.testkit.ts",
166
- status: "failed",
187
+ status: "skipped",
188
+ reason: "Billing is stubbed",
167
189
  },
168
190
  ],
169
191
  });
@@ -173,10 +195,9 @@ describe("runner reporting", () => {
173
195
  const status = buildStatusArtifact({
174
196
  productDir: "/tmp/my-product",
175
197
  results: [],
176
- suiteType: "all",
177
- suiteNames: [],
198
+ typeValues: ["all"],
199
+ suiteSelectors: [],
178
200
  fileNames: [],
179
- framework: "all",
180
201
  shard: null,
181
202
  serviceFilter: null,
182
203
  metadata: {
@@ -26,14 +26,15 @@ export function buildServiceTrackers(servicePlans, startedAt) {
26
26
  key: `${suite.type}:${suite.name}`,
27
27
  name: suite.name,
28
28
  type: suite.type,
29
+ displayType: suite.displayType || suite.type,
29
30
  framework: suite.framework,
30
31
  orderIndex: suite.orderIndex,
31
- fileCount: suite.files.length,
32
+ fileCount: suite.totalFileCount ?? suite.files.length,
32
33
  completedFileCount: 0,
33
34
  failedFiles: [],
34
35
  failedFileSet: new Set(),
35
- fileResultsByPath: new Map(
36
- suite.files.map((file) => {
36
+ fileResultsByPath: new Map([
37
+ ...suite.files.map((file) => {
37
38
  const normalizedPath = normalizePathSeparators(file);
38
39
  return [
39
40
  normalizedPath,
@@ -42,11 +43,23 @@ export function buildServiceTrackers(servicePlans, startedAt) {
42
43
  failed: false,
43
44
  durationMs: 0,
44
45
  error: null,
46
+ reason: null,
45
47
  status: "not_run",
46
48
  },
47
49
  ];
48
- })
49
- ),
50
+ }),
51
+ ...(suite.skippedFiles || []).map((file) => [
52
+ file.path,
53
+ {
54
+ path: file.path,
55
+ failed: false,
56
+ durationMs: 0,
57
+ error: null,
58
+ reason: file.reason,
59
+ status: "skipped",
60
+ },
61
+ ]),
62
+ ]),
50
63
  durationMs: 0,
51
64
  error: null,
52
65
  }));
@@ -92,25 +105,30 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
92
105
  : Math.max(tracker.lastTaskAt, outcomeFinishedAt);
93
106
  tracker.totalTaskDurationMs += outcomeDurationMs;
94
107
 
95
- suite.completedFileCount += 1;
96
108
  suite.durationMs += outcomeDurationMs;
97
109
  const normalizedPath = normalizePathSeparators(task.file);
98
110
  const existingFileResult = suite.fileResultsByPath.get(normalizedPath);
111
+ const status = normalizeOutcomeStatus(outcome);
112
+ if (status !== "skipped") {
113
+ suite.completedFileCount += 1;
114
+ }
99
115
  if (existingFileResult) {
100
- existingFileResult.failed = outcome.failed;
116
+ existingFileResult.failed = status === "failed";
101
117
  existingFileResult.durationMs = outcomeDurationMs;
102
118
  existingFileResult.error = outcome.error;
103
- existingFileResult.status = outcome.failed ? "failed" : "passed";
119
+ existingFileResult.reason = outcome.reason || null;
120
+ existingFileResult.status = status;
104
121
  } else {
105
122
  suite.fileResultsByPath.set(normalizedPath, {
106
123
  path: normalizedPath,
107
- failed: outcome.failed,
124
+ failed: status === "failed",
108
125
  durationMs: outcomeDurationMs,
109
126
  error: outcome.error,
110
- status: outcome.failed ? "failed" : "passed",
127
+ reason: outcome.reason || null,
128
+ status,
111
129
  });
112
130
  }
113
- if (outcome.failed && !suite.failedFileSet.has(task.file)) {
131
+ if (status === "failed" && !suite.failedFileSet.has(task.file)) {
114
132
  suite.failedFileSet.add(task.file);
115
133
  suite.failedFiles.push(task.file);
116
134
  }
@@ -144,7 +162,14 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
144
162
  skipped: true,
145
163
  suiteCount: 0,
146
164
  completedSuiteCount: 0,
165
+ skippedSuiteCount: 0,
147
166
  failedSuiteCount: 0,
167
+ totalFileCount: 0,
168
+ completedFileCount: 0,
169
+ passedFileCount: 0,
170
+ failedFileCount: 0,
171
+ skippedFileCount: 0,
172
+ notRunFileCount: 0,
148
173
  durationMs: 0,
149
174
  totalTaskDurationMs: 0,
150
175
  suites: [],
@@ -157,14 +182,18 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
157
182
  .map((suite) => finalizeSuite(suite));
158
183
 
159
184
  const completedSuiteCount = suites.filter(
160
- (suite) => suite.completedFileCount === suite.fileCount
185
+ (suite) => suite.completedFileCount + suite.skippedFileCount === suite.fileCount
186
+ ).length;
187
+ const skippedSuiteCount = suites.filter(
188
+ (suite) => suite.skippedFileCount === suite.fileCount && suite.fileCount > 0
161
189
  ).length;
162
190
  const failedSuiteCount = suites.filter((suite) => suite.failedFileCount > 0).length;
163
191
  const totalFileCount = suites.reduce((sum, suite) => sum + suite.fileCount, 0);
164
192
  const completedFileCount = suites.reduce((sum, suite) => sum + suite.completedFileCount, 0);
165
193
  const failedFileCount = suites.reduce((sum, suite) => sum + suite.failedFileCount, 0);
166
194
  const passedFileCount = suites.reduce((sum, suite) => sum + suite.passedFileCount, 0);
167
- const notRunFileCount = totalFileCount - completedFileCount;
195
+ const skippedFileCount = suites.reduce((sum, suite) => sum + suite.skippedFileCount, 0);
196
+ const notRunFileCount = totalFileCount - completedFileCount - skippedFileCount;
168
197
  const totalTaskDurationMs =
169
198
  tracker.totalTaskDurationMs || suites.reduce((sum, suite) => sum + suite.durationMs, 0);
170
199
  const durationMs =
@@ -182,11 +211,13 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
182
211
  skipped: false,
183
212
  suiteCount: tracker.suiteCount,
184
213
  completedSuiteCount,
214
+ skippedSuiteCount,
185
215
  failedSuiteCount,
186
216
  totalFileCount,
187
217
  completedFileCount,
188
218
  passedFileCount,
189
219
  failedFileCount,
220
+ skippedFileCount,
190
221
  notRunFileCount,
191
222
  durationMs,
192
223
  totalTaskDurationMs,
@@ -211,18 +242,23 @@ function finalizeSuite(suite) {
211
242
  status: file.status,
212
243
  durationMs: file.durationMs,
213
244
  error: file.error,
245
+ reason: file.reason,
214
246
  }));
215
247
 
216
248
  return {
217
249
  name: suite.name,
218
- type: suite.type,
250
+ type: suite.displayType,
219
251
  framework: formatFrameworkForArtifact(suite.framework),
220
252
  failed: suite.failedFiles.length > 0,
221
253
  fileCount: suite.fileCount,
222
254
  completedFileCount: suite.completedFileCount,
223
255
  passedFileCount: files.filter((file) => file.status === "passed").length,
224
256
  failedFileCount: suite.failedFiles.length,
225
- notRunFileCount: suite.fileCount - suite.completedFileCount,
257
+ skippedFileCount: files.filter((file) => file.status === "skipped").length,
258
+ notRunFileCount:
259
+ suite.fileCount -
260
+ suite.completedFileCount -
261
+ files.filter((file) => file.status === "skipped").length,
226
262
  failedFiles: suite.failedFiles,
227
263
  durationMs: suite.durationMs,
228
264
  error: suite.error,
@@ -238,3 +274,8 @@ function formatFrameworkForArtifact(framework) {
238
274
  if (framework === "k6") return "default";
239
275
  return framework;
240
276
  }
277
+
278
+ function normalizeOutcomeStatus(outcome) {
279
+ if (outcome?.status === "skipped") return "skipped";
280
+ return outcome?.failed ? "failed" : "passed";
281
+ }
@@ -72,6 +72,7 @@ describe("runner results", () => {
72
72
  status: "failed",
73
73
  durationMs: 250,
74
74
  error: "boom",
75
+ reason: null,
75
76
  },
76
77
  ]);
77
78
  });
@@ -193,6 +194,119 @@ describe("runner results", () => {
193
194
  expect(result.totalTaskDurationMs).toBe(3_000);
194
195
  });
195
196
 
197
+ it("counts config-skipped files and runtime-skipped files separately from passed work", () => {
198
+ const trackers = buildServiceTrackers(
199
+ [
200
+ {
201
+ skipped: false,
202
+ config: {
203
+ name: "api",
204
+ testkit: {
205
+ database: {
206
+ selectedBackend: null,
207
+ },
208
+ },
209
+ },
210
+ suites: [
211
+ {
212
+ name: "billing",
213
+ type: "integration",
214
+ framework: "k6",
215
+ files: ["tests/billing-live.int.testkit.ts"],
216
+ skippedFiles: [
217
+ {
218
+ path: "tests/billing-stubbed.int.testkit.ts",
219
+ reason: "Billing is stubbed",
220
+ },
221
+ ],
222
+ totalFileCount: 2,
223
+ orderIndex: 0,
224
+ },
225
+ {
226
+ name: "frontend-smoke",
227
+ type: "e2e",
228
+ framework: "playwright",
229
+ files: ["tests/frontend-smoke.pw.testkit.ts"],
230
+ orderIndex: 1,
231
+ },
232
+ ],
233
+ },
234
+ ],
235
+ 1000
236
+ );
237
+
238
+ recordTaskOutcome(
239
+ trackers,
240
+ {
241
+ serviceName: "api",
242
+ suiteKey: "integration:billing",
243
+ file: "tests/billing-live.int.testkit.ts",
244
+ },
245
+ {
246
+ failed: false,
247
+ durationMs: 100,
248
+ error: null,
249
+ },
250
+ 1100
251
+ );
252
+ recordTaskOutcome(
253
+ trackers,
254
+ {
255
+ serviceName: "api",
256
+ suiteKey: "e2e:frontend-smoke",
257
+ file: "tests/frontend-smoke.pw.testkit.ts",
258
+ },
259
+ {
260
+ failed: false,
261
+ status: "skipped",
262
+ reason: "Playwright test.skip()",
263
+ durationMs: 50,
264
+ error: null,
265
+ },
266
+ 1150
267
+ );
268
+
269
+ const result = finalizeServiceResult(trackers.get("api"), 1000, 1200);
270
+
271
+ expect(result.failed).toBe(false);
272
+ expect(result.completedSuiteCount).toBe(2);
273
+ expect(result.skippedSuiteCount).toBe(1);
274
+ expect(result.totalFileCount).toBe(3);
275
+ expect(result.completedFileCount).toBe(1);
276
+ expect(result.passedFileCount).toBe(1);
277
+ expect(result.failedFileCount).toBe(0);
278
+ expect(result.skippedFileCount).toBe(2);
279
+ expect(result.notRunFileCount).toBe(0);
280
+ expect(result.suites[0].files).toEqual([
281
+ {
282
+ path: "tests/billing-live.int.testkit.ts",
283
+ failed: false,
284
+ status: "passed",
285
+ durationMs: 100,
286
+ error: null,
287
+ reason: null,
288
+ },
289
+ {
290
+ path: "tests/billing-stubbed.int.testkit.ts",
291
+ failed: false,
292
+ status: "skipped",
293
+ durationMs: 0,
294
+ error: null,
295
+ reason: "Billing is stubbed",
296
+ },
297
+ ]);
298
+ expect(result.suites[1].files).toEqual([
299
+ {
300
+ path: "tests/frontend-smoke.pw.testkit.ts",
301
+ failed: false,
302
+ status: "skipped",
303
+ durationMs: 50,
304
+ error: null,
305
+ reason: "Playwright test.skip()",
306
+ },
307
+ ]);
308
+ });
309
+
196
310
  it("summarizes mixed db backends", () => {
197
311
  expect(
198
312
  summarizeDbBackend([{ dbBackend: "local" }, { dbBackend: "neon" }])
@@ -1,15 +1,16 @@
1
1
  export function findUnmatchedRequestedFiles(
2
2
  configs,
3
- suiteType,
4
- suiteNames,
5
- framework,
3
+ typeValues,
4
+ suiteSelectors,
6
5
  fileNames,
7
6
  collectSuites,
8
7
  normalizePathSeparators
9
8
  ) {
10
9
  const matchedFiles = new Set();
11
10
  for (const config of configs) {
12
- const suites = collectSuites(config, suiteType, suiteNames, framework, []);
11
+ const suites = collectSuites(config, typeValues, suiteSelectors, [], {
12
+ ignoreSkipRules: true,
13
+ });
13
14
  for (const suite of suites) {
14
15
  for (const file of suite.files) {
15
16
  matchedFiles.add(normalizePathSeparators(file));
@@ -22,11 +23,12 @@ export function findUnmatchedRequestedFiles(
22
23
  );
23
24
  }
24
25
 
25
- export function isFullRunSelection(suiteNames, fileNames, framework, shard, serviceFilter) {
26
+ export function isFullRunSelection(typeValues, suiteSelectors, fileNames, shard, serviceFilter) {
26
27
  return (
27
- (suiteNames || []).length === 0 &&
28
+ (typeValues || []).length === 1 &&
29
+ typeValues[0] === "all" &&
30
+ (suiteSelectors || []).length === 0 &&
28
31
  (fileNames || []).length === 0 &&
29
- (framework || "all") === "all" &&
30
32
  (shard || null) === null &&
31
33
  (serviceFilter || null) === null
32
34
  );
@@ -5,9 +5,8 @@ describe("runner selection", () => {
5
5
  it("finds unmatched requested files", () => {
6
6
  const unmatched = findUnmatchedRequestedFiles(
7
7
  [{ name: "api" }],
8
- "int",
8
+ ["int"],
9
9
  [],
10
- "all",
11
10
  ["tests/a.int.testkit.ts", "tests/missing.int.testkit.ts"],
12
11
  () => [{ files: ["tests/a.int.testkit.ts"] }],
13
12
  (value) => value
@@ -17,9 +16,9 @@ describe("runner selection", () => {
17
16
  });
18
17
 
19
18
  it("detects a full run selection", () => {
20
- expect(isFullRunSelection([], [], "all", null, null)).toBe(true);
21
- expect(isFullRunSelection(["auth"], [], "all", null, null)).toBe(false);
22
- expect(isFullRunSelection([], ["a"], "all", null, null)).toBe(false);
23
- expect(isFullRunSelection([], [], "playwright", null, null)).toBe(false);
19
+ expect(isFullRunSelection(["all"], [], [], null, null)).toBe(true);
20
+ expect(isFullRunSelection(["all"], [{ raw: "auth" }], [], null, null)).toBe(false);
21
+ expect(isFullRunSelection(["all"], [], ["a"], null, null)).toBe(false);
22
+ expect(isFullRunSelection(["int"], [], [], null, null)).toBe(false);
24
23
  });
25
24
  });
@@ -0,0 +1,91 @@
1
+ const USER_TYPES = new Set(["int", "e2e", "dal", "load", "pw", "all"]);
2
+
3
+ export function normalizeTypeValues(values = []) {
4
+ const expanded = [];
5
+ for (const rawValue of values) {
6
+ if (rawValue == null) continue;
7
+ for (const part of String(rawValue).split(",")) {
8
+ const value = part.trim();
9
+ if (!value) continue;
10
+ if (!USER_TYPES.has(value)) {
11
+ throw new Error(
12
+ `Unknown type "${value}". Expected one of: int, e2e, dal, load, pw, all.`
13
+ );
14
+ }
15
+ expanded.push(value);
16
+ }
17
+ }
18
+
19
+ if (expanded.length === 0) {
20
+ return ["all"];
21
+ }
22
+
23
+ const deduped = [...new Set(expanded)];
24
+ if (deduped.includes("all") && deduped.length > 1) {
25
+ throw new Error(`"--type all" cannot be combined with other types.`);
26
+ }
27
+
28
+ const order = ["int", "e2e", "dal", "load", "pw", "all"];
29
+ return deduped.sort((left, right) => order.indexOf(left) - order.indexOf(right));
30
+ }
31
+
32
+ export function isAllTypeSelection(typeValues = []) {
33
+ return typeValues.length === 0 || (typeValues.length === 1 && typeValues[0] === "all");
34
+ }
35
+
36
+ export function parseSuiteSelectors(values = []) {
37
+ const selectors = [];
38
+
39
+ for (const rawValue of values) {
40
+ if (rawValue == null) continue;
41
+ for (const part of String(rawValue).split(",")) {
42
+ const value = part.trim();
43
+ if (!value) continue;
44
+
45
+ const typeMatch = value.match(/^([a-z]+):(.*)$/);
46
+ if (!typeMatch) {
47
+ selectors.push({ kind: "plain", name: value, raw: value });
48
+ continue;
49
+ }
50
+
51
+ const type = typeMatch[1];
52
+ const name = typeMatch[2].trim();
53
+ if (!USER_TYPES.has(type) || type === "all") {
54
+ throw new Error(
55
+ `Unknown suite selector type "${type}". Expected one of: int, e2e, dal, load, pw.`
56
+ );
57
+ }
58
+ if (!name) {
59
+ throw new Error(`Invalid suite selector "${value}". Expected "<type>:<suite-name>".`);
60
+ }
61
+
62
+ selectors.push({
63
+ kind: "typed",
64
+ type,
65
+ name,
66
+ raw: `${type}:${name}`,
67
+ });
68
+ }
69
+ }
70
+
71
+ return selectors;
72
+ }
73
+
74
+ export function suiteSelectionType(type, framework) {
75
+ if ((framework || "k6") === "playwright") return "pw";
76
+ if (type === "integration") return "int";
77
+ return type;
78
+ }
79
+
80
+ export function matchesSelectedTypes(selectionType, typeValues) {
81
+ return isAllTypeSelection(typeValues) || typeValues.includes(selectionType);
82
+ }
83
+
84
+ export function matchesSuiteSelectors(selectionType, suiteName, selectors = []) {
85
+ if (!selectors || selectors.length === 0) return true;
86
+
87
+ return selectors.some((selector) => {
88
+ if (selector.kind === "plain") return selector.name === suiteName;
89
+ return selector.type === selectionType && selector.name === suiteName;
90
+ });
91
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ isAllTypeSelection,
4
+ matchesSelectedTypes,
5
+ matchesSuiteSelectors,
6
+ normalizeTypeValues,
7
+ parseSuiteSelectors,
8
+ suiteSelectionType,
9
+ } from "./suite-selection.mjs";
10
+
11
+ describe("runner suite selection", () => {
12
+ it("normalizes selected type values", () => {
13
+ expect(normalizeTypeValues([])).toEqual(["all"]);
14
+ expect(normalizeTypeValues(["int,e2e", "dal"])).toEqual(["int", "e2e", "dal"]);
15
+ expect(() => normalizeTypeValues(["all", "int"])).toThrow("cannot be combined");
16
+ expect(() => normalizeTypeValues(["jest"])).toThrow("Unknown type");
17
+ });
18
+
19
+ it("parses suite selectors", () => {
20
+ expect(parseSuiteSelectors(["auth,dal:queries"])).toEqual([
21
+ { kind: "plain", name: "auth", raw: "auth" },
22
+ { kind: "typed", type: "dal", name: "queries", raw: "dal:queries" },
23
+ ]);
24
+ expect(() => parseSuiteSelectors(["all:auth"])).toThrow("Unknown suite selector type");
25
+ expect(() => parseSuiteSelectors(["int:"])).toThrow("Invalid suite selector");
26
+ });
27
+
28
+ it("matches suite types and selectors", () => {
29
+ expect(isAllTypeSelection(["all"])).toBe(true);
30
+ expect(matchesSelectedTypes("int", ["int", "dal"])).toBe(true);
31
+ expect(matchesSelectedTypes("pw", ["int", "dal"])).toBe(false);
32
+ expect(matchesSuiteSelectors("int", "auth", parseSuiteSelectors(["auth"]))).toBe(true);
33
+ expect(matchesSuiteSelectors("e2e", "auth", parseSuiteSelectors(["int:auth"]))).toBe(false);
34
+ expect(matchesSuiteSelectors("int", "auth", parseSuiteSelectors(["int:auth"]))).toBe(true);
35
+ });
36
+
37
+ it("maps discovered suites to user-facing selection types", () => {
38
+ expect(suiteSelectionType("integration", "k6")).toBe("int");
39
+ expect(suiteSelectionType("e2e", "playwright")).toBe("pw");
40
+ expect(suiteSelectionType("dal", "k6")).toBe("dal");
41
+ });
42
+ });
@@ -18,6 +18,21 @@ export interface LifecycleConfig {
18
18
  testkitCwd?: string;
19
19
  }
20
20
 
21
+ export interface SkipFileRule {
22
+ path: string;
23
+ reason: string;
24
+ }
25
+
26
+ export interface SkipSuiteRule {
27
+ selector: string;
28
+ reason: string;
29
+ }
30
+
31
+ export interface SkipConfig {
32
+ files?: SkipFileRule[];
33
+ suites?: SkipSuiteRule[];
34
+ }
35
+
21
36
  export interface ServiceConfig {
22
37
  database?: LocalDatabaseConfig;
23
38
  databaseFrom?: string;
@@ -38,6 +53,7 @@ export interface ServiceConfig {
38
53
  };
39
54
  migrate?: LifecycleConfig;
40
55
  seed?: LifecycleConfig;
56
+ skip?: SkipConfig;
41
57
  }
42
58
 
43
59
  export interface TestkitSetup {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "types": "./lib/index.d.ts",