UNPKG

@btravers/pdf-snapshot-jest

Version:
237 lines (200 loc) 5.94 kB
import * as path from 'node:path'; import * as fs from 'node:fs'; import fetch from 'node-fetch-commonjs'; import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import _ from 'lodash'; import dedent from 'dedent'; import * as process from 'process'; import type { AppRouter } from '@btravers/pdf-snapshot-service/dist/router'; const globalAny = global as any; globalAny.fetch = fetch; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace jest { interface Matchers<R> { toMatchPdfSnapshot(options?: Options): Promise<R>; } } } type Options = { scale?: number; failureThreshold?: number; }; type Result = { pass: boolean; message: () => string; }; expect.extend({ /** * Jest matcher for visual regression testing of PDF document. * Behaves just like tradition Jest snapshots. * * @param pdf PDF content as Buffer. * @param options Comparison options. */ async toMatchPdfSnapshot(pdf: Buffer, options?: Options): Promise<Result> { const { testPath, currentTestName, isNot, snapshotState } = this; if (isNot) { throw new Error( 'Jest: `.not` cannot be used with `.toMatchPdfSnapshot()`.', ); } const snapshotsDirectory = path.join( path.dirname(testPath ?? ''), '__pdf_snapshots__', ); await fs.promises.mkdir(snapshotsDirectory, { recursive: true, }); // remove old diff files const diffFilesToRemove = ( await fs.promises.readdir(snapshotsDirectory) ).filter((file) => file.endsWith('-diff.png')); await Promise.all( diffFilesToRemove.map((file) => fs.promises.rm(path.join(snapshotsDirectory, file)), ), ); /** * Create snapshot file identifier. * * @param pageNumber The page of the pdf file. */ function snapshotIdentifier(pageNumber: number): string { return `${pageNumber.toString().padStart(2, '0')}_${_.kebabCase( currentTestName, )}-snap`; } /** * Write file from base64 encoded content. * * @param data Base64 encoded data. * @param filePath File path. */ async function writeFile(data: string, filePath: string): Promise<void> { const decodedData = Buffer.from(data, 'base64'); await fs.promises.writeFile(filePath, decodedData); } /** * Get the list of existing snapshots. */ async function getSnapshots(): Promise<Buffer[]> { const result: Buffer[] = []; let i = 1; do { const snapshotPath = path.join( snapshotsDirectory, `${snapshotIdentifier(i)}.png`, ); try { result.push(await fs.promises.readFile(snapshotPath)); } catch (e) { break; } i++; } while (true); return result; } const trpc = createTRPCProxyClient<AppRouter>({ links: [ httpBatchLink({ url: process.env.PDF_SNAPSHOT_SERVER_URL ?? 'http://localhost:3000', }), ], }); const snapshots = (await getSnapshots()).map((snapshot) => snapshot.toString('base64'), ); const { results } = await trpc.matchPdfSnapshot.mutate({ pdf: pdf.toString('base64'), snapshots, options: { scale: options?.scale, failureThreshold: options?.failureThreshold, }, }); if (_.every(results, 'pass')) { snapshotState.matched++; return { pass: true, message: () => '', }; } if (_.every(results, 'added')) { snapshotState.added++; await Promise.all( results.map(async (result, index) => { if ('newPage' in result) { const snapshotPath = path.join( snapshotsDirectory, `${snapshotIdentifier(index + 1)}.png`, ); await writeFile(result.newPage, snapshotPath); } }), ); return { pass: true, message: () => '', }; } if (snapshotState._updateSnapshot === 'all') { snapshotState.updated++; await Promise.all( results.map(async (result, index) => { if ('pass' in result && result.pass) { return; } const pageNumber = index + 1; const snapshotPath = path.join( snapshotsDirectory, `${snapshotIdentifier(pageNumber)}.png`, ); if ('deleted' in result && result.deleted) { await fs.promises.rm(snapshotPath); return; } if ('newPage' in result) { await writeFile(result.newPage, snapshotPath); return; } }), ); return { pass: true, message: () => '', }; } snapshotState.unmatched++; const resultDetails = await Promise.all( results.map(async (result, index) => { const pageNumber = index + 1; const diffOutputPath = path.join( snapshotsDirectory, `${snapshotIdentifier(pageNumber)}-diff.png`, ); if ('added' in result && result.added) { return `[Page ${pageNumber}] - Added`; } if ('deleted' in result && result.deleted) { return `[Page ${pageNumber}] - Deleted`; } if ('pass' in result && result.pass) { return `[Page ${pageNumber}] - Pass - diffRatio ${result.diffRatio}`; } if ('pass' in result && !result.pass) { await writeFile(result.diffImage, diffOutputPath); return `[Page ${pageNumber}] - Does not match snapshot - diffRatio ${result.diffRatio}`; } return `[Page ${pageNumber}] - Unhandled result`; }), ); return { pass: false, message: () => dedent` Does not match with snapshot. ${resultDetails.join('\n')} `, }; }, });