diff --git a/packages/dev-test/src/env/expect.spec.ts b/packages/dev-test/src/env/expect.spec.ts index b14a0c3..f692748 100644 --- a/packages/dev-test/src/env/expect.spec.ts +++ b/packages/dev-test/src/env/expect.spec.ts @@ -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
')) + ).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(); }); diff --git a/packages/dev-test/src/env/expect.ts b/packages/dev-test/src/env/expect.ts index 2a2fec9..6093aaa 100644 --- a/packages/dev-test/src/env/expect.ts +++ b/packages/dev-test/src/env/expect.ts @@ -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