Skip to content

Commit

Permalink
chore: allow client operation w/o local utils (#34790)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Feb 14, 2025
1 parent 90ec838 commit 163aacf
Show file tree
Hide file tree
Showing 30 changed files with 646 additions and 226 deletions.
3 changes: 1 addition & 2 deletions packages/playwright-core/src/client/DEPS.list
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[*]
../common/
../protocol/
../utils/**
../utilsBundle.ts
../utils/isomorphic
4 changes: 2 additions & 2 deletions packages/playwright-core/src/client/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
import { connectOverWebSocket } from './webSocket';

import type { Page } from './page';
import type * as types from './types';
Expand Down Expand Up @@ -69,9 +70,8 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
return await this._wrapApiCall(async () => {
const deadline = options.timeout ? monotonicTime() + options.timeout : 0;
const headers = { 'x-playwright-browser': 'android', ...options.headers };
const localUtils = this._connection.localUtils();
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout };
const connection = await localUtils.connect(connectParams);
const connection = await connectOverWebSocket(this._connection, connectParams);

let device: AndroidDevice;
connection.on('close', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { ChannelOwner } from './channelOwner';
import { Stream } from './stream';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';

import type * as channels from '@protocol/channels';
import type { Readable } from 'stream';
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { CDPSession } from './cdpSession';
import { ChannelOwner } from './channelOwner';
import { isTargetClosedError } from './errors';
import { Events } from './events';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';

import type { BrowserType } from './browserType';
import type { Page } from './page';
Expand Down
12 changes: 9 additions & 3 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { Waiter } from './waiter';
import { WebError } from './webError';
import { Worker } from './worker';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { urlMatchesEqual } from '../utils/isomorphic/urlMatch';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
Expand Down Expand Up @@ -361,11 +361,14 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}

async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full' } = {}): Promise<void> {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Route from har is not supported in thin clients');
if (options.update) {
await this._recordIntoHAR(har, null, options);
return;
}
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url });
this._harRouters.push(harRouter);
await harRouter.addContextRoute(this);
}
Expand Down Expand Up @@ -484,8 +487,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const isCompressed = harParams.content === 'attach' || harParams.path.endsWith('.zip');
const needCompressed = harParams.path.endsWith('.zip');
if (isCompressed && !needCompressed) {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Uncompressed har is not supported in thin clients');
await artifact.saveAs(harParams.path + '.tmp');
await this._connection.localUtils().harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
await localUtils.harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
} else {
await artifact.saveAs(harParams.path);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { assert } from '../utils/isomorphic/debug';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
import { connectOverWebSocket } from './webSocket';

import type { Playwright } from './playwright';
import type { ConnectOptions, LaunchOptions, LaunchPersistentContextOptions, LaunchServerOptions, Logger } from './types';
Expand Down Expand Up @@ -124,7 +125,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
return await this._wrapApiCall(async () => {
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
const localUtils = this._connection.localUtils();
const connectParams: channels.LocalUtilsConnectParams = {
wsEndpoint: params.wsEndpoint,
headers,
Expand All @@ -134,7 +134,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
};
if ((params as any).__testHookRedirectPortForwarding)
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
const connection = await localUtils.connect(connectParams);
const connection = await connectOverWebSocket(this._connection, connectParams);
let browser: Browser;
connection.on('close', () => {
// Emulate all pages, contexts and the browser closing upon disconnect.
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ export class Connection extends EventEmitter {
return this._rawBuffers;
}

localUtils(): LocalUtils {
return this._localUtils!;
localUtils(): LocalUtils | undefined {
return this._localUtils;
}

async initializePlaywright(): Promise<Playwright> {
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/client/elementHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import { promisify } from 'util';
import { Frame } from './frame';
import { JSHandle, parseResult, serializeArgument } from './jsHandle';
import { assert } from '../utils/isomorphic/debug';
import { fileUploadSizeLimit, mkdirIfNeeded } from '../common/fileUtils';
import { fileUploadSizeLimit, mkdirIfNeeded } from './fileUtils';
import { isString } from '../utils/isomorphic/rtti';
import { mime } from '../utilsBundle';
import { WritableStream } from './writableStream';
import { getMimeTypeForPath } from '../utils/isomorphic/mimeType';

import type { BrowserContext } from './browserContext';
import type { ChannelOwner } from './channelOwner';
Expand Down Expand Up @@ -327,7 +327,7 @@ export async function convertInputFiles(platform: Platform, files: string | File

export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
if (options.path) {
const mimeType = mime.getType(options.path);
const mimeType = getMimeTypeForPath(options.path);
if (mimeType === 'image/png')
return 'png';
else if (mimeType === 'image/jpeg')
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { TargetClosedError, isTargetClosedError } from './errors';
import { RawHeaders } from './network';
import { Tracing } from './tracing';
import { assert } from '../utils/isomorphic/debug';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { isString } from '../utils/isomorphic/rtti';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,12 @@
* limitations under the License.
*/

import type { Platform } from './platform';
import type { Platform } from '../common/platform';

// Keep in sync with the server.
export const fileUploadSizeLimit = 50 * 1024 * 1024;

export async function mkdirIfNeeded(platform: Platform, filePath: string) {
// This will harmlessly throw on windows if the dirname is the root directory.
await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {});
}

export async function removeFolders(platform: Platform, dirs: string[]): Promise<Error[]> {
return await Promise.all(dirs.map((dir: string) =>
platform.fs().promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
));
}
122 changes: 9 additions & 113 deletions packages/playwright-core/src/client/localUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@
*/

import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import * as localUtils from '../common/localUtils';

import type { HeadersArray, Size } from './types';
import type { HarBackend } from '../common/harBackend';
import type { Platform } from '../common/platform';
import type { Size } from './types';
import type * as channels from '@protocol/channels';

type DeviceDescriptor = {
Expand All @@ -35,8 +31,6 @@ type Devices = { [name: string]: DeviceDescriptor };

export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
readonly devices: Devices;
private _harBackends = new Map<string, HarBackend>();
private _stackSessions = new Map<string, localUtils.StackSession>();

constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
super(parent, type, guid, initializer);
Expand All @@ -47,132 +41,34 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
}

async zip(params: channels.LocalUtilsZipParams): Promise<void> {
return await localUtils.zip(this._platform, this._stackSessions, params);
return await this._channel.zip(params);
}

async harOpen(params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
return await localUtils.harOpen(this._platform, this._harBackends, params);
return await this._channel.harOpen(params);
}

async harLookup(params: channels.LocalUtilsHarLookupParams): Promise<channels.LocalUtilsHarLookupResult> {
return await localUtils.harLookup(this._harBackends, params);
return await this._channel.harLookup(params);
}

async harClose(params: channels.LocalUtilsHarCloseParams): Promise<void> {
return await localUtils.harClose(this._harBackends, params);
return await this._channel.harClose(params);
}

async harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise<void> {
return await localUtils.harUnzip(params);
return await this._channel.harUnzip(params);
}

async tracingStarted(params: channels.LocalUtilsTracingStartedParams): Promise<channels.LocalUtilsTracingStartedResult> {
return await localUtils.tracingStarted(this._stackSessions, params);
return await this._channel.tracingStarted(params);
}

async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
return await localUtils.traceDiscarded(this._platform, this._stackSessions, params);
return await this._channel.traceDiscarded(params);
}

async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise<void> {
return await localUtils.addStackToTracingNoReply(this._stackSessions, params);
}

async connect(params: channels.LocalUtilsConnectParams): Promise<Connection> {
const transport = this._platform.ws ? new WebSocketTransport(this._platform) : new JsonPipeTransport(this);
const connectHeaders = await transport.connect(params);
const connection = new Connection(this, this._platform, this._instrumentation, connectHeaders);
connection.markAsRemote();
connection.on('close', () => transport.close());

let closeError: string | undefined;
const onTransportClosed = (reason?: string) => {
connection.close(reason || closeError);
};
transport.onClose(reason => onTransportClosed(reason));
connection.onmessage = message => transport.send(message).catch(() => onTransportClosed());
transport.onMessage(message => {
try {
connection!.dispatch(message);
} catch (e) {
closeError = String(e);
transport.close();
}
});
return connection;
}
}
interface Transport {
connect(params: channels.LocalUtilsConnectParams): Promise<HeadersArray>;
send(message: any): Promise<void>;
onMessage(callback: (message: object) => void): void;
onClose(callback: (reason?: string) => void): void;
close(): Promise<void>;
}

class JsonPipeTransport implements Transport {
private _pipe: channels.JsonPipeChannel | undefined;
private _owner: ChannelOwner<channels.LocalUtilsChannel>;

constructor(owner: ChannelOwner<channels.LocalUtilsChannel>) {
this._owner = owner;
}

async connect(params: channels.LocalUtilsConnectParams) {
const { pipe, headers: connectHeaders } = await this._owner._wrapApiCall(async () => {
return await this._owner._channel.connect(params);
}, /* isInternal */ true);
this._pipe = pipe;
return connectHeaders;
}

async send(message: object) {
this._owner._wrapApiCall(async () => {
await this._pipe!.send({ message });
}, /* isInternal */ true);
}

onMessage(callback: (message: object) => void) {
this._pipe!.on('message', ({ message }) => callback(message));
}

onClose(callback: (reason?: string) => void) {
this._pipe!.on('closed', ({ reason }) => callback(reason));
}

async close() {
await this._owner._wrapApiCall(async () => {
await this._pipe!.close().catch(() => {});
}, /* isInternal */ true);
}
}

class WebSocketTransport implements Transport {
private _platform: Platform;
private _ws: WebSocket | undefined;

constructor(platform: Platform) {
this._platform = platform;
}

async connect(params: channels.LocalUtilsConnectParams) {
this._ws = this._platform.ws!(params.wsEndpoint);
return [];
}

async send(message: object) {
this._ws!.send(JSON.stringify(message));
}

onMessage(callback: (message: object) => void) {
this._ws!.addEventListener('message', event => callback(JSON.parse(event.data)));
}

onClose(callback: (reason?: string) => void) {
this._ws!.addEventListener('close', () => callback());
}

async close() {
this._ws!.close();
return await this._channel.addStackToTracingNoReply(params);
}
}
4 changes: 2 additions & 2 deletions packages/playwright-core/src/client/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualProm
import { MultiMap } from '../utils/isomorphic/multimap';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import { mime } from '../utilsBundle';
import { getMimeTypeForPath } from '../utils/isomorphic/mimeType';

import type { BrowserContext } from './browserContext';
import type { Page } from './page';
Expand Down Expand Up @@ -413,7 +413,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
else if (options.json)
headers['content-type'] = 'application/json';
else if (options.path)
headers['content-type'] = mime.getType(options.path) || 'application/octet-stream';
headers['content-type'] = getMimeTypeForPath(options.path) || 'application/octet-stream';
if (length && !('content-length' in headers))
headers['content-length'] = String(length);

Expand Down
7 changes: 5 additions & 2 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { Waiter } from './waiter';
import { Worker } from './worker';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { assert } from '../utils/isomorphic/debug';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils';
import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch';
Expand Down Expand Up @@ -525,11 +525,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}

async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Route from har is not supported in thin clients');
if (options.update) {
await this._browserContext._recordIntoHAR(har, this, options);
return;
}
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url });
this._harRouters.push(harRouter);
await harRouter.addPageRoute(this);
}
Expand Down
Loading

0 comments on commit 163aacf

Please sign in to comment.