From d470af4397d79b9a25c920b3313fc9a8622d5125 Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Mon, 28 Apr 2025 17:35:48 +0800 Subject: [PATCH 1/4] Create unstable copies of files --- assert/unstable_is_error.ts | 65 ++++++++++++ assert/unstable_is_error_test.ts | 81 +++++++++++++++ assert/unstable_rejects.ts | 123 ++++++++++++++++++++++ assert/unstable_rejects_test.ts | 149 ++++++++++++++++++++++++++ assert/unstable_throws.ts | 111 ++++++++++++++++++++ assert/unstable_throws_test.ts | 173 +++++++++++++++++++++++++++++++ 6 files changed, 702 insertions(+) create mode 100644 assert/unstable_is_error.ts create mode 100644 assert/unstable_is_error_test.ts create mode 100644 assert/unstable_rejects.ts create mode 100644 assert/unstable_rejects_test.ts create mode 100644 assert/unstable_throws.ts create mode 100644 assert/unstable_throws_test.ts diff --git a/assert/unstable_is_error.ts b/assert/unstable_is_error.ts new file mode 100644 index 000000000000..22dab3f30904 --- /dev/null +++ b/assert/unstable_is_error.ts @@ -0,0 +1,65 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import { AssertionError } from "./assertion_error.ts"; +import { stripAnsiCode } from "@std/internal/styles"; + +/** + * Make an assertion that `error` is an `Error`. + * If not then an error will be thrown. + * An error class and a string that should be included in the + * error message can also be asserted. + * + * @example Usage + * ```ts ignore + * import { assertIsError } from "@std/assert"; + * + * assertIsError(null); // Throws + * assertIsError(new RangeError("Out of range")); // Doesn't throw + * assertIsError(new RangeError("Out of range"), SyntaxError); // Throws + * assertIsError(new RangeError("Out of range"), SyntaxError, "Out of range"); // Doesn't throw + * assertIsError(new RangeError("Out of range"), SyntaxError, "Within range"); // Throws + * ``` + * + * @typeParam E The type of the error to assert. + * @param error The error to assert. + * @param ErrorClass The optional error class to assert. + * @param msgMatches The optional string or RegExp to assert in the error message. + * @param msg The optional message to display if the assertion fails. + */ +export function assertIsError( + error: unknown, + // deno-lint-ignore no-explicit-any + ErrorClass?: abstract new (...args: any[]) => E, + msgMatches?: string | RegExp, + msg?: string, +): asserts error is E { + const msgSuffix = msg ? `: ${msg}` : "."; + if (!(error instanceof Error)) { + throw new AssertionError( + `Expected "error" to be an Error object${msgSuffix}`, + ); + } + if (ErrorClass && !(error instanceof ErrorClass)) { + msg = + `Expected error to be instance of "${ErrorClass.name}", but was "${error?.constructor?.name}"${msgSuffix}`; + throw new AssertionError(msg); + } + let msgCheck; + if (typeof msgMatches === "string") { + msgCheck = stripAnsiCode(error.message).includes( + stripAnsiCode(msgMatches), + ); + } + if (msgMatches instanceof RegExp) { + msgCheck = msgMatches.test(stripAnsiCode(error.message)); + } + + if (msgMatches && !msgCheck) { + msg = `Expected error message to include ${ + msgMatches instanceof RegExp + ? msgMatches.toString() + : JSON.stringify(msgMatches) + }, but got ${JSON.stringify(error?.message)}${msgSuffix}`; + throw new AssertionError(msg); + } +} diff --git a/assert/unstable_is_error_test.ts b/assert/unstable_is_error_test.ts new file mode 100644 index 000000000000..f94129bfa564 --- /dev/null +++ b/assert/unstable_is_error_test.ts @@ -0,0 +1,81 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { AssertionError, assertIsError, assertThrows } from "./mod.ts"; + +class CustomError extends Error {} +class AnotherCustomError extends Error {} + +Deno.test("assertIsError() throws when given value isn't error", () => { + assertThrows( + () => assertIsError("Panic!", undefined, "Panic!"), + AssertionError, + `Expected "error" to be an Error object.`, + ); + + assertThrows( + () => assertIsError(null), + AssertionError, + `Expected "error" to be an Error object.`, + ); + + assertThrows( + () => assertIsError(undefined), + AssertionError, + `Expected "error" to be an Error object.`, + ); +}); + +Deno.test("assertIsError() allows subclass of Error", () => { + assertIsError(new AssertionError("Fail!"), Error, "Fail!"); +}); + +Deno.test("assertIsError() allows custom error", () => { + assertIsError(new CustomError("failed"), CustomError, "fail"); + assertThrows( + () => assertIsError(new AnotherCustomError("failed"), CustomError, "fail"), + AssertionError, + 'Expected error to be instance of "CustomError", but was "AnotherCustomError".', + ); +}); + +Deno.test("assertIsError() accepts abstract class", () => { + abstract class AbstractError extends Error {} + class ConcreteError extends AbstractError {} + + assertIsError(new ConcreteError("failed"), AbstractError, "fail"); +}); + +Deno.test("assertIsError() throws with message diff containing double quotes", () => { + assertThrows( + () => + assertIsError( + new CustomError('error with "double quotes"'), + CustomError, + 'doesn\'t include "this message"', + ), + AssertionError, + `Expected error message to include "doesn't include \\"this message\\"", but got "error with \\"double quotes\\"".`, + ); +}); + +Deno.test("assertIsError() throws when given value doesn't match regex ", () => { + assertIsError(new AssertionError("Regex test"), Error, /ege/); + assertThrows( + () => assertIsError(new AssertionError("Regex test"), Error, /egg/), + Error, + `Expected error message to include /egg/, but got "Regex test"`, + ); +}); + +Deno.test("assertIsError() throws with custom message", () => { + assertThrows( + () => + assertIsError( + new CustomError("failed"), + AnotherCustomError, + "fail", + "CUSTOM MESSAGE", + ), + AssertionError, + 'Expected error to be instance of "AnotherCustomError", but was "CustomError": CUSTOM MESSAGE', + ); +}); diff --git a/assert/unstable_rejects.ts b/assert/unstable_rejects.ts new file mode 100644 index 000000000000..b61aa317d762 --- /dev/null +++ b/assert/unstable_rejects.ts @@ -0,0 +1,123 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import { AssertionError } from "./assertion_error.ts"; +import { assertIsError } from "./is_error.ts"; + +/** + * Executes a function which returns a promise, expecting it to reject. + * + * To assert that a synchronous function throws, use {@linkcode assertThrows}. + * + * @example Usage + * ```ts ignore + * import { assertRejects } from "@std/assert"; + * + * await assertRejects(async () => Promise.reject(new Error())); // Doesn't throw + * await assertRejects(async () => console.log("Hello world")); // Throws + * ``` + * + * @param fn The function to execute. + * @param msg The optional message to display if the assertion fails. + * @returns The promise which resolves to the thrown error. + */ +export function assertRejects( + fn: () => PromiseLike, + msg?: string, +): Promise; +/** + * Executes a function which returns a promise, expecting it to reject. + * If it does not, then it throws. An error class and a string that should be + * included in the error message can also be asserted. + * + * To assert that a synchronous function throws, use {@linkcode assertThrows}. + * + * @example Usage + * ```ts ignore + * import { assertRejects } from "@std/assert"; + * + * await assertRejects(async () => Promise.reject(new Error()), Error); // Doesn't throw + * await assertRejects(async () => Promise.reject(new Error()), SyntaxError); // Throws + * ``` + * + * @typeParam E The error class to assert. + * @param fn The function to execute. + * @param ErrorClass The error class to assert. + * @param msgIncludes The string that should be included in the error message. + * @param msg The optional message to display if the assertion fails. + * @returns The promise which resolves to the thrown error. + */ +export function assertRejects( + fn: () => PromiseLike, + // deno-lint-ignore no-explicit-any + ErrorClass: abstract new (...args: any[]) => E, + msgIncludes?: string, + msg?: string, +): Promise; +export async function assertRejects( + fn: () => PromiseLike, + errorClassOrMsg?: + // deno-lint-ignore no-explicit-any + | (abstract new (...args: any[]) => E) + | string, + msgIncludesOrMsg?: string, + msg?: string, +): Promise { + // deno-lint-ignore no-explicit-any + let ErrorClass: (abstract new (...args: any[]) => E) | undefined; + let msgIncludes: string | undefined; + let err; + + if (typeof errorClassOrMsg !== "string") { + if ( + errorClassOrMsg === undefined || + errorClassOrMsg.prototype instanceof Error || + errorClassOrMsg.prototype === Error.prototype + ) { + ErrorClass = errorClassOrMsg; + msgIncludes = msgIncludesOrMsg; + } + } else { + msg = errorClassOrMsg; + } + let doesThrow = false; + let isPromiseReturned = false; + const msgSuffix = msg ? `: ${msg}` : "."; + try { + const possiblePromise = fn(); + if ( + possiblePromise && + typeof possiblePromise === "object" && + typeof possiblePromise.then === "function" + ) { + isPromiseReturned = true; + await possiblePromise; + } else { + throw new Error(); + } + } catch (error) { + if (!isPromiseReturned) { + throw new AssertionError( + `Function throws when expected to reject${msgSuffix}`, + ); + } + if (ErrorClass) { + if (!(error instanceof Error)) { + throw new AssertionError(`A non-Error object was rejected${msgSuffix}`); + } + assertIsError( + error, + ErrorClass, + msgIncludes, + msg, + ); + } + err = error; + doesThrow = true; + } + if (!doesThrow) { + throw new AssertionError( + `Expected function to reject${msgSuffix}`, + ); + } + return err; +} diff --git a/assert/unstable_rejects_test.ts b/assert/unstable_rejects_test.ts new file mode 100644 index 000000000000..c4c065743001 --- /dev/null +++ b/assert/unstable_rejects_test.ts @@ -0,0 +1,149 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { assert, assertEquals, AssertionError, assertRejects } from "./mod.ts"; + +Deno.test("assertRejects() with return type", async () => { + await assertRejects(() => { + return Promise.reject(new Error()); + }); +}); + +Deno.test("assertRejects() with synchronous function that throws", async () => { + await assertRejects(() => + assertRejects(() => { + throw new Error(); + }) + ); + await assertRejects( + () => + assertRejects(() => { + throw { wrong: "true" }; + }), + AssertionError, + "Function throws when expected to reject.", + ); +}); + +Deno.test("assertRejects() with PromiseLike", async () => { + await assertRejects( + () => ({ + then() { + throw new Error("some error"); + }, + }), + Error, + "some error", + ); +}); + +Deno.test("assertRejects() with non-error value rejected and error class", async () => { + await assertRejects( + () => { + return assertRejects( + () => { + return Promise.reject("Panic!"); + }, + Error, + "Panic!", + ); + }, + AssertionError, + "A non-Error object was rejected.", + ); +}); + +Deno.test("assertRejects() with non-error value rejected", async () => { + await assertRejects(() => { + return Promise.reject(null); + }); + await assertRejects(() => { + return Promise.reject(undefined); + }); +}); + +Deno.test("assertRejects() with error class", async () => { + await assertRejects( + () => { + return Promise.reject(new Error("foo")); + }, + Error, + "foo", + ); +}); + +Deno.test("assertRejects() resolves with caught error", async () => { + const error = await assertRejects( + () => { + return Promise.reject(new Error("foo")); + }, + ); + assert(error instanceof Error); + assertEquals(error.message, "foo"); +}); + +Deno.test("assertRejects() throws async parent error ", async () => { + await assertRejects( + () => { + return Promise.reject(new AssertionError("Fail!")); + }, + Error, + "Fail!", + ); +}); + +Deno.test("assertRejects() accepts abstract class", () => { + abstract class AbstractError extends Error {} + class ConcreteError extends AbstractError {} + + assertRejects( + () => Promise.reject(new ConcreteError("failed")), + AbstractError, + "fail", + ); +}); + +Deno.test( + "assertRejects() throws with custom Error", + async () => { + class CustomError extends Error {} + class AnotherCustomError extends Error {} + await assertRejects( + () => + assertRejects( + () => Promise.reject(new AnotherCustomError("failed")), + CustomError, + "fail", + ), + AssertionError, + 'Expected error to be instance of "CustomError", but was "AnotherCustomError".', + ); + }, +); + +Deno.test("assertRejects() throws when no promise is returned", async () => { + await assertRejects( + // @ts-expect-error - testing invalid input + async () => await assertRejects(() => {}), + AssertionError, + "Function throws when expected to reject.", + ); +}); + +Deno.test("assertRejects() throws when the promise doesn't reject", async () => { + await assertRejects( + async () => await assertRejects(async () => await Promise.resolve(42)), + AssertionError, + "Expected function to reject.", + ); +}); + +Deno.test("assertRejects() throws with custom message", async () => { + await assertRejects( + async () => + await assertRejects( + async () => await Promise.resolve(42), + "CUSTOM MESSAGE", + ), + AssertionError, + "Expected function to reject: CUSTOM MESSAGE", + ); +}); diff --git a/assert/unstable_throws.ts b/assert/unstable_throws.ts new file mode 100644 index 000000000000..392ff912c49f --- /dev/null +++ b/assert/unstable_throws.ts @@ -0,0 +1,111 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import { assertIsError } from "./is_error.ts"; +import { AssertionError } from "./assertion_error.ts"; + +/** + * Executes a function, expecting it to throw. If it does not, then it + * throws. + * + * To assert that an asynchronous function rejects, use + * {@linkcode assertRejects}. + * + * @example Usage + * ```ts ignore + * import { assertThrows } from "@std/assert"; + * + * assertThrows(() => { throw new TypeError("hello world!"); }); // Doesn't throw + * assertThrows(() => console.log("hello world!")); // Throws + * ``` + * + * @param fn The function to execute. + * @param msg The optional message to display if the assertion fails. + * @returns The error that was thrown. + */ +export function assertThrows( + fn: () => unknown, + msg?: string, +): unknown; +/** + * Executes a function, expecting it to throw. If it does not, then it + * throws. An error class and a string that should be included in the + * error message can also be asserted. + * + * To assert that an asynchronous function rejects, use + * {@linkcode assertRejects}. + * + * @example Usage + * ```ts ignore + * import { assertThrows } from "@std/assert"; + * + * assertThrows(() => { throw new TypeError("hello world!"); }, TypeError); // Doesn't throw + * assertThrows(() => { throw new TypeError("hello world!"); }, RangeError); // Throws + * ``` + * + * @typeParam E The error class to assert. + * @param fn The function to execute. + * @param ErrorClass The error class to assert. + * @param msgIncludes The string that should be included in the error message. + * @param msg The optional message to display if the assertion fails. + * @returns The error that was thrown. + */ +export function assertThrows( + fn: () => unknown, + // deno-lint-ignore no-explicit-any + ErrorClass: abstract new (...args: any[]) => E, + msgIncludes?: string, + msg?: string, +): E; +export function assertThrows( + fn: () => unknown, + errorClassOrMsg?: + // deno-lint-ignore no-explicit-any + | (abstract new (...args: any[]) => E) + | string, + msgIncludesOrMsg?: string, + msg?: string, +): E | Error | unknown { + // deno-lint-ignore no-explicit-any + let ErrorClass: (abstract new (...args: any[]) => E) | undefined; + let msgIncludes: string | undefined; + let err; + + if (typeof errorClassOrMsg !== "string") { + if ( + errorClassOrMsg === undefined || + errorClassOrMsg?.prototype instanceof Error || + errorClassOrMsg?.prototype === Error.prototype + ) { + ErrorClass = errorClassOrMsg; + msgIncludes = msgIncludesOrMsg; + } else { + msg = msgIncludesOrMsg; + } + } else { + msg = errorClassOrMsg; + } + let doesThrow = false; + const msgSuffix = msg ? `: ${msg}` : "."; + try { + fn(); + } catch (error) { + if (ErrorClass) { + if (error instanceof Error === false) { + throw new AssertionError(`A non-Error object was thrown${msgSuffix}`); + } + assertIsError( + error, + ErrorClass, + msgIncludes, + msg, + ); + } + err = error; + doesThrow = true; + } + if (!doesThrow) { + msg = `Expected function to throw${msgSuffix}`; + throw new AssertionError(msg); + } + return err; +} diff --git a/assert/unstable_throws_test.ts b/assert/unstable_throws_test.ts new file mode 100644 index 000000000000..d18451879ae9 --- /dev/null +++ b/assert/unstable_throws_test.ts @@ -0,0 +1,173 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { + assert, + assertEquals, + AssertionError, + assertThrows, + fail, +} from "./mod.ts"; + +Deno.test("assertThrows() throws when thrown error class does not match expected", () => { + assertThrows( + () => { + //This next assertThrows will throw an AssertionError due to the wrong + //expected error class + assertThrows( + () => { + fail("foo"); + }, + TypeError, + "Failed assertion: foo", + ); + }, + AssertionError, + `Expected error to be instance of "TypeError", but was "AssertionError"`, + ); +}); + +Deno.test("assertThrows() changes its return type by parameter", () => { + assertThrows(() => { + throw new Error(); + }); +}); + +Deno.test("assertThrows() throws when error class is expected but non-error value is thrown", () => { + assertThrows( + () => { + assertThrows( + () => { + throw "Panic!"; + }, + Error, + "Panic!", + ); + }, + AssertionError, + "A non-Error object was thrown.", + ); +}); + +Deno.test("assertThrows() matches thrown non-error value", () => { + assertThrows( + () => { + throw "Panic!"; + }, + ); + assertThrows( + () => { + throw null; + }, + ); + assertThrows( + () => { + throw undefined; + }, + ); +}); + +Deno.test("assertThrows() matches thrown error with given error class", () => { + assertThrows( + () => { + throw new Error("foo"); + }, + Error, + "foo", + ); +}); + +Deno.test("assertThrows() matches and returns thrown error value", () => { + const error = assertThrows( + () => { + throw new Error("foo"); + }, + ); + assert(error instanceof Error); + assertEquals(error.message, "foo"); +}); + +Deno.test("assertThrows() matches and returns thrown non-error", () => { + const stringError = assertThrows( + () => { + throw "Panic!"; + }, + ); + assert(typeof stringError === "string"); + assertEquals(stringError, "Panic!"); + + const numberError = assertThrows( + () => { + throw 1; + }, + ); + assert(typeof numberError === "number"); + assertEquals(numberError, 1); + + const nullError = assertThrows( + () => { + throw null; + }, + ); + assert(nullError === null); + + const undefinedError = assertThrows( + () => { + throw undefined; + }, + ); + assert(typeof undefinedError === "undefined"); + assertEquals(undefinedError, undefined); +}); + +Deno.test("assertThrows() matches subclass of expected error", () => { + assertThrows( + () => { + throw new AssertionError("Fail!"); + }, + Error, + "Fail!", + ); +}); + +Deno.test("assertThrows() accepts abstract class", () => { + abstract class AbstractError extends Error {} + class ConcreteError extends AbstractError {} + + assertThrows( + () => { + throw new ConcreteError("failed"); + }, + AbstractError, + "fail", + ); +}); + +Deno.test("assertThrows() throws when input function does not throw", () => { + assertThrows( + () => { + assertThrows(() => {}); + }, + AssertionError, + "Expected function to throw.", + ); +}); + +Deno.test("assertThrows() throws with custom message", () => { + assertThrows( + () => { + assertThrows(() => {}, "CUSTOM MESSAGE"); + }, + AssertionError, + "Expected function to throw: CUSTOM MESSAGE", + ); +}); + +Deno.test("assertThrows() throws with custom message and no error class", () => { + assertThrows( + () => { + // @ts-expect-error testing invalid input + assertThrows(() => {}, null, "CUSTOM MESSAGE"); + }, + AssertionError, + "Expected function to throw: CUSTOM MESSAGE", + ); +}); From 1b63c5bf7acd82927a25290a08320cc65580baca Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Mon, 28 Apr 2025 17:45:26 +0800 Subject: [PATCH 2/4] feat(assert/unstable): allow asserting predicates against thrown/rejected errors --- assert/unstable_is_error.ts | 42 ++++++++++++++------- assert/unstable_is_error_test.ts | 33 +++++++++++++++- assert/unstable_rejects.ts | 14 +++---- assert/unstable_rejects_test.ts | 51 ++++++++++++++++++++++++- assert/unstable_throws.ts | 14 +++---- assert/unstable_throws_test.ts | 65 ++++++++++++++++++++++++++++---- 6 files changed, 180 insertions(+), 39 deletions(-) diff --git a/assert/unstable_is_error.ts b/assert/unstable_is_error.ts index 22dab3f30904..1d3c13370403 100644 --- a/assert/unstable_is_error.ts +++ b/assert/unstable_is_error.ts @@ -3,6 +3,14 @@ import { AssertionError } from "./assertion_error.ts"; import { stripAnsiCode } from "@std/internal/styles"; +/** + * A check to be applied against the error: + * - If a string is supplied, this must be present in the error's `message` property. + * - If a RegExp is supplied, this must match against the error's `message` property. + * - If a predicate function is provided, this must return `true` for the error. + */ +export type ErrorCheck = string | RegExp | ((e: E) => boolean); + /** * Make an assertion that `error` is an `Error`. * If not then an error will be thrown. @@ -16,24 +24,25 @@ import { stripAnsiCode } from "@std/internal/styles"; * assertIsError(null); // Throws * assertIsError(new RangeError("Out of range")); // Doesn't throw * assertIsError(new RangeError("Out of range"), SyntaxError); // Throws - * assertIsError(new RangeError("Out of range"), SyntaxError, "Out of range"); // Doesn't throw - * assertIsError(new RangeError("Out of range"), SyntaxError, "Within range"); // Throws + * assertIsError(new RangeError("Out of range"), RangeError, "Out of range"); // Doesn't throw + * assertIsError(new RangeError("Out of range"), RangeError, "Within range"); // Throws * ``` * * @typeParam E The type of the error to assert. * @param error The error to assert. * @param ErrorClass The optional error class to assert. - * @param msgMatches The optional string or RegExp to assert in the error message. + * @param check The optional string or RegExp to assert in the error message. * @param msg The optional message to display if the assertion fails. */ export function assertIsError( error: unknown, // deno-lint-ignore no-explicit-any ErrorClass?: abstract new (...args: any[]) => E, - msgMatches?: string | RegExp, + check?: ErrorCheck, msg?: string, ): asserts error is E { const msgSuffix = msg ? `: ${msg}` : "."; + if (!(error instanceof Error)) { throw new AssertionError( `Expected "error" to be an Error object${msgSuffix}`, @@ -45,20 +54,25 @@ export function assertIsError( throw new AssertionError(msg); } let msgCheck; - if (typeof msgMatches === "string") { + if (typeof check === "string") { msgCheck = stripAnsiCode(error.message).includes( - stripAnsiCode(msgMatches), + stripAnsiCode(check), ); - } - if (msgMatches instanceof RegExp) { - msgCheck = msgMatches.test(stripAnsiCode(error.message)); + } else if (check instanceof RegExp) { + msgCheck = check.test(stripAnsiCode(error.message)); + } else if (typeof check === "function") { + msgCheck = check(error as E); + if (!msgCheck) { + msg = `Error failed the check${msgSuffix}`; + throw new AssertionError(msg); + } } - if (msgMatches && !msgCheck) { - msg = `Expected error message to include ${ - msgMatches instanceof RegExp - ? msgMatches.toString() - : JSON.stringify(msgMatches) + if (check && !msgCheck) { + msg = `Expected error message to ${ + check instanceof RegExp + ? `match ${check}` + : `include ${JSON.stringify(check)}` }, but got ${JSON.stringify(error?.message)}${msgSuffix}`; throw new AssertionError(msg); } diff --git a/assert/unstable_is_error_test.ts b/assert/unstable_is_error_test.ts index f94129bfa564..606528a86f9d 100644 --- a/assert/unstable_is_error_test.ts +++ b/assert/unstable_is_error_test.ts @@ -1,5 +1,6 @@ // Copyright 2018-2025 the Deno authors. MIT license. -import { AssertionError, assertIsError, assertThrows } from "./mod.ts"; +import { AssertionError, assertThrows } from "./mod.ts"; +import { assertIsError } from "./unstable_is_error.ts"; class CustomError extends Error {} class AnotherCustomError extends Error {} @@ -62,7 +63,7 @@ Deno.test("assertIsError() throws when given value doesn't match regex ", () => assertThrows( () => assertIsError(new AssertionError("Regex test"), Error, /egg/), Error, - `Expected error message to include /egg/, but got "Regex test"`, + `Expected error message to match /egg/, but got "Regex test"`, ); }); @@ -79,3 +80,31 @@ Deno.test("assertIsError() throws with custom message", () => { 'Expected error to be instance of "AnotherCustomError", but was "CustomError": CUSTOM MESSAGE', ); }); + +Deno.test("assertIsError() with custom error check", () => { + class CustomError extends Error { + readonly code: number; + constructor(code: number) { + super(); + this.code = code; + } + } + + assertIsError( + new CustomError(-1), + CustomError, + (e) => e.code === -1, + ); + + assertThrows( + () => { + assertIsError( + new CustomError(-1), + CustomError, + (e) => e.code === -2, + ); + }, + AssertionError, + "Error failed the check.", + ); +}); diff --git a/assert/unstable_rejects.ts b/assert/unstable_rejects.ts index b61aa317d762..99ca2c54620d 100644 --- a/assert/unstable_rejects.ts +++ b/assert/unstable_rejects.ts @@ -1,7 +1,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. // This module is browser compatible. +import { assertIsError, type ErrorCheck } from "./unstable_is_error.ts"; import { AssertionError } from "./assertion_error.ts"; -import { assertIsError } from "./is_error.ts"; /** * Executes a function which returns a promise, expecting it to reject. @@ -42,7 +42,7 @@ export function assertRejects( * @typeParam E The error class to assert. * @param fn The function to execute. * @param ErrorClass The error class to assert. - * @param msgIncludes The string that should be included in the error message. + * @param check A string that should be included in the error message, or a callback that should return `true` for the error. * @param msg The optional message to display if the assertion fails. * @returns The promise which resolves to the thrown error. */ @@ -50,7 +50,7 @@ export function assertRejects( fn: () => PromiseLike, // deno-lint-ignore no-explicit-any ErrorClass: abstract new (...args: any[]) => E, - msgIncludes?: string, + check?: ErrorCheck, msg?: string, ): Promise; export async function assertRejects( @@ -59,12 +59,11 @@ export async function assertRejects( // deno-lint-ignore no-explicit-any | (abstract new (...args: any[]) => E) | string, - msgIncludesOrMsg?: string, + check?: ErrorCheck, msg?: string, ): Promise { // deno-lint-ignore no-explicit-any let ErrorClass: (abstract new (...args: any[]) => E) | undefined; - let msgIncludes: string | undefined; let err; if (typeof errorClassOrMsg !== "string") { @@ -74,7 +73,8 @@ export async function assertRejects( errorClassOrMsg.prototype === Error.prototype ) { ErrorClass = errorClassOrMsg; - msgIncludes = msgIncludesOrMsg; + } else { + msg = check as string; } } else { msg = errorClassOrMsg; @@ -107,7 +107,7 @@ export async function assertRejects( assertIsError( error, ErrorClass, - msgIncludes, + check, msg, ); } diff --git a/assert/unstable_rejects_test.ts b/assert/unstable_rejects_test.ts index c4c065743001..501df99ebcde 100644 --- a/assert/unstable_rejects_test.ts +++ b/assert/unstable_rejects_test.ts @@ -1,5 +1,6 @@ // Copyright 2018-2025 the Deno authors. MIT license. -import { assert, assertEquals, AssertionError, assertRejects } from "./mod.ts"; +import { assert, assertEquals, AssertionError } from "./mod.ts"; +import { assertRejects } from "./unstable_rejects.ts"; Deno.test("assertRejects() with return type", async () => { await assertRejects(() => { @@ -147,3 +148,51 @@ Deno.test("assertRejects() throws with custom message", async () => { "Expected function to reject: CUSTOM MESSAGE", ); }); + +Deno.test("assertRejects() with regex", () => { + assertRejects( + () => Promise.reject(new Error("HELLO WORLD!")), + Error, + /hello world/i, + ); + + assertRejects( + async () => { + await assertRejects( + () => Promise.reject(new Error("HELLO WORLD!")), + Error, + /^hello world$/i, + ); + }, + AssertionError, + "Expected error message to match /^hello world$/i", + ); +}); + +Deno.test("assertRejects() with custom error check", () => { + class CustomError extends Error { + readonly code: number; + constructor(code: number) { + super(); + this.code = code; + } + } + + assertRejects( + () => Promise.reject(new CustomError(-1)), + CustomError, + (e) => e.code === -1, + ); + + assertRejects( + async () => { + await assertRejects( + () => Promise.reject(new CustomError(-1)), + CustomError, + (e) => e.code === -2, + ); + }, + AssertionError, + "Error failed the check.", + ); +}); diff --git a/assert/unstable_throws.ts b/assert/unstable_throws.ts index 392ff912c49f..54be117d2634 100644 --- a/assert/unstable_throws.ts +++ b/assert/unstable_throws.ts @@ -1,6 +1,6 @@ // Copyright 2018-2025 the Deno authors. MIT license. // This module is browser compatible. -import { assertIsError } from "./is_error.ts"; +import { assertIsError, type ErrorCheck } from "./unstable_is_error.ts"; import { AssertionError } from "./assertion_error.ts"; /** @@ -45,7 +45,7 @@ export function assertThrows( * @typeParam E The error class to assert. * @param fn The function to execute. * @param ErrorClass The error class to assert. - * @param msgIncludes The string that should be included in the error message. + * @param check A string that should be included in the error message, or a callback that should return `true` for the error. * @param msg The optional message to display if the assertion fails. * @returns The error that was thrown. */ @@ -53,7 +53,7 @@ export function assertThrows( fn: () => unknown, // deno-lint-ignore no-explicit-any ErrorClass: abstract new (...args: any[]) => E, - msgIncludes?: string, + check?: ErrorCheck, msg?: string, ): E; export function assertThrows( @@ -62,12 +62,11 @@ export function assertThrows( // deno-lint-ignore no-explicit-any | (abstract new (...args: any[]) => E) | string, - msgIncludesOrMsg?: string, + check?: ErrorCheck, msg?: string, ): E | Error | unknown { // deno-lint-ignore no-explicit-any let ErrorClass: (abstract new (...args: any[]) => E) | undefined; - let msgIncludes: string | undefined; let err; if (typeof errorClassOrMsg !== "string") { @@ -77,9 +76,8 @@ export function assertThrows( errorClassOrMsg?.prototype === Error.prototype ) { ErrorClass = errorClassOrMsg; - msgIncludes = msgIncludesOrMsg; } else { - msg = msgIncludesOrMsg; + msg = check as string; } } else { msg = errorClassOrMsg; @@ -96,7 +94,7 @@ export function assertThrows( assertIsError( error, ErrorClass, - msgIncludes, + check, msg, ); } diff --git a/assert/unstable_throws_test.ts b/assert/unstable_throws_test.ts index d18451879ae9..c505377adf47 100644 --- a/assert/unstable_throws_test.ts +++ b/assert/unstable_throws_test.ts @@ -1,11 +1,6 @@ // Copyright 2018-2025 the Deno authors. MIT license. -import { - assert, - assertEquals, - AssertionError, - assertThrows, - fail, -} from "./mod.ts"; +import { assert, assertEquals, AssertionError, fail } from "./mod.ts"; +import { assertThrows } from "./unstable_throws.ts"; Deno.test("assertThrows() throws when thrown error class does not match expected", () => { assertThrows( @@ -171,3 +166,59 @@ Deno.test("assertThrows() throws with custom message and no error class", () => "Expected function to throw: CUSTOM MESSAGE", ); }); + +Deno.test("assertThrows() with regex", () => { + assertThrows( + () => { + throw new Error("HELLO WORLD!"); + }, + Error, + /hello world/i, + ); + + assertThrows( + () => { + assertThrows( + () => { + throw new Error("HELLO WORLD!"); + }, + Error, + /^hello world$/i, + ); + }, + AssertionError, + "Expected error message to match /^hello world$/i", + ); +}); + +Deno.test("assertThrows() with custom error check", () => { + class CustomError extends Error { + readonly code: number; + constructor(code: number) { + super(); + this.code = code; + } + } + + assertThrows( + () => { + throw new CustomError(-1); + }, + CustomError, + (e) => e.code === -1, + ); + + assertThrows( + () => { + assertThrows( + () => { + throw new CustomError(-1); + }, + CustomError, + (e) => e.code === -2, + ); + }, + AssertionError, + "Error failed the check.", + ); +}); From 47481207605d51c430fc2915db3864fccfd6514b Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Mon, 28 Apr 2025 17:52:41 +0800 Subject: [PATCH 3/4] Add to deno.json --- assert/deno.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assert/deno.json b/assert/deno.json index b0d1d7e1a9ed..f13fa6694590 100644 --- a/assert/deno.json +++ b/assert/deno.json @@ -30,6 +30,12 @@ "./equal": "./equal.ts", "./fail": "./fail.ts", "./unimplemented": "./unimplemented.ts", + "./unstable-is-error": "./unstable_is_error.ts", + "./unstable-is-error-test": "./unstable_is_error_test.ts", + "./unstable-rejects": "./unstable_rejects.ts", + "./unstable-rejects-test": "./unstable_rejects_test.ts", + "./unstable-throws": "./unstable_throws.ts", + "./unstable-throws-test": "./unstable_throws_test.ts", "./unreachable": "./unreachable.ts" } } From 477eccadee8531382282d2f30861ea22a47f7a7d Mon Sep 17 00:00:00 2001 From: lionel-rowe Date: Mon, 28 Apr 2025 18:06:15 +0800 Subject: [PATCH 4/4] Copy edits --- assert/unstable_is_error.ts | 31 +++++++++++++++++-------------- assert/unstable_rejects.ts | 12 ++++++------ assert/unstable_throws.ts | 12 ++++++------ 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/assert/unstable_is_error.ts b/assert/unstable_is_error.ts index 1d3c13370403..aaa918005583 100644 --- a/assert/unstable_is_error.ts +++ b/assert/unstable_is_error.ts @@ -4,12 +4,15 @@ import { AssertionError } from "./assertion_error.ts"; import { stripAnsiCode } from "@std/internal/styles"; /** - * A check to be applied against the error: + * A predicate to be checked against the error: * - If a string is supplied, this must be present in the error's `message` property. * - If a RegExp is supplied, this must match against the error's `message` property. * - If a predicate function is provided, this must return `true` for the error. */ -export type ErrorCheck = string | RegExp | ((e: E) => boolean); +export type ErrorPredicate = + | string + | RegExp + | ((e: E) => boolean); /** * Make an assertion that `error` is an `Error`. @@ -31,14 +34,14 @@ export type ErrorCheck = string | RegExp | ((e: E) => boolean); * @typeParam E The type of the error to assert. * @param error The error to assert. * @param ErrorClass The optional error class to assert. - * @param check The optional string or RegExp to assert in the error message. + * @param predicate An optional string or RegExp to match against the error message, or a callback that should return `true` for the error. * @param msg The optional message to display if the assertion fails. */ export function assertIsError( error: unknown, // deno-lint-ignore no-explicit-any ErrorClass?: abstract new (...args: any[]) => E, - check?: ErrorCheck, + predicate?: ErrorPredicate, msg?: string, ): asserts error is E { const msgSuffix = msg ? `: ${msg}` : "."; @@ -54,25 +57,25 @@ export function assertIsError( throw new AssertionError(msg); } let msgCheck; - if (typeof check === "string") { + if (typeof predicate === "string") { msgCheck = stripAnsiCode(error.message).includes( - stripAnsiCode(check), + stripAnsiCode(predicate), ); - } else if (check instanceof RegExp) { - msgCheck = check.test(stripAnsiCode(error.message)); - } else if (typeof check === "function") { - msgCheck = check(error as E); + } else if (predicate instanceof RegExp) { + msgCheck = predicate.test(stripAnsiCode(error.message)); + } else if (typeof predicate === "function") { + msgCheck = predicate(error as E); if (!msgCheck) { msg = `Error failed the check${msgSuffix}`; throw new AssertionError(msg); } } - if (check && !msgCheck) { + if (predicate && !msgCheck) { msg = `Expected error message to ${ - check instanceof RegExp - ? `match ${check}` - : `include ${JSON.stringify(check)}` + predicate instanceof RegExp + ? `match ${predicate}` + : `include ${JSON.stringify(predicate)}` }, but got ${JSON.stringify(error?.message)}${msgSuffix}`; throw new AssertionError(msg); } diff --git a/assert/unstable_rejects.ts b/assert/unstable_rejects.ts index 99ca2c54620d..479264106ffb 100644 --- a/assert/unstable_rejects.ts +++ b/assert/unstable_rejects.ts @@ -1,6 +1,6 @@ // Copyright 2018-2025 the Deno authors. MIT license. // This module is browser compatible. -import { assertIsError, type ErrorCheck } from "./unstable_is_error.ts"; +import { assertIsError, type ErrorPredicate } from "./unstable_is_error.ts"; import { AssertionError } from "./assertion_error.ts"; /** @@ -42,7 +42,7 @@ export function assertRejects( * @typeParam E The error class to assert. * @param fn The function to execute. * @param ErrorClass The error class to assert. - * @param check A string that should be included in the error message, or a callback that should return `true` for the error. + * @param predicate An optional string or RegExp to match against the error message, or a callback that should return `true` for the error. * @param msg The optional message to display if the assertion fails. * @returns The promise which resolves to the thrown error. */ @@ -50,7 +50,7 @@ export function assertRejects( fn: () => PromiseLike, // deno-lint-ignore no-explicit-any ErrorClass: abstract new (...args: any[]) => E, - check?: ErrorCheck, + predicate?: ErrorPredicate, msg?: string, ): Promise; export async function assertRejects( @@ -59,7 +59,7 @@ export async function assertRejects( // deno-lint-ignore no-explicit-any | (abstract new (...args: any[]) => E) | string, - check?: ErrorCheck, + predicate?: ErrorPredicate, msg?: string, ): Promise { // deno-lint-ignore no-explicit-any @@ -74,7 +74,7 @@ export async function assertRejects( ) { ErrorClass = errorClassOrMsg; } else { - msg = check as string; + msg = predicate as string; } } else { msg = errorClassOrMsg; @@ -107,7 +107,7 @@ export async function assertRejects( assertIsError( error, ErrorClass, - check, + predicate, msg, ); } diff --git a/assert/unstable_throws.ts b/assert/unstable_throws.ts index 54be117d2634..03f6de06d791 100644 --- a/assert/unstable_throws.ts +++ b/assert/unstable_throws.ts @@ -1,6 +1,6 @@ // Copyright 2018-2025 the Deno authors. MIT license. // This module is browser compatible. -import { assertIsError, type ErrorCheck } from "./unstable_is_error.ts"; +import { assertIsError, type ErrorPredicate } from "./unstable_is_error.ts"; import { AssertionError } from "./assertion_error.ts"; /** @@ -45,7 +45,7 @@ export function assertThrows( * @typeParam E The error class to assert. * @param fn The function to execute. * @param ErrorClass The error class to assert. - * @param check A string that should be included in the error message, or a callback that should return `true` for the error. + * @param predicate An optional string or RegExp to match against the error message, or a callback that should return `true` for the error. * @param msg The optional message to display if the assertion fails. * @returns The error that was thrown. */ @@ -53,7 +53,7 @@ export function assertThrows( fn: () => unknown, // deno-lint-ignore no-explicit-any ErrorClass: abstract new (...args: any[]) => E, - check?: ErrorCheck, + predicate?: ErrorPredicate, msg?: string, ): E; export function assertThrows( @@ -62,7 +62,7 @@ export function assertThrows( // deno-lint-ignore no-explicit-any | (abstract new (...args: any[]) => E) | string, - check?: ErrorCheck, + predicate?: ErrorPredicate, msg?: string, ): E | Error | unknown { // deno-lint-ignore no-explicit-any @@ -77,7 +77,7 @@ export function assertThrows( ) { ErrorClass = errorClassOrMsg; } else { - msg = check as string; + msg = predicate as string; } } else { msg = errorClassOrMsg; @@ -94,7 +94,7 @@ export function assertThrows( assertIsError( error, ErrorClass, - check, + predicate, msg, ); }