feat(core): Add new matchers and improve negation messaging

This commit is contained in:
2025-04-28 20:42:58 +00:00
parent 1847838ac3
commit 9b488a87a0
13 changed files with 300 additions and 21 deletions

View File

@ -31,7 +31,9 @@ export class ArrayMatchers<T> {
toContainEqual(item: T) {
return this.assertion.customAssertion(
(value) => (value as T[]).some((e) => plugins.fastDeepEqual(e, item)),
`Expected array to contain equal to ${JSON.stringify(item)}`
(value) =>
`Expected array to contain equal to ${JSON.stringify(item)}` +
`\nReceived: ${JSON.stringify(value, null, 2)}`
);
}
@ -55,6 +57,12 @@ export class ArrayMatchers<T> {
`Expected array to be empty`
);
}
/**
* Alias for empty array check
*/
toBeEmpty() {
return this.toBeEmptyArray();
}
toHaveLengthGreaterThan(length: number) {
return this.assertion.customAssertion(

View File

@ -26,4 +26,36 @@ export class FunctionMatchers {
`Expected function to throw${expectedError ? ` ${expectedError}` : ''}`
);
}
/**
* Assert thrown error message matches the given regex
*/
toThrowErrorMatching(regex: RegExp) {
return this.assertion.customAssertion(
(value) => {
try {
(value as Function)();
} catch (e: any) {
return regex.test(e && e.message);
}
return false;
},
`Expected function to throw error matching ${regex}`
);
}
/**
* Assert thrown error message equals the given string
*/
toThrowErrorWithMessage(expectedMessage: string) {
return this.assertion.customAssertion(
(value) => {
try {
(value as Function)();
} catch (e: any) {
return e && e.message === expectedMessage;
}
return false;
},
`Expected function to throw error with message "${expectedMessage}"`
);
}
}

View File

@ -53,4 +53,31 @@ export class NumberMatchers {
`Expected number to equal ${value}`
);
}
/**
* Checks for NaN
*/
toBeNaN() {
return this.assertion.customAssertion(
(v) => Number.isNaN(v as number),
`Expected number to be NaN`
);
}
/**
* Checks for finite number
*/
toBeFinite() {
return this.assertion.customAssertion(
(v) => Number.isFinite(v as number),
`Expected number to be finite`
);
}
/**
* Checks if number is within inclusive range
*/
toBeWithinRange(min: number, max: number) {
return this.assertion.customAssertion(
(v) => (v as number) >= min && (v as number) <= max,
`Expected number to be within range ${min} - ${max}`
);
}
}

View File

@ -1,4 +1,4 @@
import { Assertion } from '../smartexpect.classes.assertion.js';
import { Assertion, AnyMatcher, AnythingMatcher } from '../smartexpect.classes.assertion.js';
import * as plugins from '../plugins.js';
/**
@ -10,21 +10,43 @@ export class ObjectMatchers<T extends object> {
toEqual(expected: any) {
return this.assertion.customAssertion(
(v) => plugins.fastDeepEqual(v, expected),
`Expected objects to be deeply equal to ${JSON.stringify(expected)}`
(v) =>
`Expected objects to be deeply equal to ${JSON.stringify(expected, null, 2)}` +
`\nReceived: ${JSON.stringify(v, null, 2)}`
);
}
toMatchObject(expected: object) {
return this.assertion.customAssertion(
(v) => {
const obj = v as any;
for (const key of Object.keys(expected)) {
if (!plugins.fastDeepEqual((v as any)[key], (expected as any)[key])) {
const expectedVal = (expected as any)[key];
const actualVal = obj[key];
if (expectedVal instanceof AnyMatcher) {
const ctor = expectedVal.expectedConstructor;
if (ctor === Number) {
if (typeof actualVal !== 'number') return false;
} else if (ctor === String) {
if (typeof actualVal !== 'string') return false;
} else if (ctor === Boolean) {
if (typeof actualVal !== 'boolean') return false;
} else {
if (!(actualVal instanceof ctor)) return false;
}
} else if (expectedVal instanceof AnythingMatcher) {
if (actualVal === null || actualVal === undefined) {
return false;
}
} else if (!plugins.fastDeepEqual(actualVal, expectedVal)) {
return false;
}
}
return true;
},
`Expected object to match properties ${JSON.stringify(expected)}`
(v) =>
`Expected object to match properties ${JSON.stringify(expected, null, 2)}` +
`\nReceived: ${JSON.stringify(v, null, 2)}`
);
}
@ -86,4 +108,25 @@ export class ObjectMatchers<T extends object> {
`Expected value to be null or undefined`
);
}
/**
* Checks own property only (not inherited)
*/
toHaveOwnProperty(property: string, value?: any) {
return this.assertion.customAssertion(
(v) => {
const obj = v as any;
if (!Object.prototype.hasOwnProperty.call(obj, property)) {
return false;
}
if (arguments.length === 2) {
return plugins.fastDeepEqual(obj[property], value);
}
return true;
},
(v) =>
`Expected object to have own property ${property}` +
(value !== undefined ? ` with value ${JSON.stringify(value)}` : ``) +
`\nReceived: ${JSON.stringify(v, null, 2)}`
);
}
}

View File

@ -47,4 +47,13 @@ export class StringMatchers {
`Expected string to have length ${length}`
);
}
/**
* Alias for empty string check
*/
toBeEmpty() {
return this.assertion.customAssertion(
(value) => (value as string).length === 0,
`Expected string to be empty`
);
}
}