From 4b29f4bd1d465705a03cf8a6f75855c84cdcbe04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1r=20=C3=96rlygsson?= Date: Tue, 20 Aug 2019 19:00:30 +0000 Subject: [PATCH] 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 * 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 --- types/ospec/index.d.ts | 92 ++++++++++++++ types/ospec/ospec-tests.ts | 243 +++++++++++++++++++++++++++++++++++++ types/ospec/tsconfig.json | 23 ++++ types/ospec/tslint.json | 1 + 4 files changed, 359 insertions(+) create mode 100644 types/ospec/index.d.ts create mode 100644 types/ospec/ospec-tests.ts create mode 100644 types/ospec/tsconfig.json create mode 100644 types/ospec/tslint.json diff --git a/types/ospec/index.d.ts b/types/ospec/index.d.ts new file mode 100644 index 0000000000..84bdf89ba3 --- /dev/null +++ b/types/ospec/index.d.ts @@ -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 +// Mike Linkovich +// Isiah Meadows +// 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: 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 { + /** 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, expected: T): AssertionDescriber; + /** Asserts that two objects are **not** recursively equal */ + notDeepEquals(this: Assertion, 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; + + 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 */ + (actual: T): Assertion; + + /** 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(): Spy; // tslint:disable-line:no-unnecessary-generics + spy(fn: (...args: A) => R): Spy; + + /** 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; diff --git a/types/ospec/ospec-tests.ts b/types/ospec/ospec-tests.ts new file mode 100644 index 0000000000..3ba76f6755 --- /dev/null +++ b/types/ospec/ospec-tests.ts @@ -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 + o(numOrStr); // $ExpectType Assertion + o(arr); // $ExpectType Assertion + o(obj); // $ExpectType Assertion<{ a: number; }> + o(new Date()); // $ExpectType Assertion + 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; // $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 diff --git a/types/ospec/tsconfig.json b/types/ospec/tsconfig.json new file mode 100644 index 0000000000..6862d8a794 --- /dev/null +++ b/types/ospec/tsconfig.json @@ -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" + ] +} diff --git a/types/ospec/tslint.json b/types/ospec/tslint.json new file mode 100644 index 0000000000..3db14f85ea --- /dev/null +++ b/types/ospec/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dt.json" }