Skip to content

Commit fa42835

Browse files
committed
Switch to function spies
1 parent 41567b2 commit fa42835

25 files changed

Lines changed: 1523 additions & 1409 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"travis-fold": "latest",
8181
"ts-node": "latest",
8282
"tslint": "latest",
83+
"typemock": "file:scripts/typemock",
8384
"typescript": "next",
8485
"vinyl": "latest",
8586
"xml2js": "^0.4.19"

scripts/typemock/gulpfile.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ gulp.task("test", ["build"], () => gulp
2323
.src(["dist/tests/index.js"], { read: false })
2424
.pipe(mocha({ reporter: "dot" })));
2525

26-
27-
gulp.task("watch", ["test"], () => gulp.watch(["src/**/*"], ["test"]));
26+
gulp.task("watch", () => gulp.watch(["src/**/*"], ["test"]));
2827

2928
gulp.task("default", ["test"]);

scripts/typemock/src/arg.ts

Lines changed: 69 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,36 @@
22
* Represents an argument condition used during verification.
33
*/
44
export class Arg {
5-
private _condition: (value: any, args: ReadonlyArray<any>, index: number) => { valid: boolean, next?: number };
5+
private _validate: (value: any) => boolean;
66
private _message: string;
7+
private _rest: boolean;
78

8-
private constructor(condition: (value: any, args: ReadonlyArray<any>, index: number) => { valid: boolean, next?: number }, message: string) {
9-
this._condition = condition;
9+
private constructor(condition: (value: any) => boolean, message: string, rest = false) {
10+
this._validate = condition;
1011
this._message = message;
12+
this._rest = rest;
1113
}
1214

1315
/**
1416
* Allows any value.
1517
*/
1618
public static any<T = any>(): T & Arg {
17-
return <any>new Arg(() => ({ valid: true }), `any`);
19+
return <any>new Arg(() => true, `any`);
1820
}
1921

2022
/**
2123
* Allows a value that matches the specified condition.
2224
* @param match The condition used to match the value.
2325
*/
2426
public static is<T = any>(match: (value: T) => boolean): T & Arg {
25-
return <any>new Arg(value => ({ valid: match(value) }), `is`);
27+
return <any>new Arg(match, `is`);
2628
}
2729

2830
/**
2931
* Allows only a null value.
3032
*/
3133
public static null<T = any>(): T & Arg {
32-
return <any>new Arg(value => ({ valid: value === null }), `null`);
34+
return <any>new Arg(value => value === null, `null`);
3335
}
3436

3537
/**
@@ -43,7 +45,7 @@ export class Arg {
4345
* Allows only an undefined value.
4446
*/
4547
public static undefined<T = any>(): T & Arg {
46-
return <any>new Arg(value => ({ valid: value === undefined }), `undefined`);
48+
return <any>new Arg(value => value === undefined, `undefined`);
4749
}
4850

4951
/**
@@ -67,20 +69,24 @@ export class Arg {
6769
return Arg.not(Arg.nullOrUndefined());
6870
}
6971

72+
public static optional<T = any>(condition: T | T & Arg): T & Arg {
73+
return Arg.or(condition, Arg.undefined());
74+
}
75+
7076
/**
7177
* Allows any value within the provided range.
7278
* @param min The minimum value.
7379
* @param max The maximum value.
7480
*/
7581
public static between<T = any>(min: T, max: T): T & Arg {
76-
return <any>new Arg(value => ({ valid: min <= value && value <= max }), `between ${min} and ${max}`);
82+
return <any>new Arg(value => min <= value && value <= max, `between ${min} and ${max}`);
7783
}
7884

7985
/**
8086
* Allows any value in the provided array.
8187
*/
8288
public static in<T = any>(values: T[]): T & Arg {
83-
return <any>new Arg(value => ({ valid: values.indexOf(value) > -1 }), `in ${values.join(", ")}`);
89+
return <any>new Arg(value => values.indexOf(value) > -1, `in ${values.join(", ")}`);
8490
}
8591

8692
/**
@@ -94,19 +100,26 @@ export class Arg {
94100
* Allows any value that matches the provided pattern.
95101
*/
96102
public static match<T = any>(pattern: RegExp): T & Arg {
97-
return <any>new Arg(value => ({ valid: pattern.test(value) }), `matches ${pattern}`);
103+
return <any>new Arg(value => pattern.test(value), `matches ${pattern}`);
98104
}
99105

100106
public static startsWith(text: string): string & Arg {
101-
return <any>new Arg(value => ({ valid: String(value).startsWith(text) }), `starts with ${text}`);
107+
return <any>new Arg(value => typeof value === "string" && value.startsWith(text), `starts with ${text}`);
102108
}
103109

104110
public static endsWith(text: string): string & Arg {
105-
return <any>new Arg(value => ({ valid: String(value).endsWith(text) }), `ends with ${text}`);
111+
return <any>new Arg(value => typeof value === "string" && value.endsWith(text), `ends with ${text}`);
112+
}
113+
114+
public static includes(value: string): string & string[] & Arg;
115+
public static includes<T>(value: T): T[] & Arg;
116+
public static includes<T>(value: T): Arg {
117+
return new Arg(value_ => Array.isArray(value_) ? value_.includes(value) : typeof value_ === "string" && value_.includes("" + value), `contains ${value}`);
106118
}
107119

108-
public static includes(text: string): string & Arg {
109-
return <any>new Arg(value => ({ valid: String(value).includes(text) }), `contains ${text}`);
120+
public static array<T>(values: (T | T & Arg)[]): T[] & Arg {
121+
const conditions = values.map(Arg.from);
122+
return <any>new Arg(value => value.length === conditions.length && Arg.validateAll(conditions, value), `array [${conditions.join(", ")}]`);
110123
}
111124

112125
/**
@@ -142,7 +155,7 @@ export class Arg {
142155
*/
143156
public static typeof<T = any>(tag: string): T & Arg;
144157
public static typeof(tag: string): any {
145-
return <any>new Arg(value => ({ valid: typeof value === tag }), `typeof ${tag}`);
158+
return <any>new Arg(value => typeof value === tag, `typeof ${tag}`);
146159
}
147160

148161
public static string() { return this.typeof("string"); }
@@ -157,21 +170,21 @@ export class Arg {
157170
* @param type The expected constructor.
158171
*/
159172
public static instanceof<TClass extends { new (...args: any[]): object; prototype: object; }>(type: TClass): TClass["prototype"] & Arg {
160-
return <any>new Arg(value => ({ valid: value instanceof type }), `instanceof ${type.name}`);
173+
return <any>new Arg(value => value instanceof type, `instanceof ${type.name}`);
161174
}
162175

163176
/**
164177
* Allows any value that has the provided property names in its prototype chain.
165178
*/
166179
public static has<T>(...names: string[]): T & Arg {
167-
return <any>new Arg(value => ({ valid: names.filter(name => name in value).length === names.length }), `has ${names.join(", ")}`);
180+
return <any>new Arg(value => names.filter(name => name in value).length === names.length, `has ${names.join(", ")}`);
168181
}
169182

170183
/**
171184
* Allows any value that has the provided property names on itself but not its prototype chain.
172185
*/
173186
public static hasOwn<T>(...names: string[]): T & Arg {
174-
return <any>new Arg(value => ({ valid: names.filter(name => Object.prototype.hasOwnProperty.call(value, name)).length === names.length }), `hasOwn ${names.join(", ")}`);
187+
return <any>new Arg(value => names.filter(name => Object.prototype.hasOwnProperty.call(value, name)).length === names.length, `hasOwn ${names.join(", ")}`);
175188
}
176189

177190
/**
@@ -180,74 +193,51 @@ export class Arg {
180193
*/
181194
public static rest<T>(condition?: T | (T & Arg)): T & Arg {
182195
if (condition === undefined) {
183-
return <any>new Arg((_, args) => ({ valid: true, next: args.length }), `rest`);
196+
return <any>new Arg(() => true, `rest`, /*rest*/ true);
184197
}
185198

186199
const arg = Arg.from(condition);
187-
return <any>new Arg(
188-
(_, args, index) => {
189-
while (index < args.length) {
190-
const { valid, next } = Arg.validate(arg, args, index);
191-
if (!valid) return { valid: false };
192-
index = typeof next === "undefined" ? index + 1 : next;
193-
}
194-
return { valid: true, next: index };
195-
},
196-
`rest ${arg._message}`
197-
);
200+
return <any>new Arg(value => arg._validate(value), `rest ${arg._message}`, /*rest*/ true);
198201
}
199202

200203
/**
201204
* Negates a condition.
202205
*/
203206
public static not<T = any>(value: T | (T & Arg)): T & Arg {
204207
const arg = Arg.from(value);
205-
return <any>new Arg((value, args, index) => {
206-
const result = arg._condition(value, args, index);
207-
return { valid: !result.valid, next: result.next };
208-
}, `not ${arg._message}`);
208+
return <any>new Arg(value => !arg._validate(value), `not ${arg._message}`);
209209
}
210210

211211
/**
212212
* Combines conditions, where all conditions must be `true`.
213213
*/
214214
public static and<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
215215
const conditions = args.map(Arg.from);
216-
return <any>new Arg((value, args, index) => {
217-
for (const condition of conditions) {
218-
const result = condition._condition(value, args, index);
219-
if (!result.valid) return { valid: false };
220-
}
221-
return { valid: true };
222-
}, conditions.map(condition => condition._message).join(" and "));
216+
return <any>new Arg(value => conditions.every(condition => condition._validate(value)), conditions.map(condition => condition._message).join(" and "));
223217
}
224218

225219
/**
226220
* Combines conditions, where no condition may be `true`.
227221
*/
228222
public static nand<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
229-
return this.not(this.and(...args));
223+
const conditions = args.map(Arg.from);
224+
return <any>new Arg(value => !conditions.every(condition => condition._validate(value)), "not " + conditions.map(condition => condition._message).join(" and "));
230225
}
231226

232227
/**
233228
* Combines conditions, where any conditions may be `true`.
234229
*/
235230
public static or<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
236231
const conditions = args.map(Arg.from);
237-
return <any>new Arg((value, args, index) => {
238-
for (const condition of conditions) {
239-
const result = condition._condition(value, args, index);
240-
if (result.valid) return { valid: true };
241-
}
242-
return { valid: false };
243-
}, conditions.map(condition => condition._message).join(" or "));
232+
return <any>new Arg(value => conditions.some(condition => condition._validate(value)), conditions.map(condition => condition._message).join(" or "));
244233
}
245234

246235
/**
247236
* Combines conditions, where all conditions must be `true`.
248237
*/
249238
public static nor<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
250-
return this.not(this.or(...args));
239+
const conditions = args.map(Arg.from);
240+
return <any>new Arg(value => !conditions.some(condition => condition._validate(value)), "neither " + conditions.map(condition => condition._message).join(" nor "));
251241
}
252242

253243
/**
@@ -256,25 +246,40 @@ export class Arg {
256246
* @returns The condition
257247
*/
258248
public static from<T>(value: T): T & Arg {
259-
if (value instanceof Arg) {
260-
return value;
261-
}
249+
return value instanceof Arg ? value :
250+
value === undefined ? Arg.undefined() :
251+
value === null ? Arg.null() :
252+
<any>new Arg(v => is(v, value), JSON.stringify(value));
253+
}
262254

263-
return <any>new Arg(v => ({ valid: is(v, value) }), JSON.stringify(value));
255+
/**
256+
* Validates an argument against a condition
257+
* @param condition The condition to validate.
258+
* @param arg The argument to validate against the condition.
259+
*/
260+
public static validate(condition: Arg, arg: any): boolean {
261+
return condition._validate(arg);
264262
}
265263

266264
/**
267265
* Validates the arguments against the condition.
268-
* @param args The arguments for the execution
269-
* @param index The current index into the `args` array
270-
* @returns An object that specifies whether the condition is `valid` and what the `next` index should be.
271-
*/
272-
public static validate(arg: Arg, args: ReadonlyArray<any>, index: number): { valid: boolean, next?: number } {
273-
const value = index >= 0 && index < args.length ? args[index] : undefined;
274-
const { valid, next } = arg._condition(value, args, index);
275-
return valid
276-
? { valid: true, next: next === undefined ? index + 1 : next }
277-
: { valid: false };
266+
* @param conditions The conditions to validate.
267+
* @param args The arguments for the execution.
268+
*/
269+
public static validateAll(conditions: ReadonlyArray<Arg>, args: ReadonlyArray<any>): boolean {
270+
const length = Math.max(conditions.length, args.length);
271+
let conditionIndex = 0;
272+
let argIndex = 0;
273+
while (argIndex < length) {
274+
const condition = conditionIndex < conditions.length ? conditions[conditionIndex] : undefined;
275+
const arg = argIndex < args.length ? args[argIndex] : undefined;
276+
if (!condition) return false;
277+
if (argIndex >= args.length && condition._rest) return true;
278+
if (!condition._validate(arg)) return false;
279+
if (!condition._rest) conditionIndex++;
280+
argIndex++;
281+
}
282+
return true;
278283
}
279284

280285
/**

scripts/typemock/src/index.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
export { Arg } from "./arg";
22
export { Times } from "./times";
3-
export { Mock, Returns, Throws } from "./mock";
4-
export { Spy, Callable, Constructable } from "./spy";
5-
export { Stub } from "./stub";
6-
export { Timers, Timer, Timeout, Interval, Immediate, AnimationFrame } from "./timers";
3+
export { Mock, Spy, Returns, Throws, ThisArg, Callback, Fallback, Setup, Callable, Constructable } from "./mock";
4+
export { Inject } from "./inject";
5+
export { Timers, Timer, Timeout, Interval, Immediate, AnimationFrame } from "./timers";
6+
7+
import { Mock, Spy, Callable, Constructable } from "./mock";
8+
9+
/**
10+
* Creates a spy on an object or function.
11+
*/
12+
export function spy<T extends Callable | Constructable = Callable & Constructable>(): Mock<T>;
13+
/**
14+
* Creates a spy on an object or function.
15+
*/
16+
export function spy<T extends object>(target: T): Mock<T>;
17+
/**
18+
* Installs a spy on a method of an object. Use `revoke()` on the result to reset the spy.
19+
* @param object The object containing a method.
20+
* @param propertyKey The name of the method on the object.
21+
*/
22+
export function spy<T extends { [P in K]: (...args: any[]) => any }, K extends keyof T>(object: T, propertyKey: K): Spy<T, K>;
23+
export function spy<T extends { [P in K]: (...args: any[]) => any }, K extends keyof T>(object?: T, propertyKey?: K) {
24+
return object === undefined ? Mock.spy() : propertyKey === undefined ? Mock.spy(object) : Mock.spy(object, propertyKey);
25+
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Temporarily injects a value into an object property
33
*/
4-
export class Stub<T, K extends keyof T> {
4+
export class Inject<T extends object, K extends keyof T> {
55
private _target: T;
66
private _key: K;
77
private _value: any;
@@ -28,11 +28,11 @@ export class Stub<T, K extends keyof T> {
2828
return this._key;
2929
}
3030

31-
public get stubValue(): T[K] {
31+
public get injectedValue(): T[K] {
3232
return this._installed ? this.currentValue : this._value;
3333
}
3434

35-
public set stubValue(value: T[K]) {
35+
public set injectedValue(value: T[K]) {
3636
if (this._installed) {
3737
this._target[this._key] = value;
3838
}
@@ -79,8 +79,8 @@ export class Stub<T, K extends keyof T> {
7979
this._originalValue = null;
8080
}
8181

82-
public static exec<T, K extends keyof T, V>(target: T, propertyKey: K, value: T[K], action: () => V) {
83-
const stub = new Stub<T, K>(target, propertyKey, value);
82+
public static exec<T extends object, K extends keyof T, V>(target: T, propertyKey: K, value: T[K], action: () => V) {
83+
const stub = new Inject<T, K>(target, propertyKey, value);
8484
return stub.exec(action);
8585
}
8686

0 commit comments

Comments
 (0)