Switch to function spies

This commit is contained in:
Ron Buckton
2017-11-22 18:59:59 -08:00
parent 41567b2261
commit fa428356d5
25 changed files with 1528 additions and 1414 deletions

View File

@@ -23,7 +23,6 @@ gulp.task("test", ["build"], () => gulp
.src(["dist/tests/index.js"], { read: false })
.pipe(mocha({ reporter: "dot" })));
gulp.task("watch", ["test"], () => gulp.watch(["src/**/*"], ["test"]));
gulp.task("watch", () => gulp.watch(["src/**/*"], ["test"]));
gulp.task("default", ["test"]);

View File

@@ -2,19 +2,21 @@
* Represents an argument condition used during verification.
*/
export class Arg {
private _condition: (value: any, args: ReadonlyArray<any>, index: number) => { valid: boolean, next?: number };
private _validate: (value: any) => boolean;
private _message: string;
private _rest: boolean;
private constructor(condition: (value: any, args: ReadonlyArray<any>, index: number) => { valid: boolean, next?: number }, message: string) {
this._condition = condition;
private constructor(condition: (value: any) => boolean, message: string, rest = false) {
this._validate = condition;
this._message = message;
this._rest = rest;
}
/**
* Allows any value.
*/
public static any<T = any>(): T & Arg {
return <any>new Arg(() => ({ valid: true }), `any`);
return <any>new Arg(() => true, `any`);
}
/**
@@ -22,14 +24,14 @@ export class Arg {
* @param match The condition used to match the value.
*/
public static is<T = any>(match: (value: T) => boolean): T & Arg {
return <any>new Arg(value => ({ valid: match(value) }), `is`);
return <any>new Arg(match, `is`);
}
/**
* Allows only a null value.
*/
public static null<T = any>(): T & Arg {
return <any>new Arg(value => ({ valid: value === null }), `null`);
return <any>new Arg(value => value === null, `null`);
}
/**
@@ -43,7 +45,7 @@ export class Arg {
* Allows only an undefined value.
*/
public static undefined<T = any>(): T & Arg {
return <any>new Arg(value => ({ valid: value === undefined }), `undefined`);
return <any>new Arg(value => value === undefined, `undefined`);
}
/**
@@ -67,20 +69,24 @@ export class Arg {
return Arg.not(Arg.nullOrUndefined());
}
public static optional<T = any>(condition: T | T & Arg): T & Arg {
return Arg.or(condition, Arg.undefined());
}
/**
* Allows any value within the provided range.
* @param min The minimum value.
* @param max The maximum value.
*/
public static between<T = any>(min: T, max: T): T & Arg {
return <any>new Arg(value => ({ valid: min <= value && value <= max }), `between ${min} and ${max}`);
return <any>new Arg(value => min <= value && value <= max, `between ${min} and ${max}`);
}
/**
* Allows any value in the provided array.
*/
public static in<T = any>(values: T[]): T & Arg {
return <any>new Arg(value => ({ valid: values.indexOf(value) > -1 }), `in ${values.join(", ")}`);
return <any>new Arg(value => values.indexOf(value) > -1, `in ${values.join(", ")}`);
}
/**
@@ -94,19 +100,26 @@ export class Arg {
* Allows any value that matches the provided pattern.
*/
public static match<T = any>(pattern: RegExp): T & Arg {
return <any>new Arg(value => ({ valid: pattern.test(value) }), `matches ${pattern}`);
return <any>new Arg(value => pattern.test(value), `matches ${pattern}`);
}
public static startsWith(text: string): string & Arg {
return <any>new Arg(value => ({ valid: String(value).startsWith(text) }), `starts with ${text}`);
return <any>new Arg(value => typeof value === "string" && value.startsWith(text), `starts with ${text}`);
}
public static endsWith(text: string): string & Arg {
return <any>new Arg(value => ({ valid: String(value).endsWith(text) }), `ends with ${text}`);
return <any>new Arg(value => typeof value === "string" && value.endsWith(text), `ends with ${text}`);
}
public static includes(text: string): string & Arg {
return <any>new Arg(value => ({ valid: String(value).includes(text) }), `contains ${text}`);
public static includes(value: string): string & string[] & Arg;
public static includes<T>(value: T): T[] & Arg;
public static includes<T>(value: T): Arg {
return new Arg(value_ => Array.isArray(value_) ? value_.includes(value) : typeof value_ === "string" && value_.includes("" + value), `contains ${value}`);
}
public static array<T>(values: (T | T & Arg)[]): T[] & Arg {
const conditions = values.map(Arg.from);
return <any>new Arg(value => value.length === conditions.length && Arg.validateAll(conditions, value), `array [${conditions.join(", ")}]`);
}
/**
@@ -142,7 +155,7 @@ export class Arg {
*/
public static typeof<T = any>(tag: string): T & Arg;
public static typeof(tag: string): any {
return <any>new Arg(value => ({ valid: typeof value === tag }), `typeof ${tag}`);
return <any>new Arg(value => typeof value === tag, `typeof ${tag}`);
}
public static string() { return this.typeof("string"); }
@@ -157,21 +170,21 @@ export class Arg {
* @param type The expected constructor.
*/
public static instanceof<TClass extends { new (...args: any[]): object; prototype: object; }>(type: TClass): TClass["prototype"] & Arg {
return <any>new Arg(value => ({ valid: value instanceof type }), `instanceof ${type.name}`);
return <any>new Arg(value => value instanceof type, `instanceof ${type.name}`);
}
/**
* Allows any value that has the provided property names in its prototype chain.
*/
public static has<T>(...names: string[]): T & Arg {
return <any>new Arg(value => ({ valid: names.filter(name => name in value).length === names.length }), `has ${names.join(", ")}`);
return <any>new Arg(value => names.filter(name => name in value).length === names.length, `has ${names.join(", ")}`);
}
/**
* Allows any value that has the provided property names on itself but not its prototype chain.
*/
public static hasOwn<T>(...names: string[]): T & Arg {
return <any>new Arg(value => ({ valid: names.filter(name => Object.prototype.hasOwnProperty.call(value, name)).length === names.length }), `hasOwn ${names.join(", ")}`);
return <any>new Arg(value => names.filter(name => Object.prototype.hasOwnProperty.call(value, name)).length === names.length, `hasOwn ${names.join(", ")}`);
}
/**
@@ -180,21 +193,11 @@ export class Arg {
*/
public static rest<T>(condition?: T | (T & Arg)): T & Arg {
if (condition === undefined) {
return <any>new Arg((_, args) => ({ valid: true, next: args.length }), `rest`);
return <any>new Arg(() => true, `rest`, /*rest*/ true);
}
const arg = Arg.from(condition);
return <any>new Arg(
(_, args, index) => {
while (index < args.length) {
const { valid, next } = Arg.validate(arg, args, index);
if (!valid) return { valid: false };
index = typeof next === "undefined" ? index + 1 : next;
}
return { valid: true, next: index };
},
`rest ${arg._message}`
);
return <any>new Arg(value => arg._validate(value), `rest ${arg._message}`, /*rest*/ true);
}
/**
@@ -202,10 +205,7 @@ export class Arg {
*/
public static not<T = any>(value: T | (T & Arg)): T & Arg {
const arg = Arg.from(value);
return <any>new Arg((value, args, index) => {
const result = arg._condition(value, args, index);
return { valid: !result.valid, next: result.next };
}, `not ${arg._message}`);
return <any>new Arg(value => !arg._validate(value), `not ${arg._message}`);
}
/**
@@ -213,20 +213,15 @@ export class Arg {
*/
public static and<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
const conditions = args.map(Arg.from);
return <any>new Arg((value, args, index) => {
for (const condition of conditions) {
const result = condition._condition(value, args, index);
if (!result.valid) return { valid: false };
}
return { valid: true };
}, conditions.map(condition => condition._message).join(" and "));
return <any>new Arg(value => conditions.every(condition => condition._validate(value)), conditions.map(condition => condition._message).join(" and "));
}
/**
* Combines conditions, where no condition may be `true`.
*/
public static nand<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
return this.not(this.and(...args));
const conditions = args.map(Arg.from);
return <any>new Arg(value => !conditions.every(condition => condition._validate(value)), "not " + conditions.map(condition => condition._message).join(" and "));
}
/**
@@ -234,20 +229,15 @@ export class Arg {
*/
public static or<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
const conditions = args.map(Arg.from);
return <any>new Arg((value, args, index) => {
for (const condition of conditions) {
const result = condition._condition(value, args, index);
if (result.valid) return { valid: true };
}
return { valid: false };
}, conditions.map(condition => condition._message).join(" or "));
return <any>new Arg(value => conditions.some(condition => condition._validate(value)), conditions.map(condition => condition._message).join(" or "));
}
/**
* Combines conditions, where all conditions must be `true`.
*/
public static nor<T = any>(...args: ((T & Arg) | T)[]): T & Arg {
return this.not(this.or(...args));
const conditions = args.map(Arg.from);
return <any>new Arg(value => !conditions.some(condition => condition._validate(value)), "neither " + conditions.map(condition => condition._message).join(" nor "));
}
/**
@@ -256,25 +246,40 @@ export class Arg {
* @returns The condition
*/
public static from<T>(value: T): T & Arg {
if (value instanceof Arg) {
return value;
}
return value instanceof Arg ? value :
value === undefined ? Arg.undefined() :
value === null ? Arg.null() :
<any>new Arg(v => is(v, value), JSON.stringify(value));
}
return <any>new Arg(v => ({ valid: is(v, value) }), JSON.stringify(value));
/**
* Validates an argument against a condition
* @param condition The condition to validate.
* @param arg The argument to validate against the condition.
*/
public static validate(condition: Arg, arg: any): boolean {
return condition._validate(arg);
}
/**
* Validates the arguments against the condition.
* @param args The arguments for the execution
* @param index The current index into the `args` array
* @returns An object that specifies whether the condition is `valid` and what the `next` index should be.
* @param conditions The conditions to validate.
* @param args The arguments for the execution.
*/
public static validate(arg: Arg, args: ReadonlyArray<any>, index: number): { valid: boolean, next?: number } {
const value = index >= 0 && index < args.length ? args[index] : undefined;
const { valid, next } = arg._condition(value, args, index);
return valid
? { valid: true, next: next === undefined ? index + 1 : next }
: { valid: false };
public static validateAll(conditions: ReadonlyArray<Arg>, args: ReadonlyArray<any>): boolean {
const length = Math.max(conditions.length, args.length);
let conditionIndex = 0;
let argIndex = 0;
while (argIndex < length) {
const condition = conditionIndex < conditions.length ? conditions[conditionIndex] : undefined;
const arg = argIndex < args.length ? args[argIndex] : undefined;
if (!condition) return false;
if (argIndex >= args.length && condition._rest) return true;
if (!condition._validate(arg)) return false;
if (!condition._rest) conditionIndex++;
argIndex++;
}
return true;
}
/**

View File

@@ -1,6 +1,25 @@
export { Arg } from "./arg";
export { Times } from "./times";
export { Mock, Returns, Throws } from "./mock";
export { Spy, Callable, Constructable } from "./spy";
export { Stub } from "./stub";
export { Timers, Timer, Timeout, Interval, Immediate, AnimationFrame } from "./timers";
export { Mock, Spy, Returns, Throws, ThisArg, Callback, Fallback, Setup, Callable, Constructable } from "./mock";
export { Inject } from "./inject";
export { Timers, Timer, Timeout, Interval, Immediate, AnimationFrame } from "./timers";
import { Mock, Spy, Callable, Constructable } from "./mock";
/**
* Creates a spy on an object or function.
*/
export function spy<T extends Callable | Constructable = Callable & Constructable>(): Mock<T>;
/**
* Creates a spy on an object or function.
*/
export function spy<T extends object>(target: T): Mock<T>;
/**
* Installs a spy on a method of an object. Use `revoke()` on the result to reset the spy.
* @param object The object containing a method.
* @param propertyKey The name of the method on the object.
*/
export function spy<T extends { [P in K]: (...args: any[]) => any }, K extends keyof T>(object: T, propertyKey: K): Spy<T, K>;
export function spy<T extends { [P in K]: (...args: any[]) => any }, K extends keyof T>(object?: T, propertyKey?: K) {
return object === undefined ? Mock.spy() : propertyKey === undefined ? Mock.spy(object) : Mock.spy(object, propertyKey);
}

View File

@@ -1,7 +1,7 @@
/**
* Temporarily injects a value into an object property
*/
export class Stub<T, K extends keyof T> {
export class Inject<T extends object, K extends keyof T> {
private _target: T;
private _key: K;
private _value: any;
@@ -28,11 +28,11 @@ export class Stub<T, K extends keyof T> {
return this._key;
}
public get stubValue(): T[K] {
public get injectedValue(): T[K] {
return this._installed ? this.currentValue : this._value;
}
public set stubValue(value: T[K]) {
public set injectedValue(value: T[K]) {
if (this._installed) {
this._target[this._key] = value;
}
@@ -79,8 +79,8 @@ export class Stub<T, K extends keyof T> {
this._originalValue = null;
}
public static exec<T, K extends keyof T, V>(target: T, propertyKey: K, value: T[K], action: () => V) {
const stub = new Stub<T, K>(target, propertyKey, value);
public static exec<T extends object, K extends keyof T, V>(target: T, propertyKey: K, value: T[K], action: () => V) {
const stub = new Inject<T, K>(target, propertyKey, value);
return stub.exec(action);
}

View File

@@ -1,28 +1,53 @@
import { Times } from "./times";
import { Arg } from "./arg";
import { Inject } from "./inject";
const weakHandler = new WeakMap<object, MockHandler<object>>();
const weakMock = new WeakMap<object, Mock<object>>();
function noop() {}
const empty = {};
function getHandler(value: object) {
return weakHandler.get(value);
export type Callable = (...args: any[]) => any;
export type Constructable = new (...args: any[]) => any;
export interface ThisArg {
this: any;
}
export interface Returns<U> {
returns: U;
return: U;
}
export interface Fallback {
fallback: true;
}
export interface Throws {
throws: any;
throw: any;
}
export interface Callback {
callback: Callable;
}
export type Setup<U> =
| Returns<U> & (ThisArg & Callback | ThisArg | Callback)
| Returns<U>
| Throws & (ThisArg & Callback | ThisArg | Callback)
| Throws
| Fallback & (ThisArg & Callback | ThisArg | Callback)
| Fallback
| ThisArg & Callback
| ThisArg
| Callback;
/**
* A mock version of another oject
*/
export class Mock<T extends object> {
private _target: T;
private _handler = new MockHandler<T>();
private _handler: MockHandler<T>;
private _proxy: T;
private _revoke: () => void;
@@ -32,13 +57,16 @@ export class Mock<T extends object> {
* @param setups Optional setups to use
*/
constructor(target: T = <T>{}, setups?: Partial<T>) {
this._target = target;
this._handler = typeof target === "function"
? new MockFunctionHandler<T & (Callable | Constructable)>()
: new MockHandler<T>();
const { proxy, revoke } = Proxy.revocable<T>(this._target, this._handler);
const { proxy, revoke } = Proxy.revocable<T>(target, this._handler);
this._proxy = proxy;
this._revoke = revoke;
weakHandler.set(proxy, this._handler);
weakMock.set(proxy, this);
if (setups) {
this.setup(setups);
@@ -48,24 +76,66 @@ export class Mock<T extends object> {
/**
* Gets the mock version of the target
*/
public get value(): T {
public get proxy(): T {
return this._proxy;
}
/**
* Creates an empty Mock object.
*/
public static object<T extends object = any>() {
return new Mock(<T>{});
}
/**
* Creates an empty Mock function.
*/
public static function<T extends Callable | Constructable = Callable & Constructable>() {
return new Mock(<T>function () {});
}
/**
* Creates a function spy.
*/
public static spy<T extends Callable | Constructable = Callable & Constructable>(): Mock<T>;
/**
* Creates a spy on an object or function.
*/
public static spy<T extends object>(target: T): Mock<T>;
/**
* Installs a spy on a method of an object. Use `revoke()` on the result to reset the spy.
* @param object The object containing a method.
* @param propertyKey The name of the method on the object.
*/
public static spy<T extends { [P in K]: (...args: any[]) => any }, K extends keyof T>(object: T, propertyKey: K): Spy<T, K>;
public static spy<T extends { [P in K]: (...args: any[]) => any }, K extends keyof T>(object?: T, propertyKey?: K) {
return object !== undefined && propertyKey !== undefined
? new Spy(object, propertyKey)
: new Mock(object || noop);
}
/**
* Gets the mock for an object.
* @param target The target.
*/
public static from<T extends object>(target: T) {
return <Mock<T> | undefined>weakMock.get(target);
}
/**
* Performs setup of the mock object, overriding the target object's functionality with that provided by the setup
* @param callback A function used to set up a method result.
* @param result An object used to describe the result of the method.
* @returns This mock instance.
*/
public setup<U = any>(callback: (value: T) => U, result?: Returns<U> | Throws): Mock<T>;
public setup<U = any>(callback: (value: T) => U, result?: Setup<U>): this;
/**
* Performs setup of the mock object, overriding the target object's functionality with that provided by the setup
* @param setups An object whose members are used instead of the target object.
* @returns This mock instance.
*/
public setup(setups: Partial<T>): Mock<T>;
public setup<U>(setup: Partial<T> | ((value: T) => U), result?: Returns<U> | Throws): Mock<T> {
public setup(setups: Partial<T>): this;
public setup<U>(setup: Partial<T> | ((value: T) => U), result?: Setup<U>): this {
if (typeof setup === "function") {
this._handler.setupCall(setup, result);
}
@@ -75,187 +145,259 @@ export class Mock<T extends object> {
return this;
}
/**
* Performs verification that a specific action occurred at least once.
* @param callback A callback that simulates the expected action.
* @param message An optional message to use if verification fails.
* @returns This mock instance.
*/
public verify<U>(callback: (value: T) => U, message?: string): this;
/**
* Performs verification that a specific action occurred.
* @param callback A callback that simulates the expected action.
* @param times The number of times the action should have occurred.
* @param message An optional message to use if verification fails.
* @returns This mock instance.
*/
public verify<U>(callback: (value: T) => U, times: Times, message?: string): this;
/**
* Performs verification that a specific action occurred.
* @param callback A callback that simulates the expected action.
* @param times The number of times the action should have occurred.
* @returns This mock instance.
*/
public verify(callback: (value: T) => any, times: Times): Mock<T> {
public verify<U>(callback: (value: T) => U, times?: Times | string, message?: string): this {
if (typeof times === "string") {
message = times;
times = undefined;
}
if (times === undefined) {
times = Times.atLeastOnce();
}
this._handler.verify(callback, times);
return this;
}
public revoke() {
weakMock.delete(this._proxy);
weakHandler.delete(this._proxy);
this._handler.revoke();
this._revoke();
}
}
class Setup {
public recording: Recording;
public result: Partial<Returns<any> & Throws> | undefined;
export class Spy<T extends { [P in K]: (...args: any[]) => any }, K extends keyof T> extends Mock<T[K]> {
private _spy: Inject<any, any> | undefined;
constructor (recording: Recording, result?: Returns<any> | Throws) {
this.recording = recording;
this.result = result;
constructor(target: T, propertyKey: K) {
super(target[propertyKey]);
this._spy = new Inject(target, propertyKey, this.proxy);
this._spy.install();
}
public static evaluate(setups: ReadonlyArray<Setup> | undefined, trap: string, args: any[], newTarget?: any) {
if (setups) {
for (let i = setups.length - 1; i >= 0; i--) {
const setup = setups[i];
if (setup.recording.trap === trap &&
setup.recording.newTarget === newTarget &&
setup.matchArguments(args)) {
return setup.getResult();
}
}
public get installed() {
return this._spy ? this._spy.installed : false;
}
public install() {
if (!this._spy) throw new Error("Cannot install a revoked spy.");
this._spy.install();
return this;
}
public uninstall() {
if (this._spy) this._spy.uninstall();
return this;
}
public revoke() {
if (this._spy) {
this._spy.uninstall();
this._spy = undefined;
}
throw new Error("No matching setups.");
}
public matchArguments(args: any[]) {
return this.recording.matchArguments(args);
}
public getResult() {
if (this.result) {
if (this.result.throws) {
throw this.result.throws;
}
return this.result.returns;
}
return undefined;
super.revoke();
}
}
class Recording {
public static readonly noThisArg = {};
public readonly trap: string;
public readonly name: PropertyKey | undefined;
public readonly args: ReadonlyArray<any>;
public readonly thisArg: any;
public readonly argArray: ReadonlyArray<any>;
public readonly newTarget: any;
public readonly result: Partial<Returns<any> & Throws & Fallback> | undefined;
public readonly callback: Callable | undefined;
private _thisCondition: Arg | undefined;
private _newTargetCondition: Arg | undefined;
private _conditions: ReadonlyArray<Arg> | undefined;
constructor(trap: string, name: PropertyKey | undefined, args: ReadonlyArray<any>, newTarget?: any) {
constructor(trap: string, name: PropertyKey | undefined, thisArg: any, argArray: ReadonlyArray<any>, newTarget: any, result: Partial<Returns<any> & Throws & Fallback> | undefined, callback: Callable | undefined) {
this.trap = trap;
this.name = name;
this.args = args || [];
this.thisArg = thisArg;
this.argArray = argArray || [];
this.newTarget = newTarget;
this.result = result;
this.callback = callback;
}
public get conditions() {
return this._conditions || (this._conditions = this.args.map(Arg.from));
public get thisCondition() {
return this._thisCondition || (this._thisCondition = this.thisArg === Recording.noThisArg ? Arg.any() : Arg.from(this.thisArg));
}
public get newTargetCondition() {
return this._newTargetCondition || (this._newTargetCondition = Arg.from(this.newTarget));
}
public get argConditions() {
return this._conditions || (this._conditions = this.argArray.map(Arg.from));
}
public get kind() {
switch (this.trap) {
case "apply": return "function";
case "construct": return "function";
case "invoke": return "method";
case "get": return "property";
case "set": return "property";
}
}
public static select(setups: ReadonlyArray<Recording>, kind: Recording["kind"], name: PropertyKey | undefined) {
return setups.filter(setup => setup.kind === kind && setup.name === name);
}
public static evaluate(setups: ReadonlyArray<Recording> | undefined, trap: string, name: PropertyKey | undefined, thisArg: any, argArray: any[], newTarget: any, fallback: () => any) {
if (setups && setups.length > 0) {
for (const setup of setups) {
if (setup.match(trap, name, thisArg, argArray, newTarget)) {
const callback = setup.callback;
if (callback) {
Reflect.apply(callback, thisArg, argArray);
}
const result = setup.getResult(fallback);
return trap === "set" ? true : result;
}
}
return trap === "set" ? false : undefined;
}
return fallback();
}
public toString(): string {
return `${this.trap} ${this.name || ""}(${this.conditions.join(", ")})${this.newTarget ? ` [${this.newTarget.name}]` : ``}`;
return `${this.trap} ${this.name || ""}(${this.argConditions.join(", ")})${this.newTarget ? ` [${this.newTarget.name}]` : ``}`;
}
public match(trap: string, name: PropertyKey | undefined, thisArg: any, argArray: any, newTarget: any) {
return this.trap === trap
&& this.name === name
&& Arg.validate(this.thisCondition, thisArg)
&& Arg.validateAll(this.argConditions, argArray)
&& Arg.validate(this.newTargetCondition, newTarget);
}
public matchRecording(recording: Recording) {
if (recording.trap !== this.trap ||
recording.name !== this.name ||
recording.newTarget !== this.newTarget) {
return false;
}
return this.matchArguments(recording.args);
return this.match(recording.trap, recording.name, recording.thisArg, recording.argArray, recording.newTarget)
&& this.matchResult(recording.result);
}
public matchArguments(args: ReadonlyArray<any>) {
let argi = 0;
while (argi < this.conditions.length) {
const condition = this.conditions[argi];
const { valid, next } = Arg.validate(condition, args, argi);
if (!valid) {
return false;
}
argi = typeof next === "number" ? next : argi + 1;
}
if (argi < args.length) {
return false;
}
return true;
private matchThisArg(thisArg: any) {
return thisArg === Recording.noThisArg
|| Arg.validate(this.thisCondition, thisArg);
}
private matchResult(result: Partial<Returns<any> & Throws> | undefined) {
return !this.result
|| this.result.return === (result && result.return)
&& this.result.throw === (result && result.throw);
}
private getResult(fallback: () => any) {
if (hasOwn(this.result, "throw")) throw this.result.throw;
if (hasOwn(this.result, "return")) return this.result.return;
if (hasOwn(this.result, "fallback")) return this.result.fallback ? fallback() : undefined;
return undefined;
}
}
class MockHandler<T extends object> implements ProxyHandler<T> {
private readonly overrides = Object.create(null);
private readonly recordings: Recording[] = [];
private readonly selfSetups: Setup[] = [];
private readonly memberSetups = new Map<PropertyKey, Setup[]>();
private readonly methodTargets = new WeakMap<Function, Function>();
private readonly methodProxies = new Map<PropertyKey, Function>();
private readonly methodRevocations = new Set<() => void>();
protected readonly overrides = Object.create(null);
protected readonly recordings: Recording[] = [];
protected readonly setups: Recording[] = [];
protected readonly methodTargets = new WeakMap<Function, Function>();
protected readonly methodProxies = new Map<PropertyKey, Function>();
protected readonly methodRevocations = new Set<() => void>();
constructor() {
}
public apply(target: T | Function, thisArg: any, argArray: any[]): any {
if (typeof target === "function") {
this.recordings.push(new Recording("apply", undefined, argArray));
return this.selfSetups.length > 0
? Setup.evaluate(this.selfSetups, "apply", argArray)
: Reflect.apply(target, thisArg, argArray);
public get(target: T, name: PropertyKey, receiver: any = target): any {
const setups = Recording.select(this.setups, "property", name);
const result: Partial<Returns<any> & Throws> = {};
const recording = new Recording("get", name, target, [], /*newTarget*/ undefined, result, /*callback*/ undefined);
this.recordings.push(recording);
try {
const value = Recording.evaluate(setups, "get", name, receiver, [], /*newTarget*/ undefined,
() => Reflect.get(this.getTarget(target, name), name, receiver));
return typeof value === "function" ? this.getMethod(name, value) : value;
}
return undefined;
}
public construct(target: T | Function, argArray: any[], newTarget?: any): any {
if (typeof target === "function") {
this.recordings.push(new Recording("construct", undefined, argArray, newTarget));
return this.selfSetups.length > 0
? Setup.evaluate(this.selfSetups, "construct", argArray, newTarget)
: Reflect.construct(target, argArray, newTarget);
catch (e) {
throw result.throw = e;
}
return undefined;
}
public get(target: T, name: PropertyKey, receiver: any): any {
this.recordings.push(new Recording("get", name, []));
const value = Reflect.get(this.getTarget(target, name), name, receiver);
return typeof value === "function" ? this.getMethod(name, value) : value;
}
public set(target: T, name: PropertyKey, value: any, receiver: any): boolean {
this.recordings.push(new Recording("set", name, [value]));
public set(target: T, name: PropertyKey, value: any, receiver: any = target): boolean {
if (typeof value === "function" && this.methodTargets.has(value)) {
value = this.methodTargets.get(value);
}
return Reflect.set(this.getTarget(target, name), name, value, receiver);
const setups = Recording.select(this.setups, "property", name);
const result: Partial<Returns<any> & Throws> = {};
const recording = new Recording("set", name, target, [value], /*newTarget*/ undefined, result, /*callback*/ undefined);
this.recordings.push(recording);
try {
const success = Recording.evaluate(setups, "set", name, receiver, [value], /*newTarget*/ undefined,
() => Reflect.set(this.getTarget(target, name), name, value, receiver));
result.return = undefined;
return success;
}
catch (e) {
throw result.throw = e;
}
}
public invoke(proxy: T, name: PropertyKey, method: Function, argArray: any[]): any {
this.recordings.push(new Recording("invoke", name, argArray));
return Reflect.apply(method, proxy, argArray);
const setups = Recording.select(this.setups, "method", name);
const result: Partial<Returns<any> & Throws> = {};
const recording = new Recording("invoke", name, proxy, argArray, /*newTarget*/ undefined, result, /*callback*/ undefined);
this.recordings.push(recording);
try {
return Recording.evaluate(setups, "invoke", name, proxy, argArray, /*newTarget*/ undefined,
() => Reflect.apply(method, proxy, argArray));
}
catch (e) {
throw result.throw = e;
}
}
public setupCall(callback: (value: any) => any, result: Returns<any> | Throws | undefined) {
const recording = capture(callback);
if (recording.name === undefined) {
this.selfSetups.push(new Setup(recording, result));
public setupCall(callback: (value: any) => any, result: Setup<any> | undefined) {
const recording = this.capture(callback, result);
const existing = this.setups.find(setup => setup.name === recording.name);
if (existing) {
if (existing.kind !== recording.kind) {
throw new Error(`Cannot mix method and property setups for the same member name.`);
}
}
else {
let setups = this.memberSetups.get(recording.name);
if (!setups) {
this.memberSetups.set(recording.name, setups = []);
if (recording.trap === "invoke") {
this.defineMethod(recording.name);
}
else {
this.defineAccessor(recording.name);
}
else if (recording.name !== undefined) {
if (recording.kind === "method") {
this.defineMethod(recording.name);
}
else {
if ((setups[0].recording.trap === "invoke") !== (recording.trap === "invoke")) {
throw new Error(`Cannot mix method and acessor setups for the same property.`);
}
else if (recording.kind === "property") {
this.defineAccessor(recording.name);
}
}
setups.push(new Setup(recording, result));
}
this.setups.push(recording);
}
public setupMembers(setup: object) {
@@ -270,8 +412,8 @@ class MockHandler<T extends object> implements ProxyHandler<T> {
}
}
public verify(callback: (value: T) => any, times: Times): void {
const expectation = capture(callback);
public verify<U>(callback: (value: T) => U, times: Times, message?: string): void {
const expectation = this.capture(callback, /*result*/ undefined);
let count: number = 0;
for (const recording of this.recordings) {
@@ -280,7 +422,7 @@ class MockHandler<T extends object> implements ProxyHandler<T> {
}
}
times.check(count, `An error occured when verifying expectation: ${expectation}`);
times.check(count, message || `An error occured when verifying expectation: ${expectation}`);
}
public getTarget(target: T, name: PropertyKey) {
@@ -307,28 +449,80 @@ class MockHandler<T extends object> implements ProxyHandler<T> {
}
}
protected capture<U>(callback: (value: T) => U, result: Setup<any> | undefined) {
return this.captureCore(<T>empty, new CapturingHandler<T, U>(result), callback);
}
protected captureCore<T extends object, U>(target: T, handler: CapturingHandler<T, U>, callback: (value: T) => U): Recording {
const { proxy, revoke } = Proxy.revocable<T>(target, handler);
try {
callback(proxy);
if (!handler.recording) {
throw new Error("Nothing was captured.");
}
return handler.recording;
}
finally {
revoke();
}
}
private defineMethod(name: PropertyKey) {
const setups = this.memberSetups;
const setups = this.setups;
this.setupMembers({
[name](...args: any[]) {
return Setup.evaluate(setups.get(name), "invoke", args);
[name](...argArray: any[]) {
return Recording.evaluate(setups, "invoke", name, this, argArray, /*newTarget*/ undefined, noop);
}
});
}
private defineAccessor(name: PropertyKey) {
const setups = this.memberSetups;
const setups = this.setups;
this.setupMembers({
get [name]() {
return Setup.evaluate(setups.get(name), "get", []);
return Recording.evaluate(setups, "get", name, this, [], /*newTarget*/ undefined, noop);
},
set [name](value: any) {
Setup.evaluate(setups.get(name), "set", [value]);
Recording.evaluate(setups, "set", name, this, [value], /*newTarget*/ undefined, noop);
}
});
}
}
class MockFunctionHandler<T extends Callable | Constructable> extends MockHandler<T> {
public apply(target: T, thisArg: any, argArray: any[]): any {
const setups = Recording.select(this.setups, "function", /*name*/ undefined);
const result: Partial<Returns<any> & Throws> = {};
const recording = new Recording("apply", /*name*/ undefined, thisArg, argArray, /*newTarget*/ undefined, result, /*callback*/ undefined);
this.recordings.push(recording);
try {
return Recording.evaluate(setups, "apply", /*name*/ undefined, thisArg, argArray, /*newTarget*/ undefined,
() => Reflect.apply(target, thisArg, argArray));
}
catch (e) {
throw result.throw = e;
}
}
public construct(target: T, argArray: any[], newTarget?: any): any {
const setups = Recording.select(this.setups, "function", /*name*/ undefined);
const result: Partial<Returns<any> & Throws> = {};
const recording = new Recording("construct", /*name*/ undefined, /*thisArg*/ undefined, argArray, newTarget, result, /*callback*/ undefined);
this.recordings.push(recording);
try {
return Recording.evaluate(setups, "construct", /*name*/ undefined, /*thisArg*/ undefined, argArray, newTarget,
() => Reflect.construct(target, argArray, newTarget));
}
catch (e) {
throw result.throw = e;
}
}
protected capture<U>(callback: (value: T) => U, result: Returns<any> & ThisArg | Returns<any> | Throws & ThisArg | Throws | ThisArg | undefined) {
return this.captureCore(<T>noop, new CapturingFunctionHandler<T, U>(result), callback);
}
}
class MethodHandler {
public name: PropertyKey;
@@ -337,58 +531,53 @@ class MethodHandler {
}
public apply(target: Function, thisArgument: any, argumentsList: any[]): any {
const handler = getHandler(thisArgument);
const handler = weakHandler.get(thisArgument);
return handler
? handler.invoke(thisArgument, this.name, target, argumentsList)
: Reflect.apply(target, thisArgument, argumentsList);
}
}
class CapturingHandler {
class CapturingHandler<T extends object, U> implements ProxyHandler<T> {
public recording: Recording | undefined;
private _name: PropertyKey;
private _method: Function;
protected readonly callback: Callable | undefined;
protected readonly thisArg: any;
protected readonly result: Returns<U> | Throws | Fallback | undefined;
constructor() {
this._method = (...args: any[]) => {
this.recording = new Recording("invoke", this._name, args);
};
constructor(result: Partial<Returns<U> & Throws & ThisArg & Callback & Fallback> | undefined) {
this.thisArg = hasOwn(result, "this") ? result.this : Recording.noThisArg;
this.callback = hasOwn(result, "callback") ? result.callback : undefined;
this.result = hasOwn(result, "return") ? { return: result.return } :
hasOwn(result, "throw") ? { throw: result.throw } :
hasOwn(result, "fallback") && result.fallback ? { fallback: true } :
undefined;
}
public apply(_target: object, _thisArg: any, argArray: any[]): any {
this.recording = new Recording("apply", /*name*/ undefined, argArray);
return undefined;
public get(_target: T, name: PropertyKey, _receiver: any): any {
this.recording = new Recording("get", name, this.thisArg, [], /*newTarget*/ undefined, this.result, this.callback);
return (...argArray: any[]) => { this.recording = new Recording("invoke", name, this.thisArg, argArray, /*newTarget*/ undefined, this.result, this.callback); };
}
public construct(_target: object, argArray: any[], newTarget?: any): any {
this.recording = new Recording("construct", /*name*/ undefined, argArray, newTarget);
return undefined;
}
public get(_target: object, name: PropertyKey, _receiver: any): any {
this.recording = new Recording("get", name, []);
this._name = name;
return this._method;
}
public set(_target: object, name: PropertyKey, value: any, _receiver: any): boolean {
this.recording = new Recording("set", name, [value]);
public set(_target: T, name: PropertyKey, value: any, _receiver: any): boolean {
this.recording = new Recording("set", name, this.thisArg, [value], /*newTarget*/ undefined, this.result, this.callback);
return true;
}
}
function capture<T, U>(callback: (value: T) => U): Recording {
const handler = new CapturingHandler();
const { proxy, revoke } = Proxy.revocable<any>(noop, handler);
try {
callback(proxy);
if (!handler.recording) {
throw new Error("Nothing was captured.");
}
return handler.recording;
class CapturingFunctionHandler<T extends Callable | Constructable, U> extends CapturingHandler<T, U> {
public apply(_target: T, _thisArg: any, argArray: any[]): any {
this.recording = new Recording("apply", /*name*/ undefined, this.thisArg, argArray, /*newTarget*/ undefined, this.result, this.callback);
return undefined;
}
finally {
revoke();
public construct(_target: T, argArray: any[], newTarget?: any): any {
this.recording = new Recording("construct", /*name*/ undefined, /*thisArg*/ undefined, argArray, newTarget, this.result, this.callback);
return undefined;
}
}
function hasOwn<T extends object, K extends keyof T>(object: Partial<T> | undefined, key: K): object is (T | T & never) & { [P in K]: T[P] } {
return object !== undefined
&& Object.prototype.hasOwnProperty.call(object, key);
}

View File

@@ -1,38 +0,0 @@
import { Mock } from "./mock";
import { Times } from "./times";
import { Arg } from "./arg";
function noop() {}
export type Callable = ((...args: any[]) => any);
export type Constructable = (new (...args: any[]) => any);
export class Spy<T extends Callable | Constructable = Callable & Constructable> {
private _mock: Mock<T>;
constructor(target = <T>noop) {
this._mock = new Mock<T>(target);
}
public get value(): T {
return this._mock.value;
}
public verify(callback: (value: T) => any, times: Times): this {
this._mock.verify(callback, times);
return this;
}
public called(times: Times): this {
return this.verify(_ => (<Callable>_)(Arg.rest()), times);
}
public constructed(times: Times): this {
return this.verify(_ => new (<Constructable>_)(Arg.rest()), times);
}
public revoke(): void {
this._mock.revoke();
}
}

View File

@@ -9,11 +9,10 @@ describe("arg", () => {
const target = Arg.from(Arg.any());
// act
const result = Arg.validate(target, ["a"], 0);
const result = Arg.validate(target, "a");
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("toString", () => {
// arrange
@@ -32,21 +31,20 @@ describe("arg", () => {
const target = Arg.from(Arg.is(value => value === "a"));
// act
const result = Arg.validate(target, ["a"], 0);
const result = Arg.validate(target, "a");
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.is(value => value === "a"));
// act
const result = Arg.validate(target, ["b"], 0);
const result = Arg.validate(target, "b");
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -65,21 +63,20 @@ describe("arg", () => {
const target = Arg.from(Arg.notNull());
// act
const result = Arg.validate(target, [{}], 0);
const result = Arg.validate(target, {});
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.notNull());
// act
const result = Arg.validate(target, [null], 0);
const result = Arg.validate(target, null);
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -98,21 +95,20 @@ describe("arg", () => {
const target = Arg.from(Arg.null());
// act
const result = Arg.validate(target, [null], 0);
const result = Arg.validate(target, null);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.null());
// act
const result = Arg.validate(target, [{}], 0);
const result = Arg.validate(target, {});
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -131,21 +127,20 @@ describe("arg", () => {
const target = Arg.from(Arg.notUndefined());
// act
const result = Arg.validate(target, [{}], 0);
const result = Arg.validate(target, {});
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.notUndefined());
// act
const result = Arg.validate(target, [undefined], 0);
const result = Arg.validate(target, undefined);
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -164,21 +159,20 @@ describe("arg", () => {
const target = Arg.from(Arg.undefined());
// act
const result = Arg.validate(target, [undefined], 0);
const result = Arg.validate(target, undefined);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.undefined());
// act
const result = Arg.validate(target, [{}], 0);
const result = Arg.validate(target, {});
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -197,31 +191,30 @@ describe("arg", () => {
const target = Arg.from(Arg.notNullOrUndefined());
// act
const result = Arg.validate(target, [{}], 0);
const result = Arg.validate(target, {});
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("invalid (null)", () => {
// arrange
const target = Arg.from(Arg.notNullOrUndefined());
// act
const result = Arg.validate(target, [null], 0);
const result = Arg.validate(target, null);
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("invalid (undefined)", () => {
// arrange
const target = Arg.from(Arg.notNullOrUndefined());
// act
const result = Arg.validate(target, [undefined], 0);
const result = Arg.validate(target, undefined);
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -240,32 +233,30 @@ describe("arg", () => {
const target = Arg.from(Arg.nullOrUndefined());
// act
const result = Arg.validate(target, [null], 0);
const result = Arg.validate(target, null);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("valid (undefined)", () => {
// arrange
const target = Arg.from(Arg.nullOrUndefined());
// act
const result = Arg.validate(target, [undefined], 0);
const result = Arg.validate(target, undefined);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.nullOrUndefined());
// act
const result = Arg.validate(target, [{}], 0);
const result = Arg.validate(target, {});
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -284,26 +275,26 @@ describe("arg", () => {
const target = Arg.from(Arg.between(1, 3));
// act
const min = Arg.validate(target, [1], 0);
const mid = Arg.validate(target, [2], 0);
const max = Arg.validate(target, [3], 0);
const min = Arg.validate(target, 1);
const mid = Arg.validate(target, 2);
const max = Arg.validate(target, 3);
// assert
assert.isTrue(min.valid);
assert.isTrue(mid.valid);
assert.isTrue(max.valid);
assert.isTrue(min);
assert.isTrue(mid);
assert.isTrue(max);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.between(1, 3));
// act
const before = Arg.validate(target, [0], 0);
const after = Arg.validate(target, [4], 0);
const before = Arg.validate(target, 0);
const after = Arg.validate(target, 4);
// assert
assert.isFalse(before.valid);
assert.isFalse(after.valid);
assert.isFalse(before);
assert.isFalse(after);
});
it("toString", () => {
// arrange
@@ -322,20 +313,20 @@ describe("arg", () => {
const target = Arg.from(Arg.in(["a", "b"]));
// act
const result = Arg.validate(target, ["a"], 0);
const result = Arg.validate(target, "a");
// assert
assert.isTrue(result.valid);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.in(["a", "b"]));
// act
const result = Arg.validate(target, ["c"], 0);
const result = Arg.validate(target, "c");
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -354,20 +345,20 @@ describe("arg", () => {
const target = Arg.from(Arg.notIn(["a", "b"]));
// act
const result = Arg.validate(target, ["c"], 0);
const result = Arg.validate(target, "c");
// assert
assert.isTrue(result.valid);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.notIn(["a", "b"]));
// act
const result = Arg.validate(target, ["a"], 0);
const result = Arg.validate(target, "a");
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -386,20 +377,20 @@ describe("arg", () => {
const target = Arg.from(Arg.match(/^a$/));
// act
const result = Arg.validate(target, ["a"], 0);
const result = Arg.validate(target, "a");
// assert
assert.isTrue(result.valid);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.match(/^a$/));
// act
const result = Arg.validate(target, ["b"], 0);
const result = Arg.validate(target, "b");
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -418,20 +409,20 @@ describe("arg", () => {
const target = Arg.from(Arg.typeof("number"));
// act
const result = Arg.validate(target, [1], 0);
const result = Arg.validate(target, 1);
// assert
assert.isTrue(result.valid);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.typeof("number"));
// act
const result = Arg.validate(target, ["a"], 0);
const result = Arg.validate(target, "a");
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -451,10 +442,10 @@ describe("arg", () => {
const target = Arg.from(Arg.instanceof(C));
// act
const result = Arg.validate(target, [new C()], 0);
const result = Arg.validate(target, new C());
// assert
assert.isTrue(result.valid);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
@@ -462,10 +453,10 @@ describe("arg", () => {
const target = Arg.from(Arg.instanceof(C));
// act
const result = Arg.validate(target, [{}], 0);
const result = Arg.validate(target, {});
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -485,22 +476,22 @@ describe("arg", () => {
const target = Arg.from(Arg.has("a"));
// act
const own = Arg.validate(target, [{ a: 1 }], 0);
const proto = Arg.validate(target, [{ __proto__: { a: 1 } }], 0);
const own = Arg.validate(target, { a: 1 });
const proto = Arg.validate(target, { __proto__: { a: 1 } });
// assert
assert.isTrue(own.valid);
assert.isTrue(proto.valid);
assert.isTrue(own);
assert.isTrue(proto);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.has("a"));
// act
const result = Arg.validate(target, [{ b: 1 }], 0);
const result = Arg.validate(target, { b: 1 });
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -519,22 +510,22 @@ describe("arg", () => {
const target = Arg.from(Arg.hasOwn("a"));
// act
const own = Arg.validate(target, [{ a: 1 }], 0);
const own = Arg.validate(target, { a: 1 });
// assert
assert.isTrue(own.valid);
assert.isTrue(own);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.hasOwn("a"));
// act
const result = Arg.validate(target, [{ b: 1 }], 0);
const proto = Arg.validate(target, [{ __proto__: { a: 1 } }], 0);
const result = Arg.validate(target, { b: 1 });
const proto = Arg.validate(target, { __proto__: { a: 1 } });
// assert
assert.isFalse(result.valid);
assert.isFalse(proto.valid);
assert.isFalse(result);
assert.isFalse(proto);
});
it("toString", () => {
// arrange
@@ -554,14 +545,12 @@ describe("arg", () => {
const target = Arg.from(Arg.rest());
// act
const empty = Arg.validate(target, [], 0);
const multiple = Arg.validate(target, ["a", "b"], 0);
const empty = Arg.validateAll([target], []);
const multiple = Arg.validateAll([target], ["a", "b"]);
// assert
assert.isTrue(empty.valid);
assert.strictEqual(empty.next, 0);
assert.isTrue(multiple.valid);
assert.strictEqual(multiple.next, 2);
assert.isTrue(empty);
assert.isTrue(multiple);
});
it("toString", () => {
// arrange
@@ -580,24 +569,22 @@ describe("arg", () => {
const target = Arg.from(Arg.rest(Arg.typeof("string")));
// act
const empty = Arg.validate(target, [], 0);
const multiple = Arg.validate(target, ["a", "b"], 0);
const empty = Arg.validateAll([target], []);
const multiple = Arg.validateAll([target], ["a", "b"]);
// assert
assert.isTrue(empty.valid);
assert.strictEqual(empty.next, 0);
assert.isTrue(multiple.valid);
assert.strictEqual(multiple.next, 2);
assert.isTrue(empty);
assert.isTrue(multiple);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.rest(Arg.typeof("string")));
// act
const result = Arg.validate(target, ["a", 1], 0);
const result = Arg.validateAll([target], ["a", 1]);
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange
@@ -617,20 +604,20 @@ describe("arg", () => {
const target = Arg.from("a");
// act
const result = Arg.validate(target, ["a"], 0);
const result = Arg.validate(target, "a");
// assert
assert.isTrue(result.valid);
assert.isTrue(result);
});
it("invalid", () => {
// arrange
const target = Arg.from("a");
// act
const result = Arg.validate(target, ["b"], 0);
const result = Arg.validate(target, "b");
// assert
assert.isFalse(result.valid);
assert.isFalse(result);
});
it("toString", () => {
// arrange

View File

@@ -1,5 +1,5 @@
import "./argTests";
import "./timesTests";
import "./mockTests";
import "./stubTests";
import "./injectTests";
import "./timersTests";

View File

@@ -0,0 +1,79 @@
import "./sourceMapSupport";
import { Mock } from "../mock";
import { Inject } from "../inject";
import { Times } from "../times";
import { assert } from "chai";
describe("inject", () => {
it("install replaces value", () => {
// arrange
const mock = new Mock({ a: 1 });
const inject = new Inject(mock.proxy, "a", 2);
// act
inject.install();
// assert
mock.verify(_ => _.a = 2, Times.once());
});
it("install is installed", () => {
// arrange
const mock = new Mock({ a: 1 });
const inject = new Inject(mock.proxy, "a", 2);
// act
inject.install();
// assert
assert.isTrue(inject.installed);
});
it("install twice only installs once", () => {
// arrange
const mock = new Mock({ a: 1 });
const inject = new Inject(mock.proxy, "a", 2);
// act
inject.install();
inject.install();
// assert
mock.verify(_ => _.a = 2, Times.once());
});
it("uninstall restores value", () => {
// arrange
const mock = new Mock({ a: 1 });
const inject = new Inject(mock.proxy, "a", 2);
inject.install();
// act
inject.uninstall();
// assert
mock.verify(_ => _.a = 1, Times.once());
});
it("uninstall is not installed", () => {
// arrange
const mock = new Mock({ a: 1 });
const inject = new Inject(mock.proxy, "a", 2);
inject.install();
// act
inject.uninstall();
// assert
assert.isFalse(inject.installed);
});
it("uninstall twice only uninstalls once", () => {
// arrange
const mock = new Mock({ a: 1 });
const inject = new Inject(mock.proxy, "a", 2);
inject.install();
// act
inject.uninstall();
inject.uninstall();
// assert
mock.verify(_ => _.a = 1, Times.once());
});
});

View File

@@ -1,262 +1,363 @@
import "./sourceMapSupport";
import { Mock } from "../mock";
import { Stub } from "../stub";
import { Inject } from "../inject";
import { Arg } from "../arg";
import { Times } from "../times";
import { recordError } from "./utils";
import { assert } from "chai";
describe("mock", () => {
it("mock get with no setups", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
describe("no setup", () => {
it("get (exists)", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
// act
const result = mock.value.a;
// act
const result = mock.proxy.a;
// assert
assert.equal(1, result);
});
it("mock setup property get with return", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, { get a() { return 2; } });
// act
const result = mock.value.a;
// assert
assert.equal(2, result);
});
it("mock setup property get with throw", () => {
// arrange
const target = { a: 1 };
const error = new Error("error");
const mock = new Mock(target, { get a(): number { throw error; } });
// act
const e = recordError(() => mock.value.a);
// assert
assert.strictEqual(error, e);
});
it("mock setup property set", () => {
// arrange
let _a: number | undefined;
const target = { a: 1 };
const mock = new Mock(target, { set a(value: number) { _a = value; } });
// act
mock.value.a = 2;
// assert
assert.equal(2, _a);
assert.equal(1, target.a);
});
it("mock setup property set with throw", () => {
// arrange
const target = { a: 1 };
const error = new Error("error");
const mock = new Mock(target, { set a(value: number) { throw error; } });
// act
const e = recordError(() => mock.value.a = 2);
// assert
assert.strictEqual(error, e);
});
it("mock setup method call no setups", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target);
// act
const result = mock.value.a();
// assert
assert.equal(1, result);
});
it("mock setup method callback", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target, { a() { return 2; } });
// act
const result = mock.value.a();
// assert
assert.equal(2, result);
});
it("mock setup method callback throws", () => {
// arrange
const target = { a() { return 1; } };
const error = new Error("error");
const mock = new Mock(target, { a(): number { throw error; } });
// act
const e = recordError(() => mock.value.a());
// assert
assert.strictEqual(error, e);
});
it("mock setup new property", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, <any>{ b: 2 });
// act
const result = (<any>mock.value).b;
// assert
assert.equal(2, result);
});
it("mock setup new method", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, <any>{ b() { return 2; } });
// act
const result = (<any>mock.value).b();
// assert
assert.equal(2, result);
});
it("mock verify get no setups, not called throws", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
// act
const e = recordError(() => mock.verify(_ => _.a, Times.once()));
// assert
assert.instanceOf(e, Error);
});
it("mock verify get no setups, called passes", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
const result = mock.value.a;
// act
const e = recordError(() => mock.verify(_ => _.a, Times.once()));
// assert
assert.isUndefined(e);
});
it("mock verify setup get, called passes", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, { get a() { return 2 } });
const result = mock.value.a;
// act
const e = recordError(() => mock.verify(_ => _.a, Times.once()));
// assert
assert.isUndefined(e);
});
it("mock verify method no setups, not called throws", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target);
// act
const e = recordError(() => mock.verify(_ => _.a(), Times.once()));
// assert
assert.instanceOf(e, Error);
});
it("mock verify method no setups, called passes", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target);
const result = mock.value.a();
// act
const e = recordError(() => mock.verify(_ => _.a(), Times.once()));
// assert
assert.isUndefined(e);
});
it("mock verify setup method, called passes", () => {
// arrange
const target = { a(x: number) { return x + 1; } };
const mock = new Mock(target, {
a(x: number) {
return x + 2;
}
// assert
assert.equal(result, 1);
});
const result = mock.value.a(3);
it("get (missing)", () => {
// arrange
const mock = new Mock<{ a?: number }>();
// act
const e = recordError(() => mock.verify(_ => _.a(Arg.number()), Times.once()));
// act
const result = mock.proxy.a;
// assert
assert.isUndefined(e);
// assert
assert.isUndefined(result);
});
it("set (exists)", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
// act
mock.proxy.a = 2;
const result = mock.proxy.a;
// assert
assert.equal(result, 2);
});
it("set (missing)", () => {
// arrange
const mock = new Mock<{ a?: number }>();
// act
mock.proxy.a = 2;
const result = mock.proxy.a;
// assert
assert.equal(result, 2);
});
it("method (existing)", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target);
// act
const result = mock.proxy.a();
// assert
assert.equal(1, result);
});
});
it("mock setup method using callback", () => {
// arrange
const mock = new Mock<{ a(x: number): number; }>();
mock.setup(_ => _.a(1), { returns: 2 });
// act
const result = mock.value.a(1);
describe("setup", () => {
describe("using object", () => {
it("get", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, { get a() { return 2; } });
// assert
assert.strictEqual(result, 2);
// act
const result = mock.proxy.a;
// assert
assert.equal(2, result);
});
it("get with throw", () => {
// arrange
const target = { a: 1 };
const error = new Error("error");
const mock = new Mock(target, { get a(): number { throw error; } });
// act
const e = recordError(() => mock.proxy.a);
// assert
assert.strictEqual(error, e);
});
it("set", () => {
// arrange
let _a: number | undefined;
const target = { a: 1 };
const mock = new Mock(target, { set a(value: number) { _a = value; } });
// act
mock.proxy.a = 2;
// assert
assert.equal(2, _a);
assert.equal(1, target.a);
});
it("set with throw", () => {
// arrange
const target = { a: 1 };
const error = new Error("error");
const mock = new Mock(target, { set a(value: number) { throw error; } });
// act
const e = recordError(() => mock.proxy.a = 2);
// assert
assert.strictEqual(error, e);
});
it("method", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target, { a() { return 2; } });
// act
const result = mock.proxy.a();
// assert
assert.equal(2, result);
});
it("method throws", () => {
// arrange
const target = { a() { return 1; } };
const error = new Error("error");
const mock = new Mock(target, { a(): number { throw error; } });
// act
const e = recordError(() => mock.proxy.a());
// assert
assert.strictEqual(error, e);
});
it("new property", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, <any>{ b: 2 });
// act
const result = (<any>mock.proxy).b;
// assert
assert.equal(2, result);
});
it("new method", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, <any>{ b() { return 2; } });
// act
const result = (<any>mock.proxy).b();
// assert
assert.equal(2, result);
});
});
describe("using callback", () => {
it("get only", () => {
// arrange
const mock = new Mock<{ a: number }>();
mock.setup(_ => _.a, { return: 2 });
// act
const result1 = mock.proxy.a;
const err = recordError(() => mock.proxy.a = 3);
const result2 = mock.proxy.a;
// assert
assert.strictEqual(result1, 2);
assert.strictEqual(result2, 2);
assert.instanceOf(err, Error);
});
it("set only", () => {
// arrange
const mock = new Mock<{ a: number }>();
mock.setup(_ => _.a = 2);
// act
const result = mock.proxy.a;
const err2 = recordError(() => mock.proxy.a = 2);
const err3 = recordError(() => mock.proxy.a = 3);
// assert
assert.isUndefined(result);
assert.isUndefined(err2);
assert.instanceOf(err3, Error);
});
it("get and set", () => {
// arrange
const mock = new Mock<{ a: number }>();
mock.setup(_ => _.a, { return: 2 });
mock.setup(_ => _.a = Arg.any());
// act
const result = mock.proxy.a;
mock.proxy.a = 3;
// assert
assert.strictEqual(result, 2);
});
it("method", () => {
// arrange
const mock = new Mock<{ a(x: number): number; }>();
mock.setup(_ => _.a(1), { return: 2 });
// act
const result = mock.proxy.a(1);
// assert
assert.strictEqual(result, 2);
});
it("function", () => {
// arrange
const mock = new Mock<(x: number) => number>(x => 0);
mock.setup(_ => _(Arg.number()), { return: 2 });
// act
const result = mock.proxy(1);
// assert
assert.strictEqual(result, 2);
});
});
});
it("mock setup setter/getter using callback", () => {
// arrange
const mock = new Mock<{ a: number }>();
mock.setup(_ => _.a, { returns: 2 });
mock.setup(_ => _.a = Arg.any());
// act
const result = mock.value.a;
mock.value.a = 3;
describe("verify", () => {
describe("no setup", () => {
describe("get", () => {
it("not called throws", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
// assert
assert.strictEqual(result, 2);
});
it("mock setup getter only using callback", () => {
// arrange
const mock = new Mock<{ a: number }>();
mock.setup(_ => _.a, { returns: 2 });
// act
const e = recordError(() => mock.verify(_ => _.a, Times.once()));
// act
const result = mock.value.a;
const err = recordError(() => mock.value.a = 3);
// assert
assert.instanceOf(e, Error);
});
it("called passes", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
const result = mock.proxy.a;
// assert
assert.strictEqual(result, 2);
assert.instanceOf(err, Error);
});
it("mock setup setter only using callback", () => {
// arrange
const mock = new Mock<{ a: number }>();
mock.setup(_ => _.a = 2);
// act
const e = recordError(() => mock.verify(_ => _.a, Times.once()));
// act
const err1 = recordError(() => mock.value.a);
const err2 = recordError(() => mock.value.a = 2);
const err3 = recordError(() => mock.value.a = 3);
// assert
assert.isUndefined(e);
});
});
describe("set", () => {
it("not called throws", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
// assert
assert.instanceOf(err1, Error);
assert.isUndefined(err2);
assert.instanceOf(err3, Error);
});
it("mock setup function only using callback", () => {
// arrange
const mock = new Mock<(x: number) => number>(x => 0);
mock.setup(_ => _(Arg.number()), { returns: 2 });
// act
const e = recordError(() => mock.verify(_ => _.a = 2, Times.once()));
// act
const result = mock.value(1);
// assert
assert.instanceOf(e, Error);
});
it("called passes", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target);
mock.proxy.a = 2;
// assert
assert.strictEqual(result, 2);
// act
const e = recordError(() => mock.verify(_ => _.a = 2, Times.once()));
// assert
assert.isUndefined(e);
});
});
describe("method", () => {
it("not called throws", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target);
// act
const e = recordError(() => mock.verify(_ => _.a(), Times.once()));
// assert
assert.instanceOf(e, Error);
});
it("called passes", () => {
// arrange
const target = { a() { return 1; } };
const mock = new Mock(target);
const result = mock.proxy.a();
// act
const e = recordError(() => mock.verify(_ => _.a(), Times.once()));
// assert
assert.isUndefined(e);
});
});
describe("function", () => {
it("not called throws", () => {
// arrange
const mock = Mock.function();
// act
const e = recordError(() => mock.verify(_ => _(), Times.once()));
// assert
assert.instanceOf(e, Error);
});
it("called passes", () => {
// arrange
const mock = Mock.function();
const result = mock.proxy();
// act
const e = recordError(() => mock.verify(_ => _(), Times.once()));
// assert
assert.isUndefined(e);
});
});
});
it("setup get, called passes", () => {
// arrange
const target = { a: 1 };
const mock = new Mock(target, { get a() { return 2 } });
const result = mock.proxy.a;
// act
const e = recordError(() => mock.verify(_ => _.a, Times.once()));
// assert
assert.isUndefined(e);
});
it("setup method, called passes", () => {
// arrange
const target = { a(x: number) { return x + 1; } };
const mock = new Mock(target, {
a(x: number) {
return x + 2;
}
});
const result = mock.proxy.a(3);
// act
const e = recordError(() => mock.verify(_ => _.a(Arg.number()), Times.once()));
// assert
assert.isUndefined(e);
});
});
});

View File

@@ -1,79 +0,0 @@
import "./sourceMapSupport";
import { Mock } from "../mock";
import { Stub } from "../stub";
import { Times } from "../times";
import { assert } from "chai";
describe("stub", () => {
it("stub install replaces value", () => {
// arrange
const mock = new Mock({ a: 1 });
const stub = new Stub(mock.value, "a", 2);
// act
stub.install();
// assert
mock.verify(_ => _.a = 2, Times.once());
});
it("stub install is installed", () => {
// arrange
const mock = new Mock({ a: 1 });
const stub = new Stub(mock.value, "a", 2);
// act
stub.install();
// assert
assert.isTrue(stub.installed);
});
it("stub install twice only installs once", () => {
// arrange
const mock = new Mock({ a: 1 });
const stub = new Stub(mock.value, "a", 2);
// act
stub.install();
stub.install();
// assert
mock.verify(_ => _.a = 2, Times.once());
});
it("stub uninstall restores value", () => {
// arrange
const mock = new Mock({ a: 1 });
const stub = new Stub(mock.value, "a", 2);
stub.install();
// act
stub.uninstall();
// assert
mock.verify(_ => _.a = 1, Times.once());
});
it("stub uninstall is not installed", () => {
// arrange
const mock = new Mock({ a: 1 });
const stub = new Stub(mock.value, "a", 2);
stub.install();
// act
stub.uninstall();
// assert
assert.isFalse(stub.installed);
});
it("stub uninstall twice only uninstalls once", () => {
// arrange
const mock = new Mock({ a: 1 });
const stub = new Stub(mock.value, "a", 2);
stub.install();
// act
stub.uninstall();
stub.uninstall();
// assert
mock.verify(_ => _.a = 1, Times.once());
});
});

View File

@@ -1,5 +1,5 @@
import "./sourceMapSupport";
import { Spy } from "../spy";
import { Mock } from "../mock";
import { Arg } from "../arg";
import { Times } from "../times";
import { Timers } from "../timers";
@@ -10,52 +10,52 @@ describe("timers", () => {
it("set adds entry, does not invoke", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.setImmediate(spy.value);
const handle = target.setImmediate(spy.proxy);
const pending = target.getPending();
// assert
assert.strictEqual(pending.length, 1);
assert.strictEqual(pending[0].kind, "immediate");
assert.isDefined(handle);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set/clear", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.setImmediate(spy.value);
const handle = target.setImmediate(spy.proxy);
target.clearImmedate(handle);
const pending = target.getPending();
// assert
assert.strictEqual(pending.length, 0);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set one and execute", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.setImmediate(spy.value);
target.setImmediate(spy.proxy);
const count = target.executeImmediates();
// assert
assert.strictEqual(count, 1);
spy.called(Times.once());
spy.verify(_ => _(), Times.once());
});
it("set one with arg and execute", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.setImmediate(spy.value, "a");
target.setImmediate(spy.proxy, "a");
const count = target.executeImmediates();
// assert
@@ -65,108 +65,108 @@ describe("timers", () => {
it("nested with maxDepth = 0", () => {
// arrange
const target = new Timers();
const spy = new Spy(() => { target.setImmediate(spy.value); });
const spy = Mock.spy(() => { target.setImmediate(spy.proxy); });
// act
target.setImmediate(spy.value);
target.setImmediate(spy.proxy);
const count = target.executeImmediates(/*maxDepth*/ 0);
// assert
assert.strictEqual(count, 1);
spy.called(Times.once());
spy.verify(_ => _(), Times.once());
});
it("nested with maxDepth = 1", () => {
// arrange
const target = new Timers();
const spy = new Spy(() => { target.setImmediate(spy.value); });
const spy = Mock.spy(() => { target.setImmediate(spy.proxy); });
// act
target.setImmediate(spy.value);
target.setImmediate(spy.proxy);
const count = target.executeImmediates(/*maxDepth*/ 1);
// assert
assert.strictEqual(count, 2);
spy.called(Times.exactly(2));
spy.verify(_ => _(), Times.exactly(2));
});
});
describe("timeout", () => {
it("set adds entry, does not invoke", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.setTimeout(spy.value, 0);
const handle = target.setTimeout(spy.proxy, 0);
const pending = target.getPending();
// assert
assert.strictEqual(pending.length, 1);
assert.strictEqual(pending[0].kind, "timeout");
assert.isDefined(handle);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set/clear", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.setTimeout(spy.value, 0);
const handle = target.setTimeout(spy.proxy, 0);
target.clearTimeout(handle);
const pending = target.getPending();
// assert
assert.strictEqual(pending.length, 0);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set adds future entry, advance prior to due does not invoke", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.setTimeout(spy.value, 10);
target.setTimeout(spy.proxy, 10);
const count = target.advance(9);
// assert
assert.strictEqual(count, 0);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set adds future entry, advance to due invokes", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.setTimeout(spy.value, 10);
target.setTimeout(spy.proxy, 10);
const count = target.advance(10);
// assert
assert.strictEqual(count, 1);
spy.called(Times.once());
spy.verify(_ => _(), Times.once());
});
it("5 nested sets throttle", () => {
// arrange
const target = new Timers();
const spy = new Spy(() => { target.setTimeout(spy.value, 0); });
const spy = new Mock(() => { target.setTimeout(spy.proxy, 0); });
// act
target.setTimeout(spy.value, 0);
target.setTimeout(spy.proxy, 0);
const count = target.advance(1);
// assert
assert.strictEqual(count, 5);
spy.called(Times.exactly(5));
spy.verify(_ => _(), Times.exactly(5));
});
});
describe("interval", () => {
it("set adds entry, does not invoke", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.setInterval(spy.value, 0);
const handle = target.setInterval(spy.proxy, 0);
const pending = target.getPending({ kind: "interval", ms: 10 });
// assert
@@ -174,132 +174,132 @@ describe("timers", () => {
assert.strictEqual(pending[0].kind, "interval");
assert.strictEqual(pending[0].interval, 10);
assert.isDefined(handle);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set/clear", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.setInterval(spy.value, 0);
const handle = target.setInterval(spy.proxy, 0);
target.clearInterval(handle);
const pending = target.getPending({ kind: "interval", ms: 10 });
// assert
assert.strictEqual(pending.length, 0);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set adds future entry, advance prior to due does not invoke", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.setInterval(spy.value, 10);
target.setInterval(spy.proxy, 10);
const count = target.advance(9);
// assert
assert.strictEqual(count, 0);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("set adds future entry, advance to due invokes", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.setInterval(spy.value, 10);
target.setInterval(spy.proxy, 10);
const count = target.advance(10);
// assert
assert.strictEqual(count, 1);
spy.called(Times.once());
spy.verify(_ => _(), Times.once());
});
it("set adds future entry, advance to due twice invokes twice", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.setInterval(spy.value, 10);
target.setInterval(spy.proxy, 10);
const count = target.advance(20);
// assert
assert.strictEqual(count, 2);
spy.called(Times.exactly(2));
spy.verify(_ => _(), Times.exactly(2));
});
it("set adds future entry, remove before second due time", () => {
// arrange
const target = new Timers();
const spy = new Spy(() => { target.clearInterval(handle); });
const spy = new Mock(() => { target.clearInterval(handle); });
// act
const handle = target.setInterval(spy.value, 10);
const handle = target.setInterval(spy.proxy, 10);
const count = target.advance(20);
// assert
assert.strictEqual(count, 1);
spy.called(Times.exactly(1));
spy.verify(_ => _(), Times.exactly(1));
});
});
describe("frame", () => {
it("request adds entry, does not invoke", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.requestAnimationFrame(spy.value);
const handle = target.requestAnimationFrame(spy.proxy);
const pending = target.getPending({ ms: 16 });
// assert
assert.strictEqual(pending.length, 1);
assert.strictEqual(pending[0].kind, "frame");
assert.isDefined(handle);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("request/cancel", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
const handle = target.requestAnimationFrame(spy.value);
const handle = target.requestAnimationFrame(spy.proxy);
target.cancelAnimationFrame(handle);
const pending = target.getPending();
// assert
assert.strictEqual(pending.length, 0);
spy.called(Times.none());
spy.verify(_ => _(), Times.none());
});
it("request and advance past one frame", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.requestAnimationFrame(spy.value);
target.requestAnimationFrame(spy.proxy);
const count = target.advance(16);
// assert
assert.strictEqual(count, 1);
spy.called(Times.once());
spy.verify(_ => _(Arg.number()), Times.once());
});
it("requests clamped to 16ms", () => {
// arrange
const target = new Timers();
const spy = new Spy();
const spy = Mock.function();
// act
target.requestAnimationFrame(spy.value);
target.requestAnimationFrame(spy.proxy);
target.advance(10);
target.requestAnimationFrame(spy.value);
target.requestAnimationFrame(spy.proxy);
const count = target.advance(16);
// assert
assert.strictEqual(count, 2);
spy.called(Times.exactly(2));
spy.verify(_ => _(Arg.number()), Times.exactly(2));
});
});
});