feat(generics): Improve assertion and matcher type definitions by adding execution mode generics for better async/sync support
This commit is contained in:
		| @@ -1,5 +1,13 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-04-29 - 2.2.0 - feat(generics) | ||||
| Improve assertion and matcher type definitions by adding execution mode generics for better async/sync support | ||||
|  | ||||
| - Updated ts/index.ts to import and use TExecutionType in the expect function | ||||
| - Modified Assertion class to use a generic execution mode (M) for improved type inference | ||||
| - Revised all matcher namespaces (array, boolean, date, function, number, object, string, type) to accept the new generic parameter | ||||
| - Enhanced async/sync distinction for assertion methods like resolves and rejects | ||||
|  | ||||
| ## 2025-04-29 - 2.1.2 - fix(ts/index.ts) | ||||
| Remove deprecated expectAsync function and advise using .resolves/.rejects on expect for async assertions | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartexpect', | ||||
|   version: '2.1.2', | ||||
|   version: '2.2.0', | ||||
|   description: 'A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.' | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { Assertion, AnyMatcher, AnythingMatcher } from './smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from './types.js'; | ||||
| // import type { TMatcher } from './smartexpect.classes.assertion.js'; // unused | ||||
|  | ||||
| /** | ||||
| @@ -12,12 +13,12 @@ import { Assertion, AnyMatcher, AnythingMatcher } from './smartexpect.classes.as | ||||
|  * Entry point for assertions.  | ||||
|  * Automatically detects Promises to support async assertions. | ||||
|  */ | ||||
| export function expect<T>(value: Promise<T>): Assertion<T>; | ||||
| export function expect<T>(value: T): Assertion<T>; | ||||
| export function expect<T>(value: any): Assertion<T> { | ||||
| export function expect<T>(value: Promise<T>): Assertion<T, 'async'>; | ||||
| export function expect<T>(value: T): Assertion<T, 'sync'>; | ||||
| export function expect<T>(value: any): Assertion<T, TExecutionType> { | ||||
|   const isThenable = value != null && typeof (value as any).then === 'function'; | ||||
|   const mode: 'sync' | 'async' = isThenable ? 'async' : 'sync'; | ||||
|   return new Assertion<T>(value, mode); | ||||
|   return new Assertion<T, TExecutionType>(value, mode); | ||||
| } | ||||
| /** | ||||
|  * Register custom matchers. | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for array-specific matchers | ||||
|  */ | ||||
| export class ArrayMatchers<T> { | ||||
|   constructor(private assertion: Assertion<T[]>) {} | ||||
| export class ArrayMatchers<T, M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<T[], M>) {} | ||||
|  | ||||
|   toBeArray() { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for boolean-specific matchers | ||||
|  */ | ||||
| export class BooleanMatchers { | ||||
|   constructor(private assertion: Assertion<boolean>) {} | ||||
| export class BooleanMatchers<M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<boolean, M>) {} | ||||
|  | ||||
|   toBeTrue() { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for date-specific matchers | ||||
|  */ | ||||
| export class DateMatchers { | ||||
|   constructor(private assertion: Assertion<Date>) {} | ||||
| export class DateMatchers<M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<Date, M>) {} | ||||
|  | ||||
|   toBeDate() { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for function-specific matchers | ||||
|  */ | ||||
| export class FunctionMatchers { | ||||
|   constructor(private assertion: Assertion<Function>) {} | ||||
| export class FunctionMatchers<M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<Function, M>) {} | ||||
|  | ||||
|   toThrow(expectedError?: any) { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for number-specific matchers | ||||
|  */ | ||||
| export class NumberMatchers { | ||||
|   constructor(private assertion: Assertion<number>) {} | ||||
| export class NumberMatchers<M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<number, M>) {} | ||||
|  | ||||
|   toBeGreaterThan(value: number) { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| import { Assertion, AnyMatcher, AnythingMatcher } from '../smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for object-specific matchers | ||||
|  */ | ||||
| export class ObjectMatchers<T extends object> { | ||||
|   constructor(private assertion: Assertion<T>) {} | ||||
| export class ObjectMatchers<T extends object, M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<T, M>) {} | ||||
|  | ||||
|   toEqual(expected: any) { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for string-specific matchers | ||||
|  */ | ||||
| export class StringMatchers { | ||||
|   constructor(private assertion: Assertion<string>) {} | ||||
| export class StringMatchers<M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<string, M>) {} | ||||
|  | ||||
|   toStartWith(prefix: string) { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import type { TExecutionType } from '../types.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for type-based matchers | ||||
|  */ | ||||
| export class TypeMatchers { | ||||
|   constructor(private assertion: Assertion<any>) {} | ||||
| export class TypeMatchers<M extends TExecutionType> { | ||||
|   constructor(private assertion: Assertion<any, M>) {} | ||||
|  | ||||
|   toBeTypeofString() { | ||||
|     return this.assertion.customAssertion( | ||||
|   | ||||
| @@ -26,8 +26,8 @@ export class AnyMatcher { | ||||
| } | ||||
| export class AnythingMatcher {} | ||||
|  | ||||
| export class Assertion<T = unknown> { | ||||
|   executionMode: TExecutionType; | ||||
| export class Assertion<T = unknown, M extends TExecutionType = 'sync'> { | ||||
|   executionMode: M; | ||||
|   baseReference: any; | ||||
|   propertyDrillDown: Array<string | number> = []; | ||||
|  | ||||
| @@ -44,7 +44,7 @@ export class Assertion<T = unknown> { | ||||
|   /** Computed negation failure message for the current assertion */ | ||||
|   private negativeMessage: string; | ||||
|  | ||||
|   constructor(baseReferenceArg: any, executionModeArg: TExecutionType) { | ||||
|   constructor(baseReferenceArg: any, executionModeArg: M) { | ||||
|     this.baseReference = baseReferenceArg; | ||||
|     this.executionMode = executionModeArg; | ||||
|   } | ||||
| @@ -159,20 +159,23 @@ export class Assertion<T = unknown> { | ||||
|   /** | ||||
|    * Assert that a Promise resolves. | ||||
|    */ | ||||
|   public get resolves(): this { | ||||
|     this.isResolves = true; | ||||
|     this.isRejects = false; | ||||
|     this.executionMode = 'async'; | ||||
|     return this; | ||||
|   /** | ||||
|    * Switch to async (resolve) mode. Subsequent matchers return Promises. | ||||
|    */ | ||||
|   public get resolves(): Assertion<T, 'async'> { | ||||
|     return new Assertion<T, 'async'>(this.baseReference, 'async'); | ||||
|   } | ||||
|   /** | ||||
|    * Assert that a Promise rejects. | ||||
|    */ | ||||
|   public get rejects(): this { | ||||
|     this.isRejects = true; | ||||
|     this.isResolves = false; | ||||
|     this.executionMode = 'async'; | ||||
|     return this; | ||||
|   /** | ||||
|    * Switch to async (reject) mode. Subsequent matchers return Promises. | ||||
|    */ | ||||
|   public get rejects(): Assertion<T, 'async'> { | ||||
|     const a = new Assertion<T, 'async'>(this.baseReference, 'async'); | ||||
|     // mark to expect rejection | ||||
|     (a as any).isRejects = true; | ||||
|     return a; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -203,7 +206,9 @@ export class Assertion<T = unknown> { | ||||
|     return this; | ||||
|   } | ||||
|  | ||||
|   private runCheck(checkFunction: () => any): Assertion<T> | Promise<Assertion<T>> { | ||||
|   // Internal check runner: returns Promise in async mode, else sync Assertion | ||||
|   // Internal check runner; returns Promise or this at runtime, but typed via customAssertion | ||||
|   private runCheck(checkFunction: () => any): any { | ||||
|     const runDirectOrNegated = (checkFunction: () => any) => { | ||||
|       if (!this.notSetting) { | ||||
|         return checkFunction(); | ||||
| @@ -223,7 +228,7 @@ export class Assertion<T = unknown> { | ||||
|     }; | ||||
|  | ||||
|     if (this.executionMode === 'async') { | ||||
|       const done = plugins.smartpromise.defer<Assertion<T>>(); | ||||
|       const done = plugins.smartpromise.defer<Assertion<T, M>>(); | ||||
|       const isThenable = this.baseReference && typeof (this.baseReference as any).then === 'function'; | ||||
|       if (!isThenable) { | ||||
|         done.reject(new Error(`Expected a Promise but received: ${this.formatValue(this.baseReference)}`)); | ||||
| @@ -268,17 +273,20 @@ export class Assertion<T = unknown> { | ||||
|         ); | ||||
|       } | ||||
|       // return a promise resolving to this for chaining | ||||
|       return done.promise.then(() => this); | ||||
|       return done.promise.then(() => this) as any; | ||||
|     } | ||||
|     // sync: run and return this for chaining | ||||
|     runDirectOrNegated(checkFunction); | ||||
|     return this; | ||||
|     return this as any; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Execute a custom assertion. Returns a Promise in async mode, else returns this. | ||||
|    */ | ||||
|   public customAssertion( | ||||
|     assertionFunction: (value: any) => boolean, | ||||
|     errorMessage: string | ((value: any) => string) | ||||
|   ): Assertion<T> | Promise<Assertion<T>> { | ||||
|   ): M extends 'async' ? Promise<Assertion<T, M>> : Assertion<T, M> { | ||||
|     // Prepare negation message based on the positive error template, if static | ||||
|     if (typeof errorMessage === 'string') { | ||||
|       this.negativeMessage = this.computeNegationMessage(errorMessage); | ||||
| @@ -290,7 +298,7 @@ export class Assertion<T = unknown> { | ||||
|           || (typeof errorMessage === 'function' ? errorMessage(value) : errorMessage); | ||||
|         throw new Error(msg); | ||||
|       } | ||||
|     }); | ||||
|     }) as any; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -298,9 +306,9 @@ export class Assertion<T = unknown> { | ||||
|    * @param propertyName Name of the property to navigate into. | ||||
|    * @returns Assertion of the property type. | ||||
|    */ | ||||
|   public property<K extends keyof NonNullable<T>>(propertyName: K): Assertion<NonNullable<T>[K]> { | ||||
|   public property<K extends keyof NonNullable<T>>(propertyName: K): Assertion<NonNullable<T>[K], M> { | ||||
|     this.propertyDrillDown.push(propertyName as string); | ||||
|     return this as unknown as Assertion<NonNullable<T>[K]>; | ||||
|     return this as unknown as Assertion<NonNullable<T>[K], M>; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -308,9 +316,9 @@ export class Assertion<T = unknown> { | ||||
|    * @param index Index of the array item. | ||||
|    * @returns Assertion of the element type. | ||||
|    */ | ||||
|   public arrayItem(index: number): Assertion<T extends Array<infer U> ? U : unknown> { | ||||
|   public arrayItem(index: number): Assertion<T extends Array<infer U> ? U : unknown, M> { | ||||
|     this.propertyDrillDown.push(index); | ||||
|     return this as unknown as Assertion<T extends Array<infer U> ? U : unknown>; | ||||
|     return this as unknown as Assertion<T extends Array<infer U> ? U : unknown, M>; | ||||
|   } | ||||
|  | ||||
|   public log() { | ||||
| @@ -365,35 +373,35 @@ export class Assertion<T = unknown> { | ||||
|    | ||||
|   // Namespaced matcher accessors | ||||
|   /** String-specific matchers */ | ||||
|   public get string() { | ||||
|     return new StringMatchers(this as Assertion<string>); | ||||
|   public get string(): StringMatchers<M> { | ||||
|     return new StringMatchers<M>(this as Assertion<string, M>); | ||||
|   } | ||||
|   /** Array-specific matchers */ | ||||
|   public get array() { | ||||
|     return new ArrayMatchers<any>(this as Assertion<any[]>); | ||||
|   public get array(): ArrayMatchers<any, M> { | ||||
|     return new ArrayMatchers<any, M>(this as Assertion<any[], M>); | ||||
|   } | ||||
|   /** Number-specific matchers */ | ||||
|   public get number() { | ||||
|     return new NumberMatchers(this as Assertion<number>); | ||||
|   public get number(): NumberMatchers<M> { | ||||
|     return new NumberMatchers<M>(this as Assertion<number, M>); | ||||
|   } | ||||
|   /** Boolean-specific matchers */ | ||||
|   public get boolean() { | ||||
|     return new BooleanMatchers(this as Assertion<boolean>); | ||||
|   public get boolean(): BooleanMatchers<M> { | ||||
|     return new BooleanMatchers<M>(this as Assertion<boolean, M>); | ||||
|   } | ||||
|   /** Object-specific matchers */ | ||||
|   public get object() { | ||||
|     return new ObjectMatchers<any>(this as Assertion<object>); | ||||
|   public get object(): ObjectMatchers<any, M> { | ||||
|     return new ObjectMatchers<any, M>(this as Assertion<object, M>); | ||||
|   } | ||||
|   /** Function-specific matchers */ | ||||
|   public get function() { | ||||
|     return new FunctionMatchers(this as Assertion<Function>); | ||||
|   public get function(): FunctionMatchers<M> { | ||||
|     return new FunctionMatchers<M>(this as Assertion<Function, M>); | ||||
|   } | ||||
|   /** Date-specific matchers */ | ||||
|   public get date() { | ||||
|     return new DateMatchers(this as Assertion<Date>); | ||||
|   public get date(): DateMatchers<M> { | ||||
|     return new DateMatchers<M>(this as Assertion<Date, M>); | ||||
|   } | ||||
|   /** Type-based matchers */ | ||||
|   public get type() { | ||||
|     return new TypeMatchers(this as Assertion<any>); | ||||
|   public get type(): TypeMatchers<M> { | ||||
|     return new TypeMatchers<M>(this as Assertion<any, M>); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user