mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat(fixtures): respect tracing config for APIRequestContext (#11954)
This commit is contained in:
parent
e9e5de2d57
commit
706c897031
@ -74,13 +74,14 @@ export class APIRequest implements api.APIRequest {
|
|||||||
})).request);
|
})).request);
|
||||||
context._tracing._localUtils = this._playwright._utils;
|
context._tracing._localUtils = this._playwright._utils;
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
|
context._request = this;
|
||||||
await this._onDidCreateContext?.(context);
|
await this._onDidCreateContext?.(context);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
|
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
|
||||||
private _request?: APIRequest;
|
_request?: APIRequest;
|
||||||
readonly _tracing: Tracing;
|
readonly _tracing: Tracing;
|
||||||
|
|
||||||
static from(channel: channels.APIRequestContextChannel): APIRequestContext {
|
static from(channel: channels.APIRequestContextChannel): APIRequestContext {
|
||||||
@ -89,8 +90,6 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
|
||||||
super(parent, type, guid, initializer, createInstrumentation());
|
super(parent, type, guid, initializer, createInstrumentation());
|
||||||
if (parent instanceof APIRequest)
|
|
||||||
this._request = parent;
|
|
||||||
this._tracing = Tracing.from(initializer.tracing);
|
this._tracing = Tracing.from(initializer.tracing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, Video, APIRequestContext } from 'playwright-core';
|
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, Video, APIRequestContext, Tracing } from 'playwright-core';
|
||||||
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
|
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
|
||||||
import { rootTestType } from './testType';
|
import { rootTestType } from './testType';
|
||||||
import { createGuid, removeFolders, debugMode } from 'playwright-core/lib/utils/utils';
|
import { createGuid, removeFolders, debugMode } from 'playwright-core/lib/utils/utils';
|
||||||
@ -257,41 +257,51 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const startTracing = async (tracing: Tracing) => {
|
||||||
|
if (captureTrace) {
|
||||||
|
const title = [path.relative(testInfo.project.testDir, testInfo.file) + ':' + testInfo.line, ...testInfo.titlePath.slice(1)].join(' › ');
|
||||||
|
if (!(tracing as any)[kTracingStarted]) {
|
||||||
|
await tracing.start({ ...traceOptions, title });
|
||||||
|
(tracing as any)[kTracingStarted] = true;
|
||||||
|
} else {
|
||||||
|
await tracing.startChunk({ title });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(tracing as any)[kTracingStarted] = false;
|
||||||
|
await tracing.stop();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDidCreateBrowserContext = async (context: BrowserContext) => {
|
const onDidCreateBrowserContext = async (context: BrowserContext) => {
|
||||||
createdContexts.add(context);
|
createdContexts.add(context);
|
||||||
context.setDefaultTimeout(testInfo.timeout === 0 ? 0 : (actionTimeout || 0));
|
context.setDefaultTimeout(testInfo.timeout === 0 ? 0 : (actionTimeout || 0));
|
||||||
context.setDefaultNavigationTimeout(testInfo.timeout === 0 ? 0 : (navigationTimeout || actionTimeout || 0));
|
context.setDefaultNavigationTimeout(testInfo.timeout === 0 ? 0 : (navigationTimeout || actionTimeout || 0));
|
||||||
if (captureTrace) {
|
await startTracing(context.tracing);
|
||||||
const title = [path.relative(testInfo.project.testDir, testInfo.file) + ':' + testInfo.line, ...testInfo.titlePath.slice(1)].join(' › ');
|
|
||||||
if (!(context.tracing as any)[kTracingStarted]) {
|
|
||||||
await context.tracing.start({ ...traceOptions, title });
|
|
||||||
(context.tracing as any)[kTracingStarted] = true;
|
|
||||||
} else {
|
|
||||||
await context.tracing.startChunk({ title });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(context.tracing as any)[kTracingStarted] = false;
|
|
||||||
await context.tracing.stop();
|
|
||||||
}
|
|
||||||
const listener = createInstrumentationListener(context);
|
const listener = createInstrumentationListener(context);
|
||||||
(context as any)._instrumentation.addListener(listener);
|
(context as any)._instrumentation.addListener(listener);
|
||||||
(context.request as any)._instrumentation.addListener(listener);
|
(context.request as any)._instrumentation.addListener(listener);
|
||||||
};
|
};
|
||||||
const onDidCreateRequestContext = async (context: APIRequestContext) => {
|
const onDidCreateRequestContext = async (context: APIRequestContext) => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await startTracing(tracing);
|
||||||
(context as any)._instrumentation.addListener(createInstrumentationListener());
|
(context as any)._instrumentation.addListener(createInstrumentationListener());
|
||||||
};
|
};
|
||||||
|
|
||||||
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
|
||||||
|
|
||||||
const onWillCloseContext = async (context: BrowserContext) => {
|
const stopTracing = async (tracing: Tracing) => {
|
||||||
(context as any)[startedCollectingArtifacts] = true;
|
(tracing as any)[startedCollectingArtifacts] = true;
|
||||||
if (captureTrace) {
|
if (captureTrace) {
|
||||||
// Export trace for now. We'll know whether we have to preserve it
|
// Export trace for now. We'll know whether we have to preserve it
|
||||||
// after the test finishes.
|
// after the test finishes.
|
||||||
const tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
|
const tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
|
||||||
temporaryTraceFiles.push(tracePath);
|
temporaryTraceFiles.push(tracePath);
|
||||||
await context.tracing.stopChunk({ path: tracePath });
|
await tracing.stopChunk({ path: tracePath });
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWillCloseContext = async (context: BrowserContext) => {
|
||||||
|
await stopTracing(context.tracing);
|
||||||
if (screenshot === 'on' || screenshot === 'only-on-failure') {
|
if (screenshot === 'on' || screenshot === 'only-on-failure') {
|
||||||
// Capture screenshot for now. We'll know whether we have to preserve them
|
// Capture screenshot for now. We'll know whether we have to preserve them
|
||||||
// after the test finishes.
|
// after the test finishes.
|
||||||
@ -303,6 +313,11 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onWillCloseRequestContext = async (context: APIRequestContext) => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await stopTracing(tracing);
|
||||||
|
};
|
||||||
|
|
||||||
// 1. Setup instrumentation and process existing contexts.
|
// 1. Setup instrumentation and process existing contexts.
|
||||||
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
|
||||||
(browserType as any)._onDidCreateContext = onDidCreateBrowserContext;
|
(browserType as any)._onDidCreateContext = onDidCreateBrowserContext;
|
||||||
@ -312,7 +327,12 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||||||
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
|
||||||
await Promise.all(existingContexts.map(onDidCreateBrowserContext));
|
await Promise.all(existingContexts.map(onDidCreateBrowserContext));
|
||||||
}
|
}
|
||||||
(playwright.request as any)._onDidCreateContext = onDidCreateRequestContext;
|
{
|
||||||
|
(playwright.request as any)._onDidCreateContext = onDidCreateRequestContext;
|
||||||
|
(playwright.request as any)._onWillCloseContext = onWillCloseRequestContext;
|
||||||
|
const existingApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
|
await Promise.all(existingApiRequests.map(onDidCreateRequestContext));
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Run the test.
|
// 2. Run the test.
|
||||||
await use();
|
await use();
|
||||||
@ -348,25 +368,35 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||||||
(browserType as any)._defaultLaunchOptions = undefined;
|
(browserType as any)._defaultLaunchOptions = undefined;
|
||||||
}
|
}
|
||||||
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
|
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
|
||||||
(playwright.request as any)._onDidCreateContext = undefined;
|
|
||||||
for (const context of (playwright.request as any)._contexts)
|
for (const context of (playwright.request as any)._contexts)
|
||||||
context._instrumentation.removeAllListeners();
|
context._instrumentation.removeAllListeners();
|
||||||
|
const leftoverApiRequests: APIRequestContext[] = Array.from((playwright.request as any)._contexts as Set<APIRequestContext>);
|
||||||
|
(playwright.request as any)._onDidCreateContext = undefined;
|
||||||
|
(playwright.request as any)._onWillCloseContext = undefined;
|
||||||
|
|
||||||
// 5. Collect artifacts from any non-closed contexts.
|
const stopTraceChunk = async (tracing: Tracing): Promise<boolean> => {
|
||||||
await Promise.all(leftoverContexts.map(async context => {
|
|
||||||
// When we timeout during context.close(), we might end up with context still alive
|
// When we timeout during context.close(), we might end up with context still alive
|
||||||
// but artifacts being already collected. In this case, do not collect artifacts
|
// but artifacts being already collected. In this case, do not collect artifacts
|
||||||
// for the second time.
|
// for the second time.
|
||||||
if ((context as any)[startedCollectingArtifacts])
|
if ((tracing as any)[startedCollectingArtifacts])
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
if (preserveTrace)
|
if (preserveTrace)
|
||||||
await context.tracing.stopChunk({ path: addTraceAttachment() });
|
await tracing.stopChunk({ path: addTraceAttachment() });
|
||||||
else if (captureTrace)
|
else if (captureTrace)
|
||||||
await context.tracing.stopChunk();
|
await tracing.stopChunk();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. Collect artifacts from any non-closed contexts.
|
||||||
|
await Promise.all(leftoverContexts.map(async context => {
|
||||||
|
if (!await stopTraceChunk(context.tracing))
|
||||||
|
return;
|
||||||
if (captureScreenshots)
|
if (captureScreenshots)
|
||||||
await Promise.all(context.pages().map(page => page.screenshot({ timeout: 5000, path: addScreenshotAttachment() }).catch(() => {})));
|
await Promise.all(context.pages().map(page => page.screenshot({ timeout: 5000, path: addScreenshotAttachment() }).catch(() => {})));
|
||||||
}));
|
}).concat(leftoverApiRequests.map(async context => {
|
||||||
|
const tracing = (context as any)._tracing as Tracing;
|
||||||
|
await stopTraceChunk(tracing);
|
||||||
|
})));
|
||||||
|
|
||||||
// 6. Either remove or attach temporary traces and screenshots for contexts closed
|
// 6. Either remove or attach temporary traces and screenshots for contexts closed
|
||||||
// before the test has finished.
|
// before the test has finished.
|
||||||
|
@ -54,6 +54,47 @@ test('should stop tracing with trace: on-first-retry, when not retrying', async
|
|||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.zip'))).toBeTruthy();
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.zip'))).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = { use: { trace: 'on' } };
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test('pass', async ({request, page}, testInfo) => {
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await request.get('${server.EMPTY_PAGE}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('api pass', async ({playwright}, testInfo) => {
|
||||||
|
const request = await playwright.request.newContext();
|
||||||
|
await request.get('${server.EMPTY_PAGE}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fail', async ({request, page}, testInfo) => {
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await request.get('${server.EMPTY_PAGE}');
|
||||||
|
expect(1).toBe(2);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(2);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
// One trace file for request context and one for each APIRequestContext
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-pass', 'trace-1.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-api-pass', 'trace-1.zip'))).toBeFalsy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'))).toBeTruthy();
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fail', 'trace-1.zip'))).toBeTruthy();
|
||||||
|
// One leftover global APIRequestContext from 'api pass' test.
|
||||||
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-fail', 'trace-2.zip'))).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('should not throw with trace: on-first-retry and two retries in the same worker', async ({ runInlineTest }, testInfo) => {
|
test('should not throw with trace: on-first-retry and two retries in the same worker', async ({ runInlineTest }, testInfo) => {
|
||||||
const files = {};
|
const files = {};
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user