From 974049dcd5f72c971190b3e0921b9dc53bb4ea06 Mon Sep 17 00:00:00 2001 From: RaghavsBrowserStack Date: Mon, 7 Sep 2026 18:37:40 +0530 Subject: [PATCH 01/13] feat(cli-pdf): native PDF visual testing via POST /percy/pdf/snapshot Adds first-class PDF support to the CLI so an SDK can hand over PDF bytes and get one Percy snapshot per page, with synchronous comparison results. This is the replacement for the external percy-pdf solution, which wrapped the CLI from outside by serving a pdf.js viewer and driving Percy's renderer through the viewer's DOM with per-page `execute` scripts. New package @percy/cli-pdf is a leaf library: PDF bytes in, page rasters and their root DOM out. It holds no reference to @percy/core, which is what lets core list it as an optionalDependency without a cycle -- users who never snapshot a PDF do not install pdfjs-dist or the @napi-rs/canvas prebuilds. @percy/core gains the POST /percy/pdf/snapshot route and pdf-snapshot.js, which validates the request, decodes the base64 document, lazily imports @percy/cli-pdf, and pushes one snapshot per selected page through percy.upload() with `resources` as a function so rasterizing happens inside the queue task and inherits its concurrency. Each page carries resources and no `tag`, so createSnapshotsQueue routes it via client.sendSnapshot -- these are real web snapshots, not comparisons. This mirrors cli-upload's web-token path. The document travels as base64 in an ordinary JSON body and the sync response is always a JSON object (never a bare array) carrying a per-page array. Both are deliberate: every SDK, including the .NET wrapper's Dictionary-to-JSON helper and its JObject.Parse of the response, can call this with the HTTP client it already has, with no multipart or streaming code. Page snapshots are named ` | Page N`, matching percy-pdf exactly so teams migrating keep their approved baselines instead of orphaning them. Oversized pages are fitted rather than rejected: Legal (1224x2016 at scale 2) and A3 exceed Percy's 2000px cap and are exactly the documents this targets, so fitScale reduces the scale deterministically from the page's own dimensions and warns. pdfjs-dist is pinned to 4.x rather than 6.x, which requires Node >=22.13. @percy/sdk-utils exports postPdfSnapshot as the shared seam every SDK wraps. Note that sync mode is only reachable through this endpoint under `percy exec`: percy.syncMode() force-disables sync under skipUploads/deferUploads/delayUploads, which the `snapshot` and `upload` commands set. A `percy pdf ` command could therefore never return comparison results, so none is added. Co-Authored-By: Claude Opus 5 (1M context) --- packages/cli-pdf/package.json | 35 ++ packages/cli-pdf/src/index.js | 11 + packages/cli-pdf/src/page-html.js | 46 +++ packages/cli-pdf/src/pages.js | 93 +++++ packages/cli-pdf/src/rasterize.js | 150 +++++++++ packages/cli-pdf/test/.eslintrc | 6 + packages/cli-pdf/test/fixture.js | 53 +++ packages/cli-pdf/test/page-html.test.js | 51 +++ packages/cli-pdf/test/pages.test.js | 79 +++++ packages/cli-pdf/test/rasterize.test.js | 95 ++++++ packages/core/package.json | 3 +- packages/core/src/api.js | 3 + packages/core/src/config.js | 47 ++- packages/core/src/pdf-snapshot.js | 243 ++++++++++++++ packages/core/test/pdf-snapshot.test.js | 355 ++++++++++++++++++++ packages/sdk-utils/src/index.js | 2 + packages/sdk-utils/src/post-pdf-snapshot.js | 30 ++ packages/sdk-utils/test/index.test.js | 72 ++++ yarn.lock | 79 +++++ 19 files changed, 1451 insertions(+), 2 deletions(-) create mode 100644 packages/cli-pdf/package.json create mode 100644 packages/cli-pdf/src/index.js create mode 100644 packages/cli-pdf/src/page-html.js create mode 100644 packages/cli-pdf/src/pages.js create mode 100644 packages/cli-pdf/src/rasterize.js create mode 100644 packages/cli-pdf/test/.eslintrc create mode 100644 packages/cli-pdf/test/fixture.js create mode 100644 packages/cli-pdf/test/page-html.test.js create mode 100644 packages/cli-pdf/test/pages.test.js create mode 100644 packages/cli-pdf/test/rasterize.test.js create mode 100644 packages/core/src/pdf-snapshot.js create mode 100644 packages/core/test/pdf-snapshot.test.js create mode 100644 packages/sdk-utils/src/post-pdf-snapshot.js diff --git a/packages/cli-pdf/package.json b/packages/cli-pdf/package.json new file mode 100644 index 000000000..1019169e5 --- /dev/null +++ b/packages/cli-pdf/package.json @@ -0,0 +1,35 @@ +{ + "name": "@percy/cli-pdf", + "version": "1.32.8", + "license": "MIT", + "description": "Rasterizes PDF documents into per-page images for Percy snapshots", + "repository": { + "type": "git", + "url": "https://github.com/percy/cli", + "directory": "packages/cli-pdf" + }, + "publishConfig": { + "access": "public", + "tag": "latest" + }, + "engines": { + "node": ">=18" + }, + "files": [ + "dist" + ], + "main": "./dist/index.js", + "type": "module", + "exports": "./dist/index.js", + "scripts": { + "build": "node ../../scripts/build", + "lint": "eslint --ignore-path ../../.gitignore .", + "test": "node ../../scripts/test", + "test:coverage": "yarn test --coverage" + }, + "dependencies": { + "@napi-rs/canvas": "^0.1.60", + "@percy/logger": "1.32.8", + "pdfjs-dist": "^4.8.69" + } +} diff --git a/packages/cli-pdf/src/index.js b/packages/cli-pdf/src/index.js new file mode 100644 index 000000000..db032e860 --- /dev/null +++ b/packages/cli-pdf/src/index.js @@ -0,0 +1,11 @@ +// @percy/cli-pdf +// +// A leaf library: PDF bytes in, page rasters and their root DOM out. It holds +// no reference to @percy/core and knows nothing about builds, queues or +// resources -- core's pdf-snapshot.js owns all of that. Keeping the dependency +// pointing this way is what lets core list this package as an +// optionalDependency without creating a cycle. + +export { rasterizePdf, DEFAULT_SCALE, MAX_SCALE, MIN_DIMENSION, MAX_DIMENSION } from './rasterize.js'; +export { resolvePages } from './pages.js'; +export { buildPageHtml } from './page-html.js'; diff --git a/packages/cli-pdf/src/page-html.js b/packages/cli-pdf/src/page-html.js new file mode 100644 index 000000000..8c0c12b54 --- /dev/null +++ b/packages/cli-pdf/src/page-html.js @@ -0,0 +1,46 @@ +// Generates the root DOM for a single rasterized PDF page. +// +// Modelled on cli-upload's getImageResources: a minimal document that displays +// one image at its native size with no margins, padding or font metrics that +// could shift between renders. Anything more elaborate here becomes a source of +// diffs unrelated to the PDF itself. + +// `title` is user-controlled (it is the snapshot name) and lands inside +// , so it must be escaped -- an unescaped `' }); + + expect(html).not.toContain('' }); - - expect(html).not.toContain('', + imageUrl: 'http://local/a"b.png' + }); + + expect(html).not.toContain('