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" } } diff --git a/assert/unstable_is_error.ts b/assert/unstable_is_error.ts new file mode 100644 index 000000000000..aaa918005583 --- /dev/null +++ b/assert/unstable_is_error.ts @@ -0,0 +1,82 @@ +// 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"; + +/** + * 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 ErrorPredicate = + | string + | RegExp + | ((e: E) => boolean); + +/** + * 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"), 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 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, + predicate?: ErrorPredicate, + 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 predicate === "string") { + msgCheck = stripAnsiCode(error.message).includes( + stripAnsiCode(predicate), + ); + } 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 (predicate && !msgCheck) { + msg = `Expected error message to ${ + 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_is_error_test.ts b/assert/unstable_is_error_test.ts new file mode 100644 index 000000000000..606528a86f9d --- /dev/null +++ b/assert/unstable_is_error_test.ts @@ -0,0 +1,110 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { AssertionError, assertThrows } from "./mod.ts"; +import { assertIsError } from "./unstable_is_error.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 match /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', + ); +}); + +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 new file mode 100644 index 000000000000..479264106ffb --- /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 { assertIsError, type ErrorPredicate } from "./unstable_is_error.ts"; +import { AssertionError } from "./assertion_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 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. + */ +export function assertRejects( + fn: () => PromiseLike, + // deno-lint-ignore no-explicit-any + ErrorClass: abstract new (...args: any[]) => E, + predicate?: ErrorPredicate, + msg?: string, +): Promise; +export async function assertRejects( + fn: () => PromiseLike, + errorClassOrMsg?: + // deno-lint-ignore no-explicit-any + | (abstract new (...args: any[]) => E) + | string, + predicate?: ErrorPredicate, + msg?: string, +): Promise { + // deno-lint-ignore no-explicit-any + let ErrorClass: (abstract new (...args: any[]) => E) | undefined; + let err; + + if (typeof errorClassOrMsg !== "string") { + if ( + errorClassOrMsg === undefined || + errorClassOrMsg.prototype instanceof Error || + errorClassOrMsg.prototype === Error.prototype + ) { + ErrorClass = errorClassOrMsg; + } else { + msg = predicate as string; + } + } 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, + predicate, + 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..501df99ebcde --- /dev/null +++ b/assert/unstable_rejects_test.ts @@ -0,0 +1,198 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +import { assert, assertEquals, AssertionError } from "./mod.ts"; +import { assertRejects } from "./unstable_rejects.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", + ); +}); + +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 new file mode 100644 index 000000000000..03f6de06d791 --- /dev/null +++ b/assert/unstable_throws.ts @@ -0,0 +1,109 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +// This module is browser compatible. +import { assertIsError, type ErrorPredicate } from "./unstable_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 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. + */ +export function assertThrows( + fn: () => unknown, + // deno-lint-ignore no-explicit-any + ErrorClass: abstract new (...args: any[]) => E, + predicate?: ErrorPredicate, + msg?: string, +): E; +export function assertThrows( + fn: () => unknown, + errorClassOrMsg?: + // deno-lint-ignore no-explicit-any + | (abstract new (...args: any[]) => E) + | string, + predicate?: ErrorPredicate, + msg?: string, +): E | Error | unknown { + // deno-lint-ignore no-explicit-any + let ErrorClass: (abstract new (...args: any[]) => E) | undefined; + let err; + + if (typeof errorClassOrMsg !== "string") { + if ( + errorClassOrMsg === undefined || + errorClassOrMsg?.prototype instanceof Error || + errorClassOrMsg?.prototype === Error.prototype + ) { + ErrorClass = errorClassOrMsg; + } else { + msg = predicate as string; + } + } 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, + predicate, + 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..c505377adf47 --- /dev/null +++ b/assert/unstable_throws_test.ts @@ -0,0 +1,224 @@ +// Copyright 2018-2025 the Deno authors. MIT license. +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( + () => { + //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", + ); +}); + +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.", + ); +});