Skip to content
3 changes: 1 addition & 2 deletions spec/issues/51.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ test('issue 51 - All functions shares the same state', async t => {
t.is(result, 4);
try {
calculator.received().divide(1, 2);
t.fail('Expected to have failed.');
} catch (e) {
t.regex(e.toString(), /Expected 1 or more calls to the property divide with no arguments/);
t.regex(e.toString(), /Error: there are no mock for property: divide/);
}
});
48 changes: 26 additions & 22 deletions src/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,51 @@ export class Context {

private _proxy: any;
private _rootProxy: any;
private _receivedProxy: any;

private _state: ContextState;
private _receivedState: ContextState;

constructor() {
this._initialState = new InitialState();
this._state = this._initialState;

this._proxy = new Proxy(() => { }, {
apply: (_target, _this, args) => {
return this.apply(args);
},
set: (_target, property, value) => {
this.set(property, value);
return true;
},
get: (_target, property) => {
return this.get(property);
}
apply: (_target, _this, args) => this.apply(_target, _this, args),
set: (_target, property, value) => (this.set(_target, property, value), true),
get: (_target, property) => this.get(_target, property)
});

this._rootProxy = new Proxy(() => { }, {
apply: (_target, _this, args) => {
return this.initialState.apply(this, args);
},
set: (_target, property, value) => {
this.initialState.set(this, property, value);
return true;
},
apply: (_target, _this, args) => this.initialState.apply(this, args),
set: (_target, property, value) => (this.initialState.set(this, property, value), true),
get: (_target, property) => this.initialState.get(this, property)
});

this._receivedProxy = new Proxy(() => { }, {
apply: (_target, _this, args) => this._receivedState.apply(this, args),
set: (_target, property, value) => (this.set(_target, property, value), true),
get: (_target, property) => {
return this.initialState.get(this, property);
const state = this.initialState.getPropertyStates.find(getPropertyState => getPropertyState.property === property)
if (state === void 0) throw new Error(`there are no mock for property: ${String(property)}`)
this._receivedState = state
return this.receivedProxy;
}
});
}

apply(args: any[]) {
apply(_target: any, _this: any, args: any[]) {
return this._state.apply(this, args);
}

set(property: PropertyKey, value: any) {
set(_target: any, property: PropertyKey, value: any) {
return this._state.set(this, property, value);
}

get(property: PropertyKey) {
if(property === HandlerKey)
get(_target: any, property: PropertyKey) {
if(property === HandlerKey) {
return this;
}

return this._state.get(this, property);
}
Expand All @@ -64,6 +64,10 @@ export class Context {
return this._rootProxy;
}

public get receivedProxy() {
return this._receivedProxy;
}

public get initialState() {
return this._initialState;
}
Expand Down
20 changes: 14 additions & 6 deletions src/Substitute.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Context } from "./Context";
import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from "./Transformations";
import { Get } from './Utilities'

export const HandlerKey = Symbol();
export const AreProxiesDisabledKey = Symbol();
Expand All @@ -13,22 +14,29 @@ export class Substitute {
}

static disableFor<T extends ObjectSubstitute<OmitProxyMethods<any>>>(substitute: T): DisabledSubstituteObject<T> {
const thisProxy = substitute as any;
const thisExposedProxy = thisProxy[HandlerKey];
const thisProxy = substitute as any; // rootProxy
const thisExposedProxy = thisProxy[HandlerKey]; // Context

const disableProxy = <K extends Function>(f: K): K => {
return function() {
thisProxy[AreProxiesDisabledKey] = true;
thisProxy[AreProxiesDisabledKey] = true; // for what reason need to do this?
const returnValue = f.call(thisExposedProxy, ...arguments);
thisProxy[AreProxiesDisabledKey] = false;
return returnValue;
} as any;
};

return new Proxy(() => { }, {
apply: disableProxy(thisExposedProxy.apply),
set: disableProxy(thisExposedProxy.set),
get: disableProxy(thisExposedProxy.get)
apply: function (_target, _this, args) {
return disableProxy(thisExposedProxy.apply)(...arguments)
},
set: function (_target, property, value) {
return disableProxy(thisExposedProxy.set)(...arguments)
},
get: function (_target, property) {
Get(thisExposedProxy._initialState, thisExposedProxy, property)
return disableProxy(thisExposedProxy.get)(...arguments)
}
}) as any;
}
}
25 changes: 24 additions & 1 deletion src/Utilities.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { Argument, AllArguments } from "./Arguments";
import { GetPropertyState } from './states/GetPropertyState'
import { InitialState } from './states/InitialState'
import { Context } from './Context'
import util = require('util')

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

export enum Type {
method = 'method',
property = 'property'
}

export function stringifyArguments(args: any[]) {
args = args.map(x => util.inspect(x));
return args && args.length > 0 ? 'arguments [' + args.join(', ') + ']' : 'no arguments';
Expand Down Expand Up @@ -51,4 +59,19 @@ export function areArgumentsEqual(a: any, b: any) {
return b.matches(a);

return a === b;
};
};

export function Get(recorder: InitialState, context: Context, property: PropertyKey) {
const existingGetState = recorder.getPropertyStates.find(state => state.property === property);
if (existingGetState) {
context.state = existingGetState;
return context.get(void 0, property);
}

const getState = new GetPropertyState(property);
context.state = getState;

recorder.recordGetPropertyState(property, getState);

return context.get(void 0, property);
}
5 changes: 2 additions & 3 deletions src/states/FunctionState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ContextState, PropertyKey } from "./ContextState";
import { Context } from "src/Context";
import { stringifyArguments, stringifyCalls, areArgumentsEqual, areArgumentArraysEqual, Call } from "../Utilities";
import { areArgumentArraysEqual, Call, Type } from "../Utilities";
import { GetPropertyState } from "./GetPropertyState";
import { Argument, Arg } from "../Arguments";

const Nothing = Symbol()

Expand Down Expand Up @@ -48,7 +47,7 @@ export class FunctionState implements ContextState {
context.initialState.assertCallCountMatchesExpectations(
this._calls,
this.getCallCount(args),
'method',
Type.method,
this.property,
args);

Expand Down
5 changes: 3 additions & 2 deletions src/states/GetPropertyState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ContextState, PropertyKey } from "./ContextState";
import { Context } from "src/Context";
import { FunctionState } from "./FunctionState";
import { Type } from "../Utilities";

const Nothing = Symbol();

Expand Down Expand Up @@ -45,7 +46,7 @@ export class GetPropertyState implements ContextState {
context.state = functionState;
this._functionState = functionState

return context.apply(args);
return context.apply(void 0, void 0, args);
}

set(context: Context, property: PropertyKey, value: any) {
Expand Down Expand Up @@ -99,7 +100,7 @@ export class GetPropertyState implements ContextState {
context.initialState.assertCallCountMatchesExpectations(
[[]], // I'm not sure what this was supposed to mean
this.callCount,
'property',
Type.property,
this.property,
[]);

Expand Down
43 changes: 27 additions & 16 deletions src/states/InitialState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContextState, PropertyKey } from "./ContextState";
import { Context } from "src/Context";
import { GetPropertyState } from "./GetPropertyState";
import { SetPropertyState } from "./SetPropertyState";
import { stringifyArguments, stringifyCalls, Call } from "../Utilities";
import { stringifyArguments, stringifyCalls, Call, Type, Get } from "../Utilities";
import { AreProxiesDisabledKey } from "../Substitute";

export class InitialState implements ContextState {
Expand All @@ -13,6 +13,8 @@ export class InitialState implements ContextState {
private _areProxiesDisabled: boolean;

public get expectedCount() {
// expected count of calls,
// being assigned with received() method call
return this._expectedCount;
}

Expand All @@ -28,6 +30,14 @@ export class InitialState implements ContextState {
return [...this.recordedGetPropertyStates.values()];
}

public recordGetPropertyState(property: PropertyKey, getState: GetPropertyState) {
this.recordedGetPropertyStates.set(property, getState);
}

public recordSetPropertyState(setState: SetPropertyState) {
this.recordedSetPropertyStates.push(setState);
}

constructor() {
this.recordedGetPropertyStates = new Map();
this.recordedSetPropertyStates = [];
Expand All @@ -36,14 +46,26 @@ export class InitialState implements ContextState {
this._expectedCount = void 0;
}

assertCallCountMatchesExpectations(calls: Call[], callCount: number, type: string, property: PropertyKey, args: any[]) {
assertCallCountMatchesExpectations(
calls: Call[], // list of arguments
callCount: number,
type: Type, // method or property
property: PropertyKey,
args: any[]
) {
const expectedCount = this._expectedCount;

this.clearExpectations();
if(this.doesCallCountMatchExpectations(expectedCount, callCount))
return;

throw new Error('Expected ' + (expectedCount === null ? '1 or more' : expectedCount) + ' call' + (expectedCount === 1 ? '' : 's') + ' to the ' + type + ' ' + property.toString() + ' with ' + stringifyArguments(args) + ', but received ' + (callCount === 0 ? 'none' : callCount) + ' of such call' + (callCount === 1 ? '' : 's') + '.\nAll calls received to ' + type + ' ' + property.toString() + ':' + stringifyCalls(calls));
throw new Error(
'Expected ' + (expectedCount === null ? '1 or more' : expectedCount) +
' call' + (expectedCount === 1 ? '' : 's') + ' to the ' + type + ' ' + property.toString() +
' with ' + stringifyArguments(args) + ', but received ' + (callCount === 0 ? 'none' : callCount) +
' of such call' + (callCount === 1 ? '' : 's') +
'.\nAll calls received to ' + type + ' ' + property.toString() + ':' + stringifyCalls(calls)
);
}

private doesCallCountMatchExpectations(expectedCount: number|undefined|null, actualCount: number) {
Expand Down Expand Up @@ -116,22 +138,11 @@ export class InitialState implements ContextState {
if (property === 'received') {
return (count?: number) => {
this._expectedCount = count === void 0 ? null : count;
return context.proxy;
return context.receivedProxy;
};
}

const existingGetState = this.recordedGetPropertyStates.get(property);
if (existingGetState) {
context.state = existingGetState;
return context.get(property);
}

const getState = new GetPropertyState(property);
context.state = getState;

this.recordedGetPropertyStates.set(property, getState);

return context.get(property);
return Get(this, context, property)
}

private clearExpectations() {
Expand Down
8 changes: 4 additions & 4 deletions src/states/SetPropertyState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ContextState, PropertyKey } from "./ContextState";
import { Context } from "src/Context";
import { areArgumentsEqual } from "../Utilities";
import { areArgumentsEqual, Type } from "../Utilities";

const Nothing = Symbol();

Expand All @@ -27,7 +27,7 @@ export class SetPropertyState implements ContextState {
}

apply(context: Context): undefined {
return void 0;
throw new Error('Calling apply of setPropertyState is not normal behaviour, something gone wrong')
}

set(context: Context, property: PropertyKey, value: any) {
Expand All @@ -44,7 +44,7 @@ export class SetPropertyState implements ContextState {
context.initialState.assertCallCountMatchesExpectations(
[[]], // not sure what this was supposed to do
callCount,
'property',
Type.property,
this.property,
this.arguments);

Expand All @@ -54,6 +54,6 @@ export class SetPropertyState implements ContextState {
}

get(context: Context, property: PropertyKey): undefined {
return void 0;
throw new Error('Calling get of setPropertyState is not normal behaviour, something gone wrong')
}
}