diff --git a/changelog.md b/changelog.md index 3be13e8..c952098 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 561d48b..a3d68d1 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/namespaces/array.ts b/ts/namespaces/array.ts index 0b222b8..b8015a1 100644 --- a/ts/namespaces/array.ts +++ b/ts/namespaces/array.ts @@ -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 { constructor(private assertion: Assertion) {} 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}` + ); } } \ No newline at end of file diff --git a/ts/namespaces/boolean.ts b/ts/namespaces/boolean.ts index 017bccd..68b841c 100644 --- a/ts/namespaces/boolean.ts +++ b/ts/namespaces/boolean.ts @@ -7,18 +7,30 @@ export class BooleanMatchers { constructor(private assertion: Assertion) {} 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` + ); } } \ No newline at end of file diff --git a/ts/namespaces/date.ts b/ts/namespaces/date.ts index 67cb009..a525070 100644 --- a/ts/namespaces/date.ts +++ b/ts/namespaces/date.ts @@ -7,14 +7,23 @@ export class DateMatchers { constructor(private assertion: Assertion) {} 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()}` + ); } } \ No newline at end of file diff --git a/ts/namespaces/function.ts b/ts/namespaces/function.ts index e5931ff..5dd7b96 100644 --- a/ts/namespaces/function.ts +++ b/ts/namespaces/function.ts @@ -7,6 +7,23 @@ export class FunctionMatchers { constructor(private assertion: Assertion) {} 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}` : ''}` + ); } } \ No newline at end of file diff --git a/ts/namespaces/number.ts b/ts/namespaces/number.ts index 219e1d4..378d3fb 100644 --- a/ts/namespaces/number.ts +++ b/ts/namespaces/number.ts @@ -7,26 +7,50 @@ export class NumberMatchers { constructor(private assertion: Assertion) {} 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}` + ); } } \ No newline at end of file diff --git a/ts/namespaces/object.ts b/ts/namespaces/object.ts index 341ace8..5bb2715 100644 --- a/ts/namespaces/object.ts +++ b/ts/namespaces/object.ts @@ -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 { constructor(private assertion: Assertion) {} 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` + ); } } \ No newline at end of file diff --git a/ts/namespaces/string.ts b/ts/namespaces/string.ts index 174ece4..33387ff 100644 --- a/ts/namespaces/string.ts +++ b/ts/namespaces/string.ts @@ -7,26 +7,44 @@ export class StringMatchers { constructor(private assertion: Assertion) {} 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}` + ); } } \ No newline at end of file diff --git a/ts/namespaces/type.ts b/ts/namespaces/type.ts index db533b4..f7e3b09 100644 --- a/ts/namespaces/type.ts +++ b/ts/namespaces/type.ts @@ -7,22 +7,37 @@ export class TypeMatchers { constructor(private assertion: Assertion) {} 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` + ); } } \ No newline at end of file diff --git a/ts/smartexpect.classes.assertion.ts b/ts/smartexpect.classes.assertion.ts index 89c05e1..3bc639e 100644 --- a/ts/smartexpect.classes.assertion.ts +++ b/ts/smartexpect.classes.assertion.ts @@ -288,6 +288,49 @@ export class Assertion { 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() {