diff --git a/packages/dev-test/package.json b/packages/dev-test/package.json
index ed29e2a..c836614 100644
--- a/packages/dev-test/package.json
+++ b/packages/dev-test/package.json
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
- "version": "0.86.6",
+ "version": "0.86.7",
"main": "./index.js",
"exports": {
"./globals.d.ts": "./src/globals.d.ts"
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)[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, message && { message } as Error)
+ toThrow: (check?: RegExp | Error | string) => assert.rejects(value as Promise, (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)
};
}
diff --git a/packages/dev-test/src/packageInfo.ts b/packages/dev-test/src/packageInfo.ts
index 65cf17e..d67254e 100644
--- a/packages/dev-test/src/packageInfo.ts
+++ b/packages/dev-test/src/packageInfo.ts
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @pezkuwi/dev
-export const packageInfo = { name: '@pezkuwi/dev-test', path: 'auto', type: 'auto', version: '0.86.6' };
+export const packageInfo = { name: '@pezkuwi/dev-test', path: 'auto', type: 'auto', version: '0.86.7' };
diff --git a/packages/dev-ts/package.json b/packages/dev-ts/package.json
index d600536..e63fa69 100644
--- a/packages/dev-ts/package.json
+++ b/packages/dev-ts/package.json
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
- "version": "0.86.6",
+ "version": "0.86.7",
"main": "./index.js",
"exports": {
"./globals.d.ts": "./src/globals.d.ts"
diff --git a/packages/dev-ts/src/packageInfo.ts b/packages/dev-ts/src/packageInfo.ts
index e5aec23..b6cff06 100644
--- a/packages/dev-ts/src/packageInfo.ts
+++ b/packages/dev-ts/src/packageInfo.ts
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @pezkuwi/dev
-export const packageInfo = { name: '@pezkuwi/dev-ts', path: 'auto', type: 'auto', version: '0.86.6' };
+export const packageInfo = { name: '@pezkuwi/dev-ts', path: 'auto', type: 'auto', version: '0.86.7' };
diff --git a/packages/dev/package.json b/packages/dev/package.json
index 559a971..6b1d0de 100644
--- a/packages/dev/package.json
+++ b/packages/dev/package.json
@@ -15,7 +15,7 @@
},
"sideEffects": false,
"type": "module",
- "version": "0.86.6",
+ "version": "0.86.7",
"bin": {
"pezkuwi-ci-ghact-build": "./scripts/pezkuwi-ci-ghact-build.mjs",
"pezkuwi-ci-ghact-docs": "./scripts/pezkuwi-ci-ghact-docs.mjs",
@@ -52,8 +52,8 @@
"dependencies": {
"@eslint/compat": "^2.1.1",
"@eslint/js": "^10.0.1",
- "@pezkuwi/dev-test": "^0.86.6",
- "@pezkuwi/dev-ts": "^0.86.6",
+ "@pezkuwi/dev-test": "^0.86.7",
+ "@pezkuwi/dev-ts": "^0.86.7",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^29.0.3",
"@rollup/plugin-dynamic-import-vars": "^2.1.5",
diff --git a/packages/dev/src/packageInfo.ts b/packages/dev/src/packageInfo.ts
index 1ec83e0..e138bc1 100644
--- a/packages/dev/src/packageInfo.ts
+++ b/packages/dev/src/packageInfo.ts
@@ -3,4 +3,4 @@
// Do not edit, auto-generated by @pezkuwi/dev
-export const packageInfo = { name: '@pezkuwi/dev', path: 'auto', type: 'auto', version: '0.86.6' };
+export const packageInfo = { name: '@pezkuwi/dev', path: 'auto', type: 'auto', version: '0.86.7' };
diff --git a/yarn.lock b/yarn.lock
index ace19c3..a7720ca 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1189,7 +1189,7 @@ __metadata:
languageName: node
linkType: hard
-"@pezkuwi/dev-test@npm:^0.86.6, @pezkuwi/dev-test@workspace:packages/dev-test":
+"@pezkuwi/dev-test@npm:^0.86.7, @pezkuwi/dev-test@workspace:packages/dev-test":
version: 0.0.0-use.local
resolution: "@pezkuwi/dev-test@workspace:packages/dev-test"
dependencies:
@@ -1205,7 +1205,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@pezkuwi/dev-ts@npm:^0.86.6, @pezkuwi/dev-ts@workspace:packages/dev-ts":
+"@pezkuwi/dev-ts@npm:^0.86.7, @pezkuwi/dev-ts@workspace:packages/dev-ts":
version: 0.0.0-use.local
resolution: "@pezkuwi/dev-ts@workspace:packages/dev-ts"
dependencies:
@@ -1221,8 +1221,8 @@ __metadata:
dependencies:
"@eslint/compat": "npm:^2.1.1"
"@eslint/js": "npm:^10.0.1"
- "@pezkuwi/dev-test": "npm:^0.86.6"
- "@pezkuwi/dev-ts": "npm:^0.86.6"
+ "@pezkuwi/dev-test": "npm:^0.86.7"
+ "@pezkuwi/dev-ts": "npm:^0.86.7"
"@rollup/plugin-alias": "npm:^5.1.1"
"@rollup/plugin-commonjs": "npm:^29.0.3"
"@rollup/plugin-dynamic-import-vars": "npm:^2.1.5"