Skip to content

Commit d4b40ee

Browse files
Fix promise on properties and refactoring (#112)
* add generics correctly to Arg.is and Arg.all * rework arguments and implement .not * add some arguments test * upgrade dependencies * add Arguments spec * fix types to support ts 3.9 * add documentation for inverse matchers * require one input on substitute methods * refactor and renaming code * merge function and get states * remove skip on broken tests * linting and nullish coalescing * add typescript's Omit polyfill * add more tests to resolves and rejects
1 parent b0337d3 commit d4b40ee

File tree

12 files changed

+237
-368
lines changed

12 files changed

+237
-368
lines changed

spec/rejects.spec.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,31 @@ test('rejects a method with arguments', async t => {
2222
await t.throwsAsync(calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8), { instanceOf: Error, message: 'Wrong sequence!' });
2323
});
2424

25-
test.skip('rejects a property', async t => {
25+
test('rejects different values in the specified order on a method', async t => {
26+
const calculator = Substitute.for<Calculator>();
27+
calculator.heavyOperation(Arg.any('number')).rejects(new Error('Wrong!'), new Error('Wrong again!'));
28+
29+
await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong!' });
30+
await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong again!' });
31+
await calculator.heavyOperation(0)
32+
.then(() => t.fail('Promise.catch should have been executed'))
33+
.catch(error => t.is(error, void 0));
34+
});
35+
36+
test('rejects a property', async t => {
2637
const calculator = Substitute.for<Calculator>();
2738
calculator.model.rejects(new Error('No model'));
2839

2940
await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' });
3041
});
42+
43+
test('rejects different values in the specified order on a property', async t => {
44+
const calculator = Substitute.for<Calculator>();
45+
calculator.model.rejects(new Error('No model'), new Error('I said "no model"'));
46+
47+
await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' });
48+
await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'I said "no model"' });
49+
await calculator.model
50+
.then(() => t.fail('Promise.catch should have been executed'))
51+
.catch(error => t.is(error, void 0));
52+
});

spec/resolves.spec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,28 @@ test('resolves a method with arguments', async t => {
2222
t.is(await calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8), 13);
2323
});
2424

25-
test.skip('resolves a property', async t => {
25+
test('resolves different values in the specified order on a method', async t => {
26+
const calculator = Substitute.for<Calculator>();
27+
calculator.heavyOperation(Arg.any('number')).resolves(1, 2, 3);
28+
29+
t.is(await calculator.heavyOperation(0), 1);
30+
t.is(await calculator.heavyOperation(0), 2);
31+
t.is(await calculator.heavyOperation(0), 3);
32+
t.is(await calculator.heavyOperation(0), void 0);
33+
});
34+
35+
test('resolves a property', async t => {
2636
const calculator = Substitute.for<Calculator>();
2737
calculator.model.resolves('Casio FX-82');
2838

2939
t.is(await calculator.model, 'Casio FX-82');
3040
});
41+
42+
test('resolves different values in the specified order on a property', async t => {
43+
const calculator = Substitute.for<Calculator>();
44+
calculator.model.resolves('Casio FX-82', 'TI-84 Plus');
45+
46+
t.is(await calculator.model, 'Casio FX-82');
47+
t.is(await calculator.model, 'TI-84 Plus');
48+
t.is(await calculator.model, void 0);
49+
});

src/Context.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { inspect } from 'util'
2-
import { ContextState } from "./states/ContextState";
3-
import { InitialState } from "./states/InitialState";
4-
import { HandlerKey } from "./Substitute";
5-
import { Type } from "./Utilities";
6-
import { SetPropertyState } from "./states/SetPropertyState";
2+
import { ContextState } from './states/ContextState';
3+
import { InitialState } from './states/InitialState';
4+
import { HandlerKey } from './Substitute';
5+
import { PropertyType } from './Utilities';
6+
import { SetPropertyState } from './states/SetPropertyState';
77
import { SubstituteJS as SubstituteBase, SubstituteException } from './SubstituteBase'
88

99
export class Context {
@@ -23,9 +23,9 @@ export class Context {
2323
this._getState = this._initialState;
2424

2525
this._proxy = new Proxy(SubstituteBase, {
26-
apply: (_target, _this, args) => this.apply(_target, _this, args),
27-
set: (_target, property, value) => (this.set(_target, property, value), true),
28-
get: (_target, property) => this._filterAndReturnProperty(_target, property, this.get)
26+
apply: (_target, _this, args) => this.getStateApply(_target, _this, args),
27+
set: (_target, property, value) => (this.setStateSet(_target, property, value), true),
28+
get: (_target, property) => this._filterAndReturnProperty(_target, property, this.getStateGet)
2929
});
3030

3131
this._rootProxy = new Proxy(SubstituteBase, {
@@ -36,19 +36,19 @@ export class Context {
3636

3737
this._receivedProxy = new Proxy(SubstituteBase, {
3838
apply: (_target, _this, args) => this._receivedState === void 0 ? void 0 : this._receivedState.apply(this, args),
39-
set: (_target, property, value) => (this.set(_target, property, value), true),
39+
set: (_target, property, value) => (this.setStateSet(_target, property, value), true),
4040
get: (_target, property) => {
4141
const state = this.initialState.getPropertyStates.find(getPropertyState => getPropertyState.property === property);
4242
if (state === void 0) return this.handleNotFoundState(property);
43-
if (!state.functionState)
43+
if (!state.isFunctionState)
4444
state.get(this, property);
4545
this._receivedState = state;
4646
return this.receivedProxy;
4747
}
4848
});
4949
}
5050

51-
private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, defaultGet: Context['get']) {
51+
private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, getToExecute: ContextState['get']) {
5252
switch (property) {
5353
case 'constructor':
5454
case 'valueOf':
@@ -68,13 +68,13 @@ export class Context {
6868
return target.prototype[Symbol.toStringTag];
6969
default:
7070
target.prototype.lastRegisteredSubstituteJSMethodOrProperty = property.toString()
71-
return defaultGet.bind(this)(target, property);
71+
return getToExecute.bind(this)(target as any, property);
7272
}
7373
}
7474

7575
private handleNotFoundState(property: PropertyKey) {
7676
if (this.initialState.hasExpectations && this.initialState.expectedCount !== null) {
77-
this.initialState.assertCallCountMatchesExpectations([], 0, Type.property, property, []);
77+
this.initialState.assertCallCountMatchesExpectations([], 0, PropertyType.property, property, []);
7878
return this.receivedProxy;
7979
}
8080
throw SubstituteException.forPropertyNotMocked(property);
@@ -84,15 +84,15 @@ export class Context {
8484
return this.initialState.get(this, property);
8585
}
8686

87-
apply(_target: any, _this: any, args: any[]) {
87+
getStateApply(_target: any, _this: any, args: any[]) {
8888
return this._getState.apply(this, args);
8989
}
9090

91-
set(_target: any, property: PropertyKey, value: any) {
91+
setStateSet(_target: any, property: PropertyKey, value: any) {
9292
return this._setState.set(this, property, value);
9393
}
9494

95-
get(_target: any, property: PropertyKey) {
95+
getStateGet(_target: any, property: PropertyKey) {
9696
if (property === HandlerKey) {
9797
return this;
9898
}

src/Substitute.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { Context } from "./Context";
2-
import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from "./Transformations";
3-
import { Get } from './Utilities'
1+
import { Context } from './Context';
2+
import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from './Transformations';
43

54
export const HandlerKey = Symbol();
65
export const AreProxiesDisabledKey = Symbol();
@@ -18,8 +17,8 @@ export class Substitute {
1817
const thisExposedProxy = thisProxy[HandlerKey]; // Context
1918

2019
const disableProxy = <K extends Function>(f: K): K => {
21-
return function() {
22-
thisProxy[AreProxiesDisabledKey] = true; // for what reason need to do this?
20+
return function () {
21+
thisProxy[AreProxiesDisabledKey] = true;
2322
const returnValue = f.call(thisExposedProxy, ...arguments);
2423
thisProxy[AreProxiesDisabledKey] = false;
2524
return returnValue;
@@ -28,14 +27,14 @@ export class Substitute {
2827

2928
return new Proxy(() => { }, {
3029
apply: function (_target, _this, args) {
31-
return disableProxy(thisExposedProxy.apply)(...arguments)
30+
return disableProxy(thisExposedProxy.getStateApply)(...arguments)
3231
},
3332
set: function (_target, property, value) {
34-
return disableProxy(thisExposedProxy.set)(...arguments)
33+
return disableProxy(thisExposedProxy.setStateSet)(...arguments)
3534
},
3635
get: function (_target, property) {
37-
Get(thisExposedProxy._initialState, thisExposedProxy, property)
38-
return disableProxy(thisExposedProxy.get)(...arguments)
36+
thisExposedProxy._initialState.handleGet(thisExposedProxy, property)
37+
return disableProxy(thisExposedProxy.getStateGet)(...arguments)
3938
}
4039
}) as any;
4140
}

src/SubstituteBase.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { inspect } from 'util';
2-
import { Type, stringifyArguments, stringifyCalls, Call } from './Utilities';
2+
import { PropertyType, stringifyArguments, stringifyCalls, Call } from './Utilities';
33

44
export class SubstituteJS {
55
private _lastRegisteredSubstituteJSMethodOrProperty: string
@@ -52,7 +52,7 @@ export class SubstituteException extends Error {
5252

5353
static forCallCountMissMatch(
5454
callCount: { expected: number | null, received: number },
55-
property: { type: Type, value: PropertyKey },
55+
property: { type: PropertyType, value: PropertyKey },
5656
calls: { expectedArguments: any[], received: Call[] }
5757
) {
5858
const message = 'Expected ' + (callCount.expected === null ? '1 or more' : callCount.expected) +

src/Transformations.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,24 @@ export type FunctionSubstitute<TArguments extends any[], TReturnType> =
4646
export type NoArgumentFunctionSubstitute<TReturnType> = (() => (TReturnType & NoArgumentMockObjectMixin<TReturnType>))
4747
export type PropertySubstitute<TReturnType> = (TReturnType & Partial<NoArgumentMockObjectMixin<TReturnType>>);
4848

49+
type OneArgumentRequiredFunction<TArgs, TReturnType> = (requiredInput: TArgs, ...restInputs: TArgs[]) => TReturnType;
50+
4951
type MockObjectPromise<TReturnType> = TReturnType extends Promise<infer U> ? {
50-
resolves: (...args: U[]) => void;
51-
rejects: (exception: any) => void;
52+
resolves: OneArgumentRequiredFunction<U, void>;
53+
rejects: OneArgumentRequiredFunction<any, void>;
5254
} : {}
5355

5456
type BaseMockObjectMixin<TReturnType> = MockObjectPromise<TReturnType> & {
55-
returns: (...args: TReturnType[]) => void;
56-
throws: (exception: any) => never;
57+
returns: OneArgumentRequiredFunction<TReturnType, void>;
58+
throws: OneArgumentRequiredFunction<any, never>;
5759
}
5860

5961
type NoArgumentMockObjectMixin<TReturnType> = BaseMockObjectMixin<TReturnType> & {
60-
mimicks: (func: () => TReturnType) => void;
62+
mimicks: OneArgumentRequiredFunction<() => TReturnType, void>;
6163
}
6264

6365
type MockObjectMixin<TArguments extends any[], TReturnType> = BaseMockObjectMixin<TReturnType> & {
64-
mimicks: (func: (...args: TArguments) => TReturnType) => void;
66+
mimicks: OneArgumentRequiredFunction<(...args: TArguments) => TReturnType, void>;
6567
}
6668

6769
export type ObjectSubstitute<T extends Object, K extends Object = T> = ObjectSubstituteTransformation<T> & {
@@ -88,8 +90,7 @@ type ObjectSubstituteTransformation<T extends Object> = {
8890
PropertySubstitute<T[P]>;
8991
}
9092

91-
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
93+
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
9294

93-
// @ts-expect-error
9495
export type OmitProxyMethods<T extends any> = Omit<T, 'mimick' | 'received' | 'didNotReceive'>;
9596
export type DisabledSubstituteObject<T> = T extends ObjectSubstitute<OmitProxyMethods<infer K>, infer K> ? K : never;

src/Utilities.ts

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as util from 'util';
66

77
export type Call = any[] // list of args
88

9-
export enum Type {
9+
export enum PropertyType {
1010
method = 'method',
1111
property = 'property'
1212
}
@@ -22,8 +22,6 @@ export enum SubstituteMethods {
2222
}
2323

2424
const seenObject = Symbol();
25-
export const Nothing = Symbol();
26-
export type Nothing = typeof Nothing
2725

2826
export function stringifyArguments(args: any[]) {
2927
args = args.map(x => util.inspect(x));
@@ -35,7 +33,7 @@ export function areArgumentArraysEqual(a: any[], b: any[]) {
3533
return true;
3634
}
3735

38-
for (var i = 0; i < Math.max(b.length, a.length); i++) {
36+
for (let i = 0; i < Math.max(b.length, a.length); i++) {
3937
if (!areArgumentsEqual(b[i], a[i])) {
4038
return false;
4139
}
@@ -74,42 +72,28 @@ export function areArgumentsEqual(a: any, b: any) {
7472
return deepEqual(a, b);
7573
};
7674

77-
function deepEqual(realA: any, realB: any, objectReferences: Object[] = []): boolean {
75+
function deepEqual(realA: any, realB: any, objectReferences: object[] = []): boolean {
7876
const a = objectReferences.includes(realA) ? seenObject : realA;
7977
const b = objectReferences.includes(realB) ? seenObject : realB;
8078
const newObjectReferences = updateObjectReferences(objectReferences, a, b);
8179

8280
if (nonNullObject(a) && nonNullObject(b)) {
8381
if (a.constructor !== b.constructor) return false;
84-
if (Object.keys(a).length !== Object.keys(b).length) return false;
85-
for (const key in a) {
82+
const objectAKeys = Object.keys(a);
83+
if (objectAKeys.length !== Object.keys(b).length) return false;
84+
for (const key of objectAKeys) {
8685
if (!deepEqual(a[key], b[key], newObjectReferences)) return false;
8786
}
8887
return true;
8988
}
9089
return a === b;
9190
}
9291

93-
function updateObjectReferences(objectReferences: Array<Object>, a: any, b: any) {
92+
function updateObjectReferences(objectReferences: Array<object>, a: any, b: any) {
9493
const tempObjectReferences = [...objectReferences, nonNullObject(a) && !objectReferences.includes(a) ? a : void 0];
9594
return [...tempObjectReferences, nonNullObject(b) && !tempObjectReferences.includes(b) ? b : void 0];
9695
}
9796

98-
function nonNullObject(value: any) {
97+
function nonNullObject(value: any): value is { [key: string]: any } {
9998
return typeof value === 'object' && value !== null;
100-
}
101-
102-
export function Get(recorder: InitialState, context: Context, property: PropertyKey) {
103-
const existingGetState = recorder.getPropertyStates.find(state => state.property === property);
104-
if (existingGetState) {
105-
context.state = existingGetState;
106-
return context.get(void 0, property);
107-
}
108-
109-
const getState = new GetPropertyState(property);
110-
context.state = getState;
111-
112-
recorder.recordGetPropertyState(property, getState);
113-
114-
return context.get(void 0, property);
11599
}

src/states/ContextState.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { Context } from "../Context";
2-
import { FunctionState } from "./FunctionState";
32

4-
export type PropertyKey = string|number|symbol;
3+
export type PropertyKey = string | number | symbol;
54

65
export interface ContextState {
76
onSwitchedTo?(context: Context): void;
8-
apply(context: Context, args: any[], matchingFunctionStates?: FunctionState[]): any;
7+
apply(context: Context, args: any[]): any;
98
set(context: Context, property: PropertyKey, value: any): void;
109
get(context: Context, property: PropertyKey): any;
1110
}

0 commit comments

Comments
 (0)