expect: fix toBeNull, add not.toHaveLength and toHaveProperty, toThrow matches as jest does

- toBeNull and not.toBeNull took a parameter named value, which shadowed
  the value under test: .toBeNull() always failed and .not.toBeNull()
  always passed.
- not.toHaveLength and toHaveProperty (dotted or array path, optional
  expected value with asymmetric matchers) were unimplemented stubs.
- toThrow and rejects.toThrow with a string required the message to be
  equal; as in jest, the message now has to contain it. A RegExp matches
  the message and an Error compares messages.
This commit is contained in:
Satoshi Qazi Muhammed committed 2026-10-10 12:51:13 +04:00
1 parent c83f0adb5b
commit 709c071494
2 files changed
+116 -4

No files matched your search

+65
View File
@@ -1,6 +1,8 @@
// Copyright 2017-2026 @pezkuwi/dev-test authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { strict as assert } from 'node:assert';
describe('expect', () => {
it('has been decorated', () => {
expect(expect(true).not).toBeDefined();
@@ -24,6 +26,58 @@ describe('expect', () => {
Promise.reject(new Error('this is a rejection message'))
).rejects.toThrow(/rejection/);
});
it('matches a rejection containing the message', async () => {
await expect(
Promise.reject(new Error('Unable to find an element by: [data-testid="x"]\n\n<div />'))
).rejects.toThrow('Unable to find an element by: [data-testid="x"]');
});
it('fails on a rejection without the message', async () => {
// typed as void, it returns the promise of assert.rejects
await assert.rejects(() =>
Promise.resolve(expect(Promise.reject(new Error('another reason'))).rejects.toThrow('rejection'))
);
});
});
describe('.toBeNull', () => {
it('passes on null, and fails on anything else', () => {
expect(null).toBeNull();
expect(() => expect(undefined).toBeNull()).toThrow();
expect(() => expect(0).toBeNull()).toThrow();
});
it('supports .not', () => {
expect(undefined).not.toBeNull();
expect(() => expect(null).not.toBeNull()).toThrow();
});
});
describe('.toHaveLength', () => {
it('supports .not', () => {
expect([1]).not.toHaveLength(0);
expect(() => expect([]).not.toHaveLength(0)).toThrow();
});
});
describe('.toHaveProperty', () => {
const value = { a: { b: [1, 2] }, c: undefined };
it('finds a property by key, dotted path or array path', () => {
expect(value).toHaveProperty('c');
expect(value).toHaveProperty('a.b');
expect(value).toHaveProperty(['a', 'b', '1']);
expect(() => expect(value).toHaveProperty('a.d')).toThrow();
expect(() => expect(null).toHaveProperty('a')).toThrow();
});
it('compares the value when one is given', () => {
expect(value).toHaveProperty('a.b', [1, 2]);
expect(value).toHaveProperty('a', { b: expect.arrayContaining([2]) });
expect(() => expect(value).toHaveProperty('a.b', [1])).toThrow();
expect(() => expect(value).toHaveProperty('c', null)).toThrow();
});
});
describe('.toBeDefined', () => {
@@ -59,6 +113,17 @@ describe('expect', () => {
expect(thrower).toThrow(/me er/);
});
it('matches error containing the message', () => {
expect(thrower).toThrow('some');
expect(thrower).toThrow(new Error('some error'));
});
it('fails on an error without the message', () => {
expect(() => expect(thrower).toThrow('other')).toThrow('Expected an error containing "other", found "some error"');
expect(() => expect(thrower).toThrow(/other/)).toThrow();
expect(() => expect(thrower).toThrow(new Error('some'))).toThrow();
});
it('handles .not correctly (no throw, empty message)', () => {
expect(() => undefined).not.toThrow();
});
+51 -4
View File
@@ -279,6 +279,51 @@ function assertIncludes (value: string | unknown[], [check, Clazz]: [string | un
}
}
/**
* @internal
*
* As .toThrow(check) has it: a string is contained in the message, a RegExp
* matches it, an Error has the same message. Returns true, as the validation
* functions of assert.throws and assert.rejects must
*/
function assertErrorMatches (error: unknown, check?: RegExp | Error | string): true {
const message = error instanceof Error
? error.message
: String(error);
if (typeof check === 'string') {
assert.ok(message.includes(check), `Expected an error containing "${check}", found "${message}"`);
} else if (check instanceof RegExp) {
assert.match(message, check);
} else if (check) {
assert.strictEqual(message, check.message);
}
return true;
}
/**
* @internal
*
* As .toHaveProperty(path, expected?) has it: the (dotted or array) path
* exists, and when an expected value is given, equals it
*/
function assertHasProperty (value: unknown, path: string | string[], expected: unknown[]): void {
const keys = Array.isArray(path)
? path
: path.split('.');
let current = value;
for (const key of keys) {
assert.ok(current !== null && current !== undefined && key in Object(current), `Expected a property at "${keys.join('.')}"`);
current = (current as Record<string, unknown>)[key];
}
if (expected.length) {
assertEqualMatch(current, expected[0]);
}
}
/**
* Sets up the shimmed expect(...) function, including all .to* and .not.to*
* functions. This is not comprehensive, rather is contains what we need to
@@ -301,17 +346,18 @@ export function expect () {
not: enhanceObj({
toBe: (other: unknown) => assert.notStrictEqual(value, other),
toBeDefined: () => assert.ok(value === undefined),
toBeNull: (value: unknown) => assert.ok(value !== null),
toBeNull: () => assert.ok(value !== null, 'Expected a value other than null'),
toBeUndefined: () => assert.ok(value !== undefined),
toContain: (item: unknown) => assertNotContains(value, item),
toEqual: (other: unknown) => containsMatcher(other)
? assert.throws(() => assertEqualMatch(value, other))
: assert.notDeepEqual(value, other),
toHaveBeenCalled: () => assert.ok(!(value as Mocked | undefined)?.mock?.calls.length),
toHaveLength: (length: number) => assert.notEqual((value as unknown[] | undefined)?.length, length),
toThrow: (message?: RegExp | Error | string) => assert.doesNotThrow(value as () => unknown, message && { message } as Error)
}, stubExpectFnNot),
rejects: enhanceObj({
toThrow: (message?: RegExp | Error | string) => assert.rejects(value as Promise<unknown>, message && { message } as Error)
toThrow: (check?: RegExp | Error | string) => assert.rejects(value as Promise<unknown>, (error) => assertErrorMatches(error, check))
}, stubExpectFnRejects),
resolves: enhanceObj({}, stubExpectFnResolves),
toBe: (other: unknown) => assert.strictEqual(value, other),
@@ -319,7 +365,7 @@ export function expect () {
toBeFalsy: () => assert.ok(!value),
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
toBeInstanceOf: (Clazz: Function) => assertInstanceOf(value, Clazz),
toBeNull: (value: unknown) => assert.ok(value === null),
toBeNull: () => assert.ok(value === null, `Expected null, found ${String(value)}`),
toBeTruthy: () => assert.ok(value),
toBeUndefined: () => assert.ok(value === undefined),
toContain: (item: unknown) => assertContains(value, item),
@@ -331,9 +377,10 @@ export function expect () {
toHaveBeenCalledWith: (...args: unknown[]) => assertSomeCallHasArgs((value as Mocked | undefined), args),
toHaveBeenLastCalledWith: (...args: unknown[]) => assertCallHasArgs((value as Mocked | undefined)?.mock?.calls.at(-1), args),
toHaveLength: (length: number) => assert.equal((value as unknown[] | undefined)?.length, length),
toHaveProperty: (path: string | string[], ...expected: unknown[]) => assertHasProperty(value, path, expected),
toMatch: (check: string | RegExp) => assertMatchStr(value, check),
toMatchObject: (check: object) => assertMatchObj(value, check),
toThrow: (message?: RegExp | Error | string) => assert.throws(value as () => unknown, message && { message } as Error)
toThrow: (check?: RegExp | Error | string) => assert.throws(value as () => unknown, (error) => assertErrorMatches(error, check))
}, stubExpectFn), rootMatchers), stubExpect)
};
}