diff --git a/test-d/assertions-as-type-guards.cts b/test-d/assertions-as-type-guards.cts new file mode 100644 index 0000000000..45f2518a56 --- /dev/null +++ b/test-d/assertions-as-type-guards.cts @@ -0,0 +1,40 @@ +import test from 'ava'; +import {expectType} from 'tsd'; + +type Expected = {foo: 'bar'}; +const expected: Expected = {foo: 'bar'}; + +test('deepEqual', t => { + const actual: unknown = {}; + if (t.deepEqual(actual, expected)) { + expectType(actual); + } +}); + +test('like', t => { + const actual: unknown = {}; + if (t.like(actual, expected)) { + expectType(actual); + } +}); + +test('is', t => { + const actual: unknown = 2; + if (t.is(actual, 3 as const)) { + expectType<3>(actual); + } +}); + +test('false', t => { + const actual: unknown = true; + if (t.false(actual)) { + expectType(actual); + } +}); + +test('true', t => { + const actual: unknown = false; + if (t.true(actual)) { + expectType(actual); + } +}); diff --git a/test-d/context.cts b/test-d/context.cts new file mode 100644 index 0000000000..f2901be885 --- /dev/null +++ b/test-d/context.cts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import anyTest, {ExecutionContext, TestFn} from 'ava'; +import {expectError, expectType} from 'tsd'; + +interface Context { + foo: string; +} + +const test = anyTest as TestFn; + +const macro = test.macro((t, _expected: number) => { + expectType(t.context.foo); +}); + +test.beforeEach(t => { + expectType(t.context); +}); + +// @ts-expect-error TS2769 +expectError(test('foo is bar', macro, 'bar')); // eslint-disable-line @typescript-eslint/no-confusing-void-expression + +anyTest('default context is unknown', t => { + expectType(t.context); +}); + +// See https://github.com/avajs/ava/issues/2253 +interface Covariant extends Context { + bar: number; +} + +const test2 = anyTest as TestFn; +const hook = (_t: ExecutionContext) => {}; +test2.beforeEach(hook); diff --git a/test-d/deep-equal.cts b/test-d/deep-equal.cts new file mode 100644 index 0000000000..acb850262f --- /dev/null +++ b/test-d/deep-equal.cts @@ -0,0 +1,29 @@ +import test from 'ava'; +import {expectType} from 'tsd'; + +test('actual extends expected', t => { + type Expected = {foo: [1, 2, 3]}; + const expected: Expected = {foo: [1, 2, 3]}; + const actual = {foo: [1, 2, 3]}; + if (t.deepEqual(actual, expected)) { + expectType(actual); + } +}); + +test('expected extends actual', t => { + type Expected = {foo: Array}; + type Actual = {foo: number[]}; + const expected: Expected = {foo: [1, 2, 3]}; + const actual: Actual = {foo: [1, 2, 3]}; + if (t.deepEqual(actual, expected)) { + expectType(expected); + } +}); + +test('neither extends the each other', t => { + type Expected = {readonly foo: readonly [1, 2, 3]}; + type Actual = {foo: number[]}; + const expected: Expected = {foo: [1, 2, 3]}; + const actual: Actual = {foo: [1, 2, 3]}; + t.deepEqual(actual, expected); +}); diff --git a/test-d/implementation-result.cts b/test-d/implementation-result.cts new file mode 100644 index 0000000000..ea3ccfcda6 --- /dev/null +++ b/test-d/implementation-result.cts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import test from 'ava'; + +test.after('return anything else', _t => ({ + foo: 'bar', + subscribe() {}, + then() {}, // eslint-disable-line unicorn/no-thenable +})); + +test('return a promise-like', _t => ({ + then(resolve) { // eslint-disable-line unicorn/no-thenable + resolve?.(); // eslint-disable-line @typescript-eslint/no-floating-promises + }, +})); + +test('return a subscribable', _t => ({ + subscribe({complete}) { + complete(); + }, +})); diff --git a/test-d/like.cts b/test-d/like.cts new file mode 100644 index 0000000000..ecc6410979 --- /dev/null +++ b/test-d/like.cts @@ -0,0 +1,25 @@ +import test from 'ava'; + +test('like', t => { + t.like({ + map: new Map([['foo', 'bar']]), + nested: { + baz: 'thud', + qux: 'quux', + }, + }, { + map: new Map([['foo', 'bar']]), + nested: { + baz: 'thud', + }, + }); + + type Foo = { + foo?: 'foo'; + bar?: 'bar'; + }; + + const foo: Foo = {bar: 'bar'}; + const {foo: _, ...expected} = foo; + t.like(expected, {bar: 'bar'}); +}); diff --git a/test-d/macros.cts b/test-d/macros.cts new file mode 100644 index 0000000000..f775ed7aab --- /dev/null +++ b/test-d/macros.cts @@ -0,0 +1,126 @@ +/* eslint-disable no-lone-blocks */ +import test, {ExecutionContext} from 'ava'; +import {expectType} from 'tsd'; + +// Typed arguments through generics. +{ + const hasLength = test.macro<[string, number]>((t, input, expected) => { + expectType(input); + expectType(expected); + // @ts-expect-error TS2345 + t.is(input, expected); + }); + + test('bar has length 3', hasLength, 'bar', 3); +} + +{ + const hasLength = test.macro<[string, number]>({ + exec(t, input, expected) { + expectType(input); + expectType(expected); + // @ts-expect-error TS2345 + t.is(input, expected); + }, + title(_providedTitle, input, expected) { + expectType(input); + expectType(expected); + return 'title'; + }, + }); + + test('bar has length 3', hasLength, 'bar', 3); +} + +// Typed arguments in execution function. +{ + const hasLength = test.macro((t, input: string, expected: number) => { + // @ts-expect-error TS2345 + t.is(input, expected); + }); + + test('bar has length 3', hasLength, 'bar', 3); +} + +{ + const hasLength = test.macro({ + exec(t, input: string, expected: number) { + // @ts-expect-error TS2345 + t.is(input, expected); + }, + title(_providedTitle, input, expected) { + expectType(input); + expectType(expected); + return 'title'; + }, + }); + + test('bar has length 3', hasLength, 'bar', 3); +} + +// Untyped arguments +{ + const hasLength = test.macro((t, input, expected) => { + expectType(input); + expectType(expected); + t.is(input, expected); + }); + + test('bar has length 3', hasLength, 'bar', 3); +} + +// Usable without title, even if the macro lacks a title function. +{ + const hasLength = test.macro<[string, number]>((t, input, expected) => { + // @ts-expect-error TS2345 + t.is(input, expected); + }); + + test(hasLength, 'bar', 3); +} + +// No arguments +{ + const pass = test.macro<[]>({ // eslint-disable-line @typescript-eslint/ban-types + exec(_t, ...args) { + expectType<[]>(args); // eslint-disable-line @typescript-eslint/ban-types + }, + title(providedTitle, ...args) { + expectType(providedTitle); + expectType<[]>(args); // eslint-disable-line @typescript-eslint/ban-types + return ''; + }, + }); + + test(pass); +} + +// Without test.macro() +{ + const hasLength = (t: ExecutionContext, input: string, expected: number) => { + t.is(input.length, expected); + }; + + test('bar has length 3', hasLength, 'bar', 3); +} + +// Inline function with explicit argument types. +test('has length 3', (t: ExecutionContext, input: string, expected: number) => { + // @ts-expect-error TS2345 + t.is(input, expected); +}, 'bar', 3); + +// Completely inferred arguments for inline functions. +test('has length 3', (t, input, expected) => { + expectType(input); + expectType(expected); + // @ts-expect-error TS2345 + t.is(input, expected); +}, 'foo', 3); + +test.skip('skip', (t, input, expected) => { + expectType(input); + expectType(expected); + // @ts-expect-error TS2345 + t.is(input, expected); +}, 'foo', 3); diff --git a/test-d/plugin.cts b/test-d/plugin.cts new file mode 100644 index 0000000000..f6ad0f19bc --- /dev/null +++ b/test-d/plugin.cts @@ -0,0 +1,18 @@ +import plugin from 'ava/plugin'; +import {expectType} from 'tsd'; + +expectType(plugin.registerSharedWorker({filename: '', supportedProtocols: ['ava-4']})); + +const factory: plugin.SharedWorker.Factory = ({negotiateProtocol}) => { + const protocol = negotiateProtocol(['ava-4']); + expectType(protocol); + + (async () => { + for await (const w of protocol.testWorkers()) { + expectType<() => Promise>(w.teardown(() => {})); // eslint-disable-line @typescript-eslint/no-empty-function + expectType<() => Promise>(w.teardown(async () => {})); // eslint-disable-line @typescript-eslint/no-empty-function + } + })(); +}; + +export default factory; diff --git a/test-d/snapshot.cts b/test-d/snapshot.cts new file mode 100644 index 0000000000..ab166b2532 --- /dev/null +++ b/test-d/snapshot.cts @@ -0,0 +1,16 @@ +import test from 'ava'; +import {expectError} from 'tsd'; + +test('snapshot', t => { + t.snapshot({foo: 'bar'}); + t.snapshot(null, 'a snapshot with a message'); + // @ts-expect-error TS2345 + expectError(t.snapshot('hello world', null)); // eslint-disable-line @typescript-eslint/no-confusing-void-expression +}); + +test('snapshot.skip', t => { + t.snapshot.skip({foo: 'bar'}); + t.snapshot.skip(null, 'a snapshot with a message'); + // @ts-expect-error TS2345 + expectError(t.snapshot.skip('hello world', null)); // eslint-disable-line @typescript-eslint/no-confusing-void-expression +}); diff --git a/test-d/teardown.cts b/test-d/teardown.cts new file mode 100644 index 0000000000..baaa4628e0 --- /dev/null +++ b/test-d/teardown.cts @@ -0,0 +1,6 @@ +import test from 'ava'; + +test('test', t => { + t.teardown(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function + t.teardown(async () => {}); // eslint-disable-line @typescript-eslint/no-empty-function +}); diff --git a/test-d/throws.cts b/test-d/throws.cts new file mode 100644 index 0000000000..126dd174ba --- /dev/null +++ b/test-d/throws.cts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import test from 'ava'; +import {expectType} from 'tsd'; + +class CustomError extends Error { + foo: string; + + constructor() { + super(); + this.foo = 'foo'; + } +} + +test('throws', t => { + expectType(t.throws(() => {})); + const error2: CustomError | undefined = t.throws(() => {}); + expectType(error2); + expectType(t.throws(() => {})); +}); + +test('throwsAsync', async t => { + expectType(await t.throwsAsync(async () => {})); + expectType(await t.throwsAsync(async () => {})); + expectType(await t.throwsAsync(Promise.reject())); + expectType(await t.throwsAsync(Promise.reject())); +}); diff --git a/test-d/try-commit.cts b/test-d/try-commit.cts new file mode 100644 index 0000000000..c207a050be --- /dev/null +++ b/test-d/try-commit.cts @@ -0,0 +1,67 @@ +import test, {ExecutionContext} from 'ava'; +import {expectType} from 'tsd'; + +test('attempt', async t => { + const attempt = await t.try( + (u, a, b) => { + expectType(u); + expectType(a); + expectType(b); + }, + 'string', + 6, + ); + attempt.commit(); +}); + +test('attempt with title', async t => { + const attempt = await t.try( + 'attempt title', + (u, a, b) => { + expectType(u); + expectType(a); + expectType(b); + }, + 'string', + 6, + ); + attempt.commit(); +}); + +const lengthCheck = (t: ExecutionContext, a: string, b: number): void => { + t.is(a.length, b); +}; + +test('attempt with helper', async t => { + const attempt = await t.try(lengthCheck, 'string', 6); + attempt.commit(); +}); + +test('attempt with title', async t => { + const attempt = await t.try('title', lengthCheck, 'string', 6); + attempt.commit(); +}); + +test('all possible variants to pass to t.try', async t => { + // No params + void t.try(tt => tt.pass()); + + void t.try('test', tt => tt.pass()); + + // Some params + void t.try((tt, a, b) => tt.is(a.length, b), 'hello', 5); + + void t.try('test', (tt, a, b) => tt.is(a.length, b), 'hello', 5); + + // Macro with title + const macro1 = test.macro<[string, number]>({ + exec: (tt, a, b) => tt.is(a.length, b), + title: (title, a, b) => `${title ? `${String(title)} ` : ''}str: "${String(a)}" with len: "${String(b)}"`, + }); + const macro2 = test.macro<[string, number]>((tt, a, b) => tt.is(a.slice(b), '')); + + void t.try(macro1, 'hello', 5); + void t.try(macro2, 'hello', 5); + void t.try('title', macro1, 'hello', 5); + void t.try('title', macro2, 'hello', 5); +});