Skip to content

Commit 9a3eb2d

Browse files
authored
feat(testing): add new custom matchers to expect (#107)
1 parent ea87905 commit 9a3eb2d

File tree

5 files changed

+185
-92
lines changed

5 files changed

+185
-92
lines changed

deno.lock

Lines changed: 17 additions & 89 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

testing/deno.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"version": "4.0.3",
55
"imports": {
66
"@std/assert": "jsr:@std/assert@1",
7-
"@std/expect": "jsr:@std/expect@1",
7+
"@std/expect": "jsr:@lowlighter/std-expect@1.0.18-rc.1",
88
"@std/fmt": "jsr:@std/fmt@1",
99
"@std/html": "jsr:@std/html@1",
1010
"@std/http": "jsr:@std/http@1"

testing/expect.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,54 @@ export interface ExtendedExpected<IsAsync = false> extends Expected<IsAsync> {
5353
* ```
5454
*/
5555
toBeType: (type: string, options?: { nullable?: boolean }) => unknown
56+
/**
57+
* Asserts that the value has the specified size (variant of `.toHaveLength()` for Maps and Sets).
58+
*
59+
* ```ts
60+
* import { expect } from "./expect.ts"
61+
* expect(new Set()).toHaveSize(0)
62+
* expect(new Map([["foo", 1]])).toHaveSize(1)
63+
* ```
64+
*/
65+
toHaveSize: (size: number) => unknown
66+
/**
67+
* Asserts a promise is resolved.
68+
*
69+
* ```ts
70+
* import { expect } from "./expect.ts"
71+
* const { promise, resolve } = Promise.withResolvers<void>()
72+
* await expect(promise).not.toBeResolvedPromise()
73+
*
74+
* ```
75+
*/
76+
toBeResolvedPromise: () => Promise<unknown>
77+
/**
78+
* Asserts that the function was called with the specified arguments, and exactly once.
79+
*
80+
* This is a shorthand for `.toHaveBeenCalledTimes(1)` and `.toHaveBeenCalledWith()`.
81+
*
82+
* Note that this matcher does not support the `.not` modifier.
83+
* If you expect one of the assertions to fail, you should use the adequate matchers instead.
84+
*
85+
* ```ts
86+
* import { expect, fn } from "./expect.ts"
87+
* const mock = fn()
88+
* mock("foo", 42)
89+
* expect(mock).toHaveBeenCalledOnceWith("foo", 42)
90+
* ```
91+
* @param expected The expected arguments.
92+
*/
93+
toHaveBeenCalledOnceWith(...expected: unknown[]): unknown
94+
/**
95+
* Asserts a value is structured clonable (using `structuredClone`).
96+
*
97+
* ```ts
98+
* import { expect } from "./expect.ts"
99+
* expect({ foo: "bar" }).toBeStructuredClonable()
100+
* expect({ foo: () => {} }).not.toBeStructuredClonable()
101+
* ```
102+
*/
103+
toBeStructuredClonable: () => unknown
56104
/**
57105
* Asserts a property matches a given descriptor (using `Object.getOwnPropertyDescriptor`).
58106
*
@@ -393,6 +441,47 @@ _expect.extend({
393441
}
394442
}, `Expected value to {!NOT} be of type "${type}"${!nullable ? " and not null but " : ""}`)
395443
},
444+
toHaveSize(context, size) {
445+
const actual = (context.value as { size: number })?.size
446+
return process(context.isNot, () => {
447+
assert(actual === size)
448+
}, `Expected value to {!NOT} have size ${size}${size !== actual ? `: the value has size ${actual}` : ""}`)
449+
},
450+
async toBeResolvedPromise(context) {
451+
if (!(context.value instanceof Promise)) {
452+
throw new TypeError("Expected value to be a promise")
453+
}
454+
const test = new Promise<void>((resolve) => setTimeout(resolve, 0))
455+
const status = await Promise.race([
456+
context.value.then(() => "resolved"),
457+
test.then(() => "pending"),
458+
])
459+
await test
460+
return process(context.isNot, () => {
461+
assert(status === "resolved")
462+
}, "Expected value to {!NOT} be resolved")
463+
},
464+
toHaveBeenCalledOnceWith(context, ...args) {
465+
if (context.isNot) {
466+
throw new TypeError("`.not` modifier is not supported for this matcher")
467+
}
468+
try {
469+
_expect(context.value).toHaveBeenCalledTimes(1)
470+
_expect(context.value).toHaveBeenCalledWith(...args)
471+
return { message: () => "", pass: true }
472+
} catch (error) {
473+
return { message: () => error.message, pass: false }
474+
}
475+
},
476+
toBeStructuredClonable(context) {
477+
return process(context.isNot, () => {
478+
try {
479+
structuredClone(context.value)
480+
} catch (error) {
481+
throw new AssertionError(error.message)
482+
}
483+
}, "Expected value to {!NOT} be structured clonable")
484+
},
396485
toHaveDescribedProperty(context, key, expected) {
397486
return process(context.isNot, () => {
398487
isType(context.value, "object", { nullable: false })
@@ -626,6 +715,16 @@ _expect.extend({
626715
},
627716
})
628717

718+
/** Reset call history of a mock or spy function. */
719+
// deno-lint-ignore no-explicit-any
720+
export function reset(fn: any) {
721+
const info = fn?.[Symbol.for("@MOCK")]
722+
if (!info) {
723+
throw new Error("Received function must be a mock or spy function")
724+
}
725+
info.calls.length = 0
726+
}
727+
629728
/** https://jsr.io/@std/expect/doc/~/expect. */
630729
const expect = _expect as unknown as ((...args: Parameters<typeof _expect>) => ExtendedExpected) & { [K in keyof typeof _expect]: typeof _expect[K] }
631730

0 commit comments

Comments
 (0)