chore: remove web mode (#1625)

This commit is contained in:
Pavel Feldman 2020-04-01 14:42:47 -07:00 committed by GitHub
parent cf49a9ee7b
commit e241c1bef8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 266 additions and 942 deletions

View File

@ -47,6 +47,7 @@
"extract-zip": "^1.6.6",
"https-proxy-agent": "^3.0.0",
"jpeg-js": "^0.3.6",
"mime": "^2.4.4",
"pngjs": "^3.4.0",
"progress": "^2.0.3",
"proxy-from-env": "^1.1.0",
@ -56,6 +57,7 @@
"devDependencies": {
"@types/debug": "0.0.31",
"@types/extract-zip": "^1.6.2",
"@types/mime": "^2.0.1",
"@types/node": "^8.10.34",
"@types/pngjs": "^3.4.0",
"@types/proxy-from-env": "^1.0.0",

View File

@ -15,10 +15,10 @@
*/
import { BrowserContext, BrowserContextOptions } from './browserContext';
import * as platform from './platform';
import { Page } from './page';
import { EventEmitter } from 'events';
export interface Browser extends platform.EventEmitterType {
export interface Browser extends EventEmitter {
newContext(options?: BrowserContextOptions): Promise<BrowserContext>;
contexts(): BrowserContext[];
newPage(options?: BrowserContextOptions): Promise<Page>;

View File

@ -21,7 +21,6 @@ import { Events as CommonEvents } from '../events';
import { assert, debugError, helper } from '../helper';
import * as network from '../network';
import { Page, PageBinding, Worker } from '../page';
import * as platform from '../platform';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types';
import { ConnectionEvents, CRConnection, CRSession } from './crConnection';
@ -30,8 +29,9 @@ import { readProtocolStream } from './crProtocolHelper';
import { Events } from './events';
import { Protocol } from './protocol';
import { CRExecutionContext } from './crExecutionContext';
import { EventEmitter } from 'events';
export class CRBrowser extends platform.EventEmitter implements Browser {
export class CRBrowser extends EventEmitter implements Browser {
readonly _connection: CRConnection;
_session: CRSession;
private _clientRootSessionPromise: Promise<CRSession> | null = null;
@ -221,7 +221,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
});
}
async stopTracing(): Promise<platform.BufferType> {
async stopTracing(): Promise<Buffer> {
assert(this._tracingClient, 'Tracing was not started.');
const [event] = await Promise.all([
new Promise(f => this._tracingClient!.once('Tracing.tracingComplete', f)),
@ -242,7 +242,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
return this._clientRootSessionPromise;
}
_setDebugFunction(debugFunction: platform.DebuggerType) {
_setDebugFunction(debugFunction: debug.IDebugger) {
this._connection._debugProtocol = debugFunction;
}
}

View File

@ -16,9 +16,10 @@
*/
import { assert } from '../helper';
import * as platform from '../platform';
import * as debug from 'debug';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol';
import { EventEmitter } from 'events';
export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -28,13 +29,13 @@ export const ConnectionEvents = {
// should ignore.
export const kBrowserCloseMessageId = -9999;
export class CRConnection extends platform.EventEmitter {
export class CRConnection extends EventEmitter {
private _lastId = 0;
private readonly _transport: ConnectionTransport;
private readonly _sessions = new Map<string, CRSession>();
readonly rootSession: CRSession;
_closed = false;
_debugProtocol: platform.DebuggerType;
_debugProtocol: debug.IDebugger;
constructor(transport: ConnectionTransport) {
super();
@ -43,7 +44,7 @@ export class CRConnection extends platform.EventEmitter {
this._transport.onclose = this._onClose.bind(this);
this.rootSession = new CRSession(this, '', 'browser', '');
this._sessions.set('', this.rootSession);
this._debugProtocol = platform.debug('pw:protocol');
this._debugProtocol = debug('pw:protocol');
(this._debugProtocol as any).color = '34';
}
@ -118,7 +119,7 @@ export const CRSessionEvents = {
Disconnected: Symbol('Events.CDPSession.Disconnected')
};
export class CRSession extends platform.EventEmitter {
export class CRSession extends EventEmitter {
_connection: CRConnection | null;
private readonly _callbacks = new Map<number, {resolve: (o: any) => void, reject: (e: Error) => void, error: Error, method: string}>();
private readonly _targetType: string;

View File

@ -21,7 +21,6 @@ import { assert, debugError, helper, RegisteredListener } from '../helper';
import { Protocol } from './protocol';
import * as network from '../network';
import * as frames from '../frames';
import * as platform from '../platform';
import { Credentials } from '../types';
import { CRPage } from './crPage';
@ -191,7 +190,7 @@ export class CRNetworkManager {
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response {
const getResponseBody = async () => {
const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId });
return platform.Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
};
return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody);
}
@ -281,7 +280,7 @@ class InterceptableRequest implements network.RouteDelegate {
}
async fulfill(response: network.FulfillResponse) {
const responseBody = response.body && helper.isString(response.body) ? platform.Buffer.from(response.body) : (response.body || null);
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null);
const responseHeaders: { [s: string]: string; } = {};
if (response.headers) {
@ -291,7 +290,7 @@ class InterceptableRequest implements network.RouteDelegate {
if (response.contentType)
responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders))
responseHeaders['content-length'] = String(platform.Buffer.byteLength(responseBody));
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
await this._client.send('Fetch.fulfillRequest', {
requestId: this._interceptionId!,

View File

@ -36,7 +36,6 @@ import { CRPDF } from './crPdf';
import { CRBrowserContext } from './crBrowser';
import * as types from '../types';
import { ConsoleMessage } from '../console';
import * as platform from '../platform';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -455,7 +454,7 @@ export class CRPage implements PageDelegate {
await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
}
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {
const { visualViewport } = await this._client.send('Page.getLayoutMetrics');
if (!documentRect) {
documentRect = {
@ -472,7 +471,7 @@ export class CRPage implements PageDelegate {
// ignore current page scale.
const clip = { ...documentRect, scale: viewportRect ? visualViewport.scale : 1 };
const result = await this._client.send('Page.captureScreenshot', { format, quality, clip });
return platform.Buffer.from(result.data, 'base64');
return Buffer.from(result.data, 'base64');
}
async resetViewport(): Promise<void> {
@ -587,7 +586,7 @@ export class CRPage implements PageDelegate {
await this._client.send('Page.enable').catch(e => {});
}
async pdf(options?: types.PDFOptions): Promise<platform.BufferType> {
async pdf(options?: types.PDFOptions): Promise<Buffer> {
return this._pdf.generate(options);
}

View File

@ -16,7 +16,6 @@
*/
import { assert, helper } from '../helper';
import * as platform from '../platform';
import * as types from '../types';
import { CRSession } from './crConnection';
import { readProtocolStream } from './crProtocolHelper';
@ -77,7 +76,7 @@ export class CRPDF {
this._client = client;
}
async generate(options: types.PDFOptions = {}): Promise<platform.BufferType> {
async generate(options: types.PDFOptions = {}): Promise<Buffer> {
const {
scale = 1,
displayHeaderFooter = false,

View File

@ -18,7 +18,8 @@
import { assert } from '../helper';
import { CRSession } from './crConnection';
import { Protocol } from './protocol';
import * as platform from '../platform';
import * as fs from 'fs';
import * as util from 'util';
export function getExceptionMessage(exceptionDetails: Protocol.Runtime.ExceptionDetails): string {
if (exceptionDetails.exception)
@ -61,24 +62,24 @@ export async function releaseObject(client: CRSession, remoteObject: Protocol.Ru
await client.send('Runtime.releaseObject', {objectId: remoteObject.objectId}).catch(error => {});
}
export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise<platform.BufferType> {
export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise<Buffer> {
let eof = false;
let fd: number | undefined;
if (path)
fd = await platform.openFdAsync(path, 'w');
fd = await util.promisify(fs.open)(path, 'w');
const bufs = [];
while (!eof) {
const response = await client.send('IO.read', {handle});
eof = response.eof;
const buf = platform.Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
bufs.push(buf);
if (path)
await platform.writeFdAsync(fd!, buf);
await util.promisify(fs.write)(fd!, buf);
}
if (path)
await platform.closeFdAsync(fd!);
await util.promisify(fs.close)(fd!);
await client.send('IO.close', {handle});
return platform.Buffer.concat(bufs);
return Buffer.concat(bufs);
}
export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined) {

View File

@ -14,15 +14,18 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as mime from 'mime';
import * as path from 'path';
import * as util from 'util';
import * as frames from './frames';
import { assert, debugError, helper } from './helper';
import Injected from './injected/injected';
import * as input from './input';
import * as js from './javascript';
import * as types from './types';
import { assert, helper, debugError } from './helper';
import Injected from './injected/injected';
import { Page } from './page';
import * as platform from './platform';
import { selectors } from './selectors';
import * as types from './types';
export type PointerActionOptions = {
modifiers?: input.Modifier[];
@ -257,9 +260,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
for (const item of ff) {
if (typeof item === 'string') {
const file: types.FilePayload = {
name: platform.basename(item),
type: platform.getMimeType(item),
data: await platform.readFileAsync(item, 'base64')
name: path.basename(item),
type: mime.getType(item) || 'application/octet-stream',
data: await util.promisify(fs.readFile)(item, 'base64')
};
filePayloads.push(file);
} else {
@ -316,7 +319,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._page._delegate.getBoundingBox(this);
}
async screenshot(options?: types.ElementScreenshotOptions): Promise<platform.BufferType> {
async screenshot(options?: types.ElementScreenshotOptions): Promise<Buffer> {
return this._page._screenshotter.screenshotElement(this, options);
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { EventEmitter } from './platform';
import { EventEmitter } from 'events';
import { helper } from './helper';
export class ExtendedEventEmitter extends EventEmitter {

View File

@ -21,15 +21,15 @@ import { Events } from '../events';
import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import { Page, PageBinding } from '../page';
import * as platform from '../platform';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { headersArray } from './ffNetworkManager';
import { FFPage } from './ffPage';
import { Protocol } from './protocol';
import { EventEmitter } from 'events';
export class FFBrowser extends platform.EventEmitter implements Browser {
export class FFBrowser extends EventEmitter implements Browser {
_connection: FFConnection;
readonly _ffPages: Map<string, FFPage>;
readonly _defaultContext: FFBrowserContext;
@ -147,7 +147,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
await disconnected;
}
_setDebugFunction(debugFunction: platform.DebuggerType) {
_setDebugFunction(debugFunction: debug.IDebugger) {
this._connection._debugProtocol = debugFunction;
}
}

View File

@ -15,8 +15,9 @@
* limitations under the License.
*/
import {assert} from '../helper';
import * as platform from '../platform';
import * as debug from 'debug';
import { EventEmitter } from 'events';
import { assert } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol';
@ -28,12 +29,12 @@ export const ConnectionEvents = {
// should ignore.
export const kBrowserCloseMessageId = -9999;
export class FFConnection extends platform.EventEmitter {
export class FFConnection extends EventEmitter {
private _lastId: number;
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _transport: ConnectionTransport;
readonly _sessions: Map<string, FFSession>;
_debugProtocol: platform.DebuggerType = platform.debug('pw:protocol');
_debugProtocol = debug('pw:protocol');
_closed: boolean;
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
@ -135,7 +136,7 @@ export const FFSessionEvents = {
Disconnected: Symbol('Disconnected')
};
export class FFSession extends platform.EventEmitter {
export class FFSession extends EventEmitter {
_connection: FFConnection;
_disposed = false;
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;

View File

@ -20,7 +20,6 @@ import { FFSession } from './ffConnection';
import { Page } from '../page';
import * as network from '../network';
import * as frames from '../frames';
import * as platform from '../platform';
import { Protocol } from './protocol';
export class FFNetworkManager {
@ -73,7 +72,7 @@ export class FFNetworkManager {
});
if (response.evicted)
throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`);
return platform.Buffer.from(response.base64body, 'base64');
return Buffer.from(response.base64body, 'base64');
};
const headers: network.Headers = {};
for (const {name, value} of event.headers)
@ -171,7 +170,7 @@ class InterceptableRequest implements network.RouteDelegate {
}
async fulfill(response: network.FulfillResponse) {
const responseBody = response.body && helper.isString(response.body) ? platform.Buffer.from(response.body) : (response.body || null);
const responseBody = response.body && helper.isString(response.body) ? Buffer.from(response.body) : (response.body || null);
const responseHeaders: { [s: string]: string; } = {};
if (response.headers) {
@ -181,7 +180,7 @@ class InterceptableRequest implements network.RouteDelegate {
if (response.contentType)
responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders))
responseHeaders['content-length'] = String(platform.Buffer.byteLength(responseBody));
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
await this._session.send('Network.fulfillInterceptedRequest', {
requestId: this._id,

View File

@ -21,7 +21,6 @@ import { Events } from '../events';
import * as frames from '../frames';
import { assert, debugError, helper, RegisteredListener } from '../helper';
import { Page, PageBinding, PageDelegate, Worker } from '../page';
import * as platform from '../platform';
import { kScreenshotDuringNavigationError } from '../screenshotter';
import * as types from '../types';
import { getAccessibilityTree } from './ffAccessibility';
@ -340,7 +339,7 @@ export class FFPage implements PageDelegate {
throw new Error('Not implemented');
}
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {
if (!documentRect) {
const context = await this._page.mainFrame()._utilityContext();
const scrollOffset = await context.evaluateInternal(() => ({ x: window.scrollX, y: window.scrollY }));
@ -361,7 +360,7 @@ export class FFPage implements PageDelegate {
e.message = kScreenshotDuringNavigationError;
throw e;
});
return platform.Buffer.from(data, 'base64');
return Buffer.from(data, 'base64');
}
async resetViewport(): Promise<void> {

View File

@ -15,17 +15,18 @@
* limitations under the License.
*/
import * as types from './types';
import * as js from './javascript';
import * as fs from 'fs';
import * as util from 'util';
import { ConsoleMessage } from './console';
import * as dom from './dom';
import * as network from './network';
import { helper, assert, RegisteredListener } from './helper';
import { TimeoutError } from './errors';
import { Events } from './events';
import { assert, helper, RegisteredListener } from './helper';
import * as js from './javascript';
import * as network from './network';
import { Page } from './page';
import { ConsoleMessage } from './console';
import * as platform from './platform';
import { selectors } from './selectors';
import * as types from './types';
type ContextType = 'main' | 'utility';
type ContextData = {
@ -109,7 +110,7 @@ export class FrameManager {
await this._page._delegate.inputActionEpilogue();
await barrier.waitFor();
// Resolve in the next task, after all waitForNavigations.
await new Promise(platform.makeWaitForNextTask());
await new Promise(helper.makeWaitForNextTask());
return result;
} finally {
this._pendingNavigationBarriers.delete(barrier);
@ -549,7 +550,7 @@ export class Frame {
if (url !== null)
return (await context.evaluateHandleInternal(addScriptUrl, { url, type })).asElement()!;
if (path !== null) {
let contents = await platform.readFileAsync(path, 'utf8');
let contents = await util.promisify(fs.readFile)(path, 'utf8');
contents += '//# sourceURL=' + path.replace(/\n/g, '');
return (await context.evaluateHandleInternal(addScriptContent, { content: contents, type })).asElement()!;
}
@ -598,7 +599,7 @@ export class Frame {
return (await context.evaluateHandleInternal(addStyleUrl, url)).asElement()!;
if (path !== null) {
let contents = await platform.readFileAsync(path, 'utf8');
let contents = await util.promisify(fs.readFile)(path, 'utf8');
contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
return (await context.evaluateHandleInternal(addStyleContent, contents)).asElement()!;
}

View File

@ -15,18 +15,24 @@
* limitations under the License.
*/
import * as crypto from 'crypto';
import * as debug from 'debug';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as util from 'util';
import { TimeoutError } from './errors';
import * as platform from './platform';
import * as types from './types';
export const debugError = platform.debug(`pw:error`);
export const debugError = debug(`pw:error`);
export type RegisteredListener = {
emitter: platform.EventEmitterType;
emitter: EventEmitter;
eventName: (string | symbol);
handler: (...args: any[]) => void;
};
export type Listener = (...args: any[]) => void;
class Helper {
static evaluationString(fun: Function | string, ...args: any[]): string {
if (Helper.isString(fun)) {
@ -47,7 +53,7 @@ class Helper {
if (fun.content !== undefined) {
fun = fun.content;
} else if (fun.path !== undefined) {
let contents = await platform.readFileAsync(fun.path, 'utf8');
let contents = await util.promisify(fs.readFile)(fun.path, 'utf8');
if (addSourceUrl)
contents += '//# sourceURL=' + fun.path.replace(/\n/g, '');
fun = contents;
@ -59,7 +65,7 @@ class Helper {
}
static installApiHooks(className: string, classType: any) {
const log = platform.debug('pw:api');
const log = debug('pw:api');
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
@ -102,7 +108,7 @@ class Helper {
}
static addEventListener(
emitter: platform.EventEmitterType,
emitter: EventEmitter,
eventName: (string | symbol),
handler: (...args: any[]) => void): RegisteredListener {
emitter.on(eventName, handler);
@ -110,7 +116,7 @@ class Helper {
}
static removeEventListeners(listeners: Array<{
emitter: platform.EventEmitterType;
emitter: EventEmitter;
eventName: (string | symbol);
handler: (...args: any[]) => void;
}>) {
@ -140,7 +146,7 @@ class Helper {
}
static async waitForEvent(
emitter: platform.EventEmitterType,
emitter: EventEmitter,
eventName: (string | symbol),
predicate: Function,
timeout: number,
@ -296,6 +302,45 @@ class Helper {
assert(typeof match === 'function', 'url parameter should be string, RegExp or function');
return match(url);
}
// See https://joel.tools/microtasks/
static makeWaitForNextTask() {
if (parseInt(process.versions.node, 10) >= 11)
return setImmediate;
// Unlike Node 11, Node 10 and less have a bug with Task and MicroTask execution order:
// - https://github.com/nodejs/node/issues/22257
//
// So we can't simply run setImmediate to dispatch code in a following task.
// However, we can run setImmediate from-inside setImmediate to make sure we're getting
// in the following task.
let spinning = false;
const callbacks: (() => void)[] = [];
const loop = () => {
const callback = callbacks.shift();
if (!callback) {
spinning = false;
return;
}
setImmediate(loop);
// Make sure to call callback() as the last thing since it's
// untrusted code that might throw.
callback();
};
return (callback: () => void) => {
callbacks.push(callback);
if (!spinning) {
spinning = true;
setImmediate(loop);
}
};
}
static guid(): string {
return crypto.randomBytes(16).toString('hex');
}
}
export function assert(value: any, message?: string): asserts value {

View File

@ -16,7 +16,7 @@
import * as types from './types';
import * as dom from './dom';
import * as platform from './platform';
import { helper } from './helper';
export interface ExecutionContextDelegate {
evaluate(context: ExecutionContext, returnByValue: boolean, pageFunction: string | Function, ...args: any[]): Promise<any>;
@ -144,7 +144,7 @@ export async function prepareFunctionCall<T>(
const handles: (Promise<JSHandle | T>)[] = [];
const toDispose: Promise<JSHandle>[] = [];
const pushHandle = (handle: Promise<JSHandle | T>): string => {
const guid = platform.guid();
const guid = helper.guid();
guids.push(guid);
handles.push(handle);
return guid;

View File

@ -14,9 +14,11 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as mime from 'mime';
import * as util from 'util';
import * as frames from './frames';
import { assert, helper } from './helper';
import * as platform from './platform';
export type NetworkCookie = {
name: string,
@ -227,8 +229,8 @@ export class Route {
response = {
status: response.status,
headers: response.headers,
contentType: platform.getMimeType(response.path),
body: await platform.readFileBuffer(response.path)
contentType: mime.getType(response.path) || 'application/octet-stream',
body: await util.promisify(fs.readFile)(response.path)
};
}
await this._delegate.fulfill(response);
@ -242,11 +244,11 @@ export class Route {
export type RouteHandler = (route: Route, request: Request) => void;
type GetResponseBodyCallback = () => Promise<platform.BufferType>;
type GetResponseBodyCallback = () => Promise<Buffer>;
export class Response {
private _request: Request;
private _contentPromise: Promise<platform.BufferType> | null = null;
private _contentPromise: Promise<Buffer> | null = null;
_finishedPromise: Promise<Error | null>;
private _finishedPromiseCallback: any;
private _status: number;
@ -296,7 +298,7 @@ export class Response {
return this._finishedPromise;
}
body(): Promise<platform.BufferType> {
body(): Promise<Buffer> {
if (!this._contentPromise) {
this._contentPromise = this._finishedPromise.then(async error => {
if (error)
@ -330,7 +332,7 @@ export type FulfillResponse = {
status?: number,
headers?: Headers,
contentType?: string,
body?: string | platform.BufferType,
body?: string | Buffer,
};
export interface RouteDelegate {

View File

@ -17,7 +17,7 @@
import * as dom from './dom';
import * as frames from './frames';
import { assert, debugError, helper } from './helper';
import { assert, debugError, helper, Listener } from './helper';
import * as input from './input';
import * as js from './javascript';
import * as network from './network';
@ -28,8 +28,8 @@ import { Events } from './events';
import { BrowserContext, BrowserContextBase } from './browserContext';
import { ConsoleMessage, ConsoleMessageLocation } from './console';
import * as accessibility from './accessibility';
import * as platform from './platform';
import { ExtendedEventEmitter } from './extendedEventEmitter';
import { EventEmitter } from 'events';
export interface PageDelegate {
readonly rawMouse: input.RawMouse;
@ -55,7 +55,7 @@ export interface PageDelegate {
canScreenshotOutsideViewport(): boolean;
resetViewport(): Promise<void>; // Only called if canScreenshotOutsideViewport() returns false.
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType>;
takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer>;
isElementHandle(remoteObject: any): boolean;
adoptElementHandle<T extends Node>(handle: dom.ElementHandle<T>, to: dom.FrameExecutionContext): Promise<dom.ElementHandle<T>>;
@ -69,7 +69,7 @@ export interface PageDelegate {
scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise<void>;
getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
pdf?: (options?: types.PDFOptions) => Promise<platform.BufferType>;
pdf?: (options?: types.PDFOptions) => Promise<Buffer>;
coverage?: () => any;
// Work around Chrome's non-associated input and protocol.
@ -106,7 +106,7 @@ export class Page extends ExtendedEventEmitter {
readonly _frameManager: frames.FrameManager;
readonly accessibility: accessibility.Accessibility;
private _workers = new Map<string, Worker>();
readonly pdf: ((options?: types.PDFOptions) => Promise<platform.BufferType>) | undefined;
readonly pdf: ((options?: types.PDFOptions) => Promise<Buffer>) | undefined;
readonly coverage: any;
readonly _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = [];
_ownedContext: BrowserContext | undefined;
@ -408,7 +408,7 @@ export class Page extends ExtendedEventEmitter {
route.continue();
}
async screenshot(options?: types.ScreenshotOptions): Promise<platform.BufferType> {
async screenshot(options?: types.ScreenshotOptions): Promise<Buffer> {
return this._screenshotter.screenshotPage(options);
}
@ -506,7 +506,7 @@ export class Page extends ExtendedEventEmitter {
}
}
on(event: string | symbol, listener: platform.Listener): this {
on(event: string | symbol, listener: Listener): this {
if (event === Events.Page.FileChooser) {
if (!this.listenerCount(event))
this._delegate.setFileChooserIntercepted(true);
@ -515,7 +515,7 @@ export class Page extends ExtendedEventEmitter {
return this;
}
removeListener(event: string | symbol, listener: platform.Listener): this {
removeListener(event: string | symbol, listener: Listener): this {
super.removeListener(event, listener);
if (event === Events.Page.FileChooser && !this.listenerCount(event))
this._delegate.setFileChooserIntercepted(false);
@ -523,7 +523,7 @@ export class Page extends ExtendedEventEmitter {
}
}
export class Worker extends platform.EventEmitter {
export class Worker extends EventEmitter {
private _url: string;
private _executionContextPromise: Promise<js.ExecutionContext>;
private _executionContextCallback: (value?: js.ExecutionContext) => void;

View File

@ -1,562 +0,0 @@
/**
* 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.
*/
// Note: this is the only file outside of src/server which can import external dependencies.
// All dependencies must be listed in web.webpack.config.js to avoid bundling them.
import * as nodeEvents from 'events';
import * as nodeFS from 'fs';
import * as nodePath from 'path';
import * as nodeDebug from 'debug';
import * as nodeBuffer from 'buffer';
import * as jpeg from 'jpeg-js';
import * as png from 'pngjs';
import * as http from 'http';
import * as https from 'https';
import * as NodeWebSocket from 'ws';
import * as crypto from 'crypto';
import { assert, helper } from './helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from './transport';
export const isNode = typeof process === 'object' && !!process && typeof process.versions === 'object' && !!process.versions && !!process.versions.node;
export function promisify(nodeFunction: Function): Function {
assert(isNode);
function promisified(...args: any[]) {
return new Promise((resolve, reject) => {
function callback(err: any, ...result: any[]) {
if (err)
return reject(err);
if (result.length === 1)
return resolve(result[0]);
return resolve(result);
}
nodeFunction.call(null, ...args, callback);
});
}
return promisified;
}
export type Listener = (...args: any[]) => void;
export const EventEmitter: typeof nodeEvents.EventEmitter = isNode ? nodeEvents.EventEmitter : (
class EventEmitterImpl {
private _deliveryQueue?: {listener: Listener, args: any[]}[];
private _listeners = new Map<string | symbol, Set<Listener>>();
on(event: string | symbol, listener: Listener): this {
let set = this._listeners.get(event);
if (!set) {
set = new Set();
this._listeners.set(event, set);
}
set.add(listener);
return this;
}
addListener(event: string | symbol, listener: Listener): this {
return this.on(event, listener);
}
once(event: string | symbol, listener: Listener): this {
const wrapped = (...args: any[]) => {
this.removeListener(event, wrapped);
listener(...args);
};
return this.addListener(event, wrapped);
}
removeListener(event: string | symbol, listener: Listener): this {
const set = this._listeners.get(event);
if (set)
set.delete(listener);
return this;
}
emit(event: string | symbol, ...args: any[]): boolean {
const set = this._listeners.get(event);
if (!set || !set.size)
return true;
const dispatch = !this._deliveryQueue;
if (!this._deliveryQueue)
this._deliveryQueue = [];
for (const listener of set)
this._deliveryQueue.push({ listener, args });
if (!dispatch)
return true;
for (let index = 0; index < this._deliveryQueue.length; index++) {
const { listener, args } = this._deliveryQueue[index];
listener(...args);
}
this._deliveryQueue = undefined;
return true;
}
listenerCount(event: string | symbol): number {
const set = this._listeners.get(event);
return set ? set.size : 0;
}
}
) as any as typeof nodeEvents.EventEmitter;
export type EventEmitterType = nodeEvents.EventEmitter;
export type DebuggerType = nodeDebug.IDebugger;
export type DebugType = nodeDebug.IDebug;
export const debug: DebugType = isNode ? nodeDebug : (
function debug(namespace: string) {
return () => {};
}
) as any as DebugType;
export const Buffer: typeof nodeBuffer.Buffer = isNode ? nodeBuffer.Buffer : (
class BufferImpl {
readonly data: ArrayBuffer;
static from(data: string | ArrayBuffer, encoding: string = 'utf8'): BufferImpl {
return new BufferImpl(data, encoding);
}
static byteLength(buffer: BufferImpl | string, encoding: string = 'utf8'): number {
if (helper.isString(buffer))
buffer = new BufferImpl(buffer, encoding);
return buffer.data.byteLength;
}
static concat(buffers: BufferImpl[]): BufferImpl {
if (!buffers.length)
return new BufferImpl(new ArrayBuffer(0));
if (buffers.length === 1)
return buffers[0];
const view = new Uint8Array(buffers.reduce((a, b) => a + b.data.byteLength, 0));
let offset = 0;
for (const buffer of buffers) {
view.set(new Uint8Array(buffer.data), offset);
offset += buffer.data.byteLength;
}
return new BufferImpl(view.buffer);
}
constructor(data: string | ArrayBuffer, encoding: string = 'utf8') {
if (data instanceof ArrayBuffer) {
this.data = data;
} else {
if (encoding === 'base64') {
const binary = atob(data);
this.data = new ArrayBuffer(binary.length * 2);
const view = new Uint16Array(this.data);
for (let i = 0; i < binary.length; i++)
view[i] = binary.charCodeAt(i);
} else if (encoding === 'utf8') {
const encoder = new TextEncoder();
this.data = encoder.encode(data).buffer;
} else {
throw new Error('Unsupported encoding "' + encoding + '"');
}
}
}
toString(encoding: string = 'utf8'): string {
if (encoding === 'base64') {
const binary = String.fromCharCode(...new Uint16Array(this.data));
return btoa(binary);
}
const decoder = new TextDecoder(encoding, { fatal: true });
return decoder.decode(this.data);
}
}
) as any as typeof nodeBuffer.Buffer;
export type BufferType = Buffer;
function assertFileAccess() {
assert(isNode, 'Working with filesystem using "path" is only supported in Node.js');
}
export async function readFileAsync(file: string, encoding: string): Promise<string> {
assertFileAccess();
return await promisify(nodeFS.readFile)(file, encoding);
}
export async function readFileBuffer(file: string): Promise<BufferType> {
assertFileAccess();
return await promisify(nodeFS.readFile)(file);
}
export async function writeFileAsync(file: string, data: any) {
assertFileAccess();
return await promisify(nodeFS.writeFile)(file, data);
}
export function basename(file: string): string {
assertFileAccess();
return nodePath.basename(file);
}
export async function openFdAsync(file: string, flags: string): Promise<number> {
assertFileAccess();
return await promisify(nodeFS.open)(file, flags);
}
export async function writeFdAsync(fd: number, buffer: Buffer): Promise<void> {
assertFileAccess();
return await promisify(nodeFS.write)(fd, buffer);
}
export async function closeFdAsync(fd: number): Promise<void> {
assertFileAccess();
return await promisify(nodeFS.close)(fd);
}
export function getMimeType(file: string): string {
const extension = file.substring(file.lastIndexOf('.') + 1);
return extensionToMime[extension] || 'application/octet-stream';
}
export function pngToJpeg(buffer: Buffer, quality?: number): Buffer {
assert(isNode, 'Converting from png to jpeg is only supported in Node.js');
return jpeg.encode(png.PNG.sync.read(buffer), quality).data;
}
function nodeFetch(url: string): Promise<string> {
let resolve: (url: string) => void;
let reject: (e: Error) => void = () => {};
const promise = new Promise<string>((res, rej) => { resolve = res; reject = rej; });
const endpointURL = new URL(url);
const protocol = endpointURL.protocol === 'https:' ? https : http;
const request = protocol.request(endpointURL, res => {
let data = '';
if (res.statusCode !== 200) {
// Consume response data to free up memory.
res.resume();
reject(new Error('HTTP ' + res.statusCode));
return;
}
res.setEncoding('utf8');
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
});
request.on('error', reject);
request.end();
return promise;
}
export function fetchUrl(url: string): Promise<string> {
if (isNode)
return nodeFetch(url);
return fetch(url).then(response => {
if (!response.ok)
throw new Error('HTTP ' + response.status + ' ' + response.statusText);
return response.text();
});
}
// See https://joel.tools/microtasks/
export function makeWaitForNextTask() {
if (!isNode)
return (func: () => void) => setTimeout(func, 0);
if (parseInt(process.versions.node, 10) >= 11)
return setImmediate;
// Unlike Node 11, Node 10 and less have a bug with Task and MicroTask execution order:
// - https://github.com/nodejs/node/issues/22257
//
// So we can't simply run setImmediate to dispatch code in a following task.
// However, we can run setImmediate from-inside setImmediate to make sure we're getting
// in the following task.
let spinning = false;
const callbacks: (() => void)[] = [];
const loop = () => {
const callback = callbacks.shift();
if (!callback) {
spinning = false;
return;
}
setImmediate(loop);
// Make sure to call callback() as the last thing since it's
// untrusted code that might throw.
callback();
};
return (callback: () => void) => {
callbacks.push(callback);
if (!spinning) {
spinning = true;
setImmediate(loop);
}
};
}
export function guid(): string {
if (isNode)
return crypto.randomBytes(16).toString('hex');
const a = new Uint8Array(16);
window.crypto.getRandomValues(a);
return Array.from(a).map(b => b.toString(16).padStart(2, '0')).join('');
}
// 'onmessage' handler must be installed synchronously when 'onopen' callback is invoked to
// avoid missing incoming messages.
export async function connectToWebsocket<T>(url: string, onopen: (transport: ConnectionTransport) => Promise<T> | T): Promise<T> {
const transport = new WebSocketTransport(url);
return new Promise<T>((fulfill, reject) => {
transport._ws.addEventListener('open', async () => fulfill(await onopen(transport)));
transport._ws.addEventListener('error', event => reject(new Error('WebSocket error: ' + (event as ErrorEvent).message)));
});
}
class WebSocketTransport implements ConnectionTransport {
_ws: WebSocket;
onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
constructor(url: string) {
this._ws = (isNode ? new NodeWebSocket(url, [], {
perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb
}) : new WebSocket(url)) as WebSocket;
// The 'ws' module in node sometimes sends us multiple messages in a single task.
// In Web, all IO callbacks (e.g. WebSocket callbacks)
// are dispatched into separate tasks, so there's no need
// to do anything extra.
const messageWrap: (cb: () => void) => void = isNode ? makeWaitForNextTask() : cb => cb();
this._ws.addEventListener('message', event => {
messageWrap(() => {
if (this.onmessage)
this.onmessage.call(null, JSON.parse(event.data));
});
});
this._ws.addEventListener('close', event => {
if (this.onclose)
this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {});
}
send(message: ProtocolRequest) {
this._ws.send(JSON.stringify(message));
}
close() {
this._ws.close();
}
}
const extensionToMime: { [key: string]: string } = {
'ai': 'application/postscript',
'apng': 'image/apng',
'appcache': 'text/cache-manifest',
'au': 'audio/basic',
'bmp': 'image/bmp',
'cer': 'application/pkix-cert',
'cgm': 'image/cgm',
'coffee': 'text/coffeescript',
'conf': 'text/plain',
'crl': 'application/pkix-crl',
'css': 'text/css',
'csv': 'text/csv',
'def': 'text/plain',
'doc': 'application/msword',
'dot': 'application/msword',
'drle': 'image/dicom-rle',
'dtd': 'application/xml-dtd',
'ear': 'application/java-archive',
'emf': 'image/emf',
'eps': 'application/postscript',
'exr': 'image/aces',
'fits': 'image/fits',
'g3': 'image/g3fax',
'gbr': 'application/rpki-ghostbusters',
'gif': 'image/gif',
'glb': 'model/gltf-binary',
'gltf': 'model/gltf+json',
'gz': 'application/gzip',
'h261': 'video/h261',
'h263': 'video/h263',
'h264': 'video/h264',
'heic': 'image/heic',
'heics': 'image/heic-sequence',
'heif': 'image/heif',
'heifs': 'image/heif-sequence',
'htm': 'text/html',
'html': 'text/html',
'ics': 'text/calendar',
'ief': 'image/ief',
'ifb': 'text/calendar',
'iges': 'model/iges',
'igs': 'model/iges',
'in': 'text/plain',
'ini': 'text/plain',
'jade': 'text/jade',
'jar': 'application/java-archive',
'jls': 'image/jls',
'jp2': 'image/jp2',
'jpe': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpf': 'image/jpx',
'jpg': 'image/jpeg',
'jpg2': 'image/jp2',
'jpgm': 'video/jpm',
'jpgv': 'video/jpeg',
'jpm': 'image/jpm',
'jpx': 'image/jpx',
'js': 'application/javascript',
'json': 'application/json',
'json5': 'application/json5',
'jsx': 'text/jsx',
'jxr': 'image/jxr',
'kar': 'audio/midi',
'ktx': 'image/ktx',
'less': 'text/less',
'list': 'text/plain',
'litcoffee': 'text/coffeescript',
'log': 'text/plain',
'm1v': 'video/mpeg',
'm21': 'application/mp21',
'm2a': 'audio/mpeg',
'm2v': 'video/mpeg',
'm3a': 'audio/mpeg',
'm4a': 'audio/mp4',
'm4p': 'application/mp4',
'man': 'text/troff',
'manifest': 'text/cache-manifest',
'markdown': 'text/markdown',
'mathml': 'application/mathml+xml',
'md': 'text/markdown',
'mdx': 'text/mdx',
'me': 'text/troff',
'mesh': 'model/mesh',
'mft': 'application/rpki-manifest',
'mid': 'audio/midi',
'midi': 'audio/midi',
'mj2': 'video/mj2',
'mjp2': 'video/mj2',
'mjs': 'application/javascript',
'mml': 'text/mathml',
'mov': 'video/quicktime',
'mp2': 'audio/mpeg',
'mp21': 'application/mp21',
'mp2a': 'audio/mpeg',
'mp3': 'audio/mpeg',
'mp4': 'video/mp4',
'mp4a': 'audio/mp4',
'mp4s': 'application/mp4',
'mp4v': 'video/mp4',
'mpe': 'video/mpeg',
'mpeg': 'video/mpeg',
'mpg': 'video/mpeg',
'mpg4': 'video/mp4',
'mpga': 'audio/mpeg',
'mrc': 'application/marc',
'ms': 'text/troff',
'msh': 'model/mesh',
'n3': 'text/n3',
'oga': 'audio/ogg',
'ogg': 'audio/ogg',
'ogv': 'video/ogg',
'ogx': 'application/ogg',
'otf': 'font/otf',
'p10': 'application/pkcs10',
'p7c': 'application/pkcs7-mime',
'p7m': 'application/pkcs7-mime',
'p7s': 'application/pkcs7-signature',
'p8': 'application/pkcs8',
'pdf': 'application/pdf',
'pki': 'application/pkixcmp',
'pkipath': 'application/pkix-pkipath',
'png': 'image/png',
'ps': 'application/postscript',
'pskcxml': 'application/pskc+xml',
'qt': 'video/quicktime',
'rmi': 'audio/midi',
'rng': 'application/xml',
'roa': 'application/rpki-roa',
'roff': 'text/troff',
'rsd': 'application/rsd+xml',
'rss': 'application/rss+xml',
'rtf': 'application/rtf',
'rtx': 'text/richtext',
's3m': 'audio/s3m',
'sgi': 'image/sgi',
'sgm': 'text/sgml',
'sgml': 'text/sgml',
'shex': 'text/shex',
'shtml': 'text/html',
'sil': 'audio/silk',
'silo': 'model/mesh',
'slim': 'text/slim',
'slm': 'text/slim',
'snd': 'audio/basic',
'spx': 'audio/ogg',
'stl': 'model/stl',
'styl': 'text/stylus',
'stylus': 'text/stylus',
'svg': 'image/svg+xml',
'svgz': 'image/svg+xml',
't': 'text/troff',
't38': 'image/t38',
'text': 'text/plain',
'tfx': 'image/tiff-fx',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'tr': 'text/troff',
'ts': 'video/mp2t',
'tsv': 'text/tab-separated-values',
'ttc': 'font/collection',
'ttf': 'font/ttf',
'ttl': 'text/turtle',
'txt': 'text/plain',
'uri': 'text/uri-list',
'uris': 'text/uri-list',
'urls': 'text/uri-list',
'vcard': 'text/vcard',
'vrml': 'model/vrml',
'vtt': 'text/vtt',
'war': 'application/java-archive',
'wasm': 'application/wasm',
'wav': 'audio/wav',
'weba': 'audio/webm',
'webm': 'video/webm',
'webmanifest': 'application/manifest+json',
'webp': 'image/webp',
'wmf': 'image/wmf',
'woff': 'font/woff',
'woff2': 'font/woff2',
'wrl': 'model/vrml',
'x3d': 'model/x3d+xml',
'x3db': 'model/x3d+fastinfoset',
'x3dbz': 'model/x3d+binary',
'x3dv': 'model/x3d-vrml',
'x3dvz': 'model/x3d+vrml',
'x3dz': 'model/x3d+xml',
'xaml': 'application/xaml+xml',
'xht': 'application/xhtml+xml',
'xhtml': 'application/xhtml+xml',
'xm': 'audio/xm',
'xml': 'text/xml',
'xsd': 'application/xml',
'xsl': 'application/xml',
'xslt': 'application/xslt+xml',
'yaml': 'text/yaml',
'yml': 'text/yaml',
'zip': 'application/zip'
};

View File

@ -15,11 +15,13 @@
* limitations under the License.
*/
import * as fs from 'fs';
import * as mime from 'mime';
import * as util from 'util';
import * as dom from './dom';
import { assert, helper } from './helper';
import * as types from './types';
import { Page } from './page';
import * as platform from './platform';
import * as types from './types';
export class Screenshotter {
private _queue = new TaskQueue();
@ -78,7 +80,7 @@ export class Screenshotter {
return fullPageSize;
}
async screenshotPage(options: types.ScreenshotOptions = {}): Promise<platform.BufferType> {
async screenshotPage(options: types.ScreenshotOptions = {}): Promise<Buffer> {
const format = validateScreenshotOptions(options);
return this._queue.postTask(async () => {
const { viewportSize, originalViewportSize } = await this._originalViewportSize();
@ -102,7 +104,7 @@ export class Screenshotter {
}).catch(rewriteError);
}
async screenshotElement(handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise<platform.BufferType> {
async screenshotElement(handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise<Buffer> {
const format = validateScreenshotOptions(options);
return this._queue.postTask(async () => {
const { viewportSize, originalViewportSize } = await this._originalViewportSize();
@ -138,7 +140,7 @@ export class Screenshotter {
}).catch(rewriteError);
}
private async _screenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, options: types.ElementScreenshotOptions, overridenViewportSize: types.Size | null, originalViewportSize: types.Size | null): Promise<platform.BufferType> {
private async _screenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, options: types.ElementScreenshotOptions, overridenViewportSize: types.Size | null, originalViewportSize: types.Size | null): Promise<Buffer> {
const shouldSetDefaultBackground = options.omitBackground && format === 'png';
if (shouldSetDefaultBackground)
await this._page._delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0});
@ -153,7 +155,7 @@ export class Screenshotter {
await this._page._delegate.resetViewport();
}
if (options.path)
await platform.writeFileAsync(options.path, buffer);
await util.promisify(fs.writeFile)(options.path, buffer);
return buffer;
}
}
@ -196,7 +198,7 @@ function validateScreenshotOptions(options: types.ScreenshotOptions): 'png' | 'j
assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type);
format = options.type;
} else if (options.path) {
const mimeType = platform.getMimeType(options.path);
const mimeType = mime.getType(options.path);
if (mimeType === 'image/png')
format = 'png';
else if (mimeType === 'image/jpeg')

View File

@ -25,10 +25,9 @@ import * as path from 'path';
import { getProxyForUrl } from 'proxy-from-env';
import * as URL from 'url';
import { assert } from '../helper';
import * as platform from '../platform';
const unlinkAsync = platform.promisify(fs.unlink.bind(fs));
const chmodAsync = platform.promisify(fs.chmod.bind(fs));
const unlinkAsync = util.promisify(fs.unlink.bind(fs));
const chmodAsync = util.promisify(fs.chmod.bind(fs));
const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
const DEFAULT_DOWNLOAD_HOSTS = {

View File

@ -15,7 +15,7 @@
*/
import { ChildProcess, execSync } from 'child_process';
import * as platform from '../platform';
import { EventEmitter } from 'events';
export class WebSocketWrapper {
readonly wsEndpoint: string;
@ -46,7 +46,7 @@ export class WebSocketWrapper {
}
}
export class BrowserServer extends platform.EventEmitter {
export class BrowserServer extends EventEmitter {
private _process: ChildProcess;
private _gracefullyClose: () => Promise<void>;
private _webSocketWrapper: WebSocketWrapper | null;

View File

@ -18,9 +18,9 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import { debugError, helper, assert } from '../helper';
import { CRBrowser } from '../chromium/crBrowser';
import * as platform from '../platform';
import * as ws from 'ws';
import { launchProcess } from '../server/processLauncher';
import { kBrowserCloseMessageId } from '../chromium/crConnection';
@ -29,7 +29,7 @@ import { LaunchOptions, BrowserArgOptions, BrowserType, ConnectOptions, LaunchSe
import { LaunchType } from '../browser';
import { BrowserServer, WebSocketWrapper } from './browserServer';
import { Events } from '../events';
import { ConnectionTransport, ProtocolRequest } from '../transport';
import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../transport';
import { BrowserContext } from '../browserContext';
export class Chromium implements BrowserType<CRBrowser> {
@ -85,14 +85,14 @@ export class Chromium implements BrowserType<CRBrowser> {
let temporaryUserDataDir: string | null = null;
if (!userDataDir) {
userDataDir = await mkdtempAsync(CHROMIUM_PROFILE_PATH);
temporaryUserDataDir = userDataDir!;
temporaryUserDataDir = userDataDir;
}
const chromeArguments = [];
if (!ignoreDefaultArgs)
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir!));
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir));
else if (Array.isArray(ignoreDefaultArgs))
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir!).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
chromeArguments.push(...this._defaultArgs(options, launchType, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
else
chromeArguments.push(...args);
@ -133,7 +133,7 @@ export class Chromium implements BrowserType<CRBrowser> {
}
async connect(options: ConnectOptions): Promise<CRBrowser> {
return await platform.connectToWebsocket(options.wsEndpoint, transport => {
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
return CRBrowser.connect(transport, false, options.slowMo);
});
}
@ -178,7 +178,7 @@ export class Chromium implements BrowserType<CRBrowser> {
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper {
const server = new ws.Server({ port });
const guid = platform.guid();
const guid = helper.guid();
const awaitingBrowserTarget = new Map<number, ws>();
const sessionToSocket = new Map<string, ws>();
@ -296,7 +296,7 @@ function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number
}
const mkdtempAsync = platform.promisify(fs.mkdtemp);
const mkdtempAsync = util.promisify(fs.mkdtemp);
const CHROMIUM_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');

View File

@ -18,6 +18,7 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as util from 'util';
import * as ws from 'ws';
import { LaunchType } from '../browser';
import { BrowserContext } from '../browserContext';
@ -26,13 +27,12 @@ import { Events } from '../events';
import { FFBrowser } from '../firefox/ffBrowser';
import { kBrowserCloseMessageId } from '../firefox/ffConnection';
import { debugError, helper, assert } from '../helper';
import * as platform from '../platform';
import { BrowserServer, WebSocketWrapper } from './browserServer';
import { BrowserArgOptions, BrowserType, LaunchOptions, LaunchServerOptions, ConnectOptions } from './browserType';
import { launchProcess, waitForLine } from './processLauncher';
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
const mkdtempAsync = platform.promisify(fs.mkdtemp);
const mkdtempAsync = util.promisify(fs.mkdtemp);
export class Firefox implements BrowserType<FFBrowser> {
private _executablePath: (string|undefined);
@ -50,7 +50,7 @@ export class Firefox implements BrowserType<FFBrowser> {
async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const browserServer = await this._launchServer(options, 'local');
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => {
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, false, options.slowMo);
});
// Hack: for typical launch scenario, ensure that close waits for actual process termination.
@ -69,7 +69,7 @@ export class Firefox implements BrowserType<FFBrowser> {
slowMo = 0,
} = options;
const browserServer = await this._launchServer(options, 'persistent', userDataDir);
const browser = await platform.connectToWebsocket(browserServer.wsEndpoint()!, transport => {
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, true, slowMo);
});
await helper.waitWithTimeout(browser._firstPagePromise, 'first page', timeout);
@ -103,9 +103,9 @@ export class Firefox implements BrowserType<FFBrowser> {
}
if (!ignoreDefaultArgs)
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir!, 0));
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, 0));
else if (Array.isArray(ignoreDefaultArgs))
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir!, 0).filter(arg => !ignoreDefaultArgs.includes(arg)));
firefoxArguments.push(...this._defaultArgs(options, launchType, userDataDir, 0).filter(arg => !ignoreDefaultArgs.includes(arg)));
else
firefoxArguments.push(...args);
@ -133,7 +133,7 @@ export class Firefox implements BrowserType<FFBrowser> {
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId.
const transport = await platform.connectToWebsocket(browserWSEndpoint!, async transport => transport);
const transport = await WebSocketTransport.connect(browserWSEndpoint!, async transport => transport);
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
await transport.send(message);
},
@ -149,14 +149,14 @@ export class Firefox implements BrowserType<FFBrowser> {
let browserServer: BrowserServer | undefined = undefined;
let browserWSEndpoint: string | undefined = undefined;
const webSocketWrapper = launchType === 'server' ? (await platform.connectToWebsocket(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new WebSocketWrapper(innerEndpoint, []);
const webSocketWrapper = launchType === 'server' ? (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, port))) : new WebSocketWrapper(innerEndpoint, []);
browserWSEndpoint = webSocketWrapper.wsEndpoint;
browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper);
return browserServer;
}
async connect(options: ConnectOptions): Promise<FFBrowser> {
return await platform.connectToWebsocket(options.wsEndpoint, transport => {
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
return FFBrowser.connect(transport, false, options.slowMo);
});
}
@ -197,7 +197,7 @@ export class Firefox implements BrowserType<FFBrowser> {
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper {
const server = new ws.Server({ port });
const guid = platform.guid();
const guid = helper.guid();
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
const pendingBrowserContextCreations = new Set<number>();
const pendingBrowserContextDeletions = new Map<number, string>();

View File

@ -17,13 +17,12 @@
import { debugError, helper, RegisteredListener } from '../helper';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { makeWaitForNextTask } from '../platform';
export class PipeTransport implements ConnectionTransport {
private _pipeWrite: NodeJS.WritableStream | null;
private _pendingMessage = '';
private _eventListeners: RegisteredListener[];
private _waitForNextTask = makeWaitForNextTask();
private _waitForNextTask = helper.makeWaitForNextTask();
private readonly _closeCallback: () => void;
onmessage?: (message: ProtocolResponse) => void;

View File

@ -16,14 +16,15 @@
*/
import * as childProcess from 'child_process';
import * as stream from 'stream';
import * as removeFolder from 'rimraf';
import { helper } from '../helper';
import * as debug from 'debug';
import * as readline from 'readline';
import * as removeFolder from 'rimraf';
import * as stream from 'stream';
import * as util from 'util';
import { TimeoutError } from '../errors';
import * as platform from '../platform';
import { helper } from '../helper';
const removeFolderAsync = platform.promisify(removeFolder);
const removeFolderAsync = util.promisify(removeFolder);
export type LaunchProcessOptions = {
executablePath: string,
@ -48,9 +49,9 @@ let lastLaunchedId = 0;
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
const id = ++lastLaunchedId;
const debugBrowser = platform.debug(`pw:browser:proc:[${id}]`);
const debugBrowserOut = platform.debug(`pw:browser:out:[${id}]`);
const debugBrowserErr = platform.debug(`pw:browser:err:[${id}]`);
const debugBrowser = debug(`pw:browser:proc:[${id}]`);
const debugBrowserOut = debug(`pw:browser:out:[${id}]`);
const debugBrowserErr = debug(`pw:browser:err:[${id}]`);
(debugBrowser as any).color = '33';
(debugBrowserOut as any).color = '178';
(debugBrowserErr as any).color = '160';

View File

@ -20,12 +20,12 @@ import { PipeTransport } from './pipeTransport';
import { launchProcess } from './processLauncher';
import * as fs from 'fs';
import * as path from 'path';
import * as platform from '../platform';
import * as os from 'os';
import * as util from 'util';
import { debugError, helper, assert } from '../helper';
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
import { LaunchOptions, BrowserArgOptions, BrowserType, LaunchServerOptions, ConnectOptions } from './browserType';
import { ConnectionTransport, SequenceNumberMixer } from '../transport';
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
import * as ws from 'ws';
import { LaunchType } from '../browser';
import { BrowserServer, WebSocketWrapper } from './browserServer';
@ -85,14 +85,14 @@ export class WebKit implements BrowserType<WKBrowser> {
let temporaryUserDataDir: string | null = null;
if (!userDataDir) {
userDataDir = await mkdtempAsync(WEBKIT_PROFILE_PATH);
temporaryUserDataDir = userDataDir!;
temporaryUserDataDir = userDataDir;
}
const webkitArguments = [];
if (!ignoreDefaultArgs)
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir!, port));
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port));
else if (Array.isArray(ignoreDefaultArgs))
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir!, port).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
webkitArguments.push(...this._defaultArgs(options, launchType, userDataDir, port).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
else
webkitArguments.push(...args);
@ -103,7 +103,7 @@ export class WebKit implements BrowserType<WKBrowser> {
const { launchedProcess, gracefullyClose } = await launchProcess({
executablePath: webkitExecutable,
args: webkitArguments,
env: { ...env, CURL_COOKIE_JAR_PATH: path.join(userDataDir!, 'cookiejar.db') },
env: { ...env, CURL_COOKIE_JAR_PATH: path.join(userDataDir, 'cookiejar.db') },
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
@ -133,7 +133,7 @@ export class WebKit implements BrowserType<WKBrowser> {
}
async connect(options: ConnectOptions): Promise<WKBrowser> {
return await platform.connectToWebsocket(options.wsEndpoint, transport => {
return await WebSocketTransport.connect(options.wsEndpoint, transport => {
return WKBrowser.connect(transport, options.slowMo);
});
}
@ -163,13 +163,13 @@ export class WebKit implements BrowserType<WKBrowser> {
}
}
const mkdtempAsync = platform.promisify(fs.mkdtemp);
const mkdtempAsync = util.promisify(fs.mkdtemp);
const WEBKIT_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-');
function wrapTransportWithWebSocket(transport: ConnectionTransport, port: number): WebSocketWrapper {
const server = new ws.Server({ port });
const guid = platform.guid();
const guid = helper.guid();
const idMixer = new SequenceNumberMixer<{id: number, socket: ws}>();
const pendingBrowserContextCreations = new Set<number>();
const pendingBrowserContextDeletions = new Map<number, string>();

View File

@ -15,6 +15,9 @@
* limitations under the License.
*/
import * as WebSocket from 'ws';
import { helper } from './helper';
export type ProtocolRequest = {
id: number;
method: string;
@ -114,6 +117,57 @@ export class DeferWriteTransport implements ConnectionTransport {
}
}
export class WebSocketTransport implements ConnectionTransport {
_ws: WebSocket;
onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void;
// 'onmessage' handler must be installed synchronously when 'onopen' callback is invoked to
// avoid missing incoming messages.
static connect<T>(url: string, onopen: (transport: ConnectionTransport) => Promise<T> | T): Promise<T> {
const transport = new WebSocketTransport(url);
return new Promise<T>((fulfill, reject) => {
transport._ws.addEventListener('open', async () => fulfill(await onopen(transport)));
transport._ws.addEventListener('error', event => reject(new Error('WebSocket error: ' + event.message)));
});
}
constructor(url: string) {
this._ws = new WebSocket(url, [], {
perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb
});
// The 'ws' module in node sometimes sends us multiple messages in a single task.
// In Web, all IO callbacks (e.g. WebSocket callbacks)
// are dispatched into separate tasks, so there's no need
// to do anything extra.
const messageWrap: (cb: () => void) => void = helper.makeWaitForNextTask();
this._ws.addEventListener('message', event => {
messageWrap(() => {
if (this.onmessage)
this.onmessage.call(null, JSON.parse(event.data));
});
});
this._ws.addEventListener('close', event => {
if (this.onclose)
this.onclose.call(null);
});
// Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {});
}
send(message: ProtocolRequest) {
this._ws.send(JSON.stringify(message));
}
close() {
this._ws.close();
}
}
export class SequenceNumberMixer<V> {
static _lastSequenceNumber = 1;
private _values = new Map<number, V>();

View File

@ -1,45 +0,0 @@
/**
* 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 { CRBrowser as ChromiumBrowser } from './chromium/crBrowser';
import { FFBrowser as FirefoxBrowser } from './firefox/ffBrowser';
import { WKBrowser as WebKitBrowser } from './webkit/wkBrowser';
import * as platform from './platform';
const connect = {
chromium: {
connect: async (url: string) => {
return await platform.connectToWebsocket(url, transport => {
return ChromiumBrowser.connect(transport, false);
});
}
},
webkit: {
connect: async (url: string) => {
return await platform.connectToWebsocket(url, transport => {
return WebKitBrowser.connect(transport);
});
}
},
firefox: {
connect: async (url: string) => {
return await platform.connectToWebsocket(url, transport => {
return FirefoxBrowser.connect(transport, false);
});
}
}
};
export = connect;

View File

@ -1,59 +0,0 @@
/**
* 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.
*/
const path = require('path');
module.exports = {
entry: path.join(__dirname, 'web.ts'),
devtool: 'source-map',
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
transpileOnly: true
},
exclude: [
/node_modules/,
/crypto/,
]
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'web.js',
library: 'playwrightweb',
libraryTarget: 'window',
path: path.resolve(__dirname, '../')
},
externals: {
'crypto': 'dummy',
'events': 'dummy',
'fs': 'dummy',
'path': 'dummy',
'debug': 'dummy',
'buffer': 'dummy',
'jpeg-js': 'dummy',
'pngjs': 'dummy',
'http': 'dummy',
'https': 'dummy',
'ws': 'dummy',
}
};

View File

@ -21,16 +21,16 @@ import { Events } from '../events';
import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network';
import { Page, PageBinding } from '../page';
import * as platform from '../platform';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types';
import { Protocol } from './protocol';
import { kPageProxyMessageReceived, PageProxyMessageReceivedPayload, WKConnection, WKSession } from './wkConnection';
import { WKPage } from './wkPage';
import { EventEmitter } from 'events';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
export class WKBrowser extends platform.EventEmitter implements Browser {
export class WKBrowser extends EventEmitter implements Browser {
private readonly _connection: WKConnection;
private readonly _attachToDefaultContext: boolean;
readonly _browserSession: WKSession;
@ -186,7 +186,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
await disconnected;
}
_setDebugFunction(debugFunction: platform.DebuggerType) {
_setDebugFunction(debugFunction: debug.IDebugger) {
this._connection._debugProtocol = debugFunction;
}
}

View File

@ -15,8 +15,9 @@
* limitations under the License.
*/
import * as debug from 'debug';
import { EventEmitter } from 'events';
import { assert } from '../helper';
import * as platform from '../platform';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol';
@ -34,7 +35,7 @@ export class WKConnection {
private readonly _onDisconnect: () => void;
private _lastId = 0;
private _closed = false;
_debugProtocol: platform.DebuggerType = platform.debug('pw:protocol');
_debugProtocol = debug('pw:protocol');
readonly browserSession: WKSession;
@ -90,7 +91,7 @@ export class WKConnection {
}
}
export class WKSession extends platform.EventEmitter {
export class WKSession extends EventEmitter {
connection: WKConnection;
errorText: string;
readonly sessionId: string;
@ -127,7 +128,7 @@ export class WKSession extends platform.EventEmitter {
throw new Error(`Protocol error (${method}): ${this.errorText}`);
const id = this.connection.nextMessageId();
const messageObj = { id, method, params };
platform.debug('pw:wrapped:' + this.sessionId)('SEND ► ' + JSON.stringify(messageObj, null, 2));
debug('pw:wrapped:' + this.sessionId)('SEND ► ' + JSON.stringify(messageObj, null, 2));
this._rawSend(messageObj);
return new Promise<Protocol.CommandReturnValues[T]>((resolve, reject) => {
this._callbacks.set(id, {resolve, reject, error: new Error(), method});
@ -146,7 +147,7 @@ export class WKSession extends platform.EventEmitter {
}
dispatchMessage(object: any) {
platform.debug('pw:wrapped:' + this.sessionId)('◀ RECV ' + JSON.stringify(object, null, 2));
debug('pw:wrapped:' + this.sessionId)('◀ RECV ' + JSON.stringify(object, null, 2));
if (object.id && this._callbacks.has(object.id)) {
const callback = this._callbacks.get(object.id)!;
this._callbacks.delete(object.id);

View File

@ -18,7 +18,6 @@
import * as frames from '../frames';
import { assert, debugError, helper } from '../helper';
import * as network from '../network';
import * as platform from '../platform';
import { Protocol } from './protocol';
import { WKSession } from './wkConnection';
@ -80,7 +79,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
if (response.contentType)
responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders))
responseHeaders['content-length'] = String(platform.Buffer.byteLength(responseBody));
responseHeaders['content-length'] = String(Buffer.byteLength(responseBody));
await this._session.send('Network.interceptWithResponse', {
requestId: this._requestId,
@ -114,7 +113,7 @@ export class WKInterceptableRequest implements network.RouteDelegate {
createResponse(responsePayload: Protocol.Network.Response): network.Response {
const getResponseBody = async () => {
const response = await this._session.send('Network.getResponseBody', { requestId: this._requestId });
return platform.Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
};
return new network.Response(this.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), getResponseBody);
}

View File

@ -30,11 +30,12 @@ import * as dialog from '../dialog';
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
import * as types from '../types';
import * as accessibility from '../accessibility';
import * as platform from '../platform';
import { getAccessibilityTree } from './wkAccessibility';
import { WKProvisionalPage } from './wkProvisionalPage';
import { WKBrowserContext } from './wkBrowser';
import { selectors } from '../selectors';
import * as jpeg from 'jpeg-js';
import * as png from 'pngjs';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -73,7 +74,7 @@ export class WKPage implements PageDelegate {
this._workers = new WKWorkers(this._page);
this._session = undefined as any as WKSession;
this._browserContext = browserContext;
this._page.on(Events.Page.FrameDetached, frame => this._removeContextsForFrame(frame, false));
this._page.on(Events.Page.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
this._eventListeners = [
helper.addEventListener(this._pageProxySession, 'Target.targetCreated', this._onTargetCreated.bind(this)),
helper.addEventListener(this._pageProxySession, 'Target.targetDestroyed', this._onTargetDestroyed.bind(this)),
@ -618,15 +619,15 @@ export class WKPage implements PageDelegate {
await this._session.send('Page.setDefaultBackgroundColorOverride', { color });
}
async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<platform.BufferType> {
async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise<Buffer> {
// TODO: documentRect does not include pageScale, while backend considers it does.
// This brakes mobile screenshots of elements or full page.
const rect = (documentRect || viewportRect)!;
const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: documentRect ? 'Page' : 'Viewport' });
const prefix = 'data:image/png;base64,';
let buffer = platform.Buffer.from(result.dataURL.substr(prefix.length), 'base64');
let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64');
if (format === 'jpeg')
buffer = platform.pngToJpeg(buffer, quality);
buffer = jpeg.encode(png.PNG.sync.read(buffer), quality).data;
return buffer;
}

View File

@ -225,8 +225,4 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
loadTests('./chromium/oopif.spec.js');
loadTests('./chromium/tracing.spec.js');
});
describe('[Driver]', () => {
loadTests('./web.spec.js');
});
};

View File

@ -1,93 +0,0 @@
/**
* 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.
*/
/**
* @type {PageTestSuite}
*/
module.exports.describe = function({testRunner, expect, defaultBrowserOptions, browserType, product, CHROMIUM, FFOX}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('Web SDK', function() {
beforeAll(async state => {
state.controlledBrowserApp = await browserType.launchServer(defaultBrowserOptions);
state.hostBrowser = await browserType.launch(defaultBrowserOptions);
});
afterAll(async state => {
await state.hostBrowser.close();
state.hostBrowser = null;
await state.controlledBrowserApp.close();
state.controlledBrowserApp = null;
state.webUrl = null;
});
beforeEach(async state => {
state.page = await state.hostBrowser.newPage();
state.page.on('console', message => console.log('TEST: ' + message.text()));
await state.page.goto(state.sourceServer.PREFIX + '/test/assets/playwrightweb.html');
await state.page.evaluate(([product, wsEndpoint]) => setup(product, wsEndpoint), [product.toLowerCase(), state.controlledBrowserApp.wsEndpoint()]);
});
afterEach(async state => {
await state.page.evaluate(() => teardown());
await state.page.close();
state.page = null;
});
it('should navigate', async({page, server}) => {
const url = await page.evaluate(async url => {
await page.goto(url);
return page.evaluate(() => window.location.href);
}, server.EMPTY_PAGE);
expect(url).toBe(server.EMPTY_PAGE);
});
it('should evaluate handles', async({page, server}) => {
const foo = await page.evaluateHandle(() => ({ x: 1, y: 'foo' }));
const result = await page.evaluate(({ foo }) => {
return foo;
}, { foo });
expect(result).toEqual({ x: 1, y: 'foo' });
});
it('should receive events', async({page, server}) => {
const logs = await page.evaluate(async () => {
const logs = [];
page.on('console', message => logs.push(message.text()));
await page.evaluate(() => console.log('hello'));
await page.evaluate(() => console.log('world'));
return logs;
});
expect(logs).toEqual(['hello', 'world']);
});
it('should take screenshot', async({page, server}) => {
const { base64, bufferClassName } = await page.evaluate(async url => {
await page.setViewportSize({width: 500, height: 500});
await page.goto(url);
const screenshot = await page.screenshot();
return { base64: screenshot.toString('base64'), bufferClassName: screenshot.constructor.name };
}, server.PREFIX + '/grid.html');
const screenshot = Buffer.from(base64, 'base64');
expect(screenshot).toBeGolden('screenshot-sanity.png');
// Verify that we use web versions of node-specific classes.
expect(bufferClassName).toBe('BufferImpl');
});
});
};

View File

@ -23,9 +23,8 @@ module.exports = { checkSources, expandPrefix };
/**
* @param {!Array<!import('../Source')>} sources
* @param {!Array<string>} externalDependencies
*/
function checkSources(sources, externalDependencies) {
function checkSources(sources) {
// special treatment for Events.js
const classEvents = new Map();
const eventsSources = sources.filter(source => source.name().startsWith('events.'));
@ -108,33 +107,16 @@ function checkSources(sources, externalDependencies) {
}
if (fileName.endsWith('/api.ts') && ts.isExportSpecifier(node))
apiClassNames.add(expandPrefix((node.propertyName || node.name).text));
const isPlatform = fileName.endsWith('platform.ts');
if (!fileName.includes('src/server/')) {
// Only relative imports.
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const module = node.moduleSpecifier.text;
const isRelative = module.startsWith('.');
const isPlatformDependency = isPlatform && externalDependencies.includes(module);
const isServerDependency = path.resolve(path.dirname(fileName), module).includes('src/server');
if (isServerDependency || (!isRelative && !isPlatformDependency)) {
if (isServerDependency) {
const lac = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.moduleSpecifier.pos);
errors.push(`Disallowed import "${module}" at ${node.getSourceFile().fileName}:${lac.line + 1}`);
}
}
// No references to external types.
if (!isPlatform && ts.isTypeReferenceNode(node)) {
const isPlatformReference = ts.isQualifiedName(node.typeName) && ts.isIdentifier(node.typeName.left) && node.typeName.left.escapedText === 'platform';
if (!isPlatformReference) {
const type = checker.getTypeAtLocation(node);
if (type.symbol && type.symbol.valueDeclaration) {
const source = type.symbol.valueDeclaration.getSourceFile();
if (source.fileName.includes('@types')) {
const lac = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.pos);
errors.push(`Disallowed type reference "${type.symbol.escapedName}" at ${node.getSourceFile().fileName}:${lac.line + 1}:${lac.character + 1}`);
}
}
}
}
}
ts.forEachChild(node, visit);
}

View File

@ -33,9 +33,9 @@ const EXCLUDE_PROPERTIES = new Set([
* @param {!Array<!Source>} mdSources
* @return {!Promise<!Array<!Message>>}
*/
module.exports = async function lint(page, mdSources, jsSources, externalDependencies) {
module.exports = async function lint(page, mdSources, jsSources) {
const mdResult = await mdBuilder(page, mdSources);
const jsResult = jsBuilder.checkSources(jsSources, externalDependencies);
const jsResult = jsBuilder.checkSources(jsSources);
const jsDocumentation = filterJSDocumentation(jsSources, jsResult.documentation);
const mdDocumentation = mdResult.documentation;

View File

@ -61,8 +61,7 @@ async function run() {
const page = await browser.newPage();
const checkPublicAPI = require('./check_public_api');
const jsSources = await Source.readdir(path.join(PROJECT_DIR, 'src'));
const externalDependencies = Object.keys(require('../../src/web.webpack.config').externals);
messages.push(...await checkPublicAPI(page, mdSources, jsSources, externalDependencies));
messages.push(...await checkPublicAPI(page, mdSources, jsSources));
await browser.close();
for (const source of mdSources) {

View File

@ -37,7 +37,7 @@ let documentation;
const {documentation: mdDocumentation} = await require('../doclint/check_public_api/MDBuilder')(page, [api]);
await browser.close();
const sources = await Source.readdir(path.join(PROJECT_DIR, 'src'));
const {documentation: jsDocumentation} = await require('../doclint/check_public_api/JSBuilder').checkSources(sources, Object.keys(require('../../src/web.webpack.config').externals));
const {documentation: jsDocumentation} = await require('../doclint/check_public_api/JSBuilder').checkSources(sources);
documentation = mergeDocumentation(mdDocumentation, jsDocumentation);
const handledClasses = new Set();

View File

@ -20,7 +20,6 @@ const path = require('path');
const files = [
path.join('src', 'injected', 'zsSelectorEngine.webpack.config.js'),
path.join('src', 'injected', 'selectorEvaluator.webpack.config.js'),
path.join('src', 'web.webpack.config.js'),
];
function runOne(runner, file) {