diff options
Diffstat (limited to 'std/testing/format.ts')
-rw-r--r-- | std/testing/format.ts | 538 |
1 files changed, 538 insertions, 0 deletions
diff --git a/std/testing/format.ts b/std/testing/format.ts new file mode 100644 index 000000000..953347c27 --- /dev/null +++ b/std/testing/format.ts @@ -0,0 +1,538 @@ +// This file is ported from pretty-format@24.0.0 +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Refs = any[]; +export type Optional<T> = { [K in keyof T]?: T[K] }; + +export interface Options { + callToJSON: boolean; + escapeRegex: boolean; + escapeString: boolean; + indent: number; + maxDepth: number; + min: boolean; + printFunctionName: boolean; +} + +export interface Config { + callToJSON: boolean; + escapeRegex: boolean; + escapeString: boolean; + indent: string; + maxDepth: number; + min: boolean; + printFunctionName: boolean; + spacingInner: string; + spacingOuter: string; +} + +export type Printer = ( + val: unknown, + config: Config, + indentation: string, + depth: number, + refs: Refs, + hasCalledToJSON?: boolean +) => string; + +const toString = Object.prototype.toString; +const toISOString = Date.prototype.toISOString; +const errorToString = Error.prototype.toString; +const regExpToString = RegExp.prototype.toString; +const symbolToString = Symbol.prototype.toString; + +const DEFAULT_OPTIONS: Options = { + callToJSON: true, + escapeRegex: false, + escapeString: true, + indent: 2, + maxDepth: Infinity, + min: false, + printFunctionName: true +}; + +interface BasicValueOptions { + printFunctionName: boolean; + escapeRegex: boolean; + escapeString: boolean; +} + +/** + * Explicitly comparing typeof constructor to function avoids undefined as name + * when mock identity-obj-proxy returns the key as the value for any key. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getConstructorName = (val: new (...args: any[]) => any): string => + (typeof val.constructor === "function" && val.constructor.name) || "Object"; + +/* global window */ +/** Is val is equal to global window object? + * Works even if it does not exist :) + * */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isWindow = (val: any): val is Window => + typeof window !== "undefined" && val === window; + +const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/; + +function isToStringedArrayType(toStringed: string): boolean { + return ( + toStringed === "[object Array]" || + toStringed === "[object ArrayBuffer]" || + toStringed === "[object DataView]" || + toStringed === "[object Float32Array]" || + toStringed === "[object Float64Array]" || + toStringed === "[object Int8Array]" || + toStringed === "[object Int16Array]" || + toStringed === "[object Int32Array]" || + toStringed === "[object Uint8Array]" || + toStringed === "[object Uint8ClampedArray]" || + toStringed === "[object Uint16Array]" || + toStringed === "[object Uint32Array]" + ); +} + +function printNumber(val: number): string { + return Object.is(val, -0) ? "-0" : String(val); +} + +function printFunction(val: () => void, printFunctionName: boolean): string { + if (!printFunctionName) { + return "[Function]"; + } + return "[Function " + (val.name || "anonymous") + "]"; +} + +function printSymbol(val: symbol): string { + return symbolToString.call(val).replace(SYMBOL_REGEXP, "Symbol($1)"); +} + +function printError(val: Error): string { + return "[" + errorToString.call(val) + "]"; +} + +/** + * The first port of call for printing an object, handles most of the + * data-types in JS. + */ +function printBasicValue( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + val: any, + { printFunctionName, escapeRegex, escapeString }: BasicValueOptions +): string | null { + if (val === true || val === false) { + return String(val); + } + if (val === undefined) { + return "undefined"; + } + if (val === null) { + return "null"; + } + + const typeOf = typeof val; + + if (typeOf === "number") { + return printNumber(val); + } + if (typeOf === "string") { + if (escapeString) { + return `"${val.replace(/"|\\/g, "\\$&")}"`; + } + return `"${val}"`; + } + if (typeOf === "function") { + return printFunction(val, printFunctionName); + } + if (typeOf === "symbol") { + return printSymbol(val); + } + + const toStringed = toString.call(val); + + if (toStringed === "[object WeakMap]") { + return "WeakMap {}"; + } + if (toStringed === "[object WeakSet]") { + return "WeakSet {}"; + } + if ( + toStringed === "[object Function]" || + toStringed === "[object GeneratorFunction]" + ) { + return printFunction(val, printFunctionName); + } + if (toStringed === "[object Symbol]") { + return printSymbol(val); + } + if (toStringed === "[object Date]") { + return isNaN(+val) ? "Date { NaN }" : toISOString.call(val); + } + if (toStringed === "[object Error]") { + return printError(val); + } + if (toStringed === "[object RegExp]") { + if (escapeRegex) { + // https://github.com/benjamingr/RegExp.escape/blob/master/polyfill.js + return regExpToString.call(val).replace(/[\\^$*+?.()|[\]{}]/g, "\\$&"); + } + return regExpToString.call(val); + } + + if (val instanceof Error) { + return printError(val); + } + + return null; +} + +function printer( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + val: any, + config: Config, + indentation: string, + depth: number, + refs: Refs, + hasCalledToJSON?: boolean +): string { + const basicResult = printBasicValue(val, config); + if (basicResult !== null) { + return basicResult; + } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return printComplexValue( + val, + config, + indentation, + depth, + refs, + hasCalledToJSON + ); +} + +/** + * Return items (for example, of an array) + * with spacing, indentation, and comma + * without surrounding punctuation (for example, brackets) + */ +function printListItems( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + list: any, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: Printer +): string { + let result = ""; + + if (list.length) { + result += config.spacingOuter; + + const indentationNext = indentation + config.indent; + + for (let i = 0; i < list.length; i++) { + result += + indentationNext + + printer(list[i], config, indentationNext, depth, refs); + + if (i < list.length - 1) { + result += "," + config.spacingInner; + } else if (!config.min) { + result += ","; + } + } + + result += config.spacingOuter + indentation; + } + + return result; +} + +/** + * Return entries (for example, of a map) + * with spacing, indentation, and comma + * without surrounding punctuation (for example, braces) + */ +function printIteratorEntries( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + iterator: any, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: Printer, + // Too bad, so sad that separator for ECMAScript Map has been ' => ' + // What a distracting diff if you change a data structure to/from + // ECMAScript Object or Immutable.Map/OrderedMap which use the default. + separator = ": " +): string { + let result = ""; + let current = iterator.next(); + + if (!current.done) { + result += config.spacingOuter; + + const indentationNext = indentation + config.indent; + + while (!current.done) { + const name = printer( + current.value[0], + config, + indentationNext, + depth, + refs + ); + const value = printer( + current.value[1], + config, + indentationNext, + depth, + refs + ); + + result += indentationNext + name + separator + value; + + current = iterator.next(); + + if (!current.done) { + result += "," + config.spacingInner; + } else if (!config.min) { + result += ","; + } + } + + result += config.spacingOuter + indentation; + } + + return result; +} + +/** + * Return values (for example, of a set) + * with spacing, indentation, and comma + * without surrounding punctuation (braces or brackets) + */ +function printIteratorValues( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + iterator: Iterator<any>, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: Printer +): string { + let result = ""; + let current = iterator.next(); + + if (!current.done) { + result += config.spacingOuter; + + const indentationNext = indentation + config.indent; + + while (!current.done) { + result += + indentationNext + + printer(current.value, config, indentationNext, depth, refs); + + current = iterator.next(); + + if (!current.done) { + result += "," + config.spacingInner; + } else if (!config.min) { + result += ","; + } + } + + result += config.spacingOuter + indentation; + } + + return result; +} + +const getKeysOfEnumerableProperties = (object: {}): Array<string | symbol> => { + const keys: Array<string | symbol> = Object.keys(object).sort(); + + if (Object.getOwnPropertySymbols) { + Object.getOwnPropertySymbols(object).forEach((symbol): void => { + if (Object.getOwnPropertyDescriptor(object, symbol)!.enumerable) { + keys.push(symbol); + } + }); + } + + return keys; +}; + +/** + * Return properties of an object + * with spacing, indentation, and comma + * without surrounding punctuation (for example, braces) + */ +function printObjectProperties( + val: {}, + config: Config, + indentation: string, + depth: number, + refs: Refs, + printer: Printer +): string { + let result = ""; + const keys = getKeysOfEnumerableProperties(val); + + if (keys.length) { + result += config.spacingOuter; + + const indentationNext = indentation + config.indent; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const name = printer(key, config, indentationNext, depth, refs); + const value = printer( + val[key as keyof typeof val], + config, + indentationNext, + depth, + refs + ); + + result += indentationNext + name + ": " + value; + + if (i < keys.length - 1) { + result += "," + config.spacingInner; + } else if (!config.min) { + result += ","; + } + } + + result += config.spacingOuter + indentation; + } + + return result; +} + +/** + * Handles more complex objects ( such as objects with circular references. + * maps and sets etc ) + */ +function printComplexValue( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + val: any, + config: Config, + indentation: string, + depth: number, + refs: Refs, + hasCalledToJSON?: boolean +): string { + if (refs.indexOf(val) !== -1) { + return "[Circular]"; + } + refs = refs.slice(); + refs.push(val); + + const hitMaxDepth = ++depth > config.maxDepth; + const { min, callToJSON } = config; + + if ( + callToJSON && + !hitMaxDepth && + val.toJSON && + typeof val.toJSON === "function" && + !hasCalledToJSON + ) { + return printer(val.toJSON(), config, indentation, depth, refs, true); + } + + const toStringed = toString.call(val); + if (toStringed === "[object Arguments]") { + return hitMaxDepth + ? "[Arguments]" + : (min ? "" : "Arguments ") + + "[" + + printListItems(val, config, indentation, depth, refs, printer) + + "]"; + } + if (isToStringedArrayType(toStringed)) { + return hitMaxDepth + ? `[${val.constructor.name}]` + : (min ? "" : `${val.constructor.name} `) + + "[" + + printListItems(val, config, indentation, depth, refs, printer) + + "]"; + } + if (toStringed === "[object Map]") { + return hitMaxDepth + ? "[Map]" + : "Map {" + + printIteratorEntries( + val.entries(), + config, + indentation, + depth, + refs, + printer, + " => " + ) + + "}"; + } + if (toStringed === "[object Set]") { + return hitMaxDepth + ? "[Set]" + : "Set {" + + printIteratorValues( + val.values(), + config, + indentation, + depth, + refs, + printer + ) + + "}"; + } + + // Avoid failure to serialize global window object in jsdom test environment. + // For example, not even relevant if window is prop of React element. + return hitMaxDepth || isWindow(val) + ? "[" + getConstructorName(val) + "]" + : (min ? "" : getConstructorName(val) + " ") + + "{" + + printObjectProperties(val, config, indentation, depth, refs, printer) + + "}"; +} + +// TODO this is better done with `.padStart()` +function createIndent(indent: number): string { + return new Array(indent + 1).join(" "); +} + +const getConfig = (options: Options): Config => ({ + ...options, + indent: options.min ? "" : createIndent(options.indent), + spacingInner: options.min ? " " : "\n", + spacingOuter: options.min ? "" : "\n" +}); + +/** + * Returns a presentation string of your `val` object + * @param val any potential JavaScript object + * @param options Custom settings + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function format(val: any, options: Optional<Options> = {}): string { + const opts: Options = { + ...DEFAULT_OPTIONS, + ...options + }; + const basicResult = printBasicValue(val, opts); + if (basicResult !== null) { + return basicResult; + } + + return printComplexValue(val, getConfig(opts), "", 0, []); +} |