Last active
February 9, 2024 23:53
-
-
Save nberlette/216c2f335656aa5db5534f8e3ddcd1e3 to your computer and use it in GitHub Desktop.
Hard fork of the Deno stdlib 'expect' module + BDD testing tools (from https://deno.land/std/expect/mod.ts)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. | |
// Copyright 2019 Allain Lalonde. All rights reserved. ISC License. | |
import { | |
type AnyConstructor, | |
type Matcher, | |
matchers, | |
type TypeNames, | |
} from "./matchers.ts"; | |
import { AssertionError, type Async, type Fn, isPromiseLike } from "./utils.ts"; | |
export interface Expected { | |
not: Omit<this, "not">; | |
resolves: Async<Omit<this, "resolves" | "rejects">>; | |
rejects: Async<Omit<this, "resolves" | "rejects">>; | |
lastCalledWith<const A extends readonly unknown[]>(...expected: A): void; | |
lastReturnedWith<const T>(expected: T): void; | |
nthCalledWith<const A extends readonly unknown[]>( | |
nth: number, | |
...expected: A | |
): void; | |
nthReturnedWith<const T>(nth: number, expected: T): void; | |
toBeCalled(): void; | |
toBeCalledTimes(expected: number): void; | |
toBeCalledWith<const A extends readonly unknown[]>( | |
...expected: A | |
): void; | |
toBeCloseTo(candidate: number, tolerance?: number): void; | |
toBeDefined(): void; | |
toBeFalsy(): void; | |
toBeGreaterThan(expected: number): void; | |
toBeGreaterThanOrEqual(expected: number): void; | |
toBeInstanceOf<T extends AnyConstructor>(expected: T): void; | |
toBeLessThan(expected: number): void; | |
toBeLessThanOrEqual(expected: number): void; | |
toBeNaN(): void; | |
toBeNull(): void; | |
toBeTruthy(): void; | |
toBeUndefined(): void; | |
toBe: | |
& { | |
<const T>(expected: T): void; | |
truthy(): void; | |
falsy(): void; | |
null(): void; | |
undefined(): void; | |
defined(): void; | |
NaN(): void; | |
type<T extends TypeNames>(expected: T): void; | |
instanceof<T extends AnyConstructor>(expected: T): void; | |
instanceOf<T extends AnyConstructor>(expected: T): void; | |
closeTo(candidate: number, tolerance?: number): void; | |
greaterThan(expected: number): void; | |
greaterThanOrEqual(expected: number): void; | |
lessThan(expected: number): void; | |
lessThanOrEqual(expected: number): void; | |
iterable(): void; | |
asyncIterable(): void; | |
} | |
& { | |
[K in "a" | "an"]: { | |
<const T>(expected: T): void; | |
function(): void; | |
asyncFunction(): void; | |
generatorFunction(): void; | |
generator(): void; | |
asyncGeneratorFunction(): void; | |
asyncGenerator(): void; | |
iterable(): void; | |
asyncIterable(): void; | |
array(): void; | |
object(): void; | |
string(): void; | |
number(): void; | |
bigint(): void; | |
boolean(): void; | |
symbol(): void; | |
}; | |
}; | |
toBeFunction(): void; | |
toBeAsyncFunction(): void; | |
toBeGeneratorFunction(): void; | |
toBeGeneratorObject(): void; | |
toBeAsyncGeneratorFunction(): void; | |
toBeAsyncGeneratorObject(): void; | |
toBeIterable(): void; | |
toBeAsyncIterable(): void; | |
toBeType<T extends TypeNames>(expected: T): void; | |
toContainEqual<const T>(expected: T): void; | |
toContain<const T>(expected: T): void; | |
toEqual<const T>(expected: T): void; | |
toHaveBeenCalledTimes(expected: number): void; | |
toHaveBeenCalledWith< | |
const A extends readonly unknown[], | |
>(...expected: A): void; | |
toHaveBeenCalled(): void; | |
toHaveBeenLastCalledWith< | |
const A extends readonly unknown[], | |
>(...expected: A): void; | |
toHaveBeenNthCalledWith(nth: number, ...expected: unknown[]): void; | |
toHaveLength(expected: number): void; | |
toHaveLastReturnedWith<const T>(expected: T): void; | |
toHaveNthReturnedWith<const T>(nth: number, expected: T): void; | |
toHaveProperty<const T, const K extends PropertyKey | readonly PropertyKey[]>( | |
propName: K, | |
value?: T, | |
): void; | |
toHaveReturnedTimes(expected: number): void; | |
toHaveReturnedWith<const T>(expected: T): void; | |
toHaveReturned(): void; | |
toMatch(expected: RegExp): void; | |
toMatchObject<const T extends Record<PropertyKey, unknown>>( | |
expected: T, | |
): void; | |
toReturn(): void; | |
toReturnTimes(expected: number): void; | |
toReturnWith(expected: unknown): void; | |
toStrictEqual(candidate: unknown): void; | |
toThrow<E extends Error = Error>( | |
// deno-lint-ignore no-explicit-any | |
expected?: string | RegExp | E | (new (...args: any[]) => E), | |
): void; | |
} | |
type MatcherKey = keyof Omit<Expected, "not" | "resolves" | "rejects">; | |
export function expect<const T>(value: T, customMessage?: string): Expected; | |
export function expect(value: unknown, customMessage?: string): Expected { | |
let isNot = false; | |
let isPromised = false; | |
const self: Expected = new Proxy<Expected>( | |
<Expected> {}, | |
{ | |
get(_, name) { | |
if (name === "not") { | |
isNot = !isNot; | |
return self; | |
} | |
if (name === "resolves") { | |
if (!isPromiseLike(value)) { | |
throw new AssertionError("expected value must be Promiselike"); | |
} | |
isPromised = true; | |
return self; | |
} | |
if (name === "rejects") { | |
if (!isPromiseLike(value)) { | |
throw new AssertionError("expected value must be a PromiseLike"); | |
} | |
value = value.then( | |
(value) => { | |
throw new AssertionError( | |
`Promise did not reject. resolved to ${value}`, | |
); | |
}, | |
(err) => err, | |
); | |
isPromised = true; | |
return self; | |
} | |
const matcher: Matcher = matchers[name as MatcherKey]; | |
if (!matcher) { | |
throw new TypeError( | |
typeof name === "string" | |
? `matcher not found: ${name}` | |
: "matcher not found", | |
); | |
} | |
function applyMatcher(value: unknown, args: unknown[]) { | |
runMatcher(matcher, value, args); | |
} | |
function runMatcher(fn: Matcher, value: unknown, args: unknown[]) { | |
const context = { value, isNot, customMessage }; | |
fn(context, ...args); | |
} | |
if (name === "toBe") { | |
return new Proxy(matcher as typeof matchers.toBe, { | |
apply(target, _thisArg, args) { | |
if (args.length !== 1) { | |
throw new TypeError( | |
`toBe expects exactly one argument, ${args.length} given`, | |
); | |
} | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(target, v, args) | |
); | |
} | |
return runMatcher(target, value, args); | |
}, | |
get(target, p) { | |
if (typeof p === "string") { | |
switch (p) { | |
case "truthy": //fallthrough | |
case "falsy": //fallthrough | |
case "null": //fallthrough | |
case "undefined": //fallthrough | |
case "defined": //fallthrough | |
case "NaN": { | |
return () => { | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(Reflect.get(target, p), v, []) | |
); | |
} | |
return runMatcher(Reflect.get(target, p), value, []); | |
}; | |
} | |
case "instanceof": //fallthrough | |
case "instanceOf": { | |
return (expected: unknown) => { | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(Reflect.get(target, p), v, [expected]) | |
); | |
} | |
return runMatcher(Reflect.get(target, p), value, [ | |
expected, | |
]); | |
}; | |
} | |
case "type": { | |
return (expected: TypeNames) => { | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(Reflect.get(target, p), v, [expected]) | |
); | |
} | |
return runMatcher(Reflect.get(target, p), value, [ | |
expected, | |
]); | |
}; | |
} | |
case "closeTo": { | |
return (expected: number, tolerance?: number) => { | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(Reflect.get(target, p), v, [ | |
expected, | |
tolerance, | |
]) | |
); | |
} | |
return runMatcher(Reflect.get(target, p), value, [ | |
expected, | |
tolerance, | |
]); | |
}; | |
} | |
case "greaterThan": //fallthrough | |
case "greaterThanOrEqual": //fallthrough | |
case "lessThan": //fallthrough | |
case "lessThanOrEqual": { | |
return (expected: number) => { | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(Reflect.get(target, p), v, [expected]) | |
); | |
} | |
return runMatcher(Reflect.get(target, p), value, [ | |
expected, | |
]); | |
}; | |
} | |
case "iterable": //fallthrough | |
case "asyncIterable": { | |
return () => { | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(Reflect.get(target, p), v, []) | |
); | |
} | |
return runMatcher(Reflect.get(target, p), value, []); | |
}; | |
} | |
case "a": //fallthrough | |
case "an": { | |
return new Proxy(target.a, { | |
apply(target, _thisArg, args) { | |
if (args.length !== 1) { | |
throw new TypeError( | |
`${p} expects exactly one argument, ${args.length} given`, | |
); | |
} | |
if (isPromised) { | |
return (value as Promise<unknown>).then((v) => | |
runMatcher(target, v, args) | |
); | |
} | |
return runMatcher(target, value, args); | |
}, | |
get(target, p) { | |
if (typeof p === "string") { | |
switch (p) { | |
case "function": //fallthrough | |
case "asyncFunction": //fallthrough | |
case "generatorFunction": //fallthrough | |
case "generator": //fallthrough | |
case "asyncGeneratorFunction": //fallthrough | |
case "asyncGenerator": //fallthrough | |
case "array": //fallthrough | |
case "object": //fallthrough | |
case "string": //fallthrough | |
case "number": //fallthrough | |
case "bigint": //fallthrough | |
case "boolean": //fallthrough | |
case "symbol": //fallthrough | |
case "null": //fallthrough | |
case "undefined": //fallthrough | |
case "instanceOf": //fallthrough | |
case "instanceof": //fallthrough | |
case "NaN": { | |
return (...args: unknown[]) => { | |
if (args.length !== 0) { | |
throw new TypeError( | |
`${p} expects zero arguments, ${args.length} given`, | |
); | |
} | |
if (isPromised) { | |
return (value as Promise<unknown>).then( | |
(v) => | |
runMatcher( | |
Reflect.get(target, p), | |
v, | |
args, | |
), | |
); | |
} | |
return runMatcher( | |
Reflect.get(target, p), | |
value, | |
args, | |
); | |
}; | |
} | |
} | |
} | |
return Reflect.get(target, p); | |
}, | |
}); | |
} | |
} | |
} | |
}, | |
}); | |
} | |
return (...args: unknown[]) => | |
isPromised | |
? (value as Promise<unknown>).then((v) => applyMatcher(v, args)) | |
: applyMatcher(value, args); | |
}, | |
ownKeys() { | |
return Reflect.ownKeys(matchers); | |
}, | |
getOwnPropertyDescriptor(_, p) { | |
return Reflect.getOwnPropertyDescriptor(matchers, p); | |
}, | |
getPrototypeOf: () => | |
Object.create(matchers, { | |
[Symbol.toStringTag]: { value: "Expected" }, | |
}), | |
setPrototypeOf: () => false, // dummy | |
isExtensible: () => true, // dummy | |
preventExtensions: () => true, // dummy | |
}, | |
); | |
return self; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. | |
// deno-lint-ignore-file no-explicit-any | |
import { | |
type AnyConstructor, | |
assertEquals, | |
assertInstanceOf, | |
AssertionError, | |
assertIsError, | |
assertMatch, | |
assertNotEquals, | |
assertNotInstanceOf, | |
assertNotMatch, | |
assertNotStrictEquals, | |
assertObjectMatch, | |
assertStrictEquals, | |
AsyncFunction, | |
AsyncGenerator, | |
AsyncGeneratorFunction, | |
equal, | |
filterUndefined, | |
format, | |
Generator, | |
GeneratorFunction, | |
inspectArg, | |
inspectArgs, | |
} from "./utils.ts"; | |
import { getMockCalls } from "./mock.ts"; | |
import { assertArrayIncludes } from "./utils.ts"; | |
export interface UntypedMatcherContext { | |
value: unknown; | |
isNot: boolean; | |
customMessage?: string | undefined; | |
} | |
export interface MatcherContext<T = unknown, N extends boolean = boolean> | |
extends UntypedMatcherContext { | |
value: T; | |
isNot: N; | |
customMessage?: string | undefined; | |
} | |
export interface Matcher< | |
T = unknown, | |
U = unknown, | |
N extends boolean = boolean, | |
A extends readonly unknown[] = readonly unknown[], | |
> { | |
( | |
this: MatcherContext<U, N>, | |
context: MatcherContext<U, N>, | |
...args: A | |
): asserts context is RetypeContext<T, U, N>; | |
( | |
this: MatcherContext<T | U, N> | void, | |
context: MatcherContext<T | U, N>, | |
...args: A | |
): asserts context is RetypeContext<T, U, N>; | |
( | |
this: MatcherContext<U, N>, | |
context: MatcherContext<U, N>, | |
...args: A | |
): void | Promise<void>; | |
(context: MatcherContext<T | U, N>): context is RetypeContext<T, U, N>; | |
(context: MatcherContext<T, N>, ...args: A): void | Promise<void>; | |
} | |
export interface Matchers { | |
[key: string]: Matcher; | |
} | |
export type MatchResult<T = void> = T | Promise<T> | boolean; | |
type RetypeContext<T, U, N extends boolean = boolean> = `${N}` extends | |
infer S extends string ? "true" extends S ? MatcherContext<Exclude<U, T>, N> | |
: MatcherContext<Extract<U, T>, N> | |
: MatcherContext<T & U, N>; | |
export function toBe<T, N extends boolean, const A>( | |
context: MatcherContext<unknown, N>, | |
expect: T, | |
): asserts context is RetypeContext<T, unknown, N> { | |
if (context.isNot) { | |
assertNotStrictEquals(context.value, expect, context.customMessage); | |
} else { | |
assertStrictEquals(context.value, expect, context.customMessage); | |
} | |
} | |
toBe.truthy = toBeTruthy; | |
toBe.falsy = toBeFalsy; | |
toBe.null = toBeNull; | |
toBe.undefined = toBeUndefined; | |
toBe.defined = toBeDefined; | |
toBe.NaN = toBeNaN; | |
toBe.instanceOf = toBe.instanceof = toBeInstanceOf; | |
toBe.a = toBe.an = Object.assign( | |
((...args: Parameters<typeof toBeType>) => | |
toBeType(...args)) as typeof toBeType, | |
{ | |
function: toBeFunction, | |
asyncFunction: toBeAsyncFunction, | |
object: toBeObject, | |
string: toBeString, | |
number: toBeNumber, | |
bigint: toBeBigInt, | |
boolean: toBeBoolean, | |
symbol: toBeSymbol, | |
generator: toBeGeneratorObject, | |
generatorFunction: toBeGeneratorFunction, | |
asyncGenerator: toBeAsyncGeneratorObject, | |
asyncGeneratorFunction: toBeAsyncGeneratorFunction, | |
iterable: toBeIterable, | |
asyncIterable: toBeAsyncIterable, | |
array: toBeArray, | |
}, | |
); | |
toBe.type = toBeType; | |
toBe.closeTo = toBeCloseTo; | |
toBe.greaterThanOrEqual = toBeGreaterThanOrEqual; | |
toBe.greaterThan = toBeGreaterThan; | |
toBe.lessThanOrEqual = toBeLessThanOrEqual; | |
toBe.lessThan = toBeLessThan; | |
toBe.iterable = toBeIterable; | |
toBe.asyncIterable = toBeAsyncIterable; | |
toBe.array = toBeArray; | |
export function toBeGeneratorObject( | |
context: MatcherContext, | |
): MatchResult { | |
assertStrictEquals( | |
Object.prototype.isPrototypeOf.call( | |
Generator.prototype, | |
Object(context.value), | |
), | |
!context.isNot, | |
context.customMessage ?? | |
`Expected value ${context.isNot ? "not " : ""}to be a generator object`, | |
); | |
} | |
export function toBeGeneratorFunction( | |
context: MatcherContext, | |
): MatchResult { | |
if (context.isNot) { | |
assertNotInstanceOf( | |
context.value, | |
GeneratorFunction, | |
context.customMessage, | |
); | |
} else { | |
assertInstanceOf(context.value, GeneratorFunction, context.customMessage); | |
} | |
} | |
export function toBeAsyncGeneratorObject( | |
context: MatcherContext, | |
): MatchResult { | |
assertStrictEquals( | |
Object.prototype.isPrototypeOf.call( | |
AsyncGenerator.prototype, | |
Object(context.value), | |
), | |
!context.isNot, | |
context.customMessage ?? | |
`Expected value ${ | |
context.isNot ? "not " : "" | |
}to be an async generator object`, | |
); | |
} | |
export function toBeAsyncGeneratorFunction( | |
context: MatcherContext, | |
): MatchResult { | |
if (context.isNot) { | |
assertNotInstanceOf( | |
context.value, | |
AsyncGeneratorFunction, | |
context.customMessage, | |
); | |
} else { | |
assertInstanceOf( | |
context.value, | |
AsyncGeneratorFunction, | |
context.customMessage, | |
); | |
} | |
} | |
export function toBeIterable<T>( | |
context: MatcherContext, | |
): MatchResult<Iterable<T>> { | |
if (context.isNot) { | |
assertNotStrictEquals( | |
typeof (context.value as Iterable<unknown>)?.[Symbol.iterator], | |
"function", | |
context.customMessage ?? "Expected value to not be iterable", | |
); | |
} else { | |
assertStrictEquals( | |
typeof (context.value as Iterable<unknown>)?.[Symbol.iterator], | |
"function", | |
context.customMessage ?? "Expected value to be iterable", | |
); | |
} | |
return undefined!; | |
} | |
export function toBeIterator<T>( | |
context: MatcherContext, | |
): MatchResult<Iterator<T>> { | |
if (context.isNot) { | |
assertNotStrictEquals( | |
typeof (context.value as Iterable<unknown>)?.[Symbol.iterator], | |
"function", | |
context.customMessage ?? "Expected value to not be iterable", | |
); | |
} else { | |
assertStrictEquals( | |
typeof (context.value as Iterable<unknown>)?.[Symbol.iterator], | |
"function", | |
context.customMessage ?? "Expected value to be iterable", | |
); | |
} | |
return undefined!; | |
} | |
export function toBeAsyncIterable< | |
T = unknown, | |
U = unknown, | |
N extends boolean = boolean, | |
>( | |
context: MatcherContext<U, N>, | |
): asserts context is MatcherContext< | |
[`${N}`] extends ["true"] ? Exclude<U, AsyncIterable<T>> | |
: Extract<U, AsyncIterable<T>>, | |
N | |
> { | |
if (context.isNot) { | |
assertNotStrictEquals( | |
typeof (context.value as AsyncIterable<unknown>)?.[Symbol.asyncIterator], | |
"function", | |
context.customMessage ?? | |
"Expected value to not be asynchronously iterable", | |
); | |
} else { | |
assertStrictEquals( | |
typeof (context.value as AsyncIterable<unknown>)?.[Symbol.asyncIterator], | |
"function", | |
context.customMessage ?? "Expected value to be asynchronously iterable", | |
); | |
} | |
} | |
export function toBeAsyncIterator< | |
T = unknown, | |
U = unknown, | |
N extends boolean = boolean, | |
>( | |
context: MatcherContext<U, N>, | |
): asserts context is MatcherContext< | |
[`${N}`] extends ["true"] ? Exclude<U, AsyncIterator<T>> | |
: Extract<U, AsyncIterator<T>>, | |
N | |
> { | |
if (context.isNot) { | |
assertNotStrictEquals( | |
typeof (context.value as AsyncIterable<unknown>)?.[Symbol.asyncIterator], | |
"function", | |
context.customMessage ?? | |
"Expected value to not be asynchronously iterable", | |
); | |
} else { | |
assertStrictEquals( | |
typeof (context.value as AsyncIterable<unknown>)?.[Symbol.asyncIterator], | |
"function", | |
context.customMessage ?? "Expected value to be asynchronously iterable", | |
); | |
} | |
} | |
export function toBeArray(context: MatcherContext): MatchResult { | |
if (context.isNot) { | |
assertNotInstanceOf(context.value, Array, context.customMessage); | |
assertNotStrictEquals( | |
Array.isArray(context.value), | |
true, | |
context.customMessage, | |
); | |
} else { | |
assertInstanceOf(context.value, Array, context.customMessage); | |
assertStrictEquals( | |
Array.isArray(context.value), | |
true, | |
context.customMessage, | |
); | |
} | |
} | |
export function toEqual<const T>( | |
context: MatcherContext, | |
expected: T, | |
): MatchResult { | |
const v = filterUndefined(context.value); | |
const e = filterUndefined(expected); | |
if (context.isNot) { | |
assertNotEquals(v, e, context.customMessage); | |
} else { | |
assertEquals(v, e, context.customMessage); | |
} | |
} | |
export function toStrictEqual( | |
context: MatcherContext, | |
expected: unknown, | |
): MatchResult { | |
if (context.isNot) { | |
assertNotEquals(context.value, expected, context.customMessage); | |
} else { | |
assertEquals(context.value, expected, context.customMessage); | |
} | |
} | |
export function toBeCloseTo( | |
context: MatcherContext, | |
expected: number, | |
numDigits = 2, | |
): MatchResult { | |
if (numDigits < 0) { | |
throw new Error( | |
"toBeCloseTo second argument must be a non-negative integer. Got " + | |
numDigits, | |
); | |
} | |
const tolerance = 0.5 * Math.pow(10, -numDigits); | |
const value = Number(context.value); | |
const pass = Math.abs(expected - value) < tolerance; | |
if (context.isNot) { | |
if (pass) { | |
throw new AssertionError( | |
`Expected the value not to be close to ${expected} (using ${numDigits} digits), but it is`, | |
); | |
} | |
} else { | |
if (!pass) { | |
throw new AssertionError( | |
`Expected the value (${value} to be close to ${expected} (using ${numDigits} digits), but it is not`, | |
); | |
} | |
} | |
} | |
export function toBeDefined(context: MatcherContext): MatchResult { | |
if (context.isNot) { | |
assertStrictEquals(context.value, undefined, context.customMessage); | |
} else { | |
assertNotStrictEquals(context.value, undefined, context.customMessage); | |
} | |
} | |
export function toBeUndefined(context: MatcherContext): MatchResult { | |
if (context.isNot) { | |
assertNotStrictEquals( | |
context.value, | |
undefined, | |
context.customMessage, | |
); | |
} else { | |
assertStrictEquals(context.value, undefined, context.customMessage); | |
} | |
} | |
export function toBeFunction(context: MatcherContext): MatchResult { | |
if (context.isNot) { | |
assertNotStrictEquals( | |
typeof context.value, | |
"function", | |
context.customMessage, | |
); | |
} else { | |
assertStrictEquals(typeof context.value, "function", context.customMessage); | |
} | |
} | |
export function toBeAsyncFunction(context: MatcherContext): MatchResult { | |
if (context.isNot) { | |
assertNotInstanceOf(context.value, AsyncFunction, context.customMessage); | |
} else { | |
assertInstanceOf(context.value, AsyncFunction, context.customMessage); | |
} | |
} | |
interface TypeMap { | |
string: string; | |
number: number; | |
bigint: bigint; | |
boolean: boolean; | |
object: object; | |
// deno-lint-ignore ban-types | |
function: Function; | |
symbol: symbol; | |
null: null; | |
undefined: undefined; | |
} | |
export type TypeNames = keyof TypeMap & string; | |
export function toBeType<const K extends readonly TypeNames[]>( | |
context: MatcherContext, | |
...expectedTypes: [...K] | |
): MatchResult { | |
const actual = context.value === null ? "null" : typeof context.value; | |
const expected = expectedTypes.includes(actual as TypeNames); | |
if (context.isNot) { | |
if (expected) { | |
throw new AssertionError( | |
context.customMessage ??= `Expected value not to be of type ${ | |
expectedTypes.map((t, i, a) => | |
i === a.length - 1 ? `or '${t}'` : `'${t}'` | |
).join(", ") | |
}. Received type: '${actual}'`, | |
); | |
} | |
} else { | |
assertArrayIncludes(expectedTypes, [actual], context.customMessage); | |
} | |
} | |
export function toBeString(context: MatcherContext): MatchResult { | |
return toBeType(context, "string"); | |
} | |
export function toBeNumber(context: MatcherContext): MatchResult { | |
return toBeType(context, "number"); | |
} | |
export function toBeBigInt(context: MatcherContext): MatchResult { | |
return toBeType(context, "bigint"); | |
} | |
export function toBeBoolean(context: MatcherContext): MatchResult { | |
return toBeType(context, "boolean"); | |
} | |
export function toBeSymbol(context: MatcherContext): MatchResult { | |
return toBeType(context, "symbol"); | |
} | |
export function toBeObject(context: MatcherContext): MatchResult { | |
return toBeType(context, "object"); | |
} | |
export function toBeFalsy( | |
context: MatcherContext, | |
): MatchResult { | |
const isFalsy = !(context.value); | |
if (context.isNot) { | |
if (isFalsy) { | |
throw new AssertionError( | |
`Expected ${context.value} to NOT be falsy`, | |
); | |
} | |
} else { | |
if (!isFalsy) { | |
throw new AssertionError( | |
`Expected ${context.value} to be falsy`, | |
); | |
} | |
} | |
} | |
export function toBeTruthy( | |
context: MatcherContext, | |
): MatchResult { | |
const isTruthy = !!(context.value); | |
if (context.isNot) { | |
if (isTruthy) { | |
throw new AssertionError( | |
`Expected ${context.value} to NOT be truthy`, | |
); | |
} | |
} else { | |
if (!isTruthy) { | |
throw new AssertionError( | |
`Expected ${context.value} to be truthy`, | |
); | |
} | |
} | |
} | |
export function toBeGreaterThanOrEqual( | |
context: MatcherContext, | |
expected: number, | |
): MatchResult { | |
const isGreaterOrEqual = Number(context.value) >= Number(expected); | |
if (context.isNot) { | |
if (isGreaterOrEqual) { | |
throw new AssertionError( | |
`Expected ${context.value} to NOT be greater than or equal ${expected}`, | |
); | |
} | |
} else { | |
if (!isGreaterOrEqual) { | |
throw new AssertionError( | |
`Expected ${context.value} to be greater than or equal ${expected}`, | |
); | |
} | |
} | |
} | |
export function toBeGreaterThan( | |
context: MatcherContext, | |
expected: number, | |
): MatchResult { | |
const isGreater = Number(context.value) > Number(expected); | |
if (context.isNot) { | |
if (isGreater) { | |
throw new AssertionError( | |
`Expected ${context.value} to NOT be greater than ${expected}`, | |
); | |
} | |
} else { | |
if (!isGreater) { | |
throw new AssertionError( | |
`Expected ${context.value} to be greater than ${expected}`, | |
); | |
} | |
} | |
} | |
export function toBeLessThanOrEqual( | |
context: MatcherContext, | |
expected: number, | |
): MatchResult { | |
const isLower = Number(context.value) <= Number(expected); | |
if (context.isNot) { | |
if (isLower) { | |
throw new AssertionError( | |
`Expected ${context.value} to NOT be lower than or equal ${expected}`, | |
); | |
} | |
} else { | |
if (!isLower) { | |
throw new AssertionError( | |
`Expected ${context.value} to be lower than or equal ${expected}`, | |
); | |
} | |
} | |
} | |
export function toBeLessThan( | |
context: MatcherContext, | |
expected: number, | |
): MatchResult { | |
const isLower = Number(context.value) < Number(expected); | |
if (context.isNot) { | |
if (isLower) { | |
throw new AssertionError( | |
`Expected ${context.value} to NOT be lower than ${expected}`, | |
); | |
} | |
} else { | |
if (!isLower) { | |
throw new AssertionError( | |
`Expected ${context.value} to be lower than ${expected}`, | |
); | |
} | |
} | |
} | |
export function toBeNaN(context: MatcherContext): MatchResult { | |
if (context.isNot) { | |
assertNotEquals( | |
isNaN(Number(context.value)), | |
true, | |
context.customMessage || `Expected ${context.value} to not be NaN`, | |
); | |
} else { | |
assertEquals( | |
isNaN(Number(context.value)), | |
true, | |
context.customMessage || `Expected ${context.value} to be NaN`, | |
); | |
} | |
} | |
export function toBeNull(context: MatcherContext): MatchResult { | |
if (context.isNot) { | |
assertNotStrictEquals( | |
context.value as number, | |
null, | |
context.customMessage || `Expected ${context.value} to not be null`, | |
); | |
} else { | |
assertStrictEquals( | |
context.value as number, | |
null, | |
context.customMessage || `Expected ${context.value} to be null`, | |
); | |
} | |
} | |
export function toHaveLength( | |
context: MatcherContext, | |
expected: number, | |
): MatchResult { | |
const { value } = context; | |
// deno-lint-ignore no-explicit-any | |
const maybeLength = (value as any)?.length; | |
const hasLength = maybeLength === expected; | |
if (context.isNot) { | |
if (hasLength) { | |
throw new AssertionError( | |
`Expected value not to have length ${expected}, but it does`, | |
); | |
} | |
} else { | |
if (!hasLength) { | |
throw new AssertionError( | |
`Expected value to have length ${expected}, but it does not. (The value has length ${maybeLength})`, | |
); | |
} | |
} | |
} | |
export function toBeInstanceOf<T extends AnyConstructor>( | |
context: MatcherContext, | |
expected: T, | |
): MatchResult { | |
if (context.isNot) { | |
assertNotInstanceOf(context.value, expected); | |
} else { | |
assertInstanceOf(context.value, expected); | |
} | |
} | |
export function toHaveProperty( | |
context: MatcherContext, | |
propName: PropertyKey | PropertyKey[], | |
v?: unknown, | |
): MatchResult { | |
const { value } = context; | |
let propPath = [] as PropertyKey[]; | |
if (Array.isArray(propName)) { | |
propPath = propName; | |
} else if (typeof propName === "string") { | |
propPath = propName.split("."); | |
} else { | |
propPath = [propName]; | |
} | |
propPath = propPath.flatMap<PropertyKey>((prop) => { | |
if (typeof prop === "symbol") return prop; | |
const p = String(prop); | |
return p.split(/\.|\[(?=[^\]]*)\]/g).filter((p) => p); | |
}); | |
// deno-lint-ignore no-explicit-any | |
let current = value as any; | |
while (true) { | |
if (current === undefined || current === null) { | |
break; | |
} | |
if (propPath.length === 0) { | |
break; | |
} | |
const prop = propPath.shift()!; | |
current = current[prop]; | |
} | |
let hasProperty; | |
if (v) { | |
hasProperty = current !== undefined && propPath.length === 0 && | |
equal(current, v); | |
} else { | |
hasProperty = current !== undefined && propPath.length === 0; | |
} | |
let ofValue = ""; | |
if (v) { | |
ofValue = ` of the value ${inspectArg(v)}`; | |
} | |
const joined = propPath.reduce((acc, p) => { | |
if (!p) return acc.toString(); | |
if (typeof p === "symbol") { | |
return `${acc.toString()}[${String(p)}]`; | |
} | |
if (!isNaN(Number(p))) { | |
return `${acc.toString()}[${p}]`; | |
} | |
return `${acc ? acc.toString() + "." : ""}${p}`; | |
}, "") as string; | |
if (context.isNot) { | |
if (hasProperty) { | |
throw new AssertionError( | |
`Expected the value not to have the property ${joined}${ofValue}, but it does.`, | |
); | |
} | |
} else { | |
if (!hasProperty) { | |
throw new AssertionError( | |
`Expected the value to have the property ${joined}${ofValue}, but it does not.`, | |
); | |
} | |
} | |
} | |
export function toContain( | |
context: MatcherContext, | |
expected: unknown, | |
): MatchResult { | |
// deno-lint-ignore no-explicit-any | |
const doesContain = (context.value as any)?.includes?.(expected); | |
if (context.isNot) { | |
if (doesContain) { | |
throw new AssertionError("The value contains the expected item"); | |
} | |
} else { | |
if (!doesContain) { | |
throw new AssertionError("The value doesn't contain the expected item"); | |
} | |
} | |
} | |
export function toContainEqual( | |
context: MatcherContext, | |
expected: unknown, | |
): MatchResult { | |
const { value } = context; | |
assertIsIterable(value); | |
let doesContain = false; | |
for (const item of value) { | |
if (equal(item, expected)) { | |
doesContain = true; | |
break; | |
} | |
} | |
if (context.isNot) { | |
if (doesContain) { | |
throw new AssertionError("The value contains the expected item"); | |
} | |
} else { | |
if (!doesContain) { | |
throw new AssertionError("The value doesn't contain the expected item"); | |
} | |
} | |
} | |
// deno-lint-ignore no-explicit-any | |
function assertIsIterable(value: any): asserts value is Iterable<unknown> { | |
if (value == null) { | |
throw new AssertionError("The value is null or undefined"); | |
} | |
if (typeof value[Symbol.iterator] !== "function") { | |
throw new AssertionError("The value is not iterable"); | |
} | |
} | |
export function toMatch( | |
context: MatcherContext, | |
expected: RegExp, | |
): MatchResult { | |
if (context.isNot) { | |
assertNotMatch( | |
String(context.value), | |
expected, | |
context.customMessage, | |
); | |
} else { | |
assertMatch(String(context.value), expected, context.customMessage); | |
} | |
} | |
export function toMatchObject( | |
context: MatcherContext, | |
expected: Record<PropertyKey, unknown>, | |
): MatchResult { | |
if (context.isNot) { | |
let objectMatch = false; | |
try { | |
assertObjectMatch( | |
// deno-lint-ignore no-explicit-any | |
context.value as Record<PropertyKey, any>, | |
expected, | |
context.customMessage, | |
); | |
objectMatch = true; | |
const actualString = format(context.value); | |
const expectedString = format(expected); | |
throw new AssertionError( | |
`Expected ${actualString} to NOT match ${expectedString}`, | |
); | |
} catch (e) { | |
if (objectMatch) { | |
throw e; | |
} | |
return; | |
} | |
} else { | |
assertObjectMatch( | |
// deno-lint-ignore no-explicit-any | |
context.value as Record<PropertyKey, any>, | |
expected, | |
context.customMessage, | |
); | |
} | |
} | |
export function toHaveBeenCalled(context: MatcherContext): MatchResult { | |
const calls = getMockCalls(context.value); | |
const hasBeenCalled = calls.length > 0; | |
if (context.isNot) { | |
if (hasBeenCalled) { | |
throw new AssertionError( | |
`Expected mock function not to be called, but it was called ${calls.length} time(s)`, | |
); | |
} | |
} else { | |
if (!hasBeenCalled) { | |
throw new AssertionError( | |
`Expected mock function to be called, but it was not called`, | |
); | |
} | |
} | |
} | |
export function toHaveBeenCalledTimes( | |
context: MatcherContext, | |
expected: number, | |
): MatchResult { | |
const calls = getMockCalls(context.value); | |
if (context.isNot) { | |
if (calls.length === expected) { | |
throw new AssertionError( | |
`Expected mock function not to be called ${expected} time(s), but it was`, | |
); | |
} | |
} else { | |
if (calls.length !== expected) { | |
throw new AssertionError( | |
`Expected mock function to be called ${expected} time(s), but it was called ${calls.length} time(s)`, | |
); | |
} | |
} | |
} | |
export function toHaveBeenCalledWith( | |
context: MatcherContext, | |
...expected: unknown[] | |
): MatchResult { | |
const calls = getMockCalls(context.value); | |
const hasBeenCalled = calls.some((call) => equal(call.args, expected)); | |
if (context.isNot) { | |
if (hasBeenCalled) { | |
throw new AssertionError( | |
`Expected mock function not to be called with ${ | |
inspectArgs(expected) | |
}, but it was`, | |
); | |
} | |
} else { | |
if (!hasBeenCalled) { | |
let otherCalls = ""; | |
if (calls.length > 0) { | |
otherCalls = `\n Other calls:\n ${ | |
calls.map((call) => inspectArgs(call.args)).join("\n ") | |
}`; | |
} | |
throw new AssertionError( | |
`Expected mock function to be called with ${ | |
inspectArgs(expected) | |
}, but it was not.${otherCalls}`, | |
); | |
} | |
} | |
} | |
export function toHaveBeenLastCalledWith( | |
context: MatcherContext, | |
...expected: unknown[] | |
): MatchResult { | |
const calls = getMockCalls(context.value); | |
const hasBeenCalled = calls.length > 0 && | |
equal(calls.at(-1)?.args, expected); | |
if (context.isNot) { | |
if (hasBeenCalled) { | |
throw new AssertionError( | |
`Expected mock function not to be last called with ${ | |
inspectArgs(expected) | |
}, but it was`, | |
); | |
} | |
} else { | |
if (!hasBeenCalled) { | |
const lastCall = calls.at(-1); | |
if (!lastCall) { | |
throw new AssertionError( | |
`Expected mock function to be last called with ${ | |
inspectArgs(expected) | |
}, but it was not.`, | |
); | |
} else { | |
throw new AssertionError( | |
`Expected mock function to be last called with ${ | |
inspectArgs(expected) | |
}, but it was last called with ${inspectArgs(lastCall.args)}.`, | |
); | |
} | |
} | |
} | |
} | |
export function toHaveBeenNthCalledWith( | |
context: MatcherContext, | |
nth: number, | |
...expected: unknown[] | |
): MatchResult { | |
if (nth < 1) { | |
new Error(`nth must be greater than 0. ${nth} was given.`); | |
} | |
const calls = getMockCalls(context.value); | |
const callIndex = nth - 1; | |
const hasBeenCalled = calls.length > callIndex && | |
equal(calls[callIndex]?.args, expected); | |
if (context.isNot) { | |
if (hasBeenCalled) { | |
throw new AssertionError( | |
`Expected the n-th call (n=${nth}) of mock function is not with ${ | |
inspectArgs(expected) | |
}, but it was`, | |
); | |
} | |
} else { | |
if (!hasBeenCalled) { | |
const nthCall = calls[callIndex]; | |
if (!nthCall) { | |
throw new AssertionError( | |
`Expected the n-th call (n=${nth}) of mock function is with ${ | |
inspectArgs(expected) | |
}, but the n-th call does not exist.`, | |
); | |
} else { | |
throw new AssertionError( | |
`Expected the n-th call (n=${nth}) of mock function is with ${ | |
inspectArgs(expected) | |
}, but it was with ${inspectArgs(nthCall.args)}.`, | |
); | |
} | |
} | |
} | |
} | |
export function toHaveReturned(context: MatcherContext): MatchResult { | |
const calls = getMockCalls(context.value); | |
const returned = calls.filter((call) => call.returns); | |
if (context.isNot) { | |
if (returned.length > 0) { | |
throw new AssertionError( | |
`Expected the mock function to not have returned, but it returned ${returned.length} times`, | |
); | |
} | |
} else { | |
if (returned.length === 0) { | |
throw new AssertionError( | |
`Expected the mock function to have returned, but it did not return`, | |
); | |
} | |
} | |
} | |
export function toHaveReturnedTimes( | |
context: MatcherContext, | |
expected: number, | |
): MatchResult { | |
const calls = getMockCalls(context.value); | |
const returned = calls.filter((call) => call.returns); | |
if (context.isNot) { | |
if (returned.length === expected) { | |
throw new AssertionError( | |
`Expected the mock function to not have returned ${expected} times, but it returned ${returned.length} times`, | |
); | |
} | |
} else { | |
if (returned.length !== expected) { | |
throw new AssertionError( | |
`Expected the mock function to have returned ${expected} times, but it returned ${returned.length} times`, | |
); | |
} | |
} | |
} | |
export function toHaveReturnedWith( | |
context: MatcherContext, | |
expected: unknown, | |
): MatchResult { | |
const calls = getMockCalls(context.value); | |
const returned = calls.filter((call) => call.returns); | |
const returnedWithExpected = returned.some((call) => | |
equal(call.returned, expected) | |
); | |
if (context.isNot) { | |
if (returnedWithExpected) { | |
throw new AssertionError( | |
`Expected the mock function to not have returned with ${ | |
inspectArg(expected) | |
}, but it did`, | |
); | |
} | |
} else { | |
if (!returnedWithExpected) { | |
throw new AssertionError( | |
`Expected the mock function to have returned with ${ | |
inspectArg(expected) | |
}, but it did not`, | |
); | |
} | |
} | |
} | |
export function toHaveLastReturnedWith( | |
context: MatcherContext, | |
expected: unknown, | |
): MatchResult { | |
const calls = getMockCalls(context.value); | |
const returned = calls.filter((call) => call.returns); | |
const lastReturnedWithExpected = returned.length > 0 && | |
equal(returned.at(-1)?.returned, expected); | |
if (context.isNot) { | |
if (lastReturnedWithExpected) { | |
throw new AssertionError( | |
`Expected the mock function to not have last returned with ${ | |
inspectArg(expected) | |
}, but it did`, | |
); | |
} | |
} else { | |
if (!lastReturnedWithExpected) { | |
throw new AssertionError( | |
`Expected the mock function to have last returned with ${ | |
inspectArg(expected) | |
}, but it did not`, | |
); | |
} | |
} | |
} | |
export function toHaveNthReturnedWith( | |
context: MatcherContext, | |
nth: number, | |
expected: unknown, | |
): MatchResult { | |
if (nth < 1) { | |
throw new Error(`nth(${nth}) must be greater than 0`); | |
} | |
const calls = getMockCalls(context.value); | |
const returned = calls.filter((call) => call.returns); | |
const returnIndex = nth - 1; | |
const maybeNthReturned = returned[returnIndex]; | |
const nthReturnedWithExpected = maybeNthReturned && | |
equal(maybeNthReturned.returned, expected); | |
if (context.isNot) { | |
if (nthReturnedWithExpected) { | |
throw new AssertionError( | |
`Expected the mock function to not have n-th (n=${nth}) returned with ${ | |
inspectArg(expected) | |
}, but it did`, | |
); | |
} | |
} else { | |
if (!nthReturnedWithExpected) { | |
throw new AssertionError( | |
`Expected the mock function to have n-th (n=${nth}) returned with ${ | |
inspectArg(expected) | |
}, but it did not`, | |
); | |
} | |
} | |
} | |
export function toThrow<E extends Error = Error>( | |
context: MatcherContext, | |
// deno-lint-ignore no-explicit-any | |
expected?: string | RegExp | E | (new (...args: any[]) => E), | |
): MatchResult { | |
if (typeof context.value === "function") { | |
try { | |
context.value = context.value(); | |
} catch (err) { | |
context.value = err; | |
} | |
} | |
// deno-lint-ignore no-explicit-any | |
type ErrorClass = new (...args: any[]) => Error; | |
let expectClass: undefined | ErrorClass = undefined; | |
let expectMessage: undefined | string | RegExp = undefined; | |
if (expected instanceof Error) { | |
expectClass = expected.constructor as ErrorClass; | |
expectMessage = expected.message; | |
} | |
if (typeof expected === "function") { | |
expectClass = expected as ErrorClass; | |
} | |
if (typeof expected === "string" || expected instanceof RegExp) { | |
expectMessage = expected; | |
} | |
if (context.isNot) { | |
let isError = false; | |
try { | |
assertIsError( | |
context.value, | |
expectClass, | |
expectMessage, | |
context.customMessage, | |
); | |
isError = true; | |
throw new AssertionError(`Expected to NOT throw ${expected}`); | |
} catch (e) { | |
if (isError) { | |
throw e; | |
} | |
return; | |
} | |
} | |
return assertIsError( | |
context.value, | |
expectClass, | |
expectMessage, | |
context.customMessage, | |
); | |
} | |
export const matchers = { | |
lastCalledWith: toHaveBeenLastCalledWith, | |
lastReturnedWith: toHaveLastReturnedWith, | |
nthCalledWith: toHaveBeenNthCalledWith, | |
nthReturnedWith: toHaveNthReturnedWith, | |
toBeCalled: toHaveBeenCalled, | |
toBeCalledTimes: toHaveBeenCalledTimes, | |
toBeCalledWith: toHaveBeenCalledWith, | |
toBeCloseTo, | |
toBeDefined, | |
toBeFalsy, | |
toBeGreaterThanOrEqual, | |
toBeGreaterThan, | |
toBeInstanceOf, | |
toBeLessThanOrEqual, | |
toBeLessThan, | |
toBeNaN, | |
toBeNull, | |
toBeTruthy, | |
toBeUndefined, | |
toBe, | |
toBeBigInt, | |
toBeBoolean, | |
toBeNumber, | |
toBeObject, | |
toBeString, | |
toBeSymbol, | |
toBeType, | |
toBeAsyncFunction, | |
toBeFunction, | |
toBeGeneratorFunction, | |
toBeGeneratorObject, | |
toBeAsyncGeneratorFunction, | |
toBeAsyncGeneratorObject, | |
toBeIterable, | |
toBeAsyncIterable, | |
toContainEqual, | |
toContain, | |
toEqual, | |
toHaveBeenCalledTimes, | |
toHaveBeenCalledWith, | |
toHaveBeenCalled, | |
toHaveBeenLastCalledWith, | |
toHaveBeenNthCalledWith, | |
toHaveLength, | |
toHaveLastReturnedWith, | |
toHaveNthReturnedWith, | |
toHaveProperty, | |
toHaveReturnedTimes, | |
toHaveReturnedWith, | |
toHaveReturned, | |
toMatchObject, | |
toMatch, | |
toReturn: toHaveReturned, | |
toReturnTimes: toHaveReturnedTimes, | |
toReturnWith: toHaveReturnedWith, | |
toStrictEqual, | |
toThrow, | |
} as const; | |
export type matchers = typeof matchers; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const MOCK_SYMBOL = Symbol.for("@MOCK"); | |
type MOCK_SYMBOL = typeof MOCK_SYMBOL; | |
export interface MockCall< | |
A extends readonly unknown[] = readonly unknown[], | |
R = unknown, | |
T = unknown, | |
> { | |
args: A; | |
returned?: R; | |
thrown?: T; | |
timestamp: number; | |
returns: boolean; | |
throws: boolean; | |
} | |
export function getMockCalls< | |
const F extends { | |
readonly [MOCK_SYMBOL]: { | |
readonly calls: readonly MockCall<any, any, any>[]; | |
}; | |
}, | |
>( | |
f: F, | |
): F[MOCK_SYMBOL] extends | |
{ readonly calls: infer C extends readonly MockCall[] } ? C | |
: readonly MockCall[]; | |
export function getMockCalls(f: unknown): readonly MockCall[]; | |
export function getMockCalls(f: any): readonly MockCall[] { | |
if (f == null || !(typeof f === "function" || typeof f === "object")) { | |
throw new TypeError("Received value must be a function or object"); | |
} | |
if (!(MOCK_SYMBOL in f)) { | |
throw new TypeError("Received value must be a mock or spy function"); | |
} | |
const mockInfo = f[MOCK_SYMBOL]; | |
if (!mockInfo) { | |
throw new Error("Received function must be a mock or spy function"); | |
} | |
return [...mockInfo.calls] as any; | |
} | |
export function fn<A extends readonly unknown[], R = unknown>( | |
...stubs: readonly ((...args: A) => R)[] | |
): { | |
<const Args extends A>(...args: Args): R | undefined; | |
readonly [MOCK_SYMBOL]: { | |
readonly calls: readonly MockCall[]; | |
}; | |
} { | |
const calls: MockCall[] = []; | |
const f = (...args: A) => { | |
const stub = stubs.length === 1 | |
// keep reusing the first | |
? stubs[0] | |
// pick the exact mock for the current call | |
: stubs[calls.length]; | |
try { | |
const returned = stub ? stub(...args) : undefined; | |
calls.push({ | |
args, | |
returned, | |
timestamp: Date.now(), | |
returns: true, | |
throws: false, | |
}); | |
return returned; | |
} catch (thrown) { | |
calls.push({ | |
args, | |
timestamp: Date.now(), | |
returns: false, | |
thrown, | |
throws: true, | |
}); | |
throw thrown; | |
} | |
}; | |
Reflect.defineProperty(f, MOCK_SYMBOL, { | |
value: { calls }, | |
writable: false, | |
configurable: true, | |
enumerable: false, | |
}); | |
return f as unknown as ReturnType<typeof fn>; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// runtime exports | |
import { expect } from "./expect.ts"; | |
import { matchers } from "./matchers.ts"; | |
import { fn, getMockCalls } from "./mock.ts"; | |
// type-only exports | |
import type { Expected } from "./expect.ts"; | |
import type { Matcher, MatcherContext, Matchers } from "./matchers.ts"; | |
import type { MockCall } from "./mock.ts"; | |
// vendored behavior-driven development (BDD) helpers | |
import { | |
afterAll, | |
afterEach, | |
beforeAll, | |
beforeEach, | |
describe, | |
it, | |
} from "https://deno.land/[email protected]/testing/bdd.ts"; | |
export { | |
afterAll, | |
afterEach, | |
beforeAll, | |
beforeEach, | |
describe, | |
expect, | |
fn, | |
fn as mock, | |
getMockCalls, | |
it, | |
matchers, | |
}; | |
export type { Expected, Matcher, MatcherContext, Matchers, MockCall }; | |
export default { | |
afterAll, | |
afterEach, | |
beforeAll, | |
beforeEach, | |
describe, | |
expect, | |
fn, | |
getMockCalls, | |
it, | |
matchers, | |
mock: fn, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// deno-lint-ignore-file no-explicit-any | |
import { AssertionError } from "https://deno.land/[email protected]/assert/mod.ts"; | |
export { | |
assertArrayIncludes, | |
assertEquals, | |
assertInstanceOf, | |
AssertionError, | |
assertIsError, | |
assertMatch, | |
assertNotEquals, | |
assertNotInstanceOf, | |
assertNotMatch, | |
assertNotStrictEquals, | |
assertObjectMatch, | |
assertStrictEquals, | |
assertStringIncludes, | |
equal, | |
} from "https://deno.land/[email protected]/assert/mod.ts"; | |
export type Constructor<T, A extends readonly unknown[]> = { | |
new (...args: A): T; | |
}; | |
export type AnyConstructor = Constructor<any, any>; | |
export interface AsyncFunctionConstructor | |
extends Constructor<AsyncFunction, string[]> { | |
readonly prototype: AsyncFunction; | |
(...args: string[]): AsyncFunction; | |
} | |
export interface AsyncFunction<TReturn = unknown> { | |
<T extends TReturn>(...args: unknown[]): Promise<T>; | |
readonly prototype: AsyncFunction; | |
} | |
export type Fn<T = any, A extends readonly unknown[] = any> = (...args: A) => T; | |
/** | |
* Converts all the methods in an interface to be async functions, | |
* including deeply nested methods, preserving the original types + structure. | |
*/ | |
export type Async<T> = [T] extends [ | |
((...a: infer A) => infer R) & (infer O), | |
] ? | |
& (A extends readonly [] ? () => Promise<Awaited<R>> | |
: <const Args extends A>(...args: Args) => Promise<Awaited<R>>) | |
& ([keyof O] extends [never] ? unknown : { [P in keyof O]: Async<O[P]> }) | |
: [T] extends [(...args: infer A) => infer R] | |
? <const Args extends A>(...args: Args) => Promise<Awaited<R>> | |
: { [K in keyof T]: T[K] extends Fn ? Async<T[K]> : T[K] }; | |
export const AsyncFunction: AsyncFunctionConstructor = | |
Object.getPrototypeOf(async () => {}).constructor; | |
export const AsyncGenerator: AsyncGeneratorFunction = Object.getPrototypeOf( | |
async function* () {}, | |
); | |
export const Generator: GeneratorFunction = Object.getPrototypeOf( | |
function* () {}, | |
); | |
export const AsyncGeneratorFunction: AsyncGeneratorFunctionConstructor = | |
AsyncGenerator.constructor as AsyncGeneratorFunctionConstructor; | |
export const GeneratorFunction: GeneratorFunctionConstructor = Generator | |
.constructor as GeneratorFunctionConstructor; | |
/** | |
* Converts the input into a string. Objects, Sets and Maps are sorted so as to | |
* make tests less flaky | |
* @param v Value to be formatted | |
*/ | |
export function format(v: unknown): string { | |
const { Deno } = globalThis as any; | |
return typeof Deno?.inspect === "function" | |
? Deno.inspect(v, { | |
depth: Infinity, | |
sorted: true, | |
trailingComma: true, | |
compact: false, | |
iterableLimit: Infinity, | |
// getters should be true in assertEquals. | |
getters: true, | |
strAbbreviateSize: Infinity, | |
}) | |
: `"${String(v).replace(/(?=["\\])/g, "\\")}"`; | |
} | |
export function inspectArgs<const A extends readonly unknown[]>( | |
args: A, | |
): string { | |
return args.map(inspectArg).join(", "); | |
} | |
export function inspectArg(arg: unknown): string { | |
const { Deno } = globalThis as any; | |
return typeof Deno !== "undefined" && Deno.inspect | |
? Deno.inspect(arg) | |
: String(arg); | |
} | |
export function isPromiseLike<T>(value: unknown): value is PromiseLike<T> { | |
if (value == null) { | |
return false; | |
} else { | |
return typeof ((value as Record<string, unknown>).then) === "function"; | |
} | |
} | |
export function assertIsIterable<T>( | |
value: unknown, | |
): asserts value is Iterable<T> { | |
if (value == null || typeof value !== "object") { | |
throw new AssertionError("The value is null or undefined"); | |
} | |
if ( | |
Symbol.iterator in value && typeof value[Symbol.iterator] !== "function" | |
) { | |
throw new AssertionError("The value is not iterable"); | |
} | |
} | |
export type Filter<A extends readonly unknown[], T = never> = A extends | |
readonly [infer B, ...infer C] ? B extends T ? Filter<C, T> | |
: B extends readonly unknown[] // filter deeeep bro | |
? [Filter<B, T>, ...Filter<C, T>] | |
: [B, ...Filter<C, T>] | |
: []; | |
export function filterUndefined<const T extends readonly unknown[]>( | |
array: T, | |
): Filter<T, undefined>; | |
export function filterUndefined<const T extends object>(obj: T): { | |
[K in keyof T as [T[K]] extends [undefined] ? never : K]: T[K]; | |
}; | |
export function filterUndefined<T>(obj: T): Exclude<T, undefined>; | |
export function filterUndefined(obj: any) { | |
if ( | |
typeof obj !== "object" || obj instanceof Date || obj instanceof RegExp || | |
ArrayBuffer.isView(obj) || obj === null || "size" in obj || | |
Symbol.iterator in obj | |
) { | |
return obj; | |
} else if (Array.isArray(obj)) { | |
return obj.map(filterUndefined); | |
} | |
const result = structuredClone(obj); | |
for (const key in result) { | |
const val = result[key]; | |
if (val === undefined) { | |
Reflect.deleteProperty(result, key); | |
continue; | |
} | |
result[key] = filterUndefined(val); | |
} | |
return result; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment