playwright/src/helper.ts

377 lines
12 KiB
TypeScript
Raw Normal View History

2019-11-18 18:18:28 -08:00
/**
* Copyright 2017 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
2020-04-01 14:42:47 -07:00
import * as crypto from 'crypto';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as removeFolder from 'rimraf';
2020-04-01 14:42:47 -07:00
import * as util from 'util';
import * as types from './types';
import { Progress } from './progress';
const removeFolderAsync = util.promisify(removeFolder);
2019-11-18 18:18:28 -08:00
export type RegisteredListener = {
2020-04-01 14:42:47 -07:00
emitter: EventEmitter;
2019-11-18 18:18:28 -08:00
eventName: (string | symbol);
handler: (...args: any[]) => void;
2019-11-18 18:18:28 -08:00
};
2020-04-01 14:42:47 -07:00
export type Listener = (...args: any[]) => void;
let isInDebugMode = !!getFromENV('PWDEBUG');
let isInRecordMode = false;
const deprecatedHits = new Set();
export function deprecate(methodName: string, message: string) {
if (deprecatedHits.has(methodName))
return;
deprecatedHits.add(methodName);
console.warn(message);
}
2019-11-18 18:18:28 -08:00
class Helper {
2019-11-18 18:18:28 -08:00
static evaluationString(fun: Function | string, ...args: any[]): string {
if (Helper.isString(fun)) {
assert(args.length === 0 || (args.length === 1 && args[0] === undefined), 'Cannot evaluate a string with arguments');
return fun;
2019-11-18 18:18:28 -08:00
}
return Helper.evaluationStringForFunctionBody(String(fun), ...args);
}
2019-11-18 18:18:28 -08:00
static evaluationStringForFunctionBody(functionBody: string, ...args: any[]): string {
return `(${functionBody})(${args.map(serializeArgument).join(',')})`;
2019-11-18 18:18:28 -08:00
function serializeArgument(arg: any): string {
if (Object.is(arg, undefined))
return 'undefined';
return JSON.stringify(arg);
}
}
static async evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
if (!helper.isString(fun) && typeof fun !== 'function') {
if (fun.content !== undefined) {
fun = fun.content;
} else if (fun.path !== undefined) {
2020-04-01 14:42:47 -07:00
let contents = await util.promisify(fs.readFile)(fun.path, 'utf8');
if (addSourceUrl)
contents += '//# sourceURL=' + fun.path.replace(/\n/g, '');
fun = contents;
} else {
throw new Error('Either path or content property must be present');
}
}
return helper.evaluationString(fun, arg);
}
static installApiHooks(className: string, classType: any) {
2019-11-18 18:18:28 -08:00
for (const methodName of Reflect.ownKeys(classType.prototype)) {
const method = Reflect.get(classType.prototype, methodName);
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
continue;
const isAsync = method.constructor.name === 'AsyncFunction';
if (!isAsync)
2019-11-18 18:18:28 -08:00
continue;
const override = function(this: any, ...args: any[]) {
const syncStack: any = {};
Error.captureStackTrace(syncStack);
return method.call(this, ...args).catch((e: any) => {
2019-11-18 18:18:28 -08:00
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
const clientStack = stack.substring(stack.indexOf('\n'));
if (e instanceof Error && e.stack && !e.stack.includes(clientStack))
e.stack += '\n -- ASYNC --\n' + stack;
throw e;
});
};
Object.defineProperty(override, 'name', { writable: false, value: methodName });
Reflect.set(classType.prototype, methodName, override);
2019-11-18 18:18:28 -08:00
}
}
static addEventListener(
2020-04-01 14:42:47 -07:00
emitter: EventEmitter,
2019-11-18 18:18:28 -08:00
eventName: (string | symbol),
handler: (...args: any[]) => void): RegisteredListener {
2019-11-18 18:18:28 -08:00
emitter.on(eventName, handler);
return { emitter, eventName, handler };
}
static removeEventListeners(listeners: Array<{
2020-04-01 14:42:47 -07:00
emitter: EventEmitter;
2019-11-18 18:18:28 -08:00
eventName: (string | symbol);
handler: (...args: any[]) => void;
2019-11-18 18:18:28 -08:00
}>) {
for (const listener of listeners)
listener.emitter.removeListener(listener.eventName, listener.handler);
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 isObject(obj: any): obj is NonNullable<object> {
return typeof obj === 'object' && obj !== null;
}
static isBoolean(obj: any): obj is boolean {
return typeof obj === 'boolean' || obj instanceof Boolean;
}
static globToRegex(glob: string): RegExp {
const tokens = ['^'];
let inGroup;
for (let i = 0; i < glob.length; ++i) {
const c = glob[i];
if (escapeGlobChars.has(c)) {
tokens.push('\\' + c);
continue;
}
if (c === '*') {
const beforeDeep = glob[i - 1];
let starCount = 1;
while (glob[i + 1] === '*') {
starCount++;
i++;
}
const afterDeep = glob[i + 1];
const isDeep = starCount > 1 &&
(beforeDeep === '/' || beforeDeep === undefined) &&
(afterDeep === '/' || afterDeep === undefined);
if (isDeep) {
tokens.push('((?:[^/]*(?:\/|$))*)');
i++;
} else {
tokens.push('([^/]*)');
}
continue;
}
switch (c) {
case '?':
tokens.push('.');
break;
case '{':
inGroup = true;
tokens.push('(');
break;
case '}':
inGroup = false;
tokens.push(')');
break;
case ',':
if (inGroup) {
tokens.push('|');
break;
}
tokens.push('\\' + c);
break;
default:
tokens.push(c);
}
}
tokens.push('$');
return new RegExp(tokens.join(''));
}
static completeUserURL(urlString: string): string {
if (urlString.startsWith('localhost') || urlString.startsWith('127.0.0.1'))
urlString = 'http://' + urlString;
return urlString;
}
static trimMiddle(string: string, maxLength: number) {
if (string.length <= maxLength)
return string;
const leftHalf = maxLength >> 1;
const rightHalf = maxLength - leftHalf - 1;
return string.substr(0, leftHalf) + '\u2026' + string.substr(this.length - rightHalf, rightHalf);
}
static enclosingIntRect(rect: types.Rect): types.Rect {
const x = Math.floor(rect.x + 1e-3);
const y = Math.floor(rect.y + 1e-3);
const x2 = Math.ceil(rect.x + rect.width - 1e-3);
const y2 = Math.ceil(rect.y + rect.height - 1e-3);
return { x, y, width: x2 - x, height: y2 - y };
}
static enclosingIntSize(size: types.Size): types.Size {
return { width: Math.floor(size.width + 1e-3), height: Math.floor(size.height + 1e-3) };
}
static urlMatches(urlString: string, match: types.URLMatch | undefined): boolean {
if (match === undefined || match === '')
return true;
if (helper.isString(match))
match = helper.globToRegex(match);
if (helper.isRegExp(match))
return match.test(urlString);
if (typeof match === 'string' && match === urlString)
return true;
const url = new URL(urlString);
if (typeof match === 'string')
return url.pathname === match;
assert(typeof match === 'function', 'url parameter should be string, RegExp or function');
return match(url);
}
2020-04-01 14:42:47 -07:00
// 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');
}
static getViewportSizeFromWindowFeatures(features: string[]): types.Size | null {
const widthString = features.find(f => f.startsWith('width='));
const heightString = features.find(f => f.startsWith('height='));
const width = widthString ? parseInt(widthString.substring(6), 10) : NaN;
const height = heightString ? parseInt(heightString.substring(7), 10) : NaN;
if (!Number.isNaN(width) && !Number.isNaN(height))
return { width, height };
return null;
}
static async removeFolders(dirs: string[]) {
await Promise.all(dirs.map(dir => {
return removeFolderAsync(dir).catch((err: Error) => console.error(err));
}));
}
static async waitForEvent(progress: Progress, emitter: EventEmitter, event: string, predicate?: Function): Promise<any> {
const listeners: RegisteredListener[] = [];
const promise = new Promise((resolve, reject) => {
listeners.push(helper.addEventListener(emitter, event, eventArg => {
try {
if (predicate && !predicate(eventArg))
return;
resolve(eventArg);
} catch (e) {
reject(e);
}
}));
});
progress.cleanupWhenAborted(() => helper.removeEventListeners(listeners));
const result = await promise;
helper.removeEventListeners(listeners);
return result;
}
static isDebugMode(): boolean {
return isInDebugMode;
}
static setDebugMode(enabled: boolean) {
isInDebugMode = enabled;
}
static isRecordMode(): boolean {
return isInRecordMode;
}
static setRecordMode(enabled: boolean) {
isInRecordMode = enabled;
}
2019-11-18 18:18:28 -08:00
}
export function assert(value: any, message?: string): asserts value {
2019-11-18 18:18:28 -08:00
if (!value)
throw new Error(message);
}
2020-05-08 10:37:54 -07:00
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 assertMaxArguments(count: number, max: number): asserts count {
assert(count <= max, 'Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.');
}
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 function logPolitely(toBeLogged: string) {
const logLevel = process.env.npm_config_loglevel;
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel || '') > -1;
if (!logLevelDisplay)
console.log(toBeLogged); // eslint-disable-line no-console
}
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
2019-11-18 18:18:28 -08:00
export const helper = Helper;