fix(assertion-matchers): Refactor matcher implementations to consistently use customAssertion for improved consistency and clarity.
This commit is contained in:
		| @@ -1,5 +1,11 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-04-28 - 2.0.1 - fix(assertion-matchers) | ||||
| Refactor matcher implementations to consistently use customAssertion for improved consistency and clarity. | ||||
|  | ||||
| - Updated ArrayMatchers, BooleanMatchers, DateMatchers, FunctionMatchers, NumberMatchers, ObjectMatchers, StringMatchers, and TypeMatchers to use customAssertion directly. | ||||
| - Aligned Assertion class aliases to delegate to the namespaced matchers with the new customAssertion pattern. | ||||
|  | ||||
| ## 2025-04-28 - 2.0.0 - BREAKING CHANGE(docs) | ||||
| Update documentation and examples to unify async and sync assertions, add custom matcher guides, and update package configuration | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@push.rocks/smartexpect', | ||||
|   version: '2.0.0', | ||||
|   version: '2.0.1', | ||||
|   description: 'A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.' | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for array-specific matchers | ||||
| @@ -7,38 +8,65 @@ export class ArrayMatchers<T> { | ||||
|   constructor(private assertion: Assertion<T[]>) {} | ||||
|  | ||||
|   toBeArray() { | ||||
|     return this.assertion.toBeArray(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => Array.isArray(value), | ||||
|       `Expected value to be array` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toHaveLength(length: number) { | ||||
|     return this.assertion.toHaveLength(length); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as T[]).length === length, | ||||
|       `Expected array to have length ${length}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toContain(item: T) { | ||||
|     return this.assertion.toContain(item); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as T[]).includes(item), | ||||
|       `Expected array to contain ${JSON.stringify(item)}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toContainEqual(item: T) { | ||||
|     return this.assertion.toContainEqual(item); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as T[]).some((e) => plugins.fastDeepEqual(e, item)), | ||||
|       `Expected array to contain equal to ${JSON.stringify(item)}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toContainAll(items: T[]) { | ||||
|     return this.assertion.toContainAll(items); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => items.every((i) => (value as T[]).includes(i)), | ||||
|       `Expected array to contain all ${JSON.stringify(items)}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toExclude(item: T) { | ||||
|     return this.assertion.toExclude(item); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => !(value as T[]).includes(item), | ||||
|       `Expected array to exclude ${JSON.stringify(item)}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeEmptyArray() { | ||||
|     return this.assertion.toBeEmptyArray(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => Array.isArray(value) && (value as T[]).length === 0, | ||||
|       `Expected array to be empty` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toHaveLengthGreaterThan(length: number) { | ||||
|     return this.assertion.toHaveLengthGreaterThan(length); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as T[]).length > length, | ||||
|       `Expected array to have length greater than ${length}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toHaveLengthLessThan(length: number) { | ||||
|     return this.assertion.toHaveLengthLessThan(length); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as T[]).length < length, | ||||
|       `Expected array to have length less than ${length}` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,18 +7,30 @@ export class BooleanMatchers { | ||||
|   constructor(private assertion: Assertion<boolean>) {} | ||||
|  | ||||
|   toBeTrue() { | ||||
|     return this.assertion.toBeTrue(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v === true, | ||||
|       `Expected value to be true` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeFalse() { | ||||
|     return this.assertion.toBeFalse(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v === false, | ||||
|       `Expected value to be false` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeTruthy() { | ||||
|     return this.assertion.toBeTruthy(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => Boolean(v), | ||||
|       `Expected value to be truthy` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeFalsy() { | ||||
|     return this.assertion.toBeFalsy(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => !v, | ||||
|       `Expected value to be falsy` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,14 +7,23 @@ export class DateMatchers { | ||||
|   constructor(private assertion: Assertion<Date>) {} | ||||
|  | ||||
|   toBeDate() { | ||||
|     return this.assertion.toBeDate(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v instanceof Date, | ||||
|       `Expected value to be a Date instance` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeBeforeDate(date: Date) { | ||||
|     return this.assertion.toBeBeforeDate(date); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v instanceof Date && (v as Date).getTime() < date.getTime(), | ||||
|       `Expected date to be before ${date.toISOString()}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeAfterDate(date: Date) { | ||||
|     return this.assertion.toBeAfterDate(date); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v instanceof Date && (v as Date).getTime() > date.getTime(), | ||||
|       `Expected date to be after ${date.toISOString()}` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,6 +7,23 @@ export class FunctionMatchers { | ||||
|   constructor(private assertion: Assertion<Function>) {} | ||||
|  | ||||
|   toThrow(expectedError?: any) { | ||||
|     return this.assertion.toThrow(expectedError); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => { | ||||
|         let threw = false; | ||||
|         try { | ||||
|           (value as Function)(); | ||||
|         } catch (e: any) { | ||||
|           threw = true; | ||||
|           if (expectedError) { | ||||
|             if (typeof expectedError === 'function') { | ||||
|               return e instanceof expectedError; | ||||
|             } | ||||
|             return e === expectedError; | ||||
|           } | ||||
|         } | ||||
|         return threw; | ||||
|       }, | ||||
|       `Expected function to throw${expectedError ? ` ${expectedError}` : ''}` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,26 +7,50 @@ export class NumberMatchers { | ||||
|   constructor(private assertion: Assertion<number>) {} | ||||
|  | ||||
|   toBeGreaterThan(value: number) { | ||||
|     return this.assertion.toBeGreaterThan(value); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => (v as number) > value, | ||||
|       `Expected number to be greater than ${value}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeLessThan(value: number) { | ||||
|     return this.assertion.toBeLessThan(value); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => (v as number) < value, | ||||
|       `Expected number to be less than ${value}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeGreaterThanOrEqual(value: number) { | ||||
|     return this.assertion.toBeGreaterThanOrEqual(value); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => (v as number) >= value, | ||||
|       `Expected number to be greater than or equal to ${value}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeLessThanOrEqual(value: number) { | ||||
|     return this.assertion.toBeLessThanOrEqual(value); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => (v as number) <= value, | ||||
|       `Expected number to be less than or equal to ${value}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeCloseTo(value: number, precision?: number) { | ||||
|     return this.assertion.toBeCloseTo(value, precision); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => { | ||||
|         const num = v as number; | ||||
|         const p = precision !== undefined ? precision : 2; | ||||
|         const diff = Math.abs(num - value); | ||||
|         const tolerance = 0.5 * Math.pow(10, -p); | ||||
|         return diff <= tolerance; | ||||
|       }, | ||||
|       `Expected number to be close to ${value} within precision ${precision ?? 2}` | ||||
|     ); | ||||
|   } | ||||
|   /** Equality check for numbers */ | ||||
|   toEqual(value: number) { | ||||
|     return this.assertion.toEqual(value); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => (v as number) === value, | ||||
|       `Expected number to equal ${value}` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { Assertion } from '../smartexpect.classes.assertion.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| /** | ||||
|  * Namespace for object-specific matchers | ||||
| @@ -7,33 +8,82 @@ export class ObjectMatchers<T extends object> { | ||||
|   constructor(private assertion: Assertion<T>) {} | ||||
|  | ||||
|   toEqual(expected: any) { | ||||
|     return this.assertion.toEqual(expected); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => plugins.fastDeepEqual(v, expected), | ||||
|       `Expected objects to be deeply equal to ${JSON.stringify(expected)}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toMatchObject(expected: object) { | ||||
|     return this.assertion.toMatchObject(expected); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => { | ||||
|         for (const key of Object.keys(expected)) { | ||||
|           if (!plugins.fastDeepEqual((v as any)[key], (expected as any)[key])) { | ||||
|             return false; | ||||
|           } | ||||
|         } | ||||
|         return true; | ||||
|       }, | ||||
|       `Expected object to match properties ${JSON.stringify(expected)}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeInstanceOf(constructor: any) { | ||||
|     return this.assertion.toBeInstanceOf(constructor); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => (v as any) instanceof constructor, | ||||
|       `Expected object to be instance of ${constructor.name || constructor}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toHaveProperty(property: string, value?: any) { | ||||
|     return this.assertion.toHaveProperty(property, value); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => { | ||||
|         const obj = v as any; | ||||
|         if (!(property in obj)) { | ||||
|           return false; | ||||
|         } | ||||
|         if (arguments.length === 2) { | ||||
|           return plugins.fastDeepEqual(obj[property], value); | ||||
|         } | ||||
|         return true; | ||||
|       }, | ||||
|       `Expected object to have property ${property}${value !== undefined ? ` with value ${JSON.stringify(value)}` : ''}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toHaveDeepProperty(path: string[]) { | ||||
|     return this.assertion.toHaveDeepProperty(path); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => { | ||||
|         let obj: any = v; | ||||
|         for (const key of path) { | ||||
|           if (obj == null || !(key in obj)) { | ||||
|             return false; | ||||
|           } | ||||
|           obj = obj[key]; | ||||
|         } | ||||
|         return true; | ||||
|       }, | ||||
|       `Expected object to have deep property path ${JSON.stringify(path)}` | ||||
|     ); | ||||
|   } | ||||
|   toBeNull() { | ||||
|     return this.assertion.toBeNull(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v === null, | ||||
|       `Expected value to be null` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeUndefined() { | ||||
|     return this.assertion.toBeUndefined(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v === undefined, | ||||
|       `Expected value to be undefined` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeNullOrUndefined() { | ||||
|     return this.assertion.toBeNullOrUndefined(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v === null || v === undefined, | ||||
|       `Expected value to be null or undefined` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,26 +7,44 @@ export class StringMatchers { | ||||
|   constructor(private assertion: Assertion<string>) {} | ||||
|  | ||||
|   toStartWith(prefix: string) { | ||||
|     return this.assertion.toStartWith(prefix); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as string).startsWith(prefix), | ||||
|       `Expected string to start with "${prefix}"` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toEndWith(suffix: string) { | ||||
|     return this.assertion.toEndWith(suffix); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as string).endsWith(suffix), | ||||
|       `Expected string to end with "${suffix}"` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toInclude(substring: string) { | ||||
|     return this.assertion.toInclude(substring); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as string).includes(substring), | ||||
|       `Expected string to include "${substring}"` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toMatch(regex: RegExp) { | ||||
|     return this.assertion.toMatch(regex); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => regex.test(value as string), | ||||
|       `Expected string to match ${regex}` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeOneOf(values: string[]) { | ||||
|     return this.assertion.toBeOneOf(values); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (values as string[]).includes(value as string), | ||||
|       `Expected string to be one of ${JSON.stringify(values)}` | ||||
|     ); | ||||
|   } | ||||
|   /** Length check for strings */ | ||||
|   toHaveLength(length: number) { | ||||
|     return this.assertion.toHaveLength(length); | ||||
|     return this.assertion.customAssertion( | ||||
|       (value) => (value as string).length === length, | ||||
|       `Expected string to have length ${length}` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,22 +7,37 @@ export class TypeMatchers { | ||||
|   constructor(private assertion: Assertion<any>) {} | ||||
|  | ||||
|   toBeTypeofString() { | ||||
|     return this.assertion.toBeTypeofString(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => typeof v === 'string', | ||||
|       `Expected type to be 'string'` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeTypeofNumber() { | ||||
|     return this.assertion.toBeTypeofNumber(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => typeof v === 'number', | ||||
|       `Expected type to be 'number'` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeTypeofBoolean() { | ||||
|     return this.assertion.toBeTypeofBoolean(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => typeof v === 'boolean', | ||||
|       `Expected type to be 'boolean'` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeTypeOf(typeName: string) { | ||||
|     return this.assertion.toBeTypeOf(typeName); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => typeof v === typeName, | ||||
|       `Expected type to be '${typeName}'` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   toBeDefined() { | ||||
|     return this.assertion.toBeDefined(); | ||||
|     return this.assertion.customAssertion( | ||||
|       (v) => v !== undefined, | ||||
|       `Expected value to be defined` | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -288,6 +288,49 @@ export class Assertion<T = unknown> { | ||||
|     console.log(`Path: ${this.formatDrillDown() || '(root)'}`); | ||||
|     return this; | ||||
|   } | ||||
|   // Direct (flat) matcher aliases | ||||
|   public toEqual(expected: any) { | ||||
|     return this.customAssertion( | ||||
|       (v) => plugins.fastDeepEqual(v, expected), | ||||
|       `Expected value to equal ${JSON.stringify(expected)}` | ||||
|     ); | ||||
|   } | ||||
|   public toBeTrue() { return this.boolean.toBeTrue(); } | ||||
|   public toBeFalse() { return this.boolean.toBeFalse(); } | ||||
|   public toBeTruthy() { return this.boolean.toBeTruthy(); } | ||||
|   public toBeFalsy() { return this.boolean.toBeFalsy(); } | ||||
|   public toThrow(expectedError?: any) { return this.function.toThrow(expectedError); } | ||||
|   public toBeGreaterThan(value: number) { return this.number.toBeGreaterThan(value); } | ||||
|   public toBeLessThan(value: number) { return this.number.toBeLessThan(value); } | ||||
|   public toBeGreaterThanOrEqual(value: number) { return this.number.toBeGreaterThanOrEqual(value); } | ||||
|   public toBeLessThanOrEqual(value: number) { return this.number.toBeLessThanOrEqual(value); } | ||||
|   public toBeCloseTo(value: number, precision?: number) { return this.number.toBeCloseTo(value, precision); } | ||||
|   public toBeArray() { return this.array.toBeArray(); } | ||||
|   public toContain(item: any) { return this.array.toContain(item); } | ||||
|   public toContainEqual(item: any) { return this.array.toContainEqual(item); } | ||||
|   public toContainAll(items: any[]) { return this.array.toContainAll(items); } | ||||
|   public toExclude(item: any) { return this.array.toExclude(item); } | ||||
|   public toBeEmptyArray() { return this.array.toBeEmptyArray(); } | ||||
|   public toStartWith(prefix: string) { return this.string.toStartWith(prefix); } | ||||
|   public toEndWith(suffix: string) { return this.string.toEndWith(suffix); } | ||||
|   public toInclude(substring: string) { return this.string.toInclude(substring); } | ||||
|   public toMatch(regex: RegExp) { return this.string.toMatch(regex); } | ||||
|   public toBeOneOf(values: any[]) { return this.string.toBeOneOf(values as string[]); } | ||||
|   public toHaveProperty(property: string, value?: any) { return this.object.toHaveProperty(property, value); } | ||||
|   public toMatchObject(expected: object) { return this.object.toMatchObject(expected); } | ||||
|   public toBeInstanceOf(constructor: any) { return this.object.toBeInstanceOf(constructor); } | ||||
|   public toHaveDeepProperty(path: string[]) { return this.object.toHaveDeepProperty(path); } | ||||
|   public toBeNull() { return this.object.toBeNull(); } | ||||
|   public toBeUndefined() { return this.object.toBeUndefined(); } | ||||
|   public toBeNullOrUndefined() { return this.object.toBeNullOrUndefined(); } | ||||
|   public toBeDate() { return this.date.toBeDate(); } | ||||
|   public toBeBeforeDate(date: Date) { return this.date.toBeBeforeDate(date); } | ||||
|   public toBeAfterDate(date: Date) { return this.date.toBeAfterDate(date); } | ||||
|   public toBeTypeofString() { return this.type.toBeTypeofString(); } | ||||
|   public toBeTypeofNumber() { return this.type.toBeTypeofNumber(); } | ||||
|   public toBeTypeofBoolean() { return this.type.toBeTypeofBoolean(); } | ||||
|   public toBeTypeOf(typeName: string) { return this.type.toBeTypeOf(typeName); } | ||||
|   public toBeDefined() { return this.type.toBeDefined(); } | ||||
|   // Namespaced matcher accessors | ||||
|   /** String-specific matchers */ | ||||
|   public get string() { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user