Skip to content

Commit

Permalink
feat(playwright): support playwright trace
Browse files Browse the repository at this point in the history
  • Loading branch information
gregberge committed Nov 6, 2023
1 parent a3a432e commit d194e04
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 36 deletions.
15 changes: 13 additions & 2 deletions packages/core/src/api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe("#createArgosApiClient", () => {
const result = await apiClient.createBuild({
commit: "f16f980bd17cccfa93a1ae7766727e67950773d0",
screenshotKeys: ["123", "456"],
pwTraces: [],
});
expect(result).toEqual({
build: {
Expand All @@ -49,8 +50,18 @@ describe("#createArgosApiClient", () => {
const result = await apiClient.updateBuild({
buildId: "123",
screenshots: [
{ key: "123", name: "screenshot 1", metadata: null },
{ key: "456", name: "screenshot 2", metadata: null },
{
key: "123",
name: "screenshot 1",
metadata: null,
pwTraceKey: null,
},
{
key: "456",
name: "screenshot 2",
metadata: null,
pwTraceKey: null,
},
],
});
expect(result).toEqual({
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export interface ApiClientOptions {
export interface CreateBuildInput {
commit: string;
screenshotKeys: string[];
pwTraces: {
screenshotKey: string;
traceKey: string;
}[];
branch?: string | null;
name?: string | null;
parallel?: boolean | null;
Expand All @@ -28,6 +32,7 @@ export interface CreateBuildOutput {
screenshots: {
key: string;
putUrl: string;
putTraceUrl?: string;
}[];
}

Expand All @@ -37,6 +42,7 @@ export interface UpdateBuildInput {
key: string;
name: string;
metadata: ScreenshotMetadata | null;
pwTraceKey: string | null;
}[];
parallel?: boolean | null;
parallelTotal?: number | null;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/s3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe("#upload", () => {
await upload({
path: join(__dirname, "../../../__fixtures__/screenshots/penelope.png"),
url: "https://api.s3.dev/upload/123",
contentType: "image/png",
});
});
});
3 changes: 2 additions & 1 deletion packages/core/src/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import axios from "axios";
interface UploadInput {
url: string;
path: string;
contentType: string;
}

export const upload = async (input: UploadInput) => {
Expand All @@ -13,7 +14,7 @@ export const upload = async (input: UploadInput) => {
url: input.url,
data: file,
headers: {
"Content-Type": "image/png",
"Content-Type": input.contentType,
},
});
};
3 changes: 3 additions & 0 deletions packages/core/src/upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe("#upload", () => {
path: expect.stringMatching(
/__fixtures__\/screenshots\/penelope\.jpg$/,
),
pwTrace: null,
optimizedPath: expect.any(String),
hash: expect.stringMatching(/^[A-Fa-f0-9]{64}$/),
metadata: null,
Expand All @@ -31,6 +32,7 @@ describe("#upload", () => {
path: expect.stringMatching(
/__fixtures__\/screenshots\/penelope\.png$/,
),
pwTrace: null,
optimizedPath: expect.any(String),
hash: expect.stringMatching(/^[A-Fa-f0-9]{64}$/),
metadata: {
Expand Down Expand Up @@ -60,6 +62,7 @@ describe("#upload", () => {
path: expect.stringMatching(
/__fixtures__\/screenshots\/nested\/alicia\.jpg$/,
),
pwTrace: null,
optimizedPath: expect.any(String),
hash: expect.stringMatching(/^[A-Fa-f0-9]{64}$/),
metadata: null,
Expand Down
78 changes: 66 additions & 12 deletions packages/core/src/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createArgosApiClient, getBearerToken } from "./api-client";
import { upload as uploadToS3 } from "./s3";
import { debug, debugTime, debugTimeEnd } from "./debug";
import { chunk } from "./util/chunk";
import { readMetadata } from "@argos-ci/util";
import { getPlaywrightTracePath, readMetadata } from "@argos-ci/util";

/**
* Size of the chunks used to upload screenshots to Argos.
Expand Down Expand Up @@ -84,26 +84,59 @@ export const upload = async (params: UploadParameters) => {
// Optimize & compute hashes
const screenshots = await Promise.all(
foundScreenshots.map(async (screenshot) => {
const [metadata, optimizedPath] = await Promise.all([
const [metadata, pwTracePath, optimizedPath] = await Promise.all([
readMetadata(screenshot.path),
getPlaywrightTracePath(screenshot.path),
optimizeScreenshot(screenshot.path),
]);
const hash = await hashFile(optimizedPath);
return { ...screenshot, metadata, optimizedPath, hash };
const [hash, pwTraceHash] = await Promise.all([
hashFile(optimizedPath),
pwTracePath ? hashFile(pwTracePath) : null,
]);
return {
...screenshot,
hash,
optimizedPath,
metadata,
pwTrace:
pwTracePath && pwTraceHash
? { path: pwTracePath, hash: pwTraceHash }
: null,
};
}),
);

// Create build
debug("Creating build");
const screenshotKeys = Array.from(
new Set(screenshots.map((screenshot) => screenshot.hash)),
);
const pwTraces = screenshotKeys.reduce(
(pwTraces, key) => {
const screenshot = screenshots.find(
(screenshot) => screenshot.hash === key,
);
if (!screenshot) {
throw new Error(`Invariant: screenshot with hash ${key} not found`);
}
if (screenshot.pwTrace) {
pwTraces.push({
screenshotKey: screenshot.hash,
traceKey: screenshot.pwTrace.hash,
});
}
return pwTraces;
},
[] as { screenshotKey: string; traceKey: string }[],
);
const result = await apiClient.createBuild({
commit: config.commit,
branch: config.branch,
name: config.buildName,
parallel: config.parallel,
parallelNonce: config.parallelNonce,
screenshotKeys: Array.from(
new Set(screenshots.map((screenshot) => screenshot.hash)),
),
screenshotKeys,
pwTraces,
prNumber: config.prNumber,
prHeadCommit: config.prHeadCommit,
referenceBranch: config.referenceBranch,
Expand All @@ -118,19 +151,39 @@ export const upload = async (params: UploadParameters) => {
debug(`Starting upload of ${chunks.length} chunks`);

for (let i = 0; i < chunks.length; i++) {
// Upload screenshots
debug(`Uploading chunk ${i + 1}/${chunks.length}`);
const timeLabel = `Chunk ${i + 1}/${chunks.length}`;
debugTime(timeLabel);
await Promise.all(
chunks[i].map(async ({ key, putUrl }) => {
chunks[i].map(async ({ key, putUrl, putTraceUrl }) => {
const screenshot = screenshots.find((s) => s.hash === key);
if (!screenshot) {
throw new Error(`Invariant: screenshot with hash ${key} not found`);
}
await uploadToS3({
url: putUrl,
path: screenshot.optimizedPath,
});
await Promise.all([
// Upload screenshot
uploadToS3({
url: putUrl,
path: screenshot.optimizedPath,
contentType: "image/png",
}),
// Upload trace
(async () => {
if (putTraceUrl) {
if (!screenshot.pwTrace) {
throw new Error(
`Invariant: screenshot with hash ${key} has a putTraceUrl but no pwTrace`,
);
}
await uploadToS3({
url: putTraceUrl,
path: screenshot.pwTrace.path,
contentType: "application/zip",
});
}
})(),
]);
}),
);
debugTimeEnd(timeLabel);
Expand All @@ -144,6 +197,7 @@ export const upload = async (params: UploadParameters) => {
key: screenshot.hash,
name: screenshot.name,
metadata: screenshot.metadata,
pwTraceKey: screenshot.pwTrace?.hash ?? null,
})),
parallel: config.parallel,
parallelTotal: config.parallelTotal,
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"scripts": {
"prebuild": "rm -rf dist",
"build": "rollup -c",
"test": "pnpm exec playwright test --shard=2/4",
"test": "pnpm exec playwright test",
"e2e": "WITH_ARGOS_REPORTER=true pnpm run test"
}
}
2 changes: 1 addition & 1 deletion packages/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const defaultReporters: PlaywrightTestConfig["reporter"] = [["list"]];
export default defineConfig({
use: {
screenshot: "only-on-failure",
trace: "on",
trace: "retain-on-failure",
},
projects: [
{
Expand Down
45 changes: 45 additions & 0 deletions packages/playwright/src/attachment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TestResult } from "@playwright/test/reporter";

export function getAttachmentName(name: string, type: string) {
return `argos/${type}___${name}`;
}
Expand All @@ -15,3 +17,46 @@ export function getAttachementFilename(name: string) {
}
throw new Error(`Unknown attachment name: ${name}`);
}

export type Attachment = TestResult["attachments"][number];
export type ArgosScreenshotAttachment = Attachment & {
body: Buffer;
};
export type AutomaticScreenshotAttachment = Attachment & {
name: "screenshot";
path: string;
};
export type TraceAttachment = Attachment & {
name: "trace";
path: string;
};

export function checkIsTrace(
attachment: Attachment,
): attachment is TraceAttachment {
return (
attachment.name === "trace" &&
attachment.contentType === "application/zip" &&
Boolean(attachment.path)
);
}

export function checkIsArgosScreenshot(
attachment: Attachment,
): attachment is ArgosScreenshotAttachment {
return (
attachment.name.startsWith("argos/") &&
attachment.contentType === "image/png" &&
Boolean(attachment.body)
);
}

export function checkIsAutomaticScreenshot(
attachment: Attachment,
): attachment is AutomaticScreenshotAttachment {
return (
attachment.name === "screenshot" &&
attachment.contentType === "image/png" &&
Boolean(attachment.path)
);
}
18 changes: 14 additions & 4 deletions packages/playwright/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
readVersionFromPackage,
} from "@argos-ci/util";
import { TestInfo } from "@playwright/test";
import { TestCase } from "@playwright/test/reporter";
import { TestCase, TestResult } from "@playwright/test/reporter";
import { relative } from "node:path";
import { createRequire } from "node:module";

Expand Down Expand Up @@ -63,6 +63,8 @@ export async function getTestMetadataFromTestInfo(testInfo: TestInfo) {
id: testInfo.testId,
title: testInfo.title,
titlePath: testInfo.titlePath,
retry: testInfo.retry,
retries: testInfo.project.retries,
location: {
file: repositoryPath
? relative(repositoryPath, testInfo.file)
Expand All @@ -74,11 +76,16 @@ export async function getTestMetadataFromTestInfo(testInfo: TestInfo) {
return testMetadata;
}

export async function getTestMetadataFromTestCase(testCase: TestCase) {
export async function getTestMetadataFromTestCase(
testCase: TestCase,
testResult: TestResult,
) {
const repositoryPath = await getGitRepositoryPath();
const testMetadata: ScreenshotMetadata["test"] = {
title: testCase.title,
titlePath: testCase.titlePath(),
retry: testResult.retry,
retries: testCase.retries,
location: {
file: repositoryPath
? relative(repositoryPath, testCase.location.file)
Expand All @@ -90,10 +97,13 @@ export async function getTestMetadataFromTestCase(testCase: TestCase) {
return testMetadata;
}

export async function getMetadataFromTestCase(testCase: TestCase) {
export async function getMetadataFromTestCase(
testCase: TestCase,
testResult: TestResult,
) {
const [libMetadata, testMetadata] = await Promise.all([
getLibraryMetadata(),
getTestMetadataFromTestCase(testCase),
getTestMetadataFromTestCase(testCase, testResult),
]);

const metadata: ScreenshotMetadata = {
Expand Down
Loading

0 comments on commit d194e04

Please sign in to comment.