Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
2b803e6d57 | |||
d1969ab658 | |||
6ea5d643db | |||
6f1326a8da | |||
f099d0f98d | |||
a3d5892a13 | |||
0d9fa72b29 | |||
8cb70b6afe | |||
81bd8bfb13 | |||
dd4037677d |
35
changelog.md
35
changelog.md
@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-04-30 - 2.3.0 - feat(object-matchers)
|
||||
Add object key matchers: toHaveKeys and toHaveOwnKeys; remove obsolete roadmap plan file
|
||||
|
||||
- Implemented toHaveKeys matcher to check for both own and inherited properties
|
||||
- Implemented toHaveOwnKeys matcher to check for own properties only
|
||||
- Added tests for key matchers in test/test.keys.ts
|
||||
- Removed readme.plan.md as it is now obsolete
|
||||
|
||||
## 2025-04-29 - 2.2.2 - fix(license-files)
|
||||
Remove legacy license file and add license.md to update file naming.
|
||||
|
||||
- Removed the old 'license' file.
|
||||
- Added 'license.md' with updated file structure.
|
||||
|
||||
## 2025-04-29 - 2.2.1 - fix(readme)
|
||||
Update usage examples and full matcher reference in README
|
||||
|
||||
- Removed deprecated 'expectAsync' from import example
|
||||
- Fixed formatting in documentation
|
||||
- Added comprehensive full matcher reference section
|
||||
|
||||
## 2025-04-29 - 2.2.0 - feat(generics)
|
||||
Improve assertion and matcher type definitions by adding execution mode generics for better async/sync support
|
||||
|
||||
- Updated ts/index.ts to import and use TExecutionType in the expect function
|
||||
- Modified Assertion class to use a generic execution mode (M) for improved type inference
|
||||
- Revised all matcher namespaces (array, boolean, date, function, number, object, string, type) to accept the new generic parameter
|
||||
- Enhanced async/sync distinction for assertion methods like resolves and rejects
|
||||
|
||||
## 2025-04-29 - 2.1.2 - fix(ts/index.ts)
|
||||
Remove deprecated expectAsync function and advise using .resolves/.rejects on expect for async assertions
|
||||
|
||||
- Deleted the redundant expectAsync export in ts/index.ts
|
||||
- Users should now call expect(...).resolves or expect(...).rejects for asynchronous assertions
|
||||
|
||||
## 2025-04-29 - 2.1.1 - fix(Assertion)
|
||||
Improve chainability by fixing return types in assertion methods
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2022 Lossless GmbH (hello@lossless.com)
|
||||
Copyright (c) 2022 Task Venture Capital GmbH (hello@lossless.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/smartexpect",
|
||||
"version": "2.1.1",
|
||||
"version": "2.3.0",
|
||||
"private": false,
|
||||
"description": "A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.",
|
||||
"main": "dist_ts/index.js",
|
||||
|
94
readme.md
94
readme.md
@ -17,10 +17,10 @@ This will add `@push.rocks/smartexpect` to your project's dependencies. Make sur
|
||||
|
||||
### Getting Started
|
||||
|
||||
First, import `@push.rocks/smartexpect` into your TypeScript file:
|
||||
First, import `@push.rocks/smartexpect` into your TypeScript file:
|
||||
|
||||
```typescript
|
||||
import { expect, expectAsync } from '@push.rocks/smartexpect';
|
||||
import { expect } from '@push.rocks/smartexpect';
|
||||
```
|
||||
|
||||
### Synchronous Expectations
|
||||
@ -254,6 +254,96 @@ expect(4).not.toBeOdd();
|
||||
- Matcher functions receive the value under test (`received`) plus any arguments.
|
||||
- Must return an object with `pass` (boolean) and `message` (string or function) for failure messages.
|
||||
|
||||
### Full Matcher Reference
|
||||
|
||||
Below is a comprehensive list of all matchers and utility functions available in `@push.rocks/smartexpect`.
|
||||
|
||||
#### Modifiers and Utilities
|
||||
- `.not` Negates the next matcher in the chain.
|
||||
- `.resolves` Switches to async mode, expecting the promise to resolve.
|
||||
- `.rejects` Switches to async mode, expecting the promise to reject.
|
||||
- `.withTimeout(ms)` Sets a timeout (in milliseconds) for async assertions.
|
||||
- `.timeout(ms)` (deprecated) Alias for `.withTimeout(ms)`.
|
||||
- `.property(name)` Drill into a property of an object.
|
||||
- `.arrayItem(index)` Drill into an array element by index.
|
||||
- `.log()` Logs the current value and assertion path for debugging.
|
||||
- `.setFailMessage(message)` Override the failure message for the current assertion.
|
||||
- `.setSuccessMessage(message)` Override the success message for the current assertion.
|
||||
- `.customAssertion(fn, message)` Execute a custom assertion function with a message.
|
||||
- `expect.extend(matchers)` Register custom matchers globally.
|
||||
- `expect.any(constructor)` Matcher for values that are instances of the given constructor.
|
||||
- `expect.anything()` Matcher for any defined value (not null or undefined).
|
||||
|
||||
#### Basic Matchers
|
||||
- `.toEqual(expected)` Deep (or strict for primitives) equality.
|
||||
- `.toBeTrue()` Value is strictly `true`.
|
||||
- `.toBeFalse()` Value is strictly `false`.
|
||||
- `.toBeTruthy()` Value is truthy.
|
||||
- `.toBeFalsy()` Value is falsy.
|
||||
- `.toBeNull()` Value is `null`.
|
||||
- `.toBeUndefined()` Value is `undefined`.
|
||||
- `.toBeNullOrUndefined()` Value is `null` or `undefined`.
|
||||
- `.toBeDefined()` Value is not `undefined`.
|
||||
|
||||
#### Number Matchers
|
||||
- `.toBeGreaterThan(value)`
|
||||
- `.toBeLessThan(value)`
|
||||
- `.toBeGreaterThanOrEqual(value)`
|
||||
- `.toBeLessThanOrEqual(value)`
|
||||
- `.toBeCloseTo(value, precision?)`
|
||||
- `.toEqual(value)` Strict equality for numbers.
|
||||
- `.toBeNaN()`
|
||||
- `.toBeFinite()`
|
||||
- `.toBeWithinRange(min, max)`
|
||||
|
||||
#### String Matchers
|
||||
- `.toStartWith(prefix)`
|
||||
- `.toEndWith(suffix)`
|
||||
- `.toInclude(substring)`
|
||||
- `.toMatch(regex)`
|
||||
- `.toBeOneOf(arrayOfValues)`
|
||||
- `.toHaveLength(length)`
|
||||
- `.toBeEmpty()` Alias for empty string.
|
||||
|
||||
#### Array Matchers
|
||||
- `.toBeArray()`
|
||||
- `.toHaveLength(length)`
|
||||
- `.toContain(value)`
|
||||
- `.toContainEqual(value)`
|
||||
- `.toContainAll(arrayOfValues)`
|
||||
- `.toExclude(value)`
|
||||
- `.toBeEmptyArray()`
|
||||
- `.toBeEmpty()` Alias for empty array.
|
||||
- `.toHaveLengthGreaterThan(length)`
|
||||
- `.toHaveLengthLessThan(length)`
|
||||
|
||||
#### Object Matchers
|
||||
- `.toEqual(expected)` Deep equality for objects.
|
||||
- `.toMatchObject(partialObject)` Partial deep matching (supports `expect.any` and `expect.anything`).
|
||||
- `.toBeInstanceOf(constructor)`
|
||||
- `.toHaveProperty(propertyName, value?)`
|
||||
- `.toHaveDeepProperty(pathArray)`
|
||||
- `.toHaveOwnProperty(propertyName, value?)`
|
||||
- `.toBeNull()`
|
||||
- `.toBeUndefined()`
|
||||
- `.toBeNullOrUndefined()`
|
||||
|
||||
#### Function Matchers
|
||||
- `.toThrow(expectedError?)`
|
||||
- `.toThrowErrorMatching(regex)`
|
||||
- `.toThrowErrorWithMessage(message)`
|
||||
|
||||
#### Date Matchers
|
||||
- `.toBeDate()`
|
||||
- `.toBeBeforeDate(date)`
|
||||
- `.toBeAfterDate(date)`
|
||||
|
||||
#### Type Matchers
|
||||
- `.toBeTypeofString()`
|
||||
- `.toBeTypeofNumber()`
|
||||
- `.toBeTypeofBoolean()`
|
||||
- `.toBeTypeOf(typeName)`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Human-readable assertions**: The fluent API is designed to create tests that read like natural language sentences.
|
||||
|
@ -1,55 +0,0 @@
|
||||
# Plan for Improving @push.rocks/smartexpect API
|
||||
|
||||
This document captures the roadmap for evolving the `expect` / `expectAsync` API.
|
||||
|
||||
## Phase 1: Unify Sync + Async
|
||||
- [x] Consolidate `expect` and `expectAsync` into a single `expect()` entry point.
|
||||
- [x] Introduce `.resolves` and `.rejects` chainable helpers for Promises.
|
||||
- [x] Deprecate `expectAsync`, provide migration guidance.
|
||||
|
||||
## Phase 2: Timeout Helper
|
||||
- [x] Rename or wrap the existing `.timeout(ms)` to a more intuitive `.withTimeout(ms)`.
|
||||
|
||||
## Phase 3: Custom Matchers
|
||||
- [x] Implement `expect.extend()` API for user-defined matchers.
|
||||
|
||||
## Phase 4: TypeScript Typings
|
||||
- [ ] Enhance generic matcher types to infer narrow types after `.property()` / `.arrayItem()`.
|
||||
- [ ] Provide matcher overloads for primitive categories (string, number, array, etc.).
|
||||
|
||||
## Phase 5: Namespaced Matchers
|
||||
- [ ] Group matchers under `.string`, `.array`, `.number`, etc. for discoverability.
|
||||
|
||||
## Phase 6: Jest-Style Convenience
|
||||
- [x] Add `expect.any()` and `expect.anything()` matchers for use in `.toMatchObject()` patterns
|
||||
(Snapshot matchers still TBD)
|
||||
|
||||
The next items to tackle:
|
||||
|
||||
3. Improve negation (`.not`) messaging
|
||||
- Today `.not` simply flips pass/fail, but the failure message isn’t very descriptive. We should capture positive/negative message templates so e.g.
|
||||
> expect(5).not.toBeGreaterThan(3)
|
||||
emits:
|
||||
"Expected 5 not to be greater than 3"
|
||||
|
||||
4. Richer error output for objects/arrays
|
||||
- Integrate a diff library (or extend `fast-deep-equal`) to show unified diffs between expected and actual values
|
||||
|
||||
5. More built-in matchers
|
||||
- toBeNaN(), toBeFinite()
|
||||
- toBeWithinRange(min, max)
|
||||
- toHaveKeys(...), toHaveOwnKeys(...)
|
||||
- toThrowErrorMatching(/regex/), toThrowErrorWithMessage('…')
|
||||
- string/array: toBeEmpty() alias
|
||||
- object: toHaveOwnProperty() shorthand
|
||||
|
||||
6. TypeScript-friendliness
|
||||
- Enhance `.d.ts` so editors autocomplete namespace methods (e.g. `expect(x).string.`)
|
||||
- Statically type matcher arguments to catch wrong types at compile time
|
||||
|
||||
7. Async assertions and timeouts improvements
|
||||
- Support `.not.resolves`, `.rejects.toThrow()`
|
||||
- Provide clearer timeout errors (e.g. "Expected promise to resolve within …")
|
||||
|
||||
8. Plugin/extension API
|
||||
- Formalize `Assertion.extend()` plugin API for shipping matcher bundles
|
51
test/test.keys.ts
Normal file
51
test/test.keys.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { tap, expect as tExpect } from '@push.rocks/tapbundle';
|
||||
import * as smartexpect from '../dist_ts/index.js';
|
||||
|
||||
// Positive: toHaveKeys should match both own and inherited properties
|
||||
tap.test('toHaveKeys includes own and inherited properties', async () => {
|
||||
class Parent { a = 1; }
|
||||
class Child extends Parent { b = 2; }
|
||||
const child = new Child();
|
||||
smartexpect.expect(child).object.toHaveKeys(['a', 'b']);
|
||||
});
|
||||
|
||||
// Positive: toHaveOwnKeys should match only own properties
|
||||
tap.test('toHaveOwnKeys includes only own properties', async () => {
|
||||
class Parent { a = 1; }
|
||||
class Child extends Parent { b = 2; }
|
||||
const child = new Child();
|
||||
smartexpect.expect(child).object.toHaveOwnKeys(['b']);
|
||||
});
|
||||
|
||||
// Negative: toHaveKeys should fail if any key is missing
|
||||
tap.test('toHaveKeys fails when keys are missing', async () => {
|
||||
const obj = { a: 1 };
|
||||
try {
|
||||
smartexpect.expect(obj).object.toHaveKeys(['a', 'b']);
|
||||
throw new Error('Assertion did not throw');
|
||||
} catch (err: any) {
|
||||
tExpect(err.message.includes('Expected object to have keys')).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
// Negative: toHaveOwnKeys should fail when own keys are missing (inherited keys not counted)
|
||||
tap.test('toHaveOwnKeys fails when own keys are missing', async () => {
|
||||
// 'a' is inherited via the prototype, not an own property
|
||||
class Parent {}
|
||||
Parent.prototype.a = 1;
|
||||
class Child extends Parent {
|
||||
constructor() {
|
||||
super();
|
||||
this.b = 2;
|
||||
}
|
||||
}
|
||||
const child = new Child();
|
||||
try {
|
||||
smartexpect.expect(child).object.toHaveOwnKeys(['a', 'b']);
|
||||
throw new Error('Assertion did not throw');
|
||||
} catch (err: any) {
|
||||
tExpect(err.message.includes('Expected object to have own keys')).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartexpect',
|
||||
version: '2.1.1',
|
||||
version: '2.3.0',
|
||||
description: 'A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.'
|
||||
}
|
||||
|
24
ts/index.ts
24
ts/index.ts
@ -1,4 +1,5 @@
|
||||
import { Assertion, AnyMatcher, AnythingMatcher } from './smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from './types.js';
|
||||
// import type { TMatcher } from './smartexpect.classes.assertion.js'; // unused
|
||||
|
||||
/**
|
||||
@ -12,12 +13,12 @@ import { Assertion, AnyMatcher, AnythingMatcher } from './smartexpect.classes.as
|
||||
* Entry point for assertions.
|
||||
* Automatically detects Promises to support async assertions.
|
||||
*/
|
||||
export function expect<T>(value: Promise<T>): Assertion<T>;
|
||||
export function expect<T>(value: T): Assertion<T>;
|
||||
export function expect<T>(value: any): Assertion<T> {
|
||||
export function expect<T>(value: Promise<T>): Assertion<T, 'async'>;
|
||||
export function expect<T>(value: T): Assertion<T, 'sync'>;
|
||||
export function expect<T>(value: any): Assertion<T, TExecutionType> {
|
||||
const isThenable = value != null && typeof (value as any).then === 'function';
|
||||
const mode: 'sync' | 'async' = isThenable ? 'async' : 'sync';
|
||||
return new Assertion<T>(value, mode);
|
||||
return new Assertion<T, TExecutionType>(value, mode);
|
||||
}
|
||||
/**
|
||||
* Register custom matchers.
|
||||
@ -38,18 +39,3 @@ export namespace expect {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `expect(...)` with `.resolves` or `.rejects` instead.
|
||||
*/
|
||||
/**
|
||||
* @deprecated Use `expect(...)` with `.resolves` or `.rejects` instead.
|
||||
*/
|
||||
/**
|
||||
* @deprecated Use `expect(...)` with `.resolves` or `.rejects` instead.
|
||||
*/
|
||||
export const expectAsync = (baseArg: any) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[DEPRECATED] expectAsync() is deprecated. Use expect(...).resolves / .rejects');
|
||||
return new Assertion<any>(baseArg, 'async');
|
||||
};
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
|
||||
/**
|
||||
* Namespace for array-specific matchers
|
||||
*/
|
||||
export class ArrayMatchers<T> {
|
||||
constructor(private assertion: Assertion<T[]>) {}
|
||||
export class ArrayMatchers<T, M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<T[], M>) {}
|
||||
|
||||
toBeArray() {
|
||||
return this.assertion.customAssertion(
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
|
||||
/**
|
||||
* Namespace for boolean-specific matchers
|
||||
*/
|
||||
export class BooleanMatchers {
|
||||
constructor(private assertion: Assertion<boolean>) {}
|
||||
export class BooleanMatchers<M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<boolean, M>) {}
|
||||
|
||||
toBeTrue() {
|
||||
return this.assertion.customAssertion(
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
|
||||
/**
|
||||
* Namespace for date-specific matchers
|
||||
*/
|
||||
export class DateMatchers {
|
||||
constructor(private assertion: Assertion<Date>) {}
|
||||
export class DateMatchers<M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<Date, M>) {}
|
||||
|
||||
toBeDate() {
|
||||
return this.assertion.customAssertion(
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
|
||||
/**
|
||||
* Namespace for function-specific matchers
|
||||
*/
|
||||
export class FunctionMatchers {
|
||||
constructor(private assertion: Assertion<Function>) {}
|
||||
export class FunctionMatchers<M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<Function, M>) {}
|
||||
|
||||
toThrow(expectedError?: any) {
|
||||
return this.assertion.customAssertion(
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
|
||||
/**
|
||||
* Namespace for number-specific matchers
|
||||
*/
|
||||
export class NumberMatchers {
|
||||
constructor(private assertion: Assertion<number>) {}
|
||||
export class NumberMatchers<M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<number, M>) {}
|
||||
|
||||
toBeGreaterThan(value: number) {
|
||||
return this.assertion.customAssertion(
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Assertion, AnyMatcher, AnythingMatcher } from '../smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
/**
|
||||
* Namespace for object-specific matchers
|
||||
*/
|
||||
export class ObjectMatchers<T extends object> {
|
||||
constructor(private assertion: Assertion<T>) {}
|
||||
export class ObjectMatchers<T extends object, M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<T, M>) {}
|
||||
|
||||
toEqual(expected: any) {
|
||||
return this.assertion.customAssertion(
|
||||
@ -129,4 +130,30 @@ export class ObjectMatchers<T extends object> {
|
||||
`\nReceived: ${JSON.stringify(v, null, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert object has the given keys (including inherited properties).
|
||||
* @param keys Array of keys to check for presence.
|
||||
*/
|
||||
public toHaveKeys(keys: string[]) {
|
||||
return this.assertion.customAssertion(
|
||||
(v) => keys.every((key) => key in (v as any)),
|
||||
(v) =>
|
||||
`Expected object to have keys ${JSON.stringify(keys)}` +
|
||||
`\nReceived keys: ${JSON.stringify(Object.keys(v), null, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert object has the given own keys (excluding inherited properties).
|
||||
* @param keys Array of own keys to check.
|
||||
*/
|
||||
public toHaveOwnKeys(keys: string[]) {
|
||||
return this.assertion.customAssertion(
|
||||
(v) => keys.every((key) => Object.prototype.hasOwnProperty.call(v as any, key)),
|
||||
(v) =>
|
||||
`Expected object to have own keys ${JSON.stringify(keys)}` +
|
||||
`\nReceived own keys: ${JSON.stringify(Object.keys(v), null, 2)}`
|
||||
);
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
|
||||
/**
|
||||
* Namespace for string-specific matchers
|
||||
*/
|
||||
export class StringMatchers {
|
||||
constructor(private assertion: Assertion<string>) {}
|
||||
export class StringMatchers<M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<string, M>) {}
|
||||
|
||||
toStartWith(prefix: string) {
|
||||
return this.assertion.customAssertion(
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||
import type { TExecutionType } from '../types.js';
|
||||
|
||||
/**
|
||||
* Namespace for type-based matchers
|
||||
*/
|
||||
export class TypeMatchers {
|
||||
constructor(private assertion: Assertion<any>) {}
|
||||
export class TypeMatchers<M extends TExecutionType> {
|
||||
constructor(private assertion: Assertion<any, M>) {}
|
||||
|
||||
toBeTypeofString() {
|
||||
return this.assertion.customAssertion(
|
||||
|
@ -26,8 +26,8 @@ export class AnyMatcher {
|
||||
}
|
||||
export class AnythingMatcher {}
|
||||
|
||||
export class Assertion<T = unknown> {
|
||||
executionMode: TExecutionType;
|
||||
export class Assertion<T = unknown, M extends TExecutionType = 'sync'> {
|
||||
executionMode: M;
|
||||
baseReference: any;
|
||||
propertyDrillDown: Array<string | number> = [];
|
||||
|
||||
@ -44,7 +44,7 @@ export class Assertion<T = unknown> {
|
||||
/** Computed negation failure message for the current assertion */
|
||||
private negativeMessage: string;
|
||||
|
||||
constructor(baseReferenceArg: any, executionModeArg: TExecutionType) {
|
||||
constructor(baseReferenceArg: any, executionModeArg: M) {
|
||||
this.baseReference = baseReferenceArg;
|
||||
this.executionMode = executionModeArg;
|
||||
}
|
||||
@ -159,20 +159,23 @@ export class Assertion<T = unknown> {
|
||||
/**
|
||||
* Assert that a Promise resolves.
|
||||
*/
|
||||
public get resolves(): this {
|
||||
this.isResolves = true;
|
||||
this.isRejects = false;
|
||||
this.executionMode = 'async';
|
||||
return this;
|
||||
/**
|
||||
* Switch to async (resolve) mode. Subsequent matchers return Promises.
|
||||
*/
|
||||
public get resolves(): Assertion<T, 'async'> {
|
||||
return new Assertion<T, 'async'>(this.baseReference, 'async');
|
||||
}
|
||||
/**
|
||||
* Assert that a Promise rejects.
|
||||
*/
|
||||
public get rejects(): this {
|
||||
this.isRejects = true;
|
||||
this.isResolves = false;
|
||||
this.executionMode = 'async';
|
||||
return this;
|
||||
/**
|
||||
* Switch to async (reject) mode. Subsequent matchers return Promises.
|
||||
*/
|
||||
public get rejects(): Assertion<T, 'async'> {
|
||||
const a = new Assertion<T, 'async'>(this.baseReference, 'async');
|
||||
// mark to expect rejection
|
||||
(a as any).isRejects = true;
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -203,7 +206,9 @@ export class Assertion<T = unknown> {
|
||||
return this;
|
||||
}
|
||||
|
||||
private runCheck(checkFunction: () => any): Assertion<T> | Promise<Assertion<T>> {
|
||||
// Internal check runner: returns Promise in async mode, else sync Assertion
|
||||
// Internal check runner; returns Promise or this at runtime, but typed via customAssertion
|
||||
private runCheck(checkFunction: () => any): any {
|
||||
const runDirectOrNegated = (checkFunction: () => any) => {
|
||||
if (!this.notSetting) {
|
||||
return checkFunction();
|
||||
@ -223,7 +228,7 @@ export class Assertion<T = unknown> {
|
||||
};
|
||||
|
||||
if (this.executionMode === 'async') {
|
||||
const done = plugins.smartpromise.defer<Assertion<T>>();
|
||||
const done = plugins.smartpromise.defer<Assertion<T, M>>();
|
||||
const isThenable = this.baseReference && typeof (this.baseReference as any).then === 'function';
|
||||
if (!isThenable) {
|
||||
done.reject(new Error(`Expected a Promise but received: ${this.formatValue(this.baseReference)}`));
|
||||
@ -268,17 +273,20 @@ export class Assertion<T = unknown> {
|
||||
);
|
||||
}
|
||||
// return a promise resolving to this for chaining
|
||||
return done.promise.then(() => this);
|
||||
return done.promise.then(() => this) as any;
|
||||
}
|
||||
// sync: run and return this for chaining
|
||||
runDirectOrNegated(checkFunction);
|
||||
return this;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a custom assertion. Returns a Promise in async mode, else returns this.
|
||||
*/
|
||||
public customAssertion(
|
||||
assertionFunction: (value: any) => boolean,
|
||||
errorMessage: string | ((value: any) => string)
|
||||
): Assertion<T> | Promise<Assertion<T>> {
|
||||
): M extends 'async' ? Promise<Assertion<T, M>> : Assertion<T, M> {
|
||||
// Prepare negation message based on the positive error template, if static
|
||||
if (typeof errorMessage === 'string') {
|
||||
this.negativeMessage = this.computeNegationMessage(errorMessage);
|
||||
@ -290,7 +298,7 @@ export class Assertion<T = unknown> {
|
||||
|| (typeof errorMessage === 'function' ? errorMessage(value) : errorMessage);
|
||||
throw new Error(msg);
|
||||
}
|
||||
});
|
||||
}) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,9 +306,9 @@ export class Assertion<T = unknown> {
|
||||
* @param propertyName Name of the property to navigate into.
|
||||
* @returns Assertion of the property type.
|
||||
*/
|
||||
public property<K extends keyof NonNullable<T>>(propertyName: K): Assertion<NonNullable<T>[K]> {
|
||||
public property<K extends keyof NonNullable<T>>(propertyName: K): Assertion<NonNullable<T>[K], M> {
|
||||
this.propertyDrillDown.push(propertyName as string);
|
||||
return this as unknown as Assertion<NonNullable<T>[K]>;
|
||||
return this as unknown as Assertion<NonNullable<T>[K], M>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -308,9 +316,9 @@ export class Assertion<T = unknown> {
|
||||
* @param index Index of the array item.
|
||||
* @returns Assertion of the element type.
|
||||
*/
|
||||
public arrayItem(index: number): Assertion<T extends Array<infer U> ? U : unknown> {
|
||||
public arrayItem(index: number): Assertion<T extends Array<infer U> ? U : unknown, M> {
|
||||
this.propertyDrillDown.push(index);
|
||||
return this as unknown as Assertion<T extends Array<infer U> ? U : unknown>;
|
||||
return this as unknown as Assertion<T extends Array<infer U> ? U : unknown, M>;
|
||||
}
|
||||
|
||||
public log() {
|
||||
@ -365,35 +373,35 @@ export class Assertion<T = unknown> {
|
||||
|
||||
// Namespaced matcher accessors
|
||||
/** String-specific matchers */
|
||||
public get string() {
|
||||
return new StringMatchers(this as Assertion<string>);
|
||||
public get string(): StringMatchers<M> {
|
||||
return new StringMatchers<M>(this as Assertion<string, M>);
|
||||
}
|
||||
/** Array-specific matchers */
|
||||
public get array() {
|
||||
return new ArrayMatchers<any>(this as Assertion<any[]>);
|
||||
public get array(): ArrayMatchers<any, M> {
|
||||
return new ArrayMatchers<any, M>(this as Assertion<any[], M>);
|
||||
}
|
||||
/** Number-specific matchers */
|
||||
public get number() {
|
||||
return new NumberMatchers(this as Assertion<number>);
|
||||
public get number(): NumberMatchers<M> {
|
||||
return new NumberMatchers<M>(this as Assertion<number, M>);
|
||||
}
|
||||
/** Boolean-specific matchers */
|
||||
public get boolean() {
|
||||
return new BooleanMatchers(this as Assertion<boolean>);
|
||||
public get boolean(): BooleanMatchers<M> {
|
||||
return new BooleanMatchers<M>(this as Assertion<boolean, M>);
|
||||
}
|
||||
/** Object-specific matchers */
|
||||
public get object() {
|
||||
return new ObjectMatchers<any>(this as Assertion<object>);
|
||||
public get object(): ObjectMatchers<any, M> {
|
||||
return new ObjectMatchers<any, M>(this as Assertion<object, M>);
|
||||
}
|
||||
/** Function-specific matchers */
|
||||
public get function() {
|
||||
return new FunctionMatchers(this as Assertion<Function>);
|
||||
public get function(): FunctionMatchers<M> {
|
||||
return new FunctionMatchers<M>(this as Assertion<Function, M>);
|
||||
}
|
||||
/** Date-specific matchers */
|
||||
public get date() {
|
||||
return new DateMatchers(this as Assertion<Date>);
|
||||
public get date(): DateMatchers<M> {
|
||||
return new DateMatchers<M>(this as Assertion<Date, M>);
|
||||
}
|
||||
/** Type-based matchers */
|
||||
public get type() {
|
||||
return new TypeMatchers(this as Assertion<any>);
|
||||
public get type(): TypeMatchers<M> {
|
||||
return new TypeMatchers<M>(this as Assertion<any, M>);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user