@elench/testkit 0.1.77 → 0.1.79
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 +24 -1
- package/lib/app/doctor.mjs +43 -0
- package/lib/cli/presentation/colors.mjs +21 -4
- package/lib/cli/presentation/discovery-reporter.mjs +33 -12
- package/lib/cli/presentation/run-reporter.mjs +90 -10
- package/lib/cli/presentation/run-reporter.test.mjs +3 -2
- package/lib/config-api/index.d.ts +189 -16
- package/lib/config-api/index.mjs +14 -170
- package/lib/config-api/index.test.mjs +282 -4
- package/lib/config-api/profiles.mjs +640 -0
- package/lib/runner/orchestrator.mjs +1 -0
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -365,8 +365,31 @@ Named HTTP profiles live in `testkit.config.ts` and can be referenced by name:
|
|
|
365
365
|
|
|
366
366
|
```ts
|
|
367
367
|
import { defineHttpSuite } from "@elench/testkit";
|
|
368
|
+
import { defineConfig, profiles } from "@elench/testkit/config";
|
|
368
369
|
|
|
369
|
-
|
|
370
|
+
export default defineConfig({
|
|
371
|
+
profiles: {
|
|
372
|
+
http: {
|
|
373
|
+
defaultAuth: profiles.localJson({
|
|
374
|
+
password: "password",
|
|
375
|
+
identities: {
|
|
376
|
+
primary: {
|
|
377
|
+
email: "test@example.com",
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
session: {
|
|
381
|
+
authCookie: "session",
|
|
382
|
+
},
|
|
383
|
+
headers: {
|
|
384
|
+
contentTypeJson: true,
|
|
385
|
+
forwardedFor: "deterministic",
|
|
386
|
+
},
|
|
387
|
+
}).session(),
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const suite = defineHttpSuite({ profile: "defaultAuth" }, ({ req, setupData }) => {
|
|
370
393
|
req("GET", "/api/auth/session", setupData);
|
|
371
394
|
});
|
|
372
395
|
```
|
package/lib/app/doctor.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import ts from "typescript";
|
|
|
4
4
|
import { discoverTests } from "../discovery/index.mjs";
|
|
5
5
|
import { loadConfigContext } from "../config/index.mjs";
|
|
6
6
|
import { runTestkitTypecheck } from "./typecheck.mjs";
|
|
7
|
+
import { findConfigFile } from "../config/config-loader.mjs";
|
|
7
8
|
|
|
8
9
|
export async function runDoctor(options = {}) {
|
|
9
10
|
const checks = [];
|
|
@@ -39,6 +40,17 @@ export async function runDoctor(options = {}) {
|
|
|
39
40
|
details: playwrightViolations,
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
const configImportViolations = findConfigImportViolations(productDir);
|
|
44
|
+
checks.push({
|
|
45
|
+
code: "config-import-hygiene",
|
|
46
|
+
level: configImportViolations.length === 0 ? "pass" : "fail",
|
|
47
|
+
message:
|
|
48
|
+
configImportViolations.length === 0
|
|
49
|
+
? "Repo config does not import __testkit__ helper modules"
|
|
50
|
+
: `Found ${configImportViolations.length} repo config import violation(s)`,
|
|
51
|
+
details: configImportViolations,
|
|
52
|
+
});
|
|
53
|
+
|
|
42
54
|
const hasBrowserOrNextWork = discovery.files.some((entry) => entry.selectionType === "pw");
|
|
43
55
|
if (hasBrowserOrNextWork) {
|
|
44
56
|
const nodeCount = discovery.coverageGraph?.nodes?.length || 0;
|
|
@@ -113,6 +125,31 @@ function findPlaywrightRuntimeImportViolations(productDir) {
|
|
|
113
125
|
return violations;
|
|
114
126
|
}
|
|
115
127
|
|
|
128
|
+
function findConfigImportViolations(productDir) {
|
|
129
|
+
const configFile = findConfigFile(productDir);
|
|
130
|
+
if (!configFile || !fs.existsSync(configFile)) return [];
|
|
131
|
+
|
|
132
|
+
const sourceText = fs.readFileSync(configFile, "utf8");
|
|
133
|
+
const sourceFile = ts.createSourceFile(configFile, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
134
|
+
const violations = [];
|
|
135
|
+
|
|
136
|
+
for (const statement of sourceFile.statements) {
|
|
137
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
138
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
139
|
+
const specifier = statement.moduleSpecifier.text;
|
|
140
|
+
if (!isRepoLocalConfigImportViolation(specifier)) continue;
|
|
141
|
+
const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
|
|
142
|
+
violations.push({
|
|
143
|
+
file: path.relative(productDir, configFile).split(path.sep).join("/"),
|
|
144
|
+
line: position.line + 1,
|
|
145
|
+
specifier,
|
|
146
|
+
snippet: statement.getText(sourceFile),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return violations;
|
|
151
|
+
}
|
|
152
|
+
|
|
116
153
|
function collectFiles(rootDir, out = []) {
|
|
117
154
|
if (!fs.existsSync(rootDir)) return out;
|
|
118
155
|
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
@@ -129,6 +166,12 @@ function collectFiles(rootDir, out = []) {
|
|
|
129
166
|
return out.sort((left, right) => left.localeCompare(right));
|
|
130
167
|
}
|
|
131
168
|
|
|
169
|
+
function isRepoLocalConfigImportViolation(specifier) {
|
|
170
|
+
if (typeof specifier !== "string") return false;
|
|
171
|
+
if (!specifier.startsWith(".") && !specifier.startsWith("/")) return false;
|
|
172
|
+
return specifier.includes("__testkit__");
|
|
173
|
+
}
|
|
174
|
+
|
|
132
175
|
function relativeViolation(productDir, absolutePath, sourceFile, statement) {
|
|
133
176
|
const position = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
|
|
134
177
|
return {
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { createColors } from "picocolors";
|
|
2
|
+
import figures from "figures";
|
|
2
3
|
|
|
3
4
|
const pc = createColors(Boolean(process.stdout?.isTTY || process.env.FORCE_COLOR));
|
|
4
5
|
|
|
5
6
|
export function colorStatus(status) {
|
|
6
|
-
if (status === "PASS") return pc.green(
|
|
7
|
-
if (status === "FAIL") return pc.red(
|
|
8
|
-
if (status === "SKIP") return pc.yellow(
|
|
9
|
-
if (status === "RUN") return pc.cyan(
|
|
7
|
+
if (status === "PASS") return pc.green(figures.tick);
|
|
8
|
+
if (status === "FAIL") return pc.red(figures.cross);
|
|
9
|
+
if (status === "SKIP") return pc.yellow(figures.arrowDown);
|
|
10
|
+
if (status === "RUN") return pc.cyan(figures.play);
|
|
11
|
+
return status;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function statusLabel(status) {
|
|
15
|
+
if (status === "PASS") return pc.green(`${figures.tick} PASS`);
|
|
16
|
+
if (status === "FAIL") return pc.red(`${figures.cross} FAIL`);
|
|
17
|
+
if (status === "SKIP") return pc.yellow(`${figures.arrowDown} SKIP`);
|
|
18
|
+
if (status === "RUN") return pc.cyan(`${figures.play} RUN`);
|
|
10
19
|
return status;
|
|
11
20
|
}
|
|
12
21
|
|
|
@@ -18,6 +27,14 @@ export function bold(text) {
|
|
|
18
27
|
return pc.bold(text);
|
|
19
28
|
}
|
|
20
29
|
|
|
30
|
+
export function boldRed(text) {
|
|
31
|
+
return pc.bold(pc.red(text));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function red(text) {
|
|
35
|
+
return pc.red(text);
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
export function muted(text) {
|
|
22
39
|
return pc.dim(text);
|
|
23
40
|
}
|
|
@@ -5,13 +5,18 @@ import {
|
|
|
5
5
|
colorDiagnosticSeverity,
|
|
6
6
|
colorHeading,
|
|
7
7
|
colorService,
|
|
8
|
-
colorStatus,
|
|
9
8
|
colorTypeBadge,
|
|
10
9
|
muted,
|
|
10
|
+
statusLabel,
|
|
11
11
|
} from "./colors.mjs";
|
|
12
12
|
|
|
13
13
|
const TYPE_ORDER = ["int", "e2e", "scenario", "dal", "load", "pw"];
|
|
14
14
|
|
|
15
|
+
const TREE_BRANCH = "\u251C\u2500\u2500 ";
|
|
16
|
+
const TREE_LAST = "\u2514\u2500\u2500 ";
|
|
17
|
+
const TREE_PIPE = "\u2502 ";
|
|
18
|
+
const TREE_SPACE = " ";
|
|
19
|
+
|
|
15
20
|
export function buildDiscoveryReportLines(result, options = {}) {
|
|
16
21
|
const mode = options.outputMode || "compact";
|
|
17
22
|
return mode === "verbose" ? buildVerboseLines(result) : buildCompactLines(result);
|
|
@@ -20,7 +25,7 @@ export function buildDiscoveryReportLines(result, options = {}) {
|
|
|
20
25
|
function buildCompactLines(result) {
|
|
21
26
|
const lines = [];
|
|
22
27
|
lines.push(
|
|
23
|
-
`${colorHeading("Summary")} ${result.summary.files} files
|
|
28
|
+
`${colorHeading("Summary")} ${result.summary.files} files \u00B7 ${result.summary.activeFiles} active \u00B7 ${result.summary.skippedFiles} skipped \u00B7 ${result.summary.suites} suites \u00B7 ${result.summary.services} services`
|
|
24
29
|
);
|
|
25
30
|
|
|
26
31
|
if (result.history?.available) {
|
|
@@ -35,17 +40,33 @@ function buildCompactLines(result) {
|
|
|
35
40
|
for (const service of result.services) {
|
|
36
41
|
lines.push("");
|
|
37
42
|
lines.push(
|
|
38
|
-
`${colorService(service.name)} ${muted(`(${service.fileCount} files${service.dependsOn.length > 0 ? `
|
|
43
|
+
`${colorService(service.name)} ${muted(`(${service.fileCount} files${service.dependsOn.length > 0 ? ` \u00B7 depends on ${service.dependsOn.join(", ")}` : ""})`)}`
|
|
39
44
|
);
|
|
40
45
|
const typeGroups = suitesByService.get(service.name) || new Map();
|
|
41
|
-
|
|
46
|
+
const types = orderedTypes([...typeGroups.keys()]);
|
|
47
|
+
|
|
48
|
+
for (let ti = 0; ti < types.length; ti++) {
|
|
49
|
+
const type = types[ti];
|
|
50
|
+
const isLastType = ti === types.length - 1;
|
|
51
|
+
const typeConnector = isLastType ? TREE_LAST : TREE_BRANCH;
|
|
52
|
+
const typePrefix = isLastType ? TREE_SPACE : TREE_PIPE;
|
|
42
53
|
const suites = typeGroups.get(type) || [];
|
|
43
|
-
lines.push(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
lines.push(`${typeConnector}${colorTypeBadge(type.toUpperCase())} ${formatSelectionTypeLabel(type)}`);
|
|
55
|
+
|
|
56
|
+
for (let si = 0; si < suites.length; si++) {
|
|
57
|
+
const suite = suites[si];
|
|
58
|
+
const isLastSuite = si === suites.length - 1;
|
|
59
|
+
const suiteConnector = isLastSuite ? TREE_LAST : TREE_BRANCH;
|
|
60
|
+
const suitePrefix = isLastSuite ? TREE_SPACE : TREE_PIPE;
|
|
61
|
+
lines.push(`${typePrefix}${suiteConnector}${bold(suite.groupLabel)} ${muted(`(${suite.fileCount} files)`)}`);
|
|
62
|
+
|
|
63
|
+
const files = filesBySuite.get(suite.id) || [];
|
|
64
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
65
|
+
const file = files[fi];
|
|
66
|
+
const isLastFile = fi === files.length - 1;
|
|
67
|
+
const fileConnector = isLastFile ? TREE_LAST : TREE_BRANCH;
|
|
68
|
+
const status = file.skipped ? `${statusLabel("SKIP")} ${file.skipReason}` : muted(buildHistoryHint(file));
|
|
69
|
+
lines.push(`${typePrefix}${suitePrefix}${fileConnector}${file.displayName}${status ? ` ${status}` : ""}`);
|
|
49
70
|
}
|
|
50
71
|
}
|
|
51
72
|
}
|
|
@@ -73,7 +94,7 @@ function buildVerboseLines(result) {
|
|
|
73
94
|
if (service.dependsOn.length > 0) {
|
|
74
95
|
lines.push(` dependsOn ${service.dependsOn.join(", ")}`);
|
|
75
96
|
}
|
|
76
|
-
lines.push(` files ${service.fileCount}
|
|
97
|
+
lines.push(` files ${service.fileCount} \u00B7 active ${service.activeFileCount} \u00B7 skipped ${service.skippedFileCount}`);
|
|
77
98
|
|
|
78
99
|
const suites = result.suites.filter((suite) => suite.service === service.name).sort(compareSuites);
|
|
79
100
|
for (const suite of suites) {
|
|
@@ -162,5 +183,5 @@ function buildHistoryHint(file) {
|
|
|
162
183
|
file.history.firstSeenAt ? `seen ${file.history.firstSeenAt.slice(0, 10)}` : null,
|
|
163
184
|
]
|
|
164
185
|
.filter(Boolean)
|
|
165
|
-
.join("
|
|
186
|
+
.join(" \u00B7 ");
|
|
166
187
|
}
|
|
@@ -1,15 +1,89 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import figures from "figures";
|
|
2
3
|
import {
|
|
3
4
|
buildCompactRunSummaryLines,
|
|
4
5
|
buildDebugRunSummaryLines,
|
|
5
6
|
formatDuration,
|
|
6
7
|
} from "../../runner/formatting.mjs";
|
|
7
|
-
import {
|
|
8
|
+
import { boldRed, colorSectionLine, dim, red, statusLabel } from "./colors.mjs";
|
|
9
|
+
|
|
10
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
11
|
+
const DURATION_RE = /\b(\d+m\s+\d+s|\d+s)\b/g;
|
|
12
|
+
|
|
13
|
+
function stripAnsi(text) {
|
|
14
|
+
return text.replace(ANSI_RE, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function boxLines(lines) {
|
|
18
|
+
const nonEmpty = lines.filter((l) => stripAnsi(l).trim().length > 0);
|
|
19
|
+
if (nonEmpty.length === 0) return lines;
|
|
20
|
+
const maxWidth = lines.reduce((max, l) => Math.max(max, stripAnsi(l).length), 0);
|
|
21
|
+
const pad = 1;
|
|
22
|
+
const innerWidth = maxWidth + pad * 2;
|
|
23
|
+
const top = `${figures.lineDownRight}${figures.line.repeat(innerWidth)}${figures.lineDownLeft}`;
|
|
24
|
+
const bottom = `${figures.lineUpRight}${figures.line.repeat(innerWidth)}${figures.lineUpLeft}`;
|
|
25
|
+
const boxed = [top];
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const visible = stripAnsi(line).length;
|
|
28
|
+
const right = innerWidth - pad - visible;
|
|
29
|
+
boxed.push(`${figures.lineVertical}${" ".repeat(pad)}${line}${" ".repeat(Math.max(0, right))}${figures.lineVertical}`);
|
|
30
|
+
}
|
|
31
|
+
boxed.push(bottom);
|
|
32
|
+
return boxed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function colorFailureSummaryLines(lines) {
|
|
36
|
+
const result = [];
|
|
37
|
+
let inFailuresSection = false;
|
|
38
|
+
let lastWasFilePath = false;
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const raw = stripAnsi(line);
|
|
41
|
+
if (raw === "Failures:" || raw === "Runtime Errors:") {
|
|
42
|
+
inFailuresSection = true;
|
|
43
|
+
lastWasFilePath = false;
|
|
44
|
+
result.push(line);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (/^(Summary:|Result:|Known-failure|Triage:)/.test(raw) || raw === "") {
|
|
48
|
+
inFailuresSection = false;
|
|
49
|
+
lastWasFilePath = false;
|
|
50
|
+
}
|
|
51
|
+
if (inFailuresSection) {
|
|
52
|
+
if (/^ {2}\S/.test(raw)) {
|
|
53
|
+
lastWasFilePath = true;
|
|
54
|
+
result.push(boldRed(raw));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (/^ {4}\S/.test(raw)) {
|
|
58
|
+
if (lastWasFilePath) {
|
|
59
|
+
lastWasFilePath = false;
|
|
60
|
+
result.push(boldRed(raw));
|
|
61
|
+
} else {
|
|
62
|
+
result.push(red(raw));
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lastWasFilePath = false;
|
|
68
|
+
result.push(line);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dimDurations(line) {
|
|
74
|
+
return line.replace(DURATION_RE, (match) => dim(match));
|
|
75
|
+
}
|
|
8
76
|
|
|
9
77
|
export function createRunReporter({ outputMode = "compact", stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
10
78
|
const mode = outputMode || "compact";
|
|
79
|
+
let completedCount = 0;
|
|
80
|
+
let totalFileCount = 0;
|
|
81
|
+
|
|
11
82
|
return {
|
|
12
83
|
outputMode: mode,
|
|
84
|
+
setTotalFileCount(count) {
|
|
85
|
+
totalFileCount = count;
|
|
86
|
+
},
|
|
13
87
|
writeLine(line = "") {
|
|
14
88
|
stdout.write(`${line}\n`);
|
|
15
89
|
},
|
|
@@ -34,7 +108,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
34
108
|
if (operation.status === "failed") {
|
|
35
109
|
const detail = shortenMessage(operation.error || operation.summary || operation.stage);
|
|
36
110
|
stdout.write(
|
|
37
|
-
`${
|
|
111
|
+
`${statusLabel("FAIL")} ${"SETUP"} ${operation.serviceName} ${operation.stage} ${dim(duration)} ${boldRed(detail)}\n`
|
|
38
112
|
);
|
|
39
113
|
return;
|
|
40
114
|
}
|
|
@@ -44,7 +118,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
44
118
|
(operation.durationMs || 0) >= 5_000
|
|
45
119
|
) {
|
|
46
120
|
const summary = shortenMessage(operation.summary || operation.stage);
|
|
47
|
-
stdout.write(`${
|
|
121
|
+
stdout.write(`${statusLabel("RUN")} ${"SETUP"} ${operation.serviceName} ${summary} ${dim(duration)}\n`);
|
|
48
122
|
}
|
|
49
123
|
},
|
|
50
124
|
localServiceStarting(config, command) {
|
|
@@ -52,19 +126,20 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
52
126
|
stdout.write(`Starting ${config.runtimeLabel}:${config.name}: ${command}\n`);
|
|
53
127
|
},
|
|
54
128
|
serviceSkipped(config, reason) {
|
|
55
|
-
stdout.write(`${
|
|
129
|
+
stdout.write(`${statusLabel("SKIP")} ${config.name} ${reason}\n`);
|
|
56
130
|
},
|
|
57
131
|
plannedSkip(entry) {
|
|
58
132
|
stdout.write(
|
|
59
|
-
`${
|
|
133
|
+
`${statusLabel("SKIP")} ${entry.serviceName} ${entry.type} ${normalizePath(entry.file)} ${dim("0s")} ${shortenMessage(entry.reason || "skipped")}\n`
|
|
60
134
|
);
|
|
61
135
|
},
|
|
62
136
|
taskStarted(task, targetConfig) {
|
|
63
137
|
if (mode !== "debug") return;
|
|
64
|
-
stdout.write(`${
|
|
138
|
+
stdout.write(`${statusLabel("RUN")} ${targetConfig.name} ${task.type} ${task.file}\n`);
|
|
65
139
|
},
|
|
66
140
|
taskFinished(task, outcome) {
|
|
67
141
|
if (mode === "json") return;
|
|
142
|
+
completedCount += 1;
|
|
68
143
|
const status = outcome.status === "skipped" ? "SKIP" : outcome.failed ? "FAIL" : "PASS";
|
|
69
144
|
const duration = formatDuration(outcome.durationMs || 0);
|
|
70
145
|
const primaryFailure = firstFailureDetail(outcome);
|
|
@@ -72,12 +147,13 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
72
147
|
primaryFailure && isThresholdWrapperMessage(outcome.error) ? primaryFailure : outcome.error || primaryFailure;
|
|
73
148
|
const detail =
|
|
74
149
|
status === "FAIL"
|
|
75
|
-
? ` ${shortenMessage(preferredFailure || "failed")}`
|
|
150
|
+
? ` ${boldRed(shortenMessage(preferredFailure || "failed"))}`
|
|
76
151
|
: outcome.status === "not_run"
|
|
77
|
-
? ` ${shortenMessage(outcome.reason || "not run")}`
|
|
152
|
+
? ` ${dim(shortenMessage(outcome.reason || "not run"))}`
|
|
78
153
|
: "";
|
|
154
|
+
const progress = mode === "compact" && totalFileCount > 0 ? `${dim(`[${completedCount}/${totalFileCount}]`)} ` : "";
|
|
79
155
|
stdout.write(
|
|
80
|
-
`${
|
|
156
|
+
`${progress}${statusLabel(status)} ${task.serviceName} ${displayTaskType(task)} ${normalizePath(task.file)} ${dim(duration)}${detail}\n`
|
|
81
157
|
);
|
|
82
158
|
},
|
|
83
159
|
telemetry(message) {
|
|
@@ -89,7 +165,11 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
|
|
|
89
165
|
mode === "debug"
|
|
90
166
|
? buildDebugRunSummaryLines(results, durationMs, knownFailureIssueValidation)
|
|
91
167
|
: buildCompactRunSummaryLines(results, durationMs, knownFailureIssueValidation);
|
|
92
|
-
|
|
168
|
+
const colored = colorFailureSummaryLines(lines.map((line) => colorSectionLine(line)));
|
|
169
|
+
const dimmed = colored.map((line) => dimDurations(line));
|
|
170
|
+
const boxed = boxLines(dimmed);
|
|
171
|
+
stdout.write("\n");
|
|
172
|
+
for (const line of boxed) stdout.write(`${line}\n`);
|
|
93
173
|
},
|
|
94
174
|
error(message) {
|
|
95
175
|
stderr.write(`${message}\n`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Writable } from "stream";
|
|
2
|
+
import figures from "figures";
|
|
2
3
|
import { describe, expect, it } from "vitest";
|
|
3
4
|
import { createRunReporter } from "./run-reporter.mjs";
|
|
4
5
|
|
|
@@ -24,7 +25,7 @@ describe("run reporter setup output", () => {
|
|
|
24
25
|
durationMs: 8_000,
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
expect(stdout).toContain(
|
|
28
|
+
expect(stdout).toContain(`${figures.play} RUN SETUP api template rebuild`);
|
|
28
29
|
expect(stdout).toContain("8s");
|
|
29
30
|
});
|
|
30
31
|
|
|
@@ -74,7 +75,7 @@ describe("run reporter setup output", () => {
|
|
|
74
75
|
error: "Command failed with exit code 1: node scripts/fail-prepare.mjs",
|
|
75
76
|
});
|
|
76
77
|
|
|
77
|
-
expect(stdout).toContain(
|
|
78
|
+
expect(stdout).toContain(`${figures.cross} FAIL SETUP api runtime:prepare`);
|
|
78
79
|
expect(stdout).toContain("Command failed with exit code 1");
|
|
79
80
|
});
|
|
80
81
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HttpSuiteConfig, RuntimeEnv, RuntimeOptions } from "../index";
|
|
2
2
|
|
|
3
3
|
export interface DatabaseTemplateConfig {
|
|
4
4
|
inputs?: string[];
|
|
@@ -192,6 +192,187 @@ export interface TestkitFileMetadata {
|
|
|
192
192
|
skip?: string | { reason: string };
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
export interface ProfileRequestContext {
|
|
196
|
+
actor: string;
|
|
197
|
+
actorIndex: number;
|
|
198
|
+
env: RuntimeEnv;
|
|
199
|
+
phase: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type ProfileValueFactory<TValue> =
|
|
203
|
+
| TValue
|
|
204
|
+
| ((context: ProfileRequestContext) => TValue);
|
|
205
|
+
|
|
206
|
+
export interface ProfileRequestConfig {
|
|
207
|
+
body?: ProfileValueFactory<unknown>;
|
|
208
|
+
contentTypeJson?: boolean;
|
|
209
|
+
expect?: number | number[];
|
|
210
|
+
headers?: ProfileValueFactory<Record<string, string>>;
|
|
211
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
212
|
+
path: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface SessionAuthSourceConfig {
|
|
216
|
+
key: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface SessionAuthHeaderConfig {
|
|
220
|
+
header?: string;
|
|
221
|
+
prefix?: string;
|
|
222
|
+
source: SessionAuthSourceConfig;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface SessionCaptureConfig {
|
|
226
|
+
auth?: SessionAuthHeaderConfig;
|
|
227
|
+
cookies?: Record<string, string>;
|
|
228
|
+
fields?: Record<string, string>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface SessionActorConfig {
|
|
232
|
+
bootstrap?: ProfileRequestConfig | ProfileRequestConfig[];
|
|
233
|
+
login: ProfileRequestConfig;
|
|
234
|
+
session: SessionCaptureConfig;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ProfileHeaderContext<TSession = unknown> {
|
|
238
|
+
actor: string | null;
|
|
239
|
+
env: RuntimeEnv;
|
|
240
|
+
session: TSession | null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface ProfileSessionHeaderConfig {
|
|
244
|
+
actor?: string;
|
|
245
|
+
field: string;
|
|
246
|
+
header: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface DeterministicForwardedForConfig {
|
|
250
|
+
actor?: string;
|
|
251
|
+
header?: string;
|
|
252
|
+
seed?: string;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface ProfileHeaderConfig<TSession = unknown> {
|
|
256
|
+
contentTypeJson?: boolean;
|
|
257
|
+
forwardedFor?: "deterministic" | DeterministicForwardedForConfig;
|
|
258
|
+
fromSession?: ProfileSessionHeaderConfig[];
|
|
259
|
+
values?: Record<string, string> | ((context: ProfileHeaderContext<TSession>) => Record<string, string>);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface RawHttpProfileOptions {
|
|
263
|
+
env?: RuntimeEnv;
|
|
264
|
+
headers?: ProfileHeaderConfig;
|
|
265
|
+
options?: RuntimeOptions;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface SessionHttpProfileOptions {
|
|
269
|
+
actor: SessionActorConfig;
|
|
270
|
+
env?: RuntimeEnv;
|
|
271
|
+
headers?: ProfileHeaderConfig<Record<string, unknown>>;
|
|
272
|
+
options?: RuntimeOptions;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface MultiActorHttpProfileOptions {
|
|
276
|
+
actors: Record<string, SessionActorConfig>;
|
|
277
|
+
env?: RuntimeEnv;
|
|
278
|
+
headers?: ProfileHeaderConfig<Record<string, Record<string, unknown>>>;
|
|
279
|
+
options?: RuntimeOptions;
|
|
280
|
+
primaryActor?: string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface LocalJsonIdentityContext {
|
|
284
|
+
actor: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export type LocalJsonIdentityValue = string | ((context: LocalJsonIdentityContext) => string);
|
|
288
|
+
|
|
289
|
+
export interface LocalJsonActorIdentityConfig {
|
|
290
|
+
email?: LocalJsonIdentityValue;
|
|
291
|
+
loginBody?: Record<string, unknown>;
|
|
292
|
+
name?: LocalJsonIdentityValue;
|
|
293
|
+
organizationName?: LocalJsonIdentityValue;
|
|
294
|
+
password?: LocalJsonIdentityValue;
|
|
295
|
+
signupBody?: Record<string, unknown>;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface LocalJsonSessionAuthOptions {
|
|
299
|
+
header?: string;
|
|
300
|
+
prefix?: string;
|
|
301
|
+
sourceKey?: string;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface LocalJsonSessionOptions {
|
|
305
|
+
auth?: LocalJsonSessionAuthOptions;
|
|
306
|
+
authCookie?: string;
|
|
307
|
+
cookies?: Record<string, string>;
|
|
308
|
+
fields?: Record<string, string>;
|
|
309
|
+
organizationIdPath?: string;
|
|
310
|
+
refreshCookie?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface LocalJsonOrganizationHeaderConfig {
|
|
314
|
+
actor?: string;
|
|
315
|
+
field?: string;
|
|
316
|
+
header?: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface LocalJsonHeaderOptions {
|
|
320
|
+
contentTypeJson?: boolean;
|
|
321
|
+
forwardedFor?: "deterministic" | DeterministicForwardedForConfig;
|
|
322
|
+
organization?: false | string | LocalJsonOrganizationHeaderConfig;
|
|
323
|
+
values?: Record<string, string> | ((context: ProfileHeaderContext<unknown>) => Record<string, string>);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface LocalJsonSignupOptions {
|
|
327
|
+
enabled?: boolean;
|
|
328
|
+
expect?: number | number[];
|
|
329
|
+
path?: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface LocalJsonLoginOptions {
|
|
333
|
+
expect?: number | number[];
|
|
334
|
+
path?: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface LocalJsonProfileOptions {
|
|
338
|
+
env?: RuntimeEnv;
|
|
339
|
+
headers?: LocalJsonHeaderOptions;
|
|
340
|
+
identities?: Record<string, LocalJsonActorIdentityConfig>;
|
|
341
|
+
login?: LocalJsonLoginOptions;
|
|
342
|
+
options?: RuntimeOptions;
|
|
343
|
+
password?: LocalJsonIdentityValue;
|
|
344
|
+
session?: LocalJsonSessionOptions;
|
|
345
|
+
signup?: false | LocalJsonSignupOptions;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export interface LocalJsonSessionProfileOptions {
|
|
349
|
+
actor?: string;
|
|
350
|
+
env?: RuntimeEnv;
|
|
351
|
+
headers?: LocalJsonHeaderOptions;
|
|
352
|
+
identity?: LocalJsonActorIdentityConfig;
|
|
353
|
+
options?: RuntimeOptions;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface LocalJsonMultiActorProfileOptions {
|
|
357
|
+
actors?: string[] | Record<string, LocalJsonActorIdentityConfig>;
|
|
358
|
+
env?: RuntimeEnv;
|
|
359
|
+
headers?: LocalJsonHeaderOptions;
|
|
360
|
+
options?: RuntimeOptions;
|
|
361
|
+
primaryActor?: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export interface LocalJsonRawProfileOptions {
|
|
365
|
+
env?: RuntimeEnv;
|
|
366
|
+
headers?: LocalJsonHeaderOptions;
|
|
367
|
+
options?: RuntimeOptions;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export interface LocalJsonProfileBuilder {
|
|
371
|
+
multiActor(options?: LocalJsonMultiActorProfileOptions): HttpSuiteConfig<Record<string, Record<string, unknown>>>;
|
|
372
|
+
raw(options?: LocalJsonRawProfileOptions): HttpSuiteConfig<any>;
|
|
373
|
+
session(options?: LocalJsonSessionProfileOptions): HttpSuiteConfig<Record<string, unknown>>;
|
|
374
|
+
}
|
|
375
|
+
|
|
195
376
|
export interface NodeAppOptions extends Omit<ServiceConfig, "local" | "runtime" | "env"> {
|
|
196
377
|
baseUrl?: string;
|
|
197
378
|
build?: BuildConfig | null;
|
|
@@ -246,7 +427,6 @@ export interface TestkitConfig {
|
|
|
246
427
|
}
|
|
247
428
|
|
|
248
429
|
export declare function defineConfig<T extends TestkitConfig>(config: T): T;
|
|
249
|
-
export declare function defineHttpProfile<T extends HttpSuiteConfig>(profile: T): T;
|
|
250
430
|
export declare function defineFile<T extends TestkitFileMetadata>(metadata: T): T;
|
|
251
431
|
export declare const app: {
|
|
252
432
|
node(options: NodeAppOptions): ServiceConfig;
|
|
@@ -269,20 +449,13 @@ export declare const database: {
|
|
|
269
449
|
export declare const toolchain: {
|
|
270
450
|
node(options?: NodeToolchainConfig): NodeToolchainConfig;
|
|
271
451
|
};
|
|
272
|
-
export declare
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
cookieName?: string;
|
|
280
|
-
headers?: Record<string, string>;
|
|
281
|
-
loginPath?: string;
|
|
282
|
-
passwordEnv?: string;
|
|
283
|
-
successStatus?: number;
|
|
284
|
-
usernameEnv?: string;
|
|
285
|
-
}): HttpSuiteConfig;
|
|
452
|
+
export declare const profiles: {
|
|
453
|
+
custom<T extends HttpSuiteConfig>(profile: T): T;
|
|
454
|
+
localJson(options?: LocalJsonProfileOptions): LocalJsonProfileBuilder;
|
|
455
|
+
multiActor(options: MultiActorHttpProfileOptions): HttpSuiteConfig<Record<string, Record<string, unknown>>>;
|
|
456
|
+
raw(options?: RawHttpProfileOptions): HttpSuiteConfig<any>;
|
|
457
|
+
session(options: SessionHttpProfileOptions): HttpSuiteConfig<Record<string, unknown>>;
|
|
458
|
+
};
|
|
286
459
|
|
|
287
460
|
export declare function registerRepoConfig(config: unknown): void;
|
|
288
461
|
export declare function getRepoConfig(): unknown;
|