From 07165bd3d95ff8b428a526425148cb6497752426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20P=C3=B6hlmann?= Date: Wed, 20 Mar 2024 00:46:57 +0100 Subject: [PATCH] refactor utilities --- src/Utilities.ts | 168 ----------------------------------- src/utilities/Constants.ts | 17 ++++ src/utilities/Guards.ts | 35 ++++++++ src/utilities/Stringify.ts | 68 ++++++++++++++ src/utilities/TextBuilder.ts | 79 ++++++++++++++++ src/utilities/index.ts | 4 + 6 files changed, 203 insertions(+), 168 deletions(-) delete mode 100644 src/Utilities.ts create mode 100644 src/utilities/Constants.ts create mode 100644 src/utilities/Guards.ts create mode 100644 src/utilities/Stringify.ts create mode 100644 src/utilities/TextBuilder.ts create mode 100644 src/utilities/index.ts diff --git a/src/Utilities.ts b/src/Utilities.ts deleted file mode 100644 index 0d621e3..0000000 --- a/src/Utilities.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { inspect } from 'util' -import { RecordedArguments } from './RecordedArguments' -import type { AssertionMethod, ConfigurationMethod, SubstituteMethod, SubstitutionMethod, SubstituteNodeModel } from './Types' - -export const PropertyType = { - Method: 'method', - Property: 'property' -} as const - -export const isAssertionMethod = (property: PropertyKey): property is AssertionMethod => - property === 'received' || property === 'didNotReceive' - -export const isConfigurationMethod = (property: PropertyKey): property is ConfigurationMethod => property === 'clearSubstitute' || property === 'mimick' - -export const isSubstitutionMethod = (property: PropertyKey): property is SubstitutionMethod => - property === 'mimicks' || property === 'returns' || property === 'throws' || property === 'resolves' || property === 'rejects' - -export const isSubstituteMethod = (property: PropertyKey): property is SubstituteMethod => - isSubstitutionMethod(property) || isConfigurationMethod(property) || isAssertionMethod(property) - -export const ClearType = { - All: 'all', - ReceivedCalls: 'receivedCalls', - SubstituteValues: 'substituteValues' -} as const - -const stringifyArguments = (args: RecordedArguments) => args.hasArguments() - ? `(${args.value.map(x => inspect(x, { colors: true })).join(', ')})` - : '' - -const matchBasedPrefix = (isMatch?: boolean) => { - switch (isMatch) { - case true: return '✔ ' - case false: return '✘ ' - default: return '' - } -} - -const matchBasedTextPartModifier = (isMatch?: boolean) => (part: TextPart) => isMatch === undefined - ? part - : isMatch - ? part.faint() - : part.bold() - -export const stringifyCall = (context: { callPath: string, expectedArguments?: RecordedArguments }) => { - return (call: SubstituteNodeModel): TextPart[] => { - const isMatch = context.expectedArguments?.match(call.recordedArguments) - const textBuilder = new TextBuilder() - .add(matchBasedPrefix(isMatch)) - .add(context.callPath) - .add(stringifyArguments(call.recordedArguments)) - if (call.stack !== undefined && isMatch !== undefined) textBuilder.newLine().add(call.stack.split('\n')[1].replace('at ', 'called at '), t => t.faint()) - return textBuilder.parts.map(matchBasedTextPartModifier(isMatch)) - } -} - -export const stringifyExpectation = (expected: { count: number | undefined, call: SubstituteNodeModel }) => { - const textBuilder = new TextBuilder() - textBuilder.add(expected.count === undefined ? '1 or more' : expected.count.toString(), t => t.bold()) - .add(' ') - .add(expected.call.propertyType, t => t.bold()) - .add(plurify(' call', expected.count), t => t.bold()) - .add(' matching ') - .addParts(...stringifyCall({ callPath: expected.call.key.toString() })(expected.call).map(t => t.bold())) - return textBuilder.parts -} - -const createKey = () => { - const textBuilder = new TextBuilder() - textBuilder.newLine().add('› ') - return textBuilder -} - -export const stringifyReceivedCalls = (callPath: string, expected: SubstituteNodeModel, received: SubstituteNodeModel[]) => { - const textBuilder = new TextBuilder() - const stringify = stringifyCall({ callPath, expectedArguments: expected.recordedArguments }) - received.forEach(receivedCall => textBuilder.addParts(...createKey().parts, ...stringify(receivedCall))) - return textBuilder.newLine().toString() -} - -const baseTextModifier = (str: string, modifierStart: number, modifierEnd: number) => `\x1b[${modifierStart}m${str}\x1b[${modifierEnd}m` -export const textModifier = { - bold: (str: string) => baseTextModifier(str, 1, 0), - faint: (str: string) => baseTextModifier(str, 2, 0), - italic: (str: string) => baseTextModifier(str, 3, 0) -} - -const plurify = (str: string, count?: number) => `${str}${count === 1 ? '' : 's'}` - -class TextPart { - private _modifiers: string[] = [] - private readonly _value: string - constructor(valueOrInstance: string | TextPart) { - if (valueOrInstance instanceof TextPart) { - this._modifiers = [...valueOrInstance._modifiers] - this._value = valueOrInstance._value - } else this._value = valueOrInstance - } - - private baseTextModifier(modifier: number) { - return `\x1b[${modifier}m` - } - - public bold() { - this._modifiers.push(this.baseTextModifier(1)) - return this - } - - public faint() { - this._modifiers.push(this.baseTextModifier(2)) - return this - } - - public italic() { - this._modifiers.push(this.baseTextModifier(3)) - return this - } - - public underline() { - this._modifiers.push(this.baseTextModifier(4)) - return this - } - - public resetFormat(): void { - this._modifiers = [] - } - - public clone(): TextPart { - return new TextPart(this) - } - - toString() { - return this._modifiers.length > 0 ? `${this._modifiers.join('')}${this._value}\x1b[0m` : this._value - } -} - -export class TextBuilder { - private readonly _parts: TextPart[] = [] - - public add(text: string, texPartCb: (textPart: TextPart) => void = () => { }): this { - const textPart = new TextPart(text) - this._parts.push(textPart) - texPartCb(textPart) - return this - } - - public addParts(...textParts: TextPart[]): this { - this._parts.push(...textParts) - return this - } - - public newLine() { - this._parts.push(new TextPart('\n')) - return this - } - - public toString() { - return this._parts.join('') - } - - public clone() { - return new TextBuilder().addParts(...this._parts.map(x => x.clone())) - } - - public get parts() { - return this._parts - } -} \ No newline at end of file diff --git a/src/utilities/Constants.ts b/src/utilities/Constants.ts new file mode 100644 index 0000000..7d367de --- /dev/null +++ b/src/utilities/Constants.ts @@ -0,0 +1,17 @@ +import { ClearType, PropertyType } from '../Types' + +type ValueToMap = { [key in T]: key } +const propertyTypes: ValueToMap = { + method: 'method', + property: 'property' +} +const clearTypes: ValueToMap = { + all: 'all', + receivedCalls: 'receivedCalls', + substituteValues: 'substituteValues' +} + +export const constants = { + PROPERTY: propertyTypes, + CLEAR: clearTypes +} \ No newline at end of file diff --git a/src/utilities/Guards.ts b/src/utilities/Guards.ts new file mode 100644 index 0000000..1030b0f --- /dev/null +++ b/src/utilities/Guards.ts @@ -0,0 +1,35 @@ +import { AssertionMethod, ClearType, ConfigurationMethod, PropertyType, SubstituteMethod, SubstitutionMethod } from '../Types' +import { constants } from './Constants' + +const isAssertionMethod = (property: PropertyKey): property is AssertionMethod => + property === 'received' || property === 'didNotReceive' +const isConfigurationMethod = (property: PropertyKey): property is ConfigurationMethod => property === 'clearSubstitute' || property === 'mimick' +const isSubstitutionMethod = (property: PropertyKey): property is SubstitutionMethod => + property === 'mimicks' || property === 'returns' || property === 'throws' || property === 'resolves' || property === 'rejects' +const isSubstituteMethod = (property: PropertyKey): property is SubstituteMethod => + isSubstitutionMethod(property) || isConfigurationMethod(property) || isAssertionMethod(property) + +const isPropertyProperty = (value: PropertyType): value is (typeof constants['PROPERTY']['property']) => value === constants.PROPERTY.property +const isPropertyMethod = (value: PropertyType): value is (typeof constants['PROPERTY']['method']) => value === constants.PROPERTY.method + +const isClearAll = (value: ClearType): value is (typeof constants['CLEAR']['all']) => value === constants.CLEAR.all +const isClearReceivedCalls = (value: ClearType): value is (typeof constants['CLEAR']['receivedCalls']) => value === constants.CLEAR.receivedCalls +const isClearSubstituteValues = (value: ClearType): value is (typeof constants['CLEAR']['substituteValues']) => value === constants.CLEAR.substituteValues + +export const method = { + assertion: isAssertionMethod, + configuration: isConfigurationMethod, + substitution: isSubstitutionMethod, + substitute: isSubstituteMethod, +} + +export const PROPERTY = { + property: isPropertyProperty, + method: isPropertyMethod +} + +export const CLEAR = { + all: isClearAll, + receivedCalls: isClearReceivedCalls, + substituteValues: isClearSubstituteValues +} diff --git a/src/utilities/Stringify.ts b/src/utilities/Stringify.ts new file mode 100644 index 0000000..77bb071 --- /dev/null +++ b/src/utilities/Stringify.ts @@ -0,0 +1,68 @@ +import { inspect } from 'node:util' +import { RecordedArguments } from '../RecordedArguments' +import { SubstituteNodeModel } from '../Types' +import { TextBuilder, TextPart } from './TextBuilder' + +const stringifyArguments = (args: RecordedArguments) => args.hasArguments() + ? `(${args.value.map(x => inspect(x, { colors: true })).join(', ')})` + : '' + +const matchBasedPrefix = (isMatch?: boolean) => { + switch (isMatch) { + case true: return '✔ ' + case false: return '✘ ' + default: return '' + } +} + +const matchBasedTextPartModifier = (isMatch?: boolean) => (part: TextPart) => isMatch === undefined + ? part + : isMatch + ? part.faint() + : part.bold() + +const stringifyCall = (context: { callPath: string, expectedArguments?: RecordedArguments }) => { + return (call: SubstituteNodeModel): TextPart[] => { + const isMatch = context.expectedArguments?.match(call.recordedArguments) + const textBuilder = new TextBuilder() + .add(matchBasedPrefix(isMatch)) + .add(context.callPath) + .add(stringifyArguments(call.recordedArguments)) + if (call.stack !== undefined && isMatch !== undefined) textBuilder.newLine().add(call.stack.split('\n')[1].replace('at ', 'called at '), t => t.faint()) + return textBuilder.parts.map(matchBasedTextPartModifier(isMatch)) + } +} + +const plurify = (str: string, count?: number) => `${str}${count === 1 ? '' : 's'}` + +const stringifyExpectation = (expected: { count: number | undefined, call: SubstituteNodeModel }) => { + const textBuilder = new TextBuilder() + textBuilder.add(expected.count === undefined ? '1 or more' : expected.count.toString(), t => t.bold()) + .add(' ') + .add(expected.call.propertyType, t => t.bold()) + .add(plurify(' call', expected.count), t => t.bold()) + .add(' matching ') + .addParts(...stringifyCall({ callPath: expected.call.key.toString() })(expected.call).map(t => t.bold())) + return textBuilder.parts +} + +const createKey = () => { + const textBuilder = new TextBuilder() + textBuilder.newLine().add('› ') + return textBuilder +} + +const stringifyReceivedCalls = (callPath: string, expected: SubstituteNodeModel, received: SubstituteNodeModel[]) => { + const textBuilder = new TextBuilder() + const stringify = stringifyCall({ callPath, expectedArguments: expected.recordedArguments }) + received.forEach(receivedCall => textBuilder.addParts(...createKey().parts, ...stringify(receivedCall))) + return textBuilder.newLine().toString() +} + +// const stringifyNode + +export const stringify = { + call: stringifyCall, + expectation: stringifyExpectation, + receivedCalls: stringifyReceivedCalls +} diff --git a/src/utilities/TextBuilder.ts b/src/utilities/TextBuilder.ts new file mode 100644 index 0000000..b0a22d4 --- /dev/null +++ b/src/utilities/TextBuilder.ts @@ -0,0 +1,79 @@ +export class TextPart { + private _modifiers: string[] = [] + private readonly _value: string + constructor(valueOrInstance: string | TextPart) { + if (valueOrInstance instanceof TextPart) { + this._modifiers = [...valueOrInstance._modifiers] + this._value = valueOrInstance._value + } else this._value = valueOrInstance + } + + private baseTextModifier(modifier: number) { + return `\x1b[${modifier}m` + } + + public bold() { + this._modifiers.push(this.baseTextModifier(1)) + return this + } + + public faint() { + this._modifiers.push(this.baseTextModifier(2)) + return this + } + + public italic() { + this._modifiers.push(this.baseTextModifier(3)) + return this + } + + public underline() { + this._modifiers.push(this.baseTextModifier(4)) + return this + } + + public resetFormat(): void { + this._modifiers = [] + } + + public clone(): TextPart { + return new TextPart(this) + } + + toString() { + return this._modifiers.length > 0 ? `${this._modifiers.join('')}${this._value}\x1b[0m` : this._value + } +} + +export class TextBuilder { + private readonly _parts: TextPart[] = [] + + public add(text: string, texPartCb: (textPart: TextPart) => void = () => { }): this { + const textPart = new TextPart(text) + this._parts.push(textPart) + texPartCb(textPart) + return this + } + + public addParts(...textParts: TextPart[]): this { + this._parts.push(...textParts) + return this + } + + public newLine() { + this._parts.push(new TextPart('\n')) + return this + } + + public toString() { + return this._parts.join('') + } + + public clone() { + return new TextBuilder().addParts(...this._parts.map(x => x.clone())) + } + + public get parts() { + return this._parts + } +} \ No newline at end of file diff --git a/src/utilities/index.ts b/src/utilities/index.ts new file mode 100644 index 0000000..6448fa3 --- /dev/null +++ b/src/utilities/index.ts @@ -0,0 +1,4 @@ +export * from './TextBuilder' +export * from './Stringify' +export * from './Constants' +export * as is from './Guards'