Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@

## New Features

- **`assert.rejects` / `assert.doesNotReject`**, matching Node's `assert` API. Each takes a promise or a
function returning one, accepts the same error criteria as `throws`, and returns a promise that
must be awaited.
- **`assert.notDeepStrictEqual` / `notDeepStrictEquals`**, the negation of `deepStrictEqual`, so
Node's `assert` API maps across completely for equality checks. Values differing only in type
(`{ a: 1 }` vs `{ a: "1" }`) are not strictly equal, so they pass.
Expand Down
4 changes: 4 additions & 0 deletions core/src/assert/assertClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { isEmptyFunc } from "./funcs/isEmpty";
import { matchFunc } from "./funcs/match";
import { isErrorFunc, throwsFunc } from "./funcs/throws";
import { rejectsFunc } from "./funcs/rejects";
import { hasPropertyFunc, hasOwnPropertyFunc, hasDeepPropertyFunc, hasDeepOwnPropertyFunc } from "./funcs/hasProperty";
import { hasNestedPropertyFunc, hasDeepNestedPropertyFunc, nestedIncludeFunc, deepNestedIncludeFunc } from "./funcs/nested";
import { createContext } from "./scopeContext";
Expand Down Expand Up @@ -256,6 +257,9 @@ export function createAssert(): IAssertClass {
throws: { scopeFn: throwsFunc, nArgs: 3 },
doesNotThrow: { scopeFn: createNotAdapter(throwsFunc), nArgs: 3 },

rejects: { scopeFn: rejectsFunc, nArgs: 3 },
doesNotReject: { scopeFn: createNotAdapter(rejectsFunc), nArgs: 3 },

match: { scopeFn: matchFunc, nArgs: 2 },
notMatch: { scopeFn: createNotAdapter(matchFunc), nArgs: 2 },

Expand Down
66 changes: 66 additions & 0 deletions core/src/assert/funcs/rejects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* @nevware21/tripwire
* https://github.com/nevware21/tripwire
*
* Copyright (c) 2026 NevWare21 Solutions LLC
* Licensed under the MIT license.
*/

import { arrSlice, isFunction, isPromiseLike } from "@nevware21/ts-utils";
import { createPromise } from "@nevware21/ts-async";
import { MsgSource } from "../../types/MsgSource";
import { IAssertScope } from "../interface/IAssertScope";
import { throwsFunc } from "./throws";

/**
* Asserts that the target promise (or the promise returned by calling the target function) is rejected,
* optionally with an error matching the `errorLike` / `msgMatch` criteria (the same as `throws`).
* When negated (`doesNotReject`) it asserts that the promise is not rejected (or, if criteria are
* supplied, is not rejected with a matching error).
* Returns a promise that must be awaited.
*
* @since 0.2.0
* @param this - The assert scope.
* @param errorLike - The error, error constructor, message string or RegExp the rejection must match.
* @param msgMatch - The message string or RegExp the rejection must match.
* @param evalMsg - The message to display if the assertion fails.
* @returns A promise resolving to the assert instance.
*/
export function rejectsFunc<R>(this: IAssertScope, errorLike?: ErrorConstructor | Error | string | RegExp, msgMatch?: string | RegExp, evalMsg?: MsgSource): R {
let scope = this;
let context = scope.context;
let value = context.value;
let theArgs = arrSlice(arguments);

if (!isFunction(value) && !isPromiseLike(value)) {
scope.fatal("expected {value} to be a function or a Promise");
}

return createPromise<any>((resolve, reject) => {
// A synchronous throw from the function rejects the returned promise with that error
let thePromise: PromiseLike<any> = isFunction(value) ? value() : value;
if (!isPromiseLike(thePromise)) {
scope.fatal("expected {value} to return a Promise");
}

thePromise.then(function (result) {
try {
// Running through the context will throw unless negated (doesNotReject)
context.eval(false, (theArgs.length > 2 ? evalMsg : null) || "expected {value} to reject");
resolve(scope.newScope(result).that as any);
} catch (e) {
reject(e);
}
}, function (reason) {
try {
let thrower = function () {
throw reason;
};

resolve(throwsFunc.apply(scope.newScope(thrower), theArgs as any));
} catch (e) {
reject(e);
}
});
}) as any;
}
37 changes: 37 additions & 0 deletions core/src/assert/interface/IAssertClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,43 @@ export interface IAssertClass<AssertInst extends IAssertInst = IAssertInst> {
*/
doesNotThrow(fn: () => void, msgMatch?: string | RegExp | null, initMsg?: MsgSource): AssertInst;

/**
* Awaits the promise (or calls the function and awaits the returned promise) and asserts that it
* is rejected. Optionally checks the rejection reason using the same criteria as {@link throws}.
* The returned promise must be awaited (or returned) for the assertion to be observed.
* @param asyncFn - A promise, or a function returning a promise.
* @param errorLike - Optional. The error constructor, error instance, message string or RegExp the rejection must match.
* @param msgMatch - Optional. The partial message or pattern the rejection must match.
* @param initMsg - The message to display if the assertion fails.
* @since 0.2.0
* @example
* ```typescript
* await assert.rejects(Promise.reject(new Error("test"))); // Passes
* await assert.rejects(async () => { throw new TypeError("bad"); }, TypeError); // Passes
* await assert.rejects(async () => { throw new Error("bad"); }, /bad/); // Passes
* await assert.rejects(Promise.resolve(1)); // Rejects with AssertionFailure
* ```
*/
rejects(asyncFn: (() => PromiseLike<any>) | PromiseLike<any>, errorLike?: ErrorConstructor | Error | string | RegExp | null, msgMatch?: string | RegExp | null, initMsg?: MsgSource): PromiseLike<AssertInst>;

/**
* Awaits the promise (or calls the function and awaits the returned promise) and asserts that it
* is NOT rejected (or, if criteria are given, not rejected with a matching error). This is the
* inverse of {@link rejects}. The returned promise must be awaited (or returned).
* @param asyncFn - A promise, or a function returning a promise.
* @param errorLike - Optional. The error constructor, error instance, message string or RegExp that must NOT match the rejection.
* @param msgMatch - Optional. The partial message or pattern that must NOT match the rejection.
* @param initMsg - The message to display if the assertion fails.
* @since 0.2.0
* @example
* ```typescript
* await assert.doesNotReject(Promise.resolve(1)); // Passes
* await assert.doesNotReject(async () => 42); // Passes
* await assert.doesNotReject(Promise.reject(new Error("test"))); // Rejects with AssertionFailure
* ```
*/
doesNotReject(asyncFn: (() => PromiseLike<any>) | PromiseLike<any>, errorLike?: ErrorConstructor | Error | string | RegExp | null, msgMatch?: string | RegExp | null, initMsg?: MsgSource): PromiseLike<AssertInst>;

/**
* Asserts that the provided value matches the specified regular expression.
* If the value does not match the regular expression, it throws an {@link AssertionFailure}
Expand Down
82 changes: 82 additions & 0 deletions core/test/src/assert/assert.rejects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* @nevware21/tripwire
* https://github.com/nevware21/tripwire
*
* Copyright (c) 2026 NevWare21 Solutions LLC
* Licensed under the MIT license.
*/

import { assert } from "../../../src/assert/assertClass";
import { AssertionFailure } from "../../../src/assert/assertionFailure";

async function _fails(fn: () => PromiseLike<any>): Promise<any> {
try {
await fn();
} catch (e) {
return e;
}
return null;
}

describe("assert.rejects / assert.doesNotReject", () => {
it("rejects passes for a rejected promise or function", async () => {
await assert.rejects(Promise.reject(new Error("test")));
await assert.rejects(() => Promise.reject(new Error("test")));
await assert.rejects(async () => {
throw new TypeError("bad");
}, TypeError);
await assert.rejects(async () => {
throw new Error("bad thing");
}, /bad/);
await assert.rejects(async () => {
throw new Error("bad thing");
}, "bad");
});

it("rejects fails when the promise resolves", async () => {
let e = await _fails(() => assert.rejects(Promise.resolve(1)));
assert.ok(e instanceof AssertionFailure);
e = await _fails(() => assert.rejects(async () => 1));
assert.ok(e instanceof AssertionFailure);
});

it("rejects fails when the reason does not match", async () => {
let e = await _fails(() => assert.rejects(async () => {
throw new Error("x");
}, TypeError));
assert.ok(e instanceof AssertionFailure);
e = await _fails(() => assert.rejects(async () => {
throw new Error("x");
}, /nope/));
assert.ok(e instanceof AssertionFailure);
});

it("rejects propagates a synchronous throw from the function", async () => {
let e = await _fails(() => assert.rejects((() => {
throw new RangeError("sync");
}) as any));
assert.ok(e instanceof RangeError);
});

it("rejects fails for a non promise value", async () => {
let e = await _fails(() => assert.rejects(42 as any));
assert.ok(e);
});

it("doesNotReject passes when not rejected", async () => {
await assert.doesNotReject(Promise.resolve(1));
await assert.doesNotReject(async () => 42);
await assert.doesNotReject(async () => {
throw new Error("other");
}, /test/);
});

it("doesNotReject fails when rejected", async () => {
let e = await _fails(() => assert.doesNotReject(Promise.reject(new Error("test"))));
assert.ok(e instanceof AssertionFailure);
e = await _fails(() => assert.doesNotReject(async () => {
throw new Error("test");
}, /test/));
assert.ok(e instanceof AssertionFailure);
});
});
7 changes: 3 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
| **Property Assertions** | [`hasProperty`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#hasproperty), [`hasOwnProperty`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#hasownproperty), [`hasDeepProperty`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#hasdeepproperty), [`nestedProperty`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#nestedproperty), [`deepNestedProperty`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#deepnestedproperty) |
| **Collection Operations** | [`includes`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#includes), [`sameMembers`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#samemembers), [`includeMembers`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#includemembers), [`lengthOf`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#lengthof), [`sizeOf`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#sizeof) |
| **Change Tracking** | [`changes`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#changes), [`increases`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#increases), [`decreases`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#decreases) - See [Change/Increase/Decrease Assertions Guide](change-assertions.md) |
| **Error Testing** | [`throws`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#throws), [`doesNotThrow`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#doesnotthrow) |
| **Error Testing** | [`throws`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#throws), [`doesNotThrow`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#doesnotthrow), [`rejects`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#rejects), [`doesNotReject`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertClass.html#doesnotreject) |
| **Chaining** | [`and`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertInst.html#and) continues a chain after a called assertion (`expect(5).is.number().and.is.above(1)`); [`or`](https://nevware21.github.io/tripwire/typedoc/core/interfaces/IAssertInst.html#or) passes when any of the given clause functions passes (`expect(5).or(e => e.is.string(), e => e.is.number())`) |
| **Extensibility** | [`createExprAdapter`](https://nevware21.github.io/tripwire/typedoc/core/functions/createExprAdapter.html), [`addAssertFunc`](https://nevware21.github.io/tripwire/typedoc/core/functions/addAssertFunc.html) - See [Expression Adapter Guide](expression-adapter.md) |

Expand Down Expand Up @@ -95,8 +95,7 @@ unchanged. Two methods differ:
- assert.doesNotMatch(text, /pattern/);
+ assert.notMatch(text, /pattern/);

- await assert.rejects(fn, /error pattern/);
+ // no rejects()/doesNotReject(): await the promise and assert on the caught error
await assert.rejects(fn, /error pattern/); // unchanged
```

See the [full migration guide](migration/migrating-from-node-assert.md) for complete details and examples.
Expand Down Expand Up @@ -193,7 +192,7 @@ The shim implements the Chai v5.x `assert` API including:
- Properties: `property`, `deepProperty`, `nestedProperty`, `ownProperty` with value checks
- Collections: `include`, `deepInclude`, `members`, `sameMembers`, `keys`
- Changes: `changes`, `increases`, `decreases` with delta tracking
- Errors: `throws`, `doesNotThrow`
- Errors: `throws`, `doesNotThrow`, `rejects`, `doesNotReject`
- Object state: `isExtensible`, `isSealed`, `isFrozen`, `isEmpty`

### What's NOT Supported
Expand Down
15 changes: 4 additions & 11 deletions docs/migration/migrating-from-node-assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ Most Node.js assert methods are supported in Tripwire with the same or similar b
| `assert.notDeepStrictEqual(actual, expected)` | `assert.notDeepStrictEqual(actual, expected)` | ✅ **Identical in v0.2.0+** |
| `assert.throws(fn, error?)` | `assert.throws(fn, error?)` | Similar (error messages may differ) |
| `assert.doesNotThrow(fn)` | `assert.doesNotThrow(fn)` | Similar |
| `await assert.rejects(asyncFn, error?)` | `await assert.rejects(asyncFn, error?)` | ✅ **Available in v0.2.0+** (same error matching as `throws`) |
| `await assert.doesNotReject(asyncFn, error?)` | `await assert.doesNotReject(asyncFn, error?)` | ✅ **Available in v0.2.0+** |
| `assert.match(string, regexp)` | `assert.match(string, regexp)` | Identical |
| `assert.doesNotMatch(string, regexp)` | `assert.notMatch(string, regexp)` | Renamed |
| `assert.fail(message)` | `assert.fail(message)` | Similar |
Expand Down Expand Up @@ -169,8 +171,6 @@ assert.isFinite(100);
The following Node.js assert methods are not currently available in Tripwire:

- `assert.doesNotMatch(string, regexp)` - Use `assert.notMatch(string, regexp)` or `expect(string).to.not.match(regexp)` instead (`assert.match` itself carries over unchanged)
- `assert.rejects(asyncFn)` - Tripwire does not currently provide a direct equivalent for asserting Promise rejections; use `async`/`await` with `try`/`catch` and normal assertions on the caught error.
- `assert.doesNotReject(asyncFn)` - Tripwire does not currently provide a direct equivalent; `await` the Promise and let the test fail if it rejects, or use `try`/`catch` and call `assert.fail` on rejection.

---

Expand Down Expand Up @@ -544,15 +544,8 @@ assert.throws(() => fn(), /error pattern/);
assert.match(string, /pattern/);
assert.notMatch(string, /other/);

// There is no assert.rejects(): await the promise and assert on the caught error
let caught;
try {
await fn();
} catch (e) {
caught = e;
}
assert.isDefined(caught, "expected fn() to reject");
assert.match(caught.message, /error pattern/);
// Use assert.rejects() / assert.doesNotReject() and await the result
await assert.rejects(fn, /error pattern/);
```

---
Expand Down
Loading