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

View file

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

View file

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