Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how

:pencil: - chore

## [1.6.0]
- :rocket: added `deeply strictly equal` matcher

## [1.5.0]
- :rocket: improved `AssertionError` to inherit it from nodejs `AssertionError`

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This library supports a variety of validation types, which can all be negated by
- **`equal`**: Checks for non-strict equality (`==`).
- **`strictly equal`**: Checks for strict equality (`===`).
- **`deeply equal`**: Performs a deep comparison of object properties or array elements.
- **`deeply strictly equal`**: Performs a deep comparison of object properties or array elements with nodejs [util.isDeepStrictEqual](https://nodejs.org/api/util.html#utilisdeepstrictequalval1-val2-options)
- **`case insensitive equal`**: Compares two values for non-strict equality after converting them to lowercase.
- **`contain`**: Verifies if a string contains a specific substring.
- **`include members`**: Checks if an array or object includes a specific set of members.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@qavajs/validation",
"version": "1.5.0",
"version": "1.6.0",
"description": "@qavajs library that transform plain english definition to validation functions",
"main": "./lib/index.js",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions src/matchers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect as base, type MatcherResult, type MatcherContext } from './expect';
import Ajv from 'ajv';
import { isDeepStrictEqual } from 'node:util';

export type BaseMatchers = {
toSimpleEqual(this: MatcherContext<any>, expected: any): MatcherResult;
Expand All @@ -16,6 +17,7 @@ export type BaseMatchers = {
toBeTruthy(this: MatcherContext<any>): MatcherResult;
toContain(this: MatcherContext<any>, expected: any): MatcherResult;
toDeepEqual(this: MatcherContext<any>, expected: any): MatcherResult;
toDeepStrictEqual(this: MatcherContext<any>, expected: any): MatcherResult;
toStrictEqual(this: MatcherContext<any>, expected: any): MatcherResult;
toHaveLength(this: MatcherContext<any>, expected: number): MatcherResult;
toHaveProperty(this: MatcherContext<any>, key: string, value?: any): MatcherResult;
Expand Down Expand Up @@ -134,6 +136,12 @@ export const expect = base.extend<BaseMatchers>({
return { pass, message };
},

toDeepStrictEqual(expected: any) {
const pass = isDeepStrictEqual(this.received, expected);
const message = this.formatMessage(this.received, expected, 'to deeply strictly equal', this.isNot);
return { pass, message };
},

toStrictEqual(expected: any) {
const pass = this.received === expected;
const message = this.formatMessage(this.received, expected, 'to strictly equal', this.isNot);
Expand Down
210 changes: 107 additions & 103 deletions src/verify.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { expect } from './matchers';

export const validations = {
EQUAL: 'equal',
DEEPLY_EQUAL: 'deeply equal',
STRICTLY_EQUAL: 'strictly equal',
HAVE_MEMBERS: 'have member',
MATCH: 'match',
CONTAIN: 'contain',
ABOVE: 'above',
BELOW: 'below',
GREATER: 'greater than',
LESS: 'less than',
HAVE_TYPE: 'have type',
INCLUDE_MEMBERS: 'include member',
HAVE_PROPERTY: 'have property',
MATCH_SCHEMA: 'match schema',
CASE_INSENSITIVE_EQUAL: 'case insensitive equal',
SATISFY: 'satisfy'
EQUAL: 'equal',
DEEPLY_EQUAL: 'deeply equal',
STRICTLY_EQUAL: 'strictly equal',
DEEPLY_STRICTLY_EQUAL: 'deeply strictly equal',
HAVE_MEMBERS: 'have member',
MATCH: 'match',
CONTAIN: 'contain',
ABOVE: 'above',
BELOW: 'below',
GREATER: 'greater than',
LESS: 'less than',
HAVE_TYPE: 'have type',
INCLUDE_MEMBERS: 'include member',
HAVE_PROPERTY: 'have property',
MATCH_SCHEMA: 'match schema',
CASE_INSENSITIVE_EQUAL: 'case insensitive equal',
SATISFY: 'satisfy'
};

const isClause = '(?:is |do |does |to )?';
Expand All @@ -29,123 +30,126 @@ export const validationExtractRegexp = new RegExp(`^${isClause}${notClause}${toB
export const validationRegexp = new RegExp(`(${isClause}${notClause}${toBeClause}${softlyClause}${validationClause})`);

type VerifyInput = {
received: any;
expected: any;
validation: string;
reverse: boolean;
soft: boolean;
received: any;
expected: any;
validation: string;
reverse: boolean;
soft: boolean;
};

const aboveFn = (expectClause: any, expected: any) => expectClause.toBeGreaterThan(toNumber(expected));
const belowFn = (expectClause: any, expected: any) => expectClause.toBeLessThan(toNumber(expected));
const validationFns: Record<string, (expectClause: any, expected: any) => void> = {
[validations.EQUAL]: (expectClause, expected: any) => expectClause.toSimpleEqual(expected),
[validations.STRICTLY_EQUAL]: (expectClause: any, expected: any) => expectClause.toEqual(expected),
[validations.DEEPLY_EQUAL]: (expectClause: any, expected: any) => expectClause.toDeepEqual(expected),
[validations.HAVE_MEMBERS]: (expectClause: any, expected: any) => expectClause.toHaveMembers(expected),
[validations.MATCH]: (expectClause: any, expected: any) => expectClause.toMatch(toRegexp(expected)),
[validations.CONTAIN]: (expectClause: any, expected: any) => expectClause.toContain(expected),
[validations.ABOVE]: aboveFn,
[validations.BELOW]: belowFn,
[validations.GREATER]: aboveFn,
[validations.LESS]: belowFn,
[validations.HAVE_TYPE]: (expectClause: any, expected: string) => expectClause.toHaveType(expected),
[validations.INCLUDE_MEMBERS]: (expectClause: any, expected: string) => expectClause.toIncludeMembers(expected),
[validations.HAVE_PROPERTY]: (expectClause: any, expected: string) => expectClause.toHaveProperty(expected),
[validations.MATCH_SCHEMA]: (expectClause: any, expected: string) => expectClause.toMatchSchema(expected),
[validations.CASE_INSENSITIVE_EQUAL]: (expectClause: any, expected: any) => expectClause.toCaseInsensitiveEqual(expected),
[validations.SATISFY]: (expectClause: any, expected: any) => expectClause.toSatisfy(expected),
[validations.EQUAL]: (expectClause, expected: any) => expectClause.toSimpleEqual(expected),
[validations.STRICTLY_EQUAL]: (expectClause: any, expected: any) => expectClause.toEqual(expected),
[validations.DEEPLY_EQUAL]: (expectClause: any, expected: any) => expectClause.toDeepEqual(expected),
[validations.DEEPLY_STRICTLY_EQUAL]: (expectClause: any, expected: any) => expectClause.toDeepStrictEqual(expected),
[validations.HAVE_MEMBERS]: (expectClause: any, expected: any) => expectClause.toHaveMembers(expected),
[validations.MATCH]: (expectClause: any, expected: any) => expectClause.toMatch(toRegexp(expected)),
[validations.CONTAIN]: (expectClause: any, expected: any) => expectClause.toContain(expected),
[validations.ABOVE]: aboveFn,
[validations.BELOW]: belowFn,
[validations.GREATER]: aboveFn,
[validations.LESS]: belowFn,
[validations.HAVE_TYPE]: (expectClause: any, expected: string) => expectClause.toHaveType(expected),
[validations.INCLUDE_MEMBERS]: (expectClause: any, expected: string) => expectClause.toIncludeMembers(expected),
[validations.HAVE_PROPERTY]: (expectClause: any, expected: string) => expectClause.toHaveProperty(expected),
[validations.MATCH_SCHEMA]: (expectClause: any, expected: string) => expectClause.toMatchSchema(expected),
[validations.CASE_INSENSITIVE_EQUAL]: (expectClause: any, expected: any) => expectClause.toCaseInsensitiveEqual(expected),
[validations.SATISFY]: (expectClause: any, expected: any) => expectClause.toSatisfy(expected),
};

/**
* Basic verification function
* @param {VerifyInput} object with all needed data for validation
*/
export function verify({ received, expected, validation, reverse, soft }: VerifyInput): void {
const expectClause = expect(received).configure({ not: reverse, soft });
const validate = validationFns[validation];
validate(expectClause, expected);
export function verify({received, expected, validation, reverse, soft}: VerifyInput): void {
const expectClause = expect(received).configure({not: reverse, soft});
const validate = validationFns[validation];
validate(expectClause, expected);
}

export function getValidation(validationType: string, options?: { soft: boolean }): (AR: any, expected: any) => void {
const match = validationExtractRegexp.exec(validationType);
if (!match) throw new Error(`Validation '${validationType}' is not supported`);
const { reverse, validation, soft } = match.groups as {[p: string]: string};
const softProp = options?.soft || !!soft;
return function (received: any, expected: any) {
verify({ received, expected, validation, reverse: Boolean(reverse), soft: softProp });
};
const match = validationExtractRegexp.exec(validationType);
if (!match) throw new Error(`Validation '${validationType}' is not supported`);
const {reverse, validation, soft} = match.groups as { [p: string]: string };
const softProp = options?.soft || !!soft;
return function (received: any, expected: any) {
verify({received, expected, validation, reverse: Boolean(reverse), soft: softProp});
};
}

export function getPollValidation(validationType: string, options?: { soft: boolean }): (AR: any, expected: any, options?: { timeout?: number, interval?: number }) => Promise<unknown> {
const match = validationExtractRegexp.exec(validationType);
if (!match) throw new Error(`Poll validation '${validationType}' is not supported`);
const { reverse, validation, soft } = match.groups as {[p: string]: string};
const softProp = options?.soft || !!soft;
return async function (received: any, expected: any, options?: { timeout?: number, interval?: number }) {
export function getPollValidation(validationType: string, options?: {
soft: boolean
}): (AR: any, expected: any, options?: { timeout?: number, interval?: number }) => Promise<unknown> {
const match = validationExtractRegexp.exec(validationType);
if (!match) throw new Error(`Poll validation '${validationType}' is not supported`);
const {reverse, validation, soft} = match.groups as { [p: string]: string };
const softProp = options?.soft || !!soft;
return async function (received: any, expected: any, options?: { timeout?: number, interval?: number }) {
const timeout = options?.timeout ?? 5000;
const interval = options?.interval ?? 500;
let lastError: Error = new Error(`Promise was not settled before timeout`);
let intervalId: NodeJS.Timeout;
const evaluatePromise = new Promise<void>(resolve => {
intervalId = setInterval(async () => {
try {
const actualValue = await received();
verify({
received: actualValue,
expected,
validation,
reverse: Boolean(reverse),
soft: softProp
});
clearInterval(intervalId);
resolve();
} catch (err: any) {
lastError = err;
}
}, interval);
});
const timeoutPromise = new Promise((_, reject) => setTimeout(() => {
clearInterval(intervalId);
reject(lastError)
}, timeout));
return Promise.race([evaluatePromise, timeoutPromise]);
};
}

export async function poll(fn: Function, options?: { timeout?: number, interval?: number }) {
const timeout = options?.timeout ?? 5000;
const interval = options?.interval ?? 500;
let lastError: Error = new Error(`Promise was not settled before timeout`);
let lastError: Error = new Error('Unexpected error');
let intervalId: NodeJS.Timeout;
const evaluatePromise = new Promise<void>(resolve => {
intervalId = setInterval(async () => {
try {
const actualValue = await received();
verify({
received: actualValue,
expected,
validation,
reverse: Boolean(reverse),
soft: softProp
});
clearInterval(intervalId);
resolve();
} catch (err: any) {
lastError = err;
}
}, interval);
intervalId = setInterval(async () => {
try {
await fn();
clearInterval(intervalId);
resolve();
} catch (err: any) {
lastError = err;
}
}, interval);
});
const timeoutPromise = new Promise((_, reject) => setTimeout(() => {
clearInterval(intervalId);
reject(lastError)
clearInterval(intervalId);
reject(lastError);
}, timeout));
return Promise.race([evaluatePromise, timeoutPromise]);
};
}

export async function poll(fn: Function, options?: { timeout?: number, interval?: number }) {
const timeout = options?.timeout ?? 5000;
const interval = options?.interval ?? 500;
let lastError: Error = new Error('Unexpected error');
let intervalId: NodeJS.Timeout;
const evaluatePromise = new Promise<void>(resolve => {
intervalId = setInterval(async () => {
try {
await fn();
clearInterval(intervalId);
resolve();
} catch (err: any) {
lastError = err;
}
}, interval);
});
const timeoutPromise = new Promise((_, reject) => setTimeout(() => {
clearInterval(intervalId);
reject(lastError);
}, timeout));
return Promise.race([evaluatePromise, timeoutPromise]);
}

export { expect };

function toNumber(n: any): number {
const parsedNumber = Number.parseFloat(n);
if (Number.isNaN(parsedNumber)) {
throw new TypeError(`${n} is not a number`);
}
return parsedNumber;
const parsedNumber = Number.parseFloat(n);
if (Number.isNaN(parsedNumber)) {
throw new TypeError(`${n} is not a number`);
}
return parsedNumber;
}

function toRegexp(r: string | RegExp): RegExp {
return r instanceof RegExp ? r : new RegExp(r);
return r instanceof RegExp ? r : new RegExp(r);
}
6 changes: 6 additions & 0 deletions test/expect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ describe('expect matchers', () => {
vitestExpect(() => expect([{a: 42}, {b: 'text'}]).toDeepEqual([{b: 'text'}, {a: 42}])).not.toThrow();
});

// toDeepStrictEqual
test('toDeepStrictEqual', () => {
vitestExpect(() => expect([[1, 2], [1, 2, 3]]).toDeepStrictEqual([[1, 2], [1, 2, 3]])).not.toThrow();
vitestExpect(() => expect([{a: 42}, {b: 'text'}]).toDeepStrictEqual([{a: 42}, {b: 'text'}])).not.toThrow();
});

// toHaveLength
test('toHaveLength', () => {
vitestExpect(() => expect([1,2,3]).toHaveLength(3)).not.toThrow();
Expand Down
Loading