ccqa 0.1.3 → 0.1.4
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/package.json +1 -1
- package/src/cli/run.ts +34 -14
- package/src/runtime/test-helpers.ts +77 -27
package/package.json
CHANGED
package/src/cli/run.ts
CHANGED
|
@@ -84,7 +84,6 @@ async function runTests(target?: string): Promise<void> {
|
|
|
84
84
|
"vitest",
|
|
85
85
|
"run",
|
|
86
86
|
scriptFile,
|
|
87
|
-
"--reporter=default",
|
|
88
87
|
"--reporter=json",
|
|
89
88
|
`--outputFile.json=${reportFile}`,
|
|
90
89
|
],
|
|
@@ -120,8 +119,22 @@ async function readReport(path: string): Promise<VitestJsonReport | null> {
|
|
|
120
119
|
}
|
|
121
120
|
}
|
|
122
121
|
|
|
122
|
+
const useColor = process.stdout.isTTY && process.env.NO_COLOR == null;
|
|
123
|
+
const C = {
|
|
124
|
+
reset: useColor ? "\x1b[0m" : "",
|
|
125
|
+
bold: useColor ? "\x1b[1m" : "",
|
|
126
|
+
dim: useColor ? "\x1b[2m" : "",
|
|
127
|
+
green: useColor ? "\x1b[32m" : "",
|
|
128
|
+
red: useColor ? "\x1b[31m" : "",
|
|
129
|
+
yellow: useColor ? "\x1b[33m" : "",
|
|
130
|
+
cyan: useColor ? "\x1b[36m" : "",
|
|
131
|
+
gray: useColor ? "\x1b[90m" : "",
|
|
132
|
+
};
|
|
133
|
+
|
|
123
134
|
function printSummary(summaries: SpecRunSummary[]): void {
|
|
124
|
-
process.stdout.write(
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`\n${C.cyan}${C.bold}──────── ccqa summary ────────${C.reset}\n\n`,
|
|
137
|
+
);
|
|
125
138
|
|
|
126
139
|
let totalTests = 0;
|
|
127
140
|
let totalPassed = 0;
|
|
@@ -129,10 +142,11 @@ function printSummary(summaries: SpecRunSummary[]): void {
|
|
|
129
142
|
let totalSkipped = 0;
|
|
130
143
|
|
|
131
144
|
for (const s of summaries) {
|
|
132
|
-
const header = `${s.featureName}/${s.specName}`;
|
|
145
|
+
const header = `${C.bold}${s.featureName}/${s.specName}${C.reset}`;
|
|
133
146
|
if (!s.report) {
|
|
134
|
-
const
|
|
135
|
-
|
|
147
|
+
const ok = s.exitCode === 0;
|
|
148
|
+
const icon = ok ? `${C.green}✔${C.reset}` : `${C.red}✖${C.reset}`;
|
|
149
|
+
process.stdout.write(`${icon} ${header} ${C.dim}(no report)${C.reset}\n`);
|
|
136
150
|
continue;
|
|
137
151
|
}
|
|
138
152
|
|
|
@@ -141,32 +155,38 @@ function printSummary(summaries: SpecRunSummary[]): void {
|
|
|
141
155
|
totalFailed += s.report.numFailedTests;
|
|
142
156
|
totalSkipped += s.report.numPendingTests;
|
|
143
157
|
|
|
144
|
-
const
|
|
158
|
+
const ok = s.report.success;
|
|
159
|
+
const icon = ok ? `${C.green}✔${C.reset}` : `${C.red}✖${C.reset}`;
|
|
160
|
+
const countColor = ok ? C.green : C.red;
|
|
145
161
|
process.stdout.write(
|
|
146
|
-
`${icon} ${header}
|
|
162
|
+
`${icon} ${header} ${countColor}${s.report.numPassedTests}/${s.report.numTotalTests}${C.reset} ${C.dim}passed${C.reset}\n`,
|
|
147
163
|
);
|
|
148
164
|
|
|
149
165
|
for (const file of s.report.testResults) {
|
|
150
166
|
for (const a of file.assertionResults) {
|
|
151
167
|
const aIcon = assertionIcon(a.status);
|
|
152
|
-
const dur = a.duration != null ? `
|
|
168
|
+
const dur = a.duration != null ? ` ${C.gray}${formatDuration(a.duration)}${C.reset}` : "";
|
|
153
169
|
process.stdout.write(` ${aIcon} ${a.fullName}${dur}\n`);
|
|
154
170
|
if (a.status === "failed" && a.failureMessages?.length) {
|
|
155
171
|
for (const msg of a.failureMessages) {
|
|
156
172
|
const firstLine = msg.split("\n")[0] ?? msg;
|
|
157
|
-
process.stdout.write(` ${firstLine}\n`);
|
|
173
|
+
process.stdout.write(` ${C.red}${firstLine}${C.reset}\n`);
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
}
|
|
163
179
|
|
|
180
|
+
const specsPassed = summaries.filter((s) => s.exitCode === 0).length;
|
|
181
|
+
const specsFailed = summaries.filter((s) => s.exitCode !== 0).length;
|
|
164
182
|
process.stdout.write("\n");
|
|
165
183
|
process.stdout.write(
|
|
166
|
-
`
|
|
184
|
+
` ${C.bold}Specs${C.reset} ${summaries.length} ` +
|
|
185
|
+
`(${C.green}${specsPassed} passed${C.reset}, ${specsFailed > 0 ? C.red : C.dim}${specsFailed} failed${C.reset})\n`,
|
|
167
186
|
);
|
|
168
187
|
process.stdout.write(
|
|
169
|
-
`
|
|
188
|
+
` ${C.bold}Tests${C.reset} ${totalTests} ` +
|
|
189
|
+
`(${C.green}${totalPassed} passed${C.reset}, ${totalFailed > 0 ? C.red : C.dim}${totalFailed} failed${C.reset}, ${C.yellow}${totalSkipped} skipped${C.reset})\n`,
|
|
170
190
|
);
|
|
171
191
|
process.stdout.write("\n");
|
|
172
192
|
}
|
|
@@ -174,13 +194,13 @@ function printSummary(summaries: SpecRunSummary[]): void {
|
|
|
174
194
|
function assertionIcon(status: VitestAssertionResult["status"]): string {
|
|
175
195
|
switch (status) {
|
|
176
196
|
case "passed":
|
|
177
|
-
return
|
|
197
|
+
return `${C.green}✔${C.reset}`;
|
|
178
198
|
case "failed":
|
|
179
|
-
return
|
|
199
|
+
return `${C.red}✖${C.reset}`;
|
|
180
200
|
case "skipped":
|
|
181
201
|
case "pending":
|
|
182
202
|
case "todo":
|
|
183
|
-
return
|
|
203
|
+
return `${C.yellow}◌${C.reset}`;
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
|
|
@@ -2,76 +2,126 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
|
|
3
3
|
const AB = new URL(import.meta.resolve("agent-browser/bin/agent-browser.js")).pathname;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type Result = { status: number | null; stdout: string; stderr: string };
|
|
6
|
+
|
|
7
|
+
function spawnAB(args: string[]): Result {
|
|
8
|
+
const result = spawnSync(AB, args, { stdio: "pipe" });
|
|
9
|
+
return {
|
|
10
|
+
status: result.status,
|
|
11
|
+
stdout: result.stdout?.toString() ?? "",
|
|
12
|
+
stderr: result.stderr?.toString() ?? "",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function logStep(action: string, args: readonly unknown[]): void {
|
|
17
|
+
const pretty = args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ");
|
|
18
|
+
process.stdout.write(` ▶ ${action.padEnd(14)} ${pretty}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fail(summary: string, result: Result): never {
|
|
22
|
+
process.stdout.write(` ✗ ${summary}\n`);
|
|
23
|
+
const details = [result.stdout, result.stderr].map((s) => s.trim()).filter(Boolean).join("\n");
|
|
24
|
+
if (details) {
|
|
25
|
+
for (const line of details.split("\n")) {
|
|
26
|
+
process.stdout.write(` ${line}\n`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw new Error(summary);
|
|
8
30
|
}
|
|
9
31
|
|
|
10
32
|
export function ab(...args: string[]): void {
|
|
11
|
-
const
|
|
12
|
-
|
|
33
|
+
const [command = "", ...rest] = args;
|
|
34
|
+
logStep(command, rest);
|
|
35
|
+
const result = spawnAB(args);
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
fail(`agent-browser ${command} failed (exit ${result.status})`, result);
|
|
38
|
+
}
|
|
13
39
|
}
|
|
14
40
|
|
|
15
41
|
/** Wait for element/text with an explicit timeout so long-running async ops don't hang. */
|
|
16
42
|
export function abWait(selector: string, timeoutMs = 180_000): void {
|
|
43
|
+
logStep("wait", [selector]);
|
|
17
44
|
const args = selector.startsWith("text=")
|
|
18
45
|
? ["wait", "--text", selector.slice(5), "--timeout", String(timeoutMs)]
|
|
19
46
|
: ["wait", selector, "--timeout", String(timeoutMs)];
|
|
20
|
-
const
|
|
21
|
-
if (status !== 0)
|
|
47
|
+
const result = spawnAB(args);
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
fail(`wait failed: ${selector}`, result);
|
|
50
|
+
}
|
|
22
51
|
}
|
|
23
52
|
|
|
24
53
|
/** Assert stable text is visible on page (via wait --text). */
|
|
25
54
|
export function abAssertTextVisible(text: string, timeoutMs = 30_000): void {
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
logStep("assert.text", [text]);
|
|
56
|
+
const result = spawnAB(["wait", "--text", text, "--timeout", String(timeoutMs)]);
|
|
57
|
+
if (result.status !== 0) {
|
|
58
|
+
fail(`Assertion failed: text ${JSON.stringify(text)} not found within ${timeoutMs}ms`, result);
|
|
59
|
+
}
|
|
28
60
|
}
|
|
29
61
|
|
|
30
62
|
/** Assert element is visible (via wait). */
|
|
31
63
|
export function abAssertVisible(selector: string, timeoutMs = 30_000): void {
|
|
32
|
-
|
|
33
|
-
|
|
64
|
+
logStep("assert.visible", [selector]);
|
|
65
|
+
const result = spawnAB(["wait", selector, "--timeout", String(timeoutMs)]);
|
|
66
|
+
if (result.status !== 0) {
|
|
67
|
+
fail(`Assertion failed: ${JSON.stringify(selector)} not visible within ${timeoutMs}ms`, result);
|
|
68
|
+
}
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
/** Assert element is NOT visible (via wait --state hidden). */
|
|
37
72
|
export function abAssertNotVisible(selector: string, timeoutMs = 30_000): void {
|
|
73
|
+
logStep("assert.hidden", [selector]);
|
|
38
74
|
const args = selector.startsWith("text=")
|
|
39
75
|
? ["wait", "--text", selector.slice(5), "--state", "hidden", "--timeout", String(timeoutMs)]
|
|
40
76
|
: ["wait", selector, "--state", "hidden", "--timeout", String(timeoutMs)];
|
|
41
|
-
const
|
|
42
|
-
if (status !== 0)
|
|
77
|
+
const result = spawnAB(args);
|
|
78
|
+
if (result.status !== 0) {
|
|
79
|
+
fail(`Assertion failed: ${JSON.stringify(selector)} still visible after ${timeoutMs}ms`, result);
|
|
80
|
+
}
|
|
43
81
|
}
|
|
44
82
|
|
|
45
83
|
/** Assert URL contains a pattern (via get url). */
|
|
46
84
|
export function abAssertUrl(pattern: string): void {
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
logStep("assert.url", [pattern]);
|
|
86
|
+
const result = spawnAB(["get", "url"]);
|
|
87
|
+
const url = result.stdout.trim();
|
|
88
|
+
if (!url.includes(pattern)) {
|
|
89
|
+
fail(`Assertion failed: URL ${JSON.stringify(url)} does not contain ${JSON.stringify(pattern)}`, result);
|
|
90
|
+
}
|
|
49
91
|
}
|
|
50
92
|
|
|
51
93
|
/** Assert element is enabled (via is enabled). */
|
|
52
94
|
export function abAssertEnabled(selector: string): void {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
95
|
+
logStep("assert.enabled", [selector]);
|
|
96
|
+
const result = spawnAB(["is", "enabled", selector]);
|
|
97
|
+
if (result.status !== 0) fail(`Assertion failed: element ${JSON.stringify(selector)} not found`, result);
|
|
98
|
+
const value = result.stdout.trim();
|
|
99
|
+
if (value !== "true") fail(`Assertion failed: ${JSON.stringify(selector)} is not enabled (got: ${value})`, result);
|
|
56
100
|
}
|
|
57
101
|
|
|
58
102
|
/** Assert element is disabled (via is enabled). */
|
|
59
103
|
export function abAssertDisabled(selector: string): void {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
104
|
+
logStep("assert.disabled", [selector]);
|
|
105
|
+
const result = spawnAB(["is", "enabled", selector]);
|
|
106
|
+
if (result.status !== 0) fail(`Assertion failed: element ${JSON.stringify(selector)} not found`, result);
|
|
107
|
+
const value = result.stdout.trim();
|
|
108
|
+
if (value !== "false") fail(`Assertion failed: ${JSON.stringify(selector)} is not disabled (got: ${value})`, result);
|
|
63
109
|
}
|
|
64
110
|
|
|
65
111
|
/** Assert checkbox is checked (via is checked). */
|
|
66
112
|
export function abAssertChecked(selector: string): void {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
113
|
+
logStep("assert.checked", [selector]);
|
|
114
|
+
const result = spawnAB(["is", "checked", selector]);
|
|
115
|
+
if (result.status !== 0) fail(`Assertion failed: element ${JSON.stringify(selector)} not found`, result);
|
|
116
|
+
const value = result.stdout.trim();
|
|
117
|
+
if (value !== "true") fail(`Assertion failed: ${JSON.stringify(selector)} is not checked (got: ${value})`, result);
|
|
70
118
|
}
|
|
71
119
|
|
|
72
120
|
/** Assert checkbox is unchecked (via is checked). */
|
|
73
121
|
export function abAssertUnchecked(selector: string): void {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
122
|
+
logStep("assert.unchecked", [selector]);
|
|
123
|
+
const result = spawnAB(["is", "checked", selector]);
|
|
124
|
+
if (result.status !== 0) fail(`Assertion failed: element ${JSON.stringify(selector)} not found`, result);
|
|
125
|
+
const value = result.stdout.trim();
|
|
126
|
+
if (value !== "false") fail(`Assertion failed: ${JSON.stringify(selector)} is not unchecked (got: ${value})`, result);
|
|
77
127
|
}
|