packages/testing/src/util.ts
// Significant portion of this code is copy-pasted from the node.js source
// Modifications consist primarily of removing dependencies on v8 natives and adding typings
// Original license:
/*
* Copyright Joyent, Inc. and other Node contributors. All rights reserved.
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
import { isArrayIndex, noop, Primitive } from '@aurelia/kernel';
/* eslint-disable @typescript-eslint/ban-types, no-control-regex */
export type BoxedPrimitive = Number | Boolean | String | Symbol;
export type IntArray = Int8Array | Int16Array | Int32Array | Uint8Array | Uint8ClampedArray | Uint16Array | Uint32Array;
export type FloatArray = Float32Array | Float64Array;
export type TypedArray = IntArray | FloatArray;
export type IntArrayConstructor = Int8ArrayConstructor | Int16ArrayConstructor | Int32ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Uint16ArrayConstructor | Uint32ArrayConstructor;
export type FloatArrayConstructor = Float32ArrayConstructor | Float64ArrayConstructor;
export type TypedArrayConstructor = IntArrayConstructor | FloatArrayConstructor;
export const {
getPrototypeOf,
getOwnPropertyDescriptor,
getOwnPropertyDescriptors,
getOwnPropertyNames,
getOwnPropertySymbols,
defineProperty,
defineProperties,
} = Object;
/** @internal */ export const Object_keys = Object.keys;
/** @internal */ export const Object_is = Object.is;
/** @internal */ export const Object_freeze = Object.freeze;
/** @internal */ export const Object_assign = Object.assign;
/** @internal */ export const Object_create = Object.create;
/** @internal */ export const Number_isNaN = Number.isNaN;
/** @internal */ export const Reflect_apply = Reflect.apply;
/** @internal */ export const ArrayBuffer_isView = ArrayBuffer.isView;
/** @internal */ export function uncurryThis<TArgs extends any[], TRet>(func: (...args: TArgs) => TRet): (thisArg: unknown, ...args: TArgs) => TRet {
return (thisArg: unknown, ...args: TArgs) => Reflect_apply(func, thisArg, args);
}
/** @internal */ export const hasOwnProperty = uncurryThis(Object.prototype.hasOwnProperty);
/** @internal */ export const propertyIsEnumerable = uncurryThis(Object.prototype.propertyIsEnumerable);
/** @internal */ export const TypedArrayPrototype = getPrototypeOf(Uint8Array.prototype);
const TypedArrayProto_toStringTag = uncurryThis(
(getOwnPropertyDescriptor(TypedArrayPrototype, Symbol.toStringTag) as PropertyDescriptor).get as () => string
);
/** @internal */ export const Object_toString = uncurryThis(Object.prototype.toString);
/** @internal */ export const RegExp_toString = uncurryThis(RegExp.prototype.toString);
/** @internal */ export const Date_toISOString = uncurryThis(Date.prototype.toISOString);
/** @internal */ export const Date_toString = uncurryThis(Date.prototype.toString);
/** @internal */ export const Error_toString = uncurryThis(Error.prototype.toString);
export const Date_getTime = uncurryThis(Date.prototype.getTime);
export const Set_values = uncurryThis(Set.prototype.values);
export const Map_entries = uncurryThis(Map.prototype.entries);
export const Boolean_valueOf = uncurryThis(Boolean.prototype.valueOf);
export const Number_valueOf = uncurryThis(Number.prototype.valueOf);
export const Symbol_valueOf = uncurryThis(Symbol.prototype.valueOf);
export const String_valueOf = uncurryThis(String.prototype.valueOf);
export function createNullObject<T>(input: T): Readonly<T> {
return Object_assign(Object_create(null), input);
}
export function createFrozenNullObject<T>(input: T): Readonly<T> {
return Object_freeze(createNullObject(input));
}
export function isBoolean(arg: unknown): arg is boolean {
return typeof arg === 'boolean';
}
export function isNull(arg: unknown): arg is null {
return arg === null;
}
export function isNumber(arg: unknown): arg is number {
return typeof arg === 'number';
}
export function isString(arg: unknown): arg is string {
return typeof arg === 'string';
}
export function isSymbol(arg: unknown): arg is symbol {
return typeof arg === 'symbol';
}
export function isUndefined(arg: unknown): arg is undefined {
return arg === void 0;
}
/** @internal */
export function isObject(arg: unknown): arg is Object {
return arg !== null && typeof arg === 'object';
}
export function isFunction(arg: unknown): arg is Function {
return typeof arg === 'function';
}
export function isPrimitive(arg: unknown): arg is Primitive {
return arg === null || typeof arg !== 'object' && typeof arg !== 'function';
}
export function isArrayBuffer(arg: unknown): arg is ArrayBuffer {
return arg instanceof ArrayBuffer;
}
export function isSharedArrayBuffer(arg: unknown): arg is SharedArrayBuffer {
return typeof SharedArrayBuffer !== 'undefined' && arg instanceof SharedArrayBuffer;
}
export function isAnyArrayBuffer(arg: unknown): arg is ArrayBuffer | SharedArrayBuffer {
return arg instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && arg instanceof SharedArrayBuffer);
}
export function isDate(arg: unknown): arg is Date {
return arg instanceof Date;
}
export function isMap(arg: unknown): arg is Map<any, any> {
return arg instanceof Map;
}
export function isMapIterator(arg: unknown): arg is IterableIterator<[any, any]> {
return Object_toString(arg) === '[object Map Iterator]';
}
export function isRegExp(arg: unknown): arg is RegExp {
return arg instanceof RegExp;
}
export function isSet(arg: unknown): arg is Set<any> {
return arg instanceof Set;
}
export function isSetIterator(arg: unknown): arg is IterableIterator<[any, any]> {
return Object_toString(arg) === '[object Set Iterator]';
}
export function isError(arg: unknown): arg is Error {
return arg instanceof Error;
}
export function isNumberObject(arg: unknown): arg is Number {
return arg instanceof Number;
}
export function isStringObject(arg: unknown): arg is String {
return arg instanceof String;
}
export function isBooleanObject(arg: unknown): arg is Boolean {
return arg instanceof Boolean;
}
export function isSymbolObject(arg: unknown): arg is Symbol {
return arg instanceof Symbol;
}
export function isBoxedPrimitive(arg: unknown): arg is BoxedPrimitive {
return (
isNumberObject(arg)
|| isStringObject(arg)
|| isBooleanObject(arg)
|| isSymbolObject(arg)
);
}
export function isTypedArray(value: unknown): value is TypedArray {
return TypedArrayProto_toStringTag(value) !== void 0;
}
export function isUint8Array(value: unknown): value is Uint8Array {
return TypedArrayProto_toStringTag(value) === 'Uint8Array';
}
export function isUint8ClampedArray(value: unknown): value is Uint8ClampedArray {
return TypedArrayProto_toStringTag(value) === 'Uint8ClampedArray';
}
export function isUint16Array(value: unknown): value is Uint16Array {
return TypedArrayProto_toStringTag(value) === 'Uint16Array';
}
export function isUint32Array(value: unknown): value is Uint32Array {
return TypedArrayProto_toStringTag(value) === 'Uint32Array';
}
export function isInt8Array(value: unknown): value is Int8Array {
return TypedArrayProto_toStringTag(value) === 'Int8Array';
}
export function isInt16Array(value: unknown): value is Int16Array {
return TypedArrayProto_toStringTag(value) === 'Int16Array';
}
export function isInt32Array(value: unknown): value is Int32Array {
return TypedArrayProto_toStringTag(value) === 'Int32Array';
}
export function isFloat32Array(value: unknown): value is Float32Array {
return TypedArrayProto_toStringTag(value) === 'Float32Array';
}
export function isFloat64Array(value: unknown): value is Float64Array {
return TypedArrayProto_toStringTag(value) === 'Float64Array';
}
export function isArgumentsObject(value: unknown): value is IArguments {
return Object_toString(value) === '[object Arguments]';
}
export function isDataView(value: unknown): value is DataView {
return Object_toString(value) === '[object DataView]';
}
export function isPromise(value: unknown): value is Promise<any> {
return Object_toString(value) === '[object Promise]';
}
export function isWeakSet(value: unknown): value is WeakSet<any> {
return Object_toString(value) === '[object WeakSet]';
}
export function isWeakMap(value: unknown): value is WeakMap<any, any> {
return Object_toString(value) === '[object WeakMap]';
}
export function getOwnNonIndexProperties(
val: ArrayLike<any> | ArrayBufferLike | ArrayBufferView,
showHidden: boolean,
): string[] {
if (showHidden) {
return getOwnPropertyNames(val).filter(k => !isArrayIndex(k));
} else {
return Object_keys(val).filter(k => !isArrayIndex(k));
}
}
export function getEnumerables(val: unknown, keys: PropertyKey[]): PropertyKey[] {
return keys.filter(k => propertyIsEnumerable(val, k));
}
interface IColors {
bold(str: string): string;
italic(str: string): string;
underline(str: string): string;
inverse(str: string): string;
white(str: string): string;
grey(str: string): string;
black(str: string): string;
blue(str: string): string;
cyan(str: string): string;
green(str: string): string;
magenta(str: string): string;
red(str: string): string;
yellow(str: string): string;
}
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
export const colors: Readonly<IColors> = Object_freeze(
{
bold(str: string): string {
return `\u001b[1m${str}\u001b[22m`;
},
italic(str: string): string {
return `\u001b[3m${str}\u001b[23m`;
},
underline(str: string): string {
return `\u001b[4m${str}\u001b[24m`;
},
inverse(str: string): string {
return `\u001b[7m${str}\u001b[27m`;
},
white(str: string): string {
return `\u001b[37m${str}\u001b[39m`;
},
grey(str: string): string {
return `\u001b[90m${str}\u001b[39m`;
},
black(str: string): string {
return `\u001b[30m${str}\u001b[39m`;
},
blue(str: string): string {
return `\u001b[34m${str}\u001b[39m`;
},
cyan(str: string): string {
return `\u001b[36m${str}\u001b[39m`;
},
green(str: string): string {
return `\u001b[32m${str}\u001b[39m`;
},
magenta(str: string): string {
return `\u001b[35m${str}\u001b[39m`;
},
red(str: string): string {
return `\u001b[31m${str}\u001b[39m`;
},
yellow(str: string): string {
return `\u001b[33m${str}\u001b[39m`;
},
},
);
const colorRegExp = /\u001b\[\d\d?m/g;
const strEscapeSequencesRegExp = /[\x00-\x1f\x27\x5c]/;
const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g;
const strEscapeSequencesRegExpSingle = /[\x00-\x1f\x5c]/;
const strEscapeSequencesReplacerSingle = /[\x00-\x1f\x5c]/g;
export function removeColors(str: string): string {
return str.replace(colorRegExp, '');
}
export function join(output: string[], separator: string): string {
let str = '';
if (output.length !== 0) {
let i = 0;
for (; i < output.length - 1; i++) {
str += output[i];
str += separator;
}
str += output[i];
}
return str;
}
const asciiEscape = Object_freeze([
'\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
'\\u0005', '\\u0006', '\\u0007', '\\b', '\\t',
'\\n', '\\u000b', '\\f', '\\r', '\\u000e',
'\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013',
'\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018',
'\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d',
'\\u001e', '\\u001f', '', '', '',
'', '', '', '', '\\\'', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '\\\\'
]);
function addQuotes(str: string, quotes: number): string {
if (quotes === -1) {
return `"${str}"`;
}
if (quotes === -2) {
return `\`${str}\``;
}
return `'${str}'`;
}
const escapeFn = (str: string) => asciiEscape[str.charCodeAt(0)];
export function escapeAndQuoteString(str: string): string {
let escapeTest = strEscapeSequencesRegExp;
let escapeReplace = strEscapeSequencesReplacer;
let singleQuote = 39;
if (str.includes('\'')) {
if (!str.includes('"')) {
singleQuote = -1;
} else if (!str.includes('`') && !str.includes('${')) {
singleQuote = -2;
}
if (singleQuote !== 39) {
escapeTest = strEscapeSequencesRegExpSingle;
escapeReplace = strEscapeSequencesReplacerSingle;
}
}
if (str.length < 5000 && !escapeTest.test(str))
return addQuotes(str, singleQuote);
if (str.length > 100) {
str = str.replace(escapeReplace, escapeFn);
return addQuotes(str, singleQuote);
}
let result = '';
let last = 0;
let i = 0;
for (; i < str.length; i++) {
const point = str.charCodeAt(i);
if (point === singleQuote || point === 92 || point < 32) {
if (last === i) {
result += asciiEscape[point];
} else {
result += `${str.slice(last, i)}${asciiEscape[point]}`;
}
last = i + 1;
}
}
if (last !== i) {
result += str.slice(last);
}
return addQuotes(result, singleQuote);
}
export function escapeString(str: string): string {
return str.replace(strEscapeSequencesReplacer, escapeFn);
}
export function truncate<T>(s: T, n: number): T {
if (typeof s === 'string') {
return (s.length < n ? s : s.slice(0, n)) as T & string;
} else {
return s;
}
}
export const trimFull = (function () {
const cache: Record<string, string | undefined> = {};
return function (input: string) {
let result = cache[input];
if (result === void 0) {
result = '';
const length = input.length;
let ch = 0;
for (let i = 0; i < length; ++i) {
ch = input.charCodeAt(i);
if (ch > 0x20) {
result += String.fromCharCode(ch);
}
}
cache[input] = result;
}
return result;
};
}());
type AnyFunction = (...args: unknown[]) => unknown;
type VoidFunction = (...args: unknown[]) => void;
export type ISpy<
T extends AnyFunction = AnyFunction
> = T & {
readonly calls: (readonly unknown[])[];
restore(): void;
reset(): void;
};
export function createSpy<
T extends {},
K extends keyof T,
>(instance: T, key: K): T[K] extends AnyFunction ? ISpy<T[K]> : never;
export function createSpy<
T extends {},
K extends keyof T,
F extends AnyFunction,
>(instance: T, key: K, innerFn: F): T[K] extends AnyFunction ? ISpy<T[K]> : never;
export function createSpy<
T extends {},
K extends keyof T,
>(instance: T, key: K, callThrough: true): T[K] extends AnyFunction ? ISpy<T[K]> : never;
export function createSpy<
T extends AnyFunction = VoidFunction,
>(innerFn: T): ISpy<T>;
export function createSpy(): ISpy<VoidFunction>;
export function createSpy<
T extends {} | AnyFunction = VoidFunction,
K extends keyof T = never,
F extends AnyFunction = never,
>(instanceOrInnerFn?: T, key?: K, callThroughOrInnerFn?: true | F) {
const calls: (readonly unknown[])[] = [];
function reset() {
calls.length = 0;
}
let $spy: AnyFunction;
let $restore: () => void;
if (instanceOrInnerFn === void 0) {
$spy = function spy(...args: unknown[]) {
calls.push(args);
};
$restore = noop;
} else if (key === void 0) {
$spy = function spy(...args: unknown[]) {
calls.push(args);
return (instanceOrInnerFn as AnyFunction)(...args);
};
$restore = noop;
} else {
if (!(key in instanceOrInnerFn)) {
throw new Error(`No method named '${String(key)}' exists in object of type ${Reflect.getPrototypeOf(instanceOrInnerFn)!.constructor.name}`);
}
let descriptorOwner = instanceOrInnerFn;
let descriptor = Reflect.getOwnPropertyDescriptor(descriptorOwner, key)!;
while (descriptor === void 0) {
descriptorOwner = Reflect.getPrototypeOf(descriptorOwner) as T;
descriptor = Reflect.getOwnPropertyDescriptor(descriptorOwner, key)!;
}
// Already wrapped, restore first
if (descriptor.value !== null && (typeof descriptor.value === 'object' || typeof descriptor.value === 'function') && typeof (descriptor.value as { restore?(): void }).restore === 'function') {
(descriptor.value as unknown as ISpy).restore();
descriptor = Reflect.getOwnPropertyDescriptor(descriptorOwner, key)!;
}
$restore = function restore() {
if (instanceOrInnerFn === descriptorOwner) {
Reflect.defineProperty(instanceOrInnerFn, key, descriptor);
} else {
Reflect.deleteProperty(instanceOrInnerFn, key);
}
};
if (callThroughOrInnerFn === void 0) {
$spy = function spy(...args: unknown[]) {
calls.push(args);
};
} else if (callThroughOrInnerFn === true) {
$spy = function spy(...args: unknown[]) {
calls.push(args);
return (descriptor.value as AnyFunction).apply(instanceOrInnerFn, args);
};
} else if (typeof callThroughOrInnerFn === 'function') {
$spy = function spy(...args: unknown[]) {
calls.push(args);
return callThroughOrInnerFn(...args);
};
} else {
throw new Error(`Invalid spy`);
}
Reflect.defineProperty(instanceOrInnerFn, key, {
...descriptor,
value: $spy,
});
}
Reflect.defineProperty($spy, 'calls', {
value: calls,
});
Reflect.defineProperty($spy, 'reset', {
value: reset,
});
Reflect.defineProperty($spy, 'restore', {
value: $restore,
});
return $spy;
}