ccqa 0.7.0 → 0.8.1
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 +102 -25
- package/dist/bin/ccqa.mjs +7587 -5403
- package/dist/package.json +1 -1
- package/dist/runtime/test-helpers.d.mts +11 -1
- package/dist/runtime/test-helpers.mjs +120 -2
- package/dist/{spawn-ab-DjRh1-4T.mjs → spawn-ab-Ja8NRRab.mjs} +14 -1
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -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-
|
|
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 };
|