fix(assertion-matchers): Refactor matcher implementations to consistently use customAssertion for improved consistency and clarity.

This commit is contained in:
Philipp Kunz 2025-04-28 19:58:32 +00:00
parent 4eac4544a5
commit 91a3dc43d3
11 changed files with 265 additions and 43 deletions

View File

@ -1,5 +1,11 @@
# Changelog # 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) ## 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 Update documentation and examples to unify async and sync assertions, add custom matcher guides, and update package configuration

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartexpect', 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.' description: 'A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.'
} }

View File

@ -1,4 +1,5 @@
import { Assertion } from '../smartexpect.classes.assertion.js'; import { Assertion } from '../smartexpect.classes.assertion.js';
import * as plugins from '../plugins.js';
/** /**
* Namespace for array-specific matchers * Namespace for array-specific matchers
@ -7,38 +8,65 @@ export class ArrayMatchers<T> {
constructor(private assertion: Assertion<T[]>) {} constructor(private assertion: Assertion<T[]>) {}
toBeArray() { toBeArray() {
return this.assertion.toBeArray(); return this.assertion.customAssertion(
(value) => Array.isArray(value),
`Expected value to be array`
);
} }
toHaveLength(length: number) { 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) { 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) { 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[]) { 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) { 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() { 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) { 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) { 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}`
);
} }
} }

View File

@ -7,18 +7,30 @@ export class BooleanMatchers {
constructor(private assertion: Assertion<boolean>) {} constructor(private assertion: Assertion<boolean>) {}
toBeTrue() { toBeTrue() {
return this.assertion.toBeTrue(); return this.assertion.customAssertion(
(v) => v === true,
`Expected value to be true`
);
} }
toBeFalse() { toBeFalse() {
return this.assertion.toBeFalse(); return this.assertion.customAssertion(
(v) => v === false,
`Expected value to be false`
);
} }
toBeTruthy() { toBeTruthy() {
return this.assertion.toBeTruthy(); return this.assertion.customAssertion(
(v) => Boolean(v),
`Expected value to be truthy`
);
} }
toBeFalsy() { toBeFalsy() {
return this.assertion.toBeFalsy(); return this.assertion.customAssertion(
(v) => !v,
`Expected value to be falsy`
);
} }
} }

View File

@ -7,14 +7,23 @@ export class DateMatchers {
constructor(private assertion: Assertion<Date>) {} constructor(private assertion: Assertion<Date>) {}
toBeDate() { toBeDate() {
return this.assertion.toBeDate(); return this.assertion.customAssertion(
(v) => v instanceof Date,
`Expected value to be a Date instance`
);
} }
toBeBeforeDate(date: Date) { 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) { 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()}`
);
} }
} }

View File

@ -7,6 +7,23 @@ export class FunctionMatchers {
constructor(private assertion: Assertion<Function>) {} constructor(private assertion: Assertion<Function>) {}
toThrow(expectedError?: any) { 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}` : ''}`
);
} }
} }

View File

@ -7,26 +7,50 @@ export class NumberMatchers {
constructor(private assertion: Assertion<number>) {} constructor(private assertion: Assertion<number>) {}
toBeGreaterThan(value: 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) { 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) { 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) { 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) { 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 */ /** Equality check for numbers */
toEqual(value: number) { toEqual(value: number) {
return this.assertion.toEqual(value); return this.assertion.customAssertion(
(v) => (v as number) === value,
`Expected number to equal ${value}`
);
} }
} }

View File

@ -1,4 +1,5 @@
import { Assertion } from '../smartexpect.classes.assertion.js'; import { Assertion } from '../smartexpect.classes.assertion.js';
import * as plugins from '../plugins.js';
/** /**
* Namespace for object-specific matchers * Namespace for object-specific matchers
@ -7,33 +8,82 @@ export class ObjectMatchers<T extends object> {
constructor(private assertion: Assertion<T>) {} constructor(private assertion: Assertion<T>) {}
toEqual(expected: any) { 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) { 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) { 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) { 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[]) { 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() { toBeNull() {
return this.assertion.toBeNull(); return this.assertion.customAssertion(
(v) => v === null,
`Expected value to be null`
);
} }
toBeUndefined() { toBeUndefined() {
return this.assertion.toBeUndefined(); return this.assertion.customAssertion(
(v) => v === undefined,
`Expected value to be undefined`
);
} }
toBeNullOrUndefined() { toBeNullOrUndefined() {
return this.assertion.toBeNullOrUndefined(); return this.assertion.customAssertion(
(v) => v === null || v === undefined,
`Expected value to be null or undefined`
);
} }
} }

View File

@ -7,26 +7,44 @@ export class StringMatchers {
constructor(private assertion: Assertion<string>) {} constructor(private assertion: Assertion<string>) {}
toStartWith(prefix: 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) { 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) { 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) { 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[]) { 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 */ /** Length check for strings */
toHaveLength(length: number) { toHaveLength(length: number) {
return this.assertion.toHaveLength(length); return this.assertion.customAssertion(
(value) => (value as string).length === length,
`Expected string to have length ${length}`
);
} }
} }

View File

@ -7,22 +7,37 @@ export class TypeMatchers {
constructor(private assertion: Assertion<any>) {} constructor(private assertion: Assertion<any>) {}
toBeTypeofString() { toBeTypeofString() {
return this.assertion.toBeTypeofString(); return this.assertion.customAssertion(
(v) => typeof v === 'string',
`Expected type to be 'string'`
);
} }
toBeTypeofNumber() { toBeTypeofNumber() {
return this.assertion.toBeTypeofNumber(); return this.assertion.customAssertion(
(v) => typeof v === 'number',
`Expected type to be 'number'`
);
} }
toBeTypeofBoolean() { toBeTypeofBoolean() {
return this.assertion.toBeTypeofBoolean(); return this.assertion.customAssertion(
(v) => typeof v === 'boolean',
`Expected type to be 'boolean'`
);
} }
toBeTypeOf(typeName: string) { toBeTypeOf(typeName: string) {
return this.assertion.toBeTypeOf(typeName); return this.assertion.customAssertion(
(v) => typeof v === typeName,
`Expected type to be '${typeName}'`
);
} }
toBeDefined() { toBeDefined() {
return this.assertion.toBeDefined(); return this.assertion.customAssertion(
(v) => v !== undefined,
`Expected value to be defined`
);
} }
} }

View File

@ -288,6 +288,49 @@ export class Assertion<T = unknown> {
console.log(`Path: ${this.formatDrillDown() || '(root)'}`); console.log(`Path: ${this.formatDrillDown() || '(root)'}`);
return this; 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 // Namespaced matcher accessors
/** String-specific matchers */ /** String-specific matchers */
public get string() { public get string() {