0

I'm using ava (no link, since I'm not allowed to use more than 2 ) for testing and want to type ava's test context. It's typed as any in ava's definition file.

What I specifically want is that the typescript compiler knows that t.context is of the type {foo: number} in the following test:

import test from 'ava'

test.beforeEach((t) => {
  t.context = { foo: 5 }
})

test('Is context typed', (t) => {
  // uncaught typo
  t.is(t.context.fooo, 5)
})

I tried to use declaration merging to do this, but it fails with TS2403: Subsequent variable declarations must have the same type. Variable 'context' must be of type 'any', but here has type '{ foo: number; }'. :

declare module 'ava' {
    interface ContextualTestContext {
      context: {
        foo: number,
      }
    }
}

test.beforeEach((t) => {
  t.context = { foo: 5 }
})

test('Is context typed', (t) => {
  // uncaught ypo
  t.is(t.context.fooo, 5)
})

Is there a way to do this without casting the context all the time like so:

interface IMyContext {
  foo: number
}

test.beforeEach((t) => {
  t.context = { foo: 5 }
})

test('Is context typed', (t) => {
  const context = <IMyContext> t.context
  // caught typo
  t.is(context.fooo, 5)
})
despairblue
  • 347
  • 2
  • 8

2 Answers2

0

There is no general way of doing this. In your special case, you could create a new TestContext, e.g. instead of

export type ContextualTest = (t: ContextualTestContext) => PromiseLike<void> | Iterator<any> | Observable | void;

use something like

export type MyContextualTest<T> = (t : TestContext & {context : T}) => PromiseLike<void> ...

and declare your own test function, which should be compatible to Ava's like this:

interface MyTestFunction<T> {
    (name : string, run : MyContextualTest<T>)
}

import {test as avaTest} from 'ava';
const test : MyTestFunction<IMyContext> = avaTest;

This is mostly untested, so if there are some problems, let me know.

Finn Poppinga
  • 269
  • 4
  • 11
  • 1
    @despairblue could you raise an issue for your use case? Perhaps we can accept a generic in the `test()` signature. – Mark Wubben Feb 24 '17 at 10:25
  • @FinnO that works technically, but if I want to use the functions defined on the `test` function itself (like `test.beforeEach`) I would have to redefine all declarations. There are roughly 1700 of them. – despairblue Feb 28 '17 at 16:54
0

Typing the context will be possible with the next version of ava. Then you can do something like this:

import * as ava from 'ava';

function contextualize<T>(getContext: () => T): ava.RegisterContextual<T> {
    ava.test.beforeEach(t => {
        Object.assign(t.context, getContext());
    });

    return ava.test;
}

const test = contextualize(() => {
    return { foo: 'bar' };
});

test.beforeEach(t => {
    t.context.foo = 123; // error:  Type '123' is not assignable to type 'string'
});

test.after.always.failing.cb.serial('very long chains are properly typed', t => {
    t.context.fooo = 'a value'; // error: Property 'fooo' does not exist on type '{ foo: string }'
});

test('an actual test', t => {
    t.deepEqual(t.context.foo.map(c => c), ['b', 'a', 'r']); // error: Property 'map' does not exist on type 'string'
});

If you acquire your context asynchronously you need to change the type signature of contextualize accordingly:

function contextualize<T>(getContext: () => Promise<T>): ava.RegisterContextual<T> {
    ava.test.beforeEach(async t => {
        Object.assign(t.context, await getContext());
    });

    return ava.test;
}

const test = contextualize(() => {
    const db = await mongodb.MongoClient.connect('mongodb://localhost:27017')

    return { db }
});

Otherwise the TypeScript compiler will think t.context is a Promise, although it isn't

despairblue
  • 347
  • 2
  • 8