@call decorator for callable objects?

I notice that vitest allows you to extend your test function with skipIf, only, each, etc:

import { assert, test } from 'vitest'

const isDev = process.env.NODE_ENV === 'development'

test.skipIf(isDev)('prod only test', () => {
  // this test only runs in production
})

doing bindings for this i would expect test to be an opaque type, but then I’m looking for a way to make that object callable…I suppose i can use %raw but how would you do it?

	type test
	@module("vitest") external t: test = "test"
        @send external skipIf: (test, bool) => test = "skipIf"
        ....
        let call: (test, string, () => unit) => unit = %raw(`(a, b, c) => a(b, c)`)

Why don’t you just type test as a function?

skipIf doesnt change the signature of the function but other examples do: extend/each

and not all functions of that signature will have the properties to do the decoration either.

This is what I did in rescript-bun - a regular describe function, and a Describe module with skipIf: https://github.com/zth/rescript-bun/blob/main/src/Test.res#L53-L105

describe("whatever", () => { ... })

vs

Describe.skipIf(true, () => { ... })

Makes it pretty easy to work with, just capitalize describe and add skipIf. Not as discoverable as describe.skipIf would be though of course.

1 Like

Ah Yeah, i have that in my current version.
In the latest vitest it looks like these can change the signature of the called function:

test.each([
  { a: 1, b: 1, expected: 2 },
  { a: 1, b: 2, expected: 3 },
  { a: 2, b: 1, expected: 3 },
])('add($a, $b) -> $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected)
})

and maybe theyre composable?

test.each([
  { a: 1, b: 1, expected: 2 },
  { a: 1, b: 2, expected: 3 },
  { a: 2, b: 1, expected: 3 },
]).skipIf(isDev)('add($a, $b) -> $expected', ({ a, b, expected }) => {
  expect(a + b).toBe(expected)
})

I would still say that you can enforce an order, you can say that you should always use skipIf first and not after the functions that change the signature, like each. You’d have to define different each functions depending on the number of elements it has anyway.

I would definitely model it like this playground link:

type test = (string, unit => unit) => unit
@module("vitest") external test: test = "test"
@send external skipIf: (test, bool) => test = "skipIf"
@send
external each3: (
  test,
  array<('a, 'b, 'c)>,
) => (string, ('a, 'b, 'c) => unit) => unit = "each"

type expect<'a>
@module("vitest") external expect: 'a => expect<'a> = "expect"
@send external toBe: (expect<'a>, 'a) => unit = "toBe"

test("1+1 = 2", () => expect(1 + 1)->toBe(2))
skipIf(test, isDev)("1+1 = 2", () => expect(1 + 1)->toBe(2))
each3(test, [(1, 2, 3), (3, 4, 7)])("add(%i, %i) -> %i", (a, b, c) =>
  expect(a + b)->toBe(c)
)

The only issue I could find comes from the fact there seems to be a bug with the fast pipe and uncurried mode, so you wouldn’t be able to pipe test to each3 or skipIf, which is definitely unfortunate, but hopefully can be fixed.