ccqa 1.57.0 → 1.57.2

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/bin/ccqa.mjs CHANGED
@@ -8200,52 +8200,15 @@ function exportedNames(source) {
8200
8200
  function reviewEmittedFiles(input) {
8201
8201
  const locators = collectLocators(input.files);
8202
8202
  const said = input.caseText.join("\n");
8203
- return [
8204
- ...decidesNothing(input),
8205
- ...unassertedPath(input),
8206
- ...[...input.files].flatMap(([file, source]) => [
8207
- ...toolIdentifiers(file, source),
8208
- ...containerOfPageText(file, source),
8209
- ...unjustifiedFirst(file, source),
8210
- ...describeEcho(file, source),
8211
- ...unaskedAssertions(file, source, locators, said),
8212
- ...weakerTwin(file, source),
8213
- ...unreached(file, source, input)
8214
- ])
8215
- ];
8216
- }
8217
- /**
8218
- * A path with a placeholder segment in it — `/orders/{orderId}`. Writing one
8219
- * in prose is a statement about the address, not about the page's contents:
8220
- * there is no other reason to name the part that changes per run.
8221
- */
8222
- const PLACEHOLDER_PATH = /\/[A-Za-z0-9_\-]+(?:\/[A-Za-z0-9_\-]+)*\/\{[A-Za-z0-9_]+\}/;
8223
- /**
8224
- * A case that says where the run must end up, and a test that never looks at
8225
- * the address.
8226
- *
8227
- * The row being on screen is not the same claim as being on the screen that
8228
- * shows it — a product that rendered the row without navigating passes, and
8229
- * that is the failure the expectation was written to catch. Scoped to
8230
- * placeholder paths because those are unambiguous; a path mentioned as scenery
8231
- * ("on /settings, the button is shown") is not a claim about the address.
8232
- *
8233
- * Calibrated against a small set of hand-written case definitions: a few
8234
- * name a placeholder path in their expectations, most of those already
8235
- * assert on the URL, and the one that does not is the defect this was
8236
- * written for.
8237
- */
8238
- function unassertedPath(input) {
8239
- const source = input.files.get(input.testPath);
8240
- if (source === void 0) return [];
8241
- const stated = input.caseText.find((text) => PLACEHOLDER_PATH.test(text));
8242
- if (stated === void 0 || /\btoHaveURL\b/.test(source)) return [];
8243
- return [{
8244
- file: input.testPath,
8245
- line: 1,
8246
- rule: "unasserted-path",
8247
- message: `the case says where the run ends up — "${PLACEHOLDER_PATH.exec(stated)?.[0]}" — and nothing here looks at the address. What the screen shows can be right while the screen is wrong. Assert the URL where the case says it changes`
8248
- }];
8203
+ return [...decidesNothing(input), ...[...input.files].flatMap(([file, source]) => [
8204
+ ...toolIdentifiers(file, source),
8205
+ ...containerOfPageText(file, source),
8206
+ ...unjustifiedFirst(file, source),
8207
+ ...describeEcho(file, source),
8208
+ ...unaskedAssertions(file, source, locators, said),
8209
+ ...weakerTwin(file, source),
8210
+ ...unreached(file, source, input)
8211
+ ])];
8249
8212
  }
8250
8213
  /** `readonly name =`, `name(...): Locator {`, and `export … name`. */
8251
8214
  const DECLARED = /(?:readonly\s+([A-Za-z0-9_]+)\s*=|\b([A-Za-z0-9_]+)\([^)]*\)\s*:\s*Locator\b|export\s+(?:async\s+)?(?:const|let|function|class)\s+([A-Za-z0-9_]+))/g;
@@ -13914,6 +13877,7 @@ function parseTraceEvents(jsonl) {
13914
13877
  const frames = [];
13915
13878
  const steps = [];
13916
13879
  const pendingByCallId = /* @__PURE__ */ new Map();
13880
+ const wallOffsets = /* @__PURE__ */ new Map();
13917
13881
  let maxTimestamp = -Infinity;
13918
13882
  for (const line of jsonl.split("\n")) {
13919
13883
  if (line.trim() === "") continue;
@@ -13937,8 +13901,13 @@ function parseTraceEvents(jsonl) {
13937
13901
  if (type === "screencast-frame") {
13938
13902
  if (typeof event.sha1 === "string" && typeof event.timestamp === "number") frames.push({
13939
13903
  sha1: event.sha1,
13940
- timestamp: event.timestamp
13904
+ timestamp: event.timestamp,
13905
+ ...typeof event.frameSwapWallTime === "number" ? { swapWallTime: event.frameSwapWallTime } : {}
13941
13906
  });
13907
+ } else if (type === "context-options") {
13908
+ if (typeof event.wallTime === "number" && typeof event.monotonicTime === "number") {
13909
+ if (!wallOffsets.has(event.origin)) wallOffsets.set(event.origin, event.wallTime - event.monotonicTime);
13910
+ }
13942
13911
  } else if (type === "action") {
13943
13912
  if (typeof event.callId === "string" && typeof event.startTime === "number" && typeof event.endTime === "number") steps.push({
13944
13913
  title: titleOf$1(event),
@@ -13969,10 +13938,16 @@ function parseTraceEvents(jsonl) {
13969
13938
  ...pending,
13970
13939
  endTime: maxTimestamp
13971
13940
  });
13972
- frames.sort((a, b) => a.timestamp - b.timestamp);
13941
+ const offset = wallOffsets.get("testRunner") ?? wallOffsets.values().next().value;
13942
+ const painted = offset !== void 0 && frames.every((f) => f.swapWallTime !== void 0);
13943
+ const placed = frames.map(({ sha1, timestamp, swapWallTime }) => ({
13944
+ sha1,
13945
+ timestamp: painted ? swapWallTime - offset : timestamp
13946
+ }));
13947
+ placed.sort((a, b) => a.timestamp - b.timestamp);
13973
13948
  steps.sort((a, b) => a.startTime - b.startTime);
13974
13949
  return {
13975
- frames,
13950
+ frames: placed,
13976
13951
  steps
13977
13952
  };
13978
13953
  }
@@ -13992,13 +13967,22 @@ function titleOf$1(event) {
13992
13967
  return "";
13993
13968
  }
13994
13969
  /**
13995
- * The frame on screen at `time`: the last one captured at or before it, or —
13996
- * when the step began before any frame was captured — the first frame after.
13997
- * Null when the trace holds no frames at all. `frames` comes from
13998
- * {@link parseTraceEvents}, which sorts them by timestamp.
13970
+ * One paint interval at 60fps. An assertion passes on the DOM, so a step can
13971
+ * end before the frame showing that DOM is painted — at most one interval later.
13972
+ */
13973
+ const ONE_FRAME_MS = 17;
13974
+ /**
13975
+ * The frame showing the page as it was at `time`: the last one painted within
13976
+ * a frame interval of it, or — when the step began before any frame was
13977
+ * captured — the first frame after. Null when the trace holds no frames at
13978
+ * all. `frames` comes from {@link parseTraceEvents}, sorted by paint time.
13979
+ *
13980
+ * A step's start takes the same allowance as its end: it is the same boundary
13981
+ * as the previous step's end, and would otherwise show that step unfinished.
13999
13982
  */
14000
13983
  function frameAt(frames, time) {
14001
- return frames.findLast((f) => f.timestamp <= time) ?? frames[0] ?? null;
13984
+ const painted = time + ONE_FRAME_MS;
13985
+ return frames.findLast((f) => f.timestamp <= painted) ?? frames[0] ?? null;
14002
13986
  }
14003
13987
  //#endregion
14004
13988
  //#region src/targets/playwright/trace-capture.ts
@@ -27368,7 +27352,7 @@ const evidenceCommand = addLanguageOption(new Command("evidence").argument("<cas
27368
27352
  anchors = await findSourceAnchors(sourceNeedles([...recording.actions, ...recording.cleanup ?? []]), roots);
27369
27353
  }
27370
27354
  const out = resolve(cwd, opts.out ?? join(testCase.ref.dir, "evidence.md"));
27371
- await writeFile(out, scrubEnvValues(renderEvidence({
27355
+ const markdown = renderEvidence({
27372
27356
  testCase,
27373
27357
  recording,
27374
27358
  test: {
@@ -27381,7 +27365,9 @@ const evidenceCommand = addLanguageOption(new Command("evidence").argument("<cas
27381
27365
  ...resolved.targetConfig.allowExpectInCleanup ? {} : { cleanupUnchecked: testCase.cleanupExpectations },
27382
27366
  ...review ? { review } : {},
27383
27367
  ...anchors ? { anchors } : {}
27384
- }), buildProseEnvScrubMap([], [])), "utf8");
27368
+ });
27369
+ await mkdir(dirname(out), { recursive: true });
27370
+ await writeFile(out, scrubEnvValues(markdown, buildProseEnvScrubMap([], [])), "utf8");
27385
27371
  process.stdout.write(`${relative(cwd, out) || out}\n`);
27386
27372
  hint("paste it into the pull request, or attach it — ccqa writes the table, not the body");
27387
27373
  }));
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
35
35
  running: "running";
36
36
  }>;
37
37
  kind: z.ZodDefault<z.ZodEnum<{
38
+ record: "record";
38
39
  run: "run";
39
40
  drift: "drift";
40
- record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
831
831
  declare const RunReportDataSchema: z.ZodObject<{
832
832
  schemaVersion: z.ZodLiteral<1>;
833
833
  kind: z.ZodDefault<z.ZodEnum<{
834
+ record: "record";
834
835
  run: "run";
835
836
  drift: "drift";
836
- record: "record";
837
837
  }>>;
838
838
  createdAt: z.ZodString;
839
839
  runId: z.ZodNullable<z.ZodString>;
@@ -35,9 +35,9 @@ declare const RunSchema: z.ZodObject<{
35
35
  running: "running";
36
36
  }>;
37
37
  kind: z.ZodDefault<z.ZodEnum<{
38
+ record: "record";
38
39
  run: "run";
39
40
  drift: "drift";
40
- record: "record";
41
41
  }>>;
42
42
  drift: z.ZodDefault<z.ZodNullable<z.ZodObject<{
43
43
  specs: z.ZodNumber;
@@ -831,9 +831,9 @@ type ReportSpecResult = z.infer<typeof ReportSpecResultSchema>;
831
831
  declare const RunReportDataSchema: z.ZodObject<{
832
832
  schemaVersion: z.ZodLiteral<1>;
833
833
  kind: z.ZodDefault<z.ZodEnum<{
834
+ record: "record";
834
835
  run: "run";
835
836
  drift: "drift";
836
- record: "record";
837
837
  }>>;
838
838
  createdAt: z.ZodString;
839
839
  runId: z.ZodNullable<z.ZodString>;
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.57.0",
3
+ "version": "1.57.2",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.57.0",
3
+ "version": "1.57.2",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {