feat(request): show request context methods as steps (#11337)
This commit is contained in:
parent
807f70bccf
commit
4efb30999f
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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\"}`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue