diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 58928532ac..31e0b0e8cb 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -45,7 +45,7 @@ export type FetchOptions = { maxRetries?: number, }; -type NewContextOptions = Omit & { +export type NewContextOptions = Omit & { extraHTTPHeaders?: Headers, storageState?: string | StorageState, clientCertificates?: ClientCertificate[]; @@ -65,13 +65,17 @@ export class APIRequest implements api.APIRequest { } async newContext(options: NewContextOptions = {}): Promise { + return this._newContext(options, this._playwright._channel); + } + + async _newContext(options: NewContextOptions = {}, channel: channels.PlaywrightChannel | channels.LocalUtilsChannel): Promise { options = { ...this._defaultContextOptions, ...options }; const storageState = typeof options.storageState === 'string' ? JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) : options.storageState; // We do not expose tracesDir in the API, so do not allow options to accidentally override it. const tracesDir = this._defaultContextOptions?.tracesDir; - const context = APIRequestContext.from((await this._playwright._channel.newRequest({ + const context = APIRequestContext.from((await channel.newRequest({ ...options, extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, storageState, diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index 0687ee4cff..f60df580ed 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -20,7 +20,7 @@ import { Android } from './android'; import { BrowserType } from './browserType'; import { ChannelOwner } from './channelOwner'; import { Electron } from './electron'; -import { APIRequest } from './fetch'; +import { APIRequest, type NewContextOptions } from './fetch'; import { Selectors, SelectorsOwner } from './selectors'; export class Playwright extends ChannelOwner { @@ -74,8 +74,9 @@ export class Playwright extends ChannelOwner { return (channel as any)._object; } - async _startMockingProxy() { - const { mockingProxy } = await this._connection.localUtils()._channel.newMockingProxy({}); + async _startMockingProxy(requestContextOptions: NewContextOptions) { + const requestContext = await this.request._newContext(requestContextOptions, this._connection.localUtils()._channel); + const { mockingProxy } = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel }); return (mockingProxy as any)._object; } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 36542dfea1..bd332d7d89 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -336,7 +336,43 @@ scheme.LocalUtilsTraceDiscardedParams = tObject({ stacksId: tString, }); scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({})); -scheme.LocalUtilsNewMockingProxyParams = tOptional(tObject({})); +scheme.LocalUtilsNewRequestParams = tObject({ + baseURL: tOptional(tString), + userAgent: tOptional(tString), + ignoreHTTPSErrors: tOptional(tBoolean), + extraHTTPHeaders: tOptional(tArray(tType('NameValue'))), + clientCertificates: tOptional(tArray(tObject({ + origin: tString, + cert: tOptional(tBinary), + key: tOptional(tBinary), + passphrase: tOptional(tString), + pfx: tOptional(tBinary), + }))), + httpCredentials: tOptional(tObject({ + username: tString, + password: tString, + origin: tOptional(tString), + send: tOptional(tEnum(['always', 'unauthorized'])), + })), + proxy: tOptional(tObject({ + server: tString, + bypass: tOptional(tString), + username: tOptional(tString), + password: tOptional(tString), + })), + timeout: tOptional(tNumber), + storageState: tOptional(tObject({ + cookies: tOptional(tArray(tType('NetworkCookie'))), + origins: tOptional(tArray(tType('OriginStorage'))), + })), + tracesDir: tOptional(tString), +}); +scheme.LocalUtilsNewRequestResult = tObject({ + request: tChannel(['APIRequestContext']), +}); +scheme.LocalUtilsNewMockingProxyParams = tObject({ + requestContext: tChannel(['APIRequestContext']), +}); scheme.LocalUtilsNewMockingProxyResult = tObject({ mockingProxy: tChannel(['MockingProxy']), }); diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 62323e9a64..e83dc9ab77 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -41,10 +41,10 @@ import type { Playwright } from '../playwright'; import { SdkObject } from '../../server/instrumentation'; import { serializeClientSideCallMetadata } from '../../utils'; import { deviceDescriptors as descriptors } from '../deviceDescriptors'; -import type { APIRequestContext } from '../fetch'; -import { GlobalAPIRequestContext } from '../fetch'; import { MockingProxy } from '../mockingProxy'; import { MockingProxyDispatcher } from './mockingProxyDispatcher'; +import { APIRequestContextDispatcher } from './networkDispatchers'; +import { GlobalAPIRequestContext } from '../fetch'; export class LocalUtilsDispatcher extends Dispatcher implements channels.LocalUtilsChannel { _type_LocalUtils: boolean; @@ -55,21 +55,16 @@ export class LocalUtilsDispatcher extends Dispatcher(); - private _requestContext: APIRequestContext; + private _playwright: Playwright; constructor(scope: RootDispatcher, playwright: Playwright) { const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils'); const deviceDescriptors = Object.entries(descriptors) .map(([name, descriptor]) => ({ name, descriptor })); - - const requestContext = new GlobalAPIRequestContext( - playwright, - {} // TODO: this should probably respect _combinedContextOptions from test runner - ); super(scope, localUtils, 'LocalUtils', { deviceDescriptors, }); - this._requestContext = requestContext; + this._playwright = playwright; this._type_LocalUtils = true; } @@ -285,8 +280,14 @@ export class LocalUtilsDispatcher extends Dispatcher { + const requestContext = new GlobalAPIRequestContext(this._playwright, params); + return { request: APIRequestContextDispatcher.from(this.parentScope(), requestContext) }; + } + async newMockingProxy(params: channels.LocalUtilsNewMockingProxyParams, metadata?: CallMetadata): Promise { - const mockingProxy = new MockingProxy(this._object, this._requestContext); + const requestContext = (params.requestContext as APIRequestContextDispatcher)._object; + const mockingProxy = new MockingProxy(this._object, requestContext); await mockingProxy.start(); return { mockingProxy: MockingProxyDispatcher.from(this.parentScope(), mockingProxy) }; } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 580a803ba8..bc6cab3736 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -466,7 +466,8 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel { tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise; addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise; traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise; - newMockingProxy(params?: LocalUtilsNewMockingProxyParams, metadata?: CallMetadata): Promise; + newRequest(params: LocalUtilsNewRequestParams, metadata?: CallMetadata): Promise; + newMockingProxy(params: LocalUtilsNewMockingProxyParams, metadata?: CallMetadata): Promise; } export type LocalUtilsZipParams = { zipFile: string, @@ -566,8 +567,77 @@ export type LocalUtilsTraceDiscardedOptions = { }; export type LocalUtilsTraceDiscardedResult = void; -export type LocalUtilsNewMockingProxyParams = {}; -export type LocalUtilsNewMockingProxyOptions = {}; +export type LocalUtilsNewRequestParams = { + baseURL?: string, + userAgent?: string, + ignoreHTTPSErrors?: boolean, + extraHTTPHeaders?: NameValue[], + clientCertificates?: { + origin: string, + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + httpCredentials?: { + username: string, + password: string, + origin?: string, + send?: 'always' | 'unauthorized', + }, + proxy?: { + server: string, + bypass?: string, + username?: string, + password?: string, + }, + timeout?: number, + storageState?: { + cookies?: NetworkCookie[], + origins?: OriginStorage[], + }, + tracesDir?: string, +}; +export type LocalUtilsNewRequestOptions = { + baseURL?: string, + userAgent?: string, + ignoreHTTPSErrors?: boolean, + extraHTTPHeaders?: NameValue[], + clientCertificates?: { + origin: string, + cert?: Binary, + key?: Binary, + passphrase?: string, + pfx?: Binary, + }[], + httpCredentials?: { + username: string, + password: string, + origin?: string, + send?: 'always' | 'unauthorized', + }, + proxy?: { + server: string, + bypass?: string, + username?: string, + password?: string, + }, + timeout?: number, + storageState?: { + cookies?: NetworkCookie[], + origins?: OriginStorage[], + }, + tracesDir?: string, +}; +export type LocalUtilsNewRequestResult = { + request: APIRequestContextChannel, +}; +export type LocalUtilsNewMockingProxyParams = { + requestContext: APIRequestContextChannel, +}; +export type LocalUtilsNewMockingProxyOptions = { + +}; export type LocalUtilsNewMockingProxyResult = { mockingProxy: MockingProxyChannel, }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 2aa0a34881..6737d27ea5 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -526,6 +526,56 @@ ContextOptions: - allow - block +NewRequestParameters: + type: mixin + properties: + baseURL: string? + userAgent: string? + ignoreHTTPSErrors: boolean? + extraHTTPHeaders: + type: array? + items: NameValue + clientCertificates: + type: array? + items: + type: object + properties: + origin: string + cert: binary? + key: binary? + passphrase: string? + pfx: binary? + httpCredentials: + type: object? + properties: + username: string + password: string + origin: string? + send: + type: enum? + literals: + - always + - unauthorized + proxy: + type: object? + properties: + server: string + bypass: string? + username: string? + password: string? + timeout: number? + storageState: + type: object? + properties: + cookies: + type: array? + items: NetworkCookie + origins: + type: array? + items: OriginStorage + tracesDir: string? + + EventTarget: type: interface @@ -669,7 +719,15 @@ LocalUtils: parameters: stacksId: string + newRequest: + parameters: + $mixin: NewRequestParameters + returns: + request: APIRequestContext + newMockingProxy: + parameters: + requestContext: APIRequestContext returns: mockingProxy: MockingProxy @@ -748,52 +806,7 @@ Playwright: commands: newRequest: parameters: - baseURL: string? - userAgent: string? - ignoreHTTPSErrors: boolean? - extraHTTPHeaders: - type: array? - items: NameValue - clientCertificates: - type: array? - items: - type: object - properties: - origin: string - cert: binary? - key: binary? - passphrase: string? - pfx: binary? - httpCredentials: - type: object? - properties: - username: string - password: string - origin: string? - send: - type: enum? - literals: - - always - - unauthorized - proxy: - type: object? - properties: - server: string - bypass: string? - username: string? - password: string? - timeout: number? - storageState: - type: object? - properties: - cookies: - type: array? - items: NetworkCookie - origins: - type: array? - items: OriginStorage - tracesDir: string? - + $mixin: NewRequestParameters returns: request: APIRequestContext