Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion spec/rejects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
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>();
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>();
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));
});
21 changes: 20 additions & 1 deletion spec/resolves.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
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>();
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>();
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);
});
32 changes: 16 additions & 16 deletions src/Context.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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, {
Expand All @@ -36,19 +36,19 @@ 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;
}
});
}

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':
Expand All @@ -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);
Expand All @@ -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;
}
Expand Down
17 changes: 8 additions & 9 deletions src/Substitute.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -18,8 +17,8 @@ export class Substitute {
const thisExposedProxy = thisProxy[HandlerKey]; // Context

const disableProxy = <K extends Function>(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;
Expand All @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/SubstituteBase.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) +
Expand Down
17 changes: 9 additions & 8 deletions src/Transformations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,24 @@ export type FunctionSubstitute<TArguments extends any[], TReturnType> =
export type NoArgumentFunctionSubstitute<TReturnType> = (() => (TReturnType & NoArgumentMockObjectMixin<TReturnType>))
export type PropertySubstitute<TReturnType> = (TReturnType & Partial<NoArgumentMockObjectMixin<TReturnType>>);

type OneArgumentRequiredFunction<TArgs, TReturnType> = (requiredInput: TArgs, ...restInputs: TArgs[]) => TReturnType;

type MockObjectPromise<TReturnType> = TReturnType extends Promise<infer U> ? {
resolves: (...args: U[]) => void;
rejects: (exception: any) => void;
resolves: OneArgumentRequiredFunction<U, void>;
rejects: OneArgumentRequiredFunction<any, void>;
} : {}

type BaseMockObjectMixin<TReturnType> = MockObjectPromise<TReturnType> & {
returns: (...args: TReturnType[]) => void;
throws: (exception: any) => never;
returns: OneArgumentRequiredFunction<TReturnType, void>;
throws: OneArgumentRequiredFunction<any, never>;
}

type NoArgumentMockObjectMixin<TReturnType> = BaseMockObjectMixin<TReturnType> & {
mimicks: (func: () => TReturnType) => void;
mimicks: OneArgumentRequiredFunction<() => TReturnType, void>;
}

type MockObjectMixin<TArguments extends any[], TReturnType> = BaseMockObjectMixin<TReturnType> & {
mimicks: (func: (...args: TArguments) => TReturnType) => void;
mimicks: OneArgumentRequiredFunction<(...args: TArguments) => TReturnType, void>;
}

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

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// @ts-expect-error
export type OmitProxyMethods<T extends any> = Omit<T, 'mimick' | 'received' | 'didNotReceive'>;
export type DisabledSubstituteObject<T> = T extends ObjectSubstitute<OmitProxyMethods<infer K>, infer K> ? K : never;
32 changes: 8 additions & 24 deletions src/Utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand All @@ -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));
Expand All @@ -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;
}
Expand Down Expand Up @@ -74,42 +72,28 @@ 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;
}
return a === b;
}

function updateObjectReferences(objectReferences: Array<Object>, a: any, b: any) {
function updateObjectReferences(objectReferences: Array<object>, 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);
}
5 changes: 2 additions & 3 deletions src/states/ContextState.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading