ccqa 0.1.2 → 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 CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "bin": {
7
7
  "ccqa": "./bin/ccqa.ts"
8
8
  },
9
+ "exports": {
10
+ "./test-helpers": "./src/runtime/test-helpers.ts"
11
+ },
9
12
  "files": [
10
13
  "bin/",
11
14
  "src/"
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("\n────── ccqa summary ──────\n\n");
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 icon = s.exitCode === 0 ? "✓" : "✗";
135
- process.stdout.write(`${icon} ${header} (no report)\n`);
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 icon = s.report.success ? "✓" : "✗";
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} (${s.report.numPassedTests}/${s.report.numTotalTests} passed)\n`,
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 ? ` ${formatDuration(a.duration)}` : "";
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
- ` Specs ${summaries.length} (${summaries.filter((s) => s.exitCode === 0).length} passed, ${summaries.filter((s) => s.exitCode !== 0).length} failed)\n`,
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
- ` Tests ${totalTests} (${totalPassed} passed, ${totalFailed} failed, ${totalSkipped} skipped)\n`,
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
 
@@ -11,13 +11,10 @@ export interface SetupScript {
11
11
  }
12
12
 
13
13
  export function actionsToScript(actions: TraceAction[], title: string, setupScripts?: SetupScript[]): string {
14
- // Resolve the helpers path relative to this file so it works from any cwd
15
- const helpersPath = new URL("../runtime/test-helpers.ts", import.meta.url).pathname;
16
-
17
14
  const imports = [
18
15
  `import { test } from "vitest";`,
19
16
  `import { spawnSync } from "node:child_process";`,
20
- `import { ab, abWait, abAssertTextVisible, abAssertVisible, abAssertNotVisible, abAssertUrl, abAssertEnabled, abAssertDisabled, abAssertChecked, abAssertUnchecked } from ${JSON.stringify(helpersPath)};`,
17
+ `import { ab, abWait, abAssertTextVisible, abAssertVisible, abAssertNotVisible, abAssertUrl, abAssertEnabled, abAssertDisabled, abAssertChecked, abAssertUnchecked } from "ccqa/test-helpers";`,
21
18
  "",
22
19
  `// Single session shared across all tests — reset per run via cookies clear in first test`,
23
20
  `process.env.AGENT_BROWSER_SESSION = \`ccqa-run-\${Date.now()}\`;`,
@@ -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
- function spawnAB(args: string[], stdio: "inherit" | "pipe" = "inherit"): { status: number | null; stdout: string } {
6
- const result = spawnSync(AB, args, { stdio });
7
- return { status: result.status, stdout: result.stdout?.toString().trim() ?? "" };
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 { status } = spawnAB(args);
12
- if (status !== 0) throw new Error(`agent-browser ${args[0]} failed (exit ${status})`);
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 { status } = spawnAB(args);
21
- if (status !== 0) throw new Error(`agent-browser wait failed (exit ${status})`);
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
- const { status } = spawnAB(["wait", "--text", text, "--timeout", String(timeoutMs)]);
27
- if (status !== 0) throw new Error(`Assertion failed: text ${JSON.stringify(text)} not found within ${timeoutMs}ms`);
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
- const { status } = spawnAB(["wait", selector, "--timeout", String(timeoutMs)]);
33
- if (status !== 0) throw new Error(`Assertion failed: ${JSON.stringify(selector)} not visible within ${timeoutMs}ms`);
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 { status } = spawnAB(args);
42
- if (status !== 0) throw new Error(`Assertion failed: ${JSON.stringify(selector)} still visible after ${timeoutMs}ms`);
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
- const { stdout: url } = spawnAB(["get", "url"], "pipe");
48
- if (!url.includes(pattern)) throw new Error(`Assertion failed: URL ${JSON.stringify(url)} does not contain ${JSON.stringify(pattern)}`);
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
- const { status, stdout } = spawnAB(["is", "enabled", selector], "pipe");
54
- if (status !== 0) throw new Error(`Assertion failed: element ${JSON.stringify(selector)} not found`);
55
- if (stdout !== "true") throw new Error(`Assertion failed: ${JSON.stringify(selector)} is not enabled (got: ${stdout})`);
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
- const { status, stdout } = spawnAB(["is", "enabled", selector], "pipe");
61
- if (status !== 0) throw new Error(`Assertion failed: element ${JSON.stringify(selector)} not found`);
62
- if (stdout !== "false") throw new Error(`Assertion failed: ${JSON.stringify(selector)} is not disabled (got: ${stdout})`);
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
- const { status, stdout } = spawnAB(["is", "checked", selector], "pipe");
68
- if (status !== 0) throw new Error(`Assertion failed: element ${JSON.stringify(selector)} not found`);
69
- if (stdout !== "true") throw new Error(`Assertion failed: ${JSON.stringify(selector)} is not checked (got: ${stdout})`);
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
- const { status, stdout } = spawnAB(["is", "checked", selector], "pipe");
75
- if (status !== 0) throw new Error(`Assertion failed: element ${JSON.stringify(selector)} not found`);
76
- if (stdout !== "false") throw new Error(`Assertion failed: ${JSON.stringify(selector)} is not unchecked (got: ${stdout})`);
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
  }