diff --git a/src/browser.ts b/src/browser.ts index b3aa547db3..0291080b5e 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -16,7 +16,7 @@ import { BrowserContext, BrowserContextOptions } from './browserContext'; import { ChildProcess } from 'child_process'; -import { EventEmitter } from 'events'; +import { EventEmitter } from './platform'; export class Browser extends EventEmitter { newContext(options?: BrowserContextOptions): Promise { throw new Error('Not implemented'); } diff --git a/src/browserFetcher.ts b/src/browserFetcher.ts index c61be4e7eb..540f550bc5 100644 --- a/src/browserFetcher.ts +++ b/src/browserFetcher.ts @@ -19,16 +19,17 @@ import * as extract from 'extract-zip'; import * as fs from 'fs'; import * as ProxyAgent from 'https-proxy-agent'; import * as path from 'path'; +import * as platform from './platform'; // @ts-ignore import { getProxyForUrl } from 'proxy-from-env'; import * as removeRecursive from 'rimraf'; import * as URL from 'url'; -import { assert, helper } from './helper'; +import { assert } from './helper'; -const readdirAsync = helper.promisify(fs.readdir.bind(fs)); -const mkdirAsync = helper.promisify(fs.mkdir.bind(fs)); -const unlinkAsync = helper.promisify(fs.unlink.bind(fs)); -const chmodAsync = helper.promisify(fs.chmod.bind(fs)); +const readdirAsync = platform.promisify(fs.readdir.bind(fs)); +const mkdirAsync = platform.promisify(fs.mkdir.bind(fs)); +const unlinkAsync = platform.promisify(fs.unlink.bind(fs)); +const chmodAsync = platform.promisify(fs.chmod.bind(fs)); function existsAsync(filePath) { let fulfill = null; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 1feb4b78c7..30dac55ca4 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -27,6 +27,7 @@ import { CRPage } from './crPage'; import * as browser from '../browser'; import * as network from '../network'; import * as types from '../types'; +import * as platform from '../platform'; import { CRWorker } from './features/crWorkers'; import { ConnectionTransport } from '../transport'; import { readProtocolStream } from './crProtocolHelper'; @@ -268,10 +269,10 @@ export class CRBrowser extends browser.Browser { }); } - async stopTracing(): Promise { + async stopTracing(): Promise { assert(this._tracingClient, 'Tracing was not started.'); - let fulfill: (buffer: Buffer) => void; - const contentPromise = new Promise(x => fulfill = x); + let fulfill: (buffer: platform.BufferType) => void; + const contentPromise = new Promise(x => fulfill = x); this._tracingClient.once('Tracing.tracingComplete', event => { readProtocolStream(this._tracingClient, event.stream, this._tracingPath).then(fulfill); }); diff --git a/src/chromium/crConnection.ts b/src/chromium/crConnection.ts index 26783d5f33..02bd3ba489 100644 --- a/src/chromium/crConnection.ts +++ b/src/chromium/crConnection.ts @@ -15,19 +15,18 @@ * limitations under the License. */ -import * as debug from 'debug'; -import { EventEmitter } from 'events'; +import * as platform from '../platform'; import { ConnectionTransport } from '../transport'; import { assert } from '../helper'; import { Protocol } from './protocol'; -const debugProtocol = debug('playwright:protocol'); +const debugProtocol = platform.debug('playwright:protocol'); export const ConnectionEvents = { Disconnected: Symbol('ConnectionEvents.Disconnected') }; -export class CRConnection extends EventEmitter { +export class CRConnection extends platform.EventEmitter { private _lastId = 0; private _transport: ConnectionTransport; private _sessions = new Map(); @@ -113,7 +112,7 @@ export const CRSessionEvents = { Disconnected: Symbol('Events.CDPSession.Disconnected') }; -export class CRSession extends EventEmitter { +export class CRSession extends platform.EventEmitter { _connection: CRConnection; private _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); private _targetType: string; diff --git a/src/chromium/crLauncher.ts b/src/chromium/crLauncher.ts index 2822770438..fde11a6aec 100644 --- a/src/chromium/crLauncher.ts +++ b/src/chromium/crLauncher.ts @@ -19,15 +19,16 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; +import * as platform from '../platform'; import { BrowserFetcher, BrowserFetcherOptions } from '../browserFetcher'; import { TimeoutError } from '../errors'; -import { assert, helper } from '../helper'; +import { assert } from '../helper'; import { launchProcess, waitForLine } from '../processLauncher'; import { ConnectionTransport, PipeTransport, SlowMoTransport, WebSocketTransport } from '../transport'; import { CRBrowser } from './crBrowser'; import { BrowserServer } from '../browser'; -const mkdtempAsync = helper.promisify(fs.mkdtemp); +const mkdtempAsync = platform.promisify(fs.mkdtemp); const CHROME_PROFILE_PATH = path.join(os.tmpdir(), 'playwright_dev_profile-'); diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index 94a4a2bcdf..88aab3b98b 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -21,6 +21,7 @@ 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'; export class CRNetworkManager { @@ -188,7 +189,7 @@ export class CRNetworkManager { const remoteAddress: network.RemoteAddress = { ip: responsePayload.remoteIPAddress, port: responsePayload.remotePort }; const getResponseBody = async () => { const response = await this._client.send('Network.getResponseBody', { requestId: request._requestId }); - return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); + return platform.Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); }; return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), remoteAddress, getResponseBody); } @@ -261,7 +262,7 @@ class InterceptableRequest implements network.RequestDelegate { event.request.url, event.type.toLowerCase(), event.request.method, event.request.postData, headersObject(event.request.headers)); } - async continue(overrides: { headers?: {[key: string]: string}; } = {}) { + async continue(overrides: { headers?: network.Headers; } = {}) { await this._client.send('Fetch.continueRequest', { requestId: this._interceptionId, headers: overrides.headers ? headersArray(overrides.headers) : undefined, @@ -272,8 +273,8 @@ class InterceptableRequest implements network.RequestDelegate { }); } - async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { - const responseBody = response.body && helper.isString(response.body) ? Buffer.from(/** @type {string} */(response.body)) : /** @type {?Buffer} */(response.body || null); + async fulfill(response: { status: number; headers: network.Headers; contentType: string; body: (string | platform.BufferType); }) { + const responseBody = response.body && helper.isString(response.body) ? platform.Buffer.from(response.body) : (response.body || null); const responseHeaders: { [s: string]: string; } = {}; if (response.headers) { @@ -283,7 +284,7 @@ class InterceptableRequest implements network.RequestDelegate { if (response.contentType) responseHeaders['content-type'] = response.contentType; if (responseBody && !('content-length' in responseHeaders)) - responseHeaders['content-length'] = String(Buffer.byteLength(responseBody)); + responseHeaders['content-length'] = String(platform.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 ad7bf1442d..1f75a5c7da 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -38,6 +38,7 @@ import { BrowserContext } from '../browserContext'; import * as types from '../types'; import { ConsoleMessage } from '../console'; import * as accessibility from '../accessibility'; +import * as platform from '../platform'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -375,11 +376,11 @@ export class CRPage implements PageDelegate { await this._client.send('Emulation.setDefaultBackgroundColorOverride', { color }); } - async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { + async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { await this._client.send('Page.bringToFront', {}); const clip = options.clip ? { ...options.clip, scale: 1 } : undefined; const result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip }); - return Buffer.from(result.data, 'base64'); + return platform.Buffer.from(result.data, 'base64'); } async resetViewport(): Promise { @@ -494,7 +495,7 @@ export class ChromiumPage extends Page { this._networkManager = new CRNetworkManager(client, this); } - async pdf(options?: PDFOptions): Promise { + async pdf(options?: PDFOptions): Promise { return this._pdf.generate(options); } diff --git a/src/chromium/crProtocolHelper.ts b/src/chromium/crProtocolHelper.ts index a5fcc9b137..c311663fb5 100644 --- a/src/chromium/crProtocolHelper.ts +++ b/src/chromium/crProtocolHelper.ts @@ -14,15 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as fs from 'fs'; -import {helper, assert, debugError} from '../helper'; + +import { assert, debugError } from '../helper'; import { CRSession } from './crConnection'; import { Protocol } from './protocol'; - -const openAsync = helper.promisify(fs.open); -const writeAsync = helper.promisify(fs.write); -const closeAsync = helper.promisify(fs.close); - +import * as platform from '../platform'; export function getExceptionMessage(exceptionDetails: Protocol.Runtime.ExceptionDetails): string { if (exceptionDetails.exception) @@ -69,26 +65,26 @@ export async function releaseObject(client: CRSession, remoteObject: Protocol.Ru }); } -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 file; + let fd; if (path) - file = await openAsync(path, 'w'); + fd = await platform.openFdAsync(path, 'w'); const bufs = []; while (!eof) { const response = await client.send('IO.read', {handle}); eof = response.eof; - const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); + const buf = platform.Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); bufs.push(buf); if (path) - await writeAsync(file, buf); + await platform.writeFdAsync(fd, buf); } if (path) - await closeAsync(file); + await platform.closeFdAsync(fd); await client.send('IO.close', {handle}); let resultBuffer = null; try { - resultBuffer = Buffer.concat(bufs); + resultBuffer = platform.Buffer.concat(bufs); } finally { return resultBuffer; } diff --git a/src/chromium/features/crPdf.ts b/src/chromium/features/crPdf.ts index 1bde2f69d9..008b57b297 100644 --- a/src/chromium/features/crPdf.ts +++ b/src/chromium/features/crPdf.ts @@ -18,6 +18,7 @@ import { assert, helper } from '../../helper'; import { CRSession } from '../crConnection'; import { readProtocolStream } from '../crProtocolHelper'; +import * as platform from '../../platform'; export type PDFOptions = { scale?: number, @@ -91,7 +92,7 @@ export class CRPDF { this._client = client; } - async generate(options: PDFOptions = {}): Promise { + async generate(options: PDFOptions = {}): Promise { const { scale = 1, displayHeaderFooter = false, diff --git a/src/chromium/features/crWorkers.ts b/src/chromium/features/crWorkers.ts index b761af04fd..66ad311bae 100644 --- a/src/chromium/features/crWorkers.ts +++ b/src/chromium/features/crWorkers.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; +import { EventEmitter } from '../../platform'; import { CRSession, CRConnection } from '../crConnection'; import { debugError } from '../../helper'; import { Protocol } from '../protocol'; diff --git a/src/dom.ts b/src/dom.ts index 188e5d6928..dd6ceabb1a 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -23,10 +23,7 @@ import * as zsSelectorEngineSource from './generated/zsSelectorEngineSource'; import { assert, helper, debugError } from './helper'; import Injected from './injected/injected'; import { Page } from './page'; -import * as path from 'path'; -import * as fs from 'fs'; - -const readFileAsync = helper.promisify(fs.readFile); +import * as platform from './platform'; export class FrameExecutionContext extends js.ExecutionContext { readonly frame: frames.Frame; @@ -409,9 +406,9 @@ export class ElementHandle extends js.JSHandle { const filePayloads = await Promise.all(files.map(async item => { if (typeof item === 'string') { const file: types.FilePayload = { - name: path.basename(item), + name: platform.basename(item), type: 'application/octet-stream', - data: (await readFileAsync(item)).toString('base64') + data: await platform.readFileAsync(item, 'base64') }; return file; } @@ -445,7 +442,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/firefox/ffConnection.ts b/src/firefox/ffConnection.ts index 35723080c0..354839fd30 100644 --- a/src/firefox/ffConnection.ts +++ b/src/firefox/ffConnection.ts @@ -16,17 +16,17 @@ */ import {assert} from '../helper'; -import {EventEmitter} from 'events'; -import * as debug from 'debug'; +import * as platform from '../platform'; import { ConnectionTransport } from '../transport'; import { Protocol } from './protocol'; -const debugProtocol = debug('playwright:protocol'); + +const debugProtocol = platform.debug('playwright:protocol'); export const ConnectionEvents = { Disconnected: Symbol('Disconnected'), }; -export class FFConnection extends EventEmitter { +export class FFConnection extends platform.EventEmitter { private _lastId: number; private _callbacks: Map; private _transport: ConnectionTransport; @@ -131,7 +131,7 @@ export const FFSessionEvents = { Disconnected: Symbol('Disconnected') }; -export class FFSession extends EventEmitter { +export class FFSession extends platform.EventEmitter { _connection: FFConnection; private _callbacks: Map; private _targetType: string; diff --git a/src/firefox/ffNetworkManager.ts b/src/firefox/ffNetworkManager.ts index 4529f2df19..c0801ab01f 100644 --- a/src/firefox/ffNetworkManager.ts +++ b/src/firefox/ffNetworkManager.ts @@ -20,6 +20,7 @@ import { FFSession } from './ffConnection'; import { Page } from '../page'; import * as network from '../network'; import * as frames from '../frames'; +import * as platform from '../platform'; export class FFNetworkManager { private _session: FFSession; @@ -76,7 +77,7 @@ export class FFNetworkManager { }); if (response.evicted) throw new Error(`Response body for ${request.request.method()} ${request.request.url()} was evicted!`); - return Buffer.from(response.base64body, 'base64'); + return platform.Buffer.from(response.base64body, 'base64'); }; const headers: network.Headers = {}; for (const {name, value} of event.headers) @@ -168,7 +169,7 @@ class InterceptableRequest implements network.RequestDelegate { }); } - async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { + async fulfill(response: { status: number; headers: network.Headers; contentType: string; body: (string | platform.BufferType); }) { throw new Error('Fulfill is not supported in Firefox'); } diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index d4e56d76a9..f009db4353 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -31,6 +31,7 @@ import { getAccessibilityTree } from './ffAccessibility'; import * as network from '../network'; import * as types from '../types'; import * as accessibility from '../accessibility'; +import * as platform from '../platform'; export class FFPage implements PageDelegate { readonly rawMouse: RawMouseImpl; @@ -272,13 +273,13 @@ export class FFPage implements PageDelegate { throw new Error('Not implemented'); } - async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { + async takeScreenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions): Promise { const { data } = await this._session.send('Page.screenshot', { mimeType: ('image/' + format) as ('image/png' | 'image/jpeg'), fullPage: options.fullPage, clip: options.clip, }); - return Buffer.from(data, 'base64'); + return platform.Buffer.from(data, 'base64'); } async resetViewport(): Promise { diff --git a/src/frames.ts b/src/frames.ts index 1d0f476c37..8483166ac9 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -16,7 +16,6 @@ */ import * as types from './types'; -import * as fs from 'fs'; import * as js from './javascript'; import * as dom from './dom'; import * as network from './network'; @@ -26,8 +25,7 @@ import { TimeoutError } from './errors'; import { Events } from './events'; import { Page } from './page'; import { ConsoleMessage } from './console'; - -const readFileAsync = helper.promisify(fs.readFile); +import * as platform from './platform'; type ContextType = 'main' | 'utility'; type ContextData = { @@ -507,7 +505,7 @@ export class Frame { if (url !== null) return (await context.evaluateHandle(addScriptUrl, url, type)).asElement(); if (path !== null) { - let contents = await readFileAsync(path, 'utf8'); + let contents = await platform.readFileAsync(path, 'utf8'); contents += '//# sourceURL=' + path.replace(/\n/g, ''); return (await context.evaluateHandle(addScriptContent, contents, type)).asElement(); } @@ -557,7 +555,7 @@ export class Frame { return (await context.evaluateHandle(addStyleUrl, url)).asElement(); if (path !== null) { - let contents = await readFileAsync(path, 'utf8'); + let contents = await platform.readFileAsync(path, 'utf8'); contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/'; return (await context.evaluateHandle(addStyleContent, contents)).asElement(); } @@ -909,7 +907,7 @@ class LifecycleWatcher { } _urlMatches(urlString: string): boolean { - return !this._urlMatch || helper.urlMatches(urlString, this._urlMatch); + return !this._urlMatch || platform.urlMatches(urlString, this._urlMatch); } setExpectedDocumentId(documentId: string, url: string) { diff --git a/src/helper.ts b/src/helper.ts index 34bf507255..89b5d0044a 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -15,15 +15,13 @@ * limitations under the License. */ -import * as debug from 'debug'; -import * as types from './types'; -import * as kurl from 'url'; import { TimeoutError } from './errors'; +import * as platform from './platform'; -export const debugError = debug(`playwright:error`); +export const debugError = platform.debug(`playwright:error`); export type RegisteredListener = { - emitter: NodeJS.EventEmitter; + emitter: platform.EventEmitterType; eventName: (string | symbol); handler: (...args: any[]) => void; }; @@ -63,7 +61,7 @@ class Helper { } static addEventListener( - emitter: NodeJS.EventEmitter, + emitter: platform.EventEmitterType, eventName: (string | symbol), handler: (...args: any[]) => void): RegisteredListener { emitter.on(eventName, handler); @@ -71,7 +69,7 @@ class Helper { } static removeEventListeners(listeners: Array<{ - emitter: NodeJS.EventEmitter; + emitter: platform.EventEmitterType; eventName: (string | symbol); handler: (...args: any[]) => void; }>) { @@ -88,24 +86,8 @@ class Helper { return typeof obj === 'number' || obj instanceof Number; } - static promisify(nodeFunction: Function): Function { - function promisified(...args) { - return new Promise((resolve, reject) => { - function callback(err, ...result) { - if (err) - return reject(err); - if (result.length === 1) - return resolve(result[0]); - return resolve(result); - } - nodeFunction.call(null, ...args, callback); - }); - } - return promisified; - } - static async waitForEvent( - emitter: NodeJS.EventEmitter, + emitter: platform.EventEmitterType, eventName: (string | symbol), predicate: Function, timeout: number, @@ -159,22 +141,6 @@ class Helper { clearTimeout(timeoutTimer); } } - - static urlMatches(urlString: string, match: types.URLMatch | undefined): boolean { - if (match === undefined) - return true; - if (typeof match === 'string') - return match === urlString; - if (match instanceof RegExp) - return match.test(urlString); - assert(typeof match === 'function', 'url parameter should be string, RegExp or function'); - - try { - return match(new kurl.URL(urlString)); - } catch (e) { - } - return false; - } } export function assert(value: any, message?: string) { diff --git a/src/network.ts b/src/network.ts index c1416eb14c..650fba8e2c 100644 --- a/src/network.ts +++ b/src/network.ts @@ -16,6 +16,7 @@ import * as frames from './frames'; import { assert } from './helper'; +import * as platform from './platform'; export type NetworkCookie = { name: string, @@ -202,7 +203,7 @@ export class Request { await this._delegate.abort(errorCode); } - async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { // Mocking responses for dataURL requests is not currently supported. + async fulfill(response: { status: number; headers: Headers; contentType: string; body: (string | platform.BufferType); }) { // Mocking responses for dataURL requests is not currently supported. if (this.url().startsWith('data:')) return; assert(this._delegate, 'Request Interception is not enabled!'); @@ -226,11 +227,11 @@ export type RemoteAddress = { port: number, }; -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 _remoteAddress: RemoteAddress; @@ -282,7 +283,7 @@ export class Response { return this._headers; } - buffer(): Promise { + buffer(): Promise { if (!this._contentPromise) { this._contentPromise = this._finishedPromise.then(async error => { if (error) @@ -314,8 +315,8 @@ export class Response { export interface RequestDelegate { abort(errorCode: string): Promise; - fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }): Promise; - continue(overrides: { url?: string; method?: string; postData?: string; headers?: { [key: string]: string; }; }): Promise; + fulfill(response: { status: number; headers: Headers; contentType: string; body: (string | platform.BufferType); }): Promise; + continue(overrides: { url?: string; method?: string; postData?: string; headers?: Headers; }): Promise; } // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. diff --git a/src/page.ts b/src/page.ts index bacad7ac9d..5ba4184090 100644 --- a/src/page.ts +++ b/src/page.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; import * as dom from './dom'; import * as frames from './frames'; import { assert, debugError, helper } from './helper'; @@ -30,6 +29,7 @@ import { BrowserContext } from './browserContext'; import { ConsoleMessage, ConsoleMessageLocation } from './console'; import Injected from './injected/injected'; import * as accessibility from './accessibility'; +import * as platform from './platform'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -56,7 +56,7 @@ export interface PageDelegate { getBoundingBoxForScreenshot(handle: dom.ElementHandle): Promise; canScreenshotOutsideViewport(): boolean; setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise; - takeScreenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise; + takeScreenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise; resetViewport(oldSize: types.Size): Promise; isElementHandle(remoteObject: any): boolean; @@ -87,7 +87,7 @@ export type FileChooser = { multiple: boolean }; -export class Page extends EventEmitter { +export class Page extends platform.EventEmitter { private _closed = false; private _closedCallback: () => void; private _closedPromise: Promise; @@ -336,7 +336,7 @@ export class Page extends EventEmitter { const { timeout = this._timeoutSettings.timeout() } = options; return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => { if (helper.isString(urlOrPredicate) || urlOrPredicate instanceof RegExp) - return helper.urlMatches(request.url(), urlOrPredicate); + return platform.urlMatches(request.url(), urlOrPredicate); return urlOrPredicate(request); }, timeout, this._disconnectedPromise); } @@ -345,7 +345,7 @@ export class Page extends EventEmitter { const { timeout = this._timeoutSettings.timeout() } = options; return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => { if (helper.isString(urlOrPredicate) || urlOrPredicate instanceof RegExp) - return helper.urlMatches(response.url(), urlOrPredicate); + return platform.urlMatches(response.url(), urlOrPredicate); return urlOrPredicate(response); }, timeout, this._disconnectedPromise); } @@ -430,7 +430,7 @@ export class Page extends EventEmitter { await this._delegate.authenticate(credentials); } - async screenshot(options?: types.ScreenshotOptions): Promise { + async screenshot(options?: types.ScreenshotOptions): Promise { return this._screenshotter.screenshotPage(options); } diff --git a/src/platform.ts b/src/platform.ts new file mode 100644 index 0000000000..401652dd86 --- /dev/null +++ b/src/platform.ts @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +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 mime from 'mime'; +import * as jpeg from 'jpeg-js'; +import * as png from 'pngjs'; + +import { assert, helper } from './helper'; +import * as types from './types'; + +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) { + return new Promise((resolve, reject) => { + function callback(err, ...result) { + if (err) + return reject(err); + if (result.length === 1) + return resolve(result[0]); + return resolve(result); + } + nodeFunction.call(null, ...args, callback); + }); + } + return promisified; +} + +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>(); + + addListener(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; + } + + on(event: string | symbol, listener: Listener): this { + return this.addListener(event, listener); + } + + once(event: string | symbol, listener: Listener): this { + const wrapped = (...args: any[]) => { + this.removeListener(event, wrapped); + listener(...args); + }; + return this.on(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; + +type DebugType = typeof nodeDebug; +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 = NodeBuffer; + +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 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 { + assertFileAccess(); + return mime.getType(file); +} + +export function urlMatches(urlString: string, match: types.URLMatch | undefined): boolean { + if (match === undefined) + return true; + if (typeof match === 'string') + return match === urlString; + if (match instanceof RegExp) + return match.test(urlString); + assert(typeof match === 'function', 'url parameter should be string, RegExp or function'); + + try { + return match(new URL(urlString)); + } catch (e) { + } + return false; +} + +export function pngToJpeg(buffer: Buffer): Buffer { + assert(isNode, 'Converting from png to jpeg is only supported in Node.js'); + return jpeg.encode(png.PNG.sync.read(buffer)).data; +} diff --git a/src/processLauncher.ts b/src/processLauncher.ts index 8e18c06a19..073a72c4f3 100644 --- a/src/processLauncher.ts +++ b/src/processLauncher.ts @@ -21,8 +21,9 @@ import * as removeFolder from 'rimraf'; import { helper } from './helper'; import * as readline from 'readline'; import { TimeoutError } from './errors'; +import * as platform from './platform'; -const removeFolderAsync = helper.promisify(removeFolder); +const removeFolderAsync = platform.promisify(removeFolder); export type LaunchProcessOptions = { executablePath: string, diff --git a/src/screenshotter.ts b/src/screenshotter.ts index 51472ba133..0fbdea1bf1 100644 --- a/src/screenshotter.ts +++ b/src/screenshotter.ts @@ -15,14 +15,11 @@ * limitations under the License. */ -import * as fs from 'fs'; -import * as mime from 'mime'; import * as dom from './dom'; -import { assert, helper } from './helper'; +import { assert } from './helper'; import * as types from './types'; import { Page } from './page'; - -const writeFileAsync = helper.promisify(fs.writeFile); +import * as platform from './platform'; export class Screenshotter { private _queue = new TaskQueue(); @@ -39,7 +36,7 @@ export class Screenshotter { } } - async screenshotPage(options: types.ScreenshotOptions = {}): Promise { + async screenshotPage(options: types.ScreenshotOptions = {}): Promise { const format = validateScreeshotOptions(options); return this._queue.postTask(async () => { let overridenViewport: types.Viewport | undefined; @@ -82,7 +79,7 @@ export class Screenshotter { }); } - async screenshotElement(handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise { + async screenshotElement(handle: dom.ElementHandle, options: types.ElementScreenshotOptions = {}): Promise { const format = validateScreeshotOptions(options); const rewrittenOptions: types.ScreenshotOptions = { ...options }; return this._queue.postTask(async () => { @@ -121,7 +118,7 @@ export class Screenshotter { }); } - private async _screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewport: types.Viewport): Promise { + private async _screenshot(format: 'png' | 'jpeg', options: types.ScreenshotOptions, viewport: types.Viewport): Promise { const shouldSetDefaultBackground = options.omitBackground && format === 'png'; if (shouldSetDefaultBackground) await this._page._delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0}); @@ -129,7 +126,7 @@ export class Screenshotter { if (shouldSetDefaultBackground) await this._page._delegate.setBackgroundColor(); if (options.path) - await writeFileAsync(options.path, buffer); + await platform.writeFileAsync(options.path, buffer); return buffer; } } @@ -168,7 +165,7 @@ function validateScreeshotOptions(options: types.ScreenshotOptions): 'png' | 'jp assert(options.type === 'png' || options.type === 'jpeg', 'Unknown options.type value: ' + options.type); format = options.type; } else if (options.path) { - const mimeType = mime.getType(options.path); + const mimeType = platform.getMimeType(options.path); if (mimeType === 'image/png') format = 'png'; else if (mimeType === 'image/jpeg') diff --git a/src/types.ts b/src/types.ts index 1c6751edd5..49fd3a4436 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,6 @@ import * as js from './javascript'; import * as dom from './dom'; -import * as kurl from 'url'; type Boxed = { [Index in keyof Args]: Args[Index] | js.JSHandle }; type PageFunction = string | ((...args: Args) => R | Promise); @@ -63,7 +62,7 @@ export type Viewport = { hasTouch?: boolean; }; -export type URLMatch = string | RegExp | ((url: kurl.URL) => boolean); +export type URLMatch = string | RegExp | ((url: URL) => boolean); export type Credentials = { username: string; diff --git a/src/webkit/wkConnection.ts b/src/webkit/wkConnection.ts index 9489e678e8..35a6d3b020 100644 --- a/src/webkit/wkConnection.ts +++ b/src/webkit/wkConnection.ts @@ -16,13 +16,12 @@ */ import { assert } from '../helper'; -import * as debug from 'debug'; -import { EventEmitter } from 'events'; +import * as platform from '../platform'; import { ConnectionTransport } from '../transport'; import { Protocol } from './protocol'; -const debugProtocol = debug('playwright:protocol'); -const debugWrappedMessage = require('debug')('wrapped'); +const debugProtocol = platform.debug('playwright:protocol'); +const debugWrappedMessage = platform.debug('wrapped'); export const WKConnectionEvents = { PageProxyCreated: Symbol('ConnectionEvents.PageProxyCreated'), @@ -35,7 +34,7 @@ export const WKPageProxySessionEvents = { DidCommitProvisionalTarget: Symbol('PageProxyEvents.DidCommitProvisionalTarget'), }; -export class WKConnection extends EventEmitter { +export class WKConnection extends platform.EventEmitter { private _lastId = 0; private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); private readonly _transport: ConnectionTransport; @@ -137,7 +136,7 @@ export const WKTargetSessionEvents = { Disconnected: Symbol('TargetSessionEvents.Disconnected') }; -export class WKPageProxySession extends EventEmitter { +export class WKPageProxySession extends platform.EventEmitter { _connection: WKConnection; private readonly _sessions = new Map(); private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); @@ -215,7 +214,7 @@ export class WKPageProxySession extends EventEmitter { } } -export class WKTargetSession extends EventEmitter { +export class WKTargetSession extends platform.EventEmitter { _pageProxySession: WKPageProxySession; private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); private readonly _targetType: string; diff --git a/src/webkit/wkNetworkManager.ts b/src/webkit/wkNetworkManager.ts index 853a87c800..6cf7edb9e3 100644 --- a/src/webkit/wkNetworkManager.ts +++ b/src/webkit/wkNetworkManager.ts @@ -22,6 +22,7 @@ import { Protocol } from './protocol'; import * as network from '../network'; import * as frames from '../frames'; import * as types from '../types'; +import * as platform from '../platform'; export class WKNetworkManager { private _session: WKTargetSession; @@ -106,7 +107,7 @@ export class WKNetworkManager { const remoteAddress: network.RemoteAddress = { ip: '', port: 0 }; const getResponseBody = async () => { const response = await this._session.send('Network.getResponseBody', { requestId: request._requestId }); - return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); + return platform.Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8'); }; return new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObject(responsePayload.headers), remoteAddress, getResponseBody); } @@ -208,7 +209,7 @@ class InterceptableRequest implements network.RequestDelegate { await this._session.send('Network.interceptAsError', { requestId: this._requestId, reason }); } - async fulfill(response: { status: number; headers: {[key: string]: string}; contentType: string; body: (string | Buffer); }) { + async fulfill(response: { status: number; headers: network.Headers; contentType: string; body: (string | platform.BufferType); }) { await this._interceptedPromise; const base64Encoded = !!response.body && !helper.isString(response.body); @@ -222,7 +223,7 @@ class InterceptableRequest implements network.RequestDelegate { if (response.contentType) responseHeaders['content-type'] = response.contentType; if (responseBody && !('content-length' in responseHeaders)) - responseHeaders['content-length'] = String(Buffer.byteLength(responseBody)); + responseHeaders['content-length'] = String(platform.Buffer.byteLength(responseBody)); await this._session.send('Network.interceptWithResponse', { requestId: this._requestId, diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 1b8a865d09..2f174e875d 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -30,9 +30,8 @@ import { WKBrowser } from './wkBrowser'; import { BrowserContext } from '../browserContext'; import { RawMouseImpl, RawKeyboardImpl } from './wkInput'; import * as types from '../types'; -import * as jpeg from 'jpeg-js'; -import { PNG } from 'pngjs'; import * as accessibility from '../accessibility'; +import * as platform from '../platform'; import { getAccessibilityTree } from './wkAccessibility'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -386,13 +385,13 @@ export class WKPage implements PageDelegate { this._session.send('Page.setDefaultBackgroundColorOverride', { color }); } - async takeScreenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise { + async takeScreenshot(format: string, options: types.ScreenshotOptions, viewport: types.Viewport): Promise { const rect = options.clip || { x: 0, y: 0, width: viewport.width, height: viewport.height }; const result = await this._session.send('Page.snapshotRect', { ...rect, coordinateSystem: options.fullPage ? 'Page' : 'Viewport' }); const prefix = 'data:image/png;base64,'; - let buffer = Buffer.from(result.dataURL.substr(prefix.length), 'base64'); + let buffer = platform.Buffer.from(result.dataURL.substr(prefix.length), 'base64'); if (format === 'jpeg') - buffer = jpeg.encode(PNG.sync.read(buffer)).data; + buffer = platform.pngToJpeg(buffer); return buffer; } diff --git a/test/firefox/launcher.spec.js b/test/firefox/launcher.spec.js index 399604abb5..599eebac34 100644 --- a/test/firefox/launcher.spec.js +++ b/test/firefox/launcher.spec.js @@ -17,9 +17,9 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); -const {helper} = require('../../lib/helper'); -const rmAsync = helper.promisify(require('rimraf')); -const mkdtempAsync = helper.promisify(fs.mkdtemp); +const utils = require('../utils'); +const rmAsync = utils.promisify(require('rimraf')); +const mkdtempAsync = utils.promisify(fs.mkdtemp); const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); module.exports.describe = function ({ testRunner, expect, defaultBrowserOptions, playwright }) { diff --git a/test/launcher.spec.js b/test/launcher.spec.js index 3f9b3235e1..82f8437cb3 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -15,12 +15,7 @@ * limitations under the License. */ const fs = require('fs'); -const os = require('os'); const path = require('path'); -const {helper} = require('../lib/helper'); -const rmAsync = helper.promisify(require('rimraf')); -const mkdtempAsync = helper.promisify(fs.mkdtemp); -const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); const utils = require('./utils'); module.exports.describe = function({testRunner, expect, defaultBrowserOptions, playwright, WEBKIT}) { diff --git a/test/utils.js b/test/utils.js index defb4f3265..22e5f26a28 100644 --- a/test/utils.js +++ b/test/utils.js @@ -56,6 +56,22 @@ function traceAPICoverage(apiCoverage, events, className, classType) { } const utils = module.exports = { + promisify: function (nodeFunction) { + function promisified(...args) { + return new Promise((resolve, reject) => { + function callback(err, ...result) { + if (err) + return reject(err); + if (result.length === 1) + return resolve(result[0]); + return resolve(result); + } + nodeFunction.call(null, ...args, callback); + }); + } + return promisified; + }, + recordAPICoverage: function(testRunner, api, events) { const coverage = new Map(); for (const [className, classType] of Object.entries(api)) @@ -96,7 +112,7 @@ const utils = module.exports = { for (const frame of page.frames()) { if (!frames.has(frame)) return frame; - } + } } return null; diff --git a/test/webkit/launcher.spec.js b/test/webkit/launcher.spec.js index b02423c52b..6e5dc0be51 100644 --- a/test/webkit/launcher.spec.js +++ b/test/webkit/launcher.spec.js @@ -14,14 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const fs = require('fs'); -const os = require('os'); -const path = require('path'); -const {helper} = require('../../lib/helper'); -const rmAsync = helper.promisify(require('rimraf')); -const mkdtempAsync = helper.promisify(fs.mkdtemp); -const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); -const utils = require('../utils'); module.exports.describe = function ({ testRunner, expect, playwright }) { const {describe, xdescribe, fdescribe} = testRunner; diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index 72536972b1..86e652f06b 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -44,12 +44,12 @@ function checkSources(sources) { }); const checker = program.getTypeChecker(); const sourceFiles = program.getSourceFiles(); + const errors = []; /** @type {!Array} */ const classes = []; /** @type {!Map} */ const inheritance = new Map(); sourceFiles.filter(x => !x.fileName.includes('node_modules')).map(x => visit(x)); - const errors = []; const documentation = new Documentation(recreateClassesWithInheritance(classes, inheritance)); return {errors, documentation}; @@ -98,6 +98,30 @@ function checkSources(sources) { excludeClasses.add(className); } } + if (!node.getSourceFile().fileName.endsWith('platform.ts')) { + // Only relative imports. + if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) { + const module = node.moduleSpecifier.text; + if (!module.startsWith('.')) { + 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 (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); }