diff --git a/.gitignore b/.gitignore index 0fe5f8ea5..9c7dbb960 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules .nyc_output coverage bench/.results +types/generated.d.ts diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index c949a124d..000000000 --- a/index.d.ts +++ /dev/null @@ -1,212 +0,0 @@ -export interface Observable { - subscribe(observer: (value: {}) => void): void; -} - -export type Test = (t: TestContext) => Promise | Iterator | Observable | void; -export type ContextualTest = (t: ContextualTestContext) => Promise | Iterator | Observable | void; -export type SerialTest = (t: TestContext) => void; -export type ContextualSerialTest = (t: ContextualTestContext) => void; -export type CallbackTest = (t: CallbackTestContext) => void; -export type ContextualCallbackTest = (t: ContextualCallbackTestContext) => void; - -export interface Runner { - (name: string, run: Test): void; - (run: Test): void; - skip: Runner; - cb: CallbackRunner; -} -export interface AfterRunner extends Runner { - always: Runner; -} -export interface ContextualRunner { - (name: string, run: ContextualTest): void; - (run: ContextualTest): void; - skip: ContextualRunner; - cb: ContextualCallbackRunner; -} -export interface ContextualAfterRunner extends ContextualRunner { - always: ContextualRunner; -} -export interface SerialRunner { - (name: string, run: SerialTest): void; - (run: SerialTest): void; - skip: SerialRunner; -} -export interface ContextualSerialRunner { - (name: string, run: ContextualSerialTest): void; - (run: ContextualSerialTest): void; - skip: ContextualSerialRunner; -} -export interface CallbackRunner { - (name: string, run: CallbackTest): void; - (run: CallbackTest): void; - skip: CallbackRunner; -} -export interface ContextualCallbackRunner { - (name: string, run: ContextualCallbackTest): void; - (run: ContextualCallbackTest): void; - skip: ContextualCallbackRunner; -} - -export function test(name: string, run: ContextualTest): void; -export function test(run: ContextualTest): void; -export namespace test { - export const before: Runner; - export const after: AfterRunner; - export const beforeEach: ContextualRunner; - export const afterEach: ContextualAfterRunner; - - export const skip: typeof test; - export const only: typeof test; - - export function serial(name: string, run: ContextualSerialTest): void; - export function serial(run: ContextualSerialTest): void; - export function failing(name: string, run: ContextualCallbackTest): void; - export function failing(run: ContextualCallbackTest): void; - export function cb(name: string, run: ContextualCallbackTest): void; - export function cb(run: ContextualCallbackTest): void; - export function todo(name: string): void; -} -export namespace test.serial { - export const before: SerialRunner; - export const after: SerialRunner; - export const beforeEach: ContextualSerialRunner; - export const afterEach: ContextualSerialRunner; - - export const skip: typeof test.serial; - export const only: typeof test.serial; - - export function cb(name: string, run: ContextualCallbackTest): void; - export function cb(run: ContextualCallbackTest): void; -} -export namespace test.failing { - export const before: CallbackRunner; - export const after: CallbackRunner; - export const beforeEach: ContextualCallbackRunner; - export const afterEach: ContextualCallbackRunner; - - export const skip: typeof test.cb; - export const only: typeof test.cb; - - export function cb(name: string, run: ContextualCallbackTest): void; - export function cb(run: ContextualCallbackTest): void; -} -export namespace test.cb { - export const before: CallbackRunner; - export const after: CallbackRunner; - export const beforeEach: ContextualCallbackRunner; - export const afterEach: ContextualCallbackRunner; - - export const skip: typeof test.cb; - export const only: typeof test.cb; -} -export default test; - -export type ErrorValidator - = (new (...args: any[]) => any) - | RegExp - | string - | ((error: any) => boolean); - -export interface AssertContext { - /** - * Passing assertion. - */ - pass(message?: string): void; - /** - * Failing assertion. - */ - fail(message?: string): void; - /** - * Assert that value is truthy. - */ - truthy(value: any, message?: string): void; - /** - * Assert that value is falsy. - */ - falsy(value: any, message?: string): void; - /** - * DEPRECATED, use `truthy`. Assert that value is truthy. - */ - ok(value: any, message?: string): void; - /** - * DEPRECATED, use `falsy`. Assert that value is falsy. - */ - notOk(value: any, message?: string): void; - /** - * Assert that value is true. - */ - true(value: boolean, message?: string): void; - /** - * Assert that value is false. - */ - false(value: boolean, message?: string): void; - /** - * Assert that value is equal to expected. - */ - is(value: U, expected: U, message?: string): void; - /** - * Assert that value is not equal to expected. - */ - not(value: U, expected: U, message?: string): void; - /** - * Assert that value is deep equal to expected. - */ - deepEqual(value: U, expected: U, message?: string): void; - /** - * Assert that value is not deep equal to expected. - */ - notDeepEqual(value: U, expected: U, message?: string): void; - /** - * Assert that function throws an error or promise rejects. - * @param error Can be a constructor, regex, error message or validation function. - */ - /** - * DEPRECATED, use `deepEqual`. Assert that value is deep equal to expected. - */ - same(value: U, expected: U, message?: string): void; - /** - * DEPRECATED use `notDeepEqual`. Assert that value is not deep equal to expected. - */ - notSame(value: U, expected: U, message?: string): void; - /** - * Assert that function throws an error or promise rejects. - * @param error Can be a constructor, regex, error message or validation function. - */ - throws(value: Promise<{}>, error?: ErrorValidator, message?: string): Promise; - throws(value: () => void, error?: ErrorValidator, message?: string): any; - /** - * Assert that function doesn't throw an error or promise resolves. - */ - notThrows(value: Promise, message?: string): Promise; - notThrows(value: () => void, message?: string): void; - /** - * Assert that contents matches regex. - */ - regex(contents: string, regex: RegExp, message?: string): void; - /** - * Assert that error is falsy. - */ - ifError(error: any, message?: string): void; -} -export interface TestContext extends AssertContext { - /** - * Plan how many assertion there are in the test. - * The test will fail if the actual assertion count doesn't match planned assertions. - */ - plan(count: number): void; - - skip: AssertContext; -} -export interface CallbackTestContext extends TestContext { - /** - * End the test. - */ - end(): void; -} -export interface ContextualTestContext extends TestContext { - context: any; -} -export interface ContextualCallbackTestContext extends CallbackTestContext { - context: any; -} diff --git a/lib/runner.js b/lib/runner.js index 250159372..f39ed1070 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -198,3 +198,5 @@ Runner.prototype.run = function (options) { return Promise.resolve(this.tests.build(this._bail).run()).then(this._buildStats); }; + +Runner._chainableMethods = chainableMethods.chainableMethods; diff --git a/package.json b/package.json index 2acc191bc..c3d97a2f0 100644 --- a/package.json +++ b/package.json @@ -33,18 +33,21 @@ } ], "bin": "cli.js", + "typings": "types/generated.d.ts", "engines": { "node": ">=0.10.0" }, "scripts": { "test": "xo && nyc --cache --reporter=lcov --reporter=text tap --no-cov --timeout=150 test/*.js test/reporters/*.js", "test-win": "tap --no-cov --reporter=classic --timeout=150 test/*.js test/reporters/*.js", - "visual": "node test/visual/run-visual-tests.js" + "visual": "node test/visual/run-visual-tests.js", + "prepublish": "npm run make-ts", + "make-ts": "babel-node --presets=babel-preset-es2015 --plugins=transform-runtime types/make.js" }, "files": [ "lib", "*.js", - "index.d.ts" + "types/generated.d.ts" ], "keywords": [ "test", @@ -153,6 +156,7 @@ "update-notifier": "^1.0.0" }, "devDependencies": { + "babel-cli": "^6.10.1", "babel-preset-react": "^6.5.0", "cli-table2": "^0.2.0", "coveralls": "^2.11.4", @@ -182,7 +186,9 @@ }, "overrides": [ { - "files": ["test/**/*.js"], + "files": [ + "test/**/*.js" + ], "rules": { "max-lines": 0 } diff --git a/types/base.d.ts b/types/base.d.ts new file mode 100644 index 000000000..e1f4f974b --- /dev/null +++ b/types/base.d.ts @@ -0,0 +1,124 @@ +export default test; + +export type ErrorValidator + = (new (...args: any[]) => any) + | RegExp + | string + | ((error: any) => boolean); + +export interface Observable { + subscribe(observer: (value: {}) => void): void; +} + +export type Test = (t: TestContext) => PromiseLike | Iterator | Observable | void; +export type ContextualTest = (t: ContextualTestContext) => PromiseLike | Iterator | Observable | void; +export type CallbackTest = (t: CallbackTestContext) => void; +export type ContextualCallbackTest = (t: ContextualCallbackTestContext) => void; + +export interface AssertContext { + /** + * Passing assertion. + */ + pass(message?: string): void; + /** + * Failing assertion. + */ + fail(message?: string): void; + /** + * Assert that value is truthy. + */ + truthy(value: any, message?: string): void; + /** + * Assert that value is falsy. + */ + falsy(value: any, message?: string): void; + /** + * DEPRECATED, use `truthy`. Assert that value is truthy. + */ + ok(value: any, message?: string): void; + /** + * DEPRECATED, use `falsy`. Assert that value is falsy. + */ + notOk(value: any, message?: string): void; + /** + * Assert that value is true. + */ + true(value: boolean, message?: string): void; + /** + * Assert that value is false. + */ + false(value: boolean, message?: string): void; + /** + * Assert that value is equal to expected. + */ + is(value: U, expected: U, message?: string): void; + /** + * Assert that value is not equal to expected. + */ + not(value: U, expected: U, message?: string): void; + /** + * Assert that value is deep equal to expected. + */ + deepEqual(value: U, expected: U, message?: string): void; + /** + * Assert that value is not deep equal to expected. + */ + notDeepEqual(value: U, expected: U, message?: string): void; + /** + * Assert that function throws an error or promise rejects. + * DEPRECATED, use `deepEqual`. Assert that value is deep equal to expected. + * @param error Can be a constructor, regex, error message or validation function. + */ + same(value: U, expected: U, message?: string): void; + /** + * DEPRECATED use `notDeepEqual`. Assert that value is not deep equal to expected. + */ + notSame(value: U, expected: U, message?: string): void; + /** + * Assert that function throws an error or promise rejects. + * @param error Can be a constructor, regex, error message or validation function. + */ + throws(value: PromiseLike, error?: ErrorValidator, message?: string): Promise; + throws(value: () => void, error?: ErrorValidator, message?: string): any; + /** + * Assert that function doesn't throw an error or promise resolves. + */ + notThrows(value: PromiseLike, message?: string): Promise; + notThrows(value: () => void, message?: string): void; + /** + * Assert that contents matches regex. + */ + regex(contents: string, regex: RegExp, message?: string): void; + /** + * Assert that contents does not match regex. + */ + notRegex(contents, regex, message?: string): void; + /** + * Assert that error is falsy. + */ + ifError(error: any, message?: string): void; +} +export interface TestContext extends AssertContext { + /** + * Plan how many assertion there are in the test. + * The test will fail if the actual assertion count doesn't match planned assertions. + */ + plan(count: number): void; + + skip: AssertContext; +} +export interface CallbackTestContext extends TestContext { + /** + * End the test. + */ + end(): void; +} +export interface ContextualTestContext extends TestContext { + context: any; +} +export interface ContextualCallbackTestContext extends CallbackTestContext { + context: any; +} + +export function test(name: string, run: ContextualTest): void; +export function test(run: ContextualTest): void; diff --git a/types/make.js b/types/make.js new file mode 100644 index 000000000..0fa222dac --- /dev/null +++ b/types/make.js @@ -0,0 +1,124 @@ +'use strict'; + +// TypeScript definitions are generated here. +// AVA allows chaining of function names, like `test.after.cb.always`. +// The order of these names is not important. +// Writing these definitions by hand is hard. Because of chaining, +// the number of combinations grows fast (2^n). To reduce this number, +// illegal combinations are filtered out in `verify`. +// The order of the options is not important. We could generate full +// definitions for each possible order, but that would give a very big +// output. Instead, we write an alias for different orders. For instance, +// `after.cb` is fully written, and `cb.after` is emitted as an alias +// using `typeof after.cb`. + +const path = require('path'); +const fs = require('fs'); +const runner = require('../lib/runner'); + +const arrayHas = parts => part => parts.includes(part); + +const base = fs.readFileSync(path.join(__dirname, 'base.d.ts'), 'utf8'); + +// All suported function names +const allParts = Object.keys(runner._chainableMethods).filter(name => name !== 'test'); + +const output = base + generatePrefixed([]); +fs.writeFileSync(path.join(__dirname, 'generated.d.ts'), output); + +// Generates type definitions, for the specified prefix +// The prefix is an array of function names +function generatePrefixed(prefix) { + let output = ''; + let children = ''; + + for (const part of allParts) { + const parts = prefix.concat([part]); + + if (prefix.includes(part) || !verify(parts, true)) { + // Function already in prefix or not allowed here + continue; + } + + // Check that `part` is a valid function name. + // `always` is a valid prefix, for instance of `always.after`, + // but not a valid function name. + if (verify(parts, false)) { + if (!isSorted(parts)) { + output += '\texport const ' + part + ': typeof test.' + parts.sort().join('.') + ';\n'; + continue; + } else if (prefix.includes(parts, 'todo')) { + output += '\t' + writeFunction(part, 'name: string', 'void'); + } else { + const type = testType(parts); + output += '\t' + writeFunction(part, 'name: string, implementation: ' + type); + output += '\t' + writeFunction(part, 'implementation: ' + type); + } + } + + children += generatePrefixed(parts); + } + if (output === '') { + return children; + } + return 'export namespace ' + ['test'].concat(prefix).join('.') + ' {\n' + output + '}\n' + children; +} + +function writeFunction(name, args) { + return 'export function ' + name + '(' + args + '): void;\n'; +} + +function verify(parts, asPrefix) { + const has = arrayHas(parts); + if (has('only') + has('skip') + has('todo') > 1) { + return false; + } + const beforeAfterCount = has('before') + has('beforeEach') + has('after') + has('afterEach'); + if (beforeAfterCount > 1) { + return false; + } + if (beforeAfterCount === 1) { + if (has('only')) { + return false; + } + } + + if (has('always')) { + // `always` can only be used with `after` or `afterEach`. + // Without it can still be a valid prefix + if (has('after') || has('afterEach')) { + if (!asPrefix) { + return false; + } + } else if (!verify(parts.concat(['after'])) && !verify(parts.concat(['afterEach']))) { + // If `after` nor `afterEach` cannot be added to this prefix, + // `always` is not allowed here. + return false; + } + } + + return true; +} + +// Checks that an array is sorted +function isSorted(a) { + for (let i = 1; i < a.length; i++) { + if (a[i - 1] >= a[i]) { + return false; + } + } + return true; +} + +// Returns the type name of for the test implementation +function testType(parts) { + const has = arrayHas(parts); + let type = 'Test'; + if (has('cb')) { + type = 'Callback' + type; + } + if (!has('beforeEach') && !has('afterEach')) { + type = 'Contextual' + type; + } + return type; +}