@afixt/screenshot-utils 0.9.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +13 -0
  3. package/README.md +132 -0
  4. package/bin/afixt-screenshot +14 -0
  5. package/dist/index.js +2797 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +2795 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +164 -0
  10. package/src/adapters/cdp.js +116 -0
  11. package/src/adapters/index.js +17 -0
  12. package/src/adapters/playwright.js +150 -0
  13. package/src/adapters/puppeteer.js +128 -0
  14. package/src/capture/element.js +257 -0
  15. package/src/capture/index.js +17 -0
  16. package/src/capture/long-page.js +267 -0
  17. package/src/capture/page.js +207 -0
  18. package/src/capture/responsive.js +94 -0
  19. package/src/capture/settle.js +182 -0
  20. package/src/capture/viewport.js +21 -0
  21. package/src/cli/index.js +266 -0
  22. package/src/compose/annotate.js +283 -0
  23. package/src/compose/diff.js +173 -0
  24. package/src/compose/heatmap.js +159 -0
  25. package/src/compose/index.js +8 -0
  26. package/src/compose/theme.js +45 -0
  27. package/src/errors/adapter.js +65 -0
  28. package/src/errors/base.js +48 -0
  29. package/src/errors/capture.js +175 -0
  30. package/src/errors/compose.js +78 -0
  31. package/src/errors/index.js +75 -0
  32. package/src/errors/optional.js +37 -0
  33. package/src/errors/redaction.js +42 -0
  34. package/src/errors/tile.js +61 -0
  35. package/src/errors/transform.js +78 -0
  36. package/src/index.js +69 -0
  37. package/src/redact/index.js +6 -0
  38. package/src/redact/policy.js +67 -0
  39. package/src/redact/redact.js +268 -0
  40. package/src/tile/dzi.js +268 -0
  41. package/src/tile/index.js +5 -0
  42. package/src/transform/convert.js +54 -0
  43. package/src/transform/crop.js +251 -0
  44. package/src/transform/index.js +8 -0
  45. package/src/transform/normalize.js +83 -0
  46. package/src/transform/resize.js +99 -0
  47. package/src/types/index.d.ts +432 -0
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ BufferCorruptError,
5
+ DimensionMismatchError,
6
+ OptionalDependencyMissingError,
7
+ } = require('../errors/index.js');
8
+
9
+ /**
10
+ * Perceptual visual diff between two image buffers.
11
+ *
12
+ * Implements `spec.md` § 8.3.4. Requires the optional peer
13
+ * `pixelmatch` (≥ v7); throws `OptionalDependencyMissingError` if it
14
+ * isn't installed.
15
+ *
16
+ * @public
17
+ * @param {Buffer} bufferA
18
+ * @param {Buffer} bufferB
19
+ * @param {import('../types/index.d.ts').DiffOptions} [options]
20
+ * @returns {Promise<import('../types/index.d.ts').DiffResult>}
21
+ */
22
+ async function diff(bufferA, bufferB, options = {}) {
23
+ assertBuffer(bufferA, 'bufferA');
24
+ assertBuffer(bufferB, 'bufferB');
25
+ const {
26
+ threshold = 0.1,
27
+ includeAA = false,
28
+ alpha = 0.5,
29
+ diffColor = [255, 0, 0],
30
+ output = 'overlay',
31
+ } = options;
32
+ if (!['overlay', 'side-by-side', 'mask-only'].includes(output)) {
33
+ throw new DimensionMismatchError(
34
+ `diff output must be 'overlay', 'side-by-side', or 'mask-only'; received ${output}`,
35
+ { context: { output } }
36
+ );
37
+ }
38
+
39
+ const pixelmatch = await loadPixelmatch();
40
+ const sharp = require('sharp');
41
+
42
+ const a = await rawRgba(sharp, bufferA);
43
+ const b = await rawRgba(sharp, bufferB);
44
+ if (a.width !== b.width || a.height !== b.height) {
45
+ throw new DimensionMismatchError(
46
+ `diff requires equal dimensions: bufferA is ${a.width}x${a.height}, bufferB is ${b.width}x${b.height}`,
47
+ {
48
+ context: {
49
+ a: { width: a.width, height: a.height },
50
+ b: { width: b.width, height: b.height },
51
+ },
52
+ }
53
+ );
54
+ }
55
+ const totalPixels = a.width * a.height;
56
+
57
+ const diffData = Buffer.alloc(a.data.length);
58
+ const changedPixels = pixelmatch(a.data, b.data, diffData, a.width, a.height, {
59
+ threshold,
60
+ includeAA,
61
+ alpha,
62
+ diffColor,
63
+ });
64
+
65
+ const diffBuffer = await renderOutput(sharp, output, {
66
+ a,
67
+ b,
68
+ diffData,
69
+ width: a.width,
70
+ height: a.height,
71
+ });
72
+
73
+ return {
74
+ diffBuffer,
75
+ changedPixels,
76
+ totalPixels,
77
+ percentChanged: totalPixels === 0 ? 0 : (changedPixels / totalPixels) * 100,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Dynamic-import pixelmatch so the package can be installed without it.
83
+ *
84
+ * @returns {Promise<Function>}
85
+ */
86
+ async function loadPixelmatch() {
87
+ try {
88
+ // pixelmatch v7 is ESM-only. dynamic import returns the module
89
+ // namespace; default export is the function.
90
+ const mod = await import('pixelmatch');
91
+ return mod.default ?? mod;
92
+ } catch (error) {
93
+ throw new OptionalDependencyMissingError(
94
+ 'diff() requires the optional peer dependency `pixelmatch`. Install it before calling diff().',
95
+ { packageName: 'pixelmatch', cause: error }
96
+ );
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Decode a buffer to raw RGBA pixel data via sharp.
102
+ *
103
+ * @param {ReturnType<typeof require>} sharp
104
+ * @param {Buffer} buffer
105
+ * @returns {Promise<{data: Buffer, width: number, height: number}>}
106
+ */
107
+ async function rawRgba(sharp, buffer) {
108
+ try {
109
+ const { data, info } = await sharp(buffer)
110
+ .ensureAlpha()
111
+ .raw()
112
+ .toBuffer({ resolveWithObject: true });
113
+ return { data, width: info.width, height: info.height };
114
+ } catch (error) {
115
+ throw new BufferCorruptError('sharp could not decode diff input buffer', {
116
+ cause: error,
117
+ });
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Render the requested diff output mode.
123
+ *
124
+ * @param {ReturnType<typeof require>} sharp
125
+ * @param {string} mode 'overlay' | 'side-by-side' | 'mask-only'
126
+ * @param {{a: {data: Buffer, width: number, height: number}, b: {data: Buffer, width: number, height: number}, diffData: Buffer, width: number, height: number}} ctx
127
+ * @returns {Promise<Buffer>}
128
+ */
129
+ async function renderOutput(sharp, mode, ctx) {
130
+ const diffPng = await sharp(ctx.diffData, {
131
+ raw: { width: ctx.width, height: ctx.height, channels: 4 },
132
+ })
133
+ .png()
134
+ .toBuffer();
135
+
136
+ if (mode === 'mask-only') {
137
+ return diffPng;
138
+ }
139
+ if (mode === 'side-by-side') {
140
+ const bImage = await sharp(ctx.b.data, {
141
+ raw: { width: ctx.b.width, height: ctx.b.height, channels: 4 },
142
+ })
143
+ .png()
144
+ .toBuffer();
145
+ return sharp({
146
+ create: {
147
+ width: ctx.width * 2,
148
+ height: ctx.height,
149
+ channels: 4,
150
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
151
+ },
152
+ })
153
+ .composite([
154
+ { input: bImage, top: 0, left: 0 },
155
+ { input: diffPng, top: 0, left: ctx.width },
156
+ ])
157
+ .png()
158
+ .toBuffer();
159
+ }
160
+ // 'overlay' — the diff image already encodes the overlay because
161
+ // pixelmatch writes the b image with changed pixels highlighted.
162
+ return diffPng;
163
+ }
164
+
165
+ function assertBuffer(buffer, name) {
166
+ if (!Buffer.isBuffer(buffer)) {
167
+ throw new BufferCorruptError(`${name} must be a Buffer`, {
168
+ context: { received: typeof buffer, name },
169
+ });
170
+ }
171
+ }
172
+
173
+ module.exports = { diff };
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ const { BufferCorruptError, ComposeError, HeatmapPointError } = require('../errors/index.js');
4
+
5
+ const MAX_POINTS = 100_000;
6
+ const VALID_BLEND_MODES = new Set(['overlay', 'screen', 'multiply']);
7
+
8
+ /**
9
+ * Render a kernel-density heatmap over a screenshot.
10
+ *
11
+ * Implements `spec.md` § 8.3.3 with a simple SVG-circles + blur approach.
12
+ * Each point gets a circle whose radius scales with weight; the
13
+ * collection is then blurred to produce a smooth heatmap surface and
14
+ * composited onto the source using the requested blend mode.
15
+ *
16
+ * @public
17
+ * @param {Buffer} input
18
+ * @param {Array<import('../types/index.d.ts').HeatmapPoint>} points
19
+ * @param {import('../types/index.d.ts').HeatmapOptions} [options]
20
+ * @returns {Promise<Buffer>}
21
+ */
22
+ async function heatmap(input, points, options = {}) {
23
+ assertBuffer(input);
24
+ if (!Array.isArray(points)) {
25
+ throw new HeatmapPointError('points must be an array', {
26
+ context: { received: typeof points },
27
+ });
28
+ }
29
+ if (points.length > MAX_POINTS) {
30
+ throw new ComposeError(
31
+ `Cannot render a heatmap with more than ${MAX_POINTS} points in a single call`,
32
+ { context: { received: points.length, max: MAX_POINTS } }
33
+ );
34
+ }
35
+ const { radius = 40, intensity = 0.7, colorScale = 'viridis', blendMode = 'overlay' } = options;
36
+ if (!VALID_BLEND_MODES.has(blendMode)) {
37
+ throw new ComposeError(
38
+ `blendMode must be one of ${[...VALID_BLEND_MODES].join(', ')}; received ${blendMode}`,
39
+ { context: { received: blendMode } }
40
+ );
41
+ }
42
+ if (!Number.isFinite(radius) || radius <= 0) {
43
+ throw new HeatmapPointError('radius must be a positive number', {
44
+ context: { received: radius },
45
+ });
46
+ }
47
+
48
+ for (const [i, point] of points.entries()) {
49
+ if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
50
+ throw new HeatmapPointError(`points[${i}] must have finite x and y`, {
51
+ context: { index: i, point },
52
+ });
53
+ }
54
+ if (point.weight !== undefined && !Number.isFinite(point.weight)) {
55
+ throw new HeatmapPointError(`points[${i}].weight must be a finite number`, {
56
+ context: { index: i, weight: point.weight },
57
+ });
58
+ }
59
+ }
60
+
61
+ const sharp = require('sharp');
62
+ const meta = await readMeta(sharp, input);
63
+
64
+ if (points.length === 0) {
65
+ return input;
66
+ }
67
+
68
+ const color = resolveColor(colorScale);
69
+ const svg = buildSvg(points, radius, intensity, color, meta);
70
+ const blurred = await sharp(Buffer.from(svg, 'utf8'))
71
+ .resize(meta.width, meta.height, { fit: 'fill' })
72
+ .blur(Math.min(50, radius / 2))
73
+ .toBuffer();
74
+
75
+ return sharp(input)
76
+ .composite([{ input: blurred, blend: blendMode }])
77
+ .toBuffer();
78
+ }
79
+
80
+ /**
81
+ * Build the SVG layer with one circle per point.
82
+ *
83
+ * @param {Array<import('../types/index.d.ts').HeatmapPoint>} points
84
+ * @param {number} radius
85
+ * @param {number} intensity
86
+ * @param {string} color CSS color for the dot fill.
87
+ * @param {{width: number, height: number}} meta
88
+ * @returns {string}
89
+ */
90
+ function buildSvg(points, radius, intensity, color, meta) {
91
+ const opacity = Math.max(0, Math.min(1, intensity));
92
+ const circles = points
93
+ .map(p => {
94
+ const w = Math.max(0.1, Number(p.weight ?? 1));
95
+ const r = radius * Math.sqrt(w);
96
+ return `<circle cx="${p.x}" cy="${p.y}" r="${r}" fill="${color}" fill-opacity="${opacity}"/>`;
97
+ })
98
+ .join('');
99
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${meta.width}" height="${meta.height}"><g>${circles}</g></svg>`;
100
+ }
101
+
102
+ /**
103
+ * Resolve the colorScale option into a single CSS color used for every
104
+ * point. Full multi-stop color scales are deferred to v1.1; for v1.0
105
+ * we pick a representative warm color per built-in scale.
106
+ *
107
+ * @param {string | Function} colorScale
108
+ * @returns {string}
109
+ */
110
+ function resolveColor(colorScale) {
111
+ if (typeof colorScale === 'function') {
112
+ // Sample at the high-intensity end (t=1).
113
+ try {
114
+ const result = colorScale(1);
115
+ if (Array.isArray(result) && result.length >= 3) {
116
+ return `rgb(${result[0]}, ${result[1]}, ${result[2]})`;
117
+ }
118
+ } catch {
119
+ // fall through to default
120
+ }
121
+ return '#FF0000';
122
+ }
123
+ switch (colorScale) {
124
+ case 'magma':
125
+ return '#FB8861';
126
+ case 'severity':
127
+ return '#D32F2F';
128
+ default:
129
+ // 'viridis' (default) or any unknown scale.
130
+ return '#FDE725';
131
+ }
132
+ }
133
+
134
+ function assertBuffer(input) {
135
+ if (!Buffer.isBuffer(input)) {
136
+ throw new BufferCorruptError('input must be a Buffer', {
137
+ context: { received: typeof input },
138
+ });
139
+ }
140
+ }
141
+
142
+ async function readMeta(sharp, input) {
143
+ try {
144
+ const meta = await sharp(input).metadata();
145
+ if (!meta.width || !meta.height) {
146
+ throw new BufferCorruptError('Image has no detectable dimensions');
147
+ }
148
+ return { width: meta.width, height: meta.height };
149
+ } catch (error) {
150
+ if (error instanceof BufferCorruptError) {
151
+ throw error;
152
+ }
153
+ throw new BufferCorruptError('sharp could not read heatmap input buffer', {
154
+ cause: error,
155
+ });
156
+ }
157
+ }
158
+
159
+ module.exports = { heatmap, MAX_POINTS };
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const { annotate } = require('./annotate.js');
4
+ const { defaultTheme } = require('./theme.js');
5
+ const { diff } = require('./diff.js');
6
+ const { heatmap } = require('./heatmap.js');
7
+
8
+ module.exports = { annotate, defaultTheme, diff, heatmap };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Default annotation theme shipped with the package.
5
+ *
6
+ * Implements `spec.md` § 8.3.2. The four severity keys cover the standard
7
+ * accessibility-issue grading. Consumers can replace any subset by passing
8
+ * their own theme object to `annotate()`; the function shallow-merges over
9
+ * this default.
10
+ *
11
+ * Color choices target WCAG 2.2 AA when rendered against typical light or
12
+ * dark page backgrounds. Stroke widths are in CSS px and scale with the
13
+ * captured image's pixel space.
14
+ *
15
+ * @public
16
+ * @type {Readonly<import('../types/index.d.ts').AnnotationTheme>}
17
+ */
18
+ const defaultTheme = Object.freeze({
19
+ 'severity-critical': Object.freeze({
20
+ stroke: '#D32F2F',
21
+ strokeWidth: 3,
22
+ fill: '#D32F2F22',
23
+ badge: Object.freeze({ bg: '#D32F2F', fg: '#fff' }),
24
+ }),
25
+ 'severity-serious': Object.freeze({
26
+ stroke: '#F57C00',
27
+ strokeWidth: 3,
28
+ fill: '#F57C0022',
29
+ badge: Object.freeze({ bg: '#F57C00', fg: '#fff' }),
30
+ }),
31
+ 'severity-moderate': Object.freeze({
32
+ stroke: '#FBC02D',
33
+ strokeWidth: 2,
34
+ fill: '#FBC02D22',
35
+ badge: Object.freeze({ bg: '#FBC02D', fg: '#000' }),
36
+ }),
37
+ 'severity-minor': Object.freeze({
38
+ stroke: '#1976D2',
39
+ strokeWidth: 2,
40
+ fill: '#1976D222',
41
+ badge: Object.freeze({ bg: '#1976D2', fg: '#fff' }),
42
+ }),
43
+ });
44
+
45
+ module.exports = { defaultTheme };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const { ScreenshotUtilsError } = require('./base.js');
4
+
5
+ /**
6
+ * Base for errors arising at the `PageAdapter` boundary
7
+ * (third-party browser object integration).
8
+ *
9
+ * @public
10
+ */
11
+ class AdapterError extends ScreenshotUtilsError {
12
+ /**
13
+ * @param {string} message
14
+ * @param {object} [options]
15
+ */
16
+ constructor(message, options) {
17
+ super(message, options);
18
+ this.code = 'ADAPTER_ERROR';
19
+ this.retriable = false;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * The wrapped third-party page object is missing a method required by the
25
+ * `PageAdapter` contract (spec § 10). Deterministic — the caller passed the
26
+ * wrong kind of object.
27
+ *
28
+ * @public
29
+ */
30
+ class AdapterContractError extends AdapterError {
31
+ /**
32
+ * @param {string} message
33
+ * @param {object} [options]
34
+ */
35
+ constructor(message, options) {
36
+ super(message, options);
37
+ this.code = 'ADAPTER_CONTRACT';
38
+ this.retriable = false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * The underlying puppeteer / playwright / CDP call threw. Wrapped so consumers
44
+ * can rely on the package's error shape without sniffing browser-library
45
+ * internals. Retriable because most browser-API errors are transient.
46
+ *
47
+ * @public
48
+ */
49
+ class BrowserApiError extends AdapterError {
50
+ /**
51
+ * @param {string} message
52
+ * @param {object} [options]
53
+ */
54
+ constructor(message, options) {
55
+ super(message, options);
56
+ this.code = 'BROWSER_API';
57
+ this.retriable = true;
58
+ }
59
+ }
60
+
61
+ module.exports = {
62
+ AdapterError,
63
+ AdapterContractError,
64
+ BrowserApiError,
65
+ };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Abstract base class for all errors thrown by `@afixt/screenshot-utils`.
5
+ *
6
+ * Concrete subclasses must:
7
+ * - set a stable `code` string (machine-readable; never localized)
8
+ * - set `retriable: true` only when a consumer-side retry has a chance of succeeding
9
+ * without operator intervention (transient browser timeouts, transport blips, etc.)
10
+ * - never include PII in `context`. The platform privacy rules in
11
+ * the parent CLAUDE.md forbid logging user data; the same applies to
12
+ * error payloads since they are commonly logged verbatim.
13
+ *
14
+ * See `spec.md` § 14 for the full error model.
15
+ *
16
+ * @public
17
+ */
18
+ class ScreenshotUtilsError extends Error {
19
+ /**
20
+ * Construct an error. Subclasses set `code` and `retriable`; callers may
21
+ * supply `reason`, `cause`, and `context`.
22
+ *
23
+ * @param {string} message Human-readable summary. Should not include PII.
24
+ * @param {object} [options]
25
+ * @param {string} [options.reason] Sub-classification (enum string) — see spec § 14.3.
26
+ * @param {Error} [options.cause] Underlying error being wrapped, preserved for stack traces.
27
+ * @param {Record<string, unknown>} [options.context] Structured debug data. NO PII.
28
+ */
29
+ constructor(message, { reason, cause, context } = {}) {
30
+ super(message, cause === undefined ? undefined : { cause });
31
+ this.name = new.target.name;
32
+ // Subclasses overwrite `code` and `retriable` after super().
33
+ // Defaults here keep base instances valid for testing the shape.
34
+ this.code = 'SCREENSHOT_UTILS_ERROR';
35
+ this.retriable = false;
36
+ if (reason !== undefined) {
37
+ this.reason = reason;
38
+ }
39
+ if (context !== undefined) {
40
+ this.context = context;
41
+ }
42
+ // Note: modern V8 automatically excludes the Error constructor
43
+ // from the stack trace when subclassing, so an explicit
44
+ // Error.captureStackTrace(this, new.target) is unnecessary.
45
+ }
46
+ }
47
+
48
+ module.exports = { ScreenshotUtilsError };
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ const { ScreenshotUtilsError } = require('./base.js');
4
+
5
+ /**
6
+ * Base for any error arising during the capture phase
7
+ * (browser interaction, scroll, settle, screenshot).
8
+ *
9
+ * @public
10
+ */
11
+ class CaptureError extends ScreenshotUtilsError {
12
+ /**
13
+ * @param {string} message
14
+ * @param {object} [options]
15
+ * @param {string} [options.reason]
16
+ * @param {Error} [options.cause]
17
+ * @param {Record<string, unknown>} [options.context]
18
+ */
19
+ constructor(message, options) {
20
+ super(message, options);
21
+ this.code = 'CAPTURE_ERROR';
22
+ this.retriable = false;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * The browser context is unreachable (process crashed, pool exhausted, etc.).
28
+ * Retriable because the consumer may obtain a fresh browser context.
29
+ *
30
+ * @public
31
+ */
32
+ class BrowserUnavailableError extends CaptureError {
33
+ /**
34
+ * @param {string} message
35
+ * @param {object} [options]
36
+ */
37
+ constructor(message, options) {
38
+ super(message, options);
39
+ this.code = 'BROWSER_UNAVAILABLE';
40
+ this.retriable = true;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Document scrollHeight exceeds the configured `maxHeight` ceiling.
46
+ *
47
+ * @public
48
+ */
49
+ class PageTooTallError extends CaptureError {
50
+ /**
51
+ * @param {string} message
52
+ * @param {object} [options]
53
+ */
54
+ constructor(message, options) {
55
+ super(message, options);
56
+ this.code = 'PAGE_TOO_TALL';
57
+ this.retriable = false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * The requested selector / xpath returned no DOM node.
63
+ *
64
+ * @public
65
+ */
66
+ class ElementNotFoundError extends CaptureError {
67
+ /**
68
+ * @param {string} message
69
+ * @param {object} [options]
70
+ */
71
+ constructor(message, options) {
72
+ super(message, options);
73
+ this.code = 'ELEMENT_NOT_FOUND';
74
+ this.retriable = false;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * The element exists but has zero size or is hidden from layout.
80
+ *
81
+ * @public
82
+ */
83
+ class ElementNotVisibleError extends CaptureError {
84
+ /**
85
+ * @param {string} message
86
+ * @param {object} [options]
87
+ */
88
+ constructor(message, options) {
89
+ super(message, options);
90
+ this.code = 'ELEMENT_NOT_VISIBLE';
91
+ this.retriable = false;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * A verification snippet was provided but the live DOM did not match.
97
+ *
98
+ * @public
99
+ */
100
+ class ElementMismatchError extends CaptureError {
101
+ /**
102
+ * @param {string} message
103
+ * @param {object} [options]
104
+ */
105
+ constructor(message, options) {
106
+ super(message, options);
107
+ this.code = 'ELEMENT_MISMATCH';
108
+ this.retriable = false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Element dimensions exceed `maxDimensions` (default 4000×4000 CSS px).
114
+ *
115
+ * @public
116
+ */
117
+ class ElementTooLargeError extends CaptureError {
118
+ /**
119
+ * @param {string} message
120
+ * @param {object} [options]
121
+ */
122
+ constructor(message, options) {
123
+ super(message, options);
124
+ this.code = 'ELEMENT_TOO_LARGE';
125
+ this.retriable = false;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * `waitForStable` exceeded its `maxWait`. The page was still settling.
131
+ * Retriable because subsequent calls may catch a stable frame.
132
+ *
133
+ * @public
134
+ */
135
+ class StabilityTimeoutError extends CaptureError {
136
+ /**
137
+ * @param {string} message
138
+ * @param {object} [options]
139
+ */
140
+ constructor(message, options) {
141
+ super(message, options);
142
+ this.code = 'STABILITY_TIMEOUT';
143
+ this.retriable = true;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * The underlying browser screenshot call exceeded the configured `timeout`.
149
+ * Retriable for the same reasons as StabilityTimeoutError.
150
+ *
151
+ * @public
152
+ */
153
+ class CaptureTimeoutError extends CaptureError {
154
+ /**
155
+ * @param {string} message
156
+ * @param {object} [options]
157
+ */
158
+ constructor(message, options) {
159
+ super(message, options);
160
+ this.code = 'CAPTURE_TIMEOUT';
161
+ this.retriable = true;
162
+ }
163
+ }
164
+
165
+ module.exports = {
166
+ CaptureError,
167
+ BrowserUnavailableError,
168
+ PageTooTallError,
169
+ ElementNotFoundError,
170
+ ElementNotVisibleError,
171
+ ElementMismatchError,
172
+ ElementTooLargeError,
173
+ StabilityTimeoutError,
174
+ CaptureTimeoutError,
175
+ };