@elench/testkit 0.1.25 → 0.1.26
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/README.md +3 -0
- package/lib/config/model.mjs +0 -88
- package/lib/config/model.test.mjs +0 -31
- package/lib/index.d.ts +92 -0
- package/lib/package.test.mjs +24 -0
- package/lib/runner/index.mjs +3 -0
- package/lib/runner/results.mjs +49 -3
- package/lib/runner/results.test.mjs +24 -1
- package/lib/runtime/index.d.ts +183 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -83,6 +83,9 @@ import { check, group, http } from "@elench/testkit/runtime";
|
|
|
83
83
|
|
|
84
84
|
`testkit` bundles these imports before execution, so tests do not need
|
|
85
85
|
generated `_testkit` files, direct package-manager path imports, or any separate engine installation.
|
|
86
|
+
The published package also ships first-party TypeScript declarations for both
|
|
87
|
+
`@elench/testkit` and `@elench/testkit/runtime`, so consumer repos do not need
|
|
88
|
+
local ambient module shims for the supported authoring surface.
|
|
86
89
|
|
|
87
90
|
Legacy compatibility:
|
|
88
91
|
|
package/lib/config/model.mjs
CHANGED
|
@@ -21,94 +21,6 @@ export function parseDotenvString(source) {
|
|
|
21
21
|
return env;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function validateRunnerManifest(raw, manifestName = "runner.manifest.json", manifestPath = manifestName) {
|
|
25
|
-
if (!isObject(raw.services)) {
|
|
26
|
-
throw new Error(`${manifestName} must have a "services" object (${manifestPath})`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
for (const [serviceName, service] of Object.entries(raw.services)) {
|
|
30
|
-
if (!isObject(service)) {
|
|
31
|
-
throw new Error(`Service "${serviceName}" in ${manifestName} must be an object`);
|
|
32
|
-
}
|
|
33
|
-
if (!isObject(service.suites)) {
|
|
34
|
-
throw new Error(`Service "${serviceName}" in ${manifestName} must define suites`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const [suiteType, suites] of Object.entries(service.suites)) {
|
|
38
|
-
if (!Array.isArray(suites)) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`Service "${serviceName}" suite type "${suiteType}" must be an array`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const seenNames = new Set();
|
|
45
|
-
for (const suite of suites) {
|
|
46
|
-
if (!isObject(suite)) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
`Service "${serviceName}" suite type "${suiteType}" contains a non-object suite`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (typeof suite.name !== "string" || !suite.name.length) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Service "${serviceName}" suite type "${suiteType}" has a suite with no name`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
if (seenNames.has(suite.name)) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Service "${serviceName}" suite type "${suiteType}" has duplicate suite name "${suite.name}"`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
seenNames.add(suite.name);
|
|
62
|
-
|
|
63
|
-
if (!Array.isArray(suite.files) || suite.files.length === 0) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`Service "${serviceName}" suite "${suite.name}" must define one or more files`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
for (const file of suite.files) {
|
|
69
|
-
if (typeof file !== "string" || !file.length) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
`Service "${serviceName}" suite "${suite.name}" contains an invalid file entry`
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const framework = suite.framework || "k6";
|
|
77
|
-
if (!VALID_FRAMEWORKS.has(framework)) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`Service "${serviceName}" suite "${suite.name}" uses unsupported framework "${framework}"`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (suite.testkit !== undefined) {
|
|
84
|
-
if (!isObject(suite.testkit)) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`Service "${serviceName}" suite "${suite.name}" testkit config must be an object`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
if (
|
|
90
|
-
suite.testkit.maxFileConcurrency !== undefined &&
|
|
91
|
-
(!Number.isInteger(suite.testkit.maxFileConcurrency) ||
|
|
92
|
-
suite.testkit.maxFileConcurrency <= 0)
|
|
93
|
-
) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Service "${serviceName}" suite "${suite.name}" testkit.maxFileConcurrency must be a positive integer`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
if (
|
|
99
|
-
suite.testkit.weight !== undefined &&
|
|
100
|
-
(!Number.isInteger(suite.testkit.weight) || suite.testkit.weight <= 0)
|
|
101
|
-
) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
`Service "${serviceName}" suite "${suite.name}" testkit.weight must be a positive integer`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
24
|
export function validateConfigCoverage(
|
|
113
25
|
config,
|
|
114
26
|
configName = "testkit.config.json"
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
resolveSelectedDatabase,
|
|
9
9
|
validateConfigCoverage,
|
|
10
10
|
validateLifecycleConfig,
|
|
11
|
-
validateRunnerManifest,
|
|
12
11
|
validateServiceConfig,
|
|
13
12
|
validateTelemetryConfig,
|
|
14
13
|
} from "./model.mjs";
|
|
@@ -29,36 +28,6 @@ QUX='zap'
|
|
|
29
28
|
});
|
|
30
29
|
});
|
|
31
30
|
|
|
32
|
-
it("validates runner manifests", () => {
|
|
33
|
-
expect(() =>
|
|
34
|
-
validateRunnerManifest({
|
|
35
|
-
services: {
|
|
36
|
-
api: {
|
|
37
|
-
suites: {
|
|
38
|
-
integration: [
|
|
39
|
-
{ name: "health", files: ["tests/health.js"] },
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
})
|
|
45
|
-
).not.toThrow();
|
|
46
|
-
|
|
47
|
-
expect(() =>
|
|
48
|
-
validateRunnerManifest({
|
|
49
|
-
services: {
|
|
50
|
-
api: {
|
|
51
|
-
suites: {
|
|
52
|
-
integration: [
|
|
53
|
-
{ name: "health", files: ["a.js"], framework: "jest" },
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
})
|
|
59
|
-
).toThrow("unsupported framework");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
31
|
it("validates config coverage", () => {
|
|
63
32
|
const config = {
|
|
64
33
|
services: {
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpClient,
|
|
3
|
+
HttpClientConfig,
|
|
4
|
+
RuntimeDb,
|
|
5
|
+
RuntimeDalContext,
|
|
6
|
+
RuntimeEnv,
|
|
7
|
+
RuntimeHeaders,
|
|
8
|
+
RuntimeOptions,
|
|
9
|
+
RuntimeResponse,
|
|
10
|
+
} from "./runtime/index";
|
|
11
|
+
|
|
12
|
+
export interface TestkitSuite<TSetup = unknown> {
|
|
13
|
+
options: RuntimeOptions;
|
|
14
|
+
setup: () => TSetup | null;
|
|
15
|
+
exec: (setupData: TSetup | null) => unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface HeaderBuilderContext {
|
|
19
|
+
env: RuntimeEnv;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type HeaderBuilder<TSetup = unknown> = (
|
|
23
|
+
setupData?: TSetup | null,
|
|
24
|
+
context?: HeaderBuilderContext
|
|
25
|
+
) => RuntimeHeaders | void;
|
|
26
|
+
|
|
27
|
+
export interface AuthAdapter<TSetup = unknown> {
|
|
28
|
+
setup?: (context: { env: RuntimeEnv }) => TSetup;
|
|
29
|
+
headers?: HeaderBuilder<TSetup>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface HttpSuiteContext<TSetup = unknown> {
|
|
33
|
+
env: RuntimeEnv;
|
|
34
|
+
req: HttpClient<TSetup>["request"];
|
|
35
|
+
rawReq: HttpClient["raw"];
|
|
36
|
+
getWithHeaders: HttpClient<TSetup>["getWithHeaders"];
|
|
37
|
+
setupData: TSetup | null;
|
|
38
|
+
session: TSetup | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HttpSuiteConfig<TSetup = unknown> {
|
|
42
|
+
auth?: AuthAdapter<TSetup> | null;
|
|
43
|
+
env?: RuntimeEnv;
|
|
44
|
+
headers?: HeaderBuilder<TSetup>;
|
|
45
|
+
rawHeaders?: HeaderBuilder<never>;
|
|
46
|
+
options?: RuntimeOptions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DalSuiteContext<TSetup = unknown> {
|
|
50
|
+
db: RuntimeDb;
|
|
51
|
+
dal: RuntimeDalContext;
|
|
52
|
+
setupData: TSetup | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DalSuiteConfig<TSetup = unknown> {
|
|
56
|
+
db?: RuntimeDb;
|
|
57
|
+
options?: RuntimeOptions;
|
|
58
|
+
setup?: (context: { db: RuntimeDb; dal: RuntimeDalContext }) => TSetup;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export declare function defineHttpSuite<TSetup = unknown>(
|
|
62
|
+
run: (context: HttpSuiteContext<TSetup>) => unknown
|
|
63
|
+
): TestkitSuite<TSetup>;
|
|
64
|
+
|
|
65
|
+
export declare function defineHttpSuite<TSetup = unknown>(
|
|
66
|
+
config: HttpSuiteConfig<TSetup>,
|
|
67
|
+
run: (context: HttpSuiteContext<TSetup>) => unknown
|
|
68
|
+
): TestkitSuite<TSetup>;
|
|
69
|
+
|
|
70
|
+
export declare function defineDalSuite<TSetup = unknown>(
|
|
71
|
+
run: (context: DalSuiteContext<TSetup>) => unknown
|
|
72
|
+
): TestkitSuite<TSetup>;
|
|
73
|
+
|
|
74
|
+
export declare function defineDalSuite<TSetup = unknown>(
|
|
75
|
+
config: DalSuiteConfig<TSetup>,
|
|
76
|
+
run: (context: DalSuiteContext<TSetup>) => unknown
|
|
77
|
+
): TestkitSuite<TSetup>;
|
|
78
|
+
|
|
79
|
+
export declare function createAuthAdapter<TSetup = unknown>(
|
|
80
|
+
adapter?: AuthAdapter<TSetup>
|
|
81
|
+
): AuthAdapter<TSetup>;
|
|
82
|
+
|
|
83
|
+
export type {
|
|
84
|
+
HttpClient,
|
|
85
|
+
HttpClientConfig,
|
|
86
|
+
RuntimeDb,
|
|
87
|
+
RuntimeDalContext,
|
|
88
|
+
RuntimeEnv,
|
|
89
|
+
RuntimeHeaders,
|
|
90
|
+
RuntimeOptions,
|
|
91
|
+
RuntimeResponse,
|
|
92
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
8
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
9
|
+
|
|
10
|
+
describe("package metadata", () => {
|
|
11
|
+
it("ships first-party type declarations for the public exports", () => {
|
|
12
|
+
expect(packageJson.types).toBe("./lib/index.d.ts");
|
|
13
|
+
expect(packageJson.exports["."]).toEqual({
|
|
14
|
+
types: "./lib/index.d.ts",
|
|
15
|
+
default: "./lib/index.mjs",
|
|
16
|
+
});
|
|
17
|
+
expect(packageJson.exports["./runtime"]).toEqual({
|
|
18
|
+
types: "./lib/runtime/index.d.ts",
|
|
19
|
+
default: "./lib/runtime/index.mjs",
|
|
20
|
+
});
|
|
21
|
+
expect(fs.existsSync(path.join(rootDir, "lib", "index.d.ts"))).toBe(true);
|
|
22
|
+
expect(fs.existsSync(path.join(rootDir, "lib", "runtime", "index.d.ts"))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
});
|
package/lib/runner/index.mjs
CHANGED
|
@@ -832,12 +832,15 @@ function printRunSummary(results, durationMs) {
|
|
|
832
832
|
);
|
|
833
833
|
const failedSuites = executedServices.reduce((sum, result) => sum + result.failedSuiteCount, 0);
|
|
834
834
|
const passedSuites = completedSuites - failedSuites;
|
|
835
|
+
const totalFiles = executedServices.reduce((sum, result) => sum + (result.totalFileCount || 0), 0);
|
|
836
|
+
const passedFiles = executedServices.reduce((sum, result) => sum + (result.passedFileCount || 0), 0);
|
|
835
837
|
|
|
836
838
|
console.log("\n══ Summary ══");
|
|
837
839
|
console.log(
|
|
838
840
|
[
|
|
839
841
|
`services ${passedServices.length}/${executedServices.length} passed`,
|
|
840
842
|
`suites ${passedSuites}/${totalSuites} passed`,
|
|
843
|
+
totalFiles > 0 ? `files ${passedFiles}/${totalFiles} passed` : null,
|
|
841
844
|
skippedServices.length > 0 ? `${skippedServices.length} skipped` : null,
|
|
842
845
|
`duration ${formatDuration(durationMs)}`,
|
|
843
846
|
]
|
package/lib/runner/results.mjs
CHANGED
|
@@ -156,6 +156,19 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
156
156
|
(suite) => suite.completedFileCount === suite.fileCount
|
|
157
157
|
).length;
|
|
158
158
|
const failedSuiteCount = suites.filter((suite) => suite.failedFiles.length > 0).length;
|
|
159
|
+
const totalFileCount = suites.reduce((sum, suite) => sum + suite.fileCount, 0);
|
|
160
|
+
const completedFileCount = suites.reduce(
|
|
161
|
+
(sum, suite) => sum + suite.completedFileCount,
|
|
162
|
+
0
|
|
163
|
+
);
|
|
164
|
+
const failedFileCount = suites.reduce((sum, suite) => sum + suite.failedFiles.length, 0);
|
|
165
|
+
const passedFileCount = suites.reduce(
|
|
166
|
+
(sum, suite) =>
|
|
167
|
+
sum +
|
|
168
|
+
suite.fileResults.filter((file) => file.status === "passed").length,
|
|
169
|
+
0
|
|
170
|
+
);
|
|
171
|
+
const notRunFileCount = totalFileCount - completedFileCount;
|
|
159
172
|
const accumulatedDurationMs = suites.reduce((sum, suite) => sum + suite.durationMs, 0);
|
|
160
173
|
const durationMs =
|
|
161
174
|
tracker.firstTaskAt && tracker.lastTaskAt
|
|
@@ -173,6 +186,11 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
173
186
|
suiteCount: tracker.suiteCount,
|
|
174
187
|
completedSuiteCount,
|
|
175
188
|
failedSuiteCount,
|
|
189
|
+
totalFileCount,
|
|
190
|
+
completedFileCount,
|
|
191
|
+
passedFileCount,
|
|
192
|
+
failedFileCount,
|
|
193
|
+
notRunFileCount,
|
|
176
194
|
durationMs,
|
|
177
195
|
suites: suites.map((suite) => ({
|
|
178
196
|
name: suite.name,
|
|
@@ -180,6 +198,10 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
180
198
|
framework: formatFrameworkForArtifact(suite.framework),
|
|
181
199
|
failed: suite.failedFiles.length > 0,
|
|
182
200
|
fileCount: suite.fileCount,
|
|
201
|
+
completedFileCount: suite.completedFileCount,
|
|
202
|
+
passedFileCount: suite.fileResults.filter((file) => file.status === "passed").length,
|
|
203
|
+
failedFileCount: suite.failedFiles.length,
|
|
204
|
+
notRunFileCount: suite.fileCount - suite.completedFileCount,
|
|
183
205
|
failedFiles: suite.failedFiles,
|
|
184
206
|
durationMs: suite.durationMs,
|
|
185
207
|
error: suite.error,
|
|
@@ -299,6 +321,10 @@ export function buildRunArtifact({
|
|
|
299
321
|
const totalSuites = executed.reduce((sum, result) => sum + result.suiteCount, 0);
|
|
300
322
|
const completedSuites = executed.reduce((sum, result) => sum + result.completedSuiteCount, 0);
|
|
301
323
|
const failedSuites = executed.reduce((sum, result) => sum + result.failedSuiteCount, 0);
|
|
324
|
+
const totalFiles = executed.reduce((sum, result) => sum + (result.totalFileCount || 0), 0);
|
|
325
|
+
const passedFiles = executed.reduce((sum, result) => sum + (result.passedFileCount || 0), 0);
|
|
326
|
+
const failedFiles = executed.reduce((sum, result) => sum + (result.failedFileCount || 0), 0);
|
|
327
|
+
const notRunFiles = executed.reduce((sum, result) => sum + (result.notRunFileCount || 0), 0);
|
|
302
328
|
const dbBackend = summarizeDbBackend(results);
|
|
303
329
|
|
|
304
330
|
return {
|
|
@@ -339,6 +365,12 @@ export function buildRunArtifact({
|
|
|
339
365
|
passed: completedSuites - failedSuites,
|
|
340
366
|
failed: failedSuites,
|
|
341
367
|
},
|
|
368
|
+
files: {
|
|
369
|
+
total: totalFiles,
|
|
370
|
+
passed: passedFiles,
|
|
371
|
+
failed: failedFiles,
|
|
372
|
+
notRun: notRunFiles,
|
|
373
|
+
},
|
|
342
374
|
},
|
|
343
375
|
services: results.map((result) => ({
|
|
344
376
|
name: result.name,
|
|
@@ -347,6 +379,11 @@ export function buildRunArtifact({
|
|
|
347
379
|
suiteCount: result.suiteCount,
|
|
348
380
|
completedSuiteCount: result.completedSuiteCount,
|
|
349
381
|
failedSuiteCount: result.failedSuiteCount,
|
|
382
|
+
totalFileCount: result.totalFileCount,
|
|
383
|
+
completedFileCount: result.completedFileCount,
|
|
384
|
+
passedFileCount: result.passedFileCount,
|
|
385
|
+
failedFileCount: result.failedFileCount,
|
|
386
|
+
notRunFileCount: result.notRunFileCount,
|
|
350
387
|
durationMs: result.durationMs,
|
|
351
388
|
dbBackend: result.dbBackend,
|
|
352
389
|
suites: result.suites,
|
|
@@ -372,10 +409,15 @@ export function formatDuration(durationMs) {
|
|
|
372
409
|
|
|
373
410
|
export function formatServiceSummary(result) {
|
|
374
411
|
const passedSuites = result.completedSuiteCount - result.failedSuiteCount;
|
|
375
|
-
const
|
|
412
|
+
const notRunSuites = result.suiteCount - result.completedSuiteCount;
|
|
376
413
|
let detail = `${passedSuites}/${result.suiteCount} suites passed`;
|
|
377
|
-
if (
|
|
378
|
-
detail += `, ${
|
|
414
|
+
if ((result.totalFileCount || 0) > 0) {
|
|
415
|
+
detail += `, ${result.passedFileCount}/${result.totalFileCount} files passed`;
|
|
416
|
+
}
|
|
417
|
+
if (notRunSuites > 0) {
|
|
418
|
+
detail += `, ${notRunSuites} ${pluralize(notRunSuites, "suite", "suites")} not run`;
|
|
419
|
+
} else if ((result.notRunFileCount || 0) > 0) {
|
|
420
|
+
detail += `, ${result.notRunFileCount} ${pluralize(result.notRunFileCount, "file", "files")} not run`;
|
|
379
421
|
}
|
|
380
422
|
return detail;
|
|
381
423
|
}
|
|
@@ -404,3 +446,7 @@ function sanitizeErrorMessage(message) {
|
|
|
404
446
|
.replace(/Command failed with exit code (\d+): k6 run\b/g, "Default runtime failed with exit code $1:")
|
|
405
447
|
.replace(/[\\/]vendor[\\/]k6\b/g, "default-runtime");
|
|
406
448
|
}
|
|
449
|
+
|
|
450
|
+
function pluralize(value, singular, plural) {
|
|
451
|
+
return value === 1 ? singular : plural;
|
|
452
|
+
}
|
|
@@ -64,8 +64,12 @@ describe("runner-results", () => {
|
|
|
64
64
|
const result = finalizeServiceResult(tracker, 1000, 1500);
|
|
65
65
|
expect(result.failed).toBe(true);
|
|
66
66
|
expect(result.failedSuiteCount).toBe(1);
|
|
67
|
+
expect(result.totalFileCount).toBe(1);
|
|
68
|
+
expect(result.failedFileCount).toBe(1);
|
|
69
|
+
expect(result.passedFileCount).toBe(0);
|
|
67
70
|
expect(result.errors).toEqual(["worker failed", "graph failed"]);
|
|
68
71
|
expect(result.suites[0].framework).toBe("default");
|
|
72
|
+
expect(result.suites[0].failedFileCount).toBe(1);
|
|
69
73
|
expect(result.suites[0].files).toEqual([
|
|
70
74
|
{
|
|
71
75
|
path: "tests/health.js",
|
|
@@ -86,6 +90,11 @@ describe("runner-results", () => {
|
|
|
86
90
|
suiteCount: 1,
|
|
87
91
|
completedSuiteCount: 1,
|
|
88
92
|
failedSuiteCount: 0,
|
|
93
|
+
totalFileCount: 3,
|
|
94
|
+
completedFileCount: 3,
|
|
95
|
+
passedFileCount: 3,
|
|
96
|
+
failedFileCount: 0,
|
|
97
|
+
notRunFileCount: 0,
|
|
89
98
|
durationMs: 1200,
|
|
90
99
|
dbBackend: "local",
|
|
91
100
|
suites: [],
|
|
@@ -98,6 +107,11 @@ describe("runner-results", () => {
|
|
|
98
107
|
suiteCount: 0,
|
|
99
108
|
completedSuiteCount: 0,
|
|
100
109
|
failedSuiteCount: 0,
|
|
110
|
+
totalFileCount: 0,
|
|
111
|
+
completedFileCount: 0,
|
|
112
|
+
passedFileCount: 0,
|
|
113
|
+
failedFileCount: 0,
|
|
114
|
+
notRunFileCount: 0,
|
|
101
115
|
durationMs: 0,
|
|
102
116
|
dbBackend: null,
|
|
103
117
|
suites: [],
|
|
@@ -134,6 +148,12 @@ describe("runner-results", () => {
|
|
|
134
148
|
|
|
135
149
|
expect(artifact.product.name).toBe("my-product");
|
|
136
150
|
expect(artifact.summary.services.total).toBe(1);
|
|
151
|
+
expect(artifact.summary.files).toEqual({
|
|
152
|
+
total: 3,
|
|
153
|
+
passed: 3,
|
|
154
|
+
failed: 0,
|
|
155
|
+
notRun: 0,
|
|
156
|
+
});
|
|
137
157
|
expect(summarizeDbBackend(results)).toBe("local");
|
|
138
158
|
expect(formatDuration(65_000)).toBe("1m 05s");
|
|
139
159
|
expect(
|
|
@@ -141,8 +161,11 @@ describe("runner-results", () => {
|
|
|
141
161
|
completedSuiteCount: 2,
|
|
142
162
|
failedSuiteCount: 1,
|
|
143
163
|
suiteCount: 3,
|
|
164
|
+
totalFileCount: 6,
|
|
165
|
+
passedFileCount: 5,
|
|
166
|
+
notRunFileCount: 1,
|
|
144
167
|
})
|
|
145
|
-
).toBe("1/3 suites passed, 1 not run");
|
|
168
|
+
).toBe("1/3 suites passed, 5/6 files passed, 1 suite not run");
|
|
146
169
|
expect(formatError(new Error("boom"))).toBe("boom");
|
|
147
170
|
});
|
|
148
171
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export type RuntimeMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
2
|
+
|
|
3
|
+
export interface RuntimeHeaders {
|
|
4
|
+
[key: string]: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RuntimeCookie {
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RuntimeResponse {
|
|
12
|
+
body: string;
|
|
13
|
+
cookies?: Record<string, RuntimeCookie[]>;
|
|
14
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
15
|
+
status: number;
|
|
16
|
+
timings?: {
|
|
17
|
+
duration: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RuntimeOptions {
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
thresholds?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RuntimeEnv {
|
|
27
|
+
BASE: string;
|
|
28
|
+
MACHINE_ID?: string;
|
|
29
|
+
routeParams: RuntimeHeaders;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RuntimeDb {
|
|
33
|
+
exec(sql: string): unknown;
|
|
34
|
+
query<T = Record<string, unknown>>(sql: string): T[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RuntimeDalContext {
|
|
38
|
+
db: RuntimeDb;
|
|
39
|
+
truncate(...tables: string[]): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface HttpRequestParams {
|
|
43
|
+
headers?: RuntimeHeaders;
|
|
44
|
+
redirects?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RuntimeHttpClient {
|
|
48
|
+
del(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
|
|
49
|
+
file(data: unknown, filename?: string, contentType?: string): unknown;
|
|
50
|
+
get(url: string, params?: HttpRequestParams): RuntimeResponse;
|
|
51
|
+
patch(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
|
|
52
|
+
post(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
|
|
53
|
+
put(url: string, body?: unknown, params?: HttpRequestParams): RuntimeResponse;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Metric {
|
|
57
|
+
add(value: number): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export declare class Rate implements Metric {
|
|
61
|
+
constructor(name: string, isTime?: boolean);
|
|
62
|
+
add(value: number): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export declare class Trend implements Metric {
|
|
66
|
+
constructor(name: string, isTime?: boolean);
|
|
67
|
+
add(value: number): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface HttpClientConfig<TSetup = unknown> {
|
|
71
|
+
baseUrl: string;
|
|
72
|
+
defaultHeaders?: RuntimeHeaders;
|
|
73
|
+
getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void;
|
|
74
|
+
getRawHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void;
|
|
75
|
+
routeHeaders?: RuntimeHeaders;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface HttpClient<TSetup = unknown> {
|
|
79
|
+
delete(path: string, setupData?: TSetup | null, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
80
|
+
get(path: string, setupData?: TSetup | null, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
81
|
+
getWithHeaders(
|
|
82
|
+
path: string,
|
|
83
|
+
setupData?: TSetup | null,
|
|
84
|
+
extraHeaders?: RuntimeHeaders
|
|
85
|
+
): RuntimeResponse;
|
|
86
|
+
patch(
|
|
87
|
+
path: string,
|
|
88
|
+
setupData?: TSetup | null,
|
|
89
|
+
body?: unknown,
|
|
90
|
+
extraHeaders?: RuntimeHeaders
|
|
91
|
+
): RuntimeResponse;
|
|
92
|
+
post(
|
|
93
|
+
path: string,
|
|
94
|
+
setupData?: TSetup | null,
|
|
95
|
+
body?: unknown,
|
|
96
|
+
extraHeaders?: RuntimeHeaders
|
|
97
|
+
): RuntimeResponse;
|
|
98
|
+
put(
|
|
99
|
+
path: string,
|
|
100
|
+
setupData?: TSetup | null,
|
|
101
|
+
body?: unknown,
|
|
102
|
+
extraHeaders?: RuntimeHeaders
|
|
103
|
+
): RuntimeResponse;
|
|
104
|
+
raw(
|
|
105
|
+
method: RuntimeMethod,
|
|
106
|
+
path: string,
|
|
107
|
+
body?: unknown,
|
|
108
|
+
extraHeaders?: RuntimeHeaders
|
|
109
|
+
): RuntimeResponse;
|
|
110
|
+
rawDelete(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
111
|
+
rawGet(path: string, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
112
|
+
rawPatch(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
113
|
+
rawPost(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
114
|
+
rawPut(path: string, body?: unknown, extraHeaders?: RuntimeHeaders): RuntimeResponse;
|
|
115
|
+
request(
|
|
116
|
+
method: RuntimeMethod,
|
|
117
|
+
path: string,
|
|
118
|
+
setupData?: TSetup | null,
|
|
119
|
+
body?: unknown,
|
|
120
|
+
extraHeaders?: RuntimeHeaders
|
|
121
|
+
): RuntimeResponse;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export declare const check: <T>(
|
|
125
|
+
value: T,
|
|
126
|
+
checks: Record<string, (value: T) => boolean>
|
|
127
|
+
) => boolean;
|
|
128
|
+
export declare const fail: (message: string) => never;
|
|
129
|
+
export declare const group: (name: string, fn: () => void) => void;
|
|
130
|
+
export declare const sleep: (seconds?: number) => void;
|
|
131
|
+
|
|
132
|
+
export declare const http: RuntimeHttpClient;
|
|
133
|
+
|
|
134
|
+
export declare function file(data: unknown, filename?: string, contentType?: string): unknown;
|
|
135
|
+
export declare function json<T = unknown>(response: Pick<RuntimeResponse, "body">): T;
|
|
136
|
+
export declare function contains<T extends Record<string, unknown>>(
|
|
137
|
+
rows: T[],
|
|
138
|
+
field: keyof T | string,
|
|
139
|
+
value: unknown
|
|
140
|
+
): boolean;
|
|
141
|
+
export declare function allMatch<T>(
|
|
142
|
+
rows: T[],
|
|
143
|
+
predicate: (row: T) => boolean
|
|
144
|
+
): boolean;
|
|
145
|
+
export declare function isSorted<T extends Record<string, unknown>>(
|
|
146
|
+
rows: T[],
|
|
147
|
+
field: keyof T | string,
|
|
148
|
+
direction?: "asc" | "desc"
|
|
149
|
+
): boolean;
|
|
150
|
+
|
|
151
|
+
export declare function singleIterationOptions(overrides?: RuntimeOptions): RuntimeOptions;
|
|
152
|
+
export declare const defaultOptions: RuntimeOptions;
|
|
153
|
+
export declare const httpDefaultOptions: RuntimeOptions;
|
|
154
|
+
|
|
155
|
+
export declare function createDalContext(db?: RuntimeDb): RuntimeDalContext;
|
|
156
|
+
export declare function openDb(): RuntimeDb;
|
|
157
|
+
export declare function truncate(db: RuntimeDb, ...tables: string[]): void;
|
|
158
|
+
|
|
159
|
+
export declare function getEnv(): RuntimeEnv;
|
|
160
|
+
export declare function createHttpClient<TSetup = unknown>(
|
|
161
|
+
config: HttpClientConfig<TSetup>
|
|
162
|
+
): HttpClient<TSetup>;
|
|
163
|
+
export declare function makeReq<TSetup = unknown>(
|
|
164
|
+
baseUrl: string,
|
|
165
|
+
routeHeaders?: RuntimeHeaders,
|
|
166
|
+
getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
|
|
167
|
+
): HttpClient<TSetup>["request"];
|
|
168
|
+
export declare function makeRawReq(
|
|
169
|
+
baseUrl: string,
|
|
170
|
+
routeHeaders?: RuntimeHeaders,
|
|
171
|
+
getRawHeaders?: (setupData?: never) => RuntimeHeaders | void
|
|
172
|
+
): HttpClient["raw"];
|
|
173
|
+
export declare function makeGetWithHeaders<TSetup = unknown>(
|
|
174
|
+
baseUrl: string,
|
|
175
|
+
routeHeaders?: RuntimeHeaders,
|
|
176
|
+
getHeaders?: (setupData?: TSetup | null) => RuntimeHeaders | void
|
|
177
|
+
): HttpClient<TSetup>["getWithHeaders"];
|
|
178
|
+
|
|
179
|
+
declare global {
|
|
180
|
+
const __ENV: Record<string, string | undefined>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"types": "./lib/index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
|
-
".":
|
|
8
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"default": "./lib/index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./runtime": {
|
|
13
|
+
"types": "./lib/runtime/index.d.ts",
|
|
14
|
+
"default": "./lib/runtime/index.mjs"
|
|
15
|
+
},
|
|
9
16
|
"./package.json": "./package.json"
|
|
10
17
|
},
|
|
11
18
|
"bin": {
|