feat(generics): Improve assertion and matcher type definitions by adding execution mode generics for better async/sync support
This commit is contained in:
parent
81bd8bfb13
commit
8cb70b6afe
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-04-29 - 2.1.2 - fix(ts/index.ts)
|
||||||
Remove deprecated expectAsync function and advise using .resolves/.rejects on expect for async assertions
|
Remove deprecated expectAsync function and advise using .resolves/.rejects on expect for async assertions
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartexpect',
|
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.'
|
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 { Assertion, AnyMatcher, AnythingMatcher } from './smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from './types.js';
|
||||||
// import type { TMatcher } from './smartexpect.classes.assertion.js'; // unused
|
// 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.
|
* Entry point for assertions.
|
||||||
* Automatically detects Promises to support async assertions.
|
* Automatically detects Promises to support async assertions.
|
||||||
*/
|
*/
|
||||||
export function expect<T>(value: Promise<T>): Assertion<T>;
|
export function expect<T>(value: Promise<T>): Assertion<T, 'async'>;
|
||||||
export function expect<T>(value: T): Assertion<T>;
|
export function expect<T>(value: T): Assertion<T, 'sync'>;
|
||||||
export function expect<T>(value: any): Assertion<T> {
|
export function expect<T>(value: any): Assertion<T, TExecutionType> {
|
||||||
const isThenable = value != null && typeof (value as any).then === 'function';
|
const isThenable = value != null && typeof (value as any).then === 'function';
|
||||||
const mode: 'sync' | 'async' = isThenable ? 'async' : 'sync';
|
const mode: 'sync' | 'async' = isThenable ? 'async' : 'sync';
|
||||||
return new Assertion<T>(value, mode);
|
return new Assertion<T, TExecutionType>(value, mode);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Register custom matchers.
|
* Register custom matchers.
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for array-specific matchers
|
* Namespace for array-specific matchers
|
||||||
*/
|
*/
|
||||||
export class ArrayMatchers<T> {
|
export class ArrayMatchers<T, M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<T[]>) {}
|
constructor(private assertion: Assertion<T[], M>) {}
|
||||||
|
|
||||||
toBeArray() {
|
toBeArray() {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for boolean-specific matchers
|
* Namespace for boolean-specific matchers
|
||||||
*/
|
*/
|
||||||
export class BooleanMatchers {
|
export class BooleanMatchers<M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<boolean>) {}
|
constructor(private assertion: Assertion<boolean, M>) {}
|
||||||
|
|
||||||
toBeTrue() {
|
toBeTrue() {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for date-specific matchers
|
* Namespace for date-specific matchers
|
||||||
*/
|
*/
|
||||||
export class DateMatchers {
|
export class DateMatchers<M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<Date>) {}
|
constructor(private assertion: Assertion<Date, M>) {}
|
||||||
|
|
||||||
toBeDate() {
|
toBeDate() {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for function-specific matchers
|
* Namespace for function-specific matchers
|
||||||
*/
|
*/
|
||||||
export class FunctionMatchers {
|
export class FunctionMatchers<M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<Function>) {}
|
constructor(private assertion: Assertion<Function, M>) {}
|
||||||
|
|
||||||
toThrow(expectedError?: any) {
|
toThrow(expectedError?: any) {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for number-specific matchers
|
* Namespace for number-specific matchers
|
||||||
*/
|
*/
|
||||||
export class NumberMatchers {
|
export class NumberMatchers<M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<number>) {}
|
constructor(private assertion: Assertion<number, M>) {}
|
||||||
|
|
||||||
toBeGreaterThan(value: number) {
|
toBeGreaterThan(value: number) {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Assertion, AnyMatcher, AnythingMatcher } from '../smartexpect.classes.assertion.js';
|
import { Assertion, AnyMatcher, AnythingMatcher } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for object-specific matchers
|
* Namespace for object-specific matchers
|
||||||
*/
|
*/
|
||||||
export class ObjectMatchers<T extends object> {
|
export class ObjectMatchers<T extends object, M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<T>) {}
|
constructor(private assertion: Assertion<T, M>) {}
|
||||||
|
|
||||||
toEqual(expected: any) {
|
toEqual(expected: any) {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for string-specific matchers
|
* Namespace for string-specific matchers
|
||||||
*/
|
*/
|
||||||
export class StringMatchers {
|
export class StringMatchers<M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<string>) {}
|
constructor(private assertion: Assertion<string, M>) {}
|
||||||
|
|
||||||
toStartWith(prefix: string) {
|
toStartWith(prefix: string) {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Namespace for type-based matchers
|
* Namespace for type-based matchers
|
||||||
*/
|
*/
|
||||||
export class TypeMatchers {
|
export class TypeMatchers<M extends TExecutionType> {
|
||||||
constructor(private assertion: Assertion<any>) {}
|
constructor(private assertion: Assertion<any, M>) {}
|
||||||
|
|
||||||
toBeTypeofString() {
|
toBeTypeofString() {
|
||||||
return this.assertion.customAssertion(
|
return this.assertion.customAssertion(
|
||||||
|
@ -26,8 +26,8 @@ export class AnyMatcher {
|
|||||||
}
|
}
|
||||||
export class AnythingMatcher {}
|
export class AnythingMatcher {}
|
||||||
|
|
||||||
export class Assertion<T = unknown> {
|
export class Assertion<T = unknown, M extends TExecutionType = 'sync'> {
|
||||||
executionMode: TExecutionType;
|
executionMode: M;
|
||||||
baseReference: any;
|
baseReference: any;
|
||||||
propertyDrillDown: Array<string | number> = [];
|
propertyDrillDown: Array<string | number> = [];
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ export class Assertion<T = unknown> {
|
|||||||
/** Computed negation failure message for the current assertion */
|
/** Computed negation failure message for the current assertion */
|
||||||
private negativeMessage: string;
|
private negativeMessage: string;
|
||||||
|
|
||||||
constructor(baseReferenceArg: any, executionModeArg: TExecutionType) {
|
constructor(baseReferenceArg: any, executionModeArg: M) {
|
||||||
this.baseReference = baseReferenceArg;
|
this.baseReference = baseReferenceArg;
|
||||||
this.executionMode = executionModeArg;
|
this.executionMode = executionModeArg;
|
||||||
}
|
}
|
||||||
@ -159,20 +159,23 @@ export class Assertion<T = unknown> {
|
|||||||
/**
|
/**
|
||||||
* Assert that a Promise resolves.
|
* Assert that a Promise resolves.
|
||||||
*/
|
*/
|
||||||
public get resolves(): this {
|
/**
|
||||||
this.isResolves = true;
|
* Switch to async (resolve) mode. Subsequent matchers return Promises.
|
||||||
this.isRejects = false;
|
*/
|
||||||
this.executionMode = 'async';
|
public get resolves(): Assertion<T, 'async'> {
|
||||||
return this;
|
return new Assertion<T, 'async'>(this.baseReference, 'async');
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Assert that a Promise rejects.
|
* Assert that a Promise rejects.
|
||||||
*/
|
*/
|
||||||
public get rejects(): this {
|
/**
|
||||||
this.isRejects = true;
|
* Switch to async (reject) mode. Subsequent matchers return Promises.
|
||||||
this.isResolves = false;
|
*/
|
||||||
this.executionMode = 'async';
|
public get rejects(): Assertion<T, 'async'> {
|
||||||
return this;
|
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;
|
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) => {
|
const runDirectOrNegated = (checkFunction: () => any) => {
|
||||||
if (!this.notSetting) {
|
if (!this.notSetting) {
|
||||||
return checkFunction();
|
return checkFunction();
|
||||||
@ -223,7 +228,7 @@ export class Assertion<T = unknown> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.executionMode === 'async') {
|
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';
|
const isThenable = this.baseReference && typeof (this.baseReference as any).then === 'function';
|
||||||
if (!isThenable) {
|
if (!isThenable) {
|
||||||
done.reject(new Error(`Expected a Promise but received: ${this.formatValue(this.baseReference)}`));
|
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 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
|
// sync: run and return this for chaining
|
||||||
runDirectOrNegated(checkFunction);
|
runDirectOrNegated(checkFunction);
|
||||||
return this;
|
return this as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a custom assertion. Returns a Promise in async mode, else returns this.
|
||||||
|
*/
|
||||||
public customAssertion(
|
public customAssertion(
|
||||||
assertionFunction: (value: any) => boolean,
|
assertionFunction: (value: any) => boolean,
|
||||||
errorMessage: string | ((value: any) => string)
|
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
|
// Prepare negation message based on the positive error template, if static
|
||||||
if (typeof errorMessage === 'string') {
|
if (typeof errorMessage === 'string') {
|
||||||
this.negativeMessage = this.computeNegationMessage(errorMessage);
|
this.negativeMessage = this.computeNegationMessage(errorMessage);
|
||||||
@ -290,7 +298,7 @@ export class Assertion<T = unknown> {
|
|||||||
|| (typeof errorMessage === 'function' ? errorMessage(value) : errorMessage);
|
|| (typeof errorMessage === 'function' ? errorMessage(value) : errorMessage);
|
||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
});
|
}) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -298,9 +306,9 @@ export class Assertion<T = unknown> {
|
|||||||
* @param propertyName Name of the property to navigate into.
|
* @param propertyName Name of the property to navigate into.
|
||||||
* @returns Assertion of the property type.
|
* @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);
|
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.
|
* @param index Index of the array item.
|
||||||
* @returns Assertion of the element type.
|
* @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);
|
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() {
|
public log() {
|
||||||
@ -365,35 +373,35 @@ export class Assertion<T = unknown> {
|
|||||||
|
|
||||||
// Namespaced matcher accessors
|
// Namespaced matcher accessors
|
||||||
/** String-specific matchers */
|
/** String-specific matchers */
|
||||||
public get string() {
|
public get string(): StringMatchers<M> {
|
||||||
return new StringMatchers(this as Assertion<string>);
|
return new StringMatchers<M>(this as Assertion<string, M>);
|
||||||
}
|
}
|
||||||
/** Array-specific matchers */
|
/** Array-specific matchers */
|
||||||
public get array() {
|
public get array(): ArrayMatchers<any, M> {
|
||||||
return new ArrayMatchers<any>(this as Assertion<any[]>);
|
return new ArrayMatchers<any, M>(this as Assertion<any[], M>);
|
||||||
}
|
}
|
||||||
/** Number-specific matchers */
|
/** Number-specific matchers */
|
||||||
public get number() {
|
public get number(): NumberMatchers<M> {
|
||||||
return new NumberMatchers(this as Assertion<number>);
|
return new NumberMatchers<M>(this as Assertion<number, M>);
|
||||||
}
|
}
|
||||||
/** Boolean-specific matchers */
|
/** Boolean-specific matchers */
|
||||||
public get boolean() {
|
public get boolean(): BooleanMatchers<M> {
|
||||||
return new BooleanMatchers(this as Assertion<boolean>);
|
return new BooleanMatchers<M>(this as Assertion<boolean, M>);
|
||||||
}
|
}
|
||||||
/** Object-specific matchers */
|
/** Object-specific matchers */
|
||||||
public get object() {
|
public get object(): ObjectMatchers<any, M> {
|
||||||
return new ObjectMatchers<any>(this as Assertion<object>);
|
return new ObjectMatchers<any, M>(this as Assertion<object, M>);
|
||||||
}
|
}
|
||||||
/** Function-specific matchers */
|
/** Function-specific matchers */
|
||||||
public get function() {
|
public get function(): FunctionMatchers<M> {
|
||||||
return new FunctionMatchers(this as Assertion<Function>);
|
return new FunctionMatchers<M>(this as Assertion<Function, M>);
|
||||||
}
|
}
|
||||||
/** Date-specific matchers */
|
/** Date-specific matchers */
|
||||||
public get date() {
|
public get date(): DateMatchers<M> {
|
||||||
return new DateMatchers(this as Assertion<Date>);
|
return new DateMatchers<M>(this as Assertion<Date, M>);
|
||||||
}
|
}
|
||||||
/** Type-based matchers */
|
/** Type-based matchers */
|
||||||
public get type() {
|
public get type(): TypeMatchers<M> {
|
||||||
return new TypeMatchers(this as Assertion<any>);
|
return new TypeMatchers<M>(this as Assertion<any, M>);
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user