Migrate tscWatchMode to vfs

This commit is contained in:
Ron Buckton
2017-11-21 19:47:13 -08:00
parent 3d3977f2b7
commit 41567b2261
35 changed files with 5715 additions and 3080 deletions

View File

@@ -0,0 +1,29 @@
const gulp = require("gulp");
const gutil = require("gulp-util");
const sourcemaps = require("gulp-sourcemaps");
const tsb = require("gulp-tsb");
const mocha = require("gulp-mocha");
const del = require("del");
const src = {
compile: tsb.create("src/tsconfig.json"),
src: () => gulp.src(["src/**/*.ts"]),
dest: () => gulp.dest("dist")
};
gulp.task("clean", () => del(["dist/**/*"]));
gulp.task("build", () => src.src()
.pipe(sourcemaps.init())
.pipe(src.compile())
.pipe(sourcemaps.write(".", { includeContent: false, destPath: "dist" }))
.pipe(gulp.dest("dist")));
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("default", ["test"]);

View File

@@ -0,0 +1,35 @@
{
"private": true,
"name": "typemock",
"version": "0.0.0",
"description": "JavaScript Mock object framework",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "gulp test"
},
"keywords": [
"javascript",
"mock",
"type",
"typescript"
],
"author": "Ron Buckton (ron.buckton@microsoft.com)",
"license": "Apache-2.0",
"devDependencies": {
"@types/chai": "^4.0.4",
"@types/mocha": "^2.2.27",
"@types/node": "^8.0.20",
"@types/source-map-support": "^0.4.0",
"chai": "^4.1.2",
"del": "^2.0.2",
"gulp": "^3.9.1",
"gulp-mocha": "^4.3.1",
"gulp-sourcemaps": "^2.6.1",
"gulp-tsb": "^2.0.5",
"merge2": "^0.3.6",
"mocha": "^2.2.5",
"source-map-support": "^0.5.0",
"typescript": "^2.6.1"
}
}

293
scripts/typemock/src/arg.ts Normal file
View File

@@ -0,0 +1,293 @@
/**
* Represents an argument condition used during verification.
*/
export class Arg {
private _condition: (value: any, args: ReadonlyArray<any>, index: number) => { valid: boolean, next?: number };
private _message: string;
private constructor(condition: (value: any, args: ReadonlyArray<any>, index: number) => { valid: boolean, next?: number }, message: string) {
this._condition = condition;
this._message = message;
}
/**
* Allows any value.
*/
public static any<T = any>(): T & Arg {
return <any>new Arg(() => ({ valid: true }), `any`);
}
/**
* Allows a value that matches the specified condition.
* @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`);
}
/**
* Allows only a null value.
*/
public static null<T = any>(): T & Arg {
return <any>new Arg(value => ({ valid: value === null }), `null`);
}
/**
* Allows only a non-null value.
*/
public static notNull<T = any>(): T & Arg {
return Arg.not(Arg.null());
}
/**
* Allows only an undefined value.
*/
public static undefined<T = any>(): T & Arg {
return <any>new Arg(value => ({ valid: value === undefined }), `undefined`);
}
/**
* Allows only a non-undefined value.
*/
public static notUndefined<T = any>(): T & Arg {
return Arg.not(Arg.undefined());
}
/**
* Allows only an undefined or null value.
*/
public static nullOrUndefined<T = any>(): T & Arg {
return Arg.or(Arg.null(), Arg.undefined());
}
/**
* Allows only a non-undefined, non-null value.
*/
public static notNullOrUndefined<T = any>(): T & Arg {
return Arg.not(Arg.nullOrUndefined());
}
/**
* 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}`);
}
/**
* 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(", ")}`);
}
/**
* Allows any value not in the provided array.
*/
public static notIn<T = any>(values: T[]): T & Arg {
return Arg.not(Arg.in(values));
}
/**
* 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}`);
}
public static startsWith(text: string): string & Arg {
return <any>new Arg(value => ({ valid: 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}`);
}
public static includes(text: string): string & Arg {
return <any>new Arg(value => ({ valid: String(value).includes(text) }), `contains ${text}`);
}
/**
* Allows any value with the provided `typeof` tag.
*/
public static typeof(tag: "string"): string & Arg;
/**
* Allows any value with the provided `typeof` tag.
*/
public static typeof(tag: "number"): number & Arg;
/**
* Allows any value with the provided `typeof` tag.
*/
public static typeof(tag: "boolean"): boolean & Arg;
/**
* Allows any value with the provided `typeof` tag.
*/
public static typeof(tag: "symbol"): symbol & Arg;
/**
* Allows any value with the provided `typeof` tag.
*/
public static typeof(tag: "object"): object & Arg;
/**
* Allows any value with the provided `typeof` tag.
*/
public static typeof(tag: "function"): ((...args: any[]) => any) & Arg;
/**
* Allows any value with the provided `typeof` tag.
*/
public static typeof(tag: "undefined"): undefined & Arg;
/**
* Allows any value with the provided `typeof` tag.
*/
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}`);
}
public static string() { return this.typeof("string"); }
public static number() { return this.typeof("number"); }
public static boolean() { return this.typeof("boolean"); }
public static symbol() { return this.typeof("symbol"); }
public static object() { return this.typeof("object"); }
public static function() { return this.typeof("function"); }
/**
* Allows any value that is an instance of the provided function.
* @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}`);
}
/**
* 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(", ")}`);
}
/**
* 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(", ")}`);
}
/**
* Allows any value that matches the provided condition for the rest of the arguments in the call.
* @param condition The optional condition for each other element.
*/
public static rest<T>(condition?: T | (T & Arg)): T & Arg {
if (condition === undefined) {
return <any>new Arg((_, args) => ({ valid: true, next: args.length }), `rest`);
}
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}`
);
}
/**
* Negates a condition.
*/
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}`);
}
/**
* Combines conditions, where all conditions must be `true`.
*/
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 "));
}
/**
* 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));
}
/**
* Combines conditions, where any conditions may be `true`.
*/
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 "));
}
/**
* 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));
}
/**
* Ensures the value is a `Condition`
* @param value The value to coerce
* @returns The condition
*/
public static from<T>(value: T): T & Arg {
if (value instanceof Arg) {
return value;
}
return <any>new Arg(v => ({ valid: is(v, value) }), JSON.stringify(value));
}
/**
* 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.
*/
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 };
}
/**
* Gets a string that represents this condition.
*/
public toString(): string {
return `<${this._message}>`;
}
}
/**
* SameValueZero (from ECMAScript spec), which has stricter equality sematics than "==" or "===".
*/
function is(x: any, y: any) {
return (x === y) ? (x !== 0 || 1 / x === 1 / y) : (x !== x && y !== y);
}

View File

@@ -0,0 +1,6 @@
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";

View File

@@ -0,0 +1,394 @@
import { Times } from "./times";
import { Arg } from "./arg";
const weakHandler = new WeakMap<object, MockHandler<object>>();
function noop() {}
function getHandler(value: object) {
return weakHandler.get(value);
}
export interface Returns<U> {
returns: U;
}
export interface Throws {
throws: any;
}
/**
* A mock version of another oject
*/
export class Mock<T extends object> {
private _target: T;
private _handler = new MockHandler<T>();
private _proxy: T;
private _revoke: () => void;
/**
* A mock version of another object
* @param target The object to mock.
* @param setups Optional setups to use
*/
constructor(target: T = <T>{}, setups?: Partial<T>) {
this._target = target;
const { proxy, revoke } = Proxy.revocable<T>(this._target, this._handler);
this._proxy = proxy;
this._revoke = revoke;
weakHandler.set(proxy, this._handler);
if (setups) {
this.setup(setups);
}
}
/**
* Gets the mock version of the target
*/
public get value(): T {
return this._proxy;
}
/**
* 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>;
/**
* 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> {
if (typeof setup === "function") {
this._handler.setupCall(setup, result);
}
else {
this._handler.setupMembers(setup);
}
return 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> {
this._handler.verify(callback, times);
return this;
}
public revoke() {
this._handler.revoke();
this._revoke();
}
}
class Setup {
public recording: Recording;
public result: Partial<Returns<any> & Throws> | undefined;
constructor (recording: Recording, result?: Returns<any> | Throws) {
this.recording = recording;
this.result = result;
}
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();
}
}
}
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;
}
}
class Recording {
public readonly trap: string;
public readonly name: PropertyKey | undefined;
public readonly args: ReadonlyArray<any>;
public readonly newTarget: any;
private _conditions: ReadonlyArray<Arg> | undefined;
constructor(trap: string, name: PropertyKey | undefined, args: ReadonlyArray<any>, newTarget?: any) {
this.trap = trap;
this.name = name;
this.args = args || [];
this.newTarget = newTarget;
}
public get conditions() {
return this._conditions || (this._conditions = this.args.map(Arg.from));
}
public toString(): string {
return `${this.trap} ${this.name || ""}(${this.conditions.join(", ")})${this.newTarget ? ` [${this.newTarget.name}]` : ``}`;
}
public matchRecording(recording: Recording) {
if (recording.trap !== this.trap ||
recording.name !== this.name ||
recording.newTarget !== this.newTarget) {
return false;
}
return this.matchArguments(recording.args);
}
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;
}
}
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>();
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);
}
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);
}
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]));
if (typeof value === "function" && this.methodTargets.has(value)) {
value = this.methodTargets.get(value);
}
return Reflect.set(this.getTarget(target, name), name, value, receiver);
}
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);
}
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));
}
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 ((setups[0].recording.trap === "invoke") !== (recording.trap === "invoke")) {
throw new Error(`Cannot mix method and acessor setups for the same property.`);
}
}
setups.push(new Setup(recording, result));
}
}
public setupMembers(setup: object) {
for (const propertyKey of Reflect.ownKeys(setup)) {
const descriptor = Reflect.getOwnPropertyDescriptor(setup, propertyKey);
if (descriptor) {
if (propertyKey in this.overrides) {
throw new Error(`Property '${propertyKey.toString()}' already exists.`);
}
Reflect.defineProperty(this.overrides, propertyKey, descriptor);
}
}
}
public verify(callback: (value: T) => any, times: Times): void {
const expectation = capture(callback);
let count: number = 0;
for (const recording of this.recordings) {
if (expectation.matchRecording(recording)) {
count++;
}
}
times.check(count, `An error occured when verifying expectation: ${expectation}`);
}
public getTarget(target: T, name: PropertyKey) {
return name in this.overrides ? this.overrides : target;
}
public getMethod(name: PropertyKey, value: Function): Function {
const proxy = this.methodProxies.get(name);
if (proxy && this.methodTargets.get(proxy) === value) {
return proxy;
}
else {
const { proxy, revoke } = Proxy.revocable(value, new MethodHandler(name));
this.methodProxies.set(name, proxy);
this.methodRevocations.add(revoke);
this.methodTargets.set(proxy, value);
return proxy;
}
}
public revoke() {
for (const revoke of this.methodRevocations) {
revoke();
}
}
private defineMethod(name: PropertyKey) {
const setups = this.memberSetups;
this.setupMembers({
[name](...args: any[]) {
return Setup.evaluate(setups.get(name), "invoke", args);
}
});
}
private defineAccessor(name: PropertyKey) {
const setups = this.memberSetups;
this.setupMembers({
get [name]() {
return Setup.evaluate(setups.get(name), "get", []);
},
set [name](value: any) {
Setup.evaluate(setups.get(name), "set", [value]);
}
});
}
}
class MethodHandler {
public name: PropertyKey;
constructor(name: PropertyKey) {
this.name = name;
}
public apply(target: Function, thisArgument: any, argumentsList: any[]): any {
const handler = getHandler(thisArgument);
return handler
? handler.invoke(thisArgument, this.name, target, argumentsList)
: Reflect.apply(target, thisArgument, argumentsList);
}
}
class CapturingHandler {
public recording: Recording | undefined;
private _name: PropertyKey;
private _method: Function;
constructor() {
this._method = (...args: any[]) => {
this.recording = new Recording("invoke", this._name, args);
};
}
public apply(_target: object, _thisArg: any, argArray: any[]): any {
this.recording = new Recording("apply", /*name*/ undefined, argArray);
return undefined;
}
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]);
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;
}
finally {
revoke();
}
}

View File

@@ -0,0 +1,38 @@
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

@@ -0,0 +1,103 @@
/**
* Temporarily injects a value into an object property
*/
export class Stub<T, K extends keyof T> {
private _target: T;
private _key: K;
private _value: any;
private _originalValue: any;
private _installed: boolean = false;
/**
* Temporarily injects a value into an object property
* @param target The target object into which to inject a property
* @param propertyKey The name of the property to inject
* @param value The value to inject
*/
constructor(target: T, propertyKey: K, value?: T[K]) {
this._target = target;
this._key = propertyKey;
this._value = arguments.length === 2 ? target[propertyKey] : value;
}
public get target() {
return this._target;
}
public get key() {
return this._key;
}
public get stubValue(): T[K] {
return this._installed ? this.currentValue : this._value;
}
public set stubValue(value: T[K]) {
if (this._installed) {
this._target[this._key] = value;
}
this._value = value;
}
public get originalValue(): T[K] {
if (this._installed) {
return this._originalValue;
}
else {
return this.currentValue;
}
}
public get currentValue(): T[K] {
return this._target[this._key];
}
/**
* Gets a value indicating whether the Stub is currently installed.
*/
public get installed(): boolean {
return this._installed;
}
/**
* Installs the stub
*/
public install(): void {
if (this._installed) return;
this._originalValue = this._target[this._key];
this._target[this._key] = this._value;
this._installed = true;
}
/**
* Uninstalls the stub
*/
public uninstall(): void {
if (!this._installed) return;
this._target[this._key] = this._originalValue;
this._installed = false;
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);
return stub.exec(action);
}
/**
* Executes `action` with the stub installed.
*/
public exec<V>(action: () => V): V {
if (this._installed) {
return action();
}
try {
this.install();
return action();
}
finally {
this.uninstall();
}
}
}

View File

@@ -0,0 +1,646 @@
import "./sourceMapSupport";
import { Arg } from "../arg";
import { assert } from "chai";
describe("arg", () => {
describe("any", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.any());
// act
const result = Arg.validate(target, ["a"], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.any());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<any>`);
});
});
describe("is", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.is(value => value === "a"));
// act
const result = Arg.validate(target, ["a"], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.is(value => value === "a"));
// act
const result = Arg.validate(target, ["b"], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.is(value => value === "a"));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<is>`);
});
});
describe("notNull", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.notNull());
// act
const result = Arg.validate(target, [{}], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.notNull());
// act
const result = Arg.validate(target, [null], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.notNull());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<not null>`);
});
});
describe("null", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.null());
// act
const result = Arg.validate(target, [null], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.null());
// act
const result = Arg.validate(target, [{}], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.null());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<null>`);
});
});
describe("notUndefined", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.notUndefined());
// act
const result = Arg.validate(target, [{}], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.notUndefined());
// act
const result = Arg.validate(target, [undefined], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.notUndefined());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<not undefined>`);
});
});
describe("undefined", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.undefined());
// act
const result = Arg.validate(target, [undefined], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.undefined());
// act
const result = Arg.validate(target, [{}], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.undefined());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<undefined>`);
});
});
describe("notNullOrUndefined", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.notNullOrUndefined());
// act
const result = Arg.validate(target, [{}], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("invalid (null)", () => {
// arrange
const target = Arg.from(Arg.notNullOrUndefined());
// act
const result = Arg.validate(target, [null], 0);
// assert
assert.isFalse(result.valid);
});
it("invalid (undefined)", () => {
// arrange
const target = Arg.from(Arg.notNullOrUndefined());
// act
const result = Arg.validate(target, [undefined], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.notNullOrUndefined());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<not null or undefined>`);
});
});
describe("nullOrUndefined", () => {
it("valid (null)", () => {
// arrange
const target = Arg.from(Arg.nullOrUndefined());
// act
const result = Arg.validate(target, [null], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("valid (undefined)", () => {
// arrange
const target = Arg.from(Arg.nullOrUndefined());
// act
const result = Arg.validate(target, [undefined], 0);
// assert
assert.isTrue(result.valid);
assert.strictEqual(result.next, 1);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.nullOrUndefined());
// act
const result = Arg.validate(target, [{}], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.nullOrUndefined());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<null or undefined>`);
});
});
describe("between", () => {
it("valid", () => {
// arrange
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);
// assert
assert.isTrue(min.valid);
assert.isTrue(mid.valid);
assert.isTrue(max.valid);
});
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);
// assert
assert.isFalse(before.valid);
assert.isFalse(after.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.between(1, 3));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<between 1 and 3>`);
});
});
describe("in", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.in(["a", "b"]));
// act
const result = Arg.validate(target, ["a"], 0);
// assert
assert.isTrue(result.valid);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.in(["a", "b"]));
// act
const result = Arg.validate(target, ["c"], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.in(["a", "b"]));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<in a, b>`);
});
});
describe("notIn", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.notIn(["a", "b"]));
// act
const result = Arg.validate(target, ["c"], 0);
// assert
assert.isTrue(result.valid);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.notIn(["a", "b"]));
// act
const result = Arg.validate(target, ["a"], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.notIn(["a", "b"]));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<not in a, b>`);
});
});
describe("match", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.match(/^a$/));
// act
const result = Arg.validate(target, ["a"], 0);
// assert
assert.isTrue(result.valid);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.match(/^a$/));
// act
const result = Arg.validate(target, ["b"], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.match(/^a$/));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<matches /^a$/>`);
});
});
describe("typeof", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.typeof("number"));
// act
const result = Arg.validate(target, [1], 0);
// assert
assert.isTrue(result.valid);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.typeof("number"));
// act
const result = Arg.validate(target, ["a"], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.typeof("number"));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<typeof number>`);
});
});
describe("instanceof", () => {
it("valid", () => {
// arrange
class C {}
const target = Arg.from(Arg.instanceof(C));
// act
const result = Arg.validate(target, [new C()], 0);
// assert
assert.isTrue(result.valid);
});
it("invalid", () => {
// arrange
class C {}
const target = Arg.from(Arg.instanceof(C));
// act
const result = Arg.validate(target, [{}], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
class C {}
const target = Arg.from(Arg.instanceof(C));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<instanceof C>`);
});
});
describe("has", () => {
it("valid", () => {
// arrange
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);
// assert
assert.isTrue(own.valid);
assert.isTrue(proto.valid);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.has("a"));
// act
const result = Arg.validate(target, [{ b: 1 }], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.has("a"));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<has a>`);
});
});
describe("hasOwn", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.hasOwn("a"));
// act
const own = Arg.validate(target, [{ a: 1 }], 0);
// assert
assert.isTrue(own.valid);
});
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);
// assert
assert.isFalse(result.valid);
assert.isFalse(proto.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.hasOwn("a"));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<hasOwn a>`);
});
});
describe("rest", () => {
describe("no condition", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.rest());
// act
const empty = Arg.validate(target, [], 0);
const multiple = Arg.validate(target, ["a", "b"], 0);
// assert
assert.isTrue(empty.valid);
assert.strictEqual(empty.next, 0);
assert.isTrue(multiple.valid);
assert.strictEqual(multiple.next, 2);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.rest());
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<rest>`);
});
});
describe("condition", () => {
it("valid", () => {
// arrange
const target = Arg.from(Arg.rest(Arg.typeof("string")));
// act
const empty = Arg.validate(target, [], 0);
const multiple = Arg.validate(target, ["a", "b"], 0);
// assert
assert.isTrue(empty.valid);
assert.strictEqual(empty.next, 0);
assert.isTrue(multiple.valid);
assert.strictEqual(multiple.next, 2);
});
it("invalid", () => {
// arrange
const target = Arg.from(Arg.rest(Arg.typeof("string")));
// act
const result = Arg.validate(target, ["a", 1], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from(Arg.rest(Arg.typeof("string")));
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<rest typeof string>`);
});
});
});
describe("from", () => {
it("valid", () => {
// arrange
const target = Arg.from("a");
// act
const result = Arg.validate(target, ["a"], 0);
// assert
assert.isTrue(result.valid);
});
it("invalid", () => {
// arrange
const target = Arg.from("a");
// act
const result = Arg.validate(target, ["b"], 0);
// assert
assert.isFalse(result.valid);
});
it("toString", () => {
// arrange
const target = Arg.from("a");
// act
const result = target.toString();
// assert
assert.strictEqual(result, `<"a">`);
});
});
});

View File

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

View File

@@ -0,0 +1,262 @@
import "./sourceMapSupport";
import { Mock } from "../mock";
import { Stub } from "../stub";
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);
// act
const result = mock.value.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;
}
});
const result = mock.value.a(3);
// act
const e = recordError(() => mock.verify(_ => _.a(Arg.number()), Times.once()));
// assert
assert.isUndefined(e);
});
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);
// 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;
// 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 result = mock.value.a;
const err = recordError(() => mock.value.a = 3);
// 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 err1 = recordError(() => mock.value.a);
const err2 = recordError(() => mock.value.a = 2);
const err3 = recordError(() => mock.value.a = 3);
// 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 result = mock.value(1);
// assert
assert.strictEqual(result, 2);
});
});

View File

@@ -0,0 +1,3 @@
import { install } from "source-map-support";
install();

View File

@@ -0,0 +1,79 @@
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

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

View File

@@ -0,0 +1,236 @@
import "./sourceMapSupport";
import { Times } from "../times";
import { theory, recordError } from "./utils";
import { assert } from "chai";
describe("times", () => {
function makeTimesNoneValidationData(): any[][]{
return [
[0, true],
[1, false]
];
}
theory("Times.none validation", makeTimesNoneValidationData, function (count: number, expected: boolean): void {
// arrange
const times = Times.none();
// act
const result = times.validate(count);
// assert
assert.equal(expected, result);
});
function makeTimesOnceValidationData(): any[][]{
return [
[0, false],
[1, true],
[2, false]
];
}
theory("Times.once validation", makeTimesOnceValidationData, function (count: number, expected: boolean): void {
// arrange
const times = Times.once();
// act
const result = times.validate(count);
// assert
assert.equal(expected, result);
});
function makeTimesAtLeastOnceValidationData(): any[] {
return [
[0, false],
[1, true],
[2, true]
];
}
theory("Times.atLeastOnce validation", makeTimesAtLeastOnceValidationData, function (count: number, expected: boolean): void {
// arrange
const times = Times.atLeastOnce();
// act
const result = times.validate(count);
// assert
assert.equal(expected, result);
});
function makeTimesAtMostOnceValidationData(): any[][]{
return [
[0, true],
[1, true],
[2, false]
];
}
theory("Times.atMostOnce validation", makeTimesAtMostOnceValidationData, function (count: number, expected: boolean): void {
// arrange
const times = Times.atMostOnce();
// act
const result = times.validate(count);
// assert
assert.equal(expected, result);
});
function makeTimesExactlyValidationData(): any[][]{
return [
[0, 0, true],
[0, 1, false],
[1, 0, false],
[1, 1, true]];
}
theory("Times.exactly validation", makeTimesExactlyValidationData, function (expectedCount: number, count: number, expectedResult: boolean): void {
// arrange
const times = Times.exactly(expectedCount);
// act
const result = times.validate(count);
// assert
assert.equal(expectedResult, result);
});
function makeTimesAtLeastValidationData(): any[][]{
return [
[0, 0, true],
[0, 1, true],
[1, 0, false],
[1, 1, true],
[1, 2, true]
];
}
theory("Times.atLeast validation", makeTimesAtLeastValidationData, function (expectedCount: number, count: number, expectedResult: boolean): void {
// arrange
const times = Times.atLeast(expectedCount);
// act
const result = times.validate(count);
// assert
assert.equal(expectedResult, result);
});
function makeTimesAtMostValidationData(): any[][]{
return [
[0, 0, true],
[0, 1, false],
[1, 0, true],
[1, 1, true],
[1, 2, false]
];
}
theory("Times.atMost validation", makeTimesAtMostValidationData, function (expectedCount: number, count: number, expectedResult: boolean): void {
// arrange
const times = Times.atMost(expectedCount);
// act
const result = times.validate(count);
// assert
assert.equal(expectedResult, result);
});
function makeTimesBetweenValidationData(): any[][]{
return [
[1, 2, 0, false],
[1, 2, 1, true],
[1, 2, 2, true],
[1, 2, 3, false]
];
}
theory("Times.between validation", makeTimesBetweenValidationData, function (min: number, max: number, count: number, expectedResult: boolean): void {
// arrange
const times = Times.between(min, max);
// act
const result = times.validate(count);
// assert
assert.equal(expectedResult, result);
});
function makeTimesToStringData(): any[][]{
return [
[Times.none(), "<never>"],
[Times.once(), "<exactly once>"],
[Times.atLeastOnce(), "<at least once>"],
[Times.atMostOnce(), "<at most once>"],
[Times.atLeast(2), "<at least 2 time(s)>"],
[Times.atMost(2), "<at most 2 time(s)>"],
[Times.exactly(2), "<exactly 2 time(s)>"],
[Times.between(1, 2), "<between 1 and 2 time(s)>"]
];
}
theory("Times.toString", makeTimesToStringData, function (times: Times, expected: string): void {
// arrange
// act
const result = times.toString();
// assert
assert.equal(expected, result);
});
function makeTimesCheckThrowsData(): any[][]{
return [
[Times.none(), 1],
[Times.once(), 0],
[Times.once(), 2],
[Times.atLeastOnce(), 0],
[Times.atMostOnce(), 2],
[Times.atLeast(2), 1],
[Times.atMost(2), 3],
[Times.exactly(1), 0],
[Times.exactly(1), 2],
[Times.between(1, 2), 0],
[Times.between(1, 2), 3]
]
}
theory("Times.check throws", makeTimesCheckThrowsData, (times: Times, count: number) => {
// arrange
// act
const e = recordError(() => times.check(count, "test"));
// assert
assert.instanceOf(e, Error);
});
function makeTimesCheckPassesData(): any[][] {
return [
[Times.none(), 0],
[Times.once(), 1],
[Times.atLeastOnce(), 1],
[Times.atLeastOnce(), 2],
[Times.atMostOnce(), 1],
[Times.atMostOnce(), 0],
[Times.atLeast(2), 2],
[Times.atLeast(2), 3],
[Times.atMost(2), 2],
[Times.atMost(2), 1],
[Times.exactly(1), 1],
[Times.between(1, 2), 1],
[Times.between(1, 2), 2]
];
}
theory("Times.check passes", makeTimesCheckPassesData, (times: Times, count: number) => {
// arrange
// act
const e = recordError(() => times.check(count, "test"));
// assert
assert.isUndefined(e);
});
});

View File

@@ -0,0 +1,17 @@
export function theory(name: string, data: any[][] | (() => any[][]), callback: (...args: any[]) => any) {
describe(name, () => {
for (const row of typeof data === "function" ? data() : data) {
it(row.toString(), () => callback(...row));
}
});
}
export function recordError(action: () => void): Error | undefined {
try {
action();
return undefined;
}
catch (e) {
return e;
}
}

View File

@@ -0,0 +1,475 @@
export interface Immediate {
readonly kind: "immediate";
readonly handle: number;
readonly callback: (...args: any[]) => void;
readonly args: ReadonlyArray<any>;
}
export interface Timeout {
readonly kind: "timeout";
readonly handle: number;
readonly callback: (...args: any[]) => void;
readonly args: ReadonlyArray<any>;
}
export interface Interval {
readonly kind: "interval";
readonly handle: number;
readonly callback: (...args: any[]) => void;
readonly args: ReadonlyArray<any>;
readonly interval: number;
}
export interface AnimationFrame {
readonly kind: "frame";
readonly handle: number;
readonly callback: (time: number) => void;
}
export type Timer = Immediate | Timeout | Interval | AnimationFrame;
type NonImmediateTimer = Timeout | Interval | AnimationFrame;
interface Due<T extends Timer> {
timer: T;
due: number;
depth?: number;
enabled?: boolean;
timeline?: boolean;
}
const MAX_INT32 = 2 ** 31 - 1;
const MIN_TIMEOUT_VALUE = 4;
const CLAMP_TIMEOUT_NESTING_LEVEL = 5;
/**
* Programmatic control over timers.
*/
export class Timers {
public static readonly MAX_DEPTH = MAX_INT32;
private _nextHandle = 1;
private _immediates = new Map<number, Due<Immediate>>();
private _timeouts = new Map<number, Due<Timeout>>();
private _intervals = new Map<number, Due<Interval>>();
private _frames = new Map<number, Due<AnimationFrame>>();
private _timeline: Due<NonImmediateTimer>[] = [];
private _time: number;
private _depth = 0;
constructor() {
this._time = 0;
// bind each timer method so that it can be detached from this instance.
this.setImmediate = this.setImmediate.bind(this);
this.clearImmedate = this.clearImmedate.bind(this);
this.setTimeout = this.setTimeout.bind(this);
this.clearTimeout = this.clearTimeout.bind(this);
this.setInterval = this.setInterval.bind(this);
this.clearInterval = this.clearInterval.bind(this);
this.requestAnimationFrame = this.requestAnimationFrame.bind(this);
this.cancelAnimationFrame = this.cancelAnimationFrame.bind(this);
}
/**
* Get the current time.
*/
public get time(): number {
return this._time;
}
/**
* Gets the time of the last scheduled timer (not including repeating intervals).
*/
public get endTime(): number {
return this._timeline && this._timeline.length > 0
? this._timeline[this._timeline.length - 1].due
: this._time;
}
/**
* Gets the estimated time remaining.
*/
public get remainingTime(): number {
return this.endTime - this.time;
}
public getPending(options: { kind: "immediate", ms?: number }): Immediate[];
public getPending(options: { kind: "timeout", ms?: number }): Timeout[];
public getPending(options: { kind: "interval", ms?: number }): Interval[];
public getPending(options: { kind: "frame", ms?: number }): AnimationFrame[];
public getPending(options?: { kind?: Timer["kind"], ms?: number }): Timer[];
public getPending(options: { kind?: Timer["kind"], ms?: number } = {}): Timer[] {
const { kind, ms = 0 } = options;
if (ms < 0) throw new TypeError("Argument 'ms' out of range.");
const dueTimers: Due<Timer>[] = [];
if (!kind || kind === "immediate") {
this.copyImmediates(dueTimers);
}
if (kind !== "immediate") {
this.copyTimelineBefore(dueTimers, this._time + ms, kind);
}
return dueTimers.map(dueTimer => dueTimer.timer);
}
/**
* Advance the current time and trigger callbacks, returning the number of callbacks triggered.
* @param ms The number of milliseconds to advance.
* @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing.
* - Use `0` (default) to disable processing of nested `setImmediate` calls.
* - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth.
*/
public advance(ms: number, maxDepth = 0): number {
if (ms <= 0) throw new TypeError("Argument 'ms' out of range.");
if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range.");
let count = 0;
const endTime = this._time + (ms | 0);
while (true) {
count += this.executeImmediates(maxDepth);
const dueTimer = this.dequeueIfBefore(endTime);
if (dueTimer) {
this._time = dueTimer.due;
this.executeTimer(dueTimer);
count++;
}
else {
this._time = endTime;
return count;
}
}
}
/**
* Advance the current time to the estimated end time and trigger callbacks, returning the number of callbacks triggered.
* @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing.
* - Use `0` (default) to disable processing of nested `setImmediate` calls.
* - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth.
*/
public advanceToEnd(maxDepth = 0) {
return this.remainingTime > 0 ? this.advance(this.remainingTime, maxDepth) : 0;
}
/**
* Execute any pending immediate timers, returning the number of timers triggered.
* @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing.
* - Use `0` (default) to disable processing of nested `setImmediate` calls.
* - Use `Timers.MAX_DEPTH` to continue processing nested `setImmediate` calls up to the maximum depth.
*/
public executeImmediates(maxDepth = 0): number {
if ((maxDepth |= 0) < 0) throw new TypeError("Argument 'maxDepth' out of range.");
const dueTimers: Due<Timer>[] = [];
this.copyImmediates(dueTimers);
let count = this.executeTimers(dueTimers);
for (let depth = 0; depth < maxDepth && this._immediates.size > 0; depth++) {
count += this.executeImmediates();
}
return count;
}
public setImmediate(callback: (...args: any[]) => void, ...args: any[]): any {
if (this._depth >= Timers.MAX_DEPTH) {
throw new Error("callback nested too deeply.");
}
const timer: Immediate = { kind: "immediate", handle: this._nextHandle++, callback, args };
const dueTimer: Due<Immediate> = { timer, due: -1 };
this.addTimer(this._immediates, dueTimer);
return timer.handle;
}
public clearImmedate(timerId: any): void {
const dueTimer = this._immediates.get(timerId);
if (dueTimer) {
this.deleteTimer(this._immediates, dueTimer);
}
}
public setTimeout(callback: (...args: any[]) => void, timeout: number, ...args: any[]): any {
if (this._depth >= Timers.MAX_DEPTH) {
throw new Error("callback nested too deeply.");
}
if ((timeout |= 0) < 0) timeout = 0;
if (this._depth >= CLAMP_TIMEOUT_NESTING_LEVEL && timeout < MIN_TIMEOUT_VALUE) {
timeout = MIN_TIMEOUT_VALUE;
}
const timer: Timeout = { kind: "timeout", handle: this._nextHandle++, callback, args };
const dueTimer: Due<Timeout> = { timer, due: this._time + timeout };
this.addTimer(this._timeouts, dueTimer);
this.addToTimeline(dueTimer);
return timer.handle;
}
public clearTimeout(timerId: any): void {
const dueTimer = this._timeouts.get(timerId);
if (dueTimer) {
this.deleteTimer(this._timeouts, dueTimer);
this.removeFromTimeline(dueTimer);
}
}
public setInterval(callback: (...args: any[]) => void, interval: number, ...args: any[]): any {
if (this._depth >= Timers.MAX_DEPTH) {
throw new Error("callback nested too deeply.");
}
if ((interval |= 0) < 10) interval = 10;
const timer: Interval = { kind: "interval", handle: this._nextHandle++, callback, args, interval };
const dueTimer: Due<Interval> = { timer, due: this._time + interval };
this.addTimer(this._intervals, dueTimer);
this.addToTimeline(dueTimer);
return timer.handle;
}
public clearInterval(timerId: any): void {
const dueTimer = this._intervals.get(timerId);
if (dueTimer) {
this.deleteTimer(this._intervals, dueTimer);
this.removeFromTimeline(dueTimer);
}
}
public requestAnimationFrame(callback: (time: number) => void): any {
if (this._depth >= Timers.MAX_DEPTH) {
throw new Error("callback nested too deeply.");
}
const timer: AnimationFrame = { kind: "frame", handle: this._nextHandle++, callback };
const dueTimer: Due<AnimationFrame> = { timer, due: this.nextFrameDueTime() };
this.addTimer(this._frames, dueTimer);
this.addToTimeline(dueTimer);
return timer.handle;
}
public cancelAnimationFrame(timerId: any): void {
const dueTimer = this._frames.get(timerId);
if (dueTimer) {
this.deleteTimer(this._frames, dueTimer);
this.removeFromTimeline(dueTimer);
}
}
private nextFrameDueTime() {
return this._time + this.nextFrameDelta();
}
private nextFrameDelta() {
return 16 - this._time % 16;
}
private addTimer<T extends Timer>(timers: Map<number, Due<T>>, dueTimer: Due<T>) {
if (dueTimer.enabled) return;
timers.set(dueTimer.timer.handle, dueTimer);
dueTimer.depth = this._depth + 1;
dueTimer.enabled = true;
}
private deleteTimer<T extends Timer>(timers: Map<number, Due<T>>, dueTimer: Due<T>) {
if (!dueTimer.enabled) return;
timers.delete(dueTimer.timer.handle);
dueTimer.enabled = false;
}
private executeTimers(dueTimers: Due<Timer>[]) {
let count = 0;
for (const dueTimer of dueTimers) {
this.executeTimer(dueTimer);
count++;
}
return count;
}
private executeTimer(dueTimer: Due<Timer>) {
switch (dueTimer.timer.kind) {
case "immediate": return this.executeImmediate(<Due<Immediate>>dueTimer);
case "timeout": return this.executeTimeout(<Due<Timeout>>dueTimer);
case "interval": return this.executeInterval(<Due<Interval>>dueTimer);
case "frame": return this.executeAnimationFrame(<Due<AnimationFrame>>dueTimer);
}
}
private executeImmediate(dueTimer: Due<Immediate>) {
if (!dueTimer.enabled) return;
this.deleteTimer(this._immediates, dueTimer);
this.executeCallback(dueTimer.depth, dueTimer.timer.callback, ...dueTimer.timer.args);
}
private executeTimeout(dueTimer: Due<Timeout>) {
if (!dueTimer.enabled) return;
this.deleteTimer(this._timeouts, dueTimer);
this.removeFromTimeline(dueTimer);
this.executeCallback(dueTimer.depth, dueTimer.timer.callback, ...dueTimer.timer.args);
}
private executeInterval(dueTimer: Due<Interval>) {
if (!dueTimer.enabled) return;
this.removeFromTimeline(dueTimer);
this.executeCallback(dueTimer.depth, dueTimer.timer.callback, ...dueTimer.timer.args);
if (dueTimer.enabled) {
dueTimer.due += dueTimer.timer.interval;
this.addToTimeline(dueTimer);
}
}
private executeAnimationFrame(dueTimer: Due<AnimationFrame>) {
if (!dueTimer.enabled) return;
this.deleteTimer(this._frames, dueTimer);
this.removeFromTimeline(dueTimer);
this.executeCallback(dueTimer.depth, dueTimer.timer.callback, this._time);
}
private executeCallback(depth = 0, callback: (...args: any[]) => void, ...args: any[]) {
const savedDepth = this._depth;
this._depth = depth;
try {
callback(...args);
}
finally {
this._depth = savedDepth;
}
}
private dequeueIfBefore(dueTime: number) {
if (this._timeline.length > 0) {
const dueTimer = this._timeline[0];
if (dueTimer.due <= dueTime) {
this._timeline.shift();
dueTimer.timeline = false;
return dueTimer;
}
}
}
private copyImmediates(dueTimers: Due<Timer>[]) {
for (const dueTimer of this._immediates.values()) {
dueTimers.push(dueTimer);
}
}
private copyTimelineBefore(dueTimers: Due<Timer>[], dueTime: number, kind?: Timer["kind"]) {
for (const dueTimer of this._timeline) {
if (dueTimer.due <= dueTime && (!kind || dueTimer.timer.kind === kind)) {
dueTimers.push(dueTimer);
}
}
}
private addToTimeline(dueTimer: Due<NonImmediateTimer>) {
if (dueTimer.timeline) return;
let index = binarySearch(this._timeline, dueTimer, getDueTime, compareTimestamps);
if (index < 0) {
index = ~index;
}
else {
while (index < this._timeline.length) {
if (this._timeline[index].due > dueTimer.due) {
break;
}
index++;
}
}
insertAt(this._timeline, index, dueTimer);
dueTimer.timeline = true;
}
private removeFromTimeline(dueTimer: Due<NonImmediateTimer>) {
if (dueTimer.timeline) {
let index = binarySearch(this._timeline, dueTimer, getDueTime, compareTimestamps);
if (index >= 0) {
while (index < this._timeline.length) {
const event = this._timeline[index];
if (event === dueTimer) {
removeAt(this._timeline, index);
dueTimer.timeline = false;
return true;
}
if (event.due > dueTimer.due) {
break;
}
index++;
}
}
}
return false;
}
}
function getDueTime(v: Due<Timer>) {
return v.due;
}
function compareTimestamps(a: number, b: number) {
return a - b;
}
function binarySearch<T, U>(array: ReadonlyArray<T>, value: T, keySelector: (v: T) => U, keyComparer: (a: U, b: U) => number): number {
if (array.length === 0) {
return -1;
}
let low = 0;
let high = array.length - 1;
const key = keySelector(value);
while (low <= high) {
const middle = low + ((high - low) >> 1);
const midKey = keySelector(array[middle]);
const result = keyComparer(midKey, key);
if (result < 0) {
low = middle + 1;
}
else if (result > 0) {
high = middle - 1;
}
else {
return middle;
}
}
return ~low;
}
function removeAt<T>(array: T[], index: number): void {
if (array.length === 0) {
return;
}
else if (index === 0) {
array.shift();
}
else if (index === array.length - 1) {
array.pop();
}
else {
for (let i = index; i < array.length - 1; i++) {
array[i] = array[i + 1];
}
array.length--;
}
}
function insertAt<T>(array: T[], index: number, value: T): void {
if (index === 0) {
array.unshift(value);
}
else if (index === array.length) {
array.push(value);
}
else {
for (let i = array.length; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = value;
}
}

View File

@@ -0,0 +1,120 @@
/**
* Defines the number of times an action must have been executed during verification of a Mock.
*/
export class Times {
private static _none: Times | undefined;
private static _once: Times | undefined;
private static _atLeastOnce: Times | undefined;
private static _atMostOnce: Times | undefined;
private _min: number;
private _max: number;
private _message: string;
private constructor(min: number, max: number, message: string) {
this._min = min;
this._max = max;
this._message = message;
}
/**
* Expects that an action was never executed.
* @returns A new `Times` instance.
*/
public static none(): Times {
return this._none || (this._none = new Times(0, 0, `never`));
}
/**
* Expects that an action was executed exactly once.
* @returns A new `Times` instance.
*/
public static once(): Times {
return this._once || (this._once = new Times(1, 1, `exactly once`));
}
/**
* Expects that an action was executed at least once.
* @returns A new `Times` instance.
*/
public static atLeastOnce(): Times {
return this._atLeastOnce || (this._atLeastOnce = new Times(1, Number.MAX_SAFE_INTEGER, `at least once`));
}
/**
* Expects that an action was executed at least the specified number of times.
* @param count The number of times.
* @returns A new `Times` instance.
*/
public static atLeast(count: number): Times {
return new Times(count, Number.MAX_SAFE_INTEGER, `at least ${count} time(s)`);
}
/**
* Expects that an action was executed exactly the specified number of times.
* @param count The number of times.
* @returns A new `Times` instance.
*/
public static exactly(count: number): Times {
return new Times(count, count, `exactly ${count} time(s)`);
}
/**
* Expects that an action was executed at most the specified number of times.
* @param count The number of times.
* @returns A new `Times` instance.
*/
public static atMost(count: number): Times {
return new Times(0, count, `at most ${count} time(s)`);
}
/**
* Expects that an action was executed at most once.
* @returns A new `Times` instance.
*/
public static atMostOnce(): Times {
return this._atMostOnce || (this._atMostOnce = new Times(0, 1, `at most once`));
}
/**
* Expects that an action was executed between a range of times, inclusive.
* @param min The minimum number of times, inclusive.
* @param max The maximum number of times, inclusive.
* @returns A new `Times` instance.
*/
public static between(min: number, max: number): Times {
return new Times(min, max, `between ${min} and ${max} time(s)`);
}
/**
* Validates the number of times an action was executed.
* @param count The number of times the action was executed.
* @returns `true` if the provided count was valid; otherwise, `false`.
*/
public validate(count: number): boolean {
if (count < this._min) return false;
if (count > this._max) return false;
return true;
}
/**
* Checks the number of times an action was executed, throwing an error if the count was not valid.
* @param count The number of times the action was executed.
* @param message The message to use to begin the check.
*/
public check(count: number, message: string): void {
if (!this.validate(count)) {
const expectedMessage = this._message === `never`
? `Expected to never be executed.`
: `Expected to be executed ${this._message}.`;
throw new Error(`${message}\n${expectedMessage} Actually executed ${count} time(s).`);
}
}
/**
* Gets the string representation of this object.
*/
public toString(): string {
return `<${this._message}>`;
}
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"strict": true,
"declaration": true,
"sourceMap": true,
"types": ["mocha"]
}
}