Skip to content

[INT-18] send pdf snapshots to Visual #191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,777 changes: 2,659 additions & 118 deletions visual-js/visual-snapshots/package-lock.json

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions visual-js/visual-snapshots/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name": "@saucelabs/visual-snapshots",
"description": "CLI which generates Visual snapshots from a data source such as pdf",
"version": "0.1.0",
"main": "lib/index.js",
"main": "./lib/src/index.js",
"license": "MIT",
"bin": "./lib/index.js",
"bin": "./lib/src/index.js",
"files": [
"lib",
"README.md"
Expand All @@ -24,10 +24,13 @@
"watch": "tsc-watch --declaration -p .",
"lint": "eslint \"{src,test}/**/*.ts\"",
"lint-fix": "eslint \"{src,test}/**/*.ts\" --fix",
"test": "jest"
"test": "jest",
"test-with-coverage": "jest --collect-coverage"
},
"dependencies": {
"commander": "^12.0.0"
"@saucelabs/visual": "0.13.0",
"commander": "^12.0.0",
"pdf-to-img": "4.4.0"
},
"devDependencies": {
"@types/jest": "29.5.14",
Expand Down
19 changes: 19 additions & 0 deletions visual-js/visual-snapshots/src/api/visual-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getApi, SauceRegion } from "@saucelabs/visual";

export interface VisualApiParams {
username: string;
accessKey: string;
region: SauceRegion;
}

export const initializeVisualApi = (params: VisualApiParams) =>
getApi(
{
user: params.username,
key: params.accessKey,
region: params.region,
},
{
userAgent: "visual-snapshots",
},
);
86 changes: 86 additions & 0 deletions visual-js/visual-snapshots/src/api/visual-snapshots-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { DiffingMethod, VisualApi } from "@saucelabs/visual";

export interface CreateVisualSnapshotsParams {
branch: string;
buildName: string;
defaultBranch: string;
project: string;
customId: string;
buildId: string;
}

export class VisualSnapshotsApi {
private api: VisualApi;

constructor(api: VisualApi) {
this.api = api;
}

public async generateAndSendPdfFilSnapshots(
pdfFilePages: AsyncGenerator<Buffer>,
params: CreateVisualSnapshotsParams,
) {
const buildId = await this.createBuild(params);

let pageNumber = 1;
for await (const pdfPageImage of pdfFilePages) {
await this.uploadImageAndCreateSnapshot(
pdfPageImage,
buildId,
pageNumber,
);
pageNumber++;
}

await this.finishBuild(buildId);
}

private async createBuild(
params: CreateVisualSnapshotsParams,
): Promise<string> {
const build = await this.api.createBuild({
name: params.buildName,
branch: params.branch,
defaultBranch: params.defaultBranch,
project: params.project,
customId: params.customId,
});
console.info(`Build ${build.id} created.`);
return build.id;
}

private async uploadImageAndCreateSnapshot(
snapshot: Buffer,
buildId: string,
snapshotId: number,
) {
const uploadId = await this.api.uploadSnapshot({
buildId,
image: { data: snapshot },
});

console.info(`Uploaded image to build ${buildId}: upload id=${uploadId}.`);

const snapshotName = `page-${snapshotId}`;
const snapshotMetadata = {
diffingMethod: DiffingMethod.Balanced,
buildUuid: buildId,
name: snapshotName,
};

await this.api.createSnapshot({
...snapshotMetadata,
buildUuid: buildId,
uploadUuid: uploadId,
});

console.info(`Created a snapshot ${snapshotName} for build ${buildId}.`);
}

private async finishBuild(buildId: string) {
await this.api.finishBuild({
uuid: buildId,
});
console.info(`Build ${buildId} finished.`);
}
}
11 changes: 11 additions & 0 deletions visual-js/visual-snapshots/src/app/pdf-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { pdf } from "pdf-to-img";

export class PdfConverter {
public async *convertPagesToImages(
pdfFilePath: string,
): AsyncGenerator<Buffer> {
for await (const pdfPageImage of await pdf(pdfFilePath)) {
yield pdfPageImage;
}
}
}
24 changes: 24 additions & 0 deletions visual-js/visual-snapshots/src/app/pdf-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
CreateVisualSnapshotsParams,
VisualSnapshotsApi,
} from "../api/visual-snapshots-api.js";
import { initializeVisualApi, VisualApiParams } from "../api/visual-client.js";
import { PdfConverter } from "./pdf-converter.js";

export interface PdfCommandParams
extends VisualApiParams,
CreateVisualSnapshotsParams {}

export class PdfCommandHandler {
public async handle(pdfFilePath: string, params: PdfCommandParams) {
const visualApi = initializeVisualApi(params as VisualApiParams);
const visualSnapshots = new VisualSnapshotsApi(visualApi);
const pdfConverter = new PdfConverter();

const pdfPageImages = await pdfConverter.convertPagesToImages(pdfFilePath);
await visualSnapshots.generateAndSendPdfFilSnapshots(
pdfPageImages,
params as CreateVisualSnapshotsParams,
);
}
}
14 changes: 10 additions & 4 deletions visual-js/visual-snapshots/src/commands/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
regionOption,
usernameOption,
} from "./options.js";
import { PdfCommandHandler, PdfCommandParams } from "../app/pdf-handler.js";

export const pdfCommand = () => {
return new Command()
Expand All @@ -25,9 +26,14 @@ export const pdfCommand = () => {
.addOption(projectOption)
.addOption(buildIdOption)
.addOption(customIdOption)
.action((pdfFilePath: string, options: Record<string, string>) => {
console.info(
`Create snapshots of a pdf file: '${pdfFilePath}' with options: ${Object.entries(options)}`,
);
.action((pdfFilePath: string, params: PdfCommandParams) => {
new PdfCommandHandler()
.handle(pdfFilePath, params)
.then(() => {
console.log("Successfully created PDF snapshots");
})
.catch((err) => {
console.error(`An error occured when creating PDF snapshots: ${err}`);
});
});
};
128 changes: 128 additions & 0 deletions visual-js/visual-snapshots/test/api/visual-api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { DiffingMethod, VisualApi } from "@saucelabs/visual";
import {
CreateVisualSnapshotsParams,
VisualSnapshotsApi,
} from "../../src/api/visual-snapshots-api.js";

async function* pdfPagesGenerator(): AsyncGenerator<Buffer> {
for (let i = 0; i < 2; ++i) {
yield Promise.resolve(Buffer.from(`fake-image-buffer-${i}`));
}
}

describe("VisualSnapshots", () => {
describe("generateAndSendPdfFilSnapshots", () => {
const consoleInfoSpy = jest
.spyOn(console, "info")
.mockImplementation(() => {});

let pdfPages: AsyncGenerator<Buffer>;

const createBuildMock = jest.fn();
const uploadSnapshotMock = jest.fn();
const createSnapshotMock = jest.fn();
const finishBuildMock = jest.fn();
const visualApiMock: VisualApi = {
...jest.requireActual<VisualApi>("@saucelabs/visual"),
createBuild: createBuildMock,
uploadSnapshot: uploadSnapshotMock,
createSnapshot: createSnapshotMock,
finishBuild: finishBuildMock,
};
const visualSnapshots = new VisualSnapshotsApi(visualApiMock);

beforeEach(() => {
createBuildMock.mockReset();
createBuildMock.mockReturnValueOnce(Promise.resolve({ id: "build-id" }));
uploadSnapshotMock.mockReset();
uploadSnapshotMock
.mockReturnValueOnce(Promise.resolve("upload-id-0"))
.mockReturnValueOnce(Promise.resolve("upload-id-1"));
createSnapshotMock.mockReset();
finishBuildMock.mockReset();
consoleInfoSpy.mockReset();

pdfPages = pdfPagesGenerator();
});

const assertSuccessfulPdfSnapshotsGeneration = (
params: CreateVisualSnapshotsParams,
) => {
expect(createBuildMock).toHaveBeenCalledWith({
name: params.buildName,
branch: params.branch,
defaultBranch: params.defaultBranch,
project: params.project,
customId: params.customId,
});

expect(uploadSnapshotMock.mock.calls).toEqual([
[
{
buildId: "build-id",
image: { data: Buffer.from("fake-image-buffer-0") },
},
],
[
{
buildId: "build-id",
image: { data: Buffer.from("fake-image-buffer-1") },
},
],
]);

expect(createSnapshotMock.mock.calls).toEqual([
[
{
diffingMethod: DiffingMethod.Balanced,
buildUuid: "build-id",
name: "page-1",
uploadUuid: "upload-id-0",
},
],
[
{
diffingMethod: DiffingMethod.Balanced,
buildUuid: "build-id",
name: "page-2",
uploadUuid: "upload-id-1",
},
],
]);

expect(finishBuildMock).toHaveBeenCalledWith({
uuid: "build-id",
});

expect(consoleInfoSpy.mock.calls).toEqual([
["Build build-id created."],
["Uploaded image to build build-id: upload id=upload-id-0."],
["Created a snapshot page-1 for build build-id."],
["Uploaded image to build build-id: upload id=upload-id-1."],
["Created a snapshot page-2 for build build-id."],
["Build build-id finished."],
]);
};

test("with params", async () => {
const params = {
branch: "fake-branch",
buildName: "fake-build-name",
defaultBranch: "fake-default-branch",
project: "fake-project",
customId: "fake-custom-id",
buildId: "fake-build-id",
} as CreateVisualSnapshotsParams;
await visualSnapshots.generateAndSendPdfFilSnapshots(pdfPages, params);

assertSuccessfulPdfSnapshotsGeneration(params);
});

test("without params", async () => {
const params = {} as CreateVisualSnapshotsParams;
await visualSnapshots.generateAndSendPdfFilSnapshots(pdfPages, params);

assertSuccessfulPdfSnapshotsGeneration(params);
});
});
});
35 changes: 35 additions & 0 deletions visual-js/visual-snapshots/test/api/visual-client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
initializeVisualApi,
VisualApiParams,
} from "../../src/api/visual-client.js";
import * as sauceVisual from "@saucelabs/visual";

jest.mock("@saucelabs/visual", () => {
return {
getApi: jest.fn(),
};
});

describe("visual api client", () => {
test("initializeVisualApi", async () => {
const getApiSpy = sauceVisual.getApi;

const params = {
username: "fake-username",
accessKey: "fake-access-key",
region: "us-west-1",
} as VisualApiParams;
await initializeVisualApi(params);

expect(getApiSpy).toHaveBeenCalledWith(
{
user: params.username,
key: params.accessKey,
region: params.region,
},
{
userAgent: "visual-snapshots",
},
);
});
});
Loading