Skip to content
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

feat(playwright): support playwright trace #79

Merged
merged 1 commit into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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