feat: Add type definitions for npm module ospec (#37645)

* feat: Add type definitions for npm module `ospec`

* Apply suggestions from code review

Co-Authored-By: Isiah Meadows <contact@isiahmeadows.com>

* fix: Disable no-unnecessary-generics for dummy o.spy() generation

* fix: Rename namespace to match thew global export's name

* fix: Properly guard against non-newable functions

...and relax TypeScript version requirement to 3.1

* feat: Simplify the assertion signature

* fix: Error in `tslint:disable` syntax

* feat: Make .notEquals() and .notDeepEquals() type safe

* feat: Update the `Definer` type...

* Allow tests to return any `PromiseLike` objects.
* Disallow anything but Error and null as argument for `done()`

* feat: Add tests for Definer` functions returning promises

* style: Prefer `import o = require('')` over `import o from` in test file
This commit is contained in:
Már Örlygsson
2019-08-20 19:00:30 +00:00
committed by Sheetal Nandi
parent a9950fcb4a
commit 4b29f4bd1d
4 changed files with 359 additions and 0 deletions

92
types/ospec/index.d.ts vendored Normal file
View File

@@ -0,0 +1,92 @@
// Type definitions for ospec 4.0
// Project: https://github.com/MithrilJS/mithril.js/tree/next/ospec
// Definitions by: Már Örlygsson <https://github.com/maranomynet>
// Mike Linkovich <https://github.com/spacejack>
// Isiah Meadows <https://github.com/isiahmeadows>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 3.1
type ObjectConstructor = new (...args: any[]) => any;
declare namespace o {
type AssertionDescriber = (description: string) => void;
interface Spy<Args extends any[], Returns> {
(...args: Args): Returns;
/** The number of times the function has been called */
readonly callCount: number;
/** The arguments that were passed to the function in the last time it was called */
readonly args: Args;
/** List of arguments that were passed to the function each tine it was called */
readonly calls: Args[];
}
interface Assertion<T> {
/** Asserts that two values are strictly equal */
equals(expected: T): AssertionDescriber;
/** Asserts that two values are **not** strictly equal */
notEquals(value: T): AssertionDescriber;
/** Asserts that two objects are recursively equal */
deepEquals(this: Assertion<object>, expected: T): AssertionDescriber;
/** Asserts that two objects are **not** recursively equal */
notDeepEquals(this: Assertion<object>, value: T): AssertionDescriber;
/** Asserts that the function throws an error of a given type */
throws(this: Assertion<() => any>, error: string | ObjectConstructor): AssertionDescriber;
/** Asserts that the function does **not** throw an error of given type */
notThrows(this: Assertion<() => any>, error: string | ObjectConstructor): AssertionDescriber; // See above
}
type Definer = (done: (error?: Error | null) => void, timeout: (delay: number) => void) => void | PromiseLike<any>;
interface Result {
pass: boolean | null;
context: string;
message: string;
error: Error | null;
testError: Error | null;
}
type Reporter = (results: Result[]) => number;
interface Ospec {
/** Starts an assertion */
<T>(actual: T): Assertion<T>;
/** Defines a test */
(name: string, assertions: Definer): void;
/** Defines a group of tests */
spec(name: string, tests: () => void): void;
/** Defines code to be run at the beginning of a test group */
before(setup: Definer): void;
/** Defines code to be run before each test in a group */
beforeEach(teardown: Definer): void;
/** Defines code to be run at the end of a test group */
after(setup: Definer): void;
/** Defines code to be run after each test in a group */
afterEach(teardown: Definer): void;
/** Returns a function that records the number of times it gets called, and its arguments */
spy<A extends any[]>(): Spy<A, undefined>; // tslint:disable-line:no-unnecessary-generics
spy<A extends any[], R>(fn: (...args: A) => R): Spy<A, R>;
/** Amount of time (in milliseconds) to wait until bailing out of a test */
timeout(delay: number): void;
/** Configure the default amount of time (in milliseconds) to wait until bailing out of a group of tests */
specTimeout(delay: number): void;
/** Runs the test suite */
run(reporter?: Reporter): void;
/** Default reporter used by `o.run()` */
report: Reporter;
'new'(): Ospec;
}
}
declare const o: o.Ospec;
export = o;
export as namespace o;

243
types/ospec/ospec-tests.ts Normal file
View File

@@ -0,0 +1,243 @@
import o = require('ospec');
// import o, { Definer } from 'ospec'; // NOTE: this only works with "esModuleInterop": true
const exampleTypeUse1: o.Definer = () => {};
// const exampleTypeUse2: Definer = () => {}; // NOTE: this only works with "esModuleInterop": true
// ======================================================================
// $ExpectType void
o.spec('ospec typings', () => {
const bool = false;
const numOrStr = Date.now() > 0 ? 'hi' : 42;
const obj = { a: 1 };
const arr = [1];
const fn = () => {};
// $ExpectType void
o('o(actual) returns assertion interface based on input type', () => {
o(bool); // $ExpectType Assertion<boolean>
o(numOrStr); // $ExpectType Assertion<string | number>
o(arr); // $ExpectType Assertion<number[]>
o(obj); // $ExpectType Assertion<{ a: number; }>
o(new Date()); // $ExpectType Assertion<Date>
o(fn); // $ExpectType Assertion<() => void>
});
o('.equals() is type safe', () => {
o(bool).equals(true); // $ExpectType AssertionDescriber
o(bool).equals(true)('description text');
o(numOrStr).equals('hello');
o(numOrStr).equals(1);
o(fn).equals(() => {});
o(obj).equals({ a: 1 });
o(arr).equals([1, 2]);
// $ExpectError
o(bool).equals(1);
// $ExpectError
o(numOrStr).equals(true);
// $ExpectError
o(fn).equals(1);
// $ExpectError
o(obj).equals({});
// $ExpectError
o(arr).equals(['hi']);
});
o('.notEquals() is also type safe', () => {
o(bool).notEquals(true); // $ExpectType AssertionDescriber
o(bool).notEquals(true)('description text');
o(numOrStr).notEquals('hello');
o(numOrStr).notEquals(1);
o(fn).notEquals(() => {});
o(obj).notEquals({ a: 1 });
o(arr).notEquals([1, 2]);
// $ExpectError
o(bool).notEquals(1);
// $ExpectError
o(numOrStr).notEquals(true);
// $ExpectError
o(fn).notEquals(1);
// $ExpectError
o(obj).notEquals({});
// $ExpectError
o(arr).notEquals(['hi']);
});
o('.deepEquals()/.notDeepEquals() only compares objects to object values', () => {
o(obj).deepEquals({
a: 1,
});
o(arr).deepEquals([1]); // $ExpectType AssertionDescriber
o(fn).deepEquals(() => {}); // $ExpectType AssertionDescriber
o(obj).notDeepEquals({
a: 1,
});
o(arr).notDeepEquals([1]); // $ExpectType AssertionDescriber
o(fn).notDeepEquals(() => {}); // $ExpectType AssertionDescriber
// $ExpectError
o(obj).deepEquals(1);
// $ExpectError
o(obj).notDeepEquals(1);
// $ExpectError
o(bool).deepEquals(1);
// $ExpectError
o(bool).notDeepEquals(1);
// $ExpectError
o(numOrStr).deepEquals(1);
// $ExpectError
o(numOrStr).notDeepEquals(1);
// $ExpectError
o(obj).notDeepEquals({});
// $ExpectError
o(arr).notDeepEquals(['hi']); // $ExpectType AssertionDescriber
// $ExpectError
o(fn).notDeepEquals({}); // $ExpectType AssertionDescriber
});
o('.throws()/.notThrows() only available for function values', () => {
o(fn).throws('baz'); // $ExpectType AssertionDescriber
o(fn).notThrows('baz'); // $ExpectType AssertionDescriber
o(fn).throws(Error);
// NOTE: ospec says trows/notThrows accepts "Object constructor"
o(fn).notThrows(String);
// $ExpectError
o(bool).throws('baz');
// $ExpectError
o(bool).notThrows('baz');
// `expected` must only be string or "Object constructor"
// $ExpectError
o(fn).throws(1);
// $ExpectError
o(fn).throws(1);
const nonNewableFn = () => {};
// $ExpectError
o(fn).throws(nonNewableFn);
// $ExpectError
o(fn).notThrows(nonNewableFn);
});
// ======================================================================
const dummySpy = o.spy();
dummySpy();
const {
callCount, // $ExpectType number
args, // $ExpectType any[]
calls, // $ExpectType any[][]
} = dummySpy;
const myFunc = (a: string, b?: boolean) => 42;
const spiedFunc = o.spy(myFunc);
type SpiedFuncParams = Parameters<typeof spiedFunc>; // $ExpectType [string, (boolean | undefined)?]
const _args1: SpiedFuncParams = ['hi', true];
const _args2: SpiedFuncParams = ['hi'];
spiedFunc(..._args1); // $ExpectType number
spiedFunc(..._args2); // $ExpectType number
spiedFunc.args; // $ExpectType [string, (boolean | undefined)?]
spiedFunc.calls; // $ExpectType [string, (boolean | undefined)?][]
// ======================================================================
let definerFn: o.Definer;
definerFn = () => {};
definerFn = done => {
done(); // $ExpectType void
done(new Error('err'));
done(null);
// $ExpectError
done('Error message');
// $ExpectError
done(1);
// $ExpectError
done(null, null);
};
definerFn = (_, timeout) => {
timeout(42); // $ExpectType void
// $ExpectError
timeout();
// $ExpectError
timeout('42');
// $ExpectError
timeout(1, 2);
};
// Tests may return a promise-like value instead of calling done()
definerFn = () => {
return Promise.resolve('Whatever');
};
definerFn = (done, timeout) => {
timeout(9000);
// TODO: Find a way to discourage the use of done() in promise returning tests
// $_ExpectError
done();
return Promise.resolve('Whatever');
};
o('async tests', definerFn);
o.before(definerFn); // $ExpectType void
o.after(definerFn); // $ExpectType void
o.beforeEach(definerFn); // $ExpectType void
o.afterEach(definerFn); // $ExpectType void
// ======================================================================
o.specTimeout(42); // $ExpectType void
// $ExpectError
o.specTimeout();
// $ExpectError
o.specTimeout('42');
o('async test timeout', _ => {
o.timeout(42); // $ExpectType void
// $ExpectError
o.timeout();
// $ExpectError
o.timeout('42');
});
// ======================================================================
const myReporter: o.Reporter = results => {
const myResult = results[0]; // $ExpectType Result
return 0;
};
o.report = myReporter;
// $ExpectError
o.report = fn;
// ======================================================================
o.run(); // $ExpectType void
o.run(myReporter);
// $ExpectError
o.run(true);
// $ExpectError
o.run(fn);
// ======================================================================
const o2: o.Ospec = o.new();
o2.spec('New Ospec instance', () => {
o2('Works?', done => {
o2('Yes').equals('Yes');
done();
});
});
// $ExpectError
o.new(true);
});
// $ExpectError
o.spec(() => {}); // Missing name parameter

23
types/ospec/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": [
"es6"
],
"noImplicitAny": true,
"noImplicitThis": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"baseUrl": "../",
"typeRoots": [
"../"
],
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.d.ts",
"ospec-tests.ts"
]
}

1
types/ospec/tslint.json Normal file
View File

@@ -0,0 +1 @@
{ "extends": "dtslint/dt.json" }