diff --git a/package.json b/package.json index 0ff910cd10..90a1910015 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/browser.ts b/src/browser.ts index 02cb42c305..582ab6cc0b 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -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; contexts(): BrowserContext[]; newPage(options?: BrowserContextOptions): Promise; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 21d1b52622..6eb66ed766 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -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 | null = null; @@ -221,7 +221,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser { }); } - async stopTracing(): Promise { + async stopTracing(): Promise { 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; } } diff --git a/src/chromium/crConnection.ts b/src/chromium/crConnection.ts index 6a4f017edd..da47e1b543 100644 --- a/src/chromium/crConnection.ts +++ b/src/chromium/crConnection.ts @@ -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(); 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 void, reject: (e: Error) => void, error: Error, method: string}>(); private readonly _targetType: string; diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index 5032f55d1f..f75cfe4733 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -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!, diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 17294221f7..3f61b3bf60 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -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 { + async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { 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 { @@ -587,7 +586,7 @@ export class CRPage implements PageDelegate { await this._client.send('Page.enable').catch(e => {}); } - async pdf(options?: types.PDFOptions): Promise { + async pdf(options?: types.PDFOptions): Promise { return this._pdf.generate(options); } diff --git a/src/chromium/crPdf.ts b/src/chromium/crPdf.ts index b5964f5fb9..e49937ad84 100644 --- a/src/chromium/crPdf.ts +++ b/src/chromium/crPdf.ts @@ -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 { + async generate(options: types.PDFOptions = {}): Promise { const { scale = 1, displayHeaderFooter = false, diff --git a/src/chromium/crProtocolHelper.ts b/src/chromium/crProtocolHelper.ts index 83f57bf693..bcc7b3c948 100644 --- a/src/chromium/crProtocolHelper.ts +++ b/src/chromium/crProtocolHelper.ts @@ -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 { +export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise { 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) { diff --git a/src/dom.ts b/src/dom.ts index e2166f2dde..9644c1daf3 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -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 extends js.JSHandle { 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 extends js.JSHandle { return this._page._delegate.getBoundingBox(this); } - async screenshot(options?: types.ElementScreenshotOptions): Promise { + async screenshot(options?: types.ElementScreenshotOptions): Promise { return this._page._screenshotter.screenshotElement(this, options); } diff --git a/src/extendedEventEmitter.ts b/src/extendedEventEmitter.ts index 49ae8f339b..b6ffb00361 100644 --- a/src/extendedEventEmitter.ts +++ b/src/extendedEventEmitter.ts @@ -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 { diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index ca8e5131fc..c065bdd03a 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -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; 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; } } diff --git a/src/firefox/ffConnection.ts b/src/firefox/ffConnection.ts index 5cbf4df4a5..98484d68f3 100644 --- a/src/firefox/ffConnection.ts +++ b/src/firefox/ffConnection.ts @@ -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; private _transport: ConnectionTransport; readonly _sessions: Map; - _debugProtocol: platform.DebuggerType = platform.debug('pw:protocol'); + _debugProtocol = debug('pw:protocol'); _closed: boolean; on: (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; diff --git a/src/firefox/ffNetworkManager.ts b/src/firefox/ffNetworkManager.ts index ed8de02f7a..eeb08ca114 100644 --- a/src/firefox/ffNetworkManager.ts +++ b/src/firefox/ffNetworkManager.ts @@ -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, diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 3c923e6414..4bca44a1bc 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -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 { + async takeScreenshot(format: 'png' | 'jpeg', documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { 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 { diff --git a/src/frames.ts b/src/frames.ts index f21f7223f2..9c765af0ee 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -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()!; } diff --git a/src/helper.ts b/src/helper.ts index a91087a640..faa42b4e6a 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -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 { diff --git a/src/javascript.ts b/src/javascript.ts index 5e69309176..d39049c85a 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -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; @@ -144,7 +144,7 @@ export async function prepareFunctionCall( const handles: (Promise)[] = []; const toDispose: Promise[] = []; const pushHandle = (handle: Promise): string => { - const guid = platform.guid(); + const guid = helper.guid(); guids.push(guid); handles.push(handle); return guid; diff --git a/src/network.ts b/src/network.ts index 137505fea7..99a76d8427 100644 --- a/src/network.ts +++ b/src/network.ts @@ -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; +type GetResponseBodyCallback = () => Promise; export class Response { private _request: Request; - private _contentPromise: Promise | null = null; + private _contentPromise: Promise | null = null; _finishedPromise: Promise; private _finishedPromiseCallback: any; private _status: number; @@ -296,7 +298,7 @@ export class Response { return this._finishedPromise; } - body(): Promise { + body(): Promise { 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 { diff --git a/src/page.ts b/src/page.ts index b5c88dc898..27851f0ec2 100644 --- a/src/page.ts +++ b/src/page.ts @@ -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; // Only called if canScreenshotOutsideViewport() returns false. setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; - takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise; + takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise; isElementHandle(remoteObject: any): boolean; adoptElementHandle(handle: dom.ElementHandle, to: dom.FrameExecutionContext): Promise>; @@ -69,7 +69,7 @@ export interface PageDelegate { scrollRectIntoViewIfNeeded(handle: dom.ElementHandle, rect?: types.Rect): Promise; getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>; - pdf?: (options?: types.PDFOptions) => Promise; + pdf?: (options?: types.PDFOptions) => Promise; 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(); - readonly pdf: ((options?: types.PDFOptions) => Promise) | undefined; + readonly pdf: ((options?: types.PDFOptions) => Promise) | 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 { + async screenshot(options?: types.ScreenshotOptions): Promise { 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; private _executionContextCallback: (value?: js.ExecutionContext) => void; diff --git a/src/platform.ts b/src/platform.ts deleted file mode 100644 index 8697bc102c..0000000000 --- a/src/platform.ts +++ /dev/null @@ -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>(); - - 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 { - assertFileAccess(); - return await promisify(nodeFS.readFile)(file, encoding); -} - -export async function readFileBuffer(file: string): Promise { - 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 { - assertFileAccess(); - return await promisify(nodeFS.open)(file, flags); -} - -export async function writeFdAsync(fd: number, buffer: Buffer): Promise { - assertFileAccess(); - return await promisify(nodeFS.write)(fd, buffer); -} - -export async function closeFdAsync(fd: number): Promise { - 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 { - let resolve: (url: string) => void; - let reject: (e: Error) => void = () => {}; - const promise = new Promise((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 { - 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(url: string, onopen: (transport: ConnectionTransport) => Promise | T): Promise { - const transport = new WebSocketTransport(url); - return new Promise((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' -}; diff --git a/src/screenshotter.ts b/src/screenshotter.ts index ed8a3bd1ea..8e3738688a 100644 --- a/src/screenshotter.ts +++ b/src/screenshotter.ts @@ -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 { + async screenshotPage(options: types.ScreenshotOptions = {}): Promise { 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 { + async screenshotElement(handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise { 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 { + 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 { 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') diff --git a/src/server/browserFetcher.ts b/src/server/browserFetcher.ts index 9a66727751..5595ff552e 100644 --- a/src/server/browserFetcher.ts +++ b/src/server/browserFetcher.ts @@ -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 => new Promise(resolve => fs.stat(path, err => resolve(!err))); const DEFAULT_DOWNLOAD_HOSTS = { diff --git a/src/server/browserServer.ts b/src/server/browserServer.ts index 95501644e9..1de4064693 100644 --- a/src/server/browserServer.ts +++ b/src/server/browserServer.ts @@ -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; private _webSocketWrapper: WebSocketWrapper | null; diff --git a/src/server/chromium.ts b/src/server/chromium.ts index 7cf456a898..8d763fa17d 100644 --- a/src/server/chromium.ts +++ b/src/server/chromium.ts @@ -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 { @@ -85,14 +85,14 @@ export class Chromium implements BrowserType { 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 { } async connect(options: ConnectOptions): Promise { - 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 { 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(); const sessionToSocket = new Map(); @@ -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-'); diff --git a/src/server/firefox.ts b/src/server/firefox.ts index 39dc323d6e..780a535c63 100644 --- a/src/server/firefox.ts +++ b/src/server/firefox.ts @@ -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 { private _executablePath: (string|undefined); @@ -50,7 +50,7 @@ export class Firefox implements BrowserType { async launch(options: LaunchOptions = {}): Promise { 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 { 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 { } 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 { // 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 { 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 { - 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 { 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(); const pendingBrowserContextDeletions = new Map(); diff --git a/src/server/pipeTransport.ts b/src/server/pipeTransport.ts index 28f68949c1..8b988f87df 100644 --- a/src/server/pipeTransport.ts +++ b/src/server/pipeTransport.ts @@ -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; diff --git a/src/server/processLauncher.ts b/src/server/processLauncher.ts index d074babc83..b86c8064b3 100644 --- a/src/server/processLauncher.ts +++ b/src/server/processLauncher.ts @@ -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 { 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'; diff --git a/src/server/webkit.ts b/src/server/webkit.ts index 6ff39996c9..7dfa6b5955 100644 --- a/src/server/webkit.ts +++ b/src/server/webkit.ts @@ -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 { 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 { 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 { } async connect(options: ConnectOptions): Promise { - 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 { } } -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(); const pendingBrowserContextDeletions = new Map(); diff --git a/src/transport.ts b/src/transport.ts index 90fff6349a..76bc918116 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -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(url: string, onopen: (transport: ConnectionTransport) => Promise | T): Promise { + const transport = new WebSocketTransport(url); + return new Promise((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 { static _lastSequenceNumber = 1; private _values = new Map(); diff --git a/src/web.ts b/src/web.ts deleted file mode 100644 index 552c98d3c2..0000000000 --- a/src/web.ts +++ /dev/null @@ -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; diff --git a/src/web.webpack.config.js b/src/web.webpack.config.js deleted file mode 100644 index 32e6fb8fd7..0000000000 --- a/src/web.webpack.config.js +++ /dev/null @@ -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', - } -}; diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 1a87760af1..515d7a39d9 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -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; } } diff --git a/src/webkit/wkConnection.ts b/src/webkit/wkConnection.ts index a4c78cd37e..03fee668ec 100644 --- a/src/webkit/wkConnection.ts +++ b/src/webkit/wkConnection.ts @@ -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((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); diff --git a/src/webkit/wkInterceptableRequest.ts b/src/webkit/wkInterceptableRequest.ts index 33acc49d0d..202b5d8e45 100644 --- a/src/webkit/wkInterceptableRequest.ts +++ b/src/webkit/wkInterceptableRequest.ts @@ -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); } diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 6fe3065083..d5b1a938e6 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -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 { + async takeScreenshot(format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined): Promise { // 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; } diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 118c389a95..47c44e708f 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -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'); - }); }; diff --git a/test/web.spec.js b/test/web.spec.js deleted file mode 100644 index 7b35046dda..0000000000 --- a/test/web.spec.js +++ /dev/null @@ -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'); - }); - }); -}; diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index cfed9e8c11..679f355d6b 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -23,9 +23,8 @@ module.exports = { checkSources, expandPrefix }; /** * @param {!Array} sources - * @param {!Array} 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); } diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js index 0b3be5a9d4..364da83bd6 100644 --- a/utils/doclint/check_public_api/index.js +++ b/utils/doclint/check_public_api/index.js @@ -33,9 +33,9 @@ const EXCLUDE_PROPERTIES = new Set([ * @param {!Array} mdSources * @return {!Promise>} */ -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; diff --git a/utils/doclint/cli.js b/utils/doclint/cli.js index ae580c9a41..5d4a16d7a6 100755 --- a/utils/doclint/cli.js +++ b/utils/doclint/cli.js @@ -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) { diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index f1e5b94cf0..07e28d9880 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -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(); diff --git a/utils/runWebpack.js b/utils/runWebpack.js index f50dbbdb35..1ecfbe6873 100644 --- a/utils/runWebpack.js +++ b/utils/runWebpack.js @@ -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) {