Migrate tscWatchMode to vfs
This commit is contained in:
Родитель
3d3977f2b7
Коммит
41567b2261
|
@ -46,6 +46,7 @@ scripts/importDefinitelyTypedTests/importDefinitelyTypedTests.js
|
|||
scripts/generateLocalizedDiagnosticMessages.js
|
||||
scripts/*.js.map
|
||||
scripts/typings/
|
||||
scripts/typemock/dist
|
||||
coverage/
|
||||
internal/
|
||||
**/.DS_Store
|
||||
|
|
|
@ -1,30 +1,34 @@
|
|||
// Available variables which can be used inside of strings.
|
||||
// ${workspaceRoot}: the root folder of the team
|
||||
// ${file}: the current opened file
|
||||
// ${fileBasename}: the current opened file's basename
|
||||
// ${fileDirname}: the current opened file's dirname
|
||||
// ${fileExtname}: the current opened file's extension
|
||||
// ${cwd}: the current working directory of the spawned process
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"command": "gulp",
|
||||
"isShellCommand": true,
|
||||
"showOutput": "silent",
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"taskName": "local",
|
||||
"isBuildCommand": true,
|
||||
"showOutput": "silent",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
]
|
||||
"type": "shell",
|
||||
"identifier": "local",
|
||||
"label": "gulp: local",
|
||||
"command": "gulp",
|
||||
"args": ["local"],
|
||||
"group": { "kind": "build", "isDefault": true },
|
||||
"problemMatcher": ["$gulp-tsc"]
|
||||
},
|
||||
{
|
||||
"taskName": "tests",
|
||||
"showOutput": "silent",
|
||||
"problemMatcher": [
|
||||
"$tsc"
|
||||
]
|
||||
"type": "shell",
|
||||
"identifier": "tsc",
|
||||
"label": "gulp: tsc",
|
||||
"command": "gulp",
|
||||
"args": ["tsc"],
|
||||
"group": "build",
|
||||
"problemMatcher": ["$gulp-tsc"]
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"identifier": "tests",
|
||||
"label": "gulp: tests",
|
||||
"command": "gulp",
|
||||
"args": ["tests"],
|
||||
"group": "build",
|
||||
"problemMatcher": ["$gulp-tsc"]
|
||||
}
|
||||
]
|
||||
}
|
27
Gulpfile.ts
27
Gulpfile.ts
|
@ -602,10 +602,19 @@ gulp.task("LKG", "Makes a new LKG out of the built js files", ["clean", "dontUse
|
|||
return runSequence("LKGInternal", "VerifyLKG");
|
||||
});
|
||||
|
||||
gulp.task("typemock", () => {
|
||||
const typemock = tsc.createProject("scripts/typemock/src/tsconfig.json", getCompilerSettings({}, /*useBuiltCompiler*/ true));
|
||||
return typemock.src()
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(newer("scripts/typemock/dist"))
|
||||
.pipe(typemock())
|
||||
.pipe(sourcemaps.write(".", <any>{ includeContent: false, destPath: "scripts/typemock/dist" }))
|
||||
.pipe(gulp.dest("scripts/typemock/dist"));
|
||||
});
|
||||
|
||||
// Task to build the tests infrastructure using the built compiler
|
||||
const run = path.join(builtLocalDirectory, "run.js");
|
||||
gulp.task(run, /*help*/ false, [servicesFile, tsserverLibraryFile], () => {
|
||||
gulp.task(run, /*help*/ false, [servicesFile, tsserverLibraryFile, "typemock"], () => {
|
||||
const testProject = tsc.createProject("src/harness/tsconfig.json", getCompilerSettings({}, /*useBuiltCompiler*/ true));
|
||||
return testProject.src()
|
||||
.pipe(newer(run))
|
||||
|
@ -644,7 +653,7 @@ function restoreSavedNodeEnv() {
|
|||
process.env.NODE_ENV = savedNodeEnv;
|
||||
}
|
||||
|
||||
function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: (e?: any) => void) {
|
||||
function runConsoleTests(defaultReporter: string, runInParallel: boolean, done: (e?: any) => void, noExit?: boolean) {
|
||||
const lintFlag = cmdLineOptions.lint;
|
||||
cleanTestDirs((err) => {
|
||||
if (err) { console.error(err); failWithStatus(err, 1); }
|
||||
|
@ -720,8 +729,10 @@ function runConsoleTests(defaultReporter: string, runInParallel: boolean, done:
|
|||
});
|
||||
|
||||
function failWithStatus(err?: any, status?: number) {
|
||||
if (err || status) {
|
||||
process.exit(typeof status === "number" ? status : 2);
|
||||
if (!noExit) {
|
||||
if (err || status) {
|
||||
process.exit(typeof status === "number" ? status : 2);
|
||||
}
|
||||
}
|
||||
done();
|
||||
}
|
||||
|
@ -762,6 +773,10 @@ gulp.task("runtests",
|
|||
runConsoleTests("mocha-fivemat-progress-reporter", /*runInParallel*/ false, done);
|
||||
});
|
||||
|
||||
gulp.task("runtests-in-watch", ["build-rules", "tests"], done => {
|
||||
runConsoleTests("min", /*runInParallel*/ false, done, /*noExit*/ true);
|
||||
});
|
||||
|
||||
const nodeServerOutFile = "tests/webTestServer.js";
|
||||
const nodeServerInFile = "tests/webTestServer.ts";
|
||||
gulp.task(nodeServerOutFile, /*help*/ false, [servicesFile], () => {
|
||||
|
@ -1110,3 +1125,7 @@ gulp.task("default", "Runs 'local'", ["local"]);
|
|||
gulp.task("watch", "Watches the src/ directory for changes and executes runtests-parallel.", [], () => {
|
||||
gulp.watch("src/**/*.*", ["runtests-parallel"]);
|
||||
});
|
||||
|
||||
gulp.task("watch-no-parallel", "Watches the src/ directory for changes and executes runtests.", [], () => {
|
||||
gulp.watch(["src/**/*.*", "scripts/typemock/src/**/*.*"], ["runtests-in-watch"]);
|
||||
});
|
|
@ -48,12 +48,13 @@
|
|||
"@types/node": "latest",
|
||||
"@types/q": "latest",
|
||||
"@types/run-sequence": "latest",
|
||||
"@types/source-map-support": "^0.4.0",
|
||||
"@types/through2": "latest",
|
||||
"@types/xml2js": "^0.4.0",
|
||||
"xml2js": "^0.4.19",
|
||||
"browser-resolve": "^1.11.2",
|
||||
"browserify": "latest",
|
||||
"chai": "latest",
|
||||
"colors": "latest",
|
||||
"convert-source-map": "latest",
|
||||
"del": "latest",
|
||||
"gulp": "3.X",
|
||||
|
@ -79,9 +80,9 @@
|
|||
"travis-fold": "latest",
|
||||
"ts-node": "latest",
|
||||
"tslint": "latest",
|
||||
"typescript": "next",
|
||||
"vinyl": "latest",
|
||||
"colors": "latest",
|
||||
"typescript": "next"
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"scripts": {
|
||||
"pretest": "jake tests",
|
||||
|
|
|
@ -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"]);
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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";
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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">`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import "./argTests";
|
||||
import "./timesTests";
|
||||
import "./mockTests";
|
||||
import "./stubTests";
|
||||
import "./timersTests";
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import { install } from "source-map-support";
|
||||
|
||||
install();
|
|
@ -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());
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}>`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"types": ["mocha"]
|
||||
}
|
||||
}
|
|
@ -191,6 +191,10 @@ namespace core {
|
|||
}
|
||||
}
|
||||
|
||||
public [Symbol.iterator]() {
|
||||
return this.entries();
|
||||
}
|
||||
|
||||
private writePreamble() {
|
||||
if (this._copyOnWrite) {
|
||||
this._keys = this._keys.slice();
|
||||
|
@ -211,6 +215,109 @@ namespace core {
|
|||
}
|
||||
}
|
||||
|
||||
export class SortedSet<T> implements ReadonlySet<T> {
|
||||
private _comparer: (a: T, b: T) => number;
|
||||
private _values: T[] = [];
|
||||
private _version = 0;
|
||||
private _copyOnWrite = false;
|
||||
|
||||
constructor(comparer: (a: T, b: T) => number) {
|
||||
this._comparer = comparer;
|
||||
}
|
||||
|
||||
public get size() {
|
||||
return this._values.length;
|
||||
}
|
||||
|
||||
public has(value: T) {
|
||||
return binarySearch(this._values, value, identity, this._comparer) >= 0;
|
||||
}
|
||||
|
||||
public add(value: T) {
|
||||
const index = binarySearch(this._values, value, identity, this._comparer);
|
||||
if (index < 0) {
|
||||
this.writePreamble();
|
||||
insertAt(this._values, ~index, value);
|
||||
this.writePostScript();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public delete(value: T) {
|
||||
const index = binarySearch(this._values, value, identity, this._comparer);
|
||||
if (index >= 0) {
|
||||
this.writePreamble();
|
||||
removeAt(this._values, index);
|
||||
this.writePostScript();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
if (this.size > 0) {
|
||||
this.writePreamble();
|
||||
this._values.length = 0;
|
||||
this.writePostScript();
|
||||
}
|
||||
}
|
||||
|
||||
public forEach(callback: (value: T, key: T, collection: this) => void) {
|
||||
const values = this._values;
|
||||
const version = this._version;
|
||||
this._copyOnWrite = true;
|
||||
for (const value of values) {
|
||||
callback(value, value, this);
|
||||
}
|
||||
if (version === this._version) {
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
public keys() {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
public * values() {
|
||||
const values = this._values;
|
||||
const version = this._version;
|
||||
this._copyOnWrite = true;
|
||||
for (const value of values) {
|
||||
yield value;
|
||||
}
|
||||
if (version === this._version) {
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
public * entries() {
|
||||
const values = this._values;
|
||||
const version = this._version;
|
||||
this._copyOnWrite = true;
|
||||
for (const value of values) {
|
||||
yield [value, value] as [T, T];
|
||||
}
|
||||
if (version === this._version) {
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
public [Symbol.iterator]() {
|
||||
return this.values();
|
||||
}
|
||||
|
||||
private writePreamble() {
|
||||
if (this._copyOnWrite) {
|
||||
this._values = this._values.slice();
|
||||
this._copyOnWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
private writePostScript() {
|
||||
this._version++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of metadata that supports inheritance.
|
||||
*/
|
||||
|
@ -314,11 +421,21 @@ namespace core {
|
|||
}
|
||||
|
||||
export function removeAt<T>(array: T[], index: number): void {
|
||||
for (let i = index; i < array.length - 1; i++) {
|
||||
array[i] = array[i + 1];
|
||||
if (index < 0 || index >= array.length) {
|
||||
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--;
|
||||
}
|
||||
|
||||
array.length--;
|
||||
}
|
||||
|
||||
export function insertAt<T>(array: T[], index: number, value: T): void {
|
||||
|
|
|
@ -1,209 +1,98 @@
|
|||
/// <reference path="./core.ts" />
|
||||
/// <reference path="./vfs.ts" />import { debug } from "util";
|
||||
|
||||
|
||||
/// <reference path="./utils.ts" />
|
||||
/// <reference path="./vfs.ts" />
|
||||
/// <reference path="./typemock.ts" />
|
||||
|
||||
// NOTE: The contents of this file are all exported from the namespace 'mocks'. This is to
|
||||
// support the eventual conversion of harness into a modular system.
|
||||
|
||||
// harness mocks
|
||||
namespace mocks {
|
||||
const MAX_INT32 = 2 ** 31 - 1;
|
||||
|
||||
export interface Immediate {
|
||||
readonly kind: "immediate";
|
||||
readonly callback: (...args: any[]) => void;
|
||||
readonly args: ReadonlyArray<any>;
|
||||
}
|
||||
|
||||
export interface Timeout {
|
||||
readonly kind: "timeout";
|
||||
readonly callback: (...args: any[]) => void;
|
||||
readonly args: ReadonlyArray<any>;
|
||||
readonly due: number;
|
||||
}
|
||||
|
||||
export interface Interval {
|
||||
readonly kind: "interval";
|
||||
readonly callback: (...args: any[]) => void;
|
||||
readonly args: ReadonlyArray<any>;
|
||||
readonly due: number;
|
||||
readonly interval: number;
|
||||
}
|
||||
|
||||
export type Timer = Immediate | Timeout | Interval;
|
||||
|
||||
interface InternalInterval extends Interval {
|
||||
due: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatic control over timers.
|
||||
*/
|
||||
export class Timers {
|
||||
public static readonly MAX_DEPTH = MAX_INT32;
|
||||
private _immediates = new Set<Immediate>();
|
||||
private _timeouts = new Set<Timeout>();
|
||||
private _intervals = new Set<InternalInterval>();
|
||||
private _time: number;
|
||||
|
||||
constructor(startTime = Date.now()) {
|
||||
this._time = startTime;
|
||||
|
||||
// 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.clearImmedate.bind(this);
|
||||
this.setInterval = this.setInterval.bind(this);
|
||||
this.clearInterval = this.clearInterval.bind(this);
|
||||
}
|
||||
|
||||
export interface MockServerHostOptions {
|
||||
/**
|
||||
* Get the current time.
|
||||
* The `VirtualFleSystem` to use. If not specified, a new case-sensitive `VirtualFileSystem`
|
||||
* is created.
|
||||
*/
|
||||
public get time(): number {
|
||||
return this._time;
|
||||
}
|
||||
|
||||
public getPending(kind: "immediate", ms?: number): Immediate[];
|
||||
public getPending(kind: "timeout", ms?: number): Timeout[];
|
||||
public getPending(kind: "interval", ms?: number): Interval[];
|
||||
public getPending(kind?: Timer["kind"], ms?: number): Timer[];
|
||||
public getPending(kind?: Timer["kind"], ms = 0) {
|
||||
if (ms < 0) throw new TypeError("Argument 'ms' out of range.");
|
||||
const pending: Timer[] = [];
|
||||
if (!kind || kind === "immediate") this.appendImmediates(pending);
|
||||
if (!kind || kind === "timeout") this.appendDueTimeouts(pending, this._time + ms);
|
||||
if (!kind || kind === "interval") this.appendDueIntervals(pending, this._time + ms, /*expand*/ false);
|
||||
return core.stableSort(pending, compareTimers);
|
||||
}
|
||||
|
||||
vfs?: vfs.VirtualFileSystem | { currentDirectory?: string, useCaseSensitiveFileNames?: boolean };
|
||||
/**
|
||||
* 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 `Timer.NO_MAX_DEPTH` to continue processing all nested `setImmediate` calls.
|
||||
* The virtual path to tsc.js. If not specified, a default of `"/.ts/tsc.js"` is used.
|
||||
*/
|
||||
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.");
|
||||
this._time += ms;
|
||||
return this.executePending(maxDepth);
|
||||
}
|
||||
|
||||
executingFilePath?: string;
|
||||
/**
|
||||
* Execute any pending 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 `Timer.NO_MAX_DEPTH` to continue processing all nested `setImmediate` calls.
|
||||
* The new-line style. If not specified, a default of `"\n"` is used.
|
||||
*/
|
||||
public executePending(maxDepth = 0): number {
|
||||
if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range.");
|
||||
const pending: Timer[] = [];
|
||||
this.appendImmediates(pending);
|
||||
this.appendDueTimeouts(pending, this._time);
|
||||
this.appendDueIntervals(pending, this._time, /*expand*/ true);
|
||||
let count = this.execute(pending);
|
||||
for (let depth = 0; depth < maxDepth && this._immediates.size > 0; depth++) {
|
||||
pending.length = 0;
|
||||
this.appendImmediates(pending);
|
||||
count += this.execute(pending);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public setImmediate(callback: (...args: any[]) => void, ...args: any[]): any {
|
||||
const timer: Immediate = { kind: "immediate", callback, args };
|
||||
this._immediates.add(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
public clearImmedate(timerId: any): void {
|
||||
this._immediates.delete(timerId);
|
||||
}
|
||||
|
||||
public setTimeout(callback: (...args: any[]) => void, timeout: number, ...args: any[]): any {
|
||||
if (timeout < 0) timeout = 0;
|
||||
const due = this._time + timeout;
|
||||
const timer: Timeout = { kind: "timeout", callback, args, due };
|
||||
this._timeouts.add(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
public clearTimeout(timerId: any): void {
|
||||
this._timeouts.delete(timerId);
|
||||
}
|
||||
|
||||
public setInterval(callback: (...args: any[]) => void, interval: number, ...args: any[]): any {
|
||||
if (interval < 0) interval = 0;
|
||||
const due = this._time + interval;
|
||||
const timer: Interval = { kind: "interval", callback, args, due, interval };
|
||||
this._intervals.add(timer);
|
||||
return timer;
|
||||
}
|
||||
|
||||
public clearInterval(timerId: any): void {
|
||||
this._intervals.delete(timerId);
|
||||
}
|
||||
|
||||
private appendImmediates(pending: Timer[]) {
|
||||
this._immediates.forEach(timer => {
|
||||
pending.push(timer);
|
||||
});
|
||||
}
|
||||
|
||||
private appendDueTimeouts(timers: Timer[], dueTime: number) {
|
||||
this._timeouts.forEach(timer => {
|
||||
if (timer.due <= dueTime) {
|
||||
timers.push(timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private appendDueIntervals(timers: Timer[], dueTime: number, expand: boolean) {
|
||||
this._intervals.forEach(timer => {
|
||||
while (timer.due <= dueTime) {
|
||||
timers.push(timer);
|
||||
if (!expand) break;
|
||||
timer.due += timer.interval;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private execute(timers: Timer[]) {
|
||||
for (const timer of core.stableSort(timers, compareTimers)) {
|
||||
switch (timer.kind) {
|
||||
case "immediate": this._immediates.delete(timer); break;
|
||||
case "timeout": this._timeouts.delete(timer); break;
|
||||
}
|
||||
const { callback, args } = timer;
|
||||
callback(...args);
|
||||
}
|
||||
return timers.length;
|
||||
}
|
||||
}
|
||||
|
||||
function compareTimers(a: Immediate | Timeout, b: Immediate | Timeout) {
|
||||
return (a.kind === "immediate" ? -1 : a.due) - (b.kind === "immediate" ? -1 : b.due);
|
||||
newLine?: "\r\n" | "\n";
|
||||
/**
|
||||
* Indicates whether to include _safeList.json_.
|
||||
*/
|
||||
safeList?: boolean;
|
||||
/**
|
||||
* Indicates whether to include a bare _lib.d.ts_.
|
||||
*/
|
||||
lib?: boolean;
|
||||
}
|
||||
|
||||
export class MockServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost {
|
||||
public readonly exitMessage = "System Exit";
|
||||
public readonly timers = new Timers();
|
||||
public static readonly defaultExecutingFilePath = "/.ts/tsc.js";
|
||||
public static readonly defaultCurrentDirectory = "/";
|
||||
public static readonly safeListPath = "/safelist.json";
|
||||
public static readonly safeListContent =
|
||||
`{\n` +
|
||||
` "commander": "commander",\n` +
|
||||
` "express": "express",\n` +
|
||||
` "jquery": "jquery",\n` +
|
||||
` "lodash": "lodash",\n` +
|
||||
` "moment": "moment",\n` +
|
||||
` "chroma": "chroma-js"\n` +
|
||||
`}`;
|
||||
|
||||
public static readonly libPath = "/.ts/lib.d.ts";
|
||||
public static readonly libContent =
|
||||
`/// <reference no-default-lib="true"/>\n` +
|
||||
`interface Boolean {}\n` +
|
||||
`interface Function {}\n` +
|
||||
`interface IArguments {}\n` +
|
||||
`interface Number { toExponential: any; }\n` +
|
||||
`interface Object {}\n` +
|
||||
`interface RegExp {}\n` +
|
||||
`interface String { charAt: any; }\n` +
|
||||
`interface Array<T> {}`;
|
||||
|
||||
public readonly timers = new typemock.Timers();
|
||||
public readonly vfs: vfs.VirtualFileSystem;
|
||||
public exitCode: number;
|
||||
|
||||
private static readonly processExitSentinel = new Error("System exit");
|
||||
private readonly _output: string[] = [];
|
||||
private readonly _executingFilePath: string;
|
||||
private readonly _getCanonicalFileName: (file: string) => string;
|
||||
|
||||
constructor(vfs: vfs.VirtualFileSystem, executingFilePath = "/.ts/tsc.js", newLine = "\n") {
|
||||
this.vfs = vfs;
|
||||
this.useCaseSensitiveFileNames = vfs.useCaseSensitiveFileNames;
|
||||
constructor(options: MockServerHostOptions = {}) {
|
||||
const {
|
||||
vfs: _vfs = {},
|
||||
executingFilePath = MockServerHost.defaultExecutingFilePath,
|
||||
newLine = "\n",
|
||||
safeList = false,
|
||||
lib = false
|
||||
} = options;
|
||||
|
||||
const { currentDirectory = MockServerHost.defaultCurrentDirectory, useCaseSensitiveFileNames = false } = _vfs;
|
||||
|
||||
this.vfs = _vfs instanceof vfs.VirtualFileSystem ? _vfs :
|
||||
new vfs.VirtualFileSystem(currentDirectory, useCaseSensitiveFileNames);
|
||||
|
||||
this.useCaseSensitiveFileNames = this.vfs.useCaseSensitiveFileNames;
|
||||
this.newLine = newLine;
|
||||
this._executingFilePath = executingFilePath;
|
||||
this._getCanonicalFileName = ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames);
|
||||
|
||||
if (safeList) {
|
||||
this.vfs.addFile(MockServerHost.safeListPath, MockServerHost.safeListContent);
|
||||
}
|
||||
|
||||
if (lib) {
|
||||
this.vfs.addFile(MockServerHost.libPath, MockServerHost.libContent);
|
||||
}
|
||||
}
|
||||
|
||||
// #region DirectoryStructureHost members
|
||||
|
@ -218,8 +107,8 @@ namespace mocks {
|
|||
return this.vfs.readFile(path);
|
||||
}
|
||||
|
||||
public writeFile(path: string, data: string): void {
|
||||
this.vfs.writeFile(path, data);
|
||||
public writeFile(path: string, data: string, writeByteOrderMark?: boolean): void {
|
||||
this.vfs.writeFile(path, writeByteOrderMark ? core.addUTF8ByteOrderMark(data) : data);
|
||||
}
|
||||
|
||||
public fileExists(path: string) {
|
||||
|
@ -250,7 +139,7 @@ namespace mocks {
|
|||
|
||||
public exit(exitCode?: number) {
|
||||
this.exitCode = exitCode;
|
||||
throw new Error("System exit");
|
||||
throw MockServerHost.processExitSentinel;
|
||||
}
|
||||
// #endregion DirectoryStructureHost members
|
||||
|
||||
|
@ -336,5 +225,26 @@ namespace mocks {
|
|||
public clearOutput() {
|
||||
this._output.length = 0;
|
||||
}
|
||||
|
||||
public checkTimeoutQueueLength(expected: number) {
|
||||
const callbacksCount = this.timers.getPending({ kind: "timeout", ms: this.timers.remainingTime }).length;
|
||||
assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
|
||||
}
|
||||
|
||||
public checkTimeoutQueueLengthAndRun(count: number) {
|
||||
this.checkTimeoutQueueLength(count);
|
||||
this.runQueuedTimeoutCallbacks();
|
||||
}
|
||||
|
||||
public runQueuedTimeoutCallbacks() {
|
||||
try {
|
||||
this.timers.advanceToEnd();
|
||||
}
|
||||
catch (e) {
|
||||
if (e !== MockServerHost.processExitSentinel) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,6 +76,7 @@
|
|||
|
||||
"core.ts",
|
||||
"utils.ts",
|
||||
"typemock.ts",
|
||||
"events.ts",
|
||||
"documents.ts",
|
||||
"vpath.ts",
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/// <reference path="./core.ts" />
|
||||
/// <reference path="./utils.ts" />
|
||||
/// <reference path="./vfs.ts" />
|
||||
|
||||
// NOTE: The contents of this file are all exported from the namespace 'typemock'. This is to
|
||||
// support the eventual conversion of harness into a modular system.
|
||||
|
||||
// typemock library
|
||||
namespace typemock {
|
||||
type Imported<T extends Function> = T["prototype"];
|
||||
|
||||
function unwrap<T>(module: any, _: () => PromiseLike<T>): T { return module; }
|
||||
|
||||
const module = unwrap(require("../../scripts/typemock"), () => import("../../scripts/typemock"));
|
||||
|
||||
export const Arg = module.Arg;
|
||||
|
||||
export interface Arg extends Imported<typeof Arg> {
|
||||
}
|
||||
|
||||
export interface Returns<U> {
|
||||
returns: U;
|
||||
}
|
||||
|
||||
export interface Throws {
|
||||
throws: any;
|
||||
}
|
||||
|
||||
export const Mock = module.Mock;
|
||||
|
||||
export interface Mock<T> extends Imported<typeof Mock> {
|
||||
readonly value: T;
|
||||
setup<U = any>(callback: (value: T) => U, result?: Returns<U> | Throws): Mock<T>;
|
||||
setup(setups: Partial<T>): Mock<T>;
|
||||
verify(callback: (value: T) => any, times: Times): Mock<T>;
|
||||
}
|
||||
|
||||
export type Callable = ((...args: any[]) => any);
|
||||
|
||||
export type Constructable = (new (...args: any[]) => any);
|
||||
|
||||
export const Spy = module.Spy;
|
||||
|
||||
export interface Spy<T extends Callable | Constructable = Callable & Constructable> extends Imported<typeof Spy> {
|
||||
readonly value: T;
|
||||
verify(callback: (value: T) => any, times: Times): this;
|
||||
}
|
||||
|
||||
export const Times = module.Times;
|
||||
|
||||
export interface Times extends Imported<typeof Times> {
|
||||
}
|
||||
|
||||
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 declare type Timer = Immediate | Timeout | Interval | AnimationFrame;
|
||||
|
||||
export const Timers = module.Timers;
|
||||
|
||||
export interface Timers extends Imported<typeof Timers> {
|
||||
}
|
||||
|
||||
export const Stub = module.Stub;
|
||||
|
||||
export interface Stub<T, K extends keyof T> extends Imported<typeof Stub> {
|
||||
readonly target: T;
|
||||
readonly key: K;
|
||||
stubValue: T[K];
|
||||
readonly originalValue: T[K];
|
||||
readonly currentValue: T[K];
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,6 +1,7 @@
|
|||
/// <reference path="../harness.ts" />
|
||||
/// <reference path="./tsserverProjectSystem.ts" />
|
||||
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
|
||||
/// <reference path="../mocks.ts" />
|
||||
|
||||
namespace ts.projectSystem {
|
||||
describe("Project errors", () => {
|
||||
|
@ -30,177 +31,127 @@ namespace ts.projectSystem {
|
|||
}
|
||||
|
||||
it("external project - diagnostics for missing files", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: ""
|
||||
};
|
||||
const file2 = {
|
||||
path: "/a/b/applib.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([file1, libFile]);
|
||||
const host = new mocks.MockServerHost({ safeList: true, lib: true });
|
||||
host.vfs.addFile("/a/b/app.ts", ``);
|
||||
|
||||
const projectFileName = "/a/b/test.csproj";
|
||||
|
||||
const session = createSession(host);
|
||||
const projectService = session.getProjectService();
|
||||
const projectFileName = "/a/b/test.csproj";
|
||||
const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = {
|
||||
type: "request",
|
||||
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
|
||||
seq: 2,
|
||||
arguments: { projectFileName }
|
||||
};
|
||||
projectService.openExternalProject({
|
||||
projectFileName,
|
||||
options: {},
|
||||
rootFiles: toExternalFiles(["/a/b/app.ts", "/a/b/applib.ts"])
|
||||
});
|
||||
|
||||
{
|
||||
projectService.openExternalProject({
|
||||
projectFileName,
|
||||
options: {},
|
||||
rootFiles: toExternalFiles([file1.path, file2.path])
|
||||
});
|
||||
// only file1 exists - expect error
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
const diags1 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName }, /*seq*/ 2);
|
||||
checkDiagnosticsWithLinePos(diags1, ["File '/a/b/applib.ts' not found."]);
|
||||
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
|
||||
// only file1 exists - expect error
|
||||
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
|
||||
}
|
||||
host.reloadFS([file2, libFile]);
|
||||
{
|
||||
// only file2 exists - expect error
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
|
||||
checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]);
|
||||
}
|
||||
host.vfs.removeFile("/a/b/app.ts");
|
||||
host.vfs.addFile("/a/b/applib.ts", ``);
|
||||
|
||||
host.reloadFS([file1, file2, libFile]);
|
||||
{
|
||||
// both files exist - expect no errors
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
|
||||
checkDiagnosticsWithLinePos(diags, []);
|
||||
}
|
||||
// only file2 exists - expect error
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
const diags2 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName }, /*seq*/ 2);
|
||||
checkDiagnosticsWithLinePos(diags2, ["File '/a/b/app.ts' not found."]);
|
||||
|
||||
host.vfs.addFile("/a/b/app.ts", ``);
|
||||
|
||||
// both files exist - expect no errors
|
||||
checkNumberOfProjects(projectService, { externalProjects: 1 });
|
||||
const diags3 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName }, /*seq*/ 2);
|
||||
checkDiagnosticsWithLinePos(diags3, []);
|
||||
});
|
||||
|
||||
it("configured projects - diagnostics for missing files", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: ""
|
||||
};
|
||||
const file2 = {
|
||||
path: "/a/b/applib.ts",
|
||||
content: ""
|
||||
};
|
||||
const config = {
|
||||
path: "/a/b/tsconfig.json",
|
||||
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
|
||||
};
|
||||
const host = createServerHost([file1, config, libFile]);
|
||||
const host = new mocks.MockServerHost({ safeList: true, lib: true });
|
||||
host.vfs.addFile("/a/b/app.ts", ``);
|
||||
host.vfs.addFile("/a/b/tsconfig.json", `{ "files": ["app.ts", "applib.ts"] }`);
|
||||
|
||||
const session = createSession(host);
|
||||
const projectService = session.getProjectService();
|
||||
openFilesForSession([file1], session);
|
||||
|
||||
openFilesForSession(["/a/b/app.ts"], session);
|
||||
|
||||
checkNumberOfProjects(projectService, { configuredProjects: 1 });
|
||||
const project = configuredProjectAt(projectService, 0);
|
||||
const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = {
|
||||
type: "request",
|
||||
command: server.CommandNames.CompilerOptionsDiagnosticsFull,
|
||||
seq: 2,
|
||||
arguments: { projectFileName: project.getProjectName() }
|
||||
};
|
||||
let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
|
||||
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
|
||||
const diags1 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName: project.getProjectName() }, /*seq*/ 2);
|
||||
checkDiagnosticsWithLinePos(diags1, ["File '/a/b/applib.ts' not found."]);
|
||||
|
||||
host.reloadFS([file1, file2, config, libFile]);
|
||||
host.vfs.addFile("/a/b/applib.ts", ``);
|
||||
|
||||
checkNumberOfProjects(projectService, { configuredProjects: 1 });
|
||||
diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
|
||||
checkDiagnosticsWithLinePos(diags, []);
|
||||
const diags2 = sendCompilerOptionsDiagnosticsRequest(session, { projectFileName: project.getProjectName() }, /*seq*/ 2);
|
||||
checkDiagnosticsWithLinePos(diags2, []);
|
||||
});
|
||||
|
||||
it("configured projects - diagnostics for corrupted config 1", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: ""
|
||||
};
|
||||
const file2 = {
|
||||
path: "/a/b/lib.ts",
|
||||
content: ""
|
||||
};
|
||||
const correctConfig = {
|
||||
path: "/a/b/tsconfig.json",
|
||||
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
|
||||
};
|
||||
const corruptedConfig = {
|
||||
path: correctConfig.path,
|
||||
content: correctConfig.content.substr(1)
|
||||
};
|
||||
const host = createServerHost([file1, file2, corruptedConfig]);
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
host.vfs.addFile("/a/b/app.ts", ``);
|
||||
host.vfs.addFile("/a/b/lib.ts", ``);
|
||||
host.vfs.addFile("/a/b/tsconfig.json", ` "files": ["app.ts", "lib.ts"] }`);
|
||||
|
||||
const projectService = createProjectService(host);
|
||||
|
||||
projectService.openClientFile(file1.path);
|
||||
{
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
|
||||
assert.isTrue(configuredProject !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject, []);
|
||||
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors, [
|
||||
"'{' expected."
|
||||
]);
|
||||
assert.isNotNull(projectErrors[0].file);
|
||||
assert.equal(projectErrors[0].file.fileName, corruptedConfig.path);
|
||||
}
|
||||
projectService.openClientFile("/a/b/app.ts");
|
||||
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
|
||||
const configuredProject1 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json");
|
||||
assert.isTrue(configuredProject1 !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject1, []);
|
||||
|
||||
const projectErrors1 = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors1, ["'{' expected."]);
|
||||
assert.isNotNull(projectErrors1[0].file);
|
||||
assert.equal(projectErrors1[0].file.fileName, "/a/b/tsconfig.json");
|
||||
|
||||
// fix config and trigger watcher
|
||||
host.reloadFS([file1, file2, correctConfig]);
|
||||
{
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
|
||||
assert.isTrue(configuredProject !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject, []);
|
||||
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors, []);
|
||||
}
|
||||
host.vfs.writeFile("/a/b/tsconfig.json", `{ "files": ["app.ts", "lib.ts"] }`);
|
||||
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
|
||||
const configuredProject2 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json");
|
||||
assert.isTrue(configuredProject2 !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject2, []);
|
||||
|
||||
const projectErrors2 = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors2, []);
|
||||
});
|
||||
|
||||
it("configured projects - diagnostics for corrupted config 2", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: ""
|
||||
};
|
||||
const file2 = {
|
||||
path: "/a/b/lib.ts",
|
||||
content: ""
|
||||
};
|
||||
const correctConfig = {
|
||||
path: "/a/b/tsconfig.json",
|
||||
content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) })
|
||||
};
|
||||
const corruptedConfig = {
|
||||
path: correctConfig.path,
|
||||
content: correctConfig.content.substr(1)
|
||||
};
|
||||
const host = createServerHost([file1, file2, correctConfig]);
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
host.vfs.addFile("/a/b/app.ts", ``);
|
||||
host.vfs.addFile("/a/b/lib.ts", ``);
|
||||
host.vfs.addFile("/a/b/tsconfig.json", `{ "files": ["app.ts", "lib.ts"] }`);
|
||||
|
||||
const projectService = createProjectService(host);
|
||||
|
||||
projectService.openClientFile(file1.path);
|
||||
{
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
|
||||
assert.isTrue(configuredProject !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject, []);
|
||||
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors, []);
|
||||
}
|
||||
projectService.openClientFile("/a/b/app.ts");
|
||||
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
const configuredProject1 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json");
|
||||
assert.isTrue(configuredProject1 !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject1, []);
|
||||
|
||||
const projectErrors1 = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors1, []);
|
||||
|
||||
// break config and trigger watcher
|
||||
host.reloadFS([file1, file2, corruptedConfig]);
|
||||
{
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f);
|
||||
assert.isTrue(configuredProject !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject, []);
|
||||
const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors, [
|
||||
"'{' expected."
|
||||
]);
|
||||
assert.isNotNull(projectErrors[0].file);
|
||||
assert.equal(projectErrors[0].file.fileName, corruptedConfig.path);
|
||||
}
|
||||
host.vfs.writeFile("/a/b/tsconfig.json", ` "files": ["app.ts", "lib.ts"] }`);
|
||||
|
||||
projectService.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
|
||||
const configuredProject2 = find(projectService.synchronizeProjectList([]), f => f.info.projectName === "/a/b/tsconfig.json");
|
||||
assert.isTrue(configuredProject2 !== undefined, "should find configured project");
|
||||
checkProjectErrors(configuredProject2, []);
|
||||
|
||||
const projectErrors2 = configuredProjectAt(projectService, 0).getAllProjectErrors();
|
||||
checkProjectErrorsWorker(projectErrors2, ["'{' expected."]);
|
||||
assert.isNotNull(projectErrors2[0].file);
|
||||
assert.equal(projectErrors2[0].file.fileName, "/a/b/tsconfig.json");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -915,7 +915,7 @@ namespace ts {
|
|||
}
|
||||
|
||||
function verifyProgram(vfs: vfs.VirtualFileSystem, rootFiles: string[], options: CompilerOptions, configFile: string) {
|
||||
const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost(vfs));
|
||||
const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost({ vfs }));
|
||||
verifyProgramWithoutConfigFile(watchingSystemHost, rootFiles, options);
|
||||
verifyProgramWithConfigFile(watchingSystemHost, configFile);
|
||||
}
|
||||
|
@ -997,7 +997,7 @@ namespace ts {
|
|||
`export default classD;`);
|
||||
const configFile = fs.addFile("/src/tsconfig.json",
|
||||
JSON.stringify({ compilerOptions, include: ["packages/**/ *.ts"] }));
|
||||
const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost(fs));
|
||||
const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost({ vfs: fs }));
|
||||
verifyProgramWithConfigFile(watchingSystemHost, configFile.path);
|
||||
});
|
||||
});
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -401,9 +401,9 @@ namespace ts.projectSystem {
|
|||
}
|
||||
}
|
||||
|
||||
export function makeSessionRequest<T>(command: string, args: T) {
|
||||
export function makeSessionRequest<T>(command: string, args: T, seq = 0) {
|
||||
const newRequest: protocol.Request = {
|
||||
seq: 0,
|
||||
seq,
|
||||
type: "request",
|
||||
command,
|
||||
arguments: args
|
||||
|
@ -411,9 +411,29 @@ namespace ts.projectSystem {
|
|||
return newRequest;
|
||||
}
|
||||
|
||||
export function openFilesForSession(files: FileOrFolder[], session: server.Session) {
|
||||
export function sendOpenRequest(session: server.Session, args: server.protocol.OpenRequestArgs, seq?: number) {
|
||||
session.executeCommand(makeSessionRequest(CommandNames.Open, args, seq));
|
||||
}
|
||||
|
||||
export function sendChangeRequest(session: server.Session, args: server.protocol.ChangeRequestArgs, seq?: number) {
|
||||
session.executeCommand(makeSessionRequest(CommandNames.Change, args, seq));
|
||||
}
|
||||
|
||||
export function sendCompileOnSaveAffectedFileListRequest(session: server.Session, args: server.protocol.FileRequestArgs, seq?: number) {
|
||||
return session.executeCommand(makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, args, seq)).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[];
|
||||
}
|
||||
|
||||
export function sendCompileOnSaveEmitFileRequest(session: server.Session, args: server.protocol.CompileOnSaveEmitFileRequestArgs, seq?: number) {
|
||||
session.executeCommand(makeSessionRequest(CommandNames.CompileOnSaveEmitFile, args, seq));
|
||||
}
|
||||
|
||||
export function sendCompilerOptionsDiagnosticsRequest(session: server.Session, args: server.protocol.CompilerOptionsDiagnosticsRequestArgs, seq?: number) {
|
||||
return session.executeCommand(makeSessionRequest(CommandNames.CompilerOptionsDiagnosticsFull, args, seq)).response as server.protocol.DiagnosticWithLinePosition[];
|
||||
}
|
||||
|
||||
export function openFilesForSession(files: (string | FileOrFolder | vfs.VirtualFile)[], session: server.Session) {
|
||||
for (const file of files) {
|
||||
const request = makeSessionRequest<protocol.OpenRequestArgs>(CommandNames.Open, { file: file.path });
|
||||
const request = makeSessionRequest<protocol.OpenRequestArgs>(CommandNames.Open, { file: typeof file === "string" ? file : file.path });
|
||||
session.executeCommand(request);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
/// <reference path="../harness.ts" />
|
||||
/// <reference path="./tsserverProjectSystem.ts" />
|
||||
/// <reference path="../../server/typingsInstaller/typingsInstaller.ts" />
|
||||
/// <reference path="../vfs.ts" />
|
||||
/// <reference path="../typemock.ts" />
|
||||
/// <reference path="../mocks.ts" />
|
||||
|
||||
namespace ts.projectSystem {
|
||||
import TI = server.typingsInstaller;
|
||||
|
@ -37,10 +40,20 @@ namespace ts.projectSystem {
|
|||
}
|
||||
}
|
||||
|
||||
function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[] | string, typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void {
|
||||
function executeCommand(self: Installer, host: TestServerHost | mocks.MockServerHost, installedTypings: string[] | string, typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void {
|
||||
self.addPostExecAction(installedTypings, success => {
|
||||
for (const file of typingFiles) {
|
||||
host.ensureFileOrFolder(file);
|
||||
if (host instanceof mocks.MockServerHost) {
|
||||
if (typeof file.content === "string") {
|
||||
host.vfs.addFile(file.path, file.content, { overwrite: true });
|
||||
}
|
||||
else {
|
||||
host.vfs.addDirectory(file.path);
|
||||
}
|
||||
}
|
||||
else {
|
||||
host.ensureFileOrFolder(file);
|
||||
}
|
||||
}
|
||||
cb(success);
|
||||
});
|
||||
|
@ -62,35 +75,22 @@ namespace ts.projectSystem {
|
|||
|
||||
describe("local module", () => {
|
||||
it("should not be picked up", () => {
|
||||
const f1 = {
|
||||
path: "/a/app.js",
|
||||
content: "const c = require('./config');"
|
||||
};
|
||||
const f2 = {
|
||||
path: "/a/config.js",
|
||||
content: "export let x = 1"
|
||||
};
|
||||
const typesCache = "/cache";
|
||||
const typesConfig = {
|
||||
path: typesCache + "/node_modules/@types/config/index.d.ts",
|
||||
content: "export let y: number;"
|
||||
};
|
||||
const config = {
|
||||
path: "/a/jsconfig.json",
|
||||
content: JSON.stringify({
|
||||
compilerOptions: { moduleResolution: "commonjs" },
|
||||
typeAcquisition: { enable: true }
|
||||
})
|
||||
};
|
||||
const host = createServerHost([f1, f2, config, typesConfig]);
|
||||
const installer = new (class extends Installer {
|
||||
const host = new mocks.MockServerHost();
|
||||
const f1 = host.vfs.addFile("/a/app.js", `const c = require('./config');`);
|
||||
const f2 = host.vfs.addFile("/a/config.js", `export let x = 1`);
|
||||
const config = host.vfs.addFile("/a/jsconfig.json", `{ "typeAcquisition": { "enable": true }, "compilerOptions": { "moduleResolution": "commonjs } }`);
|
||||
host.vfs.addFile("/cache/node_modules/@types/config/index.d.ts", `export let y: number;`);
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("config"), globalTypingsCacheLocation: typesCache });
|
||||
super(host, { typesRegistry: createTypesRegistry("config"), globalTypingsCacheLocation: "/cache" });
|
||||
}
|
||||
|
||||
installWorker(_requestId: number, _args: string[], _cwd: string, _cb: TI.RequestCompletedAction) {
|
||||
assert(false, "should not be called");
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const service = createProjectService(host, { typingsInstaller: installer });
|
||||
service.openClientFile(f1.path);
|
||||
service.checkNumberOfProjects({ configuredProjects: 1 });
|
||||
|
@ -101,46 +101,27 @@ namespace ts.projectSystem {
|
|||
|
||||
describe("typingsInstaller", () => {
|
||||
it("configured projects (typings installed) 1", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.js",
|
||||
content: ""
|
||||
};
|
||||
const tsconfig = {
|
||||
path: "/a/b/tsconfig.json",
|
||||
content: JSON.stringify({
|
||||
compilerOptions: {
|
||||
allowJs: true
|
||||
},
|
||||
typeAcquisition: {
|
||||
enable: true
|
||||
}
|
||||
})
|
||||
};
|
||||
const packageJson = {
|
||||
path: "/a/b/package.json",
|
||||
content: JSON.stringify({
|
||||
name: "test",
|
||||
dependencies: {
|
||||
jquery: "^3.1.0"
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const jquery = {
|
||||
path: "/a/data/node_modules/@types/jquery/index.d.ts",
|
||||
content: "declare const $: { x: number }"
|
||||
};
|
||||
const host = createServerHost([file1, tsconfig, packageJson]);
|
||||
const installer = new (class extends Installer {
|
||||
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/app.js", ``);
|
||||
const tsconfig = host.vfs.addFile("/a/b/tsconfig.json", `{ "compilerOptions": { "allowJs": true }, "typeAcquisition": { "enable": true } }`);
|
||||
host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "jquery": "^3.1.0" } }`);
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("jquery") });
|
||||
}
|
||||
|
||||
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
|
||||
const installedTypings = ["@types/jquery"];
|
||||
const typingFiles = [jquery];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(file1.path);
|
||||
|
@ -157,35 +138,26 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("inferred project (typings installed)", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.js",
|
||||
content: ""
|
||||
};
|
||||
const packageJson = {
|
||||
path: "/a/b/package.json",
|
||||
content: JSON.stringify({
|
||||
name: "test",
|
||||
dependencies: {
|
||||
jquery: "^3.1.0"
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
const jquery = {
|
||||
path: "/a/data/node_modules/@types/jquery/index.d.ts",
|
||||
content: "declare const $: { x: number }"
|
||||
};
|
||||
const host = createServerHost([file1, packageJson]);
|
||||
const installer = new (class extends Installer {
|
||||
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/app.js", ``);
|
||||
host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "jquery": "^3.1.0" } }`);
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("jquery") });
|
||||
}
|
||||
|
||||
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
|
||||
const installedTypings = ["@types/jquery"];
|
||||
const typingFiles = [jquery];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(file1.path);
|
||||
|
@ -201,19 +173,18 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("external project - no type acquisition, no .d.ts/js files", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([file1]);
|
||||
const installer = new (class extends Installer {
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/app.ts", ``);
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host);
|
||||
}
|
||||
|
||||
enqueueInstallTypingsRequest() {
|
||||
assert(false, "auto discovery should not be enabled");
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -222,6 +193,7 @@ namespace ts.projectSystem {
|
|||
options: {},
|
||||
rootFiles: [toExternalFile(file1.path)]
|
||||
});
|
||||
|
||||
installer.checkPendingCommands(/*expectedCount*/ 0);
|
||||
// by default auto discovery will kick in if project contain only .js/.d.ts files
|
||||
// in this case project contain only ts files - no auto discovery
|
||||
|
@ -229,19 +201,16 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("external project - no auto in typing acquisition, no .d.ts/js files", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([file1]);
|
||||
const installer = new (class extends Installer {
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/app.ts", ``);
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("jquery") });
|
||||
}
|
||||
enqueueInstallTypingsRequest() {
|
||||
assert(false, "auto discovery should not be enabled");
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -258,17 +227,16 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("external project - autoDiscovery = true, no .d.ts/js files", () => {
|
||||
const file1 = {
|
||||
path: "/a/b/app.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/app.ts", ``);
|
||||
|
||||
const jquery = {
|
||||
path: "/a/data/node_modules/@types/jquery/index.d.ts",
|
||||
content: "declare const $: { x: number }"
|
||||
};
|
||||
const host = createServerHost([file1]);
|
||||
|
||||
let enqueueIsCalled = false;
|
||||
const installer: Installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("jquery") });
|
||||
}
|
||||
|
@ -281,7 +249,7 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [jquery];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -304,18 +272,12 @@ namespace ts.projectSystem {
|
|||
// 1. react typings are installed for .jsx
|
||||
// 2. loose files names are matched against safe list for typings if
|
||||
// this is a JS project (only js, jsx, d.ts files are present)
|
||||
const file1 = {
|
||||
path: "/a/b/lodash.js",
|
||||
content: ""
|
||||
};
|
||||
const file2 = {
|
||||
path: "/a/b/file2.jsx",
|
||||
content: ""
|
||||
};
|
||||
const file3 = {
|
||||
path: "/a/b/file3.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/lodash.js", ``);
|
||||
const file2 = host.vfs.addFile("/a/b/file2.jsx", ``);
|
||||
const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``);
|
||||
host.vfs.addFile(customTypesMap.path, customTypesMap.content);
|
||||
|
||||
const react = {
|
||||
path: "/a/data/node_modules/@types/react/index.d.ts",
|
||||
content: "declare const react: { x: number }"
|
||||
|
@ -325,8 +287,7 @@ namespace ts.projectSystem {
|
|||
content: "declare const lodash: { x: number }"
|
||||
};
|
||||
|
||||
const host = createServerHost([file1, file2, file3, customTypesMap]);
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("lodash", "react") });
|
||||
}
|
||||
|
@ -335,7 +296,7 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [lodash, react];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -361,17 +322,11 @@ namespace ts.projectSystem {
|
|||
it("external project - no type acquisition, with js & ts files", () => {
|
||||
// Tests:
|
||||
// 1. No typings are included for JS projects when the project contains ts files
|
||||
const file1 = {
|
||||
path: "/a/b/jquery.js",
|
||||
content: ""
|
||||
};
|
||||
const file2 = {
|
||||
path: "/a/b/file2.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/jquery.js", ``);
|
||||
const file2 = host.vfs.addFile("/a/b/file2.ts", ``);
|
||||
|
||||
const host = createServerHost([file1, file2]);
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("jquery") });
|
||||
}
|
||||
|
@ -383,7 +338,7 @@ namespace ts.projectSystem {
|
|||
const typingFiles: FileOrFolder[] = [];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -409,27 +364,12 @@ namespace ts.projectSystem {
|
|||
// 1. Safelist matching, type acquisition includes/excludes and package.json typings are all acquired
|
||||
// 2. Types for safelist matches are not included when they also appear in the type acquisition exclude list
|
||||
// 3. Multiple includes and excludes are respected in type acquisition
|
||||
const file1 = {
|
||||
path: "/a/b/lodash.js",
|
||||
content: ""
|
||||
};
|
||||
const file2 = {
|
||||
path: "/a/b/commander.js",
|
||||
content: ""
|
||||
};
|
||||
const file3 = {
|
||||
path: "/a/b/file3.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const packageJson = {
|
||||
path: "/a/b/package.json",
|
||||
content: JSON.stringify({
|
||||
name: "test",
|
||||
dependencies: {
|
||||
express: "^3.1.0"
|
||||
}
|
||||
})
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file1 = host.vfs.addFile("/a/b/lodash.js", ``);
|
||||
const file2 = host.vfs.addFile("/a/b/commander.js", ``);
|
||||
const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``);
|
||||
host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "express": "^3.1.0" } }`);
|
||||
host.vfs.addFile(customTypesMap.path, customTypesMap.content);
|
||||
|
||||
const commander = {
|
||||
path: "/a/data/node_modules/@types/commander/index.d.ts",
|
||||
|
@ -448,8 +388,7 @@ namespace ts.projectSystem {
|
|||
content: "declare const moment: { x: number }"
|
||||
};
|
||||
|
||||
const host = createServerHost([file1, file2, file3, packageJson, customTypesMap]);
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { typesRegistry: createTypesRegistry("jquery", "commander", "moment", "express") });
|
||||
}
|
||||
|
@ -458,7 +397,7 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [commander, express, jquery, moment];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -482,27 +421,12 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("Throttle - delayed typings to install", () => {
|
||||
const lodashJs = {
|
||||
path: "/a/b/lodash.js",
|
||||
content: ""
|
||||
};
|
||||
const commanderJs = {
|
||||
path: "/a/b/commander.js",
|
||||
content: ""
|
||||
};
|
||||
const file3 = {
|
||||
path: "/a/b/file3.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const packageJson = {
|
||||
path: "/a/b/package.json",
|
||||
content: JSON.stringify({
|
||||
name: "test",
|
||||
dependencies: {
|
||||
express: "^3.1.0"
|
||||
}
|
||||
})
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const lodashJs = host.vfs.addFile("/a/b/lodash.js", ``);
|
||||
const commanderJs = host.vfs.addFile("/a/b/commander.js", ``);
|
||||
const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``);
|
||||
host.vfs.addFile("/a/b/package.json", `{ "name": "test", "dependencies": { "express": "^3.1.0" } }`);
|
||||
host.vfs.addFile(customTypesMap.path, customTypesMap.content);
|
||||
|
||||
const commander = {
|
||||
path: "/a/data/node_modules/@types/commander/index.d.ts",
|
||||
|
@ -526,8 +450,7 @@ namespace ts.projectSystem {
|
|||
};
|
||||
|
||||
const typingFiles = [commander, express, jquery, moment, lodash];
|
||||
const host = createServerHost([lodashJs, commanderJs, file3, packageJson, customTypesMap]);
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { throttleLimit: 3, typesRegistry: createTypesRegistry("commander", "express", "jquery", "moment", "lodash") });
|
||||
}
|
||||
|
@ -535,7 +458,7 @@ namespace ts.projectSystem {
|
|||
const installedTypings = ["@types/commander", "@types/express", "@types/jquery", "@types/moment", "@types/lodash"];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectFileName = "/a/app/test.csproj";
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -561,18 +484,11 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("Throttle - delayed run install requests", () => {
|
||||
const lodashJs = {
|
||||
path: "/a/b/lodash.js",
|
||||
content: ""
|
||||
};
|
||||
const commanderJs = {
|
||||
path: "/a/b/commander.js",
|
||||
content: ""
|
||||
};
|
||||
const file3 = {
|
||||
path: "/a/b/file3.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const lodashJs = host.vfs.addFile("/a/b/lodash.js", ``);
|
||||
const commanderJs = host.vfs.addFile("/a/b/commander.js", ``);
|
||||
const file3 = host.vfs.addFile("/a/b/file3.d.ts", ``);
|
||||
host.vfs.addFile(customTypesMap.path, customTypesMap.content);
|
||||
|
||||
const commander = {
|
||||
path: "/a/data/node_modules/@types/commander/index.d.ts",
|
||||
|
@ -605,8 +521,7 @@ namespace ts.projectSystem {
|
|||
typings: typingsName("gulp")
|
||||
};
|
||||
|
||||
const host = createServerHost([lodashJs, commanderJs, file3, customTypesMap]);
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { throttleLimit: 1, typesRegistry: createTypesRegistry("commander", "jquery", "lodash", "cordova", "gulp", "grunt") });
|
||||
}
|
||||
|
@ -620,7 +535,7 @@ namespace ts.projectSystem {
|
|||
}
|
||||
executeCommand(this, host, typingFiles.map(f => f.typings), typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
// Create project #1 with 4 typings
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
|
@ -658,39 +573,25 @@ namespace ts.projectSystem {
|
|||
assert.equal(installer.pendingRunRequests.length, 0, "expected no throttled requests");
|
||||
|
||||
installer.executePendingCommands();
|
||||
host.checkTimeoutQueueLengthAndRun(3); // for 2 projects and 1 refreshing inferred project
|
||||
host.checkTimeoutQueueLengthAndRun(3);
|
||||
checkProjectActualFiles(p1, [lodashJs.path, commanderJs.path, file3.path, commander.path, jquery.path, lodash.path, cordova.path]);
|
||||
checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path]);
|
||||
});
|
||||
|
||||
it("configured projects discover from node_modules", () => {
|
||||
const app = {
|
||||
path: "/app.js",
|
||||
content: ""
|
||||
};
|
||||
const jsconfig = {
|
||||
path: "/jsconfig.json",
|
||||
content: JSON.stringify({})
|
||||
};
|
||||
const jquery = {
|
||||
path: "/node_modules/jquery/index.js",
|
||||
content: ""
|
||||
};
|
||||
const jqueryPackage = {
|
||||
path: "/node_modules/jquery/package.json",
|
||||
content: JSON.stringify({ name: "jquery" })
|
||||
};
|
||||
// Should not search deeply in node_modules.
|
||||
const nestedPackage = {
|
||||
path: "/node_modules/jquery/nested/package.json",
|
||||
content: JSON.stringify({ name: "nested" }),
|
||||
};
|
||||
const jqueryDTS = {
|
||||
path: "/tmp/node_modules/@types/jquery/index.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([app, jsconfig, jquery, jqueryPackage, nestedPackage]);
|
||||
const installer = new (class extends Installer {
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const app = host.vfs.addFile("/app.js", ``);
|
||||
const jsconfig = host.vfs.addFile("/jsconfig.json", `{}`);
|
||||
host.vfs.addFile("/node_modules/jquery/index.js", ``);
|
||||
host.vfs.addFile("/node_modules/jquery/package.json", `{ "name": "jquery" }`);
|
||||
// Should not search deeply in node_modules.
|
||||
host.vfs.addFile("/node_modules/jquery/nested/package.json", `{ "name": "nested" }`);
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery", "nested") });
|
||||
}
|
||||
|
@ -700,7 +601,7 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [jqueryDTS];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(app.path);
|
||||
|
@ -717,28 +618,18 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("configured projects discover from bower_components", () => {
|
||||
const app = {
|
||||
path: "/app.js",
|
||||
content: ""
|
||||
};
|
||||
const jsconfig = {
|
||||
path: "/jsconfig.json",
|
||||
content: JSON.stringify({})
|
||||
};
|
||||
const jquery = {
|
||||
path: "/bower_components/jquery/index.js",
|
||||
content: ""
|
||||
};
|
||||
const jqueryPackage = {
|
||||
path: "/bower_components/jquery/package.json",
|
||||
content: JSON.stringify({ name: "jquery" })
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const app = host.vfs.addFile("/app.js", ``);
|
||||
const jsconfig = host.vfs.addFile("/jsconfig.json", `{}`);
|
||||
host.vfs.addFile("/bower_components/jquery/index.js", ``);
|
||||
host.vfs.addFile("/bower_components/jquery/package.json", `{ "name": "jquery" }`);
|
||||
|
||||
const jqueryDTS = {
|
||||
path: "/tmp/node_modules/@types/jquery/index.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([app, jsconfig, jquery, jqueryPackage]);
|
||||
const installer = new (class extends Installer {
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") });
|
||||
}
|
||||
|
@ -747,7 +638,7 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [jqueryDTS];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(app.path);
|
||||
|
@ -755,7 +646,7 @@ namespace ts.projectSystem {
|
|||
checkNumberOfProjects(projectService, { configuredProjects: 1 });
|
||||
const p = configuredProjectAt(projectService, 0);
|
||||
checkProjectActualFiles(p, [app.path, jsconfig.path]);
|
||||
checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", libFile.path]);
|
||||
checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", "/.ts/lib.d.ts"]);
|
||||
|
||||
installer.installAll(/*expectedCount*/ 1);
|
||||
|
||||
|
@ -765,28 +656,17 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("configured projects discover from bower.json", () => {
|
||||
const app = {
|
||||
path: "/app.js",
|
||||
content: ""
|
||||
};
|
||||
const jsconfig = {
|
||||
path: "/jsconfig.json",
|
||||
content: JSON.stringify({})
|
||||
};
|
||||
const bowerJson = {
|
||||
path: "/bower.json",
|
||||
content: JSON.stringify({
|
||||
dependencies: {
|
||||
jquery: "^3.1.0"
|
||||
}
|
||||
})
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const app = host.vfs.addFile("/app.js", ``);
|
||||
const jsconfig = host.vfs.addFile("/jsconfig.json", `{}`);
|
||||
host.vfs.addFile("/bower.json", `{ "dependencies": { "jquery": "^3.1.0" } }`);
|
||||
|
||||
const jqueryDTS = {
|
||||
path: "/tmp/node_modules/@types/jquery/index.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([app, jsconfig, bowerJson]);
|
||||
const installer = new (class extends Installer {
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("jquery") });
|
||||
}
|
||||
|
@ -795,7 +675,7 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [jqueryDTS];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { useSingleInferredProject: true, typingsInstaller: installer });
|
||||
projectService.openClientFile(app.path);
|
||||
|
@ -812,25 +692,17 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("Malformed package.json should be watched", () => {
|
||||
const f = {
|
||||
path: "/a/b/app.js",
|
||||
content: "var x = 1"
|
||||
};
|
||||
const brokenPackageJson = {
|
||||
path: "/a/b/package.json",
|
||||
content: `{ "dependencies": { "co } }`
|
||||
};
|
||||
const fixedPackageJson = {
|
||||
path: brokenPackageJson.path,
|
||||
content: `{ "dependencies": { "commander": "0.0.2" } }`
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f = host.vfs.addFile("/a/b/app.js", `var x = 1`);
|
||||
host.vfs.addFile("/a/b/package.json", `{ "dependencies": { "co } }`);
|
||||
|
||||
const cachePath = "/a/cache/";
|
||||
const commander = {
|
||||
path: cachePath + "node_modules/@types/commander/index.d.ts",
|
||||
content: "export let x: number"
|
||||
};
|
||||
const host = createServerHost([f, brokenPackageJson]);
|
||||
const installer = new (class extends Installer {
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
|
||||
}
|
||||
|
@ -839,28 +711,32 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [commander];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const service = createProjectService(host, { typingsInstaller: installer });
|
||||
service.openClientFile(f.path);
|
||||
|
||||
installer.checkPendingCommands(/*expectedCount*/ 0);
|
||||
|
||||
host.reloadFS([f, fixedPackageJson]);
|
||||
host.vfs.writeFile("/a/b/package.json", `{ "dependencies": { "commander": "0.0.2" } }`);
|
||||
|
||||
host.checkTimeoutQueueLengthAndRun(2); // To refresh the project and refresh inferred projects
|
||||
|
||||
// expected install request
|
||||
installer.installAll(/*expectedCount*/ 1);
|
||||
|
||||
host.checkTimeoutQueueLengthAndRun(2);
|
||||
|
||||
service.checkNumberOfProjects({ inferredProjects: 1 });
|
||||
checkProjectActualFiles(service.inferredProjects[0], [f.path, commander.path]);
|
||||
});
|
||||
|
||||
it("should install typings for unresolved imports", () => {
|
||||
const file = {
|
||||
path: "/a/b/app.js",
|
||||
content: `
|
||||
import * as fs from "fs";
|
||||
import * as commander from "commander";`
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const file = host.vfs.addFile("/a/b/app.js",
|
||||
`import * as fs from "fs";\n` +
|
||||
`import * as commander from "commander";`);
|
||||
|
||||
const cachePath = "/a/cache";
|
||||
const node = {
|
||||
path: cachePath + "/node_modules/@types/node/index.d.ts",
|
||||
|
@ -870,8 +746,8 @@ namespace ts.projectSystem {
|
|||
path: cachePath + "/node_modules/@types/commander/index.d.ts",
|
||||
content: "export let y: string"
|
||||
};
|
||||
const host = createServerHost([file]);
|
||||
const installer = new (class extends Installer {
|
||||
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("node", "commander") });
|
||||
}
|
||||
|
@ -880,7 +756,8 @@ namespace ts.projectSystem {
|
|||
const typingFiles = [node, commander];
|
||||
executeCommand(this, host, installedTypings, typingFiles, cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const service = createProjectService(host, { typingsInstaller: installer });
|
||||
service.openClientFile(file.path);
|
||||
|
||||
|
@ -896,28 +773,25 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("should pick typing names from non-relative unresolved imports", () => {
|
||||
const f1 = {
|
||||
path: "/a/b/app.js",
|
||||
content: `
|
||||
import * as a from "foo/a/a";
|
||||
import * as b from "foo/a/b";
|
||||
import * as c from "foo/a/c";
|
||||
import * as d from "@bar/router/";
|
||||
import * as e from "@bar/common/shared";
|
||||
import * as e from "@bar/common/apps";
|
||||
import * as f from "./lib"
|
||||
`
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f1 = host.vfs.addFile("/a/b/app.js",
|
||||
`import * as a from "foo/a/a";\n` +
|
||||
`import * as b from "foo/a/b";\n` +
|
||||
`import * as c from "foo/a/c";\n` +
|
||||
`import * as d from "@bar/router/";\n` +
|
||||
`import * as e from "@bar/common/shared";\n` +
|
||||
`import * as e from "@bar/common/apps";\n` +
|
||||
`import * as f from "./lib"`);
|
||||
|
||||
const host = createServerHost([f1]);
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: "/tmp", typesRegistry: createTypesRegistry("foo") });
|
||||
}
|
||||
installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction) {
|
||||
executeCommand(this, host, ["foo"], [], cb);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
projectService.openClientFile(f1.path);
|
||||
projectService.checkNumberOfProjects({ inferredProjects: 1 });
|
||||
|
@ -934,7 +808,7 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("cached unresolved typings are not recomputed if program structure did not change", () => {
|
||||
const host = createServerHost([]);
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const session = createSession(host);
|
||||
const f = {
|
||||
path: "/a/app.js",
|
||||
|
@ -1005,28 +879,20 @@ namespace ts.projectSystem {
|
|||
|
||||
describe("Invalid package names", () => {
|
||||
it("should not be installed", () => {
|
||||
const f1 = {
|
||||
path: "/a/b/app.js",
|
||||
content: "let x = 1"
|
||||
};
|
||||
const packageJson = {
|
||||
path: "/a/b/package.json",
|
||||
content: JSON.stringify({
|
||||
dependencies: {
|
||||
"; say ‘Hello from TypeScript!’ #": "0.0.x"
|
||||
}
|
||||
})
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f1 = host.vfs.addFile("/a/b/app.js", `let x = 1`);
|
||||
host.vfs.addFile("/a/b/package.json", `{ "dependencies": { "; say ‘Hello from TypeScript!’ #": "0.0.x" } }`);
|
||||
|
||||
const messages: string[] = [];
|
||||
const host = createServerHost([f1, packageJson]);
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: "/tmp" }, { isEnabled: () => true, writeLine: msg => messages.push(msg) });
|
||||
}
|
||||
installWorker(_requestId: number, _args: string[], _cwd: string, _cb: TI.RequestCompletedAction) {
|
||||
assert(false, "runCommand should not be invoked");
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
projectService.openClientFile(f1.path);
|
||||
|
||||
|
@ -1039,22 +905,14 @@ namespace ts.projectSystem {
|
|||
const emptySafeList = emptyMap;
|
||||
|
||||
it("should use mappings from safe list", () => {
|
||||
const app = {
|
||||
path: "/a/b/app.js",
|
||||
content: ""
|
||||
};
|
||||
const jquery = {
|
||||
path: "/a/b/jquery.js",
|
||||
content: ""
|
||||
};
|
||||
const chroma = {
|
||||
path: "/a/b/chroma.min.js",
|
||||
content: ""
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const app = host.vfs.addFile("/a/b/app.js", ``);
|
||||
const jquery = host.vfs.addFile("/a/b/jquery.js", ``);
|
||||
const chroma = host.vfs.addFile("/a/b/chroma.min.js", ``);
|
||||
|
||||
const safeList = createMapFromTemplate({ jquery: "jquery", chroma: "chroma-js" });
|
||||
|
||||
const host = createServerHost([app, jquery, chroma]);
|
||||
// const host = createServerHost([app, jquery, chroma]);
|
||||
const logger = trackingLogger();
|
||||
const result = JsTyping.discoverTypings(host, logger.log, [app.path, jquery.path, chroma.path], getDirectoryPath(<Path>app.path), safeList, emptyMap, { enable: true }, emptyArray);
|
||||
const finish = logger.finish();
|
||||
|
@ -1067,11 +925,9 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("should return node for core modules", () => {
|
||||
const f = {
|
||||
path: "/a/b/app.js",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([f]);
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f = host.vfs.addFile("/a/b/app.js", ``);
|
||||
|
||||
const cache = createMap<string>();
|
||||
|
||||
for (const name of JsTyping.nodeCoreModuleList) {
|
||||
|
@ -1086,15 +942,10 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("should use cached locations", () => {
|
||||
const f = {
|
||||
path: "/a/b/app.js",
|
||||
content: ""
|
||||
};
|
||||
const node = {
|
||||
path: "/a/b/node.d.ts",
|
||||
content: ""
|
||||
};
|
||||
const host = createServerHost([f, node]);
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f = host.vfs.addFile("/a/b/app.js", ``);
|
||||
const node = host.vfs.addFile("/a/b/node.d.ts", ``);
|
||||
|
||||
const cache = createMapFromTemplate<string>({ node: node.path });
|
||||
const logger = trackingLogger();
|
||||
const result = JsTyping.discoverTypings(host, logger.log, [f.path], getDirectoryPath(<Path>f.path), emptySafeList, cache, { enable: true }, ["fs", "bar"]);
|
||||
|
@ -1107,19 +958,11 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("should search only 2 levels deep", () => {
|
||||
const app = {
|
||||
path: "/app.js",
|
||||
content: "",
|
||||
};
|
||||
const a = {
|
||||
path: "/node_modules/a/package.json",
|
||||
content: JSON.stringify({ name: "a" }),
|
||||
};
|
||||
const b = {
|
||||
path: "/node_modules/a/b/package.json",
|
||||
content: JSON.stringify({ name: "b" }),
|
||||
};
|
||||
const host = createServerHost([app, a, b]);
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const app = host.vfs.addFile("/app.js");
|
||||
host.vfs.addFile("/node_modules/a/package.json", `{ "name": "a" }`);
|
||||
host.vfs.addFile("/node_modules/a/b/package.json", `{ "name": "b" }`);
|
||||
|
||||
const cache = createMap<string>();
|
||||
const logger = trackingLogger();
|
||||
const result = JsTyping.discoverTypings(host, logger.log, [app.path], getDirectoryPath(<Path>app.path), emptySafeList, cache, { enable: true }, /*unresolvedImports*/ []);
|
||||
|
@ -1139,22 +982,18 @@ namespace ts.projectSystem {
|
|||
|
||||
describe("telemetry events", () => {
|
||||
it("should be received", () => {
|
||||
const f1 = {
|
||||
path: "/a/app.js",
|
||||
content: ""
|
||||
};
|
||||
const packageFile = {
|
||||
path: "/a/package.json",
|
||||
content: JSON.stringify({ dependencies: { commander: "1.0.0" } })
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f1 = host.vfs.addFile("/a/app.js", ``);
|
||||
host.vfs.addFile("/a/package.json", `{ "dependencies": { "commander": "1.0.0" } }`);
|
||||
|
||||
const cachePath = "/a/cache/";
|
||||
const commander = {
|
||||
path: cachePath + "node_modules/@types/commander/index.d.ts",
|
||||
content: "export let x: number"
|
||||
};
|
||||
const host = createServerHost([f1, packageFile]);
|
||||
|
||||
let seenTelemetryEvent = false;
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
|
||||
}
|
||||
|
@ -1174,7 +1013,8 @@ namespace ts.projectSystem {
|
|||
}
|
||||
super.sendResponse(response);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
projectService.openClientFile(f1.path);
|
||||
|
||||
|
@ -1189,23 +1029,19 @@ namespace ts.projectSystem {
|
|||
|
||||
describe("progress notifications", () => {
|
||||
it("should be sent for success", () => {
|
||||
const f1 = {
|
||||
path: "/a/app.js",
|
||||
content: ""
|
||||
};
|
||||
const packageFile = {
|
||||
path: "/a/package.json",
|
||||
content: JSON.stringify({ dependencies: { commander: "1.0.0" } })
|
||||
};
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f1 = host.vfs.addFile("/a/app.js", ``);
|
||||
host.vfs.addFile("/a/package.json", `{ "dependencies": { "commander": "1.0.0" } }`);
|
||||
|
||||
const cachePath = "/a/cache/";
|
||||
const commander = {
|
||||
path: cachePath + "node_modules/@types/commander/index.d.ts",
|
||||
content: "export let x: number"
|
||||
};
|
||||
const host = createServerHost([f1, packageFile]);
|
||||
|
||||
let beginEvent: server.BeginInstallTypes;
|
||||
let endEvent: server.EndInstallTypes;
|
||||
const installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
|
||||
}
|
||||
|
@ -1225,7 +1061,8 @@ namespace ts.projectSystem {
|
|||
}
|
||||
super.sendResponse(response);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
projectService.openClientFile(f1.path);
|
||||
|
||||
|
@ -1241,19 +1078,15 @@ namespace ts.projectSystem {
|
|||
});
|
||||
|
||||
it("should be sent for error", () => {
|
||||
const f1 = {
|
||||
path: "/a/app.js",
|
||||
content: ""
|
||||
};
|
||||
const packageFile = {
|
||||
path: "/a/package.json",
|
||||
content: JSON.stringify({ dependencies: { commander: "1.0.0" } })
|
||||
};
|
||||
// const host = createServerHost([f1, packageFile]);
|
||||
const host = new mocks.MockServerHost({ safeList: true });
|
||||
const f1 = host.vfs.addFile("/a/app.js", ``);
|
||||
host.vfs.addFile("/a/package.json", `{ "dependencies": { "commander": "1.0.0" } }`);
|
||||
|
||||
const cachePath = "/a/cache/";
|
||||
const host = createServerHost([f1, packageFile]);
|
||||
let beginEvent: server.BeginInstallTypes;
|
||||
let endEvent: server.EndInstallTypes;
|
||||
const installer: Installer = new (class extends Installer {
|
||||
const installer = new class extends Installer {
|
||||
constructor() {
|
||||
super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") });
|
||||
}
|
||||
|
@ -1271,7 +1104,8 @@ namespace ts.projectSystem {
|
|||
}
|
||||
super.sendResponse(response);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
const projectService = createProjectService(host, { typingsInstaller: installer });
|
||||
projectService.openClientFile(f1.path);
|
||||
|
||||
|
|
|
@ -33,4 +33,26 @@ namespace utils {
|
|||
export function removeTestPathPrefixes(text: string) {
|
||||
return text !== undefined ? text.replace(testPathPrefixRegExp, "") : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* SameValueZero (from ECMAScript spec), which has stricter equality sematics than "==" or "===".
|
||||
*/
|
||||
export function is(x: any, y: any) {
|
||||
return (x === y) ? (x !== 0 || 1 / x === 1 / y) : (x !== x && y !== y);
|
||||
}
|
||||
|
||||
export interface Theory {
|
||||
title: string;
|
||||
args: any[];
|
||||
}
|
||||
|
||||
export function theory(name: string, data: (Theory | any[])[], callback: (...args: any[]) => any) {
|
||||
describe(name, () => {
|
||||
for (const theory of data) {
|
||||
const title = Array.isArray(theory) ? theory.toString() : theory.title;
|
||||
const args = Array.isArray(theory) ? theory : theory.args;
|
||||
it(title, () => callback(...args));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -88,6 +88,7 @@ namespace vfs {
|
|||
|
||||
interface DirectoryWatcherEntryArray extends Array<DirectoryWatcherEntry> {
|
||||
recursiveCount?: number;
|
||||
nonRecursiveCount?: number;
|
||||
}
|
||||
|
||||
interface DirectoryWatcherEntry {
|
||||
|
@ -118,6 +119,9 @@ namespace vfs {
|
|||
}
|
||||
|
||||
export class VirtualFileSystem extends VirtualFileSystemObject {
|
||||
public watchFiles = true;
|
||||
public watchDirectories = true;
|
||||
|
||||
private static _builtLocal: VirtualFileSystem | undefined;
|
||||
private static _builtLocalCI: VirtualFileSystem | undefined;
|
||||
private static _builtLocalCS: VirtualFileSystem | undefined;
|
||||
|
@ -128,7 +132,10 @@ namespace vfs {
|
|||
private _currentDirectoryStack: string[] | undefined;
|
||||
private _shadowRoot: VirtualFileSystem | undefined;
|
||||
private _watchedFiles: core.KeyedCollection<string, FileWatcherEntry[]> | undefined;
|
||||
private _watchedFilesSet: core.SortedSet<string> | undefined;
|
||||
private _watchedDirectories: core.KeyedCollection<string, DirectoryWatcherEntryArray> | undefined;
|
||||
private _watchedRecursiveDirectoriesSet: core.SortedSet<string> | undefined;
|
||||
private _watchedNonRecursiveDirectoriesSet: core.SortedSet<string> | undefined;
|
||||
private _stringComparer: ts.Comparer<string> | undefined;
|
||||
private _pathComparer: ts.Comparer<string> | undefined;
|
||||
private _metadata: core.Metadata;
|
||||
|
@ -153,6 +160,18 @@ namespace vfs {
|
|||
: vpath.compareCaseInsensitive);
|
||||
}
|
||||
|
||||
public get watchedFiles(): ReadonlySet<string> {
|
||||
return this.watchedFilesSetPrivate;
|
||||
}
|
||||
|
||||
public get watchedRecursiveDirectories(): ReadonlySet<string> {
|
||||
return this.watchedRecursiveDirectoriesSetPrivate;
|
||||
}
|
||||
|
||||
public get watchedNonRecursiveDirectories(): ReadonlySet<string> {
|
||||
return this.watchedNonRecursiveDirectoriesSetPrivate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file system shadowed by this instance.
|
||||
*/
|
||||
|
@ -181,6 +200,26 @@ namespace vfs {
|
|||
return this._currentDirectory;
|
||||
}
|
||||
|
||||
private get watchedFilesPrivate() {
|
||||
return this._watchedFiles || (this._watchedFiles = new core.KeyedCollection<string, FileWatcherEntry[]>(this.pathComparer));
|
||||
}
|
||||
|
||||
private get watchedFilesSetPrivate() {
|
||||
return this._watchedFilesSet || (this._watchedFilesSet = new core.SortedSet<string>(this.pathComparer));
|
||||
}
|
||||
|
||||
private get watchedDirectoriesPrivate() {
|
||||
return this._watchedDirectories || (this._watchedDirectories = new core.KeyedCollection<string, DirectoryWatcherEntryArray>(this.pathComparer))
|
||||
}
|
||||
|
||||
private get watchedRecursiveDirectoriesSetPrivate() {
|
||||
return this._watchedRecursiveDirectoriesSet || (this._watchedRecursiveDirectoriesSet = new core.SortedSet<string>(this.pathComparer));
|
||||
}
|
||||
|
||||
private get watchedNonRecursiveDirectoriesSetPrivate() {
|
||||
return this._watchedNonRecursiveDirectoriesSet || (this._watchedNonRecursiveDirectoriesSet = new core.SortedSet<string>(this.pathComparer));
|
||||
}
|
||||
|
||||
private get root(): VirtualRoot {
|
||||
if (this._root === undefined) {
|
||||
if (this._shadowRoot) {
|
||||
|
@ -361,8 +400,13 @@ namespace vfs {
|
|||
*/
|
||||
public writeFile(path: string, content: string): void {
|
||||
path = vpath.resolve(this.currentDirectory, path);
|
||||
const file = this.getFile(path) || this.addFile(path);
|
||||
if (file) file.writeContent(content);
|
||||
const file = this.getFile(path);
|
||||
if (file) {
|
||||
file.writeContent(content);
|
||||
}
|
||||
else {
|
||||
this.addFile(path, content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -508,25 +552,24 @@ namespace vfs {
|
|||
* Watch a path for changes to a file.
|
||||
*/
|
||||
public watchFile(path: string, watcher: (path: string, change: FileSystemChange) => void): ts.FileWatcher {
|
||||
if (!this._watchedFiles) {
|
||||
const pathComparer = this.useCaseSensitiveFileNames ? vpath.compareCaseSensitive : vpath.compareCaseInsensitive;
|
||||
this._watchedFiles = new core.KeyedCollection<string, FileWatcherEntry[]>(pathComparer);
|
||||
}
|
||||
|
||||
path = vpath.resolve(this.currentDirectory, path);
|
||||
let watchers = this._watchedFiles.get(path);
|
||||
if (!watchers) this._watchedFiles.set(path, watchers = []);
|
||||
let watchers = this.watchedFilesPrivate.get(path);
|
||||
if (!watchers) {
|
||||
this.watchedFilesPrivate.set(path, watchers = []);
|
||||
this.watchedFilesSetPrivate.add(path);
|
||||
}
|
||||
|
||||
const entry: FileWatcherEntry = { watcher };
|
||||
watchers.push(entry);
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
const watchers = this._watchedFiles.get(path);
|
||||
const watchers = this.watchedFilesPrivate.get(path);
|
||||
if (watchers) {
|
||||
ts.orderedRemoveItem(watchers, entry);
|
||||
if (watchers.length === 0) {
|
||||
this._watchedFiles.delete(path);
|
||||
this.watchedFilesPrivate.delete(path);
|
||||
this.watchedFilesSetPrivate.delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -543,27 +586,48 @@ namespace vfs {
|
|||
}
|
||||
|
||||
path = vpath.resolve(this.currentDirectory, path);
|
||||
let watchers = this._watchedDirectories.get(path);
|
||||
let watchers = this.watchedDirectoriesPrivate.get(path);
|
||||
if (!watchers) {
|
||||
watchers = [];
|
||||
watchers.recursiveCount = 0;
|
||||
this._watchedDirectories.set(path, watchers);
|
||||
watchers.nonRecursiveCount = 0;
|
||||
this.watchedDirectoriesPrivate.set(path, watchers);
|
||||
}
|
||||
|
||||
const entry: DirectoryWatcherEntry = { watcher, recursive };
|
||||
watchers.push(entry);
|
||||
if (recursive) watchers.recursiveCount++;
|
||||
if (recursive) {
|
||||
if (watchers.recursiveCount === 0) {
|
||||
this.watchedRecursiveDirectoriesSetPrivate.add(path);
|
||||
}
|
||||
watchers.recursiveCount++;
|
||||
}
|
||||
else {
|
||||
if (watchers.nonRecursiveCount === 0) {
|
||||
this.watchedNonRecursiveDirectoriesSetPrivate.add(path);
|
||||
}
|
||||
watchers.nonRecursiveCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
const watchers = this._watchedDirectories.get(path);
|
||||
const watchers = this.watchedDirectoriesPrivate.get(path);
|
||||
if (watchers) {
|
||||
ts.orderedRemoveItem(watchers, entry);
|
||||
if (watchers.length === 0) {
|
||||
this._watchedDirectories.delete(path);
|
||||
}
|
||||
else if (entry.recursive) {
|
||||
if (entry.recursive) {
|
||||
watchers.recursiveCount--;
|
||||
if (watchers.recursiveCount === 0) {
|
||||
this.watchedRecursiveDirectoriesSetPrivate.delete(path);
|
||||
}
|
||||
}
|
||||
else {
|
||||
watchers.nonRecursiveCount--;
|
||||
if (watchers.nonRecursiveCount === 0) {
|
||||
this.watchedNonRecursiveDirectoriesSetPrivate.delete(path);
|
||||
}
|
||||
}
|
||||
if (watchers.length === 0) {
|
||||
this.watchedDirectoriesPrivate.delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -603,26 +667,31 @@ namespace vfs {
|
|||
}
|
||||
|
||||
private onRootFileSystemChange(path: string, change: FileSystemChange) {
|
||||
const fileWatchers = this._watchedFiles && this._watchedFiles.get(path);
|
||||
if (fileWatchers) {
|
||||
for (const { watcher } of fileWatchers) {
|
||||
watcher(path, change);
|
||||
if (this.watchFiles) {
|
||||
const fileWatchers = this._watchedFiles && this._watchedFiles.get(path);
|
||||
if (fileWatchers) {
|
||||
for (const { watcher } of fileWatchers) {
|
||||
watcher(path, change);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._watchedDirectories && (change === "added" || change === "removed")) {
|
||||
const ignoreCase = !this.useCaseSensitiveFileNames;
|
||||
const dirname = vpath.dirname(path);
|
||||
this._watchedDirectories.forEach((watchers, path) => {
|
||||
const exactMatch = vpath.equals(dirname, path, ignoreCase);
|
||||
if (exactMatch || (watchers.recursiveCount > 0 && vpath.beneath(dirname, path, ignoreCase))) {
|
||||
for (const { recursive, watcher } of watchers) {
|
||||
if (exactMatch || !recursive) {
|
||||
watcher(path);
|
||||
if (this.watchDirectories) {
|
||||
if (this._watchedDirectories && (change === "added" || change === "removed")) {
|
||||
const ignoreCase = !this.useCaseSensitiveFileNames;
|
||||
const dirname = vpath.dirname(path);
|
||||
this._watchedDirectories.forEach((watchers, watchedPath) => {
|
||||
const nonRecursiveMatch = watchers.nonRecursiveCount > 0 && vpath.equals(watchedPath, dirname, ignoreCase);
|
||||
const recursiveMatch = watchers.recursiveCount > 0 && vpath.beneath(watchedPath, dirname, ignoreCase);
|
||||
if (nonRecursiveMatch || recursiveMatch) {
|
||||
for (const { recursive, watcher } of watchers) {
|
||||
if (recursive ? recursiveMatch : nonRecursiveMatch) {
|
||||
watcher(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -766,10 +835,16 @@ namespace vfs {
|
|||
|
||||
protected static _setNameUnsafe(entry: VirtualFileSystemEntry, name: string) {
|
||||
entry._name = name;
|
||||
entry.invalidate();
|
||||
}
|
||||
|
||||
protected static _setParentUnsafe(entry: VirtualFileSystemEntry, parent: VirtualDirectory) {
|
||||
entry._parent = parent;
|
||||
entry.invalidate();
|
||||
}
|
||||
|
||||
protected static _invalidate(entry: VirtualFileSystemEntry) {
|
||||
entry.invalidate();
|
||||
}
|
||||
|
||||
protected shadowPreamble(parent: VirtualDirectory): void {
|
||||
|
@ -812,6 +887,10 @@ namespace vfs {
|
|||
this._mtimeMS = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
protected invalidate() {
|
||||
this._path = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface VirtualDirectory {
|
||||
|
@ -879,6 +958,10 @@ namespace vfs {
|
|||
return this._shadowRoot;
|
||||
}
|
||||
|
||||
protected get hasOwnEntries() {
|
||||
return this._entries !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the child entries in this directory for the provided options.
|
||||
*/
|
||||
|
@ -1253,6 +1336,19 @@ namespace vfs {
|
|||
return true;
|
||||
}
|
||||
|
||||
protected invalidate() {
|
||||
super.invalidate();
|
||||
this.invalidateEntries();
|
||||
}
|
||||
|
||||
protected invalidateEntries() {
|
||||
if (this.hasOwnEntries) {
|
||||
for (const entry of Array.from(this.getOwnEntries().values())) {
|
||||
VirtualFileSystemEntry._invalidate(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parsePath(path: string) {
|
||||
return vpath.parse(vpath.normalize(path));
|
||||
}
|
||||
|
@ -1434,6 +1530,10 @@ namespace vfs {
|
|||
return this._allViews;
|
||||
}
|
||||
|
||||
protected invalidateEntries() {
|
||||
this.invalidateTarget();
|
||||
}
|
||||
|
||||
private getView(entry: VirtualFile): VirtualFileView;
|
||||
private getView(entry: VirtualDirectory): VirtualDirectoryView;
|
||||
private getView(entry: VirtualEntry): VirtualEntryView;
|
||||
|
@ -1788,6 +1888,11 @@ namespace vfs {
|
|||
return super.getStatsCore(S_IFLNK, this.targetPath.length);
|
||||
}
|
||||
|
||||
protected invalidate() {
|
||||
super.invalidate();
|
||||
this.invalidateTarget();
|
||||
}
|
||||
|
||||
private resolveTarget() {
|
||||
if (!this._target) {
|
||||
const entry = findTarget(this.fileSystem, this.targetPath);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
namespace ts.TestFSWithWatch {
|
||||
export const libFile: FileOrFolder = {
|
||||
path: "/a/lib/lib.d.ts",
|
||||
path: "/.ts/lib.d.ts",
|
||||
content: `/// <reference no-default-lib="true"/>
|
||||
interface Boolean {}
|
||||
interface Function {}
|
||||
|
@ -41,10 +41,24 @@ interface Array<T> {}`
|
|||
useWindowsStylePaths?: boolean;
|
||||
}
|
||||
|
||||
export function createWatchedSystem(fileOrFolderList: ReadonlyArray<FileOrFolder>, params?: TestServerHostCreationParameters): TestServerHost {
|
||||
if (!params) {
|
||||
params = {};
|
||||
}
|
||||
export function createWatchedSystem(fileOrFolderList: ReadonlyArray<FileOrFolder>, params: TestServerHostCreationParameters = {}) {
|
||||
// const host = new mocks.MockServerHost({
|
||||
// vfs: {
|
||||
// currentDirectory: params.currentDirectory,
|
||||
// useCaseSensitiveFileNames: params.useCaseSensitiveFileNames
|
||||
// },
|
||||
// executingFilePath: params.executingFilePath,
|
||||
// newLine: params.newLine as "\r\n" | "\n"
|
||||
// });
|
||||
// for (const entry of fileOrFolderList) {
|
||||
// if (typeof entry.content === "string") {
|
||||
// host.vfs.addFile(entry.path, entry.content);
|
||||
// }
|
||||
// else {
|
||||
// host.vfs.addDirectory(entry.path);
|
||||
// }
|
||||
// }
|
||||
// if (params.useWindowsStylePaths) throw new Error("Not supported");
|
||||
const host = new TestServerHost(/*withSafelist*/ false,
|
||||
params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false,
|
||||
params.executingFilePath || getExecutingFilePathFromLibFile(),
|
||||
|
@ -153,11 +167,25 @@ interface Array<T> {}`
|
|||
}
|
||||
}
|
||||
|
||||
export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) {
|
||||
function checkSortedSet<T>(set: ReadonlySet<T>, values: ReadonlyArray<T>) {
|
||||
assert.strictEqual(set.size, values.length, `Actual: ${Array.from(set)}, expected: ${values}.`);
|
||||
for (const value of values) {
|
||||
assert.isTrue(set.has(value));
|
||||
}
|
||||
}
|
||||
|
||||
export function checkWatchedFiles(host: TestServerHost | mocks.MockServerHost, expectedFiles: string[]) {
|
||||
if (host instanceof mocks.MockServerHost) {
|
||||
return checkSortedSet(host.vfs.watchedFiles, expectedFiles);
|
||||
}
|
||||
|
||||
checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles);
|
||||
}
|
||||
|
||||
export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) {
|
||||
export function checkWatchedDirectories(host: TestServerHost | mocks.MockServerHost, expectedDirectories: string[], recursive = false) {
|
||||
if (host instanceof mocks.MockServerHost) {
|
||||
return checkSortedSet(recursive ? host.vfs.watchedRecursiveDirectories : host.vfs.watchedNonRecursiveDirectories, expectedDirectories);
|
||||
}
|
||||
checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
|
||||
}
|
||||
|
||||
|
@ -244,6 +272,16 @@ interface Array<T> {}`
|
|||
ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
|
||||
}
|
||||
|
||||
function createFileMatcher(host: TestServerHost, path: string) {
|
||||
path = host.getCanonicalFileName(host.getHostSpecificPath(path));
|
||||
return (file: FileOrFolder) => host.getCanonicalFileName(host.getHostSpecificPath(file.path)) === path && isString(file.content);
|
||||
}
|
||||
|
||||
function createFolderMatcher(host: TestServerHost, path: string) {
|
||||
path = host.getCanonicalFileName(host.getHostSpecificPath(path));
|
||||
return (file: FileOrFolder) => host.getCanonicalFileName(host.getHostSpecificPath(file.path)) === path && !isString(file.content);
|
||||
}
|
||||
|
||||
export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost {
|
||||
args: string[] = [];
|
||||
|
||||
|
@ -260,6 +298,63 @@ interface Array<T> {}`
|
|||
readonly watchedFiles = createMultiMap<TestFileWatcher>();
|
||||
private readonly executingFilePath: string;
|
||||
private readonly currentDirectory: string;
|
||||
public files: FileOrFolder[] = [];
|
||||
|
||||
// temporary vfs shim
|
||||
public vfs = {
|
||||
addFile: (path: string, content: string, options?: { overwrite?: boolean }) => {
|
||||
let file = this.files.find(createFileMatcher(this, path));
|
||||
if (file) {
|
||||
if (!(options && options.overwrite)) return undefined;
|
||||
file.content = content;
|
||||
}
|
||||
else {
|
||||
file = { path, content };
|
||||
this.files.push(file);
|
||||
}
|
||||
this.reloadFS(this.files);
|
||||
return file;
|
||||
},
|
||||
writeFile: (path: string, content: string) => {
|
||||
let file = this.files.find(createFileMatcher(this, path));
|
||||
if (file) {
|
||||
file.content = content;
|
||||
}
|
||||
else {
|
||||
file = { path, content };
|
||||
this.files.push(file);
|
||||
}
|
||||
this.reloadFS(this.files);
|
||||
},
|
||||
getFile: (path: string) => {
|
||||
return this.files.find(createFileMatcher(this, path));
|
||||
},
|
||||
removeFile: (path: string) => {
|
||||
const index = this.files.findIndex(createFileMatcher(this, path));
|
||||
if (index >= 0) {
|
||||
ts.orderedRemoveItemAt(this.files, index);
|
||||
this.reloadFS(this.files);
|
||||
}
|
||||
},
|
||||
renameFile: (oldpath: string, newpath: string) => {
|
||||
const oldItem = this.vfs.getFile(oldpath);
|
||||
if (oldItem) {
|
||||
const newIndex = this.files.findIndex(createFileMatcher(this, newpath));
|
||||
if (newIndex >= 0) ts.orderedRemoveItemAt(this.files, newIndex);
|
||||
oldItem.path = newpath;
|
||||
this.reloadFS(this.files);
|
||||
}
|
||||
},
|
||||
addDirectory: (path: string) => {
|
||||
let file = this.files.find(createFolderMatcher(this, path));
|
||||
if (!file) {
|
||||
file = { path };
|
||||
this.files.push(file);
|
||||
this.reloadFS(this.files);
|
||||
}
|
||||
return file;
|
||||
},
|
||||
};
|
||||
|
||||
constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray<FileOrFolder>, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) {
|
||||
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
|
||||
|
@ -291,7 +386,9 @@ interface Array<T> {}`
|
|||
reloadFS(fileOrFolderList: ReadonlyArray<FileOrFolder>, options?: Partial<ReloadWatchInvokeOptions>) {
|
||||
const mapNewLeaves = createMap<true>();
|
||||
const isNewFs = this.fs.size === 0;
|
||||
fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []);
|
||||
if (this.files !== fileOrFolderList) {
|
||||
this.files = fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []);
|
||||
}
|
||||
const filesOrFoldersToLoad: ReadonlyArray<FileOrFolder> = !this.useWindowsStylePath ? fileOrFolderList :
|
||||
fileOrFolderList.map<FileOrFolder>(f => {
|
||||
const result = clone(f);
|
||||
|
|
Загрузка…
Ссылка в новой задаче