fix(assertion-matchers): Refactor matcher implementations to consistently use customAssertion for improved consistency and clarity.
This commit is contained in:
parent
4eac4544a5
commit
91a3dc43d3
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user