added request and requestFinished events to APIRequestContext

This commit is contained in:
Shahzad 2025-02-27 01:05:09 +01:00
parent aaac9923fd
commit 9542eefc3e
12 changed files with 258 additions and 9 deletions

View file

@ -120,6 +120,18 @@ with sync_playwright() as p:
assert await response.body() == '{"status": "ok"}'
```
## event: APIRequestContext.apiRequest
- argument: <[APIRequestEvent]>
Emitted when a request is issued from any pages created through this context.
The [APIRequestEvent] object is read-only.
## event: APIRequestContext.apiRequestFinished
- argument: <[APIRequestFinishedEvent]>
Emitted when a request finishes successfully after downloading the response body. For a successful response, the
sequence of events is `request` and `requestfinished`.
## method: APIRequestContext.createFormData
* since: v1.23
* langs: csharp

View file

@ -50,6 +50,11 @@ export const Events = {
RequestFinished: 'requestfinished',
},
APIRequestContext: {
APIRequest: 'apiRequest',
APIRequestFinished: 'apiRequestfinished',
},
BrowserServer: {
Close: 'close',
},

View file

@ -14,6 +14,8 @@
* limitations under the License.
*/
import { APIRequestEvent, APIRequestFinishedEvent } from 'playwright-core/lib/server/fetch';
import { toClientCertificatesProtocol } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { TargetClosedError, isTargetClosedError } from './errors';
@ -23,6 +25,7 @@ import { assert } from '../utils/isomorphic/assert';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { isString } from '../utils/isomorphic/rtti';
import { Events } from './events';
import type { Playwright } from './playwright';
import type { ClientCertificate, FilePayload, Headers, SetStorageState, StorageState } from './types';
@ -98,7 +101,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
super(parent, type, guid, initializer);
this._tracing = Tracing.from(initializer.tracing);
this._tracing = Tracing.from(initializer.tracing!);
this._channel.on('apiRequest', request => {
this._onRequest(request);
});
this._channel.on('apiRequestFinished', params => this._onRequestFinished(params));
}
async [Symbol.asyncDispose]() {
@ -269,6 +276,14 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
}
return state;
}
private _onRequest(request: APIRequestEvent) {
this.emit(Events.APIRequestContext.APIRequest, request);
}
private _onRequestFinished(params: APIRequestFinishedEvent) {
this.emit(Events.APIRequestContext.APIRequestFinished, params);
}
}
async function toFormField(platform: Platform, name: string, value: string | number | boolean | fs.ReadStream | FilePayload): Promise<channels.FormField> {

View file

@ -16,7 +16,7 @@
// This file is generated by generate_channels.js, do not edit manually.
import { scheme, tOptional, tObject, tBoolean, tNumber, tString, tAny, tEnum, tArray, tBinary, tChannel, tType } from './validatorPrimitives';
import { scheme, tOptional, tObject, tBoolean, tNumber, tString, tAny, tEnum, tArray, tBinary, tChannel, tType, tURL } from './validatorPrimitives';
export type { Validator, ValidatorContext } from './validatorPrimitives';
export { ValidationError, findValidator, maybeFindValidator, createMetadataValidator } from './validatorPrimitives';
@ -201,7 +201,44 @@ scheme.FormField = tObject({
})),
});
scheme.APIRequestContextInitializer = tObject({
tracing: tChannel(['Tracing']),
tracing: tOptional(tChannel(['Tracing'])),
});
scheme.APIRequestContextApiRequestEvent = tObject({
guid: tString,
url: tURL,
method: tString,
headers: tAny,
cookies: tArray(tType('NameValue')),
postData: tOptional(tBinary),
});
scheme.APIRequestContextApiRequestFinishedEvent = tObject({
requestEvent: tObject({
guid: tString,
url: tURL,
method: tString,
headers: tAny,
cookies: tArray(tType('NameValue')),
}),
httpVersion: tString,
headers: tAny,
cookies: tArray(tType('NetworkCookie')),
rawHeaders: tArray(tString),
statusCode: tNumber,
statusMessage: tString,
body: tOptional(tBinary),
timings: tObject({
blocked: tOptional(tNumber),
dns: tOptional(tNumber),
connect: tOptional(tNumber),
send: tNumber,
wait: tNumber,
receive: tNumber,
ssl: tOptional(tNumber),
comment: tOptional(tString),
}),
serverIPAddress: tOptional(tString),
serverPort: tOptional(tNumber),
securityDetails: tOptional(tType('SecurityDetails')),
});
scheme.APIRequestContextFetchParams = tObject({
url: tString,

View file

@ -128,6 +128,13 @@ export const tEnum = (e: string[]): Validator => {
return arg;
};
};
export const tURL = (arg: any, path: string, context: ValidatorContext) => {
if (arg instanceof URL)
return arg;
if (typeof arg === 'string')
return new URL(arg);
throw new ValidationError(`${path}: expected URL, got ${typeof arg}`);
};
export const tChannel = (names: '*' | string[]): Validator => {
return (arg: any, path: string, context: ValidatorContext) => {
return context.tChannelImpl(names, arg, path, context);

View file

@ -19,11 +19,11 @@ import { Dispatcher, existingDispatcher } from './dispatcher';
import { FrameDispatcher } from './frameDispatcher';
import { WorkerDispatcher } from './pageDispatcher';
import { TracingDispatcher } from './tracingDispatcher';
import { APIRequestContext, APIRequestEvent, APIRequestFinishedEvent } from '../fetch';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import type { APIRequestContext } from '../fetch';
import type { CallMetadata } from '../instrumentation';
import type { Request, Response, Route } from '../network';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type { RootDispatcher } from './dispatcher';
import type { PageDispatcher } from './pageDispatcher';
import type * as channels from '@protocol/channels';
@ -193,9 +193,17 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
tracing,
});
this.adopt(tracing);
this.addObjectListener(APIRequestContext.Events.Request, (request: APIRequestEvent) => {
this._dispatchEvent('apiRequest', request);
});
this.addObjectListener(APIRequestContext.Events.RequestFinished, (request: APIRequestFinishedEvent) => {
this._dispatchEvent('apiRequestFinished', request);
});
}
async storageState(params: channels.APIRequestContextStorageStateParams): Promise<channels.APIRequestContextStorageStateResult> {
return this._object.storageState(params.indexedDB);
}

View file

@ -63,6 +63,7 @@ type FetchRequestOptions = {
type HeadersObject = Readonly<{ [name: string]: string }>;
export type APIRequestEvent = {
guid: string,
url: URL,
method: string,
headers: HeadersObject,
@ -294,6 +295,7 @@ export abstract class APIRequestContext extends SdkObject {
return { name, value };
}) || [];
const requestEvent: APIRequestEvent = {
guid: createGuid(),
url,
method: options.method!,
headers: options.headers,

View file

@ -19,6 +19,7 @@ import { Readable } from 'stream';
import { ReadStream } from 'fs';
import { Protocol } from './protocol';
import { Serializable, EvaluationArgument, PageFunction, PageFunctionOn, SmartHandle, ElementHandleForTag, BindingSource } from './structs';
import {APIRequestEvent, APIRequestFinishedEvent} from "playwright-core/lib/server/fetch";
type PageWaitForSelectorOptionsNotHidden = PageWaitForSelectorOptions & {
state?: 'visible'|'attached';
@ -18665,6 +18666,18 @@ export interface APIRequestContext {
}>;
[Symbol.asyncDispose](): Promise<void>;
/**
* Emitted when a request is issued from API request context. The event will be emitted after the request is issued
*/
on(event: 'apiRequest', listener: (request: APIRequestEvent) => any): this;
/**
* Emitted when a request finishes successfully after downloading the response body. For a successful response, the
* sequence of events is `request`, `response` and `requestfinished`.
*/
on(event: 'apiRequestfinished', listener: (request: APIRequestFinishedEvent) => any): this;
}
/**

View file

@ -337,9 +337,11 @@ export type FormField = {
// ----------- APIRequestContext -----------
export type APIRequestContextInitializer = {
tracing: TracingChannel,
tracing?: TracingChannel,
};
export interface APIRequestContextEventTarget {
on(event: 'apiRequest', callback: (params: APIRequestContextApiRequestEvent) => void): this;
on(event: 'apiRequestFinished', callback: (params: APIRequestContextApiRequestFinishedEvent) => void): this;
}
export interface APIRequestContextChannel extends APIRequestContextEventTarget, Channel {
_type_APIRequestContext: boolean;
@ -350,6 +352,43 @@ export interface APIRequestContextChannel extends APIRequestContextEventTarget,
disposeAPIResponse(params: APIRequestContextDisposeAPIResponseParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeAPIResponseResult>;
dispose(params: APIRequestContextDisposeParams, metadata?: CallMetadata): Promise<APIRequestContextDisposeResult>;
}
export type APIRequestContextApiRequestEvent = {
guid: string,
url: URL,
method: string,
headers: any,
cookies: NameValue[],
postData?: Binary,
};
export type APIRequestContextApiRequestFinishedEvent = {
requestEvent: {
guid: string,
url: URL,
method: string,
headers: any,
cookies: NameValue[],
},
httpVersion: string,
headers: any,
cookies: NetworkCookie[],
rawHeaders: string[],
statusCode: number,
statusMessage: string,
body?: Binary,
timings: {
blocked?: number,
dns?: number,
connect?: number,
send: number,
wait: number,
receive: number,
ssl?: number,
comment?: string,
},
serverIPAddress?: string,
serverPort?: number,
securityDetails?: SecurityDetails,
};
export type APIRequestContextFetchParams = {
url: string,
encodedParams?: string,
@ -428,6 +467,8 @@ export type APIRequestContextDisposeOptions = {
export type APIRequestContextDisposeResult = void;
export interface APIRequestContextEvents {
'apiRequest': APIRequestContextApiRequestEvent;
'apiRequestFinished': APIRequestContextApiRequestFinishedEvent;
}
export type APIResponse = {

View file

@ -313,7 +313,6 @@ RecordHarOptions:
urlRegexSource: string?
urlRegexFlags: string?
FormField:
type: object
properties:
@ -330,7 +329,7 @@ APIRequestContext:
type: interface
initializer:
tracing: Tracing
tracing: Tracing?
commands:
@ -394,6 +393,54 @@ APIRequestContext:
parameters:
reason: string?
events:
apiRequest:
parameters:
guid: string
url: URL
method: string
headers: json
cookies:
type: array
items: NameValue
postData: binary?
apiRequestFinished:
parameters:
requestEvent:
type: object
properties:
guid: string
url: URL
method: string
headers: json
cookies:
type: array
items: NameValue
httpVersion: string
headers: json
cookies:
type: array
items: NetworkCookie
rawHeaders:
type: array
items: string
statusCode: number
statusMessage: string
body: binary?
timings:
type: object
properties:
blocked: number?
dns: number?
connect: number?
send: number
wait: number
receive: number
ssl: number?
comment: string?
serverIPAddress: string?
serverPort: number?
securityDetails: SecurityDetails?
APIResponse:
type: object

View file

@ -0,0 +1,59 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { browserTest as it, expect } from '../config/browserTest';
import { APIRequestEvent, APIRequestFinishedEvent } from 'playwright-core/src/server/fetch';
it('APIRequestContext.Events.Request', async ({ context, server }) => {
const requests: APIRequestEvent[] = [];
context.request.on('apiRequest', request => {
requests.push(request);
});
await context.request.fetch(server.EMPTY_PAGE);
await setTimeout(() => {}, 100);
const urls = requests.map(r => r.url.toString());
expect(urls).toEqual([
server.EMPTY_PAGE,
]);
});
it('APIRequestContext.Events.RequestFinished', async ({ context, server }) => {
const finishedRequests: APIRequestFinishedEvent[] = [];
context.request.on('apiRequestfinished', request => finishedRequests.push(request));
await context.request.fetch(server.EMPTY_PAGE);
const request = finishedRequests[0];
expect(request.requestEvent.url.toString()).toBe(server.EMPTY_PAGE);
expect(request.timings.send).toBeTruthy();
});
it('should fire events in proper order', async ({ context, server }) => {
const events: string[] = [];
context.request.on('apiRequest', () => events.push('apiRequest'));
context.request.on('apiRequestfinished', () => events.push('apiRequestfinished'));
await context.request.fetch(server.EMPTY_PAGE);
expect(events).toEqual([
'apiRequest',
'apiRequestfinished'
]);
});

View file

@ -51,6 +51,9 @@ function inlineType(type, indent, wrapEnums = false) {
}
if (type === 'Channel')
return { ts: `Channel`, scheme: `tChannel('*')`, optional };
if(type === 'URL'){
return { ts: 'URL', scheme: `tURL`, optional };
}
return { ts: type, scheme: `tType('${type}')`, optional };
}
if (type.type.startsWith('array')) {
@ -153,7 +156,7 @@ const validator_ts = [
// This file is generated by ${path.basename(__filename)}, do not edit manually.
import { scheme, tOptional, tObject, tBoolean, tNumber, tString, tAny, tEnum, tArray, tBinary, tChannel, tType } from './validatorPrimitives';
import { scheme, tOptional, tObject, tBoolean, tNumber, tString, tAny, tEnum, tArray, tBinary, tChannel, tType, tURL } from './validatorPrimitives';
export type { Validator, ValidatorContext } from './validatorPrimitives';
export { ValidationError, findValidator, maybeFindValidator, createMetadataValidator } from './validatorPrimitives';
`];