move to serverside

This commit is contained in:
Simon Knott 2025-02-04 12:47:35 +01:00
parent 7a08cd6fa7
commit 5e49e08ba2
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
11 changed files with 35 additions and 42 deletions

View file

@ -81,7 +81,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> { async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
options = { ...this._browserType._defaultContextOptions, ...options }; options = { ...this._browserType._defaultContextOptions, ...options };
const contextOptions = await prepareBrowserContextParams(options); const contextOptions = await prepareBrowserContextParams(options, this._browserType);
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
const context = BrowserContext.from(response.context); const context = BrowserContext.from(response.context);
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);

View file

@ -29,8 +29,7 @@ import { Events } from './events';
import { TimeoutSettings } from '../common/timeoutSettings'; import { TimeoutSettings } from '../common/timeoutSettings';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
import type { Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types'; import type { Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
import type { RegisteredListener } from '../utils'; import { type URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded } from '../utils';
import { type URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded, eventsHelper } from '../utils';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type * as structs from '../../types/structs'; import type * as structs from '../../types/structs';
import { CDPSession } from './cdpSession'; import { CDPSession } from './cdpSession';
@ -45,7 +44,6 @@ import { Dialog } from './dialog';
import { WebError } from './webError'; import { WebError } from './webError';
import { TargetClosedError, parseError } from './errors'; import { TargetClosedError, parseError } from './errors';
import { Clock } from './clock'; import { Clock } from './clock';
import type { MockingProxy } from './mockingProxy';
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext { export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>(); _pages = new Set<Page>();
@ -70,7 +68,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
_closeWasCalled = false; _closeWasCalled = false;
private _closeReason: string | undefined; private _closeReason: string | undefined;
private _harRouters: HarRouter[] = []; private _harRouters: HarRouter[] = [];
_mockingProxy?: MockingProxy;
static from(context: channels.BrowserContextChannel): BrowserContext { static from(context: channels.BrowserContextChannel): BrowserContext {
return (context as any)._object; return (context as any)._object;
@ -169,7 +166,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this.emit(Events.BrowserContext.Page, page); this.emit(Events.BrowserContext.Page, page);
if (page._opener && !page._opener.isClosed()) if (page._opener && !page._opener.isClosed())
page._opener.emit(Events.Page.Popup, page); page._opener.emit(Events.Page.Popup, page);
this._mockingProxy?.instrumentPage(page);
} }
_onRequest(request: network.Request, page: Page | null) { _onRequest(request: network.Request, page: Page | null) {
@ -241,12 +237,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await bindingCall.call(func); await bindingCall.call(func);
} }
async _subscribeToMockingProxy(mockingProxy: MockingProxy) {
if (this._mockingProxy)
throw new Error('Multiple mocking proxies are not supported');
this._mockingProxy = mockingProxy;
}
setDefaultNavigationTimeout(timeout: number | undefined) { setDefaultNavigationTimeout(timeout: number | undefined) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout); this._timeoutSettings.setDefaultNavigationTimeout(timeout);
this._wrapApiCall(async () => { this._wrapApiCall(async () => {
@ -530,7 +520,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
}; };
} }
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> { export async function prepareBrowserContextParams(options: BrowserContextOptions, type?: BrowserType): Promise<channels.BrowserNewContextParams> {
if (options.videoSize && !options.videosPath) if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`); throw new Error(`"videoSize" option requires "videosPath" to be specified`);
if (options.extraHTTPHeaders) if (options.extraHTTPHeaders)
@ -548,6 +538,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors, forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
mockingProxyBaseURL: type?._playwright._mockingProxy?.baseURL(),
}; };
if (!contextParams.recordVideo && options.videosPath) { if (!contextParams.recordVideo && options.videosPath) {
contextParams.recordVideo = { contextParams.recordVideo = {

View file

@ -27,7 +27,6 @@ import { assert, headersObjectToArray, monotonicTime } from '../utils';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import { raceAgainstDeadline } from '../utils/timeoutRunner'; import { raceAgainstDeadline } from '../utils/timeoutRunner';
import type { Playwright } from './playwright'; import type { Playwright } from './playwright';
import type { Page } from './page';
export interface BrowserServerLauncher { export interface BrowserServerLauncher {
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>; launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
@ -96,7 +95,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
const logger = options.logger || this._defaultLaunchOptions?.logger; const logger = options.logger || this._defaultLaunchOptions?.logger;
assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
options = { ...this._defaultLaunchOptions, ...this._defaultContextOptions, ...options }; options = { ...this._defaultLaunchOptions, ...this._defaultContextOptions, ...options };
const contextParams = await prepareBrowserContextParams(options); const contextParams = await prepareBrowserContextParams(options, this);
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = { const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
...contextParams, ...contextParams,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
@ -243,13 +242,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
if (this._defaultContextNavigationTimeout !== undefined) if (this._defaultContextNavigationTimeout !== undefined)
context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout); context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
if (this._playwright._mockingProxy) {
context.on(Events.BrowserContext.Page, (page: Page) => {
// TODO: funnel through protocol, so these headers are known to the server browsercontext and can be applied earlier
page.setExtraHTTPHeaders(this._playwright._mockingProxy!.instrumentationHeaders(page));
});
}
await this._instrumentation.runAfterCreateBrowserContext(context); await this._instrumentation.runAfterCreateBrowserContext(context);
} }

View file

@ -69,7 +69,7 @@ export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> {
} }
findPage(correlation: string): Page | undefined { findPage(correlation: string): Page | undefined {
const guid = `Page@${correlation}`; const guid = `page@${correlation}`;
// TODO: move this as list onto Playwright directly // TODO: move this as list onto Playwright directly
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) { for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) {
for (const context of browserType._contexts) { for (const context of browserType._contexts) {
@ -81,10 +81,7 @@ export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> {
} }
} }
instrumentationHeaders(page: Page) { baseURL() {
const correlation = page._guid.substring('Page@'.length); return this._initializer.baseURL;
return {
'x-playwright-proxy': `${this._initializer.baseURL}/pw_meta:${correlation}/`,
};
} }
} }

View file

@ -775,6 +775,7 @@ scheme.BrowserNewContextParams = tObject({
cookies: tOptional(tArray(tType('SetNetworkCookie'))), cookies: tOptional(tArray(tType('SetNetworkCookie'))),
origins: tOptional(tArray(tType('OriginStorage'))), origins: tOptional(tArray(tType('OriginStorage'))),
})), })),
mockingProxyBaseURL: tOptional(tString),
}); });
scheme.BrowserNewContextResult = tObject({ scheme.BrowserNewContextResult = tObject({
context: tChannel(['BrowserContext']), context: tChannel(['BrowserContext']),

View file

@ -330,7 +330,7 @@ export class FFPage implements PageDelegate {
} }
async updateExtraHTTPHeaders(): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page.extraHTTPHeaders() || [] }); await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page.extraHTTPHeaders() });
} }
async updateEmulatedViewportSize(): Promise<void> { async updateEmulatedViewportSize(): Promise<void> {

View file

@ -654,7 +654,7 @@ export class Frame extends SdkObject {
private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> { private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise<network.Response | null> {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`); progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
const headers = this._page.extraHTTPHeaders() || []; const headers = this._page.extraHTTPHeaders();
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer'); const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
let referer = refererHeader ? refererHeader.value : undefined; let referer = refererHeader ? refererHeader.value : undefined;
if (options.referer !== undefined) { if (options.referer !== undefined) {

View file

@ -365,8 +365,20 @@ export class Page extends SdkObject {
return this._delegate.updateExtraHTTPHeaders(); return this._delegate.updateExtraHTTPHeaders();
} }
extraHTTPHeaders(): types.HeadersArray | undefined { extraHTTPHeaders(): types.HeadersArray {
return this._extraHTTPHeaders; return this.instrumentationHeaders().concat(this._extraHTTPHeaders ?? []);
}
instrumentationHeaders() {
const headers: channels.NameValue[] = [];
const mockingProxyBaseURL = this.context()._options.mockingProxyBaseURL;
if (mockingProxyBaseURL) {
const correlation = this.guid.substring('Page@'.length);
headers.push({ name: 'x-playwright-proxy', value: encodeURIComponent(mockingProxyBaseURL + `pw_meta:${correlation}/`) });
}
return headers;
} }
async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) { async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) {

View file

@ -57,7 +57,7 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
_optionContextReuseMode: ContextReuseMode, _optionContextReuseMode: ContextReuseMode,
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'], _optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
_reuseContext: boolean, _reuseContext: boolean,
_mockingProxy?: MockingProxy, _mockingProxy?: void,
}; };
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
@ -124,12 +124,11 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
}, true); }, true);
}, { scope: 'worker', timeout: 0 }], }, { scope: 'worker', timeout: 0 }],
_mockingProxy: [async ({ mockingProxy: mockingProxyOption, playwright }, use) => { _mockingProxy: [async ({ mockingProxy, playwright }, use) => {
if (mockingProxyOption !== 'inject-via-header') if (mockingProxy === 'inject-via-header')
return await use(undefined); await (playwright as PlaywrightImpl)._startMockingProxy();
const mockingProxy = await (playwright as PlaywrightImpl)._startMockingProxy(); await use();
await use(mockingProxy); }, { scope: 'worker', box: true, auto: true }],
}, { scope: 'worker', box: true }],
acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }], acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }],
bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true }], bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true }],
@ -259,7 +258,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
} }
}, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any], }, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
_setupArtifacts: [async ({ playwright, screenshot, _mockingProxy }, use, testInfo) => { _setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
// This fixture has a separate zero-timeout slot to ensure that artifact collection // This fixture has a separate zero-timeout slot to ensure that artifact collection
// happens even after some fixtures or hooks time out. // happens even after some fixtures or hooks time out.
// Now that default test timeout is known, we can replace zero with an actual value. // Now that default test timeout is known, we can replace zero with an actual value.
@ -313,8 +312,6 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
currentTestInfo()?._setDebugMode(); currentTestInfo()?._setDebugMode();
}, },
runAfterCreateBrowserContext: async (context: BrowserContextImpl) => { runAfterCreateBrowserContext: async (context: BrowserContextImpl) => {
if (_mockingProxy)
await context._subscribeToMockingProxy(_mockingProxy);
await artifactsRecorder?.didCreateBrowserContext(context); await artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (testInfo) if (testInfo)

View file

@ -1369,6 +1369,7 @@ export type BrowserNewContextParams = {
cookies?: SetNetworkCookie[], cookies?: SetNetworkCookie[],
origins?: OriginStorage[], origins?: OriginStorage[],
}, },
mockingProxyBaseURL?: string,
}; };
export type BrowserNewContextOptions = { export type BrowserNewContextOptions = {
noDefaultViewport?: boolean, noDefaultViewport?: boolean,
@ -1435,6 +1436,7 @@ export type BrowserNewContextOptions = {
cookies?: SetNetworkCookie[], cookies?: SetNetworkCookie[],
origins?: OriginStorage[], origins?: OriginStorage[],
}, },
mockingProxyBaseURL?: string,
}; };
export type BrowserNewContextResult = { export type BrowserNewContextResult = {
context: BrowserContextChannel, context: BrowserContextChannel,

View file

@ -1038,6 +1038,7 @@ Browser:
origins: origins:
type: array? type: array?
items: OriginStorage items: OriginStorage
mockingProxyBaseURL: string?
returns: returns:
context: BrowserContext context: BrowserContext