diff --git a/src/browserContext.ts b/src/browserContext.ts index a1a66dd0bd..7d4944f1e3 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -20,7 +20,7 @@ import { helper } from './helper'; import * as network from './network'; import * as path from 'path'; import { Page, PageBinding } from './page'; -import { TimeoutSettings } from './timeoutSettings'; +import { TimeoutSettings } from './utils/timeoutSettings'; import * as frames from './frames'; import * as types from './types'; import { Download } from './download'; @@ -28,6 +28,7 @@ import { Browser } from './browser'; import { EventEmitter } from 'events'; import { Progress } from './progress'; import { DebugController } from './debug/debugController'; +import { isDebugMode } from './utils/utils'; export class Screencast { readonly path: string; @@ -68,7 +69,7 @@ export abstract class BrowserContext extends EventEmitter { } async _initialize() { - if (helper.isDebugMode()) + if (isDebugMode()) new DebugController(this); } @@ -250,24 +251,16 @@ export function verifyGeolocation(geolocation?: types.Geolocation) { return; geolocation.accuracy = geolocation.accuracy || 0; const { longitude, latitude, accuracy } = geolocation; - if (!helper.isNumber(longitude)) - throw new Error(`geolocation.longitude: expected number, got ${typeof longitude}`); if (longitude < -180 || longitude > 180) throw new Error(`geolocation.longitude: precondition -180 <= LONGITUDE <= 180 failed.`); - if (!helper.isNumber(latitude)) - throw new Error(`geolocation.latitude: expected number, got ${typeof latitude}`); if (latitude < -90 || latitude > 90) throw new Error(`geolocation.latitude: precondition -90 <= LATITUDE <= 90 failed.`); - if (!helper.isNumber(accuracy)) - throw new Error(`geolocation.accuracy: expected number, got ${typeof accuracy}`); if (accuracy < 0) throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`); } export function verifyProxySettings(proxy: types.ProxySettings): types.ProxySettings { let { server, bypass } = proxy; - if (!helper.isString(server)) - throw new Error(`Invalid proxy.server: ` + server); let url = new URL(server); if (!['http:', 'https:', 'socks5:'].includes(url.protocol)) { url = new URL('http://' + server); diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index be97bf7614..8496896b07 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -17,7 +17,7 @@ import { Browser, BrowserOptions } from '../browser'; import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; -import { assert } from '../helper'; +import { assert } from '../utils/utils'; import * as network from '../network'; import { Page, PageBinding, Worker } from '../page'; import { ConnectionTransport } from '../transport'; diff --git a/src/chromium/crConnection.ts b/src/chromium/crConnection.ts index 47acc41ee2..f0b5f1e24f 100644 --- a/src/chromium/crConnection.ts +++ b/src/chromium/crConnection.ts @@ -15,11 +15,12 @@ * limitations under the License. */ -import { assert, debugLogger } from '../helper'; +import { assert } from '../utils/utils'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { Protocol } from './protocol'; import { EventEmitter } from 'events'; import { rewriteErrorMessage } from '../utils/stackTrace'; +import { debugLogger } from '../utils/debugLogger'; export const ConnectionEvents = { Disconnected: Symbol('ConnectionEvents.Disconnected') diff --git a/src/chromium/crCoverage.ts b/src/chromium/crCoverage.ts index 41c7ed980b..ceb24a58ec 100644 --- a/src/chromium/crCoverage.ts +++ b/src/chromium/crCoverage.ts @@ -16,10 +16,11 @@ */ import { CRSession } from './crConnection'; -import { assert, helper, RegisteredListener } from '../helper'; +import { helper, RegisteredListener } from '../helper'; import { Protocol } from './protocol'; import * as types from '../types'; import * as sourceMap from '../utils/sourceMap'; +import { assert } from '../utils/utils'; export class CRCoverage { private _jsCoverage: JSCoverage; diff --git a/src/chromium/crInput.ts b/src/chromium/crInput.ts index 15e30894a2..6b6edf9fe5 100644 --- a/src/chromium/crInput.ts +++ b/src/chromium/crInput.ts @@ -19,7 +19,7 @@ import * as input from '../input'; import * as types from '../types'; import { CRSession } from './crConnection'; import { macEditingCommands } from '../macEditingCommands'; -import { helper } from '../helper'; +import { isString } from '../utils/utils'; function toModifiersMask(modifiers: Set): number { let mask = 0; @@ -51,7 +51,7 @@ export class RawKeyboardImpl implements input.RawKeyboard { parts.push(code); const shortcut = parts.join('+'); let commands = macEditingCommands[shortcut] || []; - if (helper.isString(commands)) + if (isString(commands)) commands = [commands]; // remove the trailing : to match the Chromium command names. return commands.map(c => c.substring(0, c.length - 1)); diff --git a/src/chromium/crNetworkManager.ts b/src/chromium/crNetworkManager.ts index 6bed18c836..57c3044962 100644 --- a/src/chromium/crNetworkManager.ts +++ b/src/chromium/crNetworkManager.ts @@ -17,13 +17,13 @@ import { CRSession } from './crConnection'; import { Page } from '../page'; -import { assert, helper, RegisteredListener } from '../helper'; +import { helper, RegisteredListener } from '../helper'; import { Protocol } from './protocol'; import * as network from '../network'; import * as frames from '../frames'; import * as types from '../types'; import { CRPage } from './crPage'; -import { headersObjectToArray } from '../converters'; +import { assert, headersObjectToArray } from '../utils/utils'; export class CRNetworkManager { private _client: CRSession; diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 0dbb46ffc9..c0240a48ad 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -17,7 +17,7 @@ import * as dom from '../dom'; import * as frames from '../frames'; -import { helper, RegisteredListener, assert } from '../helper'; +import { helper, RegisteredListener } from '../helper'; import * as network from '../network'; import { CRSession, CRConnection, CRSessionEvents } from './crConnection'; import { CRExecutionContext } from './crExecutionContext'; @@ -36,7 +36,7 @@ import * as types from '../types'; import { ConsoleMessage } from '../console'; import * as sourceMap from '../utils/sourceMap'; import { rewriteErrorMessage } from '../utils/stackTrace'; -import { headersArrayToObject } from '../converters'; +import { assert, headersArrayToObject } from '../utils/utils'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; diff --git a/src/chromium/crPdf.ts b/src/chromium/crPdf.ts index aa1f9adf6d..4f6d62136f 100644 --- a/src/chromium/crPdf.ts +++ b/src/chromium/crPdf.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { assert } from '../helper'; +import { assert } from '../utils/utils'; import * as types from '../types'; import { CRSession } from './crConnection'; import { readProtocolStream } from './crProtocolHelper'; diff --git a/src/chromium/crProtocolHelper.ts b/src/chromium/crProtocolHelper.ts index d433ff5108..5e8f31bf37 100644 --- a/src/chromium/crProtocolHelper.ts +++ b/src/chromium/crProtocolHelper.ts @@ -20,7 +20,7 @@ import { Protocol } from './protocol'; import * as fs from 'fs'; import * as util from 'util'; import * as types from '../types'; -import { mkdirIfNeeded } from '../helper'; +import { mkdirIfNeeded } from '../utils/utils'; export function getExceptionMessage(exceptionDetails: Protocol.Runtime.ExceptionDetails): string { if (exceptionDetails.exception) diff --git a/src/converters.ts b/src/converters.ts deleted file mode 100644 index 68d676b9c1..0000000000 --- a/src/converters.ts +++ /dev/null @@ -1,33 +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 * as types from './types'; - -export function headersObjectToArray(headers: { [key: string]: string }): types.HeadersArray { - const result: types.HeadersArray = []; - for (const name in headers) { - if (!Object.is(headers[name], undefined)) - result.push({ name, value: headers[name] }); - } - return result; -} - -export function headersArrayToObject(headers: types.HeadersArray, lowerCase: boolean): { [key: string]: string } { - const result: { [key: string]: string } = {}; - for (const { name, value } of headers) - result[lowerCase ? name.toLowerCase() : name] = value; - return result; -} diff --git a/src/dialog.ts b/src/dialog.ts index 2fb35ede5e..1b38557eb2 100644 --- a/src/dialog.ts +++ b/src/dialog.ts @@ -15,7 +15,8 @@ * limitations under the License. */ -import { assert, debugLogger } from './helper'; +import { assert } from './utils/utils'; +import { debugLogger } from './utils/debugLogger'; type OnHandle = (accept: boolean, promptText?: string) => Promise; diff --git a/src/dom.ts b/src/dom.ts index eb5677e846..e7bf07044d 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -15,7 +15,7 @@ */ import * as frames from './frames'; -import { assert, helper } from './helper'; +import { assert } from './utils/utils'; import InjectedScript from './injected/injectedScript'; import * as injectedScriptSource from './generated/injectedScriptSource'; import * as debugScriptSource from './generated/debugScriptSource'; @@ -429,7 +429,6 @@ export class ElementHandle extends js.JSHandle { async _fill(progress: Progress, value: string, options: types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> { progress.log(`elementHandle.fill("${value}")`); - assert(helper.isString(value), `value: expected string, got ${typeof value}`); return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { progress.log(' waiting for element to be visible, enabled and editable'); const poll = await this._evaluateHandleInUtility(([injected, node, value]) => { diff --git a/src/download.ts b/src/download.ts index 93e9e0c268..4cf3a1e699 100644 --- a/src/download.ts +++ b/src/download.ts @@ -19,7 +19,7 @@ import * as fs from 'fs'; import * as util from 'util'; import { Page } from './page'; import { Readable } from 'stream'; -import { assert, mkdirIfNeeded } from './helper'; +import { assert, mkdirIfNeeded } from './utils/utils'; export class Download { private _downloadsPath: string; diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 6627724228..bb88fe0125 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -17,7 +17,8 @@ import { Browser, BrowserOptions } from '../browser'; import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; -import { assert, helper, RegisteredListener } from '../helper'; +import { helper, RegisteredListener } from '../helper'; +import { assert } from '../utils/utils'; import * as network from '../network'; import { Page, PageBinding } from '../page'; import { ConnectionTransport } from '../transport'; diff --git a/src/firefox/ffConnection.ts b/src/firefox/ffConnection.ts index 8c057bc05a..7c9c272e09 100644 --- a/src/firefox/ffConnection.ts +++ b/src/firefox/ffConnection.ts @@ -16,10 +16,11 @@ */ import { EventEmitter } from 'events'; -import { assert, debugLogger } from '../helper'; +import { assert } from '../utils/utils'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { Protocol } from './protocol'; import { rewriteErrorMessage } from '../utils/stackTrace'; +import { debugLogger } from '../utils/debugLogger'; export const ConnectionEvents = { Disconnected: Symbol('Disconnected'), diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 01353e87ef..89c37ff701 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -18,7 +18,8 @@ import * as dialog from '../dialog'; import * as dom from '../dom'; import * as frames from '../frames'; -import { assert, helper, RegisteredListener } from '../helper'; +import { helper, RegisteredListener } from '../helper'; +import { assert } from '../utils/utils'; import { Page, PageBinding, PageDelegate, Worker } from '../page'; import { kScreenshotDuringNavigationError } from '../screenshotter'; import * as types from '../types'; diff --git a/src/frames.ts b/src/frames.ts index 79b99887a0..f4f94d8edd 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -17,7 +17,7 @@ import { ConsoleMessage } from './console'; import * as dom from './dom'; -import { assert, helper, RegisteredListener, debugLogger } from './helper'; +import { helper, RegisteredListener } from './helper'; import * as js from './javascript'; import * as network from './network'; import { Page } from './page'; @@ -26,6 +26,8 @@ import * as types from './types'; import { BrowserContext } from './browserContext'; import { Progress, ProgressController } from './progress'; import { EventEmitter } from 'events'; +import { assert, makeWaitForNextTask } from './utils/utils'; +import { debugLogger } from './utils/debugLogger'; type ContextData = { contextPromise: Promise; @@ -134,7 +136,7 @@ export class FrameManager { await barrier.waitFor(); this._signalBarriers.delete(barrier); // Resolve in the next task, after all waitForNavigations. - await new Promise(helper.makeWaitForNextTask()); + await new Promise(makeWaitForNextTask()); return result; } @@ -879,20 +881,15 @@ export class Frame extends EventEmitter { } async _waitForFunctionExpression(expression: string, isFunction: boolean, arg: any, options: types.WaitForFunctionOptions = {}): Promise> { - const { polling = 'raf' } = options; - if (helper.isString(polling)) - assert(polling === 'raf', 'Unknown polling option: ' + polling); - else if (helper.isNumber(polling)) - assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); - else - throw new Error('Unknown polling option: ' + polling); + if (typeof options.pollingInterval === 'number') + assert(options.pollingInterval > 0, 'Cannot poll with non-positive interval: ' + options.pollingInterval); const predicateBody = isFunction ? 'return (' + expression + ')(arg)' : 'return (' + expression + ')'; const task: dom.SchedulableTask = injectedScript => injectedScript.evaluateHandle((injectedScript, { predicateBody, polling, arg }) => { const innerPredicate = new Function('arg', predicateBody) as (arg: any) => R; - if (polling === 'raf') + if (typeof polling !== 'number') return injectedScript.pollRaf((progress, continuePolling) => innerPredicate(arg) || continuePolling); return injectedScript.pollInterval(polling, (progress, continuePolling) => innerPredicate(arg) || continuePolling); - }, { predicateBody, polling, arg }); + }, { predicateBody, polling: options.pollingInterval, arg }); return this._page._runAbortableTask( progress => this._scheduleRerunnableHandleTask(progress, 'main', task), this._page._timeoutSettings.timeout(options)); diff --git a/src/helper.ts b/src/helper.ts index ea534bee34..3e27e1ab84 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -21,14 +21,11 @@ import * as fs from 'fs'; import * as os from 'os'; import * as removeFolder from 'rimraf'; import * as util from 'util'; -import * as path from 'path'; import * as types from './types'; import { Progress } from './progress'; -import * as debug from 'debug'; const removeFolderAsync = util.promisify(removeFolder); const readFileAsync = util.promisify(fs.readFile.bind(fs)); -const mkdirAsync = util.promisify(fs.mkdir.bind(fs)); export type RegisteredListener = { emitter: EventEmitter; @@ -36,10 +33,6 @@ export type RegisteredListener = { handler: (...args: any[]) => void; }; -export type Listener = (...args: any[]) => void; - -const isInDebugMode = !!getFromENV('PWDEBUG'); - class Helper { static addEventListener( emitter: EventEmitter, @@ -59,30 +52,6 @@ class Helper { listeners.splice(0, listeners.length); } - static isString(obj: any): obj is string { - return typeof obj === 'string' || obj instanceof String; - } - - static isNumber(obj: any): obj is number { - return typeof obj === 'number' || obj instanceof Number; - } - - static isRegExp(obj: any): obj is RegExp { - return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; - } - - static isError(obj: any): obj is Error { - return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error'); - } - - static isObject(obj: any): obj is NonNullable { - return typeof obj === 'object' && obj !== null; - } - - static isBoolean(obj: any): obj is boolean { - return typeof obj === 'boolean' || obj instanceof Boolean; - } - static completeUserURL(urlString: string): string { if (urlString.startsWith('localhost') || urlString.startsWith('127.0.0.1')) urlString = 'http://' + urlString; @@ -101,41 +70,6 @@ class Helper { return { width: Math.floor(size.width + 1e-3), height: Math.floor(size.height + 1e-3) }; } - // 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'); } @@ -176,10 +110,6 @@ class Helper { progress.cleanupWhenAborted(dispose); return { promise, dispose }; } - - static isDebugMode(): boolean { - return isInDebugMode; - } } export async function getUbuntuVersion(): Promise { @@ -221,87 +151,4 @@ function getUbuntuVersionInternal(osReleaseText: string): string { return fields.get('version_id') || ''; } -export function assert(value: any, message?: string): asserts value { - if (!value) - throw new Error(message); -} - -let _isUnderTest = false; - -export function setUnderTest() { - _isUnderTest = true; -} - -export function isUnderTest(): boolean { - return _isUnderTest; -} - -export function debugAssert(value: any, message?: string): asserts value { - if (_isUnderTest && !value) - throw new Error(message); -} - -export function getFromENV(name: string) { - let value = process.env[name]; - value = value || process.env[`npm_config_${name.toLowerCase()}`]; - value = value || process.env[`npm_package_config_${name.toLowerCase()}`]; - return value; -} - -export async function doSlowMo(amount?: number) { - if (!amount) - return; - await new Promise(x => setTimeout(x, amount)); -} - -export async function mkdirIfNeeded(filePath: string) { - // This will harmlessly throw on windows if the dirname is the root directory. - await mkdirAsync(path.dirname(filePath), {recursive: true}).catch(() => {}); -} - export const helper = Helper; - -const debugLoggerColorMap = { - 'api': 45, // cyan - 'protocol': 34, // green - 'browser': 0, // reset - 'error': 160, // red, - 'channel:command': 33, // blue - 'channel:response': 202, // orange - 'channel:event': 207, // magenta -}; -export type LogName = keyof typeof debugLoggerColorMap; - -export class DebugLogger { - private _debuggers = new Map(); - - constructor() { - if (process.env.DEBUG_FILE) { - const ansiRegex = new RegExp([ - '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', - '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))' - ].join('|'), 'g'); - const stream = fs.createWriteStream(process.env.DEBUG_FILE); - (debug as any).log = (data: string) => { - stream.write(data.replace(ansiRegex, '')); - stream.write('\n'); - }; - } - } - - log(name: LogName, message: string | Error | object) { - let cachedDebugger = this._debuggers.get(name); - if (!cachedDebugger) { - cachedDebugger = debug(`pw:${name}`); - this._debuggers.set(name, cachedDebugger); - (cachedDebugger as any).color = debugLoggerColorMap[name]; - } - cachedDebugger(message); - } - - isEnabled(name: LogName) { - return debug.enabled(`pw:${name}`); - } -} - -export const debugLogger = new DebugLogger(); diff --git a/src/input.ts b/src/input.ts index 276aa2e2ab..39bf81e2a1 100644 --- a/src/input.ts +++ b/src/input.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { assert } from './helper'; +import { assert } from './utils/utils'; import * as keyboardLayout from './usKeyboardLayout'; import * as types from './types'; import type { Page } from './page'; diff --git a/src/install/browserFetcher.ts b/src/install/browserFetcher.ts index 4c5f6d7e41..67157c0997 100644 --- a/src/install/browserFetcher.ts +++ b/src/install/browserFetcher.ts @@ -23,7 +23,7 @@ import * as ProgressBar from 'progress'; import { getProxyForUrl } from 'proxy-from-env'; import * as URL from 'url'; import * as util from 'util'; -import { assert, getFromENV } from '../helper'; +import { assert, getFromENV } from '../utils/utils'; import * as browserPaths from './browserPaths'; import { BrowserName, BrowserPlatform, BrowserDescriptor } from './browserPaths'; diff --git a/src/install/browserPaths.ts b/src/install/browserPaths.ts index e302b111a7..1fb534f880 100644 --- a/src/install/browserPaths.ts +++ b/src/install/browserPaths.ts @@ -18,7 +18,8 @@ import { execSync } from 'child_process'; import * as os from 'os'; import * as path from 'path'; -import { getFromENV, getUbuntuVersionSync } from '../helper'; +import { getUbuntuVersionSync } from '../helper'; +import { getFromENV } from '../utils/utils'; export type BrowserName = 'chromium'|'webkit'|'firefox'; export type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'ubuntu18.04'|'ubuntu20.04'; diff --git a/src/install/installer.ts b/src/install/installer.ts index c200b31e7f..0440a9cf16 100644 --- a/src/install/installer.ts +++ b/src/install/installer.ts @@ -15,13 +15,13 @@ */ import * as crypto from 'crypto'; -import { getFromENV } from '../helper'; import * as fs from 'fs'; import * as path from 'path'; import * as util from 'util'; import * as removeFolder from 'rimraf'; import * as browserPaths from './browserPaths'; import * as browserFetcher from './browserFetcher'; +import { getFromENV } from '../utils/utils'; const fsMkdirAsync = util.promisify(fs.mkdir.bind(fs)); const fsReaddirAsync = util.promisify(fs.readdir.bind(fs)); diff --git a/src/network.ts b/src/network.ts index 93448aac39..974135a424 100644 --- a/src/network.ts +++ b/src/network.ts @@ -16,7 +16,7 @@ import * as frames from './frames'; import * as types from './types'; -import { assert } from './helper'; +import { assert } from './utils/utils'; export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { const parsedURLs = urls.map(s => new URL(s)); diff --git a/src/page.ts b/src/page.ts index 5527530b2b..4085371ba7 100644 --- a/src/page.ts +++ b/src/page.ts @@ -17,12 +17,11 @@ import * as dom from './dom'; import * as frames from './frames'; -import { assert, helper, debugLogger } from './helper'; import * as input from './input'; import * as js from './javascript'; import * as network from './network'; import { Screenshotter } from './screenshotter'; -import { TimeoutSettings } from './timeoutSettings'; +import { TimeoutSettings } from './utils/timeoutSettings'; import * as types from './types'; import { BrowserContext } from './browserContext'; import { ConsoleMessage } from './console'; @@ -30,6 +29,8 @@ import * as accessibility from './accessibility'; import { EventEmitter } from 'events'; import { FileChooser } from './fileChooser'; import { Progress, runAbortableTask } from './progress'; +import { assert, isError } from './utils/utils'; +import { debugLogger } from './utils/debugLogger'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -462,7 +463,7 @@ export class PageBinding { const result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args); context.evaluateInternal(deliverResult, { name, seq, result }).catch(e => debugLogger.log('error', e)); } catch (error) { - if (helper.isError(error)) + if (isError(error)) context.evaluateInternal(deliverError, { name, seq, message: error.message, stack: error.stack }).catch(e => debugLogger.log('error', e)); else context.evaluateInternal(deliverErrorValue, { name, seq, error }).catch(e => debugLogger.log('error', e)); diff --git a/src/progress.ts b/src/progress.ts index 436bc096e8..ce05769724 100644 --- a/src/progress.ts +++ b/src/progress.ts @@ -15,8 +15,9 @@ */ import { TimeoutError } from './utils/errors'; -import { assert, debugLogger, LogName } from './helper'; +import { assert } from './utils/utils'; import { rewriteErrorMessage } from './utils/stackTrace'; +import { debugLogger, LogName } from './utils/debugLogger'; export interface Progress { readonly aborted: Promise; diff --git a/src/protocol/serializers.ts b/src/protocol/serializers.ts index aee1c99c9e..7e404c65b1 100644 --- a/src/protocol/serializers.ts +++ b/src/protocol/serializers.ts @@ -15,11 +15,10 @@ */ import { TimeoutError } from '../utils/errors'; -import { helper } from '../helper'; import { SerializedError, SerializedValue } from './channels'; export function serializeError(e: any): SerializedError { - if (helper.isError(e)) + if (isError(e)) return { error: { message: e.message, stack: e.stack, name: e.name } }; return { value: serializeValue(e, value => ({ fallThrough: value }), new Set()) }; } diff --git a/src/protocol/transport.ts b/src/protocol/transport.ts index 6234d32abe..fbd2fabcc5 100644 --- a/src/protocol/transport.ts +++ b/src/protocol/transport.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { helper } from '../helper'; +import { makeWaitForNextTask } from '../utils/utils'; export class Transport { private _pipeWrite: NodeJS.WritableStream; private _data = Buffer.from([]); - private _waitForNextTask = helper.makeWaitForNextTask(); + private _waitForNextTask = makeWaitForNextTask(); private _closed = false; private _bytesLeft = 0; diff --git a/src/protocol/validatorPrimitives.ts b/src/protocol/validatorPrimitives.ts index 215cb6e976..ac996b078d 100644 --- a/src/protocol/validatorPrimitives.ts +++ b/src/protocol/validatorPrimitives.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { isUnderTest } from '../helper'; +import { isUnderTest } from '../utils/utils'; export class ValidationError extends Error {} export type Validator = (arg: any, path: string) => any; diff --git a/src/rpc/client/browser.ts b/src/rpc/client/browser.ts index 912e2b7bac..72175d9776 100644 --- a/src/rpc/client/browser.ts +++ b/src/rpc/client/browser.ts @@ -20,9 +20,9 @@ import { Page } from './page'; import { ChannelOwner } from './channelOwner'; import { Events } from './events'; import { BrowserType } from './browserType'; -import { headersObjectToArray } from '../../converters'; import { BrowserContextOptions } from './types'; import { validateHeaders } from './network'; +import { headersObjectToArray } from '../../utils/utils'; export class Browser extends ChannelOwner { readonly _contexts = new Set(); diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index 7b48f8017b..8dd4028e7e 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -20,13 +20,13 @@ import { Page, BindingCall } from './page'; import * as network from './network'; import { BrowserContextChannel, BrowserContextInitializer } from '../../protocol/channels'; import { ChannelOwner } from './channelOwner'; -import { isUnderTest, deprecate, evaluationScript, urlMatches } from './clientHelper'; +import { deprecate, evaluationScript, urlMatches } from './clientHelper'; import { Browser } from './browser'; import { Events } from './events'; -import { TimeoutSettings } from '../../timeoutSettings'; +import { TimeoutSettings } from '../../utils/timeoutSettings'; import { Waiter } from './waiter'; -import { headersObjectToArray } from '../../converters'; import { URLMatch, Headers, WaitForEventOptions } from './types'; +import { isUnderTest, headersObjectToArray } from '../../utils/utils'; export class BrowserContext extends ChannelOwner { _pages = new Set(); diff --git a/src/rpc/client/browserType.ts b/src/rpc/client/browserType.ts index 8675e73ce1..69e51956ef 100644 --- a/src/rpc/client/browserType.ts +++ b/src/rpc/client/browserType.ts @@ -18,17 +18,16 @@ import { BrowserTypeChannel, BrowserTypeInitializer, BrowserTypeLaunchParams, Br import { Browser } from './browser'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; -import { headersObjectToArray } from '../../converters'; -import { assert, helper } from '../../helper'; import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions } from './types'; import * as WebSocket from 'ws'; import { Connection } from './connection'; import { serializeError } from '../../protocol/serializers'; import { Events } from './events'; -import { TimeoutSettings } from '../../timeoutSettings'; +import { TimeoutSettings } from '../../utils/timeoutSettings'; import { ChildProcess } from 'child_process'; import { envObjectToArray } from './clientHelper'; import { validateHeaders } from './network'; +import { assert, makeWaitForNextTask, headersObjectToArray } from '../../utils/utils'; export interface BrowserServerLauncher { launchServer(options?: LaunchServerOptions): Promise; @@ -122,7 +121,7 @@ export class BrowserType extends ChannelOwner any) => setTimeout(cb, options.slowMo) - : helper.makeWaitForNextTask(); + : makeWaitForNextTask(); connection.onmessage = message => { if (ws.readyState !== WebSocket.OPEN) { setTimeout(() => { diff --git a/src/rpc/client/channelOwner.ts b/src/rpc/client/channelOwner.ts index 89251f5772..44fbceec96 100644 --- a/src/rpc/client/channelOwner.ts +++ b/src/rpc/client/channelOwner.ts @@ -18,7 +18,7 @@ import { EventEmitter } from 'events'; import type { Channel } from '../../protocol/channels'; import type { Connection } from './connection'; import type { LoggerSink } from './types'; -import { debugLogger } from '../../helper'; +import { debugLogger } from '../../utils/debugLogger'; export abstract class ChannelOwner extends EventEmitter { private _connection: Connection; diff --git a/src/rpc/client/clientHelper.ts b/src/rpc/client/clientHelper.ts index 53881ada27..cf13c9201d 100644 --- a/src/rpc/client/clientHelper.ts +++ b/src/rpc/client/clientHelper.ts @@ -15,10 +15,10 @@ * limitations under the License. */ -import { isUnderTest as commonIsUnderTest, helper } from '../../helper'; import * as types from './types'; import * as fs from 'fs'; import * as util from 'util'; +import { isString, isRegExp } from '../../utils/utils'; const deprecatedHits = new Set(); export function deprecate(methodName: string, message: string) { @@ -28,10 +28,6 @@ export function deprecate(methodName: string, message: string) { console.warn(message); } -export function isUnderTest() { - return commonIsUnderTest(); -} - export function envObjectToArray(env: types.Env): { name: string, value: string }[] { const result: { name: string, value: string }[] = []; for (const name in env) { @@ -49,7 +45,7 @@ export async function evaluationScript(fun: Function | string | { path?: string, } if (arg !== undefined) throw new Error('Cannot evaluate a string with arguments'); - if (helper.isString(fun)) + if (isString(fun)) return fun; if (fun.content !== undefined) return fun.content; @@ -65,9 +61,9 @@ export async function evaluationScript(fun: Function | string | { path?: string, export function urlMatches(urlString: string, match: types.URLMatch | undefined): boolean { if (match === undefined || match === '') return true; - if (helper.isString(match)) + if (isString(match)) match = globToRegex(match); - if (helper.isRegExp(match)) + if (isRegExp(match)) return match.test(urlString); if (typeof match === 'string' && match === urlString) return true; diff --git a/src/rpc/client/connection.ts b/src/rpc/client/connection.ts index b55d2f67de..49990bfe45 100644 --- a/src/rpc/client/connection.ts +++ b/src/rpc/client/connection.ts @@ -39,7 +39,7 @@ import { Stream } from './stream'; import { createScheme, Validator, ValidationError } from '../../protocol/validator'; import { WebKitBrowser } from './webkitBrowser'; import { FirefoxBrowser } from './firefoxBrowser'; -import { debugLogger } from '../../helper'; +import { debugLogger } from '../../utils/debugLogger'; class Root extends ChannelOwner { constructor(connection: Connection) { diff --git a/src/rpc/client/electron.ts b/src/rpc/client/electron.ts index 6ecedd309a..92704276ee 100644 --- a/src/rpc/client/electron.ts +++ b/src/rpc/client/electron.ts @@ -19,7 +19,7 @@ import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { Page } from './page'; import { serializeArgument, FuncOn, parseResult, SmartHandle, JSHandle } from './jsHandle'; -import { TimeoutSettings } from '../../timeoutSettings'; +import { TimeoutSettings } from '../../utils/timeoutSettings'; import { Waiter } from './waiter'; import { Events } from './events'; import { WaitForEventOptions, Env, LoggerSink } from './types'; diff --git a/src/rpc/client/elementHandle.ts b/src/rpc/client/elementHandle.ts index 7ceabb7236..88181fa9db 100644 --- a/src/rpc/client/elementHandle.ts +++ b/src/rpc/client/elementHandle.ts @@ -18,12 +18,12 @@ import { ElementHandleChannel, JSHandleInitializer, ElementHandleScrollIntoViewI import { Frame } from './frame'; import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle'; import { ChannelOwner } from './channelOwner'; -import { helper, assert, mkdirIfNeeded } from '../../helper'; import { SelectOption, FilePayload, Rect, SelectOptionOptions } from './types'; import * as fs from 'fs'; import * as mime from 'mime'; import * as path from 'path'; import * as util from 'util'; +import { assert, isString, mkdirIfNeeded } from '../../utils/utils'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); @@ -246,7 +246,7 @@ export function convertSelectOptionValues(values: string | ElementHandle | Selec assert(values[i] !== null, `options[${i}]: expected object, got null`); if (values[0] instanceof ElementHandle) return { elements: (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel) }; - if (helper.isString(values[0])) + if (isString(values[0])) return { options: (values as string[]).map(value => ({ value })) }; return { options: values as SelectOption[] }; } diff --git a/src/rpc/client/frame.ts b/src/rpc/client/frame.ts index ed8d51ba07..d96ceef22a 100644 --- a/src/rpc/client/frame.ts +++ b/src/rpc/client/frame.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { assert } from '../../helper'; +import { assert } from '../../utils/utils'; import { FrameChannel, FrameInitializer, FrameNavigatedEvent, FrameGotoOptions, FrameWaitForSelectorOptions, FrameDispatchEventOptions, FrameSetContentOptions, FrameClickOptions, FrameDblclickOptions, FrameFillOptions, FrameFocusOptions, FrameTextContentOptions, FrameInnerTextOptions, FrameInnerHTMLOptions, FrameGetAttributeOptions, FrameHoverOptions, FrameSetInputFilesOptions, FrameTypeOptions, FramePressOptions, FrameCheckOptions, FrameUncheckOptions } from '../../protocol/channels'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; diff --git a/src/rpc/client/network.ts b/src/rpc/client/network.ts index 1d95bf8fa4..32cce3ee6f 100644 --- a/src/rpc/client/network.ts +++ b/src/rpc/client/network.ts @@ -18,12 +18,11 @@ import { URLSearchParams } from 'url'; import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../../protocol/channels'; import { ChannelOwner } from './channelOwner'; import { Frame } from './frame'; -import { headersArrayToObject, headersObjectToArray } from '../../converters'; import { Headers } from './types'; import * as fs from 'fs'; import * as mime from 'mime'; import * as util from 'util'; -import { helper } from '../../helper'; +import { isString, headersObjectToArray, headersArrayToObject } from '../../utils/utils'; export type NetworkCookie = { name: string, @@ -175,7 +174,7 @@ export class Route extends ChannelOwner { body = buffer.toString('base64'); isBase64 = true; length = buffer.length; - } else if (helper.isString(response.body)) { + } else if (isString(response.body)) { body = response.body; isBase64 = false; length = Buffer.byteLength(body); @@ -204,7 +203,7 @@ export class Route extends ChannelOwner { } async continue(overrides: { method?: string, headers?: Headers, postData?: string | Buffer } = {}) { - const postDataBuffer = helper.isString(overrides.postData) ? Buffer.from(overrides.postData, 'utf8') : overrides.postData; + const postDataBuffer = isString(overrides.postData) ? Buffer.from(overrides.postData, 'utf8') : overrides.postData; await this._channel.continue({ method: overrides.method, headers: overrides.headers ? headersObjectToArray(overrides.headers) : undefined, @@ -284,7 +283,7 @@ export class Response extends ChannelOwner export function validateHeaders(headers: Headers) { for (const key of Object.keys(headers)) { const value = headers[key]; - if (!Object.is(value, undefined) && !helper.isString(value)) + if (!Object.is(value, undefined) && !isString(value)) throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`); } } diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index 9ba7c6bb3b..2ec897760e 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -16,11 +16,10 @@ */ import { Events } from './events'; -import { assert, helper, Listener, mkdirIfNeeded } from '../../helper'; -import { TimeoutSettings } from '../../timeoutSettings'; +import { assert } from '../../utils/utils'; +import { TimeoutSettings } from '../../utils/timeoutSettings'; import { BindingCallChannel, BindingCallInitializer, PageChannel, PageInitializer, PagePdfParams, FrameWaitForSelectorOptions, FrameDispatchEventOptions, FrameSetContentOptions, FrameGotoOptions, PageReloadOptions, PageGoBackOptions, PageGoForwardOptions, PageScreenshotOptions, FrameClickOptions, FrameDblclickOptions, FrameFillOptions, FrameFocusOptions, FrameTextContentOptions, FrameInnerTextOptions, FrameInnerHTMLOptions, FrameGetAttributeOptions, FrameHoverOptions, FrameSetInputFilesOptions, FrameTypeOptions, FramePressOptions, FrameCheckOptions, FrameUncheckOptions } from '../../protocol/channels'; import { parseError, serializeError } from '../../protocol/serializers'; -import { headersObjectToArray } from '../../converters'; import { Accessibility } from './accessibility'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; @@ -42,6 +41,7 @@ import * as fs from 'fs'; import * as util from 'util'; import { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types'; import { evaluationScript, urlMatches } from './clientHelper'; +import { isString, isRegExp, isObject, mkdirIfNeeded, headersObjectToArray } from '../../utils/utils'; type PDFOptions = Omit & { width?: string | number, @@ -54,6 +54,7 @@ type PDFOptions = Omit & { }, path?: string, }; +type Listener = (...args: any[]) => void; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); @@ -197,8 +198,8 @@ export class Page extends ChannelOwner { } frame(options: string | { name?: string, url?: URLMatch }): Frame | null { - const name = helper.isString(options) ? options : options.name; - const url = helper.isObject(options) ? options.url : undefined; + const name = isString(options) ? options : options.name; + const url = isObject(options) ? options.url : undefined; assert(name || url, 'Either name or url matcher should be specified'); return this.frames().find(f => { if (name) @@ -330,7 +331,7 @@ export class Page extends ChannelOwner { async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean), options: { timeout?: number } = {}): Promise { const predicate = (request: Request) => { - if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) + if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) return urlMatches(request.url(), urlOrPredicate); return urlOrPredicate(request); }; @@ -339,7 +340,7 @@ export class Page extends ChannelOwner { async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean), options: { timeout?: number } = {}): Promise { const predicate = (response: Response) => { - if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) + if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) return urlMatches(response.url(), urlOrPredicate); return urlOrPredicate(response); }; diff --git a/src/rpc/inprocess.ts b/src/rpc/inprocess.ts index 017b55a7bb..b12174c8a7 100644 --- a/src/rpc/inprocess.ts +++ b/src/rpc/inprocess.ts @@ -19,8 +19,8 @@ import type { Playwright as PlaywrightImpl } from '../server/playwright'; import type { Playwright as PlaywrightAPI } from './client/playwright'; import { PlaywrightDispatcher } from './server/playwrightDispatcher'; import { Connection } from './client/connection'; -import { isUnderTest } from '../helper'; import { BrowserServerLauncherImpl } from './browserServerImpl'; +import { isUnderTest } from '../utils/utils'; export function setupInProcess(playwright: PlaywrightImpl): PlaywrightAPI { const clientConnection = new Connection(); diff --git a/src/rpc/server/dispatcher.ts b/src/rpc/server/dispatcher.ts index 13cb007e1d..7ccc00a9d3 100644 --- a/src/rpc/server/dispatcher.ts +++ b/src/rpc/server/dispatcher.ts @@ -15,10 +15,11 @@ */ import { EventEmitter } from 'events'; -import { helper, debugAssert, assert } from '../../helper'; +import { helper } from '../../helper'; import * as channels from '../../protocol/channels'; import { serializeError } from '../../protocol/serializers'; import { createScheme, Validator, ValidationError } from '../../protocol/validator'; +import { assert, debugAssert } from '../../utils/utils'; export const dispatcherSymbol = Symbol('dispatcher'); diff --git a/src/rpc/server/frameDispatcher.ts b/src/rpc/server/frameDispatcher.ts index 6296f0ce4b..96a985bd44 100644 --- a/src/rpc/server/frameDispatcher.ts +++ b/src/rpc/server/frameDispatcher.ts @@ -173,11 +173,7 @@ export class FrameDispatcher extends Dispatcher { - const options = { - ...params, - polling: params.pollingInterval === undefined ? 'raf' as const : params.pollingInterval - }; - return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, parseArgument(params.arg), options)) }; + return { handle: createHandle(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, parseArgument(params.arg), params)) }; } async title(): Promise { diff --git a/src/screenshotter.ts b/src/screenshotter.ts index fd33d8a33b..758883d1e4 100644 --- a/src/screenshotter.ts +++ b/src/screenshotter.ts @@ -16,11 +16,12 @@ */ import * as dom from './dom'; -import { assert, helper } from './helper'; +import { helper } from './helper'; import { Page } from './page'; import * as types from './types'; import { rewriteErrorMessage } from './utils/stackTrace'; import { Progress } from './progress'; +import { assert } from './utils/utils'; export class Screenshotter { private _queue = new TaskQueue(); diff --git a/src/selectors.ts b/src/selectors.ts index df40295c0a..dcc3820919 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -16,7 +16,6 @@ import * as dom from './dom'; import * as frames from './frames'; -import { helper, assert } from './helper'; import * as js from './javascript'; import * as types from './types'; import { ParsedSelector, parseSelector } from './common/selectorParser'; @@ -116,7 +115,6 @@ export class Selectors { } _parseSelector(selector: string): SelectorInfo { - assert(helper.isString(selector), `selector must be a string`); const parsed = parseSelector(selector); for (const {name} of parsed.parts) { if (!this._builtinEngines.has(name) && !this._engines.has(name)) diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 20f1a1d747..a9b8c3edf8 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -22,13 +22,13 @@ import { BrowserContext, verifyProxySettings, validateBrowserContextOptions } fr import * as browserPaths from '../install/browserPaths'; import { ConnectionTransport, WebSocketTransport } from '../transport'; import { BrowserOptions, Browser, BrowserProcess } from '../browser'; -import { assert, helper } from '../helper'; import { launchProcess, Env, waitForLine, envArrayToObject } from './processLauncher'; import { PipeTransport } from './pipeTransport'; import { Progress, runAbortableTask } from '../progress'; import * as types from '../types'; -import { TimeoutSettings } from '../timeoutSettings'; +import { TimeoutSettings } from '../utils/timeoutSettings'; import { validateHostRequirements } from './validateDependencies'; +import { assert, isDebugMode } from '../utils/utils'; export interface BrowserType { executablePath(): string; @@ -223,7 +223,7 @@ function copyTestHooks(from: object, to: object) { } function validateLaunchOptions(options: Options): Options { - const { devtools = false, headless = !helper.isDebugMode() && !devtools } = options; + const { devtools = false, headless = !isDebugMode() && !devtools } = options; return { ...options, devtools, headless }; } diff --git a/src/server/chromium.ts b/src/server/chromium.ts index 307e627496..f2a9ed3ec8 100644 --- a/src/server/chromium.ts +++ b/src/server/chromium.ts @@ -17,7 +17,6 @@ import * as path from 'path'; import * as os from 'os'; -import { getFromENV, helper } from '../helper'; import { CRBrowser } from '../chromium/crBrowser'; import { Env } from './processLauncher'; import { kBrowserCloseMessageId } from '../chromium/crConnection'; @@ -28,6 +27,7 @@ import { BrowserDescriptor } from '../install/browserPaths'; import { CRDevTools } from '../chromium/crDevTools'; import { BrowserOptions } from '../browser'; import * as types from '../types'; +import { isDebugMode, getFromENV } from '../utils/utils'; export class Chromium extends BrowserTypeBase { private _devtools: CRDevTools | undefined; @@ -43,7 +43,7 @@ export class Chromium extends BrowserTypeBase { super(packagePath, browser, debugPort ? { webSocketRegex: /^DevTools listening on (ws:\/\/.*)$/, stream: 'stderr' } : null); this._debugPort = debugPort; - if (helper.isDebugMode()) + if (isDebugMode()) this._devtools = this._createDevTools(); } diff --git a/src/server/electron.ts b/src/server/electron.ts index acad60e540..d30dee1c34 100644 --- a/src/server/electron.ts +++ b/src/server/electron.ts @@ -20,7 +20,7 @@ import { CRConnection, CRSession } from '../chromium/crConnection'; import { CRExecutionContext } from '../chromium/crExecutionContext'; import * as js from '../javascript'; import { Page } from '../page'; -import { TimeoutSettings } from '../timeoutSettings'; +import { TimeoutSettings } from '../utils/timeoutSettings'; import { WebSocketTransport } from '../transport'; import * as types from '../types'; import { launchProcess, waitForLine, envArrayToObject } from './processLauncher'; diff --git a/src/server/pipeTransport.ts b/src/server/pipeTransport.ts index 0e6a0a546f..fd8d08fa80 100644 --- a/src/server/pipeTransport.ts +++ b/src/server/pipeTransport.ts @@ -15,14 +15,16 @@ * limitations under the License. */ -import { helper, RegisteredListener, debugLogger } from '../helper'; +import { helper, RegisteredListener } from '../helper'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; +import { makeWaitForNextTask } from '../utils/utils'; +import { debugLogger } from '../utils/debugLogger'; export class PipeTransport implements ConnectionTransport { private _pipeWrite: NodeJS.WritableStream; private _pendingMessage = ''; private _eventListeners: RegisteredListener[]; - private _waitForNextTask = helper.makeWaitForNextTask(); + private _waitForNextTask = makeWaitForNextTask(); private _closed = false; onmessage?: (message: ProtocolResponse) => void; diff --git a/src/server/processLauncher.ts b/src/server/processLauncher.ts index 436cdeffa0..bfe6893723 100644 --- a/src/server/processLauncher.ts +++ b/src/server/processLauncher.ts @@ -19,9 +19,10 @@ import * as childProcess from 'child_process'; import * as readline from 'readline'; import * as removeFolder from 'rimraf'; import * as stream from 'stream'; -import { helper, isUnderTest } from '../helper'; +import { helper } from '../helper'; import { Progress } from '../progress'; import * as types from '../types'; +import { isUnderTest } from '../utils/utils'; export type Env = {[key: string]: string | number | boolean | undefined}; diff --git a/src/transport.ts b/src/transport.ts index 3a3318bd06..1be82072ef 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -16,8 +16,8 @@ */ import * as WebSocket from 'ws'; -import { helper } from './helper'; import { Progress } from './progress'; +import { makeWaitForNextTask } from './utils/utils'; export type ProtocolRequest = { id: number; @@ -85,7 +85,7 @@ export class WebSocketTransport implements ConnectionTransport { // 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(); + const messageWrap: (cb: () => void) => void = makeWaitForNextTask(); this._ws.addEventListener('message', event => { messageWrap(() => { diff --git a/src/types.ts b/src/types.ts index 8cfa88205e..9227154b1d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,8 +26,7 @@ export type TimeoutOptions = { timeout?: number }; export type WaitForElementOptions = TimeoutOptions & { state?: 'attached' | 'detached' | 'visible' | 'hidden' }; -export type Polling = 'raf' | number; -export type WaitForFunctionOptions = TimeoutOptions & { polling?: Polling }; +export type WaitForFunctionOptions = TimeoutOptions & { pollingInterval?: number }; export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle'; export const kLifecycleEvents: Set = new Set(['load', 'domcontentloaded', 'networkidle']); diff --git a/src/utils/debugLogger.ts b/src/utils/debugLogger.ts new file mode 100644 index 0000000000..cbdf95dd0d --- /dev/null +++ b/src/utils/debugLogger.ts @@ -0,0 +1,63 @@ +/** + * 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 * as debug from 'debug'; +import * as fs from 'fs'; + +const debugLoggerColorMap = { + 'api': 45, // cyan + 'protocol': 34, // green + 'browser': 0, // reset + 'error': 160, // red, + 'channel:command': 33, // blue + 'channel:response': 202, // orange + 'channel:event': 207, // magenta +}; +export type LogName = keyof typeof debugLoggerColorMap; + +class DebugLogger { + private _debuggers = new Map(); + + constructor() { + if (process.env.DEBUG_FILE) { + const ansiRegex = new RegExp([ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))' + ].join('|'), 'g'); + const stream = fs.createWriteStream(process.env.DEBUG_FILE); + (debug as any).log = (data: string) => { + stream.write(data.replace(ansiRegex, '')); + stream.write('\n'); + }; + } + } + + log(name: LogName, message: string | Error | object) { + let cachedDebugger = this._debuggers.get(name); + if (!cachedDebugger) { + cachedDebugger = debug(`pw:${name}`); + this._debuggers.set(name, cachedDebugger); + (cachedDebugger as any).color = debugLoggerColorMap[name]; + } + cachedDebugger(message); + } + + isEnabled(name: LogName) { + return debug.enabled(`pw:${name}`); + } +} + +export const debugLogger = new DebugLogger(); diff --git a/src/utils/sourceMap.ts b/src/utils/sourceMap.ts index 163ed8a67f..10a55b8b91 100644 --- a/src/utils/sourceMap.ts +++ b/src/utils/sourceMap.ts @@ -17,7 +17,7 @@ import * as fs from 'fs'; import * as util from 'util'; import { getCallerFilePath } from './stackTrace'; -import { helper } from '../helper'; +import { isDebugMode } from './utils'; type Position = { line: number; @@ -37,7 +37,7 @@ export function ensureSourceUrl(expression: string): string { } export async function generateSourceMapUrl(functionText: string, generatedText: string): Promise { - if (!helper.isDebugMode()) + if (!isDebugMode()) return generateSourceUrl(); const sourceMapUrl = await innerGenerateSourceMapUrl(functionText, generatedText); return sourceMapUrl || generateSourceUrl(); diff --git a/src/timeoutSettings.ts b/src/utils/timeoutSettings.ts similarity index 86% rename from src/timeoutSettings.ts rename to src/utils/timeoutSettings.ts index ce318f61df..3188bd0267 100644 --- a/src/timeoutSettings.ts +++ b/src/utils/timeoutSettings.ts @@ -15,10 +15,9 @@ * limitations under the License. */ -import { TimeoutOptions } from './types'; -import { helper } from './helper'; +import { isDebugMode } from './utils'; -const DEFAULT_TIMEOUT = helper.isDebugMode() ? 0 : 30000; +const DEFAULT_TIMEOUT = isDebugMode() ? 0 : 30000; export class TimeoutSettings { private _parent: TimeoutSettings | undefined; @@ -37,7 +36,7 @@ export class TimeoutSettings { this._defaultNavigationTimeout = timeout; } - navigationTimeout(options: TimeoutOptions): number { + navigationTimeout(options: { timeout?: number }): number { if (typeof options.timeout === 'number') return options.timeout; if (this._defaultNavigationTimeout !== null) @@ -49,7 +48,7 @@ export class TimeoutSettings { return DEFAULT_TIMEOUT; } - timeout(options: TimeoutOptions): number { + timeout(options: { timeout?: number }): number { if (typeof options.timeout === 'number') return options.timeout; if (this._defaultTimeout !== null) @@ -59,7 +58,7 @@ export class TimeoutSettings { return DEFAULT_TIMEOUT; } - static timeout(options: TimeoutOptions): number { + static timeout(options: { timeout?: number }): number { if (typeof options.timeout === 'number') return options.timeout; return DEFAULT_TIMEOUT; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000000..b8d9e4867a --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,126 @@ +/** + * 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 * as path from 'path'; +import * as fs from 'fs'; +import * as util from 'util'; + +const mkdirAsync = util.promisify(fs.mkdir.bind(fs)); + +// See https://joel.tools/microtasks/ +export function 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); + } + }; +} + +export function assert(value: any, message?: string): asserts value { + if (!value) + throw new Error(message); +} + +export function debugAssert(value: any, message?: string): asserts value { + if (isUnderTest() && !value) + throw new Error(message); +} + +export function isString(obj: any): obj is string { + return typeof obj === 'string' || obj instanceof String; +} + +export function isRegExp(obj: any): obj is RegExp { + return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +export function isObject(obj: any): obj is NonNullable { + return typeof obj === 'object' && obj !== null; +} + +export function isError(obj: any): obj is Error { + return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error'); +} + +const isInDebugMode = !!getFromENV('PWDEBUG'); +export function isDebugMode(): boolean { + return isInDebugMode; +} + +let _isUnderTest = false; +export function setUnderTest() { + _isUnderTest = true; +} +export function isUnderTest(): boolean { + return _isUnderTest; +} + +export function getFromENV(name: string) { + let value = process.env[name]; + value = value || process.env[`npm_config_${name.toLowerCase()}`]; + value = value || process.env[`npm_package_config_${name.toLowerCase()}`]; + return value; +} + +export async function mkdirIfNeeded(filePath: string) { + // This will harmlessly throw on windows if the dirname is the root directory. + await mkdirAsync(path.dirname(filePath), {recursive: true}).catch(() => {}); +} + +type HeadersArray = { name: string, value: string }[]; +type HeadersObject = { [key: string]: string }; + +export function headersObjectToArray(headers: HeadersObject): HeadersArray { + const result: HeadersArray = []; + for (const name in headers) { + if (!Object.is(headers[name], undefined)) + result.push({ name, value: headers[name] }); + } + return result; +} + +export function headersArrayToObject(headers: HeadersArray, lowerCase: boolean): HeadersObject { + const result: HeadersObject = {}; + for (const { name, value } of headers) + result[lowerCase ? name.toLowerCase() : name] = value; + return result; +} diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 6b307c5ad1..962bbd42aa 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -17,7 +17,8 @@ import { Browser, BrowserOptions } from '../browser'; import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; -import { helper, RegisteredListener, assert } from '../helper'; +import { helper, RegisteredListener } from '../helper'; +import { assert } from '../utils/utils'; import * as network from '../network'; import { Page, PageBinding } from '../page'; import * as path from 'path'; diff --git a/src/webkit/wkConnection.ts b/src/webkit/wkConnection.ts index 15f7abae0d..5b90fa3ce8 100644 --- a/src/webkit/wkConnection.ts +++ b/src/webkit/wkConnection.ts @@ -16,10 +16,11 @@ */ import { EventEmitter } from 'events'; -import { assert, debugLogger } from '../helper'; +import { assert } from '../utils/utils'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { Protocol } from './protocol'; import { rewriteErrorMessage } from '../utils/stackTrace'; +import { debugLogger } from '../utils/debugLogger'; // WKPlaywright uses this special id to issue Browser.close command which we // should ignore. diff --git a/src/webkit/wkInput.ts b/src/webkit/wkInput.ts index e7aa130a0f..0999131971 100644 --- a/src/webkit/wkInput.ts +++ b/src/webkit/wkInput.ts @@ -17,9 +17,9 @@ import * as input from '../input'; import * as types from '../types'; -import { helper } from '../helper'; import { macEditingCommands } from '../macEditingCommands'; import { WKSession } from './wkConnection'; +import { isString } from '../utils/utils'; function toModifiersMask(modifiers: Set): number { // From Source/WebKit/Shared/WebEvent.h @@ -56,7 +56,7 @@ export class RawKeyboardImpl implements input.RawKeyboard { parts.push(code); const shortcut = parts.join('+'); let commands = macEditingCommands[shortcut]; - if (helper.isString(commands)) + if (isString(commands)) commands = [commands]; await this._pageProxySession.send('Input.dispatchKeyEvent', { type: 'keyDown', diff --git a/src/webkit/wkInterceptableRequest.ts b/src/webkit/wkInterceptableRequest.ts index 0809b66f29..1f28b278f1 100644 --- a/src/webkit/wkInterceptableRequest.ts +++ b/src/webkit/wkInterceptableRequest.ts @@ -16,12 +16,11 @@ */ import * as frames from '../frames'; -import { assert } from '../helper'; import * as network from '../network'; import * as types from '../types'; import { Protocol } from './protocol'; import { WKSession } from './wkConnection'; -import { headersArrayToObject, headersObjectToArray } from '../converters'; +import { assert, headersObjectToArray, headersArrayToObject } from '../utils/utils'; const errorReasons: { [reason: string]: Protocol.Network.ResourceErrorType } = { 'aborted': 'Cancellation', diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 294fbff6a9..e4d1ea26d8 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -17,7 +17,7 @@ import { Screencast, BrowserContext } from '../browserContext'; import * as frames from '../frames'; -import { helper, RegisteredListener, assert, debugAssert } from '../helper'; +import { helper, RegisteredListener } from '../helper'; import * as dom from '../dom'; import * as network from '../network'; import { WKSession } from './wkConnection'; @@ -37,7 +37,7 @@ import { selectors } from '../selectors'; import * as jpeg from 'jpeg-js'; import * as png from 'pngjs'; import { JSHandle } from '../javascript'; -import { headersArrayToObject } from '../converters'; +import { assert, debugAssert, headersArrayToObject } from '../utils/utils'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const BINDING_CALL_MESSAGE = '__playwright_binding_call__'; diff --git a/src/webkit/wkProvisionalPage.ts b/src/webkit/wkProvisionalPage.ts index 67f5842f63..afcc5de4f2 100644 --- a/src/webkit/wkProvisionalPage.ts +++ b/src/webkit/wkProvisionalPage.ts @@ -16,8 +16,9 @@ import { WKSession } from './wkConnection'; import { WKPage } from './wkPage'; -import { RegisteredListener, helper, assert } from '../helper'; +import { RegisteredListener, helper } from '../helper'; import { Protocol } from './protocol'; +import { assert } from '../utils/utils'; export class WKProvisionalPage { readonly _session: WKSession; diff --git a/test/fixtures/closeme.js b/test/fixtures/closeme.js index a916c04995..76d9c82bb9 100644 --- a/test/fixtures/closeme.js +++ b/test/fixtures/closeme.js @@ -8,7 +8,7 @@ } const path = require('path'); - const { setUnderTest } = require(path.join(playwrightPath, 'lib', 'helper')); + const { setUnderTest } = require(path.join(playwrightPath, 'lib', 'utils', 'utils')); setUnderTest(); const playwright = require(path.join(playwrightPath, 'index')); diff --git a/test/playwright.fixtures.ts b/test/playwright.fixtures.ts index 96a10b9645..5eeac93712 100644 --- a/test/playwright.fixtures.ts +++ b/test/playwright.fixtures.ts @@ -22,7 +22,7 @@ import { LaunchOptions, BrowserType, Browser, BrowserContext, Page, BrowserServe import { TestServer } from '../utils/testserver'; import { Connection } from '../lib/rpc/client/connection'; import { Transport } from '../lib/protocol/transport'; -import { setUnderTest } from '../lib/helper'; +import { setUnderTest } from '../lib/utils/utils'; import { installCoverageHooks } from './coverage'; import { parameters, registerFixture, registerWorkerFixture } from '../test-runner'; diff --git a/test/proxy.spec.ts b/test/proxy.spec.ts index fb31f5e5e8..e85d999dbf 100644 --- a/test/proxy.spec.ts +++ b/test/proxy.spec.ts @@ -19,6 +19,13 @@ import { options } from './playwright.fixtures'; import socks from 'socksv5'; +it('should throw for bad server value', async ({browserType, defaultBrowserOptions}) => { + const error = await browserType.launch({ + ...defaultBrowserOptions, + proxy: { server: 123 as any } + }).catch(e => e); + expect(error.message).toContain('proxy.server: expected string, got number'); +}); it('should use proxy', async ({browserType, defaultBrowserOptions, server}) => { server.setRoute('/target.html', async (req, res) => { diff --git a/test/queryselector.spec.ts b/test/queryselector.spec.ts index 511f7eaec5..588916e30b 100644 --- a/test/queryselector.spec.ts +++ b/test/queryselector.spec.ts @@ -17,6 +17,11 @@ import './playwright.fixtures'; +it('should throw for non-string selector', async({page}) => { + const error = await page.$(null).catch(e => e); + expect(error.message).toContain('selector: expected string, got object'); +}); + it('should query existing element with css selector', async({page, server}) => { await page.setContent('
test
'); const element = await page.$('css=section'); diff --git a/utils/check_deps.js b/utils/check_deps.js index 269ffd5c5f..08fed64bf2 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -50,17 +50,28 @@ async function checkDeps() { } function allowImport(from, to) { - const rpc = path.join('src', 'rpc'); - if (!from.includes(rpc) && to.includes(rpc)) + from = from.substring(from.indexOf('src' + path.sep)).replace(/\\/g, '/'); + to = to.substring(to.indexOf('src' + path.sep)).replace(/\\/g, '/') + '.ts'; + while (from.lastIndexOf('/') !== -1) { + from = from.substring(0, from.lastIndexOf('/')); + const allowed = DEPS.get(from + '/'); + if (!allowed) + continue; + for (const prefix of allowed) { + if (to.startsWith(prefix)) + return true; + } return false; - const rpcClient = path.join('src', 'rpc', 'client'); - const rpcServer = path.join('src', 'rpc', 'server'); - if (from.includes(rpcClient) && to.includes(rpcServer)) - return false; - // if (from.includes(rpcClient) && !to.includes(rpc)) - // return false; - return true; + } + return false; } } +const DEPS = new Map([ + ['src/utils/', ['src/utils/']], + ['src/protocol/', ['src/protocol/', 'src/utils/']], + ['src/rpc/client/', ['src/rpc/client/', 'src/utils/', 'src/protocol/', 'src/chromium/protocol.ts']], + ['src/', ['src/']], // Allow everything else for now. +]); + checkDeps();