UNPKG

@plugjs/expect5

Version:

Unit Testing for the PlugJS Build System ========================================

834 lines (700 loc) 26.8 kB
import { Expectations } from './expectations' import { matcherMarker } from './types' import type { AssertedType, AssertionFunction, ExpectationsParent, InferMatcher, InferToEqual, NegativeExpectations, } from './expectations' import type { Constructor, TypeMappings, TypeName, } from './types' type PositiveMatcherFunction = (expectations: Expectations) => Expectations type NegativeMatcherFunction = (expectations: NegativeExpectations) => Expectations /* ========================================================================== * * MATCHERS * * ========================================================================== */ export class Matcher<T = unknown> { private readonly _matchers: PositiveMatcherFunction[] readonly not: NegativeMatchers<T> constructor() { const matchers: PositiveMatcherFunction[] = [] this.not = new NegativeMatchers(this, matchers) this._matchers = matchers } expect(value: unknown, parent?: ExpectationsParent): T { let expectations = new Expectations(value, undefined, parent) for (const matcher of this._matchers) { expectations = matcher(expectations) } return expectations.value as T } private _push(matcher: PositiveMatcherFunction): Matcher<any> { const matchers = new Matcher() matchers._matchers.push(...this._matchers, matcher) return matchers } static { (this.prototype as any)[matcherMarker] = matcherMarker } /* ------------------------------------------------------------------------ * * BASIC * * ------------------------------------------------------------------------ */ /** * Expects the value to be of the specified _extended_ {@link TypeName type}. * * Negation: {@link NegativeExpectations.toBeA `not.toBeA(...)`} */ toBeA<Name extends TypeName>(type: Name): Matcher<TypeMappings[Name]> /** * Expects the value to be of the specified _extended_ {@link TypeName type}, * and further validates it with a {@link Matcher}. * * Negation: {@link NegativeExpectations.toBeA `not.toBeA(...)`} */ toBeA< Name extends TypeName, Mapped extends TypeMappings[Name], Match extends Matcher, >( type: Name, matcher: Match, ): Matcher<InferMatcher<Mapped, Match>> /** * Expects the value to be of the specified _extended_ {@link TypeName type}, * and further asserts it with an {@link AssertionFunction}. * * Negation: {@link NegativeExpectations.toBeA `not.toBeA(...)`} */ toBeA< Name extends TypeName, Mapped extends TypeMappings[Name], Assert extends AssertionFunction<Mapped>, >( type: Name, assertion: Assert, ): Matcher<AssertedType<Mapped, Assert>> toBeA( type: TypeName, assertionOrMatcher?: AssertionFunction | Matcher, ): Matcher { return this._push((e) => e.toBeA(type, assertionOrMatcher as any)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `Date`, a `string` parseable into a `Date`, or a * `number` indicating the milliseconds from the epoch, _strictly after_ * the specified date. * * Negation: {@link Matcher.toBeBeforeOrEqual `toBeBeforeOrEqual(...)`} */ toBeAfter(value: Date | number | string, deltaMs?: number): Matcher<T> { return this._push((e) => e.toBeAfter(value, deltaMs)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `Date`, a `string` parseable into a `Date`, or a * `number` indicating the milliseconds from the epoch, _after or equal_ * the specified date. * * Negation: {@link Matcher.toBeBefore `toBeBefore(...)`} */ toBeAfterOrEqual(value: Date | number | string, deltaMs?: number): Matcher<T> { return this._push((e) => e.toBeAfterOrEqual(value, deltaMs)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `Date`, a `string` parseable into a `Date`, or a * `number` indicating the milliseconds from the epoch, _strictly before_ * the specified date. * * Negation: {@link Matcher.toBeAfterOrEqual `toBeAfterOrEqual(...)`} */ toBeBefore(value: Date | number | string, deltaMs?: number): Matcher<T> { return this._push((e) => e.toBeBefore(value, deltaMs)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `Date`, a `string` parseable into a `Date`, or a * `number` indicating the milliseconds from the epoch, _before or equal_ * the specified date. * * Negation: {@link Matcher.toBeAfter `toBeAfter(...)`} */ toBeBeforeOrEqual(value: Date | number | string, deltaMs?: number): Matcher<T> { return this._push((e) => e.toBeBeforeOrEqual(value, deltaMs)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` within a given +/- _delta_ range of the * specified expected value. * * Negation: {@link NegativeMatchers.toBeCloseTo `not.toBeCloseTo(...)`} */ toBeCloseTo(value: number, delta: number): Matcher<number> /** * Expects the value to be a `bigint` within a given +/- _delta_ range of the * specified expected value. * * Negation: {@link NegativeMatchers.toBeCloseTo `not.toBeCloseTo(...)`} */ toBeCloseTo(value: bigint, delta: bigint): Matcher<bigint> /** * Expects the value to be a `number` or `bigint` within a given +/- _delta_ * range of the specified expected value. * * Negation: {@link NegativeMatchers.toBeCloseTo `not.toBeCloseTo(...)`} */ toBeCloseTo( value: number | bigint, delta: number | bigint, ): Matcher { return this._push((e) => e.toBeCloseTo(value as number, delta as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be neither `null` nor `undefined`. * * Negation: {@link NegativeMatchers.toBeDefined `not.toBeDefined()`} */ toBeDefined(): Matcher<T> { return this._push((e) => e.toBeDefined()) } /* ------------------------------------------------------------------------ */ /** * Expect the value to be an instance of {@link Error}. * * If specified, the {@link Error}'s own message will be further expected to * either match the specified {@link RegExp}, or equal the specified `string`. */ toBeError( message?: string | RegExp ): Matcher<Error> /** * Expect the value to be an instance of {@link Error} and further asserts * it to be an instance of the specifed {@link Error} {@link Constructor}. * * If specified, the {@link Error}'s own message will be further expected to * either match the specified {@link RegExp}, or equal the specified `string`. */ toBeError<Class extends Constructor<Error>>( constructor: Class, message?: string | RegExp, ): Matcher<InstanceType<Class>> toBeError( constructorOrMessage?: string | RegExp | Constructor, maybeMessage?: string | RegExp, ): Matcher { const [ constructor, message ] = typeof constructorOrMessage === 'function' ? [ constructorOrMessage, maybeMessage ] : [ Error, constructorOrMessage ] return this._push((e) => e.toBeError(constructor, message)) } /* ------------------------------------------------------------------------ */ /** Expects the value strictly equal to `false`. */ toBeFalse(): Matcher<false> { return this._push((e) => e.toBeFalse()) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be _falsy_ (zero, empty string, `false`, ...). * * Negation: {@link Matcher.toBeTruthy `toBeTruthy()`} */ toBeFalsy(): Matcher<T> { return this._push((e) => e.toBeFalsy()) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` greater than the specified* expected * value. * * Negation: {@link Matcher.toBeLessThanOrEqual `toBeLessThanOrEqual(...)`} */ toBeGreaterThan(value: number): Matcher<number> /** * Expects the value to be a `bigint` greater than the specified expected * value. * * Negation: {@link Matcher.toBeLessThanOrEqual `toBeLessThanOrEqual(...)`} */ toBeGreaterThan(value: bigint): Matcher<bigint> toBeGreaterThan(value: number | bigint): Matcher { return this._push((e) => e.toBeGreaterThan(value as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` greater than or equal to the specified * expected value. * * Negation: {@link Matcher.toBeLessThan `toBeLessThan(...)`} */ toBeGreaterThanOrEqual(value: number): Matcher<number> /** * Expects the value to be a `bigint` greater than or equal to the specified * expected value. * * Negation: {@link Matcher.toBeLessThan `toBeLessThan(...)`} */ toBeGreaterThanOrEqual(value: bigint): Matcher<bigint> toBeGreaterThanOrEqual(value: number | bigint): Matcher { return this._push((e) => e.toBeGreaterThanOrEqual(value as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be an instance of the specified {@link Constructor}. * * Negation: {@link NegativeMatchers.toBeInstanceOf `not.toInstanceOf(...)`} */ toBeInstanceOf<Class extends Constructor>( constructor: Class, ): Matcher<InstanceType<Class>> /** * Expects the value to be an instance of the specified {@link Constructor}, * and further validates it with a {@link Matcher}. * * Negation: {@link NegativeMatchers.toBeInstanceOf `not.toInstanceOf(...)`} */ toBeInstanceOf<Class extends Constructor, Match extends Matcher>( constructor: Class, matcher: Match, ): Matcher<InferMatcher<InstanceType<Class>, Match>> /** * Expects the value to be an instance of the specified {@link Constructor}, * and further asserts it with an {@link AssertionFunction}. * * Negation: {@link NegativeMatchers.toBeInstanceOf `not.toInstanceOf(...)`} */ toBeInstanceOf< Class extends Constructor, Assert extends AssertionFunction<InstanceType<Class>>, >( constructor: Class, assertion: Assert, ): Matcher<AssertedType<InstanceType<Class>, Assert>> toBeInstanceOf( constructor: Constructor, assertionOrMatcher?: AssertionFunction | Matcher, ): Matcher { return this._push((e) => e.toBeInstanceOf(constructor, assertionOrMatcher as any)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` less than the specified expected value. * * Negation: {@link Matcher.toBeGreaterThanOrEqual `toBeGreaterThanOrEqual(...)`} */ toBeLessThan(value: number): Matcher<number> /** * Expects the value to be a `bigint` less than the specified expected value. * * Negation: {@link Matcher.toBeGreaterThanOrEqual `toBeGreaterThanOrEqual(...)`} */ toBeLessThan(value: bigint): Matcher<bigint> toBeLessThan(value: number | bigint): Matcher { return this._push((e) => e.toBeLessThan(value as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` less than or equal to* the specified * expected value. * * Negation: {@link Matcher.toBeGreaterThan `toBeGreaterThan(...)`} */ toBeLessThanOrEqual(value: number): Matcher<number> /** * Expects the value to be a `bigint` less than or equal to the specified * expected value. * * Negation: {@link Matcher.toBeGreaterThan `toBeGreaterThan(...)`} */ toBeLessThanOrEqual(value: bigint): Matcher<bigint> toBeLessThanOrEqual(value: number | bigint): Matcher { return this._push((e) => e.toBeLessThanOrEqual(value as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be `NaN`. * * Negation: {@link NegativeMatchers.toBeNaN `not.toBeNaN()`} */ toBeNaN(): Matcher<number> { return this._push((e) => e.toBeNaN()) } /* ------------------------------------------------------------------------ */ /** Expects the value to strictly equal `null`. */ toBeNull(): Matcher<null> { return this._push((e) => e.toBeNull()) } /* ------------------------------------------------------------------------ */ /** Expects the value to strictly equal `true`. */ toBeTrue(): Matcher<true> { return this._push((e) => e.toBeTrue()) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be _falsy_ (non-zero, non-empty string, ...). * * Negation: {@link Matcher.toBeFalsy `toBeFalsy()`} */ toBeTruthy(): Matcher<T> { return this._push((e) => e.toBeTruthy()) } /* ------------------------------------------------------------------------ */ /** Expects the value to strictly equal `undefined`. */ toBeUndefined(): Matcher<undefined> { return this._push((e) => e.toBeUndefined()) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` within the specified range where the * minimum and maximum values are inclusive. * * Negation: {@link NegativeMatchers.toBeWithinRange `not.toBeWithinRange(...)`} */ toBeWithinRange(min: number, max: number): Matcher<number> /** * Expects the value to be a `bigint` within the specified range where the * minimum and maximum values are inclusive. * * Negation: {@link NegativeMatchers.toBeWithinRange `not.toBeWithinRange(...)`} */ toBeWithinRange(min: bigint, max: bigint): Matcher<bigint> /** * Expects the value to be a `number` or `bigint` within the specified range * where minimum and maximum values are inclusive. * * Negation: {@link NegativeMatchers.toBeWithinRange `not.toBeWithinRange(...)`} */ toBeWithinRange( min: number | bigint, max: number | bigint): Matcher { return this._push((e) => e.toBeWithinRange(min as number, max as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be _deep equal to_ the specified expected one. * * Negation: {@link NegativeMatchers.toEqual `not.toEqual(...)`} */ toEqual<Type>(expected: Type): Matcher<InferToEqual<Type>> { return this._push((e) => e.toEqual(expected)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to have a `number` _property_ `length` with the specified * expected value. * * Negation: {@link NegativeMatchers.toHaveLength `not.toHaveLength(...)`} */ toHaveLength(length: number): Matcher<T & { length: number }> { return this._push((e) => e.toHaveLength(length)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to have the specified _property_. * * Negation: {@link NegativeExpectations.toHaveProperty `not.toHaveProperty(...)`} */ toHaveProperty<Prop extends string | number | symbol>( property: Prop, ): Matcher<T & { [keyt in Prop] : unknown }> /** * Expects the value to have the specified _property_ and validates its value * with a {@link Matcher}. * * Negation: {@link NegativeExpectations.toHaveProperty `not.toHaveProperty(...)`} */ toHaveProperty< Prop extends string | number | symbol, Match extends Matcher, >( property: Prop, matcher: Match, ): Matcher<T & { [keyt in Prop] : InferMatcher<unknown, Match> }> /** * Expects the value to have the specified _property_ and further asserts * its value with an {@link AssertionFunction}. * * Negation: {@link NegativeMatchers.toHaveProperty `not.toHaveProperty(...)`} */ toHaveProperty< Prop extends string | number | symbol, Assert extends AssertionFunction, >( property: Prop, assertion: Assert, ): Matcher<T & { [keyt in Prop] : AssertedType<unknown, Assert> }> toHaveProperty( property: string | number | symbol, assertionOrMatcher?: AssertionFunction | Matcher, ): Matcher { return this._push((e) => e.toHaveProperty(property, assertionOrMatcher as any)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to have a `number` _property_ `size` with the specified * expected value. * * Negation: {@link NegativeMatchers.toHaveSize `not.toHaveSize(...)`} */ toHaveSize(size: number): Matcher<T & { size: number }> { return this._push((e) => e.toHaveSize(size)) } /* ------------------------------------------------------------------------ */ /** * Expect the value to include _all_ properties from the specified _object_. * * If the object being expected is a {@link Map}, the properties specified * here will be treated as _mappings_ for said {@link Map}. * * Negation: {@link NegativeMatchers.toInclude `not.toInclude(...)`} */ toInclude<P extends Record<string, any>>(properties: P): Matcher<T> /** * Expect the value to include _all_ mappings from the specified {@link Map}. * * Negation: {@link NegativeMatchers.toInclude `not.toInclude(...)`} */ toInclude(mappings: Map<any, any>): Matcher<T> /** * Expect the value to be an {@link Iterable} object includind _all_ values * from the specified {@link Set}, in any order. * * Negation: {@link NegativeMatchers.toInclude `not.toInclude(...)`} */ toInclude(entries: Set<any>): Matcher<T> /** * Expect the value to be an {@link Iterable} object includind _all_ values * from the specified _array_, in any order. * * Negation: {@link NegativeMatchers.toInclude `not.toInclude(...)`} */ toInclude(values: any[]): Matcher<T> toInclude( contents: Record<string, any> | Map<any, any> | Set<any> | any[], ): Matcher { return this._push((e) => e.toInclude(contents)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `string` _matching_ the specified sub-`string` * or {@link RegExp}. * * Negation: {@link NegativeMatchers.toMatch `not.toMatch(...)`} */ toMatch<Match extends string | RegExp>( matcher: Match, ): Matcher<string> { return this._push((e) => e.toMatch(matcher)) } /* ------------------------------------------------------------------------ */ /** * Expect the value to be an {@link Iterable} object includind _all_ values * (and only those values) from the specified _array_, in any order. */ toMatchContents(contents: any[]): Matcher<T> /** * Expect the value to be an {@link Iterable} object includind _all_ values * (and only those values) from the specified {@link Set}, in any order. */ toMatchContents(contents: Set<any>): Matcher<T> toMatchContents(contents: any[] | Set<any>): Matcher { return this._push((e) => e.toMatchContents(contents)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be _strictly equal to_ the specified expected one. * * Negation: {@link NegativeMatchers.toStrictlyEqual `not.toStrictlyEqual(...)`} */ toStrictlyEqual<Type>(expected: Type): Matcher<Type> { return this._push((e) => e.toStrictlyEqual(expected)) } } /* ========================================================================== * * NEGATIVE MATCHERS * * ========================================================================== */ export class NegativeMatchers<T = unknown> { constructor( private readonly _instance: Matcher<T>, private readonly _matchers: PositiveMatcherFunction[], ) {} private _push(matcher: NegativeMatcherFunction): Matcher<any> { this._matchers.push((expectations) => matcher(expectations.not)) return this._instance } /* ------------------------------------------------------------------------ */ /** * Expects the value _**NOT**_ to be of the specified _extended_ * {@link TypeName type}. * * Negates: {@link Matcher.toBeA `toBeA(...)`} */ toBeA(type: TypeName): Matcher<T> { return this._push((e) => e.toBeA(type)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` _**OUTSIDE**_ of the given +/- _delta_ * range of the specified expected value. * * Negates: {@link Matcher.toBeCloseTo `toBeCloseTo(...)`} */ toBeCloseTo(value: number, delta: number): Matcher<number> /** * Expects the value to be a `bigint` _**OUTSIDE**_ of the given +/- _delta_ * range of the specified expected value. * * Negates: {@link Matcher.toBeCloseTo `toBeCloseTo(...)`} */ toBeCloseTo(value: bigint, delta: bigint): Matcher<bigint> toBeCloseTo(value: number | bigint, delta: number | bigint): Matcher { return this._push((e) => e.toBeCloseTo(value as number, delta as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be either `null` or `undefined`. * * Negates: {@link Matcher.toBeDefined `toBeDefined()`} */ toBeDefined(): Matcher<null | undefined> { return this._push((e) => e.toBeDefined()) } /* ------------------------------------------------------------------------ */ /** * Expects the value _**NOT**_ to be an instance of the specified * {@link Constructor}. * * Negates: {@link Matcher.toBeInstanceOf `toBeInstanceOf(...)`} */ toBeInstanceOf(constructor: Constructor): Matcher<T> { return this._push((e) => e.toBeInstanceOf(constructor)) } /* ------------------------------------------------------------------------ */ /** * Expects the value _**NOT**_ to be `NaN`. * * Negates: {@link Matcher.toBeNaN `toBeNaN()`} */ toBeNaN(): Matcher<number> { return this._push((e) => e.toBeNaN()) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `number` _**OUTSIDE**_ of the specified range * where minimum and maximum values are inclusive. * * Negates: {@link Matcher.toBeWithinRange `toBeWithinRange(...)`} */ toBeWithinRange(min: number, max: number): Matcher<number> /** * Expects the value to be a `bigint` _**OUTSIDE**_ of the specified range * where minimum and maximum values are inclusive. * * Negates: {@link Matcher.toBeWithinRange `toBeWithinRange(...)`} */ toBeWithinRange(min: bigint, max: bigint): Matcher<bigint> toBeWithinRange(min: number | bigint, max: number | bigint): Matcher { return this._push((e) => e.toBeWithinRange(min as number, max as number)) } /* ------------------------------------------------------------------------ */ /** * Expects the value _**NOT**_ to be _deep equal to_ the specified expected * one. * * Negates: {@link Matcher.toEqual `toEqual(...)`} */ toEqual(expected: any): Matcher<T> { return this._push((e) => e.toEqual(expected)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to have a `number` _property_ `length` _different_ from * the specified expected value. * * Negates: {@link Matcher.toHaveLength `toHaveLength(...)`} */ toHaveLength(length: number): Matcher<T & { length: number }> { return this._push((e) => e.toHaveLength(length)) } /* ------------------------------------------------------------------------ */ /** * Expects the value _**NOT**_ to have the specified _property_. * * Negates: {@link Matcher.toHaveProperty `toHaveProperty(...)`} */ toHaveProperty(property: string | number | symbol): Matcher<T> { return this._push((e) => e.toHaveProperty(property)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to have a `number` _property_ `size` _different_ from * the specified expected value. * * Negates: {@link Matcher.toHaveSize `toHaveSize(...)`} */ toHaveSize(size: number): Matcher<T & { size: number }> { return this._push((e) => e.toHaveSize(size)) } /* ------------------------------------------------------------------------ */ /** * Expect the value to include _none_ of the properties from the specified * _object_. * * If the object being expected is a {@link Map}, the properties specified * here will be treated as _mappings_ for said {@link Map}. * * Negates: {@link Matcher.toInclude `toInclude(...)`} */ toInclude<P extends Record<string, any>>(properties: P): Matcher<T> /** * Expect the value to include _none_ of the mappings from the specified * {@link Map}. * * Negates: {@link Matcher.toInclude `toInclude(...)`} */ toInclude(mappings: Map<any, any>): Matcher<T> /** * Expect the value to be an {@link Iterable} object includind _none_ of the * values from the specified {@link Set}. * * Negates: {@link Matcher.toInclude `toInclude(...)`} */ toInclude(entries: Set<any>): Matcher<T> /** * Expect the value to be an {@link Iterable} object includind _none_ of the * values from the specified _array_. * * Negates: {@link Matcher.toInclude `toInclude(...)`} */ toInclude(values: any[]): Matcher<T> toInclude( contents: Record<string, any> | Map<any, any> | Set<any> | any[], ): Matcher { return this._push((e) => e.toInclude(contents)) } /* ------------------------------------------------------------------------ */ /** * Expects the value to be a `string` _**NOT MATCHING**_ the specified * sub-`string` or {@link RegExp}. * * Negates: {@link Matcher.toMatch `toMatch(...)`} */ toMatch(matcher: string | RegExp): Matcher<string> { return this._push((e) => e.toMatch(matcher)) } /* ------------------------------------------------------------------------ */ /** * Expects the value _**NOT**_ to be _strictly equal to_ the specified * expected one. * * Negates: {@link Matcher.toStrictlyEqual `toStrictlyEqual(...)`} */ toStrictlyEqual(expected: any): Matcher<T> { return this._push((e) => e.toStrictlyEqual(expected)) } }