craftdriver 1.6.0 → 1.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/CHANGELOG.md +35 -6
- package/README.md +17 -6
- package/dist/cli/agentSession.d.ts +62 -0
- package/dist/cli/agentSession.d.ts.map +1 -0
- package/dist/cli/agentSession.js +100 -0
- package/dist/cli/agentSession.js.map +1 -0
- package/dist/cli/artifactPaths.d.ts +42 -0
- package/dist/cli/artifactPaths.d.ts.map +1 -0
- package/dist/cli/artifactPaths.js +144 -0
- package/dist/cli/artifactPaths.js.map +1 -0
- package/dist/cli/bounds.d.ts +88 -0
- package/dist/cli/bounds.d.ts.map +1 -0
- package/dist/cli/bounds.js +128 -0
- package/dist/cli/bounds.js.map +1 -0
- package/dist/cli/client.d.ts +3 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +24 -8
- package/dist/cli/client.js.map +1 -1
- package/dist/cli/daemon.d.ts +3 -0
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +152 -39
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/defaults.d.ts +23 -3
- package/dist/cli/defaults.d.ts.map +1 -1
- package/dist/cli/defaults.js +50 -5
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/dispatcher.d.ts +54 -11
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +851 -54
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +305 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +14 -14
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +328 -180
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/journal.d.ts +153 -0
- package/dist/cli/journal.d.ts.map +1 -0
- package/dist/cli/journal.js +252 -0
- package/dist/cli/journal.js.map +1 -0
- package/dist/cli/lineReader.d.ts +33 -0
- package/dist/cli/lineReader.d.ts.map +1 -0
- package/dist/cli/lineReader.js +66 -0
- package/dist/cli/lineReader.js.map +1 -0
- package/dist/cli/locatorCandidates.d.ts +44 -0
- package/dist/cli/locatorCandidates.d.ts.map +1 -0
- package/dist/cli/locatorCandidates.js +190 -0
- package/dist/cli/locatorCandidates.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +39 -1
- package/dist/cli/mcp/artifacts.d.ts.map +1 -1
- package/dist/cli/mcp/artifacts.js +104 -9
- package/dist/cli/mcp/artifacts.js.map +1 -1
- package/dist/cli/mcp/bounds.d.ts +65 -0
- package/dist/cli/mcp/bounds.d.ts.map +1 -0
- package/dist/cli/mcp/bounds.js +124 -0
- package/dist/cli/mcp/bounds.js.map +1 -0
- package/dist/cli/mcp/params.d.ts +38 -0
- package/dist/cli/mcp/params.d.ts.map +1 -0
- package/dist/cli/mcp/params.js +174 -0
- package/dist/cli/mcp/params.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +94 -16
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +304 -150
- package/dist/cli/mcp/server.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +62 -17
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +373 -158
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/cli/pageSemantics.d.ts +42 -0
- package/dist/cli/pageSemantics.d.ts.map +1 -0
- package/dist/cli/pageSemantics.js +194 -0
- package/dist/cli/pageSemantics.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +6 -18
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +689 -45
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/cli/protocol.d.ts +8 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/sessionRegistry.d.ts +42 -0
- package/dist/cli/sessionRegistry.d.ts.map +1 -0
- package/dist/cli/sessionRegistry.js +99 -0
- package/dist/cli/sessionRegistry.js.map +1 -0
- package/dist/cli/snapshot.d.ts +100 -18
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +248 -102
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/cli/stateStore.d.ts +50 -0
- package/dist/cli/stateStore.d.ts.map +1 -0
- package/dist/cli/stateStore.js +122 -0
- package/dist/cli/stateStore.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +1 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +2 -2
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +3 -0
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +7 -0
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +14 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +45 -7
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +30 -6
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +190 -45
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +7 -2
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +38 -3
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +196 -30
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +54 -18
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +295 -92
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +3 -3
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +7 -3
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/driver.d.ts +7 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +12 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +15 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/initScript.d.ts +9 -0
- package/dist/lib/initScript.d.ts.map +1 -0
- package/dist/lib/initScript.js +14 -0
- package/dist/lib/initScript.js.map +1 -0
- package/dist/lib/page.d.ts +22 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +48 -0
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/secureFile.d.ts +8 -0
- package/dist/lib/secureFile.d.ts.map +1 -0
- package/dist/lib/secureFile.js +67 -0
- package/dist/lib/secureFile.js.map +1 -0
- package/dist/lib/sessionStateValidation.d.ts +35 -0
- package/dist/lib/sessionStateValidation.d.ts.map +1 -0
- package/dist/lib/sessionStateValidation.js +223 -0
- package/dist/lib/sessionStateValidation.js.map +1 -0
- package/dist/lib/timing.d.ts +8 -0
- package/dist/lib/timing.d.ts.map +1 -1
- package/dist/lib/timing.js +8 -0
- package/dist/lib/timing.js.map +1 -1
- package/dist/lib/tracing.d.ts +2 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +12 -1
- package/dist/lib/tracing.js.map +1 -1
- package/dist/lib/visual/antialias.d.ts +41 -0
- package/dist/lib/visual/antialias.d.ts.map +1 -0
- package/dist/lib/visual/antialias.js +126 -0
- package/dist/lib/visual/antialias.js.map +1 -0
- package/dist/lib/visual/codec.d.ts +24 -0
- package/dist/lib/visual/codec.d.ts.map +1 -0
- package/dist/lib/visual/codec.js +11 -0
- package/dist/lib/visual/codec.js.map +1 -0
- package/dist/lib/visual/compare.d.ts +38 -0
- package/dist/lib/visual/compare.d.ts.map +1 -0
- package/dist/lib/visual/compare.js +209 -0
- package/dist/lib/visual/compare.js.map +1 -0
- package/dist/lib/visual/diff.d.ts +18 -0
- package/dist/lib/visual/diff.d.ts.map +1 -0
- package/dist/lib/visual/diff.js +23 -0
- package/dist/lib/visual/diff.js.map +1 -0
- package/dist/lib/visual/expectScreenshot.d.ts +65 -0
- package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
- package/dist/lib/visual/expectScreenshot.js +287 -0
- package/dist/lib/visual/expectScreenshot.js.map +1 -0
- package/dist/lib/visual/index.d.ts +10 -0
- package/dist/lib/visual/index.d.ts.map +1 -0
- package/dist/lib/visual/index.js +9 -0
- package/dist/lib/visual/index.js.map +1 -0
- package/dist/lib/visual/pixelMath.d.ts +32 -0
- package/dist/lib/visual/pixelMath.d.ts.map +1 -0
- package/dist/lib/visual/pixelMath.js +70 -0
- package/dist/lib/visual/pixelMath.js.map +1 -0
- package/dist/lib/visual/pngHeader.d.ts +27 -0
- package/dist/lib/visual/pngHeader.d.ts.map +1 -0
- package/dist/lib/visual/pngHeader.js +63 -0
- package/dist/lib/visual/pngHeader.js.map +1 -0
- package/dist/lib/visual/pngjsCodec.d.ts +22 -0
- package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
- package/dist/lib/visual/pngjsCodec.js +70 -0
- package/dist/lib/visual/pngjsCodec.js.map +1 -0
- package/dist/lib/visual/types.d.ts +89 -0
- package/dist/lib/visual/types.d.ts.map +1 -0
- package/dist/lib/visual/types.js +20 -0
- package/dist/lib/visual/types.js.map +1 -0
- package/dist/lib/visual/visualError.d.ts +32 -0
- package/dist/lib/visual/visualError.d.ts.map +1 -0
- package/dist/lib/visual/visualError.js +59 -0
- package/dist/lib/visual/visualError.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +7 -1
- package/dist/lib/wait.js.map +1 -1
- package/docs/agents.md +56 -61
- package/docs/api-reference.md +16 -1
- package/docs/browser-api.md +24 -4
- package/docs/browser-context.md +18 -12
- package/docs/cli.md +346 -26
- package/docs/error-codes.md +6 -4
- package/docs/getting-started.md +6 -1
- package/docs/index.md +1 -1
- package/docs/mcp.md +129 -62
- package/docs/public/examples/agent-actions.html +48 -0
- package/docs/public/examples/agent-debug.html +86 -0
- package/docs/public/examples/agent-late-mutation.html +18 -0
- package/docs/public/examples/agent-ref-shift.html +33 -0
- package/docs/recipes/login-once-reuse-session.md +32 -4
- package/docs/screenshots.md +15 -6
- package/docs/session-management.md +64 -3
- package/docs/visual-testing.md +254 -0
- package/docs/why-craftdriver.md +4 -3
- package/package.json +5 -1
- package/skills/craftdriver/SKILL.md +59 -75
- package/skills/craftdriver/agents/openai.yaml +4 -0
- package/skills/craftdriver/cheatsheet.md +18 -4
- package/skills/craftdriver/cli.md +194 -68
- package/skills/craftdriver/patterns.md +14 -8
- package/skills/craftdriver/workflow.md +149 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The retrying visual assertion: read the baseline once, then poll the browser
|
|
3
|
+
* until a screenshot matches or the monotonic deadline expires.
|
|
4
|
+
*
|
|
5
|
+
* The hot path is deliberately lazy. An exact byte match costs one screenshot
|
|
6
|
+
* plus a `Buffer.equals` and never decodes. A static mismatch decodes the
|
|
7
|
+
* expected and actual images once each, then subsequent polls only re-capture
|
|
8
|
+
* and compare compressed bytes. Only changing content pays a decode per retry.
|
|
9
|
+
* The diff image is allocated and encoded exactly once, after the final failure.
|
|
10
|
+
*
|
|
11
|
+
* Baselines are managed for the caller, WebdriverIO-style:
|
|
12
|
+
* - Missing baseline (ENOENT): capture until two consecutive frames settle,
|
|
13
|
+
* write it, pass. This is the default — no flag required.
|
|
14
|
+
* - Present but differs, with `update`: capture as usual and, only if the page
|
|
15
|
+
* never settles into a match, overwrite the baseline with the final actual
|
|
16
|
+
* and pass. A page that settles back into a match passes without rewriting.
|
|
17
|
+
* A present-but-corrupt baseline, and any non-ENOENT read error, stay hard
|
|
18
|
+
* configuration errors even under `update` — they are never overwritten.
|
|
19
|
+
*
|
|
20
|
+
* Every baseline write is atomic — the PNG is written to a temp file in the same
|
|
21
|
+
* directory and renamed into place — so an interrupted run cannot corrupt a
|
|
22
|
+
* committed baseline (this matters most under `update`, which replaces one).
|
|
23
|
+
*
|
|
24
|
+
* Timing (`now`) and delays (`sleep`) are injectable so retry behaviour can be
|
|
25
|
+
* driven deterministically in tests without wall-clock waits.
|
|
26
|
+
*/
|
|
27
|
+
import { readFile as fsReadFile, writeFile as fsWriteFile, mkdir as fsMkdir, rename as fsRename, unlink as fsUnlink, } from 'node:fs/promises';
|
|
28
|
+
import { randomBytes } from 'node:crypto';
|
|
29
|
+
import { dirname } from 'node:path';
|
|
30
|
+
import { By } from '../by.js';
|
|
31
|
+
import { CraftdriverError, ErrorCode } from '../errors.js';
|
|
32
|
+
import { VISUAL_RETRY_INTERVAL_MS } from '../timing.js';
|
|
33
|
+
import { pngjsCodec } from './pngjsCodec.js';
|
|
34
|
+
import { validatePngHeaderAndSize } from './pngHeader.js';
|
|
35
|
+
import { compareDecoded, resolveCompareOptions } from './compare.js';
|
|
36
|
+
import { createDiff } from './diff.js';
|
|
37
|
+
import { VisualMismatchError } from './visualError.js';
|
|
38
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
39
|
+
const defaultReport = (message) => {
|
|
40
|
+
process.stderr.write(`[craftdriver] ${message}\n`);
|
|
41
|
+
};
|
|
42
|
+
function invalid(message, detail) {
|
|
43
|
+
throw new CraftdriverError(ErrorCode.INVALID_ARGUMENT, message, detail ? { detail } : undefined);
|
|
44
|
+
}
|
|
45
|
+
function isFileNotFound(err) {
|
|
46
|
+
return (typeof err === 'object' &&
|
|
47
|
+
err !== null &&
|
|
48
|
+
'code' in err &&
|
|
49
|
+
err.code === 'ENOENT');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Strictly interpret the `CRAFTDRIVER_UPDATE_VISUAL_BASELINES` environment
|
|
53
|
+
* variable. Unset / empty / `false` → off; `true` → on (trimmed,
|
|
54
|
+
* case-insensitive). Anything else throws `INVALID_ARGUMENT` rather than being
|
|
55
|
+
* silently coerced — this flag mutates files on disk, so a typo must fail loud.
|
|
56
|
+
*/
|
|
57
|
+
export function shouldUpdateVisualBaselines(raw) {
|
|
58
|
+
if (raw === undefined)
|
|
59
|
+
return false;
|
|
60
|
+
const value = raw.trim().toLowerCase();
|
|
61
|
+
if (value === '' || value === 'false')
|
|
62
|
+
return false;
|
|
63
|
+
if (value === 'true')
|
|
64
|
+
return true;
|
|
65
|
+
invalid(`CRAFTDRIVER_UPDATE_VISUAL_BASELINES must be "true" or "false" (or unset), got ${JSON.stringify(raw)}.`, { value: raw });
|
|
66
|
+
}
|
|
67
|
+
/** Validate the retry-specific options (compare options are validated separately). */
|
|
68
|
+
function resolveRetryOptions(options, defaultTimeout) {
|
|
69
|
+
const timeout = options.timeout ?? defaultTimeout;
|
|
70
|
+
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
71
|
+
invalid(`timeout must be a finite number >= 0, got ${timeout}.`, { timeout });
|
|
72
|
+
}
|
|
73
|
+
const interval = options.interval ?? VISUAL_RETRY_INTERVAL_MS;
|
|
74
|
+
if (!Number.isInteger(interval) || interval < 10) {
|
|
75
|
+
invalid(`interval must be an integer >= 10 ms, got ${interval}.`, { interval });
|
|
76
|
+
}
|
|
77
|
+
// Validate the capture scope's shape for JavaScript callers (TS already
|
|
78
|
+
// constrains it): correct field types, then the fullPage/selector exclusion.
|
|
79
|
+
const scope = options.screenshot;
|
|
80
|
+
if (scope !== undefined) {
|
|
81
|
+
if (typeof scope !== 'object' || scope === null) {
|
|
82
|
+
invalid('screenshot must be an object with { fullPage } or { selector }.');
|
|
83
|
+
}
|
|
84
|
+
const hasFullPage = 'fullPage' in scope && scope.fullPage !== undefined;
|
|
85
|
+
const hasSelector = 'selector' in scope && scope.selector !== undefined;
|
|
86
|
+
if (hasFullPage && typeof scope.fullPage !== 'boolean') {
|
|
87
|
+
invalid('screenshot.fullPage must be a boolean.');
|
|
88
|
+
}
|
|
89
|
+
if (hasSelector) {
|
|
90
|
+
const selector = scope.selector;
|
|
91
|
+
if (typeof selector !== 'string' && !(selector instanceof By)) {
|
|
92
|
+
invalid('screenshot.selector must be a string or a By locator.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (hasFullPage && scope.fullPage === true && hasSelector) {
|
|
96
|
+
invalid('screenshot.fullPage and screenshot.selector are mutually exclusive.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { timeout, interval };
|
|
100
|
+
}
|
|
101
|
+
/** A monotonic deadline built once, queried throughout a run. */
|
|
102
|
+
function makeClock(now, timeout) {
|
|
103
|
+
const start = now();
|
|
104
|
+
const deadline = start + timeout;
|
|
105
|
+
return {
|
|
106
|
+
elapsed: () => now() - start,
|
|
107
|
+
remaining: () => Math.max(0, deadline - now()),
|
|
108
|
+
expired: () => now() >= deadline,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export async function runExpectScreenshot(deps) {
|
|
112
|
+
const { expectedPath, options, defaultTimeout, capture } = deps;
|
|
113
|
+
const update = deps.update ?? false;
|
|
114
|
+
const readFile = deps.readFile ?? fsReadFile;
|
|
115
|
+
const writeFile = deps.writeFile ?? fsWriteFile;
|
|
116
|
+
const rename = deps.rename ?? fsRename;
|
|
117
|
+
const unlink = deps.unlink ?? fsUnlink;
|
|
118
|
+
const mkdir = deps.mkdir ?? fsMkdir;
|
|
119
|
+
const report = deps.report ?? defaultReport;
|
|
120
|
+
const now = deps.now ?? (() => performance.now());
|
|
121
|
+
const sleep = deps.sleep ?? realSleep;
|
|
122
|
+
const codec = deps.codec ?? pngjsCodec;
|
|
123
|
+
const compareOpts = resolveCompareOptions(options);
|
|
124
|
+
const { timeout, interval } = resolveRetryOptions(options, defaultTimeout);
|
|
125
|
+
const limits = { maxImagePixels: compareOpts.maxImagePixels, maxInputBytes: compareOpts.maxInputBytes };
|
|
126
|
+
/**
|
|
127
|
+
* Write a baseline atomically: the PNG is written in full to a uniquely-named
|
|
128
|
+
* temporary file in the destination's own directory, then renamed over the
|
|
129
|
+
* destination. A crash, disk error, or partial write therefore never leaves a
|
|
130
|
+
* truncated or half-updated committed baseline — the old file survives intact.
|
|
131
|
+
* On any failure the temp file is best-effort removed and the error rethrown.
|
|
132
|
+
*/
|
|
133
|
+
const writeBaselineAtomically = async (destination, data) => {
|
|
134
|
+
const tmp = `${destination}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
135
|
+
try {
|
|
136
|
+
await writeFile(tmp, data);
|
|
137
|
+
await rename(tmp, destination);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
await unlink(tmp).catch(() => { });
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* No baseline on disk yet: capture until two consecutive frames are
|
|
146
|
+
* byte-identical (or the timeout elapses) so we don't enshrine a mid-animation
|
|
147
|
+
* frame, then write that settled capture as the new baseline and pass.
|
|
148
|
+
*/
|
|
149
|
+
const createMissingBaseline = async () => {
|
|
150
|
+
const clock = makeClock(now, timeout);
|
|
151
|
+
let settled;
|
|
152
|
+
let settledDims;
|
|
153
|
+
let attempts = 0;
|
|
154
|
+
let didSettle = false;
|
|
155
|
+
for (;;) {
|
|
156
|
+
const buffer = await capture(clock.remaining());
|
|
157
|
+
attempts++;
|
|
158
|
+
const dims = validatePngHeaderAndSize(buffer, limits, 'screenshot');
|
|
159
|
+
const isSettled = settled !== undefined && settled.equals(buffer);
|
|
160
|
+
settled = buffer;
|
|
161
|
+
settledDims = dims;
|
|
162
|
+
if (isSettled) {
|
|
163
|
+
didSettle = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (clock.expired())
|
|
167
|
+
break;
|
|
168
|
+
await sleep(Math.min(interval, clock.remaining()));
|
|
169
|
+
}
|
|
170
|
+
await mkdir(dirname(expectedPath), { recursive: true });
|
|
171
|
+
await writeBaselineAtomically(expectedPath, settled);
|
|
172
|
+
// Warn on every unsettled write — including `timeout: 0`, which deliberately
|
|
173
|
+
// takes a single capture — so an arbitrary frame is never written silently.
|
|
174
|
+
// We still create the baseline (a page with a live region would otherwise be
|
|
175
|
+
// impossible to baseline); the message just flags the frame for review.
|
|
176
|
+
report(didSettle
|
|
177
|
+
? `Created visual baseline: ${expectedPath}`
|
|
178
|
+
: `Created visual baseline (page did not settle within ${timeout}ms — review it before committing): ${expectedPath}`);
|
|
179
|
+
return {
|
|
180
|
+
matches: true,
|
|
181
|
+
diffPixels: 0,
|
|
182
|
+
diffPercentage: 0,
|
|
183
|
+
ignoredAntialiasPixels: 0,
|
|
184
|
+
expectedWidth: settledDims.width,
|
|
185
|
+
expectedHeight: settledDims.height,
|
|
186
|
+
actualWidth: settledDims.width,
|
|
187
|
+
actualHeight: settledDims.height,
|
|
188
|
+
dimensionMismatch: false,
|
|
189
|
+
attempts,
|
|
190
|
+
elapsedMs: clock.elapsed(),
|
|
191
|
+
baseline: 'created',
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
// Read + validate the baseline once, before the deadline starts, so `timeout`
|
|
195
|
+
// measures browser stabilization time rather than disk latency. A missing file
|
|
196
|
+
// is the create signal; any other read error stays a caller-facing failure.
|
|
197
|
+
let expectedBuffer;
|
|
198
|
+
try {
|
|
199
|
+
expectedBuffer = await readFile(expectedPath);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (!isFileNotFound(err))
|
|
203
|
+
throw err;
|
|
204
|
+
return createMissingBaseline();
|
|
205
|
+
}
|
|
206
|
+
const expectedDims = validatePngHeaderAndSize(expectedBuffer, limits, 'baseline');
|
|
207
|
+
const clock = makeClock(now, timeout);
|
|
208
|
+
let expectedImage;
|
|
209
|
+
let previousActualBuffer;
|
|
210
|
+
let previousActualImage;
|
|
211
|
+
let previousComparison;
|
|
212
|
+
let attempts = 0;
|
|
213
|
+
for (;;) {
|
|
214
|
+
const actualBuffer = await capture(clock.remaining());
|
|
215
|
+
attempts++;
|
|
216
|
+
// Fast path 1: identical PNG bytes necessarily means identical pixels.
|
|
217
|
+
if (expectedBuffer.length === actualBuffer.length && expectedBuffer.equals(actualBuffer)) {
|
|
218
|
+
return {
|
|
219
|
+
matches: true,
|
|
220
|
+
diffPixels: 0,
|
|
221
|
+
diffPercentage: 0,
|
|
222
|
+
ignoredAntialiasPixels: 0,
|
|
223
|
+
expectedWidth: expectedDims.width,
|
|
224
|
+
expectedHeight: expectedDims.height,
|
|
225
|
+
actualWidth: expectedDims.width,
|
|
226
|
+
actualHeight: expectedDims.height,
|
|
227
|
+
dimensionMismatch: false,
|
|
228
|
+
attempts,
|
|
229
|
+
elapsedMs: clock.elapsed(),
|
|
230
|
+
baseline: 'matched',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
// Fast path 2: an unchanged failed screenshot has the same comparison result.
|
|
234
|
+
if (previousActualBuffer?.equals(actualBuffer)) {
|
|
235
|
+
if (clock.expired())
|
|
236
|
+
break;
|
|
237
|
+
await sleep(Math.min(interval, clock.remaining()));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
validatePngHeaderAndSize(actualBuffer, limits, 'screenshot');
|
|
241
|
+
expectedImage ??= await codec.decode(expectedBuffer);
|
|
242
|
+
const actualImage = await codec.decode(actualBuffer);
|
|
243
|
+
const comparison = compareDecoded(actualImage, expectedImage, compareOpts);
|
|
244
|
+
if (comparison.matches) {
|
|
245
|
+
return { ...comparison, attempts, elapsedMs: clock.elapsed(), baseline: 'matched' };
|
|
246
|
+
}
|
|
247
|
+
// Retain only the most recent failed attempt — never every retry image.
|
|
248
|
+
previousActualBuffer = actualBuffer;
|
|
249
|
+
previousActualImage = actualImage;
|
|
250
|
+
previousComparison = { ...comparison, attempts, elapsedMs: clock.elapsed(), baseline: 'matched' };
|
|
251
|
+
if (clock.expired())
|
|
252
|
+
break;
|
|
253
|
+
await sleep(Math.min(interval, clock.remaining()));
|
|
254
|
+
}
|
|
255
|
+
// The page never settled into a match. Either accept the new rendering by
|
|
256
|
+
// overwriting the baseline (update mode), or fail with the diff.
|
|
257
|
+
if (update) {
|
|
258
|
+
await writeBaselineAtomically(expectedPath, previousActualBuffer);
|
|
259
|
+
report(`Updated visual baseline: ${expectedPath} ` +
|
|
260
|
+
`(${previousComparison.diffPixels} px, ${previousComparison.diffPercentage.toFixed(2)}% changed)`);
|
|
261
|
+
return {
|
|
262
|
+
matches: true,
|
|
263
|
+
diffPixels: 0,
|
|
264
|
+
diffPercentage: 0,
|
|
265
|
+
ignoredAntialiasPixels: 0,
|
|
266
|
+
expectedWidth: previousComparison.actualWidth,
|
|
267
|
+
expectedHeight: previousComparison.actualHeight,
|
|
268
|
+
actualWidth: previousComparison.actualWidth,
|
|
269
|
+
actualHeight: previousComparison.actualHeight,
|
|
270
|
+
dimensionMismatch: false,
|
|
271
|
+
attempts,
|
|
272
|
+
elapsedMs: clock.elapsed(),
|
|
273
|
+
baseline: 'updated',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Final failure: reuse the retained decoded images, paint the diff once, encode once.
|
|
277
|
+
const rawDiff = createDiff(previousActualImage, expectedImage, compareOpts);
|
|
278
|
+
const diff = await codec.encode(rawDiff);
|
|
279
|
+
throw new VisualMismatchError(previousActualBuffer, diff, {
|
|
280
|
+
expectedPath,
|
|
281
|
+
timeout,
|
|
282
|
+
attempts,
|
|
283
|
+
elapsedMs: clock.elapsed(),
|
|
284
|
+
comparison: previousComparison,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=expectScreenshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expectScreenshot.js","sourceRoot":"","sources":["../../../src/lib/visual/expectScreenshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controlled exports for the visual-testing feature. The public entrypoint
|
|
3
|
+
* (`src/index.ts`) re-exports from here; pngjs and other codec internals are
|
|
4
|
+
* never surfaced.
|
|
5
|
+
*/
|
|
6
|
+
export { compareScreenshots } from './compare.js';
|
|
7
|
+
export { VisualMismatchError } from './visualError.js';
|
|
8
|
+
export { runExpectScreenshot, shouldUpdateVisualBaselines, type CaptureFn, } from './expectScreenshot.js';
|
|
9
|
+
export type { ScreenshotCompareOptions, ExpectScreenshotOptions, VisualScreenshotOptions, VisualComparisonResult, ScreenshotMatchResult, } from './types.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/visual/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,KAAK,SAAS,GACf,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controlled exports for the visual-testing feature. The public entrypoint
|
|
3
|
+
* (`src/index.ts`) re-exports from here; pngjs and other codec internals are
|
|
4
|
+
* never surfaced.
|
|
5
|
+
*/
|
|
6
|
+
export { compareScreenshots } from './compare.js';
|
|
7
|
+
export { VisualMismatchError } from './visualError.js';
|
|
8
|
+
export { runExpectScreenshot, shouldUpdateVisualBaselines, } from './expectScreenshot.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/visual/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,mBAAmB,EACnB,2BAA2B,GAE5B,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level per-pixel math shared by the comparison loop and the anti-alias
|
|
3
|
+
* detector. Kept in its own module so `compare.ts` and `antialias.ts` can both
|
|
4
|
+
* use it without an import cycle.
|
|
5
|
+
*
|
|
6
|
+
* Every visible-colour computation composites over a **white** background. Two
|
|
7
|
+
* fully transparent pixels therefore composite to white and compare equal, and
|
|
8
|
+
* hidden RGB bytes under alpha zero never influence the result. This is the
|
|
9
|
+
* single documented background used everywhere: ordinary comparison, sibling
|
|
10
|
+
* equality, and the anti-alias brightness heuristic. If any of them used a
|
|
11
|
+
* different background they would disagree about transparent pixels.
|
|
12
|
+
*/
|
|
13
|
+
/** Composite one 0..255 channel with 0..255 alpha over a white background. */
|
|
14
|
+
export declare function blendOverWhite(channel: number, alpha: number): number;
|
|
15
|
+
/** ITU-R BT.601 luma of an already-composited RGB triple. */
|
|
16
|
+
export declare function rgb2y(r: number, g: number, b: number): number;
|
|
17
|
+
/** Read a pixel's white-composited visible RGB into `out` (length ≥ 3). */
|
|
18
|
+
export declare function visibleRgb(data: Uint8Array, pos: number, out: number[]): void;
|
|
19
|
+
/**
|
|
20
|
+
* Brightness (luma) delta between two pixels of the same image, both composited
|
|
21
|
+
* over white. This is Pixelmatch's `colorDelta(..., yOnly=true)` with
|
|
22
|
+
* `checkerboard: false`, i.e. white compositing to match craftdriver's contract.
|
|
23
|
+
*/
|
|
24
|
+
export declare function brightnessDelta(data: Uint8Array, pos1: number, pos2: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* Whether two pixels have the same white-composited visible colour. Used for
|
|
27
|
+
* sibling counting in the AA heuristic — deliberately compares visible colour,
|
|
28
|
+
* not the raw RGBA bytes, so transparent pixels with differing hidden RGB still
|
|
29
|
+
* count as identical siblings.
|
|
30
|
+
*/
|
|
31
|
+
export declare function visibleEqual(data: Uint8Array, pos1: number, pos2: number): boolean;
|
|
32
|
+
//# sourceMappingURL=pixelMath.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pixelMath.d.ts","sourceRoot":"","sources":["../../../src/lib/visual/pixelMath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGrE;AAED,6DAA6D;AAC7D,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAW7E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAUpF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAelF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level per-pixel math shared by the comparison loop and the anti-alias
|
|
3
|
+
* detector. Kept in its own module so `compare.ts` and `antialias.ts` can both
|
|
4
|
+
* use it without an import cycle.
|
|
5
|
+
*
|
|
6
|
+
* Every visible-colour computation composites over a **white** background. Two
|
|
7
|
+
* fully transparent pixels therefore composite to white and compare equal, and
|
|
8
|
+
* hidden RGB bytes under alpha zero never influence the result. This is the
|
|
9
|
+
* single documented background used everywhere: ordinary comparison, sibling
|
|
10
|
+
* equality, and the anti-alias brightness heuristic. If any of them used a
|
|
11
|
+
* different background they would disagree about transparent pixels.
|
|
12
|
+
*/
|
|
13
|
+
/** Composite one 0..255 channel with 0..255 alpha over a white background. */
|
|
14
|
+
export function blendOverWhite(channel, alpha) {
|
|
15
|
+
// 255 + (channel - 255) * (alpha / 255): alpha 255 → channel, alpha 0 → white.
|
|
16
|
+
return 255 + ((channel - 255) * alpha) / 255;
|
|
17
|
+
}
|
|
18
|
+
/** ITU-R BT.601 luma of an already-composited RGB triple. */
|
|
19
|
+
export function rgb2y(r, g, b) {
|
|
20
|
+
return r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
|
|
21
|
+
}
|
|
22
|
+
/** Read a pixel's white-composited visible RGB into `out` (length ≥ 3). */
|
|
23
|
+
export function visibleRgb(data, pos, out) {
|
|
24
|
+
const a = data[pos + 3];
|
|
25
|
+
if (a === 255) {
|
|
26
|
+
out[0] = data[pos];
|
|
27
|
+
out[1] = data[pos + 1];
|
|
28
|
+
out[2] = data[pos + 2];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
out[0] = blendOverWhite(data[pos], a);
|
|
32
|
+
out[1] = blendOverWhite(data[pos + 1], a);
|
|
33
|
+
out[2] = blendOverWhite(data[pos + 2], a);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Brightness (luma) delta between two pixels of the same image, both composited
|
|
38
|
+
* over white. This is Pixelmatch's `colorDelta(..., yOnly=true)` with
|
|
39
|
+
* `checkerboard: false`, i.e. white compositing to match craftdriver's contract.
|
|
40
|
+
*/
|
|
41
|
+
export function brightnessDelta(data, pos1, pos2) {
|
|
42
|
+
const a1 = data[pos1 + 3];
|
|
43
|
+
const a2 = data[pos2 + 3];
|
|
44
|
+
const r1 = a1 === 255 ? data[pos1] : blendOverWhite(data[pos1], a1);
|
|
45
|
+
const g1 = a1 === 255 ? data[pos1 + 1] : blendOverWhite(data[pos1 + 1], a1);
|
|
46
|
+
const b1 = a1 === 255 ? data[pos1 + 2] : blendOverWhite(data[pos1 + 2], a1);
|
|
47
|
+
const r2 = a2 === 255 ? data[pos2] : blendOverWhite(data[pos2], a2);
|
|
48
|
+
const g2 = a2 === 255 ? data[pos2 + 1] : blendOverWhite(data[pos2 + 1], a2);
|
|
49
|
+
const b2 = a2 === 255 ? data[pos2 + 2] : blendOverWhite(data[pos2 + 2], a2);
|
|
50
|
+
return rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether two pixels have the same white-composited visible colour. Used for
|
|
54
|
+
* sibling counting in the AA heuristic — deliberately compares visible colour,
|
|
55
|
+
* not the raw RGBA bytes, so transparent pixels with differing hidden RGB still
|
|
56
|
+
* count as identical siblings.
|
|
57
|
+
*/
|
|
58
|
+
export function visibleEqual(data, pos1, pos2) {
|
|
59
|
+
const a1 = data[pos1 + 3];
|
|
60
|
+
const a2 = data[pos2 + 3];
|
|
61
|
+
if (a1 === 255 && a2 === 255) {
|
|
62
|
+
return (data[pos1] === data[pos2] &&
|
|
63
|
+
data[pos1 + 1] === data[pos2 + 1] &&
|
|
64
|
+
data[pos1 + 2] === data[pos2 + 2]);
|
|
65
|
+
}
|
|
66
|
+
return (blendOverWhite(data[pos1], a1) === blendOverWhite(data[pos2], a2) &&
|
|
67
|
+
blendOverWhite(data[pos1 + 1], a1) === blendOverWhite(data[pos2 + 1], a2) &&
|
|
68
|
+
blendOverWhite(data[pos1 + 2], a1) === blendOverWhite(data[pos2 + 2], a2));
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=pixelMath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pixelMath.js","sourceRoot":"","sources":["../../../src/lib/visual/pixelMath.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap, allocation-bounding PNG validation performed *before* a buffer is
|
|
3
|
+
* handed to pngjs.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately not a decoder. It reads only the 8-byte signature and
|
|
6
|
+
* the fixed-layout IHDR chunk to learn the declared dimensions, so a
|
|
7
|
+
* hostile/corrupt header cannot make the decoder allocate a huge RGBA array
|
|
8
|
+
* before the real (CRC-checked) decode runs. Both limits are enforced with
|
|
9
|
+
* overflow-safe arithmetic. The same checks apply to the baseline and to every
|
|
10
|
+
* unique actual screenshot.
|
|
11
|
+
*/
|
|
12
|
+
export interface PngSizeLimits {
|
|
13
|
+
maxImagePixels: number;
|
|
14
|
+
maxInputBytes: number;
|
|
15
|
+
}
|
|
16
|
+
/** Where this buffer came from, for a clearer error message. */
|
|
17
|
+
export type PngSource = 'baseline' | 'screenshot';
|
|
18
|
+
/**
|
|
19
|
+
* Validate a compressed PNG's signature, IHDR, declared dimensions, and both
|
|
20
|
+
* size limits. Returns the parsed dimensions so callers can reuse them. Throws
|
|
21
|
+
* {@link CraftdriverError} with `INVALID_ARGUMENT` on any violation.
|
|
22
|
+
*/
|
|
23
|
+
export declare function validatePngHeaderAndSize(buffer: Uint8Array, limits: PngSizeLimits, source: PngSource): {
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=pngHeader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pngHeader.d.ts","sourceRoot":"","sources":["../../../src/lib/visual/pngHeader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAcH,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,gEAAgE;AAChE,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;AAQlD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,SAAS,GAChB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA2CnC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap, allocation-bounding PNG validation performed *before* a buffer is
|
|
3
|
+
* handed to pngjs.
|
|
4
|
+
*
|
|
5
|
+
* This is deliberately not a decoder. It reads only the 8-byte signature and
|
|
6
|
+
* the fixed-layout IHDR chunk to learn the declared dimensions, so a
|
|
7
|
+
* hostile/corrupt header cannot make the decoder allocate a huge RGBA array
|
|
8
|
+
* before the real (CRC-checked) decode runs. Both limits are enforced with
|
|
9
|
+
* overflow-safe arithmetic. The same checks apply to the baseline and to every
|
|
10
|
+
* unique actual screenshot.
|
|
11
|
+
*/
|
|
12
|
+
import { CraftdriverError, ErrorCode } from '../errors.js';
|
|
13
|
+
/** 8-byte PNG magic signature. */
|
|
14
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
15
|
+
/** Byte offset of the IHDR chunk's length field (immediately after the signature). */
|
|
16
|
+
const IHDR_LENGTH_OFFSET = 8;
|
|
17
|
+
/** IHDR chunk data is a fixed 13 bytes. */
|
|
18
|
+
const IHDR_DATA_LENGTH = 13;
|
|
19
|
+
/** Minimum bytes we must see to read the full IHDR: sig + len(4) + type(4) + data(13) + crc(4). */
|
|
20
|
+
const MIN_HEADER_BYTES = IHDR_LENGTH_OFFSET + 4 + 4 + IHDR_DATA_LENGTH + 4;
|
|
21
|
+
function fail(source, message, detail) {
|
|
22
|
+
throw new CraftdriverError(ErrorCode.INVALID_ARGUMENT, `${source} ${message}`, {
|
|
23
|
+
detail: { source, ...detail },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Validate a compressed PNG's signature, IHDR, declared dimensions, and both
|
|
28
|
+
* size limits. Returns the parsed dimensions so callers can reuse them. Throws
|
|
29
|
+
* {@link CraftdriverError} with `INVALID_ARGUMENT` on any violation.
|
|
30
|
+
*/
|
|
31
|
+
export function validatePngHeaderAndSize(buffer, limits, source) {
|
|
32
|
+
if (buffer.length > limits.maxInputBytes) {
|
|
33
|
+
fail(source, `PNG is ${buffer.length} bytes, exceeding the ${limits.maxInputBytes}-byte limit.`, {
|
|
34
|
+
inputBytes: buffer.length,
|
|
35
|
+
maxInputBytes: limits.maxInputBytes,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (buffer.length < MIN_HEADER_BYTES) {
|
|
39
|
+
fail(source, 'PNG is too small to contain a valid header.', { inputBytes: buffer.length });
|
|
40
|
+
}
|
|
41
|
+
const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
42
|
+
if (!buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
43
|
+
fail(source, 'buffer is not a PNG (bad signature).');
|
|
44
|
+
}
|
|
45
|
+
const ihdrLength = buf.readUInt32BE(IHDR_LENGTH_OFFSET);
|
|
46
|
+
const ihdrType = buf.toString('ascii', IHDR_LENGTH_OFFSET + 4, IHDR_LENGTH_OFFSET + 8);
|
|
47
|
+
if (ihdrType !== 'IHDR' || ihdrLength !== IHDR_DATA_LENGTH) {
|
|
48
|
+
fail(source, 'PNG has an invalid IHDR chunk.', { ihdrType, ihdrLength });
|
|
49
|
+
}
|
|
50
|
+
const dataOffset = IHDR_LENGTH_OFFSET + 8;
|
|
51
|
+
const width = buf.readUInt32BE(dataOffset);
|
|
52
|
+
const height = buf.readUInt32BE(dataOffset + 4);
|
|
53
|
+
if (width === 0 || height === 0) {
|
|
54
|
+
fail(source, `PNG has zero dimension (${width}x${height}).`, { width, height });
|
|
55
|
+
}
|
|
56
|
+
// Overflow-safe: compare against the limit using division so width*height
|
|
57
|
+
// never overflows a JS safe integer for adversarial 2^32-scale dimensions.
|
|
58
|
+
if (width > limits.maxImagePixels || height > limits.maxImagePixels / width) {
|
|
59
|
+
fail(source, `PNG is ${width}x${height} pixels, exceeding the ${limits.maxImagePixels}-pixel limit.`, { width, height, pixels: width * height, maxImagePixels: limits.maxImagePixels });
|
|
60
|
+
}
|
|
61
|
+
return { width, height };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=pngHeader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pngHeader.js","sourceRoot":"","sources":["../../../src/lib/visual/pngHeader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolated pngjs adapter — the single place that touches the CommonJS-shaped
|
|
3
|
+
* `pngjs` API. The rest of craftdriver is ESM and works only with the
|
|
4
|
+
* promise-based {@link PngCodec} and normalized {@link DecodedImage} defined in
|
|
5
|
+
* `./codec`; pngjs types never leak past this file.
|
|
6
|
+
*
|
|
7
|
+
* Decode and encode both use pngjs's asynchronous stream APIs (`parse` /
|
|
8
|
+
* `pack`) rather than the synchronous variants, so a large image cannot block
|
|
9
|
+
* the event loop inside a single call. CRC checking is left at pngjs's default
|
|
10
|
+
* (enabled) so corrupt chunks are rejected rather than silently decoded.
|
|
11
|
+
*
|
|
12
|
+
* Maintenance risk (recorded, not hidden): `pngjs@7.0.0` is pinned exactly. It
|
|
13
|
+
* had no reported npm/OSV advisory as of 2026-07-17, but that is a timestamped
|
|
14
|
+
* check, not proof of safety — its last release was 2023 and it publishes no
|
|
15
|
+
* security policy. It relies on Node's built-in `zlib`, so keeping Node patched
|
|
16
|
+
* is part of the posture. CI runs `npm audit --omit=dev`; this seam is kept
|
|
17
|
+
* narrow (see `./codec`) so the codec can be replaced without touching callers
|
|
18
|
+
* if the project goes unmaintained or an advisory lands.
|
|
19
|
+
*/
|
|
20
|
+
import type { PngCodec } from './codec.js';
|
|
21
|
+
export declare const pngjsCodec: PngCodec;
|
|
22
|
+
//# sourceMappingURL=pngjsCodec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pngjsCodec.d.ts","sourceRoot":"","sources":["../../../src/lib/visual/pngjsCodec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EAAgB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAwDzD,eAAO,MAAM,UAAU,EAAE,QAA6B,CAAC"}
|