Skip to content

Commit 68517e4

Browse files
refactor existing interfaces
This includes SubstituteBase, SubstituteException, Arguments, Utilities
1 parent 612d7fa commit 68517e4

File tree

3 files changed

+102
-152
lines changed

3 files changed

+102
-152
lines changed

src/SubstituteBase.ts

Lines changed: 24 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,35 @@
1-
import { inspect } from 'util';
2-
import { PropertyType, stringifyArguments, stringifyCalls, Call } from './Utilities';
1+
import { inspect, InspectOptions, types } from 'util'
2+
import { SubstituteException } from './SubstituteException'
33

4-
export class SubstituteJS {
5-
private _lastRegisteredSubstituteJSMethodOrProperty: string
6-
set lastRegisteredSubstituteJSMethodOrProperty(value: string) {
7-
this._lastRegisteredSubstituteJSMethodOrProperty = value;
4+
const instance = Symbol('Substitute:Instance')
5+
type SpecialProperty = typeof instance | typeof inspect.custom | 'then'
6+
export abstract class SubstituteBase extends Function {
7+
constructor() {
8+
super()
89
}
9-
get lastRegisteredSubstituteJSMethodOrProperty() {
10-
return this._lastRegisteredSubstituteJSMethodOrProperty ?? 'root';
11-
}
12-
[Symbol.toPrimitive]() {
13-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
14-
}
15-
[Symbol.toStringTag]() {
16-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
17-
}
18-
[Symbol.iterator]() {
19-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
20-
}
21-
[inspect.custom]() {
22-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
23-
}
24-
valueOf() {
25-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
26-
}
27-
$$typeof() {
28-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
29-
}
30-
toString() {
31-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
32-
}
33-
inspect() {
34-
return `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
35-
}
36-
length = `[class ${this.constructor.name}] -> ${this.lastRegisteredSubstituteJSMethodOrProperty}`;
37-
}
3810

39-
enum SubstituteExceptionTypes {
40-
CallCountMissMatch = 'CallCountMissMatch',
41-
PropertyNotMocked = 'PropertyNotMocked'
42-
}
11+
static instance: typeof instance = instance
4312

44-
export class SubstituteException extends Error {
45-
type: SubstituteExceptionTypes
46-
constructor(msg: string, exceptionType?: SubstituteExceptionTypes) {
47-
super(msg);
48-
Error.captureStackTrace(this, SubstituteException);
49-
this.name = new.target.name;
50-
this.type = exceptionType
13+
protected isSpecialProperty(property: PropertyKey): property is SpecialProperty {
14+
return property === SubstituteBase.instance || property === inspect.custom || property === 'then'
5115
}
5216

53-
static forCallCountMissMatch(
54-
callCount: { expected: number | null, received: number },
55-
property: { type: PropertyType, value: PropertyKey },
56-
calls: { expectedArguments: any[], received: Call[] }
57-
) {
58-
const message = 'Expected ' + (callCount.expected === null ? '1 or more' : callCount.expected) +
59-
' call' + (callCount.expected === 1 ? '' : 's') + ' to the ' + property.type + ' ' + property.value.toString() +
60-
' with ' + stringifyArguments(calls.expectedArguments) + ', but received ' + (callCount.received === 0 ? 'none' : callCount.received) +
61-
' of such call' + (callCount.received === 1 ? '' : 's') +
62-
'.\nAll calls received to ' + property.type + ' ' + property.value.toString() + ':' + stringifyCalls(calls.received);
63-
return new this(message, SubstituteExceptionTypes.CallCountMissMatch);
17+
protected evaluateSpecialProperty(property: SpecialProperty) {
18+
switch (property) {
19+
case SubstituteBase.instance:
20+
return this
21+
case inspect.custom:
22+
return this.printableForm.bind(this)
23+
case 'then':
24+
return
25+
default:
26+
throw SubstituteException.generic(`Evaluation of special property ${property} is not implemented`)
27+
}
6428
}
6529

66-
static forPropertyNotMocked(property: PropertyKey) {
67-
return new this(`There is no mock for property: ${String(property)}`, SubstituteExceptionTypes.PropertyNotMocked)
68-
}
30+
protected abstract printableForm(_: number, options: InspectOptions): string
6931

70-
static generic(message: string) {
71-
return new this(message)
32+
public [inspect.custom](...args: [_: number, options: InspectOptions]): string {
33+
return types.isProxy(this) ? this[inspect.custom](...args) : this.printableForm(...args)
7234
}
7335
}

src/SubstituteException.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { RecordedArguments } from './RecordedArguments'
2+
import { PropertyType, stringifyArguments, stringifyCalls, textModifier, plurify } from './Utilities'
3+
4+
enum SubstituteExceptionTypes {
5+
CallCountMissMatch = 'CallCountMissMatch',
6+
PropertyNotMocked = 'PropertyNotMocked'
7+
}
8+
9+
export class SubstituteException extends Error {
10+
type: SubstituteExceptionTypes
11+
12+
constructor(msg: string, exceptionType?: SubstituteExceptionTypes) {
13+
super(msg)
14+
Error.captureStackTrace(this, SubstituteException)
15+
this.name = new.target.name
16+
this.type = exceptionType
17+
}
18+
19+
static forCallCountMissMatch(
20+
count: { expected: number | null, received: number },
21+
property: { type: PropertyType, value: PropertyKey },
22+
calls: { expected: RecordedArguments, received: RecordedArguments[] }
23+
) {
24+
const propertyValue = textModifier.bold(property.value.toString())
25+
const commonMessage = `Expected ${textModifier.bold(
26+
count.expected === undefined ? '1 or more' : count.expected.toString()
27+
)} ${plurify('call', count.expected)} to the ${textModifier.italic(property.type)} ${propertyValue}`
28+
29+
const messageForMethods = property.type === PropertyType.method ? ` with ${stringifyArguments(calls.expected)}` : '' // should also apply for setters (instead of methods only)
30+
const receivedMessage = `, but received ${textModifier.bold(count.received < 1 ? 'none' : count.received.toString())} of such calls.`
31+
32+
const callTrace = calls.received.length > 0
33+
? `\nAll calls received to ${textModifier.italic(property.type)} ${propertyValue}:${stringifyCalls(calls.received)}`
34+
: ''
35+
36+
return new this(
37+
commonMessage + messageForMethods + receivedMessage + callTrace,
38+
SubstituteExceptionTypes.CallCountMissMatch
39+
)
40+
}
41+
42+
static forPropertyNotMocked(property: PropertyKey) {
43+
return new this(
44+
`There is no mock for property: ${property.toString()}`,
45+
SubstituteExceptionTypes.PropertyNotMocked
46+
)
47+
}
48+
49+
static generic(message: string) {
50+
return new this(message)
51+
}
52+
}

src/Utilities.ts

Lines changed: 26 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,40 @@
1-
import { Argument, AllArguments } from './Arguments';
2-
import * as util from 'util';
3-
4-
export type Call = any[] // list of args
1+
import { inspect } from 'util'
2+
import { RecordedArguments } from './RecordedArguments'
53

64
export enum PropertyType {
7-
method = 'method',
8-
property = 'property'
9-
}
10-
11-
export enum SubstituteMethod {
12-
received = 'received',
13-
didNotReceive = 'didNotReceive',
14-
mimicks = 'mimicks',
15-
throws = 'throws',
16-
returns = 'returns',
17-
resolves = 'resolves',
18-
rejects = 'rejects'
19-
}
20-
21-
export function isSubstituteMethod(property: PropertyKey): property is SubstituteMethod {
22-
return property === SubstituteMethod.returns ||
23-
property === SubstituteMethod.mimicks ||
24-
property === SubstituteMethod.throws ||
25-
property === SubstituteMethod.resolves ||
26-
property === SubstituteMethod.rejects;
27-
}
28-
29-
const seenObject = Symbol();
30-
31-
export function stringifyArguments(args: any[]) {
32-
args = args.map(x => util.inspect(x));
33-
return args && args.length > 0 ? 'arguments [' + args.join(', ') + ']' : 'no arguments';
34-
};
35-
36-
export function areArgumentArraysEqual(a: any[], b: any[]) {
37-
if (a.find(x => x instanceof AllArguments) || b.find(b => b instanceof AllArguments)) {
38-
return true;
39-
}
40-
41-
for (let i = 0; i < Math.max(b.length, a.length); i++) {
42-
if (!areArgumentsEqual(b[i], a[i])) {
43-
return false;
44-
}
45-
}
46-
47-
return true;
5+
method = 'method',
6+
property = 'property'
487
}
498

50-
export function stringifyCalls(calls: Call[]) {
51-
52-
if (calls.length === 0)
53-
return ' (no calls)';
54-
55-
let output = '';
56-
for (let call of calls) {
57-
output += '\n-> call with ' + (call.length ? stringifyArguments(call) : '(no arguments)')
58-
}
59-
60-
return output;
61-
};
62-
63-
export function areArgumentsEqual(a: any, b: any) {
64-
65-
if (a instanceof Argument && b instanceof Argument)
66-
return false;
9+
export type AssertionMethod = 'received' | 'didNotReceive'
6710

68-
if (a instanceof AllArguments || b instanceof AllArguments)
69-
return true;
11+
export const isAssertionMethod = (property: PropertyKey): property is AssertionMethod =>
12+
property === 'received' || property === 'didNotReceive'
7013

71-
if (a instanceof Argument)
72-
return a.matches(b);
14+
export type SubstitutionMethod = 'mimicks' | 'throws' | 'returns' | 'resolves' | 'rejects'
7315

74-
if (b instanceof Argument)
75-
return b.matches(a);
16+
export const isSubstitutionMethod = (property: PropertyKey): property is SubstitutionMethod =>
17+
property === 'mimicks' || property === 'returns' || property === 'throws' || property === 'resolves' || property === 'rejects'
7618

77-
return deepEqual(a, b);
78-
};
19+
export const stringifyArguments = (args: RecordedArguments) => textModifier.faint(
20+
args.hasNoArguments
21+
? 'no arguments'
22+
: `arguments [${args.value.map(x => inspect(x, { colors: true })).join(', ')}]`
23+
)
7924

80-
function deepEqual(realA: any, realB: any, objectReferences: object[] = []): boolean {
81-
const a = objectReferences.includes(realA) ? seenObject : realA;
82-
const b = objectReferences.includes(realB) ? seenObject : realB;
83-
const newObjectReferences = updateObjectReferences(objectReferences, a, b);
25+
export const stringifyCalls = (calls: RecordedArguments[]) => {
26+
if (calls.length === 0) return ' (no calls)'
8427

85-
if (nonNullObject(a) && nonNullObject(b)) {
86-
if (a.constructor !== b.constructor) return false;
87-
const objectAKeys = Object.keys(a);
88-
if (objectAKeys.length !== Object.keys(b).length) return false;
89-
for (const key of objectAKeys) {
90-
if (!deepEqual(a[key], b[key], newObjectReferences)) return false;
91-
}
92-
return true;
93-
}
94-
return a === b;
28+
const key = '\n-> call with '
29+
const callsDetails = calls.map(stringifyArguments)
30+
return `${key}${callsDetails.join(key)}`
9531
}
9632

97-
function updateObjectReferences(objectReferences: Array<object>, a: any, b: any) {
98-
const tempObjectReferences = [...objectReferences, nonNullObject(a) && !objectReferences.includes(a) ? a : void 0];
99-
return [...tempObjectReferences, nonNullObject(b) && !tempObjectReferences.includes(b) ? b : void 0];
33+
const baseTextModifier = (str: string, modifierStart: number, modifierEnd: number) => `\x1b[${modifierStart}m${str}\x1b[${modifierEnd}m`
34+
export const textModifier = {
35+
bold: (str: string) => baseTextModifier(str, 1, 22),
36+
faint: (str: string) => baseTextModifier(str, 2, 22),
37+
italic: (str: string) => baseTextModifier(str, 3, 23)
10038
}
10139

102-
function nonNullObject(value: any): value is { [key: string]: any } {
103-
return typeof value === 'object' && value !== null;
104-
}
40+
export const plurify = (str: string, count: number) => `${str}${count === 1 ? '' : 's'}`

0 commit comments

Comments
 (0)