diff --git a/spec/rejects.spec.ts b/spec/rejects.spec.ts index f974987..8aa1db2 100644 --- a/spec/rejects.spec.ts +++ b/spec/rejects.spec.ts @@ -22,9 +22,31 @@ test('rejects a method with arguments', async t => { await t.throwsAsync(calculator.heavyOperation(0, 1, 1, 2, 4, 5, 8), { instanceOf: Error, message: 'Wrong sequence!' }); }); -test.skip('rejects a property', async t => { +test('rejects different values in the specified order on a method', async t => { + const calculator = Substitute.for(); + calculator.heavyOperation(Arg.any('number')).rejects(new Error('Wrong!'), new Error('Wrong again!')); + + await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong!' }); + await t.throwsAsync(calculator.heavyOperation(0), { instanceOf: Error, message: 'Wrong again!' }); + await calculator.heavyOperation(0) + .then(() => t.fail('Promise.catch should have been executed')) + .catch(error => t.is(error, void 0)); +}); + +test('rejects a property', async t => { const calculator = Substitute.for(); calculator.model.rejects(new Error('No model')); await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }); }); + +test('rejects different values in the specified order on a property', async t => { + const calculator = Substitute.for(); + calculator.model.rejects(new Error('No model'), new Error('I said "no model"')); + + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'No model' }); + await t.throwsAsync(calculator.model, { instanceOf: Error, message: 'I said "no model"' }); + await calculator.model + .then(() => t.fail('Promise.catch should have been executed')) + .catch(error => t.is(error, void 0)); +}); diff --git a/spec/resolves.spec.ts b/spec/resolves.spec.ts index b2cc00d..aaf5bb7 100644 --- a/spec/resolves.spec.ts +++ b/spec/resolves.spec.ts @@ -22,9 +22,28 @@ test('resolves a method with arguments', async t => { t.is(await calculator.heavyOperation(0, 1, 1, 2, 3, 5, 8), 13); }); -test.skip('resolves a property', async t => { +test('resolves different values in the specified order on a method', async t => { + const calculator = Substitute.for(); + calculator.heavyOperation(Arg.any('number')).resolves(1, 2, 3); + + t.is(await calculator.heavyOperation(0), 1); + t.is(await calculator.heavyOperation(0), 2); + t.is(await calculator.heavyOperation(0), 3); + t.is(await calculator.heavyOperation(0), void 0); +}); + +test('resolves a property', async t => { const calculator = Substitute.for(); calculator.model.resolves('Casio FX-82'); t.is(await calculator.model, 'Casio FX-82'); }); + +test('resolves different values in the specified order on a property', async t => { + const calculator = Substitute.for(); + calculator.model.resolves('Casio FX-82', 'TI-84 Plus'); + + t.is(await calculator.model, 'Casio FX-82'); + t.is(await calculator.model, 'TI-84 Plus'); + t.is(await calculator.model, void 0); +}); diff --git a/src/Context.ts b/src/Context.ts index 50ca010..9c3d415 100644 --- a/src/Context.ts +++ b/src/Context.ts @@ -1,9 +1,9 @@ import { inspect } from 'util' -import { ContextState } from "./states/ContextState"; -import { InitialState } from "./states/InitialState"; -import { HandlerKey } from "./Substitute"; -import { Type } from "./Utilities"; -import { SetPropertyState } from "./states/SetPropertyState"; +import { ContextState } from './states/ContextState'; +import { InitialState } from './states/InitialState'; +import { HandlerKey } from './Substitute'; +import { PropertyType } from './Utilities'; +import { SetPropertyState } from './states/SetPropertyState'; import { SubstituteJS as SubstituteBase, SubstituteException } from './SubstituteBase' export class Context { @@ -23,9 +23,9 @@ export class Context { this._getState = this._initialState; this._proxy = new Proxy(SubstituteBase, { - apply: (_target, _this, args) => this.apply(_target, _this, args), - set: (_target, property, value) => (this.set(_target, property, value), true), - get: (_target, property) => this._filterAndReturnProperty(_target, property, this.get) + apply: (_target, _this, args) => this.getStateApply(_target, _this, args), + set: (_target, property, value) => (this.setStateSet(_target, property, value), true), + get: (_target, property) => this._filterAndReturnProperty(_target, property, this.getStateGet) }); this._rootProxy = new Proxy(SubstituteBase, { @@ -36,11 +36,11 @@ export class Context { this._receivedProxy = new Proxy(SubstituteBase, { apply: (_target, _this, args) => this._receivedState === void 0 ? void 0 : this._receivedState.apply(this, args), - set: (_target, property, value) => (this.set(_target, property, value), true), + set: (_target, property, value) => (this.setStateSet(_target, property, value), true), get: (_target, property) => { const state = this.initialState.getPropertyStates.find(getPropertyState => getPropertyState.property === property); if (state === void 0) return this.handleNotFoundState(property); - if (!state.functionState) + if (!state.isFunctionState) state.get(this, property); this._receivedState = state; return this.receivedProxy; @@ -48,7 +48,7 @@ export class Context { }); } - private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, defaultGet: Context['get']) { + private _filterAndReturnProperty(target: typeof SubstituteBase, property: PropertyKey, getToExecute: ContextState['get']) { switch (property) { case 'constructor': case 'valueOf': @@ -68,13 +68,13 @@ export class Context { return target.prototype[Symbol.toStringTag]; default: target.prototype.lastRegisteredSubstituteJSMethodOrProperty = property.toString() - return defaultGet.bind(this)(target, property); + return getToExecute.bind(this)(target as any, property); } } private handleNotFoundState(property: PropertyKey) { if (this.initialState.hasExpectations && this.initialState.expectedCount !== null) { - this.initialState.assertCallCountMatchesExpectations([], 0, Type.property, property, []); + this.initialState.assertCallCountMatchesExpectations([], 0, PropertyType.property, property, []); return this.receivedProxy; } throw SubstituteException.forPropertyNotMocked(property); @@ -84,15 +84,15 @@ export class Context { return this.initialState.get(this, property); } - apply(_target: any, _this: any, args: any[]) { + getStateApply(_target: any, _this: any, args: any[]) { return this._getState.apply(this, args); } - set(_target: any, property: PropertyKey, value: any) { + setStateSet(_target: any, property: PropertyKey, value: any) { return this._setState.set(this, property, value); } - get(_target: any, property: PropertyKey) { + getStateGet(_target: any, property: PropertyKey) { if (property === HandlerKey) { return this; } diff --git a/src/Substitute.ts b/src/Substitute.ts index f229837..cc1ea08 100644 --- a/src/Substitute.ts +++ b/src/Substitute.ts @@ -1,6 +1,5 @@ -import { Context } from "./Context"; -import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from "./Transformations"; -import { Get } from './Utilities' +import { Context } from './Context'; +import { ObjectSubstitute, OmitProxyMethods, DisabledSubstituteObject } from './Transformations'; export const HandlerKey = Symbol(); export const AreProxiesDisabledKey = Symbol(); @@ -18,8 +17,8 @@ export class Substitute { const thisExposedProxy = thisProxy[HandlerKey]; // Context const disableProxy = (f: K): K => { - return function() { - thisProxy[AreProxiesDisabledKey] = true; // for what reason need to do this? + return function () { + thisProxy[AreProxiesDisabledKey] = true; const returnValue = f.call(thisExposedProxy, ...arguments); thisProxy[AreProxiesDisabledKey] = false; return returnValue; @@ -28,14 +27,14 @@ export class Substitute { return new Proxy(() => { }, { apply: function (_target, _this, args) { - return disableProxy(thisExposedProxy.apply)(...arguments) + return disableProxy(thisExposedProxy.getStateApply)(...arguments) }, set: function (_target, property, value) { - return disableProxy(thisExposedProxy.set)(...arguments) + return disableProxy(thisExposedProxy.setStateSet)(...arguments) }, get: function (_target, property) { - Get(thisExposedProxy._initialState, thisExposedProxy, property) - return disableProxy(thisExposedProxy.get)(...arguments) + thisExposedProxy._initialState.handleGet(thisExposedProxy, property) + return disableProxy(thisExposedProxy.getStateGet)(...arguments) } }) as any; } diff --git a/src/SubstituteBase.ts b/src/SubstituteBase.ts index 38284fc..40e1931 100644 --- a/src/SubstituteBase.ts +++ b/src/SubstituteBase.ts @@ -1,5 +1,5 @@ import { inspect } from 'util'; -import { Type, stringifyArguments, stringifyCalls, Call } from './Utilities'; +import { PropertyType, stringifyArguments, stringifyCalls, Call } from './Utilities'; export class SubstituteJS { private _lastRegisteredSubstituteJSMethodOrProperty: string @@ -52,7 +52,7 @@ export class SubstituteException extends Error { static forCallCountMissMatch( callCount: { expected: number | null, received: number }, - property: { type: Type, value: PropertyKey }, + property: { type: PropertyType, value: PropertyKey }, calls: { expectedArguments: any[], received: Call[] } ) { const message = 'Expected ' + (callCount.expected === null ? '1 or more' : callCount.expected) + diff --git a/src/Transformations.ts b/src/Transformations.ts index c9df3fd..e6a34b0 100644 --- a/src/Transformations.ts +++ b/src/Transformations.ts @@ -46,22 +46,24 @@ export type FunctionSubstitute = export type NoArgumentFunctionSubstitute = (() => (TReturnType & NoArgumentMockObjectMixin)) export type PropertySubstitute = (TReturnType & Partial>); +type OneArgumentRequiredFunction = (requiredInput: TArgs, ...restInputs: TArgs[]) => TReturnType; + type MockObjectPromise = TReturnType extends Promise ? { - resolves: (...args: U[]) => void; - rejects: (exception: any) => void; + resolves: OneArgumentRequiredFunction; + rejects: OneArgumentRequiredFunction; } : {} type BaseMockObjectMixin = MockObjectPromise & { - returns: (...args: TReturnType[]) => void; - throws: (exception: any) => never; + returns: OneArgumentRequiredFunction; + throws: OneArgumentRequiredFunction; } type NoArgumentMockObjectMixin = BaseMockObjectMixin & { - mimicks: (func: () => TReturnType) => void; + mimicks: OneArgumentRequiredFunction<() => TReturnType, void>; } type MockObjectMixin = BaseMockObjectMixin & { - mimicks: (func: (...args: TArguments) => TReturnType) => void; + mimicks: OneArgumentRequiredFunction<(...args: TArguments) => TReturnType, void>; } export type ObjectSubstitute = ObjectSubstituteTransformation & { @@ -88,8 +90,7 @@ type ObjectSubstituteTransformation = { PropertySubstitute; } -type Omit = Pick>; +type Omit = Pick>; -// @ts-expect-error export type OmitProxyMethods = Omit; export type DisabledSubstituteObject = T extends ObjectSubstitute, infer K> ? K : never; diff --git a/src/Utilities.ts b/src/Utilities.ts index ddfcc3a..207c05d 100644 --- a/src/Utilities.ts +++ b/src/Utilities.ts @@ -6,7 +6,7 @@ import * as util from 'util'; export type Call = any[] // list of args -export enum Type { +export enum PropertyType { method = 'method', property = 'property' } @@ -22,8 +22,6 @@ export enum SubstituteMethods { } const seenObject = Symbol(); -export const Nothing = Symbol(); -export type Nothing = typeof Nothing export function stringifyArguments(args: any[]) { args = args.map(x => util.inspect(x)); @@ -35,7 +33,7 @@ export function areArgumentArraysEqual(a: any[], b: any[]) { return true; } - for (var i = 0; i < Math.max(b.length, a.length); i++) { + for (let i = 0; i < Math.max(b.length, a.length); i++) { if (!areArgumentsEqual(b[i], a[i])) { return false; } @@ -74,15 +72,16 @@ export function areArgumentsEqual(a: any, b: any) { return deepEqual(a, b); }; -function deepEqual(realA: any, realB: any, objectReferences: Object[] = []): boolean { +function deepEqual(realA: any, realB: any, objectReferences: object[] = []): boolean { const a = objectReferences.includes(realA) ? seenObject : realA; const b = objectReferences.includes(realB) ? seenObject : realB; const newObjectReferences = updateObjectReferences(objectReferences, a, b); if (nonNullObject(a) && nonNullObject(b)) { if (a.constructor !== b.constructor) return false; - if (Object.keys(a).length !== Object.keys(b).length) return false; - for (const key in a) { + const objectAKeys = Object.keys(a); + if (objectAKeys.length !== Object.keys(b).length) return false; + for (const key of objectAKeys) { if (!deepEqual(a[key], b[key], newObjectReferences)) return false; } return true; @@ -90,26 +89,11 @@ function deepEqual(realA: any, realB: any, objectReferences: Object[] = []): boo return a === b; } -function updateObjectReferences(objectReferences: Array, a: any, b: any) { +function updateObjectReferences(objectReferences: Array, a: any, b: any) { const tempObjectReferences = [...objectReferences, nonNullObject(a) && !objectReferences.includes(a) ? a : void 0]; return [...tempObjectReferences, nonNullObject(b) && !tempObjectReferences.includes(b) ? b : void 0]; } -function nonNullObject(value: any) { +function nonNullObject(value: any): value is { [key: string]: any } { return typeof value === 'object' && value !== null; -} - -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); } \ No newline at end of file diff --git a/src/states/ContextState.ts b/src/states/ContextState.ts index 78d5da5..6448fbd 100644 --- a/src/states/ContextState.ts +++ b/src/states/ContextState.ts @@ -1,11 +1,10 @@ import { Context } from "../Context"; -import { FunctionState } from "./FunctionState"; -export type PropertyKey = string|number|symbol; +export type PropertyKey = string | number | symbol; export interface ContextState { onSwitchedTo?(context: Context): void; - apply(context: Context, args: any[], matchingFunctionStates?: FunctionState[]): any; + apply(context: Context, args: any[]): any; set(context: Context, property: PropertyKey, value: any): void; get(context: Context, property: PropertyKey): any; } \ No newline at end of file diff --git a/src/states/FunctionState.ts b/src/states/FunctionState.ts deleted file mode 100644 index 69afde3..0000000 --- a/src/states/FunctionState.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { SubstituteMethods, areArgumentArraysEqual, Call, Type } from "../Utilities"; -import { GetPropertyState } from "./GetPropertyState"; -import { SubstituteException } from "../SubstituteBase"; - -interface ReturnMock { - args: Call - returnValues: any[] | Symbol // why symbol, what - returnIndex: 0 -} -interface MimickMock { - args: Call - mimickFunction: Function -} -interface ThrowMock { - args: Call - throwFunction: any -} - -export class FunctionState implements ContextState { - private returns: ReturnMock[]; - private mimicks: MimickMock[]; - private throws: ThrowMock[]; - - private _calls: Call[]; // list of lists of arguments this was called with - private _lastArgs?: Call // bit of a hack - - public get calls(): Call[] { - return this._calls - } - - public get callCount() { - return this._calls.length; - } - - public get property() { - return this._getPropertyState.property; - } - - constructor(private _getPropertyState: GetPropertyState) { - this.returns = []; - this.mimicks = []; - this._calls = []; - this.throws = []; - } - - private getCallCount(args: Call): number { - return this._calls.filter(callArgs => areArgumentArraysEqual(callArgs, args)).length; - } - - apply(context: Context, args: any[]) { - const hasExpectations = context.initialState.hasExpectations; - this._lastArgs = args - - context.initialState.assertCallCountMatchesExpectations( - this._calls, - this.getCallCount(args), - Type.method, - this.property, - args); - - if (!hasExpectations) { - this._calls.push(args) - } - - if (!hasExpectations) { - if (this.mimicks.length > 0) { - const mimicks = this.mimicks.find(mimick => areArgumentArraysEqual(mimick.args, args)) - if (mimicks !== void 0) return mimicks.mimickFunction.apply(mimicks.mimickFunction, args); - } - - if (this.throws.length > 0) { - const possibleThrow = this.throws.find(throws => areArgumentArraysEqual(throws.args, args)) - if (possibleThrow !== void 0) throw possibleThrow.throwFunction; - } - - if (!this.returns.length) - return context.proxy; - const returns = this.returns.find(r => areArgumentArraysEqual(r.args, args)) - - if (returns) { - const returnValues = returns.returnValues as any[] - if (returnValues.length === 1) { - return returnValues[0] - } - if (returnValues.length > returns.returnIndex) { - return returnValues[returns.returnIndex++] - } - return void 0 // probably a test setup error, imho throwin is more helpful -- domasx2 - //throw Error(`${String(this._getPropertyState.property)} with ${stringifyArguments(returns.args)} called ${returns.returnIndex + 1} times, but only ${returnValues.length} return values were set up`) - } - } - return context.proxy - } - - set(context: Context, property: PropertyKey, value: any) { - } - - get(context: Context, property: PropertyKey) { - if (property === 'then') - return void 0; - - if (property === SubstituteMethods.mimicks) { - return (input: Function) => { - if (!this._lastArgs) { - throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this mimicks :/') - } - this.mimicks.push({ - args: this._lastArgs, - mimickFunction: input - }) - this._calls.pop() - - context.state = context.initialState; - } - } - - if (property === SubstituteMethods.throws) { - return (input: Error | Function) => { - if (!this._lastArgs) { - throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this throw :/') - } - this.throws.push({ - args: this._lastArgs, - throwFunction: input - }); - this._calls.pop(); - context.state = context.initialState; - } - } - - if (property === SubstituteMethods.returns - || property === SubstituteMethods.resolves - || property === SubstituteMethods.rejects - ) { - return (...returnValues: any[]) => { - if (!this._lastArgs) { - throw SubstituteException.generic('Eh, there\'s a bug, no args recorded for this return :/'); - } - const returnMock: Partial = { returnIndex: 0, args: this._lastArgs }; - const returns = returnValues.length === 0 ? [void 0] : returnValues - switch (property) { - case SubstituteMethods.returns: - returnMock.returnValues = returns; - break; - case SubstituteMethods.resolves: - returnMock.returnValues = returns.map(value => Promise.resolve(value)); - break; - case SubstituteMethods.rejects: - returnMock.returnValues = returns.map(value => Promise.reject(value)); - break; - default: - throw SubstituteException.generic( - `Expected one of the following methods: "${SubstituteMethods.returns}", "${SubstituteMethods.resolves}" or "${SubstituteMethods.rejects}"` - ); - } - this.returns.push(returnMock); - this._calls.pop() - - if (this.callCount === 0) { - // var indexOfSelf = this - // ._getPropertyState - // .recordedFunctionStates - // .indexOf(this); - // this._getPropertyState - // .recordedFunctionStates - // .splice(indexOfSelf, 1); - } - - context.state = context.initialState; - }; - } - - return context.proxy; - } -} \ No newline at end of file diff --git a/src/states/GetPropertyState.ts b/src/states/GetPropertyState.ts index 20c54c6..a5abd91 100644 --- a/src/states/GetPropertyState.ts +++ b/src/states/GetPropertyState.ts @@ -1,121 +1,131 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { FunctionState } from "./FunctionState"; -import { Type, Nothing, SubstituteMethods } from "../Utilities"; -import { SubstituteException } from "../SubstituteBase"; +import { ContextState, PropertyKey } from './ContextState'; +import { Context } from '../Context'; +import { PropertyType, SubstituteMethods, Call, areArgumentArraysEqual } from '../Utilities'; +import { SubstituteException } from '../SubstituteBase'; -export class GetPropertyState implements ContextState { - private returns: any[] | Nothing; - private mimicks: Function | Nothing; - private throws: any; - - private _callCount: number; - private _functionState?: FunctionState; +interface SubstituteMock { + arguments: Call + mockValues: any[] + substituteType: SubstituteMethods +} - private get isFunction(): boolean { - return !!this._functionState - } +export class GetPropertyState implements ContextState { + private _mocks: SubstituteMock[]; + private _recordedCalls: Call[]; + private _isFunctionState: boolean; + private _lastArgs?: Call; - public get property() { + public get property(): PropertyKey { return this._property; } - public get callCount() { - return this._callCount; + get isFunctionState(): boolean { + return this._isFunctionState; } - public get functionState(): FunctionState | undefined { - return this._functionState + public get callCount(): number { + return this._recordedCalls.length; } constructor(private _property: PropertyKey) { - this.returns = Nothing; - this.mimicks = Nothing; - this.throws = Nothing; - this._callCount = 0; + this._mocks = []; + this._recordedCalls = []; + this._isFunctionState = false; } - apply(context: Context, args: any[]) { - this._callCount = 0; + private getCallCount(args: Call): number { + const callFilter = (recordedCall: Call): boolean => areArgumentArraysEqual(recordedCall, args); + return this._recordedCalls.filter(callFilter).length; + } - if (this.functionState) { - context.state = this.functionState - return this.functionState.apply(context, args); + private applySubstituteMethodLogic(substituteMethod: SubstituteMethods, mockValue: any, args?: Call) { + switch (substituteMethod) { + case SubstituteMethods.resolves: + return Promise.resolve(mockValue); + case SubstituteMethods.rejects: + return Promise.reject(mockValue); + case SubstituteMethods.returns: + return mockValue; + case SubstituteMethods.throws: + throw mockValue; + case SubstituteMethods.mimicks: + return mockValue.apply(mockValue, args); + default: + throw SubstituteException.generic(`Method ${substituteMethod} not implemented`) } - - var functionState = new FunctionState(this); - context.state = functionState; - this._functionState = functionState - - return context.apply(void 0, void 0, args); } - set(context: Context, property: PropertyKey, value: any) { } - - get(context: Context, property: PropertyKey) { + private processProperty(context: Context, args: any[], propertyType: PropertyType) { const hasExpectations = context.initialState.hasExpectations; - - if (property === 'then') - return void 0; - - if (this.isFunction) - return context.proxy; - - if (property === SubstituteMethods.mimicks) { - return (input: Function) => { - this.mimicks = input; - this._callCount--; - - context.state = context.initialState; + if (!hasExpectations) { + this._recordedCalls.push(args); + const foundSubstitute = this._mocks.find(mock => areArgumentArraysEqual(mock.arguments, args)); + if (foundSubstitute !== void 0) { + const mockValue = foundSubstitute.mockValues.length > 1 ? + foundSubstitute.mockValues.shift() : + foundSubstitute.mockValues[0]; + return this.applySubstituteMethodLogic(foundSubstitute.substituteType, mockValue, args); } } - if (property === SubstituteMethods.returns) { - if (this.returns !== Nothing) - throw SubstituteException.generic('The return value for the property ' + this._property.toString() + ' has already been set to ' + this.returns); - - return (...returns: any[]) => { - this.returns = returns; - this._callCount--; - - context.state = context.initialState; - }; - } + context.initialState.assertCallCountMatchesExpectations( + this._recordedCalls, + this.getCallCount(args), + propertyType, + this.property, + args + ); - if (property === SubstituteMethods.throws) { - return (callback: Function) => { - this.throws = callback; - this._callCount--; + return context.proxy; + } - context.state = context.initialState; - } + apply(context: Context, args: any[]) { + if (!this._isFunctionState) { + this._isFunctionState = true; + this._recordedCalls = []; } + this._lastArgs = args; + return this.processProperty(context, args, PropertyType.method); + } - if (!hasExpectations) { - this._callCount++; - - if (this.mimicks !== Nothing) - return this.mimicks.apply(this.mimicks); + set(context: Context, property: PropertyKey, value: any) { } - if (this.throws !== Nothing) - throw this.throws + private isSubstituteMethod(property: PropertyKey): property is SubstituteMethods { + return property === SubstituteMethods.returns || + property === SubstituteMethods.mimicks || + property === SubstituteMethods.throws || + property === SubstituteMethods.resolves || + property === SubstituteMethods.rejects; + } - if (this.returns !== Nothing) { - var returnsArray = this.returns as any[]; - if (returnsArray.length === 1) - return returnsArray[0]; + private sanitizeSubstituteMockInputs(mockInputs: Call): Call { + if (mockInputs.length === 0) return [undefined]; + return mockInputs.length > 1 ? + [...mockInputs, undefined] : + [...mockInputs]; + } - return returnsArray[this._callCount - 1]; + get(context: Context, property: PropertyKey) { + if (property === 'then') return void 0; + + if (this.isSubstituteMethod(property)) { + return (...inputs: Call) => { + const mockInputs = this.sanitizeSubstituteMockInputs(inputs); + const args = this._isFunctionState ? this._lastArgs : []; + if (args === void 0) + throw SubstituteException.generic('Eh, there\'s a bug, no args recorded :/'); + + this._mocks.push({ + arguments: args, + mockValues: mockInputs, + substituteType: property + }); + + this._recordedCalls.pop(); + context.state = context.initialState; } } - - context.initialState.assertCallCountMatchesExpectations( - [[]], // I'm not sure what this was supposed to mean - this.callCount, - Type.property, - this.property, - []); - - return context.proxy; + if (this._isFunctionState) return context.proxy; + return this.processProperty(context, [], PropertyType.property); } } \ No newline at end of file diff --git a/src/states/InitialState.ts b/src/states/InitialState.ts index 94cc0ed..d7648ec 100644 --- a/src/states/InitialState.ts +++ b/src/states/InitialState.ts @@ -1,10 +1,10 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { GetPropertyState } from "./GetPropertyState"; -import { SetPropertyState } from "./SetPropertyState"; -import { SubstituteMethods, stringifyArguments, stringifyCalls, Call, Type, Get } from "../Utilities"; -import { AreProxiesDisabledKey } from "../Substitute"; -import { SubstituteException } from "../SubstituteBase"; +import { ContextState, PropertyKey } from './ContextState'; +import { Context } from '../Context'; +import { GetPropertyState } from './GetPropertyState'; +import { SetPropertyState } from './SetPropertyState'; +import { SubstituteMethods, Call, PropertyType } from '../Utilities'; +import { AreProxiesDisabledKey } from '../Substitute'; +import { SubstituteException } from '../SubstituteBase'; export class InitialState implements ContextState { private recordedGetPropertyStates: Map; @@ -13,29 +13,27 @@ export class InitialState implements ContextState { private _expectedCount: number | undefined | null; private _areProxiesDisabled: boolean; - public get expectedCount() { - // expected count of calls, - // being assigned with received() method call + public get expectedCount(): number | undefined | null { return this._expectedCount; } - public get hasExpectations() { + public get hasExpectations(): boolean { return this._expectedCount !== void 0; } - public get setPropertyStates() { + public get setPropertyStates(): SetPropertyState[] { return [...this.recordedSetPropertyStates]; } - public get getPropertyStates() { + public get getPropertyStates(): GetPropertyState[] { return [...this.recordedGetPropertyStates.values()]; } - public recordGetPropertyState(property: PropertyKey, getState: GetPropertyState) { + public recordGetPropertyState(property: PropertyKey, getState: GetPropertyState): void { this.recordedGetPropertyStates.set(property, getState); } - public recordSetPropertyState(setState: SetPropertyState) { + public recordSetPropertyState(setState: SetPropertyState): void { this.recordedSetPropertyStates.push(setState); } @@ -47,24 +45,24 @@ export class InitialState implements ContextState { this._expectedCount = void 0; } - assertCallCountMatchesExpectations( - receivedCalls: Call[], // list of arguments + public assertCallCountMatchesExpectations( + receivedCalls: Call[], receivedCount: number, - type: Type, // method or property + type: PropertyType, propertyValue: PropertyKey, args: any[] - ) { + ): void | never { const expectedCount = this._expectedCount; this.clearExpectations(); if (this.doesCallCountMatchExpectations(expectedCount, receivedCount)) return; - const callCount = { expected: expectedCount, received: receivedCount } - const property = { type, value: propertyValue } - const calls = { expectedArguments: args, received: receivedCalls } + const callCount = { expected: expectedCount, received: receivedCount }; + const property = { type, value: propertyValue }; + const calls = { expectedArguments: args, received: receivedCalls }; - throw SubstituteException.forCallCountMissMatch(callCount, property, calls) + throw SubstituteException.forCallCountMissMatch(callCount, property, calls); } private doesCallCountMatchExpectations(expectedCount: number | undefined | null, actualCount: number) { @@ -85,16 +83,16 @@ export class InitialState implements ContextState { return; } - const existingSetState = this.recordedSetPropertyStates.find(x => x.arguments[0] === value);; + const existingSetState = this.recordedSetPropertyStates.find(x => x.arguments[0] === value); if (existingSetState) { return existingSetState.set(context, property, value); } const setPropertyState = new SetPropertyState(property, value); - setPropertyState.set(context, property, value); + this.recordedSetPropertyStates.push(setPropertyState); context.state = setPropertyState; - this.recordedSetPropertyStates.push(setPropertyState); + return context.setStateSet(context, property, value); } get(context: Context, property: PropertyKey) { @@ -103,7 +101,7 @@ export class InitialState implements ContextState { return this._areProxiesDisabled; case SubstituteMethods.received: return (count?: number) => { - this._expectedCount = count === void 0 ? null : count; + this._expectedCount = count ?? null; return context.receivedProxy; }; case SubstituteMethods.didNotReceive: @@ -112,7 +110,7 @@ export class InitialState implements ContextState { return context.receivedProxy; }; default: - return Get(this, context, property); + return this.handleGet(context, property); } } @@ -123,4 +121,18 @@ export class InitialState implements ContextState { onSwitchedTo() { this.clearExpectations(); } + + public handleGet(context: Context, property: PropertyKey) { + const existingGetState = this.getPropertyStates.find(state => state.property === property); + if (existingGetState !== void 0) { + context.state = existingGetState; + return context.getStateGet(void 0, property); + } + + const getState = new GetPropertyState(property); + this.recordGetPropertyState(property, getState); + + context.state = getState; + return context.getStateGet(void 0, property); + } } \ No newline at end of file diff --git a/src/states/SetPropertyState.ts b/src/states/SetPropertyState.ts index 87d1502..c7e62fd 100644 --- a/src/states/SetPropertyState.ts +++ b/src/states/SetPropertyState.ts @@ -1,7 +1,7 @@ -import { ContextState, PropertyKey } from "./ContextState"; -import { Context } from "../Context"; -import { areArgumentsEqual, Type } from "../Utilities"; -import { SubstituteException } from "../SubstituteBase"; +import { ContextState, PropertyKey } from './ContextState'; +import { Context } from '../Context'; +import { areArgumentsEqual, PropertyType } from '../Utilities'; +import { SubstituteException } from '../SubstituteBase'; export class SetPropertyState implements ContextState { private _callCount: number; @@ -21,12 +21,11 @@ export class SetPropertyState implements ContextState { constructor(private _property: PropertyKey, ...args: any[]) { this._arguments = args; - this._callCount = 0; } apply(context: Context): undefined { - throw SubstituteException.generic('Calling apply of setPropertyState is not normal behaviour, something gone wrong') + throw SubstituteException.generic('Calling apply of setPropertyState is not normal behaviour, something went wrong'); } set(context: Context, property: PropertyKey, value: any) { @@ -41,11 +40,12 @@ export class SetPropertyState implements ContextState { } context.initialState.assertCallCountMatchesExpectations( - [[]], // not sure what this was supposed to do + [[]], callCount, - Type.property, + PropertyType.property, this.property, - this.arguments); + this.arguments + ); if (!hasExpectations) { this._callCount++; @@ -53,6 +53,6 @@ export class SetPropertyState implements ContextState { } get(context: Context, property: PropertyKey): undefined { - throw SubstituteException.generic('Calling get of setPropertyState is not normal behaviour, something gone wrong') + throw SubstituteException.generic('Calling get of setPropertyState is not normal behaviour, something went wrong'); } } \ No newline at end of file