UNPKG

@serenity-js/core

Version:

The core Serenity/JS framework, providing the Screenplay Pattern interfaces, as well as the test reporting and integration infrastructure

157 lines (141 loc) 4.87 kB
import { TinyType } from 'tiny-types'; /** * Base class for custom errors that may occur during execution of a test scenario. * * ## Defining a custom error * * ```ts * import { RuntimeError } from '@serenity-js/core' * * export class CustomError extends RuntimeError { * constructor(message: string, cause?: Error) { * super(CustomError, message, cause); * } * } * ``` * * ## Wrapping a sync error * * ```ts * try { * operationThatMightThrowAnError(); * } catch(error) { * // catch and re-throw * throw new CustomError('operationThatMightThrowAnError has failed', error); * } * ``` * * ## Wrapping an async error * * ```ts * operationThatMightRejectAPromise() * .catch(error => { * // catch and re-throw * throw new CustomError('operationThatMightThrowAnError has failed', error) * }) * ``` * * ## Registering a custom error with [`ErrorSerialiser`](https://serenity-js.org/api/core/class/ErrorSerialiser/) * * ```ts * import { RuntimeError } from '@serenity-js/core' * import { ErrorSerialiser } from '@serenity-js/core/io' * * export class CustomError extends RuntimeError { * * static fromJSON(serialised: JSONObject): CustomError { * const error = new CustomError( * serialised.message as string, * ErrorSerialiser.deserialise(serialised.cause as string | undefined), * ); * * error.stack = serialised.stack as string; * * return error; * } * * constructor(message: string, cause?: Error) { * super(CustomError, message, cause); * } * } * * ErrorSerialiser.registerErrorTypes(CustomError) * ``` * * @group Errors */ export abstract class RuntimeError extends Error { /** * Symbol used to brand RuntimeError instances for cross-module instanceof checks. * This addresses the dual-package hazard where the same class loaded from * both ESM and CJS creates distinct constructor functions. */ private static readonly TYPE_BRAND = Symbol.for('@serenity-js/core/RuntimeError'); /** * Custom instanceof check that works across module boundaries. * This addresses the dual-package hazard where the same class loaded from * both ESM and CJS creates distinct constructor functions. * * The check walks up the prototype chain of the instance and compares * constructor names, which remain consistent across module boundaries. */ static [Symbol.hasInstance](instance: unknown): boolean { if (instance === null || typeof instance !== 'object') { return false; } // First, verify this is a RuntimeError instance using the brand if ((instance as any)[RuntimeError.TYPE_BRAND] !== true) { return false; } // When checking against the base RuntimeError class, any branded instance qualifies if (this.name === 'RuntimeError') { return true; } // For subclass checks, walk the prototype chain and compare by name // This works across ESM/CJS boundaries where constructor references differ let proto = Object.getPrototypeOf(instance); while (proto !== null) { if (proto.constructor?.name === this.name) { return true; } proto = Object.getPrototypeOf(proto); } return false; } /** * @param type - Constructor function used to instantiate a subclass of a RuntimeError * @param message - Human-readable description of the error * @param [cause] - The root cause of this [`RuntimeError`](https://serenity-js.org/api/core/class/RuntimeError/), if any */ protected constructor( type: new (...args: any[]) => RuntimeError, message: string, public readonly cause?: Error, ) { const errorMessage = message || ''; const isMultiline = errorMessage.includes('\n'); const causeAlreadyIncluded = cause?.message && errorMessage.includes(cause.message); super( isMultiline || causeAlreadyIncluded ? errorMessage : errorMessage + (cause && cause?.message ? `; ${ cause.message }` : '') ); Object.setPrototypeOf(this, type.prototype); this.name = this.constructor.name; // Brand the instance for cross-module instanceof checks (this as any)[RuntimeError.TYPE_BRAND] = true; Error.captureStackTrace(this, type); if (cause) { this.stack = `${ this.stack }\nCaused by: ${ cause.stack }`; } } /** * Human-readable description of the error */ toString(): string { return `${ this.constructor.name }: ${ this.message }`; } toJSON(): object { return TinyType.prototype.toJSON.apply(this) } }