diff --git a/readme.md b/readme.md
index 9f13621a..55686df0 100644
--- a/readme.md
+++ b/readme.md
@@ -191,10 +191,13 @@ You can use the programmatic API to retrieve the diagnostics and do something wi
import tsd from 'tsd';
(async () => {
- const diagnostics = await tsd();
+ const diagnoser = await tsd();
- console.log(diagnostics.length);
- //=> 2
+ // Returns the number of tests evaluated.
+ console.log(diagnoser.testCount)
+
+ // Returns the diagnostics if any or just an empty array.
+ console.log(diagnoser.diagnostics);
})();
```
@@ -213,6 +216,19 @@ Default: `process.cwd()`
Current working directory of the project to retrieve the diagnostics for.
+##### typingsFile
+
+Type: `string`
+Default: The `types` property in `package.json`.
+
+Path to the type definition file you want to test. This can be useful when using a test runner to test specific type definitions per test.
+
+##### testFiles
+
+Type: `string[]`
+Default: Finds files with `.test-d.ts` or `.test-d.tsx` extension.
+
+An array of test files with their path. Uses [globby](https://github.com/sindresorhus/globby) under the hood so that you can fine tune test file discovery.
## License
diff --git a/source/cli.ts b/source/cli.ts
index 6712b892..e9d6cc6f 100644
--- a/source/cli.ts
+++ b/source/cli.ts
@@ -23,10 +23,11 @@ const cli = meow(`
try {
const options = cli.input.length > 0 ? {cwd: cli.input[0]} : undefined;
- const diagnostics = await tsd(options);
+ const extendedDiagnostics = await tsd(options);
+ const diagnostics = extendedDiagnostics.diagnostics;
if (diagnostics.length > 0) {
- throw new Error(formatter(diagnostics));
+ throw new Error(formatter(extendedDiagnostics));
}
} catch (error) {
console.error(error.message);
diff --git a/source/lib/compiler.ts b/source/lib/compiler.ts
index 07d67582..8fada40b 100644
--- a/source/lib/compiler.ts
+++ b/source/lib/compiler.ts
@@ -1,4 +1,3 @@
-import * as path from 'path';
import {
flattenDiagnosticMessageText,
createProgram,
@@ -7,7 +6,7 @@ import {
} from '../../libraries/typescript';
import {TypeChecker} from './entities/typescript';
import {extractAssertions, parseErrorAssertionToLocation} from './parser';
-import {Diagnostic, DiagnosticCode, Context, Location} from './interfaces';
+import {Diagnostic, DiagnosticCode, Context, Location, ExtendedDiagnostic} from './interfaces';
import {handle} from './assertions';
// List of diagnostic codes that should be ignored in general
@@ -64,12 +63,11 @@ const ignoreDiagnostic = (diagnostic: TSDiagnostic, expectedErrors: Map {
- const fileNames = context.testFiles.map(fileName => path.join(context.cwd, fileName));
-
+export const getDiagnostics = (context: Context): ExtendedDiagnostic => {
+ let testCount = 0;
const diagnostics: Diagnostic[] = [];
- const program = createProgram(fileNames, context.config.compilerOptions);
+ const program = createProgram(context.testFiles, context.config.compilerOptions);
const tsDiagnostics = program
.getSemanticDiagnostics()
@@ -77,6 +75,10 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
const assertions = extractAssertions(program);
+ for (const assertion of assertions) {
+ testCount = testCount + assertion[1].size;
+ }
+
diagnostics.push(...handle(program.getTypeChecker() as TypeChecker, assertions));
const expectedErrors = parseErrorAssertionToLocation(assertions);
@@ -105,5 +107,5 @@ export const getDiagnostics = (context: Context): Diagnostic[] => {
});
}
- return diagnostics;
+ return {testCount, diagnostics};
};
diff --git a/source/lib/formatter.ts b/source/lib/formatter.ts
index 3fdb23eb..9bafb36f 100644
--- a/source/lib/formatter.ts
+++ b/source/lib/formatter.ts
@@ -1,5 +1,5 @@
import * as formatter from 'eslint-formatter-pretty';
-import {Diagnostic} from './interfaces';
+import {ExtendedDiagnostic} from './interfaces';
/**
* Format the TypeScript diagnostics to a human readable output.
@@ -7,7 +7,8 @@ import {Diagnostic} from './interfaces';
* @param diagnostics - List of TypeScript diagnostics.
* @returns Beautiful diagnostics output
*/
-export default (diagnostics: Diagnostic[]) => {
+export default (extendedDiagnostics: ExtendedDiagnostic) => {
+ const diagnostics = extendedDiagnostics.diagnostics;
const fileMap = new Map();
for (const diagnostic of diagnostics) {
diff --git a/source/lib/index.ts b/source/lib/index.ts
index 4e56dbb2..cac66d90 100644
--- a/source/lib/index.ts
+++ b/source/lib/index.ts
@@ -9,11 +9,12 @@ import {Context, Config} from './interfaces';
export interface Options {
cwd: string;
+ typingsFile?: string;
+ testFiles?: string[];
}
const findTypingsFile = async (pkg: any, options: Options) => {
- const typings = pkg.types || pkg.typings || 'index.d.ts';
-
+ const typings = options.typingsFile || pkg.types || pkg.typings || 'index.d.ts';
const typingsExist = await pathExists(path.join(options.cwd, typings));
if (!typingsExist) {
@@ -23,13 +24,37 @@ const findTypingsFile = async (pkg: any, options: Options) => {
return typings;
};
-const findTestFiles = async (typingsFile: string, options: Options & {config: Config}) => {
+const normalizeTypingsFilePath = (typingsFilePath: string, options: Options) => {
+ if (options.typingsFile) {
+ return path.basename(typingsFilePath);
+ }
+
+ return typingsFilePath;
+};
+
+const findCustomTestFiles = async (testFilesPattern: readonly string[], cwd: string) => {
+ const testFiles = await globby(testFilesPattern, {cwd});
+
+ if (testFiles.length === 0) {
+ throw new Error('Could not find any test files. Create one and try again');
+ }
+
+ return testFiles.map(file => path.join(cwd, file));
+};
+
+const findTestFiles = async (typingsFilePath: string, options: Options & {config: Config}) => {
+ if (options.testFiles?.length) {
+ return findCustomTestFiles(options.testFiles, options.cwd);
+ }
+
+ // Return only the filename if the `typingsFile` option is used.
+ const typingsFile = normalizeTypingsFilePath(typingsFilePath, options);
+
const testFile = typingsFile.replace(/\.d\.ts$/, '.test-d.ts');
const tsxTestFile = typingsFile.replace(/\.d\.ts$/, '.test-d.tsx');
const testDir = options.config.directory;
let testFiles = await globby([testFile, tsxTestFile], {cwd: options.cwd});
-
const testDirExists = await pathExists(path.join(options.cwd, testDir));
if (testFiles.length === 0 && !testDirExists) {
@@ -40,7 +65,7 @@ const findTestFiles = async (typingsFile: string, options: Options & {config: Co
testFiles = await globby([`${testDir}/**/*.ts`, `${testDir}/**/*.tsx`], {cwd: options.cwd});
}
- return testFiles;
+ return testFiles.map(fileName => path.join(options.cwd, fileName));
};
/**
@@ -56,7 +81,6 @@ export default async (options: Options = {cwd: process.cwd()}) => {
}
const pkg = pkgResult.packageJson;
-
const config = loadConfig(pkg as any, options.cwd);
// Look for a typings file, otherwise use `index.d.ts` in the root directory. If the file is not found, throw an error.
@@ -72,11 +96,19 @@ export default async (options: Options = {cwd: process.cwd()}) => {
pkg,
typingsFile,
testFiles,
- config
+ typingsFile,
+ cwd: options.cwd
};
- return [
- ...getCustomDiagnostics(context),
- ...getTSDiagnostics(context)
- ];
+ const tsDiagnostics = getTSDiagnostics(context);
+ const customDiagnostics = getCustomDiagnostics(context);
+ const testCount = tsDiagnostics.testCount;
+
+ return {
+ testCount,
+ diagnostics: [
+ ...customDiagnostics,
+ ...tsDiagnostics.diagnostics
+ ]
+ };
};
diff --git a/source/lib/interfaces.ts b/source/lib/interfaces.ts
index bb289339..74344d43 100644
--- a/source/lib/interfaces.ts
+++ b/source/lib/interfaces.ts
@@ -38,6 +38,11 @@ export interface Diagnostic {
column?: number;
}
+export interface ExtendedDiagnostic {
+ testCount: number;
+ diagnostics: Diagnostic[];
+}
+
export interface Location {
fileName: string;
start: number;
diff --git a/source/lib/rules/index.ts b/source/lib/rules/index.ts
index a4229e0a..d4f71634 100644
--- a/source/lib/rules/index.ts
+++ b/source/lib/rules/index.ts
@@ -16,7 +16,7 @@ const rules = new Set([
* @param context - The context object.
* @returns List of diagnostics
*/
-export default (context: Context) => {
+export default (context: Context): Diagnostic[] => {
const diagnostics: Diagnostic[] = [];
for (const rule of rules) {
diff --git a/source/test/fixtures/specify-test-files/index.d.ts b/source/test/fixtures/specify-test-files/index.d.ts
new file mode 100644
index 00000000..0616ebaa
--- /dev/null
+++ b/source/test/fixtures/specify-test-files/index.d.ts
@@ -0,0 +1,6 @@
+declare const one: {
+ (foo: string, bar: string): string;
+ (foo: number, bar: number): number;
+};
+
+export default one;
diff --git a/source/test/fixtures/specify-test-files/index.js b/source/test/fixtures/specify-test-files/index.js
new file mode 100644
index 00000000..f17717f5
--- /dev/null
+++ b/source/test/fixtures/specify-test-files/index.js
@@ -0,0 +1,3 @@
+module.exports.default = (foo, bar) => {
+ return foo + bar;
+};
diff --git a/source/test/fixtures/specify-test-files/package.json b/source/test/fixtures/specify-test-files/package.json
new file mode 100644
index 00000000..de6dc1db
--- /dev/null
+++ b/source/test/fixtures/specify-test-files/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "foo"
+}
diff --git a/source/test/fixtures/specify-test-files/unknown.test.ts b/source/test/fixtures/specify-test-files/unknown.test.ts
new file mode 100644
index 00000000..080ee4ca
--- /dev/null
+++ b/source/test/fixtures/specify-test-files/unknown.test.ts
@@ -0,0 +1,5 @@
+import {expectType} from '../../..';
+import one from '.';
+
+expectType(one('foo', 'bar'));
+expectType(one(1, 2));
diff --git a/source/test/fixtures/typings-custom-dir/index.js b/source/test/fixtures/typings-custom-dir/index.js
new file mode 100644
index 00000000..f17717f5
--- /dev/null
+++ b/source/test/fixtures/typings-custom-dir/index.js
@@ -0,0 +1,3 @@
+module.exports.default = (foo, bar) => {
+ return foo + bar;
+};
diff --git a/source/test/fixtures/typings-custom-dir/index.test-d.ts b/source/test/fixtures/typings-custom-dir/index.test-d.ts
new file mode 100644
index 00000000..a6fedd96
--- /dev/null
+++ b/source/test/fixtures/typings-custom-dir/index.test-d.ts
@@ -0,0 +1,5 @@
+import {expectType} from '../../..';
+import one from './utils';
+
+expectType(one('foo', 'bar'));
+expectType(one(1, 2));
diff --git a/source/test/fixtures/typings-custom-dir/package.json b/source/test/fixtures/typings-custom-dir/package.json
new file mode 100644
index 00000000..de6dc1db
--- /dev/null
+++ b/source/test/fixtures/typings-custom-dir/package.json
@@ -0,0 +1,3 @@
+{
+ "name": "foo"
+}
diff --git a/source/test/fixtures/typings-custom-dir/utils/index.d.ts b/source/test/fixtures/typings-custom-dir/utils/index.d.ts
new file mode 100644
index 00000000..0616ebaa
--- /dev/null
+++ b/source/test/fixtures/typings-custom-dir/utils/index.d.ts
@@ -0,0 +1,6 @@
+declare const one: {
+ (foo: string, bar: string): string;
+ (foo: number, bar: number): number;
+};
+
+export default one;
diff --git a/source/test/fixtures/utils.ts b/source/test/fixtures/utils.ts
index cab9e6e7..d915f59a 100644
--- a/source/test/fixtures/utils.ts
+++ b/source/test/fixtures/utils.ts
@@ -1,5 +1,5 @@
import {ExecutionContext} from 'ava';
-import {Diagnostic} from '../../lib/interfaces';
+import {ExtendedDiagnostic} from '../../lib/interfaces';
type Expectation = [number, number, 'error' | 'warning', string, (string | RegExp)?];
@@ -7,10 +7,11 @@ type Expectation = [number, number, 'error' | 'warning', string, (string | RegEx
* Verify a list of diagnostics.
*
* @param t - The AVA execution context.
- * @param diagnostics - List of diagnostics to verify.
+ * @param extendedDiagnostics - Object containing testCount and list of diagnostics to verify
* @param expectations - Expected diagnostics.
*/
-export const verify = (t: ExecutionContext, diagnostics: Diagnostic[], expectations: Expectation[]) => {
+export const verify = (t: ExecutionContext, extendedDiagnostics: ExtendedDiagnostic, expectations: Expectation[]) => {
+ const diagnostics = extendedDiagnostics.diagnostics;
t.true(diagnostics.length === expectations.length);
for (const [index, diagnostic] of diagnostics.entries()) {
diff --git a/source/test/test.ts b/source/test/test.ts
index ebe99335..e4501150 100644
--- a/source/test/test.ts
+++ b/source/test/test.ts
@@ -220,3 +220,42 @@ test('strict types', async t => {
verify(t, diagnostics, []);
});
+
+test('typings in custom directory', async t => {
+ const diagnostics = await tsd({
+ cwd: path.join(__dirname, 'fixtures/typings-custom-dir'),
+ typingsFile: 'utils/index.d.ts'
+ });
+
+ verify(t, diagnostics, [
+ [5, 19, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'string\'.']
+ ]);
+});
+
+test('specify test files manually', async t => {
+ const diagnostics = await tsd({
+ cwd: path.join(__dirname, 'fixtures/specify-test-files'),
+ testFiles: [
+ 'unknown.test.ts'
+ ]
+ });
+
+ verify(t, diagnostics, [
+ [5, 19, 'error', 'Argument of type \'number\' is not assignable to parameter of type \'string\'.']
+ ]);
+});
+
+test('fails if typings file is not found in the specified path', async t => {
+ const error = await t.throwsAsync(tsd({
+ cwd: path.join(__dirname, 'fixtures/typings-custom-dir'),
+ typingsFile: 'unknown.d.ts'
+ }));
+
+ t.is(error.message, 'The type definition `unknown.d.ts` does not exist. Create one and try again.');
+});
+
+test('checking testCount', async t => {
+ const diagnostics = await tsd({cwd: path.join(__dirname, 'fixtures/failure')});
+
+ t.is(diagnostics.testCount, 2);
+});