chore: add adb-based connectivity (#4375)
This commit is contained in:
parent
06c8881dad
commit
28f6547d67
|
|
@ -47,28 +47,26 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? envObjectToArray(options.env) : undefined,
|
||||
});
|
||||
return new BrowserServerImpl(this._browserType, browser, options.port);
|
||||
return new BrowserServerImpl(browser, options.port);
|
||||
}
|
||||
}
|
||||
|
||||
export class BrowserServerImpl extends EventEmitter implements BrowserServer {
|
||||
private _server: ws.Server;
|
||||
private _browserType: BrowserType;
|
||||
private _browser: Browser;
|
||||
private _wsEndpoint: string;
|
||||
private _process: ChildProcess;
|
||||
|
||||
constructor(browserType: BrowserType, browser: Browser, port: number = 0) {
|
||||
constructor(browser: Browser, port: number = 0) {
|
||||
super();
|
||||
|
||||
this._browserType = browserType;
|
||||
this._browser = browser;
|
||||
|
||||
const token = createGuid();
|
||||
this._server = new ws.Server({ port });
|
||||
const address = this._server.address();
|
||||
this._wsEndpoint = typeof address === 'string' ? `${address}/${token}` : `ws://127.0.0.1:${address.port}/${token}`;
|
||||
this._process = browser._options.browserProcess.process;
|
||||
this._process = browser._options.browserProcess.process!;
|
||||
|
||||
this._server.on('connection', (socket: ws, req) => {
|
||||
if (req.url !== '/' + token) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
|||
readonly chromium: BrowserType;
|
||||
readonly firefox: BrowserType;
|
||||
readonly webkit: BrowserType;
|
||||
readonly _clank: BrowserType;
|
||||
readonly devices: Devices;
|
||||
readonly selectors: Selectors;
|
||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||
|
|
@ -45,6 +46,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
|||
this.chromium = BrowserType.from(initializer.chromium);
|
||||
this.firefox = BrowserType.from(initializer.firefox);
|
||||
this.webkit = BrowserType.from(initializer.webkit);
|
||||
this._clank = BrowserType.from(initializer.clank);
|
||||
if (initializer.electron)
|
||||
(this as any).electron = Electron.from(initializer.electron);
|
||||
this.devices = {};
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
|||
.map(([name, descriptor]) => ({ name, descriptor }));
|
||||
super(scope, playwright, 'Playwright', {
|
||||
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
||||
clank: new BrowserTypeDispatcher(scope, playwright.clank),
|
||||
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||
electron: electron ? new ElectronDispatcher(scope, electron) : undefined,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export type SerializedError = {
|
|||
// ----------- Playwright -----------
|
||||
export type PlaywrightInitializer = {
|
||||
chromium: BrowserTypeChannel,
|
||||
clank: BrowserTypeChannel,
|
||||
firefox: BrowserTypeChannel,
|
||||
webkit: BrowserTypeChannel,
|
||||
electron?: ElectronChannel,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ Playwright:
|
|||
|
||||
initializer:
|
||||
chromium: BrowserType
|
||||
clank: BrowserType
|
||||
firefox: BrowserType
|
||||
webkit: BrowserType
|
||||
electron: Electron?
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { ChildProcess } from 'child_process';
|
|||
|
||||
export interface BrowserProcess {
|
||||
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||||
process: ChildProcess;
|
||||
process?: ChildProcess;
|
||||
kill(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ export class CRBrowser extends Browser {
|
|||
return this._version;
|
||||
}
|
||||
|
||||
isClank(): boolean {
|
||||
return this._options.name === 'clank';
|
||||
}
|
||||
|
||||
_onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
|
||||
if (targetInfo.type === 'browser')
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -380,7 +380,9 @@ class FrameSession {
|
|||
}
|
||||
|
||||
async _initialize(hasUIWindow: boolean) {
|
||||
if (hasUIWindow && !this._crPage._browserContext._options.noDefaultViewport) {
|
||||
if (hasUIWindow &&
|
||||
!this._crPage._browserContext._browser.isClank() &&
|
||||
!this._crPage._browserContext._options.noDefaultViewport) {
|
||||
const { windowId } = await this._client.send('Browser.getWindowForTarget');
|
||||
this._windowId = windowId;
|
||||
}
|
||||
|
|
@ -825,6 +827,8 @@ class FrameSession {
|
|||
}
|
||||
|
||||
async _updateViewport(): Promise<void> {
|
||||
if (this._crPage._browserContext._browser.isClank())
|
||||
return;
|
||||
assert(this._isMainFrame());
|
||||
const options = this._crPage._browserContext._options;
|
||||
const viewportSize = this._page._state.viewportSize;
|
||||
|
|
@ -863,6 +867,8 @@ class FrameSession {
|
|||
}
|
||||
|
||||
async _updateEmulateMedia(initial: boolean): Promise<void> {
|
||||
if (this._crPage._browserContext._browser.isClank())
|
||||
return;
|
||||
const colorScheme = this._page._state.colorScheme || this._crPage._browserContext._options.colorScheme || 'light';
|
||||
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : [];
|
||||
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
|
||||
|
|
|
|||
169
src/server/clank/android.ts
Normal file
169
src/server/clank/android.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 * as debug from 'debug';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as stream from 'stream';
|
||||
import * as ws from 'ws';
|
||||
import { makeWaitForNextTask } from '../../utils/utils';
|
||||
|
||||
export interface Backend {
|
||||
devices(): Promise<DeviceBackend[]>;
|
||||
}
|
||||
|
||||
export interface DeviceBackend {
|
||||
close(): Promise<void>;
|
||||
init(): Promise<void>;
|
||||
runCommand(command: string): Promise<string>;
|
||||
open(command: string): Promise<SocketBackend>;
|
||||
}
|
||||
|
||||
export interface SocketBackend extends EventEmitter {
|
||||
write(data: Buffer): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class AndroidClient {
|
||||
backend: Backend;
|
||||
|
||||
constructor(backend: Backend) {
|
||||
this.backend = backend;
|
||||
}
|
||||
|
||||
async devices(): Promise<AndroidDevice[]> {
|
||||
const devices = await this.backend.devices();
|
||||
return devices.map(b => new AndroidDevice(b));
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidDevice {
|
||||
readonly backend: DeviceBackend;
|
||||
private _model: string | undefined;
|
||||
|
||||
constructor(backend: DeviceBackend) {
|
||||
this.backend = backend;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.backend.init();
|
||||
this._model = await this.backend.runCommand('shell:getprop ro.product.model');
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.backend.close();
|
||||
}
|
||||
|
||||
async launchBrowser(packageName: string): Promise<AndroidBrowser> {
|
||||
debug('pw:android')('Force-stopping', packageName);
|
||||
await this.backend.runCommand(`shell:am force-stop ${packageName}`);
|
||||
const hasDefaultSocket = !!(await this.backend.runCommand(`shell:cat /proc/net/unix | grep chrome_devtools_remote$`));
|
||||
debug('pw:android')('Starting', packageName);
|
||||
await this.backend.runCommand(`shell:am start -n ${packageName}/com.google.android.apps.chrome.Main about:blank`);
|
||||
let pid = 0;
|
||||
debug('pw:android')('Polling pid for', packageName);
|
||||
while (!pid) {
|
||||
const ps = (await this.backend.runCommand(`shell:ps -A | grep ${packageName}`)).split('\n');
|
||||
const proc = ps.find(line => line.endsWith(packageName));
|
||||
if (proc)
|
||||
pid = +proc.replace(/\s+/g, ' ').split(' ')[1];
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
}
|
||||
debug('pw:android')('PID=' + pid);
|
||||
const socketName = hasDefaultSocket ? `chrome_devtools_remote_${pid}` : 'chrome_devtools_remote';
|
||||
debug('pw:android')('Polling for socket', socketName);
|
||||
while (true) {
|
||||
const net = await this.backend.runCommand(`shell:cat /proc/net/unix | grep ${socketName}$`);
|
||||
if (net)
|
||||
break;
|
||||
await new Promise(f => setTimeout(f, 100));
|
||||
}
|
||||
debug('pw:android')('Got the socket, connecting');
|
||||
const browser = new AndroidBrowser(this, packageName, socketName, pid);
|
||||
await browser._open();
|
||||
return browser;
|
||||
}
|
||||
|
||||
model(): string | undefined {
|
||||
return this._model;
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidBrowser extends EventEmitter {
|
||||
readonly device: AndroidDevice;
|
||||
readonly socketName: string;
|
||||
readonly pid: number;
|
||||
private _socket: SocketBackend | undefined;
|
||||
private _receiver: stream.Writable;
|
||||
private _waitForNextTask = makeWaitForNextTask();
|
||||
onmessage?: (message: any) => void;
|
||||
onclose?: () => void;
|
||||
private _packageName: string;
|
||||
|
||||
constructor(device: AndroidDevice, packageName: string, socketName: string, pid: number) {
|
||||
super();
|
||||
this._packageName = packageName;
|
||||
this.device = device;
|
||||
this.socketName = socketName;
|
||||
this.pid = pid;
|
||||
this._receiver = new (ws as any).Receiver() as stream.Writable;
|
||||
this._receiver.on('message', message => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onmessage)
|
||||
this.onmessage(JSON.parse(message));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _open() {
|
||||
this._socket = await this.device.backend.open(`localabstract:${this.socketName}`);
|
||||
this._socket.on('close', () => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onclose)
|
||||
this.onclose();
|
||||
});
|
||||
});
|
||||
await this._socket.write(Buffer.from(`GET /devtools/browser HTTP/1.1\r
|
||||
Upgrade: WebSocket\r
|
||||
Connection: Upgrade\r
|
||||
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||||
Sec-WebSocket-Version: 13\r
|
||||
\r
|
||||
`));
|
||||
// HTTP Upgrade response.
|
||||
await new Promise(f => this._socket!.once('data', f));
|
||||
|
||||
// Start sending web frame to receiver.
|
||||
this._socket.on('data', data => this._receiver._write(data, 'binary', () => {}));
|
||||
}
|
||||
|
||||
async send(s: any) {
|
||||
await this._socket!.write(encodeWebFrame(JSON.stringify(s)));
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._socket!.close();
|
||||
await this.device.backend.runCommand(`shell:am force-stop ${this._packageName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWebFrame(data: string): Buffer {
|
||||
return (ws as any).Sender.frame(Buffer.from(data), {
|
||||
opcode: 1,
|
||||
mask: true,
|
||||
fin: true,
|
||||
readOnly: true
|
||||
})[0];
|
||||
}
|
||||
156
src/server/clank/backendAdb.ts
Normal file
156
src/server/clank/backendAdb.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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 * as assert from 'assert';
|
||||
import * as debug from 'debug';
|
||||
import * as net from 'net';
|
||||
import { EventEmitter } from 'ws';
|
||||
import { Backend, DeviceBackend, SocketBackend } from './android';
|
||||
|
||||
export class AdbBackend implements Backend {
|
||||
async devices(): Promise<DeviceBackend[]> {
|
||||
const result = await runCommand('host:devices');
|
||||
const lines = result.toString().trim().split('\n');
|
||||
const serials = lines.map(line => line.split('\t')[0]);
|
||||
return serials.map(serial => new AdbDevice(serial));
|
||||
}
|
||||
}
|
||||
|
||||
class AdbDevice implements DeviceBackend {
|
||||
readonly serial: string;
|
||||
|
||||
constructor(serial: string) {
|
||||
this.serial = serial;
|
||||
}
|
||||
|
||||
async init() {
|
||||
}
|
||||
|
||||
async close() {
|
||||
}
|
||||
|
||||
runCommand(command: string): Promise<string> {
|
||||
return runCommand(command, this.serial);
|
||||
}
|
||||
|
||||
async open(command: string): Promise<SocketBackend> {
|
||||
const result = await open(command, this.serial);
|
||||
result.becomeSocket();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommand(command: string, serial?: string): Promise<string> {
|
||||
debug('pw:adb:runCommand')(command, serial);
|
||||
const socket = new BufferedSocketWrapper(net.createConnection({ port: 5037 }));
|
||||
if (serial) {
|
||||
await socket.write(encodeMessage(`host:transport:${serial}`));
|
||||
const status = await socket.read(4);
|
||||
assert(status.toString() === 'OKAY', status.toString());
|
||||
}
|
||||
await socket.write(encodeMessage(command));
|
||||
const status = await socket.read(4);
|
||||
assert(status.toString() === 'OKAY', status.toString());
|
||||
if (!command.startsWith('shell:')) {
|
||||
const remainingLength = parseInt((await socket.read(4)).toString(), 16);
|
||||
return (await socket.read(remainingLength)).toString();
|
||||
}
|
||||
return (await socket.readAll()).toString();
|
||||
}
|
||||
|
||||
async function open(command: string, serial?: string): Promise<BufferedSocketWrapper> {
|
||||
const socket = new BufferedSocketWrapper(net.createConnection({ port: 5037 }));
|
||||
if (serial) {
|
||||
await socket.write(encodeMessage(`host:transport:${serial}`));
|
||||
const status = await socket.read(4);
|
||||
assert(status.toString() === 'OKAY', status.toString());
|
||||
}
|
||||
await socket.write(encodeMessage(command));
|
||||
const status = await socket.read(4);
|
||||
assert(status.toString() === 'OKAY', status.toString());
|
||||
return socket;
|
||||
}
|
||||
|
||||
function encodeMessage(message: string): Buffer {
|
||||
let lenHex = (message.length).toString(16);
|
||||
lenHex = '0'.repeat(4 - lenHex.length) + lenHex;
|
||||
return Buffer.from(lenHex + message);
|
||||
}
|
||||
|
||||
class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
||||
private _socket: net.Socket;
|
||||
private _buffer = Buffer.from([]);
|
||||
private _isSocket = false;
|
||||
private _notifyReader: (() => void) | undefined;
|
||||
private _connectPromise: Promise<void>;
|
||||
private _isClosed = false;
|
||||
|
||||
constructor(socket: net.Socket) {
|
||||
super();
|
||||
this._socket = socket;
|
||||
this._connectPromise = new Promise(f => this._socket.on('connect', f));
|
||||
this._socket.on('data', data => {
|
||||
debug('pw:android:adb:data')(data.toString());
|
||||
if (this._isSocket) {
|
||||
this.emit('data', data);
|
||||
return;
|
||||
}
|
||||
this._buffer = Buffer.concat([this._buffer, data]);
|
||||
if (this._notifyReader)
|
||||
this._notifyReader();
|
||||
});
|
||||
this._socket.on('close', () => {
|
||||
this._isClosed = true;
|
||||
if (this._notifyReader)
|
||||
this._notifyReader();
|
||||
this.emit('close');
|
||||
});
|
||||
this._socket.on('error', error => this.emit('error', error));
|
||||
}
|
||||
|
||||
async write(data: Buffer) {
|
||||
debug('pw:android:adb:send')(data.toString());
|
||||
await this._connectPromise;
|
||||
await new Promise(f => this._socket.write(data, f));
|
||||
}
|
||||
|
||||
async close() {
|
||||
debug('pw:android:adb')('Close');
|
||||
this._socket.destroy();
|
||||
}
|
||||
|
||||
async read(length: number): Promise<Buffer> {
|
||||
await this._connectPromise;
|
||||
assert(!this._isSocket, 'Can not read by length in socket mode');
|
||||
while (this._buffer.length < length)
|
||||
await new Promise(f => this._notifyReader = f);
|
||||
const result = this._buffer.slice(0, length);
|
||||
this._buffer = this._buffer.slice(length);
|
||||
debug('pw:android:adb:recv')(result.toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
async readAll(): Promise<Buffer> {
|
||||
while (!this._isClosed)
|
||||
await new Promise(f => this._notifyReader = f);
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
becomeSocket() {
|
||||
assert(!this._buffer.length);
|
||||
this._isSocket = true;
|
||||
}
|
||||
}
|
||||
99
src/server/clank/clank.ts
Normal file
99
src/server/clank/clank.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* Copyright 2017 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 { BrowserType } from '../browserType';
|
||||
import { Browser, BrowserOptions, BrowserProcess } from '../browser';
|
||||
import * as types from '../types';
|
||||
import { normalizeProxySettings, validateBrowserContextOptions } from '../browserContext';
|
||||
import { Progress } from '../progress';
|
||||
import { ConnectionTransport } from '../transport';
|
||||
import { Env } from '../processLauncher';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { AndroidBrowser, AndroidClient, AndroidDevice } from './android';
|
||||
import { AdbBackend } from './backendAdb';
|
||||
|
||||
export class Clank extends BrowserType {
|
||||
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<Browser> {
|
||||
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
|
||||
if ((options as any).__testHookBeforeCreateBrowser)
|
||||
await (options as any).__testHookBeforeCreateBrowser();
|
||||
|
||||
// const client = new AndroidClient(new UsbBackend());
|
||||
const client = new AndroidClient(new AdbBackend());
|
||||
const device = (await client.devices())[0];
|
||||
await device.init();
|
||||
const adbBrowser = await device.launchBrowser(options.executablePath || 'com.android.chrome'); // com.chrome.canary
|
||||
const transport = adbBrowser;
|
||||
|
||||
const browserOptions: BrowserOptions = {
|
||||
name: 'clank',
|
||||
slowMo: options.slowMo,
|
||||
persistent,
|
||||
headful: !options.headless,
|
||||
downloadsPath: undefined,
|
||||
browserProcess: new ClankBrowserProcess(device, adbBrowser),
|
||||
proxy: options.proxy,
|
||||
};
|
||||
if (persistent)
|
||||
validateBrowserContextOptions(persistent, browserOptions);
|
||||
|
||||
const browser = await this._connectToTransport(transport, browserOptions);
|
||||
// We assume no control when using custom arguments, and do not prepare the default context in that case.
|
||||
if (persistent && !options.ignoreAllDefaultArgs)
|
||||
await browser._defaultContext!._loadDefaultContext(progress);
|
||||
return browser;
|
||||
}
|
||||
|
||||
_defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser> {
|
||||
return CRBrowser.connect(transport, options);
|
||||
}
|
||||
|
||||
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||
return env;
|
||||
}
|
||||
|
||||
_rewriteStartupError(error: Error): Error {
|
||||
return error;
|
||||
}
|
||||
|
||||
_attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||
}
|
||||
}
|
||||
|
||||
class ClankBrowserProcess implements BrowserProcess {
|
||||
private _device: AndroidDevice;
|
||||
private _browser: AndroidBrowser;
|
||||
|
||||
constructor(device: AndroidDevice, browser: AndroidBrowser) {
|
||||
this._device = device;
|
||||
this._browser = browser;
|
||||
}
|
||||
|
||||
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||||
|
||||
async kill(): Promise<void> {
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this._browser.close();
|
||||
await this._device.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import { Chromium } from './chromium/chromium';
|
||||
import { Clank } from './clank/clank';
|
||||
import { WebKit } from './webkit/webkit';
|
||||
import { Firefox } from './firefox/firefox';
|
||||
import * as browserPaths from '../utils/browserPaths';
|
||||
|
|
@ -23,6 +24,7 @@ import { serverSelectors } from './selectors';
|
|||
export class Playwright {
|
||||
readonly selectors = serverSelectors;
|
||||
readonly chromium: Chromium;
|
||||
readonly clank: Clank;
|
||||
readonly firefox: Firefox;
|
||||
readonly webkit: WebKit;
|
||||
|
||||
|
|
@ -35,5 +37,11 @@ export class Playwright {
|
|||
|
||||
const webkit = browsers.find(browser => browser.name === 'webkit');
|
||||
this.webkit = new WebKit(packagePath, webkit!);
|
||||
|
||||
this.clank = new Clank(packagePath, {
|
||||
name: 'clank',
|
||||
revision: '0',
|
||||
download: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const DL_OPEN_LIBRARIES = {
|
|||
chromium: [],
|
||||
webkit: ['libGLESv2.so.2', 'libx264.so'],
|
||||
firefox: [],
|
||||
clank: [],
|
||||
};
|
||||
|
||||
async function validateDependencies(browserPath: string, browser: BrowserDescriptor) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import * as path from 'path';
|
|||
import { getUbuntuVersionSync } from './ubuntuVersion';
|
||||
import { getFromENV } from './utils';
|
||||
|
||||
export type BrowserName = 'chromium'|'webkit'|'firefox';
|
||||
export type BrowserName = 'chromium'|'webkit'|'firefox'|'clank';
|
||||
export type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'mac11.0'|'ubuntu18.04'|'ubuntu20.04';
|
||||
export type BrowserDescriptor = {
|
||||
name: BrowserName,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ it('should scope context handles', async ({browser, server}) => {
|
|||
] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
|
|
@ -57,6 +58,7 @@ it('should scope context handles', async ({browser, server}) => {
|
|||
await expectScopeState(browser, {
|
||||
_guid: '',
|
||||
objects: [
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [
|
||||
|
|
@ -91,6 +93,7 @@ it('should scope CDPSession handles', (test, { browserName }) => {
|
|||
] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
|
|
@ -109,6 +112,7 @@ it('should scope CDPSession handles', (test, { browserName }) => {
|
|||
] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
|
|
@ -129,6 +133,7 @@ it('should scope browser handles', async ({browserType, browserOptions}) => {
|
|||
{ _guid: 'Browser', objects: [] },
|
||||
]
|
||||
},
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
|
|
@ -152,6 +157,7 @@ it('should scope browser handles', async ({browserType, browserOptions}) => {
|
|||
},
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'BrowserType', objects: [] },
|
||||
{ _guid: 'Playwright', objects: [] },
|
||||
{ _guid: 'Selectors', objects: [] },
|
||||
{ _guid: 'Electron', objects: [] },
|
||||
|
|
|
|||
|
|
@ -110,10 +110,11 @@ DEPS['src/server/common/'] = [];
|
|||
// Strict dependencies for injected code.
|
||||
DEPS['src/server/injected/'] = ['src/server/common/'];
|
||||
|
||||
// Electron uses chromium internally.
|
||||
// Electron and Clank use chromium internally.
|
||||
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||
DEPS['src/server/clank/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||
|
||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/'];
|
||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/clank/'];
|
||||
DEPS['src/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
|
||||
|
||||
// Tracing is a client/server plugin, nothing should depend on it.
|
||||
|
|
|
|||
Loading…
Reference in a new issue