@crouton-kit/capture 1.3.19 → 1.3.21

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.
@@ -199,28 +199,30 @@ export class SnapshotCaptureTimeout extends Error {
199
199
  }
200
200
 
201
201
  /** Bounds one snapshot CDP request and labels the phase that owns it. */
202
- function boundedClient(client: CDPClient, phase: string, partialPath: string, onTimeout?: () => void): CDPClient {
202
+ function boundedClient(client: CDPClient, phase: string, partialPath: string, onTimeout?: (error: SnapshotCaptureTimeout) => void): CDPClient {
203
203
  const bounded = Object.create(client) as CDPClient;
204
204
  bounded.send = (method: string, params: Record<string, unknown> = {}, timeout = 60_000, sessionId?: string): Promise<unknown> => {
205
205
  const timeoutMs = Math.min(timeout, SNAPSHOT_REQUEST_TIMEOUT_MS);
206
206
  let timer: NodeJS.Timeout | undefined;
207
207
  let timedOut = false;
208
- const markTimedOut = (): void => {
208
+ const markTimedOut = (error: SnapshotCaptureTimeout): void => {
209
209
  if (timedOut) return;
210
210
  timedOut = true;
211
- onTimeout?.();
211
+ onTimeout?.(error);
212
212
  };
213
213
  const deadline = new Promise<never>((_, reject) => {
214
214
  timer = setTimeout(() => {
215
- markTimedOut();
216
- reject(new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath));
215
+ const error = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
216
+ markTimedOut(error);
217
+ reject(error);
217
218
  }, timeoutMs);
218
219
  });
219
220
  const request = Promise.resolve().then(() => client.send(method, params, timeoutMs, sessionId)).catch((error: unknown) => {
220
221
  if (error instanceof SnapshotCaptureTimeout) throw error;
221
222
  if (error instanceof Error && error.message.startsWith('CDP request timeout')) {
222
- markTimedOut();
223
- throw new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
223
+ const timeoutError = new SnapshotCaptureTimeout(phase, method, timeoutMs, partialPath);
224
+ markTimedOut(timeoutError);
225
+ throw timeoutError;
224
226
  }
225
227
  throw error;
226
228
  });
@@ -250,6 +252,7 @@ export async function captureSnapshotSubstrate(options: CaptureSnapshotOptions):
250
252
  const pixels = options.pixels ?? false;
251
253
  const state = options.state ?? [];
252
254
  const viewport = options.viewport ?? null;
255
+ const colorScheme = options.colorScheme;
253
256
  // Apply the shared artifact-string cap once at the source. URL contents
254
257
  // otherwise survive unchanged in meta.json, the result, and collector context.
255
258
  const url = options.url ? sanitizeString(options.url) : null;
@@ -386,25 +389,34 @@ export async function captureSnapshotSubstrate(options: CaptureSnapshotOptions):
386
389
 
387
390
  const startedAt = Date.now();
388
391
  let requestTimeouts = 0;
392
+ let timeoutError: SnapshotCaptureTimeout | undefined;
389
393
  const collectorWriter = makeCollectorWriter(ctx.write);
390
394
  try {
391
- // Await the collector itself, not a wall-clock race around it. Each CDP
392
- // request is still bounded at five seconds; awaiting the collector keeps
393
- // any cleanup and pending browser work quiescent before the next shared-
394
- // connection collector starts.
395
+ // Await the collector itself, not a wall-clock race around it. The
396
+ // collector owns its cleanup, and timeout handling below decides whether
397
+ // this capture can safely move on to another shared-connection collector.
395
398
  await collector.fn({
396
399
  ...ctx,
397
- client: boundedClient(client, `collector:${collector.name}`, dir, () => { requestTimeouts += 1; }),
400
+ client: boundedClient(client, `collector:${collector.name}`, dir, (error) => {
401
+ requestTimeouts += 1;
402
+ timeoutError ??= error;
403
+ }),
398
404
  write: collectorWriter.writer,
399
405
  });
406
+ // A styles timeout may be caught per-record by the provenance collector,
407
+ // but its browser operation can still belong to this shared connection.
408
+ // End this capture instead of letting another collector issue commands
409
+ // under that outstanding operation.
410
+ if (collector.name === 'styles' && timeoutError) throw timeoutError;
400
411
  const ms = Date.now() - startedAt;
401
412
  collectorWriter.commit();
402
413
  return requestTimeouts > 0
403
414
  ? { name: collector.name, status: 'degraded', ms, requestTimeouts }
404
415
  : { name: collector.name, status: 'ok', ms };
405
- } catch {
416
+ } catch (error) {
406
417
  const ms = Date.now() - startedAt;
407
418
  collectorWriter.discard();
419
+ if (collector.name === 'styles' && error instanceof SnapshotCaptureTimeout) throw error;
408
420
  return { name: collector.name, status: 'error', ms, ...(requestTimeouts > 0 ? { requestTimeouts } : {}) };
409
421
  }
410
422
  };
@@ -483,6 +495,7 @@ export async function captureSnapshotSubstrate(options: CaptureSnapshotOptions):
483
495
  captureUnsettled,
484
496
  pixels,
485
497
  states: state.map((spec) => sanitizeString(spec)),
498
+ ...(colorScheme ? { colorScheme } : {}),
486
499
  unstableRegionCount: unstableRegions.length,
487
500
  ...(freezeAnimations ? { animationsRestored: animationsRestored ?? false } : {}),
488
501
  // #8: the browser-wide `Animation.setPlaybackRate({playbackRate:0})`
@@ -53,6 +53,8 @@ export interface CaptureSnapshotOptions {
53
53
  /** `--state <state[:selector]>` (repeatable); default `[]`. */
54
54
  readonly state?: readonly string[];
55
55
  readonly viewport?: string | null;
56
+ /** Transient `prefers-color-scheme` media override used while capturing. */
57
+ readonly colorScheme?: 'dark' | 'light';
56
58
  /**
57
59
  * Poll interval for the settledness loop; default {@link DEFAULT_POLL_INTERVAL_MS}
58
60
  * (100ms). Not part of the CLI surface — a test-only override so the
@@ -109,6 +111,8 @@ export interface SnapshotMeta extends SnapMeta {
109
111
  readonly captureUnsettled: boolean;
110
112
  readonly pixels: boolean;
111
113
  readonly states: readonly string[];
114
+ /** Present only when this snapshot applied a transient color-scheme override. */
115
+ readonly colorScheme?: 'dark' | 'light';
112
116
  readonly unstableRegionCount: number;
113
117
  /** Present only when `freezeAnimations` is true — whether every animation `freezeAnimationsBeforeCapture` paused was confirmed resumed to its pre-freeze `playState` (I-6). `false` means restoration could not be guaranteed (origin capture failed, or the restore call itself threw) — never omitted to imply a clean restore. */
114
118
  readonly animationsRestored?: boolean;
package/src/cdp/types.ts CHANGED
@@ -38,6 +38,8 @@ export interface ParsedArgs {
38
38
  into?: string;
39
39
  noScreenshot?: boolean;
40
40
  viewport?: string;
41
+ /** `page shot|measure snap --color-scheme dark|light` — transient media override. */
42
+ colorScheme?: string;
41
43
  fullPage?: boolean;
42
44
  help?: boolean;
43
45
  /** `--all` — boolean scope-widener (per-leaf semantics). */