Skip to content

Commit 7101ee6

Browse files
committed
test: capture test trace and attachments
1 parent eea68f6 commit 7101ee6

File tree

6 files changed

+65
-16
lines changed

6 files changed

+65
-16
lines changed

packages/playwright-cloudflare/internal.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type TestContext = {
5656
env: Env;
5757
sessionId: string;
5858
assetsUrl: string;
59+
retry: number;
5960
};
6061

6162
export function currentTestContext(): TestContext;

packages/playwright-cloudflare/src/internal.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import fs from 'fs';
2+
13
import { asLocator, currentZone, isString, ManualPromise, setTimeOrigin, timeOrigin } from 'playwright-core/lib/utils';
24
import { loadConfig } from 'playwright/lib/common/configLoader';
35
import { currentTestInfo, setCurrentlyLoadingFileSuite } from 'playwright/lib/common/globals';
@@ -62,7 +64,6 @@ function toInfo(test: Suite | TestCase): SuiteInfo | TestCaseInfo {
6264
}
6365

6466
export const playwrightTestConfig = {
65-
preserveOutput: 'failures-only',
6667
projects: [
6768
{
6869
timeout: 5000,
@@ -115,13 +116,21 @@ class TestWorker extends WorkerMain {
115116

116117
protected override dispatchEvent(method: string, params: any): void {
117118
if (method === 'attach') {
118-
const { name, body, contentType } = params;
119-
if (!body)
120-
return;
121-
this._attachments.push({ name, body, contentType });
119+
const { name, body, path, contentType } = params;
120+
let fileContent: string | undefined;
121+
if (!body) {
122+
if (!path)
123+
throw new Error('Either body or path must be provided');
124+
if (!fs.existsSync(path))
125+
throw new Error(`File does not exist: ${path}`);
126+
fileContent = fs.readFileSync(path, 'base64') as string;
127+
}
128+
this._attachments.push({ name, body: body ?? fileContent, contentType });
122129
}
130+
123131
if (method === 'testEnd')
124132
this._testResult = params;
133+
125134
if (method === 'done') {
126135
if (!this._testResult) {
127136
this._testResult = {
@@ -170,9 +179,10 @@ export class TestRunner {
170179
context = this._testContext;
171180
const testWorker = new TestWorker(this._options);
172181
try {
182+
const { retry } = this._testContext;
173183
const [result] = await Promise.all([
174184
testWorker.testResult(),
175-
testWorker.runTestGroup({ file, entries: [{ testId, retry: 0 }] }),
185+
testWorker.runTestGroup({ file, entries: [{ testId, retry }] }),
176186
]);
177187
if (result.status === 'failed' && result.errors.some(isUnsupportedOperationError)) {
178188
return {
@@ -223,6 +233,7 @@ function renderApiCall(apiName: string, params: any) {
223233
}
224234

225235
const tracingGroupSteps: TestStepInternal[] = [];
236+
// adapted from _setupArtifacts fixture in packages/playwright/src/index.ts
226237
const expectApiListener: ClientInstrumentationListener = {
227238
onApiCallBegin: (data: ApiCallData) => {
228239
const testInfo = currentTestInfo();

packages/playwright-cloudflare/tests/playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default defineConfig({
55
reporter: process.env.CI ? 'dot' : 'list',
66
// dev mode apparently doesn't support parallelism
77
workers: process.env.TESTS_SERVER_URL ? 6 : 1,
8+
retries: process.env.TESTS_SERVER_URL ? 1 : 0,
89
timeout: 60 * 1000,
910
projects: [
1011
{

packages/playwright-cloudflare/tests/src/proxy/proxyTests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export async function proxyTests(file: string) {
6666
},
6767

6868
runTest: async ({ testId, fullTitle }: { testId: string, fullTitle: string }, testInfo: TestInfo) => {
69-
const response = await fetch(url, { body: JSON.stringify({ testId, fullTitle }), method: 'POST' });
69+
const response = await fetch(url, { body: JSON.stringify({ testId, fullTitle, retry: testInfo.retry }), method: 'POST' });
7070
if (!response.ok)
7171
throw new Error(`Failed to run test ${fullTitle} (${testId})`);
7272

packages/playwright-cloudflare/tests/src/server/testsServer.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type TestRequestPayload = {
88
testId: string;
99
fullTitle: string;
1010
timeout: number;
11+
retry: number;
1112
};
1213

1314
// eslint-disable-next-line no-console
@@ -30,10 +31,11 @@ export class TestsServer extends DurableObject<Env> {
3031
if (!sessionId)
3132
return new Response('sessionId is required', { status: 400 });
3233
const timeout = parseInt(url.searchParams.get('timeout') ?? '10', 10) * 1000;
33-
const { testId, fullTitle } = await request.json() as TestRequestPayload;
34+
const { testId, fullTitle, retry } = await request.json() as TestRequestPayload;
3435
const assetsUrl = url.origin;
3536
const { env } = this;
36-
const testRunner = new TestRunner({ env, sessionId, assetsUrl }, { timeout });
37+
const context = { env, sessionId, assetsUrl, retry };
38+
const testRunner = new TestRunner(context, { timeout });
3739
if (skipTestsFullTitles.has(fullTitle)) {
3840
log(`🚫 Skipping ${fullTitle}`);
3941
return Response.json({
@@ -48,7 +50,7 @@ export class TestsServer extends DurableObject<Env> {
4850
} satisfies TestEndPayload);
4951
}
5052

51-
log(`🧪 Running ${fullTitle}`);
53+
log(`🧪 Running ${fullTitle}${retry ? ` (retry #${retry})` : ''}`);
5254

5355
const result = await testRunner.runTest(file, testId);
5456
if (!['passed', 'skipped'].includes(result.status))

packages/playwright-cloudflare/tests/src/server/workerFixtures.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ export type PageWorkerFixtures = {
117117
headless: boolean;
118118
channel: string;
119119
screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'>;
120-
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries' | /** deprecated */ 'retry-with-trace';
120+
// we can't use trace fixture because plawyright triggers trace start/stop automatically if that fixture is available
121+
// and for some reason it causes a timeout, so we implement a simplified version of trace fixture here
122+
// See: https://github.com/microsoft/playwright/blob/v1.51.1/packages/playwright/src/worker/workerMain.ts#L342
123+
traceMode: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries';
124+
// the official trace fixture must be set to off
125+
trace: 'off';
121126
video: VideoMode | { mode: VideoMode, size: ViewportSize };
122127
browserName: 'chromium';
123128
browserVersion: string;
@@ -164,6 +169,7 @@ export const test = platformTest.extend<PageTestFixtures & ServerFixtures & Test
164169
headless: [true, { scope: 'worker' }],
165170
channel: ['stable', { scope: 'worker' }],
166171
screenshot: ['off', { scope: 'worker' }],
172+
traceMode: ['on-first-retry', { scope: 'worker' }],
167173
trace: ['off', { scope: 'worker' }],
168174
video: ['off', { scope: 'worker' }],
169175
browserName: ['chromium', { scope: 'worker' }],
@@ -216,15 +222,43 @@ export const test = platformTest.extend<PageTestFixtures & ServerFixtures & Test
216222
await run(_combinedContextOptions);
217223
},
218224

219-
context: async ({ contextFactory, _combinedContextOptions }, run, testInfo) => {
225+
context: async ({ contextFactory, _combinedContextOptions, traceMode }, run, testInfo) => {
220226
const context = await contextFactory(_combinedContextOptions);
221-
await context.tracing.start({ screenshots: true, snapshots: true });
227+
const traceStarted = traceMode === 'on' ||
228+
(traceMode === 'retain-on-failure' && testInfo.retry === 0) ||
229+
(traceMode === 'on-first-retry' && testInfo.retry === 1) ||
230+
(traceMode === 'on-all-retries' && testInfo.retry > 0);
231+
if (traceStarted)
232+
await context.tracing.start({ screenshots: true, snapshots: true });
233+
222234
await run(context);
223-
if (testInfo.status !== 'skipped' && testInfo.status !== testInfo.expectedStatus) {
235+
236+
const failed = testInfo.status !== testInfo.expectedStatus;
237+
let keepTrace = false;
238+
if (traceStarted) {
239+
switch (traceMode) {
240+
case 'on':
241+
keepTrace = true;
242+
break;
243+
case 'on-first-retry':
244+
keepTrace = testInfo.retry === 1;
245+
break;
246+
case 'on-all-retries':
247+
keepTrace = testInfo.retry > 0;
248+
break;
249+
case 'retain-on-failure':
250+
keepTrace = failed;
251+
break;
252+
}
253+
}
254+
255+
if (testInfo.status !== 'skipped' && keepTrace) {
224256
const tracePath = testInfo.outputPath('trace.zip');
225257
await context.tracing.stop({ path: tracePath });
226-
const trace = await fs.promises.readFile(tracePath);
227-
testInfo.attachments.push({ name: 'trace', body: trace, contentType: 'application/zip' });
258+
testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
259+
} else if (traceStarted) {
260+
// stop and discard trace
261+
await context.tracing.stop();
228262
}
229263
},
230264

0 commit comments

Comments
 (0)