ccqa 0.7.0 → 0.8.0

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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -1,4 +1,5 @@
1
1
  //#region src/runtime/test-helpers.d.ts
2
+ declare function __setCurrentStep(stepId: string, source: string): void;
2
3
  declare function ab(...args: string[]): void;
3
4
  /** Wait for element/text with an explicit timeout so long-running async ops don't hang. */
4
5
  declare function abWait(selector: string, timeoutMs?: number): void;
@@ -18,5 +19,14 @@ declare function abAssertDisabled(selector: string): void;
18
19
  declare function abAssertChecked(selector: string): void;
19
20
  /** Assert checkbox is unchecked (via is checked). */
20
21
  declare function abAssertUnchecked(selector: string): void;
22
+ /**
23
+ * Capture a step-boundary evidence pair (PNG + JSON metadata) so a reviewer
24
+ * can confirm at a glance that a passing spec actually drove the app to the
25
+ * state its `expected` describes. Opt-in at runtime via `CCQA_EVIDENCE_DIR` so
26
+ * generated scripts hand-run outside `ccqa run` don't write stray files. All
27
+ * errors are swallowed with a stderr warning — evidence capture must never
28
+ * flip a passing spec to red.
29
+ */
30
+ declare function abStepEvidence(stepId: string, source: string): void;
21
31
  //#endregion
22
- export { ab, abAssertChecked, abAssertDisabled, abAssertEnabled, abAssertNotVisible, abAssertTextVisible, abAssertUnchecked, abAssertUrl, abAssertVisible, abWait };
32
+ export { __setCurrentStep, ab, abAssertChecked, abAssertDisabled, abAssertEnabled, abAssertNotVisible, abAssertTextVisible, abAssertUnchecked, abAssertUrl, abAssertVisible, abStepEvidence, abWait };
@@ -1,4 +1,6 @@
1
- import { n as spawnAB, t as sleepSync } from "../spawn-ab-DjRh1-4T.mjs";
1
+ import { i as FAILURE_STEP_ID, n as spawnAB, r as FAILURE_SOURCE, t as sleepSync } from "../spawn-ab-Ja8NRRab.mjs";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
2
4
  //#region src/runtime/test-helpers.ts
3
5
  const POST_OPEN_SETTLE_MS = 600;
4
6
  function logStep(action, args) {
@@ -9,8 +11,43 @@ function fail(summary, result) {
9
11
  process.stdout.write(` ✗ ${summary}\n`);
10
12
  const details = [result.stdout, result.stderr].map((s) => s.trim()).filter(Boolean).join("\n");
11
13
  if (details) for (const line of details.split("\n")) process.stdout.write(` ${line}\n`);
14
+ captureFailureEvidence(summary);
12
15
  throw new Error(summary);
13
16
  }
17
+ /**
18
+ * Tracks the step the test is currently inside. The codegen emits one of these
19
+ * calls right after every `// step: ...` marker so when fail() fires we know
20
+ * which step to attribute the failure to. Older generated scripts that don't
21
+ * emit this still work — captureFailureEvidence() falls back to a generic
22
+ * `failure.png` when currentStep is null.
23
+ */
24
+ let currentStep = null;
25
+ function __setCurrentStep(stepId, source) {
26
+ currentStep = {
27
+ stepId,
28
+ source
29
+ };
30
+ }
31
+ function captureFailureEvidence(summary) {
32
+ if (currentStep) {
33
+ const safe = currentStep.stepId.replace(/[^A-Za-z0-9_.-]/g, "_");
34
+ captureEvidence({
35
+ stepId: currentStep.stepId,
36
+ source: currentStep.source,
37
+ pngFile: `${safe}.png`,
38
+ failureSummary: summary,
39
+ silent: true
40
+ });
41
+ return;
42
+ }
43
+ captureEvidence({
44
+ stepId: FAILURE_STEP_ID,
45
+ source: FAILURE_SOURCE,
46
+ pngFile: "failure.png",
47
+ failureSummary: summary,
48
+ silent: true
49
+ });
50
+ }
14
51
  function ab(...args) {
15
52
  const [command = "", ...rest] = args;
16
53
  logStep(command, rest);
@@ -170,5 +207,86 @@ function abAssertUnchecked(selector) {
170
207
  const value = result.stdout.trim();
171
208
  if (value !== "false") fail(`Assertion failed: ${JSON.stringify(selector)} is not unchecked (got: ${value})`, result);
172
209
  }
210
+ /**
211
+ * Capture a step-boundary evidence pair (PNG + JSON metadata) so a reviewer
212
+ * can confirm at a glance that a passing spec actually drove the app to the
213
+ * state its `expected` describes. Opt-in at runtime via `CCQA_EVIDENCE_DIR` so
214
+ * generated scripts hand-run outside `ccqa run` don't write stray files. All
215
+ * errors are swallowed with a stderr warning — evidence capture must never
216
+ * flip a passing spec to red.
217
+ */
218
+ function abStepEvidence(stepId, source) {
219
+ captureEvidence({
220
+ stepId,
221
+ source,
222
+ pngFile: `${stepId.replace(/[^A-Za-z0-9_.-]/g, "_")}.png`
223
+ });
224
+ if (currentStep && currentStep.stepId === stepId) currentStep = null;
225
+ }
226
+ /**
227
+ * Shared screenshot+meta pipeline behind both abStepEvidence (step boundary)
228
+ * and captureFailureEvidence (called from fail()). The url/title eval is one
229
+ * round-trip; agent-browser wraps eval output in JSON.stringify, so the JS
230
+ * expression must itself stringify the payload — hence the double JSON.parse.
231
+ */
232
+ function captureEvidence(opts) {
233
+ const dir = process.env["CCQA_EVIDENCE_DIR"];
234
+ if (!dir) return;
235
+ const { stepId, source, pngFile, failureSummary, silent } = opts;
236
+ const pngPath = join(dir, pngFile);
237
+ const metaPath = join(dir, pngFile.replace(/\.png$/, ".json"));
238
+ try {
239
+ mkdirSync(dirname(pngPath), { recursive: true });
240
+ } catch (e) {
241
+ if (!silent) warnEvidence(`mkdir failed (${e.message})`);
242
+ return;
243
+ }
244
+ if (!silent) logStep("evidence", [stepId]);
245
+ const shot = spawnAB(["screenshot", pngPath]);
246
+ if (shot.status !== 0) {
247
+ if (!silent) warnEvidence(`screenshot failed for ${stepId} (${shot.stderr.trim() || shot.stdout.trim()})`);
248
+ return;
249
+ }
250
+ const { url, title } = readPageContext();
251
+ const meta = {
252
+ stepId,
253
+ source,
254
+ url,
255
+ title,
256
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
257
+ pngFile
258
+ };
259
+ if (failureSummary !== void 0) meta["failureSummary"] = failureSummary;
260
+ try {
261
+ writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}\n`, "utf8");
262
+ } catch (e) {
263
+ if (!silent) warnEvidence(`meta write failed (${e.message})`);
264
+ }
265
+ }
266
+ function readPageContext() {
267
+ const ctx = spawnAB(["eval", "JSON.stringify({url: location.href, title: document.title})"]);
268
+ if (ctx.status !== 0) return {
269
+ url: null,
270
+ title: null
271
+ };
272
+ try {
273
+ const outer = JSON.parse(ctx.stdout.trim());
274
+ const inner = typeof outer === "string" ? JSON.parse(outer) : outer;
275
+ if (inner && typeof inner === "object") {
276
+ const obj = inner;
277
+ return {
278
+ url: typeof obj.url === "string" ? obj.url : null,
279
+ title: typeof obj.title === "string" ? obj.title : null
280
+ };
281
+ }
282
+ } catch {}
283
+ return {
284
+ url: null,
285
+ title: null
286
+ };
287
+ }
288
+ function warnEvidence(msg) {
289
+ process.stderr.write(`[ccqa] evidence: ${msg}\n`);
290
+ }
173
291
  //#endregion
174
- export { ab, abAssertChecked, abAssertDisabled, abAssertEnabled, abAssertNotVisible, abAssertTextVisible, abAssertUnchecked, abAssertUrl, abAssertVisible, abWait };
292
+ export { __setCurrentStep, ab, abAssertChecked, abAssertDisabled, abAssertEnabled, abAssertNotVisible, abAssertTextVisible, abAssertUnchecked, abAssertUrl, abAssertVisible, abStepEvidence, abWait };
@@ -1,5 +1,18 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { spawnSync } from "node:child_process";
3
+ //#region src/runtime/evidence-constants.ts
4
+ /**
5
+ * Shared constants for step-boundary evidence captured by abStepEvidence() /
6
+ * captureFailureEvidence() and consumed by the run report. Kept under
7
+ * `runtime/` so the test-helpers module — which generated test scripts import
8
+ * via `ccqa/test-helpers` — can stay free of CLI-side imports while still
9
+ * sharing the literal with run.ts.
10
+ */
11
+ /** stepId reserved for the screenshot captured by fail() at the moment of an assertion failure. */
12
+ const FAILURE_STEP_ID = "failure";
13
+ /** source value paired with FAILURE_STEP_ID so the report can tell failure captures apart from step captures. */
14
+ const FAILURE_SOURCE = "failed";
15
+ //#endregion
3
16
  //#region src/runtime/spawn-ab.ts
4
17
  const AB = createRequire(import.meta.url).resolve("agent-browser/bin/agent-browser.js");
5
18
  const EAGAIN_PATTERN = /Resource temporarily unavailable|os error 35/i;
@@ -62,4 +75,4 @@ function spawnAB(args) {
62
75
  return result;
63
76
  }
64
77
  //#endregion
65
- export { spawnAB as n, sleepSync as t };
78
+ export { FAILURE_STEP_ID as i, spawnAB as n, FAILURE_SOURCE as r, sleepSync as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {