feat(request): show request context methods as steps (#11337)

This commit is contained in:
Dmitry Gozman 2022-01-11 17:33:41 -08:00 committed by GitHub
parent 807f70bccf
commit 4efb30999f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 66 additions and 27 deletions

View file

@ -28,6 +28,7 @@ import * as network from './network';
import { RawHeaders } from './network';
import { FilePayload, Headers, StorageState } from './types';
import { Playwright } from './playwright';
import { createInstrumentation } from './clientInstrumentation';
export type FetchOptions = {
params?: { [key: string]: string; },
@ -51,6 +52,12 @@ type RequestWithoutBodyOptions = Omit<RequestWithBodyOptions, 'data'|'form'|'mul
export class APIRequest implements api.APIRequest {
private _playwright: Playwright;
readonly _contexts = new Set<APIRequestContext>();
// Instrumentation.
_onDidCreateContext?: (context: APIRequestContext) => Promise<void>;
_onWillCloseContext?: (context: APIRequestContext) => Promise<void>;
constructor(playwright: Playwright) {
this._playwright = playwright;
}
@ -59,25 +66,34 @@ export class APIRequest implements api.APIRequest {
const storageState = typeof options.storageState === 'string' ?
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
options.storageState;
return APIRequestContext.from((await this._playwright._channel.newRequest({
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
...options,
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
storageState,
})).request);
this._contexts.add(context);
await this._onDidCreateContext?.(context);
return context;
}
}
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
private _request?: APIRequest;
static from(channel: channels.APIRequestContextChannel): APIRequestContext {
return (channel as any)._object;
}
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
super(parent, type, guid, initializer);
super(parent, type, guid, initializer, createInstrumentation());
if (parent instanceof APIRequest)
this._request = parent;
}
async dispose(): Promise<void> {
await this._request?._onWillCloseContext?.(this);
await this._channel.dispose();
this._request?._contexts.delete(this);
}
async delete(url: string, options?: RequestWithBodyOptions): Promise<APIResponse> {

View file

@ -16,7 +16,7 @@
import * as fs from 'fs';
import * as path from 'path';
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType, Video, Browser } from 'playwright-core';
import type { LaunchOptions, BrowserContextOptions, Page, BrowserContext, BrowserType, Video, Browser, APIRequestContext } from 'playwright-core';
import type { TestType, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo } from '../types/test';
import { rootTestType } from './testType';
import { createGuid, removeFolders } from 'playwright-core/lib/utils/utils';
@ -199,30 +199,15 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
const temporaryScreenshots: string[] = [];
const createdContexts = new Set<BrowserContext>();
const onDidCreateContext = async (context: BrowserContext) => {
createdContexts.add(context);
context.setDefaultTimeout(testInfo.timeout === 0 ? 0 : (actionTimeout || 0));
context.setDefaultNavigationTimeout(testInfo.timeout === 0 ? 0 : (navigationTimeout || actionTimeout || 0));
if (captureTrace) {
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();
}
(context as any)._instrumentation.addListener({
const createInstrumentationListener = (context?: BrowserContext) => {
return {
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, userData: any) => {
if (apiCall.startsWith('expect.'))
return { userObject: null };
if (apiCall === 'page.pause') {
testInfo.setTimeout(0);
context.setDefaultNavigationTimeout(0);
context.setDefaultTimeout(0);
context?.setDefaultNavigationTimeout(0);
context?.setDefaultTimeout(0);
}
const testInfoImpl = testInfo as any;
const step = testInfoImpl._addStep({
@ -238,7 +223,31 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
const step = userData.userObject;
step?.complete(error);
},
});
};
};
const onDidCreateBrowserContext = async (context: BrowserContext) => {
createdContexts.add(context);
context.setDefaultTimeout(testInfo.timeout === 0 ? 0 : (actionTimeout || 0));
context.setDefaultNavigationTimeout(testInfo.timeout === 0 ? 0 : (navigationTimeout || actionTimeout || 0));
if (captureTrace) {
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);
(context as any)._instrumentation.addListener(listener);
(context.request as any)._instrumentation.addListener(listener);
};
const onDidCreateRequestContext = async (context: APIRequestContext) => {
(context as any)._instrumentation.addListener(createInstrumentationListener());
};
const startedCollectingArtifacts = Symbol('startedCollectingArtifacts');
@ -265,12 +274,13 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
// 1. Setup instrumentation and process existing contexts.
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit]) {
(browserType as any)._onDidCreateContext = onDidCreateContext;
(browserType as any)._onDidCreateContext = onDidCreateBrowserContext;
(browserType as any)._onWillCloseContext = onWillCloseContext;
(browserType as any)._defaultContextOptions = _combinedContextOptions;
const existingContexts = Array.from((browserType as any)._contexts) as BrowserContext[];
await Promise.all(existingContexts.map(onDidCreateContext));
await Promise.all(existingContexts.map(onDidCreateBrowserContext));
}
(playwright.request as any)._onDidCreateContext = onDidCreateRequestContext;
// 2. Run the test.
await use();
@ -305,6 +315,9 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
(browserType as any)._defaultContextOptions = undefined;
}
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
(playwright.request as any)._onDidCreateContext = undefined;
for (const context of (playwright.request as any)._contexts)
context._instrumentation.removeAllListeners();
// 5. Collect artifacts from any non-closed contexts.
await Promise.all(leftoverContexts.map(async context => {

View file

@ -58,6 +58,8 @@ class Reporter {
onStepEnd(test, result, step) {
if (step.error?.stack)
step.error.stack = '<stack>';
if (step.error?.message.includes('getaddrinfo'))
step.error.message = '<message>';
console.log('%%%% end', JSON.stringify(this.distillStep(step)));
}
}
@ -258,12 +260,14 @@ test('should report api steps', async ({ runInlineTest }) => {
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
test('pass', async ({ page, request }) => {
await Promise.all([
page.waitForNavigation(),
page.goto('data:text/html,<button></button>'),
]);
await page.click('button');
await page.request.get('http://localhost2').catch(() => {});
await request.get('http://localhost2').catch(() => {});
});
test.describe('suite', () => {
@ -299,10 +303,16 @@ test('should report api steps', async ({ runInlineTest }) => {
`%% end {\"title\":\"page.goto(data:text/html,<button></button>)\",\"category\":\"pw:api\"}`,
`%% begin {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"page.click(button)\",\"category\":\"pw:api\"}`,
`%% begin {"title":"apiRequestContext.get(http://localhost2)","category":"pw:api"}`,
`%% end {"title":"apiRequestContext.get(http://localhost2)","category":"pw:api","error":{"message":"<message>","stack":"<stack>"}}`,
`%% begin {"title":"apiRequestContext.get(http://localhost2)","category":"pw:api"}`,
`%% end {"title":"apiRequestContext.get(http://localhost2)","category":"pw:api","error":{"message":"<message>","stack":"<stack>"}}`,
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"apiRequestContext.dispose\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"apiRequestContext.dispose\",\"category\":\"pw:api\"}`,
`%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`,
`%% end {\"title\":\"After Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"apiRequestContext.dispose\",\"category\":\"pw:api\"},{\"title\":\"browserContext.close\",\"category\":\"pw:api\"}]}`,
`%% begin {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\"}`,
`%% begin {\"title\":\"browser.newPage\",\"category\":\"pw:api\"}`,