diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 61194ea345..5193a5237c 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -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(); + + // Instrumentation. + _onDidCreateContext?: (context: APIRequestContext) => Promise; + _onWillCloseContext?: (context: APIRequestContext) => Promise; + 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 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 { + await this._request?._onWillCloseContext?.(this); await this._channel.dispose(); + this._request?._contexts.delete(this); } async delete(url: string, options?: RequestWithBodyOptions): Promise { diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 7249809221..f3f995a72e 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -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({ const temporaryScreenshots: string[] = []; const createdContexts = new Set(); - 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({ 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({ // 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({ (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 => { diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index cf8c3e5e7e..bffd0e5c62 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -58,6 +58,8 @@ class Reporter { onStepEnd(test, result, step) { if (step.error?.stack) step.error.stack = ''; + if (step.error?.message.includes('getaddrinfo')) + step.error.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,'), ]); 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,)\",\"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":"","stack":""}}`, + `%% begin {"title":"apiRequestContext.get(http://localhost2)","category":"pw:api"}`, + `%% end {"title":"apiRequestContext.get(http://localhost2)","category":"pw:api","error":{"message":"","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\"}`,