Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
6ea5d643db | |||
6f1326a8da | |||
f099d0f98d | |||
a3d5892a13 | |||
0d9fa72b29 | |||
8cb70b6afe | |||
81bd8bfb13 | |||
dd4037677d | |||
d0c2d04595 | |||
db49492af6 | |||
855e20a217 | |||
9b488a87a0 | |||
1847838ac3 | |||
91a3dc43d3 | |||
4eac4544a5 | |||
47458118a6 | |||
6f1e37cf56 | |||
ed9a9b7f2c | |||
5801d34f18 | |||
f0ab180902 | |||
5f5628f647 | |||
e688207d23 | |||
7ae5b4378a | |||
1979d8dd9c | |||
847b106302 | |||
38f0996cfa |
@ -119,6 +119,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npmci node install stable
|
npmci node install stable
|
||||||
npmci npm install
|
npmci npm install
|
||||||
pnpm install -g @gitzone/tsdoc
|
pnpm install -g @git.zone/tsdoc
|
||||||
npmci command tsdoc
|
npmci command tsdoc
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
93
changelog.md
93
changelog.md
@ -1,5 +1,98 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Update runCheck method to explicitly return the correct chainable type for both async and sync assertions
|
||||||
|
- Ensure customAssertion propagates the chainable Assertion instance
|
||||||
|
- Refactor internal promise handling for clarity and consistency
|
||||||
|
|
||||||
|
## 2025-04-28 - 2.1.0 - feat(core)
|
||||||
|
Add new matchers and improve negation messaging
|
||||||
|
|
||||||
|
- Added expect.any() and expect.anything() matchers for enhanced object pattern matching
|
||||||
|
- Introduced new number matchers: toBeNaN(), toBeFinite(), and toBeWithinRange()
|
||||||
|
- Implemented alias toBeEmpty() for both string and array matchers
|
||||||
|
- Enhanced function matchers with toThrowErrorMatching() and toThrowErrorWithMessage()
|
||||||
|
- Improved negation messaging to provide clearer failure messages (e.g. 'Expected 5 not to be greater than 3')
|
||||||
|
- Enhanced object assertions with a toHaveOwnProperty() shorthand that outputs unified diff-style messages
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Added packageManager field in package.json
|
||||||
|
- Revised documentation in readme.md to use .resolves/.rejects instead of expectAsync
|
||||||
|
- Included detailed examples for custom matchers and updated API usage
|
||||||
|
- Added readme.plan.md outlining the future roadmap
|
||||||
|
- Updated tests to import the built library from dist_ts
|
||||||
|
|
||||||
|
## 2025-03-04 - 1.6.1 - fix(build)
|
||||||
|
Corrected package.json and workflow dependencies and resolved formatting issues in tests.
|
||||||
|
|
||||||
|
- Fixed incorrect global npm package reference for tsdoc installation in workflow file.
|
||||||
|
- Updated dependencies in package.json for consistency in package naming.
|
||||||
|
- Resolved inconsistent formatting and spacing in test files.
|
||||||
|
|
||||||
|
## 2025-03-04 - 1.6.0 - feat(assertion)
|
||||||
|
Enhanced the assertion error messaging and added new test cases.
|
||||||
|
|
||||||
|
- Improved error messages by incorporating path and value/placeholders in assertions.
|
||||||
|
- Added detailed testing of new assertion functionalities.
|
||||||
|
- Additional test cases for comprehensive coverage of new features.
|
||||||
|
|
||||||
|
## 2025-03-04 - 1.5.0 - feat(Assertion)
|
||||||
|
Add toBeTypeOf assertion method
|
||||||
|
|
||||||
|
- Introduced a new assertion method `toBeTypeOf` allowing checks for expected data types.
|
||||||
|
- Updated devDependencies and dependencies to their latest versions.
|
||||||
|
|
||||||
|
## 2024-12-30 - 1.4.0 - feat(Assertion)
|
||||||
|
Add log method to Assertion class
|
||||||
|
|
||||||
|
- Introduced a log method in the Assertion class to output assertion context.
|
||||||
|
|
||||||
|
## 2024-12-30 - 1.3.0 - feat(Assertion)
|
||||||
|
Refactor Assertion class for better error handling and code clarity
|
||||||
|
|
||||||
|
- Improved method runCheck to better handle async and sync execution
|
||||||
|
- Enhanced getObjectToTestReference to handle undefined or null values gracefully
|
||||||
|
- Refactored error message logic for clarity and added more descriptive fail messages
|
||||||
|
- Added arrayItem method for better handling of array index access
|
||||||
|
- Improved structure by integrating consistent error handling in assertion methods
|
||||||
|
|
||||||
## 2024-08-24 - 1.2.1 - fix(Assertion)
|
## 2024-08-24 - 1.2.1 - fix(Assertion)
|
||||||
Refactor methods for setting failure and success messages
|
Refactor methods for setting failure and success messages
|
||||||
|
|
||||||
|
@ -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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
26
package.json
26
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartexpect",
|
"name": "@push.rocks/smartexpect",
|
||||||
"version": "1.2.1",
|
"version": "2.2.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@ -10,20 +10,20 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --web)",
|
"test": "(tstest test/ --web)",
|
||||||
"build": "(tsbuild --web)",
|
"build": "(tsbuild tsfolders)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gitzone/tsbuild": "^2.1.66",
|
"@git.zone/tsbuild": "^2.2.1",
|
||||||
"@gitzone/tsbundle": "^2.0.8",
|
"@git.zone/tsbundle": "^2.2.5",
|
||||||
"@gitzone/tsrun": "^1.2.44",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@gitzone/tstest": "^1.0.77",
|
"@git.zone/tstest": "^1.0.96",
|
||||||
"@push.rocks/tapbundle": "^5.0.23",
|
"@push.rocks/tapbundle": "^5.5.6",
|
||||||
"@types/node": "^22.4.0"
|
"@types/node": "^22.13.9"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartpromise": "^4.0.4",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"fast-deep-equal": "^3.1.3"
|
"fast-deep-equal": "^3.1.3"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
@ -57,5 +57,11 @@
|
|||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://code.foss.global/push.rocks/smartexpect.git"
|
"url": "https://code.foss.global/push.rocks/smartexpect.git"
|
||||||
}
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"mongodb-memory-server"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
}
|
}
|
||||||
|
8061
pnpm-lock.yaml
generated
8061
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
328
readme.md
328
readme.md
@ -1,5 +1,5 @@
|
|||||||
# @push.rocks/smartexpect
|
# @push.rocks/smartexpect
|
||||||
manage expectations in code
|
Manage expectations in code with precise, readable assertions
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@ -17,10 +17,10 @@ This will add `@push.rocks/smartexpect` to your project's dependencies. Make sur
|
|||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
First, import `@push.rocks/smartexpect` into your TypeScript file:
|
First, import `@push.rocks/smartexpect` into your TypeScript file:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { expect, expectAsync } from '@push.rocks/smartexpect';
|
import { expect } from '@push.rocks/smartexpect';
|
||||||
```
|
```
|
||||||
|
|
||||||
### Synchronous Expectations
|
### Synchronous Expectations
|
||||||
@ -30,14 +30,16 @@ You can employ `expect` to create synchronous assertions:
|
|||||||
```typescript
|
```typescript
|
||||||
import { expect } from '@push.rocks/smartexpect';
|
import { expect } from '@push.rocks/smartexpect';
|
||||||
|
|
||||||
// String type assertion
|
// Type assertions
|
||||||
expect('hello').toBeTypeofString();
|
expect('hello').toBeTypeofString();
|
||||||
|
expect(42).toBeTypeofNumber();
|
||||||
// Negated String type assertion
|
|
||||||
expect(1).not.toBeTypeofString();
|
|
||||||
|
|
||||||
// Boolean type assertion
|
|
||||||
expect(true).toBeTypeofBoolean();
|
expect(true).toBeTypeofBoolean();
|
||||||
|
expect(() => {}).toBeTypeOf('function');
|
||||||
|
expect({}).toBeTypeOf('object');
|
||||||
|
|
||||||
|
// Negated assertions
|
||||||
|
expect(1).not.toBeTypeofString();
|
||||||
|
expect('string').not.toBeTypeofNumber();
|
||||||
|
|
||||||
// Equality assertion
|
// Equality assertion
|
||||||
expect('hithere').toEqual('hithere');
|
expect('hithere').toEqual('hithere');
|
||||||
@ -51,78 +53,308 @@ expect('hithere').toMatch(/hi/);
|
|||||||
|
|
||||||
### Asynchronous Expectations
|
### Asynchronous Expectations
|
||||||
|
|
||||||
For asynchronous operations, use `expectAsync` to return a promise:
|
For asynchronous code, use the same `expect` function with the `.resolves` or `.rejects` modifier:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { expectAsync } from '@push.rocks/smartexpect';
|
import { expect } from '@push.rocks/smartexpect';
|
||||||
|
|
||||||
const asyncStringFetcher = async (): Promise<string> => {
|
const asyncStringFetcher = async (): Promise<string> => {
|
||||||
return 'async string';
|
return 'async string';
|
||||||
};
|
};
|
||||||
|
|
||||||
const asyncTest = async () => {
|
const asyncTest = async () => {
|
||||||
await expectAsync(asyncStringFetcher()).toBeTypeofString();
|
// Add a timeout to prevent hanging tests
|
||||||
await expectAsync(asyncStringFetcher()).toEqual('async string');
|
await expect(asyncStringFetcher()).resolves.withTimeout(5000).type.toBeTypeofString();
|
||||||
|
await expect(asyncStringFetcher()).resolves.toEqual('async string');
|
||||||
};
|
};
|
||||||
|
|
||||||
asyncTest();
|
asyncTest();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Advanced Usage
|
### Navigating Complex Objects
|
||||||
|
|
||||||
- **Properties and Deep Properties:** Assert the existence of properties and their values.
|
You can navigate complex objects using the `property()` and `arrayItem()` methods:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const testObject = { level1: { level2: 'value' } };
|
const complexObject = {
|
||||||
|
users: [
|
||||||
|
{ id: 1, name: 'Alice', permissions: { admin: true } },
|
||||||
|
{ id: 2, name: 'Bob', permissions: { admin: false } }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
// Property existence
|
// Navigate to a nested property
|
||||||
expect(testObject).toHaveProperty('level1');
|
expect(complexObject)
|
||||||
|
.property('users')
|
||||||
|
.arrayItem(0)
|
||||||
|
.property('name')
|
||||||
|
.toEqual('Alice');
|
||||||
|
|
||||||
// Deep Property existence
|
// Check nested permission
|
||||||
expect(testObject).toHaveDeepProperty(['level1', 'level2']);
|
expect(complexObject)
|
||||||
```
|
.property('users')
|
||||||
|
.arrayItem(0)
|
||||||
|
.property('permissions')
|
||||||
|
.property('admin')
|
||||||
|
.toBeTrue();
|
||||||
|
```
|
||||||
|
|
||||||
- **Conditions and Comparisons:** Allow more intricate assertions like greater than, less than, or matching specific conditions.
|
### Advanced Assertions
|
||||||
|
|
||||||
```typescript
|
#### Properties and Deep Properties
|
||||||
// Greater Than
|
|
||||||
expect(5).toBeGreaterThan(3);
|
|
||||||
|
|
||||||
// Less Than
|
Assert the existence of properties and their values:
|
||||||
expect(3).toBeLessThan(5);
|
|
||||||
|
|
||||||
// Custom conditions
|
```typescript
|
||||||
expect(7).customAssertion(value => value > 5, 'Value is not greater than 5');
|
const testObject = { level1: { level2: 'value' } };
|
||||||
```
|
|
||||||
|
|
||||||
- **Arrays and Objects:** Work seamlessly with arrays and objects, checking for containment, length, or specific values.
|
// Property existence
|
||||||
|
expect(testObject).toHaveProperty('level1');
|
||||||
|
|
||||||
```typescript
|
// Property with specific value
|
||||||
const testArray = [1, 2, 3];
|
expect(testObject).toHaveProperty('level1.level2', 'value');
|
||||||
|
|
||||||
// Containment
|
// Deep Property existence
|
||||||
expect(testArray).toContain(2);
|
expect(testObject).toHaveDeepProperty(['level1', 'level2']);
|
||||||
|
```
|
||||||
|
|
||||||
// Array length
|
#### Conditions and Comparisons
|
||||||
expect(testArray).toHaveLength(3);
|
|
||||||
|
|
||||||
// Object matching
|
Perform more intricate assertions:
|
||||||
expect({ name: 'Test', value: 123 }).toMatchObject({ name: 'Test' });
|
|
||||||
```
|
|
||||||
|
|
||||||
### Handling Promises and Async Operations
|
```typescript
|
||||||
|
// Numeric comparisons
|
||||||
|
expect(5).toBeGreaterThan(3);
|
||||||
|
expect(3).toBeLessThan(5);
|
||||||
|
expect(5).toBeGreaterThanOrEqual(5);
|
||||||
|
expect(5).toBeLessThanOrEqual(5);
|
||||||
|
expect(0.1 + 0.2).toBeCloseTo(0.3, 10); // Floating point comparison with precision
|
||||||
|
|
||||||
`@push.rocks/smartexpect` gracefully integrates with asynchronous operations, providing a `expectAsync` function that handles promise-based assertions. This keeps your tests clean and readable, irrespective of the nature of the code being tested.
|
// Truthiness checks
|
||||||
|
expect(true).toBeTrue();
|
||||||
|
expect(false).toBeFalse();
|
||||||
|
expect('non-empty').toBeTruthy();
|
||||||
|
expect(0).toBeFalsy();
|
||||||
|
|
||||||
### Best Practices
|
// Null/Undefined checks
|
||||||
|
expect(null).toBeNull();
|
||||||
|
expect(undefined).toBeUndefined();
|
||||||
|
expect(null).toBeNullOrUndefined();
|
||||||
|
|
||||||
- **Readability:** Favor clarity and readability by explicitly stating your expectations. `@push.rocks/smartexpect`'s API is designed to be fluent and expressive, making your tests easy to write and, more importantly, easy to read.
|
// Custom conditions
|
||||||
|
expect(7).customAssertion(value => value % 2 === 1, 'Value is not odd');
|
||||||
|
```
|
||||||
|
|
||||||
- **Comprehensive Coverage:** Utilize the full spectrum of assertions provided to cover a broad set of use cases, ensuring your code behaves as expected not just in ideal conditions but across various edge cases.
|
#### Arrays and Collections
|
||||||
|
|
||||||
- **Maintainability:** Group related assertions together to improve test maintainability. This makes it easier to update tests as your codebase evolves.
|
Work seamlessly with arrays and collections:
|
||||||
|
|
||||||
Through judicious use of `@push.rocks/smartexpect`, you can enhance the reliability and maintainability of your test suite, making your codebase more robust and your development workflow more efficient.
|
```typescript
|
||||||
|
const testArray = [1, 2, 3];
|
||||||
|
|
||||||
|
// Array checks
|
||||||
|
expect(testArray).toBeArray();
|
||||||
|
expect(testArray).toHaveLength(3);
|
||||||
|
expect(testArray).toContain(2);
|
||||||
|
expect(testArray).toContainAll([1, 3]);
|
||||||
|
expect(testArray).toExclude(4);
|
||||||
|
expect([]).toBeEmptyArray();
|
||||||
|
expect(testArray).toHaveLengthGreaterThan(2);
|
||||||
|
expect(testArray).toHaveLengthLessThan(4);
|
||||||
|
|
||||||
|
// Deep equality in arrays
|
||||||
|
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Strings
|
||||||
|
|
||||||
|
String-specific checks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
expect('hello world').toStartWith('hello');
|
||||||
|
expect('hello world').toEndWith('world');
|
||||||
|
expect('hello world').toInclude('lo wo');
|
||||||
|
expect('options').toBeOneOf(['choices', 'options', 'alternatives']);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Functions and Exceptions
|
||||||
|
|
||||||
|
Test function behavior and exceptions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const throwingFn = () => { throw new Error('test error'); };
|
||||||
|
expect(throwingFn).toThrow();
|
||||||
|
expect(throwingFn).toThrow(Error);
|
||||||
|
|
||||||
|
const safeFn = () => 'result';
|
||||||
|
expect(safeFn).not.toThrow();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Date Assertions
|
||||||
|
|
||||||
|
Work with dates:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const now = new Date();
|
||||||
|
const past = new Date(Date.now() - 10000);
|
||||||
|
const future = new Date(Date.now() + 10000);
|
||||||
|
|
||||||
|
expect(now).toBeDate();
|
||||||
|
expect(now).toBeAfterDate(past);
|
||||||
|
expect(now).toBeBeforeDate(future);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging Assertions
|
||||||
|
|
||||||
|
The `log()` method is useful for debugging complex assertions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
expect(complexObject)
|
||||||
|
.property('users')
|
||||||
|
.log() // Logs the current value in the assertion chain
|
||||||
|
.arrayItem(0)
|
||||||
|
.log() // Logs the first user
|
||||||
|
.property('permissions')
|
||||||
|
.log() // Logs the permissions object
|
||||||
|
.property('admin')
|
||||||
|
.toBeTrue();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customizing Error Messages
|
||||||
|
|
||||||
|
You can provide custom error messages for more meaningful test failures:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
expect(user.age)
|
||||||
|
.setFailMessage('User age must be at least 18 for adult content')
|
||||||
|
.toBeGreaterThanOrEqual(18);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Matchers
|
||||||
|
|
||||||
|
You can define your own matchers via `expect.extend()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
expect.extend({
|
||||||
|
toBeOdd(received: number) {
|
||||||
|
const pass = received % 2 === 1;
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
`Expected ${received} ${pass ? 'not ' : ''}to be odd`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then use your custom matcher in tests:
|
||||||
|
expect(3).toBeOdd();
|
||||||
|
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.
|
||||||
|
|
||||||
|
- **Precise error messages**: When tests fail, the error messages provide detailed information about what went wrong, including expected vs. actual values.
|
||||||
|
|
||||||
|
- **Property path navigation**: Use the property path methods to navigate complex objects without creating temporary variables.
|
||||||
|
|
||||||
|
- **Comprehensive testing**: Take advantage of the wide range of assertion methods to test various aspects of your code.
|
||||||
|
|
||||||
|
- **Debugging with log()**: Use the `log()` method to see intermediate values in the assertion chain during test development.
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
55
readme.plan.md
Normal file
55
readme.plan.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# 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
|
221
test/test.both.ts
Normal file
221
test/test.both.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { tap } from '@push.rocks/tapbundle';
|
||||||
|
// Import the built library (dist_ts) so all matcher implementations are available
|
||||||
|
import * as smartexpect from '../dist_ts/index.js';
|
||||||
|
|
||||||
|
tap.test('basic type assertions', async () => {
|
||||||
|
// String type checks
|
||||||
|
smartexpect.expect('hello').type.toBeTypeofString();
|
||||||
|
smartexpect.expect(1).not.type.toBeTypeofString();
|
||||||
|
|
||||||
|
// Boolean type checks
|
||||||
|
smartexpect.expect(true).type.toBeTypeofBoolean();
|
||||||
|
smartexpect.expect(false).type.toBeTypeofBoolean();
|
||||||
|
smartexpect.expect(1).not.type.toBeTypeofBoolean();
|
||||||
|
|
||||||
|
// Number type checks
|
||||||
|
smartexpect.expect(42).type.toBeTypeofNumber();
|
||||||
|
smartexpect.expect(true).not.type.toBeTypeofNumber();
|
||||||
|
|
||||||
|
// Generic type checks with new method
|
||||||
|
smartexpect.expect(() => {}).type.toBeTypeOf('function');
|
||||||
|
smartexpect.expect(class Test {}).type.toBeTypeOf('function');
|
||||||
|
smartexpect.expect({}).type.toBeTypeOf('object');
|
||||||
|
smartexpect.expect(Symbol('test')).type.toBeTypeOf('symbol');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('async tests', async (toolsArg) => {
|
||||||
|
const deferred = toolsArg.defer();
|
||||||
|
toolsArg.delayFor(1000).then(() => {
|
||||||
|
deferred.resolve('hello');
|
||||||
|
});
|
||||||
|
// Using .resolves to test promise resolution with timeout
|
||||||
|
await smartexpect.expect(deferred.promise).resolves.withTimeout(2000).type.toBeTypeofString();
|
||||||
|
await smartexpect.expect(deferred.promise).resolves.not.type.toBeTypeofBoolean();
|
||||||
|
|
||||||
|
// Test async timeout handling
|
||||||
|
const longOperation = toolsArg.defer();
|
||||||
|
toolsArg.delayFor(3000).then(() => {
|
||||||
|
longOperation.resolve('completed');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// Assert that resolution must occur within timeout
|
||||||
|
await smartexpect.expect(longOperation.promise).resolves.withTimeout(1000).toBeDefined();
|
||||||
|
throw new Error('Should have timed out');
|
||||||
|
} catch (err) {
|
||||||
|
// Successfully caught timeout error from .withTimeout
|
||||||
|
console.log('Successfully caught timeout:', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('equality and matching assertions', async () => {
|
||||||
|
// Basic equality
|
||||||
|
smartexpect.expect('hithere').object.toEqual('hithere');
|
||||||
|
smartexpect.expect('hithere').not.object.toEqual('hithere2');
|
||||||
|
|
||||||
|
// Object equality
|
||||||
|
const obj1 = { a: 1, b: { c: true } };
|
||||||
|
const obj2 = { a: 1, b: { c: true } };
|
||||||
|
const obj3 = { a: 1, b: { c: false } };
|
||||||
|
smartexpect.expect(obj1).object.toEqual(obj2);
|
||||||
|
smartexpect.expect(obj1).not.object.toEqual(obj3);
|
||||||
|
|
||||||
|
// RegExp matching
|
||||||
|
smartexpect.expect('hithere').string.toMatch(/hi/);
|
||||||
|
smartexpect.expect('hithere').string.toMatch(/^hithere$/);
|
||||||
|
smartexpect.expect('hithere').not.string.toMatch(/ho/);
|
||||||
|
|
||||||
|
// String inclusion
|
||||||
|
smartexpect.expect('hithere').string.toInclude('hit');
|
||||||
|
smartexpect.expect('hithere').not.string.toInclude('missing');
|
||||||
|
|
||||||
|
// String start/end
|
||||||
|
smartexpect.expect('hithere').string.toStartWith('hi');
|
||||||
|
smartexpect.expect('hithere').string.toEndWith('ere');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('object property assertions', async () => {
|
||||||
|
const testObject = {
|
||||||
|
topLevel: 'hello',
|
||||||
|
nested: {
|
||||||
|
prop: 42,
|
||||||
|
deeplyNested: {
|
||||||
|
array: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Basic property checks
|
||||||
|
smartexpect.expect(testObject).object.toHaveProperty('topLevel');
|
||||||
|
smartexpect.expect(testObject).object.toHaveProperty('topLevel', 'hello');
|
||||||
|
smartexpect.expect(testObject).not.object.toHaveProperty('missing');
|
||||||
|
|
||||||
|
// Drill-down property navigation
|
||||||
|
smartexpect.expect(testObject).property('nested').object.toHaveProperty('prop', 42);
|
||||||
|
smartexpect
|
||||||
|
.expect(testObject)
|
||||||
|
.property('nested')
|
||||||
|
.property('deeplyNested')
|
||||||
|
.property('array')
|
||||||
|
.array.toBeArray();
|
||||||
|
|
||||||
|
// Deep property checks
|
||||||
|
smartexpect.expect(testObject).object.toHaveDeepProperty(['nested', 'deeplyNested', 'array']);
|
||||||
|
|
||||||
|
// Array item navigation
|
||||||
|
smartexpect
|
||||||
|
.expect(testObject)
|
||||||
|
.property('nested')
|
||||||
|
.property('deeplyNested')
|
||||||
|
.property('array')
|
||||||
|
.arrayItem(0)
|
||||||
|
.number.toEqual(1); // numeric equality via number namespace
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('numeric comparison assertions', async () => {
|
||||||
|
// Greater/less than
|
||||||
|
smartexpect.expect(4).number.toBeGreaterThan(3);
|
||||||
|
smartexpect.expect(4).number.toBeLessThan(5);
|
||||||
|
smartexpect.expect(4).number.toBeGreaterThanOrEqual(4);
|
||||||
|
smartexpect.expect(4).number.toBeLessThanOrEqual(4);
|
||||||
|
|
||||||
|
// Approximate equality
|
||||||
|
smartexpect.expect(0.1 + 0.2).number.toBeCloseTo(0.3, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('array assertions', async () => {
|
||||||
|
const obj1 = { id: 1 };
|
||||||
|
const obj2 = { id: 2 };
|
||||||
|
const testArray = [1, 'two', obj1, true];
|
||||||
|
|
||||||
|
// Basic array checks
|
||||||
|
smartexpect.expect(testArray).array.toBeArray();
|
||||||
|
smartexpect.expect(testArray).array.toHaveLength(4);
|
||||||
|
|
||||||
|
// Content checks
|
||||||
|
smartexpect.expect(testArray).array.toContain('two');
|
||||||
|
smartexpect.expect(testArray).array.toContain(obj1);
|
||||||
|
smartexpect.expect(testArray).not.array.toContain(obj2);
|
||||||
|
|
||||||
|
// Array with equal items (not same reference)
|
||||||
|
smartexpect.expect([{ a: 1 }, { b: 2 }]).array.toContainEqual({ a: 1 });
|
||||||
|
|
||||||
|
// Multiple values
|
||||||
|
smartexpect.expect(testArray).array.toContainAll([1, 'two']);
|
||||||
|
smartexpect.expect(testArray).array.toExclude('missing');
|
||||||
|
|
||||||
|
// Empty array
|
||||||
|
smartexpect.expect([]).array.toBeEmptyArray();
|
||||||
|
|
||||||
|
// Length comparisons
|
||||||
|
smartexpect.expect(testArray).array.toHaveLengthGreaterThan(3);
|
||||||
|
smartexpect.expect(testArray).array.toHaveLengthLessThan(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('boolean assertions', async () => {
|
||||||
|
// True/False
|
||||||
|
smartexpect.expect(true).boolean.toBeTrue();
|
||||||
|
smartexpect.expect(false).boolean.toBeFalse();
|
||||||
|
|
||||||
|
// Truthy/Falsy
|
||||||
|
smartexpect.expect('something').boolean.toBeTruthy();
|
||||||
|
smartexpect.expect(0).boolean.toBeFalsy();
|
||||||
|
|
||||||
|
// Null/Undefined
|
||||||
|
smartexpect.expect(null).object.toBeNull();
|
||||||
|
smartexpect.expect(undefined).object.toBeUndefined();
|
||||||
|
smartexpect.expect(null).object.toBeNullOrUndefined();
|
||||||
|
smartexpect.expect(undefined).object.toBeNullOrUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('function assertions', async () => {
|
||||||
|
// Function that throws
|
||||||
|
const throwingFn = () => {
|
||||||
|
throw new Error('test error');
|
||||||
|
};
|
||||||
|
smartexpect.expect(throwingFn).function.toThrow();
|
||||||
|
smartexpect.expect(throwingFn).function.toThrow(Error);
|
||||||
|
|
||||||
|
// Function that doesn't throw
|
||||||
|
const nonThrowingFn = () => 'safe';
|
||||||
|
smartexpect.expect(nonThrowingFn).not.function.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('date assertions', async () => {
|
||||||
|
const now = new Date();
|
||||||
|
const past = new Date(Date.now() - 10000);
|
||||||
|
const future = new Date(Date.now() + 10000);
|
||||||
|
|
||||||
|
smartexpect.expect(now).date.toBeDate();
|
||||||
|
smartexpect.expect(now).date.toBeAfterDate(past);
|
||||||
|
smartexpect.expect(now).date.toBeBeforeDate(future);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('custom assertions', async () => {
|
||||||
|
// Custom validation logic
|
||||||
|
smartexpect.expect(42).customAssertion((value) => value % 2 === 0, 'Expected number to be even');
|
||||||
|
|
||||||
|
// With fail message
|
||||||
|
smartexpect.expect('test').setFailMessage('Custom fail message for assertion').string.toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('logging and debugging', async () => {
|
||||||
|
// Using log() for debugging
|
||||||
|
const complexObject = {
|
||||||
|
level1: {
|
||||||
|
level2: {
|
||||||
|
value: 'nested value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// This logs the current value in the chain for debugging
|
||||||
|
smartexpect
|
||||||
|
.expect(complexObject)
|
||||||
|
.property('level1')
|
||||||
|
.property('level2')
|
||||||
|
.log()
|
||||||
|
.property('value')
|
||||||
|
.object.toEqual('nested value');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
32
test/test.diffOutput.ts
Normal file
32
test/test.diffOutput.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { tap, expect as tExpect } from '@push.rocks/tapbundle';
|
||||||
|
import * as smartexpect from '../dist_ts/index.js';
|
||||||
|
|
||||||
|
tap.test('diff-like output for object.toEqual mismatch', async () => {
|
||||||
|
const a = { x: 1, y: 2 };
|
||||||
|
const b = { x: 1, y: 3 };
|
||||||
|
try {
|
||||||
|
smartexpect.expect(a).object.toEqual(b);
|
||||||
|
throw new Error('Assertion did not throw');
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg: string = err.message;
|
||||||
|
tExpect(msg.includes('Expected objects to be deeply equal')).toBeTrue();
|
||||||
|
tExpect(msg.includes('Received:')).toBeTrue();
|
||||||
|
tExpect(msg.includes('"y": 2')).toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('diff-like output for array.toContainEqual mismatch', async () => {
|
||||||
|
const arr = [{ id: 1 }, { id: 2 }];
|
||||||
|
const item = { id: 3 };
|
||||||
|
try {
|
||||||
|
smartexpect.expect(arr).array.toContainEqual(item);
|
||||||
|
throw new Error('Assertion did not throw');
|
||||||
|
} catch (err: any) {
|
||||||
|
const msg: string = err.message;
|
||||||
|
tExpect(msg.includes('Expected array to contain equal to')).toBeTrue();
|
||||||
|
tExpect(msg.includes('Received:')).toBeTrue();
|
||||||
|
tExpect(msg.includes('"id": 1')).toBeTrue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
38
test/test.expectAny.ts
Normal file
38
test/test.expectAny.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { tap } from '@push.rocks/tapbundle';
|
||||||
|
import * as smartexpect from '../dist_ts/index.js';
|
||||||
|
|
||||||
|
tap.test('expect.any and expect.anything basic usage', async () => {
|
||||||
|
const obj = { a: 1, b: 'two', d: new Date() };
|
||||||
|
// Using expect.any to match types
|
||||||
|
smartexpect.expect(obj).object.toMatchObject({
|
||||||
|
a: smartexpect.expect.any(Number),
|
||||||
|
b: smartexpect.expect.any(String),
|
||||||
|
d: smartexpect.expect.any(Date),
|
||||||
|
});
|
||||||
|
// Using expect.anything to match any defined value
|
||||||
|
smartexpect.expect(obj).object.toMatchObject({
|
||||||
|
a: smartexpect.expect.anything(),
|
||||||
|
b: smartexpect.expect.anything(),
|
||||||
|
d: smartexpect.expect.anything(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('expect.any mismatch and anything null/undefined rejection', async () => {
|
||||||
|
const obj = { a: 1, b: null };
|
||||||
|
// Mismatch for expect.any
|
||||||
|
try {
|
||||||
|
smartexpect.expect(obj).object.toMatchObject({ a: smartexpect.expect.any(String) });
|
||||||
|
throw new Error('Expected mismatch for expect.any did not throw');
|
||||||
|
} catch (err) {
|
||||||
|
// success: thrown on mismatch
|
||||||
|
}
|
||||||
|
// anything should reject null or undefined
|
||||||
|
try {
|
||||||
|
smartexpect.expect(obj).object.toMatchObject({ b: smartexpect.expect.anything() });
|
||||||
|
throw new Error('Expected anything() to reject null or undefined');
|
||||||
|
} catch (err) {
|
||||||
|
// success: thrown on null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
22
test/test.negation.ts
Normal file
22
test/test.negation.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { tap, expect as tExpect } from '@push.rocks/tapbundle';
|
||||||
|
import * as smartexpect from '../dist_ts/index.js';
|
||||||
|
|
||||||
|
tap.test('negation message for numeric matcher', async () => {
|
||||||
|
try {
|
||||||
|
smartexpect.expect(5).not.toBeGreaterThan(3);
|
||||||
|
throw new Error('Assertion did not throw');
|
||||||
|
} catch (err: any) {
|
||||||
|
tExpect(err.message).toEqual('Expected number not to be greater than 3');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('negation message for string matcher', async () => {
|
||||||
|
try {
|
||||||
|
smartexpect.expect('hello').not.string.toInclude('he');
|
||||||
|
throw new Error('Assertion did not throw');
|
||||||
|
} catch (err: any) {
|
||||||
|
tExpect(err.message).toEqual('Expected string not to include "he"');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
52
test/test.ts
52
test/test.ts
@ -1,52 +0,0 @@
|
|||||||
import { tap } from '@push.rocks/tapbundle';
|
|
||||||
import * as smartexpect from '../ts/index.js';
|
|
||||||
|
|
||||||
tap.test('sync tests', async () => {
|
|
||||||
smartexpect.expect('hello').toBeTypeofString();
|
|
||||||
smartexpect.expect(1).not.toBeTypeofString();
|
|
||||||
smartexpect.expect(true).toBeTypeofBoolean();
|
|
||||||
smartexpect.expect(true).not.toBeTypeofNumber();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('async tests', async (toolsArg) => {
|
|
||||||
const deferred = toolsArg.defer();
|
|
||||||
toolsArg.delayFor(4000).then(() => {
|
|
||||||
deferred.resolve('hello');
|
|
||||||
});
|
|
||||||
await smartexpect.expectAsync(deferred.promise).timeout(5000).toBeTypeofString();
|
|
||||||
await smartexpect.expectAsync(deferred.promise).not.toBeTypeofBoolean();
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should check equality', async () => {
|
|
||||||
smartexpect.expect('hithere').toEqual('hithere');
|
|
||||||
smartexpect.expect('hithere').not.toEqual('hithere2');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should check for regexp matching', async () => {
|
|
||||||
smartexpect.expect('hithere').toMatch(/hi/);
|
|
||||||
smartexpect.expect('hithere').not.toMatch(/ho/);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should correctly state property presence', async () => {
|
|
||||||
const testObject = {
|
|
||||||
aprop: 'hello',
|
|
||||||
};
|
|
||||||
|
|
||||||
smartexpect.expect(testObject).toHaveProperty('aprop');
|
|
||||||
smartexpect.expect(testObject).not.toHaveProperty('aprop2');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should be greater than', async () => {
|
|
||||||
smartexpect.expect(4).toBeGreaterThan(3);
|
|
||||||
smartexpect.expect(4).toBeLessThan(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('should correctly determine toContain', async () => {
|
|
||||||
const hello = {
|
|
||||||
socool: 'yes',
|
|
||||||
};
|
|
||||||
const testArray = [hello];
|
|
||||||
smartexpect.expect(testArray).toContain(hello);
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.start();
|
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartexpect',
|
name: '@push.rocks/smartexpect',
|
||||||
version: '1.2.1',
|
version: '2.2.2',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
49
ts/index.ts
49
ts/index.ts
@ -1,12 +1,41 @@
|
|||||||
import { Assertion } from './smartexpect.classes.assertion.js';
|
import { Assertion, AnyMatcher, AnythingMatcher } from './smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from './types.js';
|
||||||
|
// import type { TMatcher } from './smartexpect.classes.assertion.js'; // unused
|
||||||
|
|
||||||
export const expect = (baseArg: any) => {
|
/**
|
||||||
const assertion = new Assertion(baseArg, 'sync');
|
* Primary entry point for assertions.
|
||||||
return assertion;
|
* Automatically detects Promises to support async assertions.
|
||||||
};
|
*/
|
||||||
|
/**
|
||||||
export const expectAsync = (baseArg: any) => {
|
* The `expect` function interface. Supports custom matchers via .extend.
|
||||||
const assertion = new Assertion(baseArg, 'async');
|
*/
|
||||||
return assertion;
|
/**
|
||||||
};
|
* Entry point for assertions.
|
||||||
|
* Automatically detects Promises to support async assertions.
|
||||||
|
*/
|
||||||
|
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, TExecutionType>(value, mode);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Register custom matchers.
|
||||||
|
*/
|
||||||
|
export namespace expect {
|
||||||
|
export const extend = Assertion.extend;
|
||||||
|
/**
|
||||||
|
* Matcher for a specific constructor. Passes if value is instance of given constructor.
|
||||||
|
*/
|
||||||
|
export function any(constructor: any) {
|
||||||
|
return new AnyMatcher(constructor);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Matcher for any defined value (not null or undefined).
|
||||||
|
*/
|
||||||
|
export function anything() {
|
||||||
|
return new AnythingMatcher();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
81
ts/namespaces/array.ts
Normal file
81
ts/namespaces/array.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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, M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<T[], M>) {}
|
||||||
|
|
||||||
|
toBeArray() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => Array.isArray(value),
|
||||||
|
`Expected value to be array`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toHaveLength(length: number) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as T[]).length === length,
|
||||||
|
`Expected array to have length ${length}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toContain(item: T) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as T[]).includes(item),
|
||||||
|
`Expected array to contain ${JSON.stringify(item)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toContainEqual(item: T) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as T[]).some((e) => plugins.fastDeepEqual(e, item)),
|
||||||
|
(value) =>
|
||||||
|
`Expected array to contain equal to ${JSON.stringify(item)}` +
|
||||||
|
`\nReceived: ${JSON.stringify(value, null, 2)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toContainAll(items: T[]) {
|
||||||
|
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.customAssertion(
|
||||||
|
(value) => !(value as T[]).includes(item),
|
||||||
|
`Expected array to exclude ${JSON.stringify(item)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeEmptyArray() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => Array.isArray(value) && (value as T[]).length === 0,
|
||||||
|
`Expected array to be empty`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Alias for empty array check
|
||||||
|
*/
|
||||||
|
toBeEmpty() {
|
||||||
|
return this.toBeEmptyArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
toHaveLengthGreaterThan(length: number) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as T[]).length > length,
|
||||||
|
`Expected array to have length greater than ${length}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toHaveLengthLessThan(length: number) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as T[]).length < length,
|
||||||
|
`Expected array to have length less than ${length}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
ts/namespaces/boolean.ts
Normal file
37
ts/namespaces/boolean.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace for boolean-specific matchers
|
||||||
|
*/
|
||||||
|
export class BooleanMatchers<M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<boolean, M>) {}
|
||||||
|
|
||||||
|
toBeTrue() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => v === true,
|
||||||
|
`Expected value to be true`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeFalse() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => v === false,
|
||||||
|
`Expected value to be false`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeTruthy() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => Boolean(v),
|
||||||
|
`Expected value to be truthy`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeFalsy() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => !v,
|
||||||
|
`Expected value to be falsy`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
ts/namespaces/date.ts
Normal file
30
ts/namespaces/date.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace for date-specific matchers
|
||||||
|
*/
|
||||||
|
export class DateMatchers<M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<Date, M>) {}
|
||||||
|
|
||||||
|
toBeDate() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => v instanceof Date,
|
||||||
|
`Expected value to be a Date instance`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeBeforeDate(date: 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.customAssertion(
|
||||||
|
(v) => v instanceof Date && (v as Date).getTime() > date.getTime(),
|
||||||
|
`Expected date to be after ${date.toISOString()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
ts/namespaces/function.ts
Normal file
62
ts/namespaces/function.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace for function-specific matchers
|
||||||
|
*/
|
||||||
|
export class FunctionMatchers<M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<Function, M>) {}
|
||||||
|
|
||||||
|
toThrow(expectedError?: any) {
|
||||||
|
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}` : ''}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
8
ts/namespaces/index.ts
Normal file
8
ts/namespaces/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export { StringMatchers } from './string.js';
|
||||||
|
export { ArrayMatchers } from './array.js';
|
||||||
|
export { NumberMatchers } from './number.js';
|
||||||
|
export { BooleanMatchers } from './boolean.js';
|
||||||
|
export { ObjectMatchers } from './object.js';
|
||||||
|
export { FunctionMatchers } from './function.js';
|
||||||
|
export { DateMatchers } from './date.js';
|
||||||
|
export { TypeMatchers } from './type.js';
|
84
ts/namespaces/number.ts
Normal file
84
ts/namespaces/number.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace for number-specific matchers
|
||||||
|
*/
|
||||||
|
export class NumberMatchers<M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<number, M>) {}
|
||||||
|
|
||||||
|
toBeGreaterThan(value: number) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => (v as number) > value,
|
||||||
|
`Expected number to be greater than ${value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeLessThan(value: number) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => (v as number) < value,
|
||||||
|
`Expected number to be less than ${value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeGreaterThanOrEqual(value: number) {
|
||||||
|
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.customAssertion(
|
||||||
|
(v) => (v as number) <= value,
|
||||||
|
`Expected number to be less than or equal to ${value}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeCloseTo(value: number, precision?: number) {
|
||||||
|
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.customAssertion(
|
||||||
|
(v) => (v as number) === value,
|
||||||
|
`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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
133
ts/namespaces/object.ts
Normal file
133
ts/namespaces/object.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
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, M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<T, M>) {}
|
||||||
|
|
||||||
|
toEqual(expected: any) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => plugins.fastDeepEqual(v, 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)) {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
(v) =>
|
||||||
|
`Expected object to match properties ${JSON.stringify(expected, null, 2)}` +
|
||||||
|
`\nReceived: ${JSON.stringify(v, null, 2)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeInstanceOf(constructor: any) {
|
||||||
|
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.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.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.customAssertion(
|
||||||
|
(v) => v === null,
|
||||||
|
`Expected value to be null`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeUndefined() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => v === undefined,
|
||||||
|
`Expected value to be undefined`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeNullOrUndefined() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => v === null || v === undefined,
|
||||||
|
`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)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
ts/namespaces/string.ts
Normal file
60
ts/namespaces/string.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace for string-specific matchers
|
||||||
|
*/
|
||||||
|
export class StringMatchers<M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<string, M>) {}
|
||||||
|
|
||||||
|
toStartWith(prefix: string) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as string).startsWith(prefix),
|
||||||
|
`Expected string to start with "${prefix}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toEndWith(suffix: string) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as string).endsWith(suffix),
|
||||||
|
`Expected string to end with "${suffix}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toInclude(substring: string) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => (value as string).includes(substring),
|
||||||
|
`Expected string to include "${substring}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toMatch(regex: RegExp) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(value) => regex.test(value as string),
|
||||||
|
`Expected string to match ${regex}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeOneOf(values: string[]) {
|
||||||
|
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.customAssertion(
|
||||||
|
(value) => (value as string).length === length,
|
||||||
|
`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`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
44
ts/namespaces/type.ts
Normal file
44
ts/namespaces/type.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Assertion } from '../smartexpect.classes.assertion.js';
|
||||||
|
import type { TExecutionType } from '../types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace for type-based matchers
|
||||||
|
*/
|
||||||
|
export class TypeMatchers<M extends TExecutionType> {
|
||||||
|
constructor(private assertion: Assertion<any, M>) {}
|
||||||
|
|
||||||
|
toBeTypeofString() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => typeof v === 'string',
|
||||||
|
`Expected type to be 'string'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeTypeofNumber() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => typeof v === 'number',
|
||||||
|
`Expected type to be 'number'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeTypeofBoolean() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => typeof v === 'boolean',
|
||||||
|
`Expected type to be 'boolean'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeTypeOf(typeName: string) {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => typeof v === typeName,
|
||||||
|
`Expected type to be '${typeName}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeDefined() {
|
||||||
|
return this.assertion.customAssertion(
|
||||||
|
(v) => v !== undefined,
|
||||||
|
`Expected value to be defined`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
import * as smartdelay from '@push.rocks/smartdelay';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
import * as smartpromise from '@push.rocks/smartpromise';
|
import * as smartpromise from '@push.rocks/smartpromise';
|
||||||
|
|
||||||
export { smartdelay, smartpromise };
|
export { smartdelay, smartpromise };
|
||||||
|
|
||||||
// third party scope
|
// third party utilities
|
||||||
import fastDeepEqual from 'fast-deep-equal';
|
import fastDeepEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
export { fastDeepEqual };
|
export { fastDeepEqual };
|
@ -1,572 +1,407 @@
|
|||||||
import * as plugins from './smartexpect.plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
import {
|
||||||
|
StringMatchers,
|
||||||
|
ArrayMatchers,
|
||||||
|
NumberMatchers,
|
||||||
|
BooleanMatchers,
|
||||||
|
ObjectMatchers,
|
||||||
|
FunctionMatchers,
|
||||||
|
DateMatchers,
|
||||||
|
TypeMatchers,
|
||||||
|
} from './namespaces/index.js';
|
||||||
|
/**
|
||||||
|
* Definition of a custom matcher function.
|
||||||
|
* Should return an object with `pass` and optional `message`.
|
||||||
|
*/
|
||||||
|
import type { TMatcher, TExecutionType } from './types.js';
|
||||||
|
|
||||||
export type TExecutionType = 'sync' | 'async';
|
/**
|
||||||
|
* Core assertion class. Generic over the current value type T.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Internal matcher classes for expect.any and expect.anything
|
||||||
|
*/
|
||||||
|
export class AnyMatcher {
|
||||||
|
constructor(public expectedConstructor: any) {}
|
||||||
|
}
|
||||||
|
export class AnythingMatcher {}
|
||||||
|
|
||||||
export class Assertion {
|
export class Assertion<T = unknown, M extends TExecutionType = 'sync'> {
|
||||||
executionMode: TExecutionType;
|
executionMode: M;
|
||||||
baseReference: any;
|
baseReference: any;
|
||||||
propertyDrillDown: string[] = [];
|
propertyDrillDown: Array<string | number> = [];
|
||||||
|
|
||||||
private notSetting = false;
|
private notSetting = false;
|
||||||
private timeoutSetting = 0;
|
private timeoutSetting = 0;
|
||||||
constructor(baseReferenceArg: any, executionModeArg: TExecutionType) {
|
/** Registry of user-defined custom matchers */
|
||||||
|
private static customMatchers: Record<string, TMatcher> = {};
|
||||||
|
/** Flag for Promise rejection assertions */
|
||||||
|
private isRejects = false;
|
||||||
|
/** Flag for Promise resolution assertions (default for async) */
|
||||||
|
private isResolves = false;
|
||||||
|
private failMessage: string;
|
||||||
|
private successMessage: string;
|
||||||
|
/** Computed negation failure message for the current assertion */
|
||||||
|
private negativeMessage: string;
|
||||||
|
|
||||||
|
constructor(baseReferenceArg: any, executionModeArg: M) {
|
||||||
this.baseReference = baseReferenceArg;
|
this.baseReference = baseReferenceArg;
|
||||||
this.executionMode = executionModeArg;
|
this.executionMode = executionModeArg;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Register custom matchers to be available on all assertions.
|
||||||
|
* @param matchers An object whose keys are matcher names and values are matcher functions.
|
||||||
|
*/
|
||||||
|
public static extend(matchers: Record<string, TMatcher>): void {
|
||||||
|
for (const [name, fn] of Object.entries(matchers)) {
|
||||||
|
if ((Assertion.prototype as any)[name]) {
|
||||||
|
throw new Error(`Cannot extend. Matcher '${name}' already exists on Assertion.`);
|
||||||
|
}
|
||||||
|
// store in registry
|
||||||
|
Assertion.customMatchers[name] = fn;
|
||||||
|
// add method to prototype
|
||||||
|
(Assertion.prototype as any)[name] = function (...args: any[]) {
|
||||||
|
return this.runCheck(() => {
|
||||||
|
const received = this.getObjectToTestReference();
|
||||||
|
const result = fn(received, ...args);
|
||||||
|
const pass = result.pass;
|
||||||
|
const msg = result.message;
|
||||||
|
if (!pass) {
|
||||||
|
const message = typeof msg === 'function' ? msg() : msg;
|
||||||
|
throw new Error(message || `Custom matcher '${name}' failed`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getObjectToTestReference() {
|
private getObjectToTestReference() {
|
||||||
let returnObjectToTestReference = this.baseReference;
|
let returnObjectToTestReference = this.baseReference;
|
||||||
for (const property of this.propertyDrillDown) {
|
for (const property of this.propertyDrillDown) {
|
||||||
|
if (returnObjectToTestReference == null) {
|
||||||
|
// if it's null or undefined, stop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We just directly access with bracket notation.
|
||||||
|
// If property is a string, it's like obj["someProp"];
|
||||||
|
// If property is a number, it's like obj[0].
|
||||||
returnObjectToTestReference = returnObjectToTestReference[property];
|
returnObjectToTestReference = returnObjectToTestReference[property];
|
||||||
}
|
}
|
||||||
return returnObjectToTestReference;
|
return returnObjectToTestReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatDrillDown(): string {
|
||||||
|
if (!this.propertyDrillDown || this.propertyDrillDown.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = this.propertyDrillDown.map(prop => {
|
||||||
|
if (typeof prop === 'number') {
|
||||||
|
return `[${prop}]`;
|
||||||
|
} else {
|
||||||
|
return `.${prop}`;
|
||||||
|
}
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatValue(value: any): string {
|
||||||
|
if (value === null) {
|
||||||
|
return 'null';
|
||||||
|
} else if (value === undefined) {
|
||||||
|
return 'undefined';
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} catch (e) {
|
||||||
|
return `[Object ${value.constructor.name}]`;
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'function') {
|
||||||
|
return `[Function${value.name ? ': ' + value.name : ''}]`;
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
return `"${value}"`;
|
||||||
|
} else {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createErrorMessage(message: string): string {
|
||||||
|
if (this.failMessage) {
|
||||||
|
return this.failMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testValue = this.getObjectToTestReference();
|
||||||
|
const formattedValue = this.formatValue(testValue);
|
||||||
|
const drillDown = this.formatDrillDown();
|
||||||
|
|
||||||
|
// Replace placeholders in the message
|
||||||
|
return message
|
||||||
|
.replace('{value}', formattedValue)
|
||||||
|
.replace('{path}', drillDown || '');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Compute a negated failure message by inserting 'not' into the positive message.
|
||||||
|
*/
|
||||||
|
private computeNegationMessage(message: string): string {
|
||||||
|
const idx = message.indexOf(' to ');
|
||||||
|
if (idx !== -1) {
|
||||||
|
return message.slice(0, idx) + ' not' + message.slice(idx);
|
||||||
|
}
|
||||||
|
return 'Negated: ' + message;
|
||||||
|
}
|
||||||
|
|
||||||
public get not() {
|
public get not() {
|
||||||
this.notSetting = true;
|
this.notSetting = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Assert that a Promise resolves.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `.withTimeout(ms)` instead for clarity
|
||||||
|
* Set a timeout (in ms) for async assertions (Promise must settle before timeout).
|
||||||
|
*/
|
||||||
public timeout(millisArg: number) {
|
public timeout(millisArg: number) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[DEPRECATED] .timeout() is deprecated. Use .withTimeout(ms)');
|
||||||
|
this.timeoutSetting = millisArg;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set a timeout (in ms) for async assertions (Promise must settle before timeout).
|
||||||
|
*/
|
||||||
|
public withTimeout(millisArg: number) {
|
||||||
this.timeoutSetting = millisArg;
|
this.timeoutSetting = millisArg;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private failMessage: string;
|
|
||||||
public setFailMessage(failMessageArg: string) {
|
public setFailMessage(failMessageArg: string) {
|
||||||
this.failMessage = failMessageArg;
|
this.failMessage = failMessageArg;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private successMessage: string;
|
|
||||||
public setSuccessMessage(successMessageArg: string) {
|
public setSuccessMessage(successMessageArg: string) {
|
||||||
this.successMessage = successMessageArg;
|
this.successMessage = successMessageArg;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private runCheck(checkFunction: () => any) {
|
// 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) => {
|
const runDirectOrNegated = (checkFunction: () => any) => {
|
||||||
if (!this.notSetting) {
|
if (!this.notSetting) {
|
||||||
return checkFunction();
|
return checkFunction();
|
||||||
} else {
|
} else {
|
||||||
let isOk = false;
|
let isOk = false;
|
||||||
try {
|
try {
|
||||||
runDirectOrNegated(checkFunction());
|
// attempt positive assertion and expect it to throw
|
||||||
|
checkFunction();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isOk = true;
|
isOk = true;
|
||||||
}
|
}
|
||||||
if (!isOk) {
|
if (!isOk) {
|
||||||
throw new Error(this.failMessage || 'Negated assertion is not ok!');
|
const msg = this.failMessage || this.negativeMessage || 'Negated assertion failed';
|
||||||
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.executionMode === 'async') {
|
if (this.executionMode === 'async') {
|
||||||
const done = plugins.smartpromise.defer();
|
const done = plugins.smartpromise.defer<Assertion<T, M>>();
|
||||||
if (!(this.baseReference instanceof Promise)) {
|
const isThenable = this.baseReference && typeof (this.baseReference as any).then === 'function';
|
||||||
done.reject(new Error(`${this.baseReference} is not of type promise.`));
|
if (!isThenable) {
|
||||||
} else {
|
done.reject(new Error(`Expected a Promise but received: ${this.formatValue(this.baseReference)}`));
|
||||||
if (this.timeoutSetting) {
|
return done.promise;
|
||||||
plugins.smartdelay.delayFor(this.timeoutSetting).then(() => {
|
}
|
||||||
if (done.status === 'pending') {
|
if (this.timeoutSetting) {
|
||||||
done.reject(new Error(`${this.baseReference} timed out at ${this.timeoutSetting}!`));
|
plugins.smartdelay.delayFor(this.timeoutSetting).then(() => {
|
||||||
}
|
if (done.status === 'pending') {
|
||||||
});
|
done.reject(new Error(`Promise timed out after ${this.timeoutSetting}ms`));
|
||||||
}
|
}
|
||||||
this.baseReference.then((promiseResultArg) => {
|
|
||||||
this.baseReference = promiseResultArg;
|
|
||||||
done.resolve(runDirectOrNegated(checkFunction));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return done.promise;
|
if (this.isRejects) {
|
||||||
} else {
|
(this.baseReference as Promise<any>).then(
|
||||||
return runDirectOrNegated(checkFunction);
|
(res: any) => {
|
||||||
|
done.reject(new Error(`Expected Promise to reject but it resolved with ${this.formatValue(res)}`));
|
||||||
|
},
|
||||||
|
(err: any) => {
|
||||||
|
this.baseReference = err;
|
||||||
|
try {
|
||||||
|
runDirectOrNegated(checkFunction);
|
||||||
|
done.resolve(this);
|
||||||
|
} catch (e: any) {
|
||||||
|
done.reject(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
(this.baseReference as Promise<any>).then(
|
||||||
|
(res: any) => {
|
||||||
|
this.baseReference = res;
|
||||||
|
try {
|
||||||
|
runDirectOrNegated(checkFunction);
|
||||||
|
done.resolve(this);
|
||||||
|
} catch (e: any) {
|
||||||
|
done.reject(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(err: any) => {
|
||||||
|
done.reject(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// return a promise resolving to this for chaining
|
||||||
|
return done.promise.then(() => this) as any;
|
||||||
}
|
}
|
||||||
|
// sync: run and return this for chaining
|
||||||
|
runDirectOrNegated(checkFunction);
|
||||||
|
return this as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if the given object is defined
|
* Execute a custom assertion. Returns a Promise in async mode, else returns this.
|
||||||
*/
|
*/
|
||||||
public toBeDefined() {
|
public customAssertion(
|
||||||
|
assertionFunction: (value: any) => boolean,
|
||||||
|
errorMessage: string | ((value: any) => string)
|
||||||
|
): 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);
|
||||||
|
}
|
||||||
return this.runCheck(() => {
|
return this.runCheck(() => {
|
||||||
if (this.getObjectToTestReference() === undefined) {
|
const value = this.getObjectToTestReference();
|
||||||
throw new Error(
|
if (!assertionFunction(value)) {
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not defined`
|
const msg = this.failMessage
|
||||||
);
|
|| (typeof errorMessage === 'function' ? errorMessage(value) : errorMessage);
|
||||||
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
});
|
}) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if the given object is not defined
|
* Drill into a property of an object.
|
||||||
|
* @param propertyName Name of the property to navigate into.
|
||||||
|
* @returns Assertion of the property type.
|
||||||
*/
|
*/
|
||||||
public toBeTypeofString() {
|
public property<K extends keyof NonNullable<T>>(propertyName: K): Assertion<NonNullable<T>[K], M> {
|
||||||
return this.runCheck(() => {
|
this.propertyDrillDown.push(propertyName as string);
|
||||||
if (typeof this.getObjectToTestReference() !== 'string') {
|
return this as unknown as Assertion<NonNullable<T>[K], M>;
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `Assertion failed: ${this.baseReference} with drill down ${
|
|
||||||
this.propertyDrillDown
|
|
||||||
} is not of type string, but typeof ${typeof this.baseReference}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toBeTypeofNumber() {
|
/**
|
||||||
return this.runCheck(() => {
|
* Drill into an array element by index.
|
||||||
if (typeof this.getObjectToTestReference() !== 'number') {
|
* @param index Index of the array item.
|
||||||
throw new Error(
|
* @returns Assertion of the element type.
|
||||||
this.failMessage || `Assertion failed: ${this.baseReference} with drill down ${
|
*/
|
||||||
this.propertyDrillDown
|
public arrayItem(index: number): Assertion<T extends Array<infer U> ? U : unknown, M> {
|
||||||
} is not of type string, but typeof ${typeof this.baseReference}`
|
this.propertyDrillDown.push(index);
|
||||||
);
|
return this as unknown as Assertion<T extends Array<infer U> ? U : unknown, M>;
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toBeTypeofBoolean() {
|
public log() {
|
||||||
return this.runCheck(() => {
|
console.log(`Current value:`);
|
||||||
if (typeof this.getObjectToTestReference() !== 'boolean') {
|
console.log(JSON.stringify(this.getObjectToTestReference(), null, 2));
|
||||||
throw new Error(
|
console.log(`Path: ${this.formatDrillDown() || '(root)'}`);
|
||||||
this.failMessage || `Assertion failed: ${this.baseReference} with drill down ${
|
|
||||||
this.propertyDrillDown
|
|
||||||
} is not of type string, but typeof ${typeof this.baseReference}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toEqual(comparisonObject: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = plugins.fastDeepEqual(this.getObjectToTestReference(), comparisonObject);
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not equal ${comparisonObject}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toMatch(comparisonObject: RegExp) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = comparisonObject.test(this.getObjectToTestReference());
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not equal ${comparisonObject}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeTrue() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result =
|
|
||||||
typeof this.getObjectToTestReference() === 'boolean' &&
|
|
||||||
this.getObjectToTestReference() === true;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not true or not of type boolean`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeFalse() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result =
|
|
||||||
typeof this.getObjectToTestReference() === 'boolean' &&
|
|
||||||
this.getObjectToTestReference() === false;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not false or not of type boolean`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeInstanceOf(classArg: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = this.getObjectToTestReference() instanceof classArg;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not an instance of ${classArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHaveProperty(propertyArg: string, equalsArg?: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = !!this.getObjectToTestReference()[propertyArg];
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not have property ${propertyArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (equalsArg) {
|
|
||||||
if (result !== equalsArg) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does have property ${propertyArg}, but it does not equal ${equalsArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHaveDeepProperty(properties: string[]) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
let obj = this.getObjectToTestReference();
|
|
||||||
let currentPath = '';
|
|
||||||
|
|
||||||
for (const property of properties) {
|
|
||||||
if (currentPath) {
|
|
||||||
currentPath += `.${property}`;
|
|
||||||
} else {
|
|
||||||
currentPath = property;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!obj || !(property in obj)) {
|
|
||||||
throw new Error(this.failMessage || `Missing property at path "${currentPath}" in ${this.baseReference}`);
|
|
||||||
}
|
|
||||||
obj = obj[property];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeGreaterThan(numberArg: number) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = this.getObjectToTestReference() > numberArg;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not greater than ${numberArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeLessThan(numberArg: number) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = this.getObjectToTestReference() < numberArg;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not less than ${numberArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeNull() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = this.getObjectToTestReference() === null;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not null`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeUndefined() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = this.getObjectToTestReference() === undefined;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not undefined`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeNullOrUndefined() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result =
|
|
||||||
this.getObjectToTestReference() === null || this.getObjectToTestReference() === undefined;
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not null or undefined`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array
|
|
||||||
|
|
||||||
public toContain(itemArg: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result =
|
|
||||||
this.getObjectToTestReference() instanceof Array &&
|
|
||||||
this.getObjectToTestReference().includes(itemArg);
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not contain ${itemArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeEmptyArray() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const arrayRef = this.getObjectToTestReference();
|
|
||||||
if (!Array.isArray(arrayRef) || arrayRef.length !== 0) {
|
|
||||||
throw new Error(this.failMessage || `Expected ${this.baseReference} to be an empty array, but it was not.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toContainAll(values: any[]) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const arrayRef = this.getObjectToTestReference();
|
|
||||||
if (!Array.isArray(arrayRef)) {
|
|
||||||
throw new Error(this.failMessage || `Expected ${this.baseReference} to be an array.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const value of values) {
|
|
||||||
if (!arrayRef.includes(value)) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `Expected ${this.baseReference} to include value "${value}", but it did not.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toExclude(value: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const arrayRef = this.getObjectToTestReference();
|
|
||||||
if (!Array.isArray(arrayRef)) {
|
|
||||||
throw new Error(this.failMessage || `Expected ${this.baseReference} to be an array.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arrayRef.includes(value)) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `Expected ${this.baseReference} to exclude value "${value}", but it included it.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toStartWith(itemArg: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const testObject = this.getObjectToTestReference();
|
|
||||||
const result = typeof testObject === 'string' && testObject.startsWith(itemArg);
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not contain ${itemArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toEndWith(itemArg: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const testObject = this.getObjectToTestReference();
|
|
||||||
const result = typeof testObject === 'string' && testObject.endsWith(itemArg);
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not contain ${itemArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... previous code ...
|
|
||||||
|
|
||||||
public toBeOneOf(values: any[]) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const result = values.includes(this.getObjectToTestReference());
|
|
||||||
if (!result) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not one of ${values}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHaveLength(length: number) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const obj = this.getObjectToTestReference();
|
|
||||||
if (typeof obj.length !== 'number' || obj.length !== length) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not have a length of ${length}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeCloseTo(value: number, precision = 2) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const difference = Math.abs(this.getObjectToTestReference() - value);
|
|
||||||
if (difference > Math.pow(10, -precision) / 2) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not close to ${value} up to ${precision} decimal places`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toThrow(expectedError?: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
let thrown = false;
|
|
||||||
try {
|
|
||||||
this.getObjectToTestReference()();
|
|
||||||
} catch (e) {
|
|
||||||
thrown = true;
|
|
||||||
if (expectedError && !(e instanceof expectedError)) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `Expected function to throw ${expectedError.name}, but it threw ${e.name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!thrown) {
|
|
||||||
throw new Error(`Expected function to throw, but it didn't.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeTruthy() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (!this.getObjectToTestReference()) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not truthy`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeFalsy() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (this.getObjectToTestReference()) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not falsy`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeGreaterThanOrEqual(numberArg: number) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (this.getObjectToTestReference() <= numberArg) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not greater than or equal to ${numberArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeLessThanOrEqual(numberArg: number) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (this.getObjectToTestReference() >= numberArg) {
|
|
||||||
throw new Error(
|
|
||||||
`${this.baseReference} with drill down ${this.propertyDrillDown} is not less than or equal to ${numberArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toMatchObject(objectArg: object) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const partialMatch = plugins.fastDeepEqual(this.getObjectToTestReference(), objectArg); // Note: Implement a deep comparison function or use one from a library
|
|
||||||
if (!partialMatch) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not match the object ${objectArg}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toContainEqual(value: any) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const arr = this.getObjectToTestReference();
|
|
||||||
const found = arr.some((item: any) => plugins.fastDeepEqual(item, value)); // Assuming fastDeepEqual checks deep equality
|
|
||||||
if (!found) {
|
|
||||||
throw new Error(
|
|
||||||
`${this.baseReference} with drill down ${this.propertyDrillDown} does not contain the value ${value}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeArray() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (!Array.isArray(this.getObjectToTestReference())) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not an array`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toInclude(substring: string) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (!this.getObjectToTestReference().includes(substring)) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not include the substring ${substring}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHaveLengthGreaterThan(length: number) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const obj = this.getObjectToTestReference();
|
|
||||||
if (typeof obj.length !== 'number' || obj.length <= length) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not have a length greater than ${length}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toHaveLengthLessThan(length: number) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
const obj = this.getObjectToTestReference();
|
|
||||||
if (typeof obj.length !== 'number' || obj.length >= length) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not have a length less than ${length}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeDate() {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (!(this.getObjectToTestReference() instanceof Date)) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not a date`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeBeforeDate(date: Date) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (!(this.getObjectToTestReference() < date)) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not before ${date}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBeAfterDate(date: Date) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (!(this.getObjectToTestReference() > date)) {
|
|
||||||
throw new Error(
|
|
||||||
this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not after ${date}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public customAssertion(assertionFunction: (value: any) => boolean, errorMessage: string) {
|
|
||||||
return this.runCheck(() => {
|
|
||||||
if (!assertionFunction(this.getObjectToTestReference())) {
|
|
||||||
throw new Error(this.failMessage || errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public property(propertyNameArg: string) {
|
|
||||||
this.propertyDrillDown.push(propertyNameArg);
|
|
||||||
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
|
||||||
|
/** String-specific matchers */
|
||||||
|
public get string(): StringMatchers<M> {
|
||||||
|
return new StringMatchers<M>(this as Assertion<string, M>);
|
||||||
|
}
|
||||||
|
/** Array-specific matchers */
|
||||||
|
public get array(): ArrayMatchers<any, M> {
|
||||||
|
return new ArrayMatchers<any, M>(this as Assertion<any[], M>);
|
||||||
|
}
|
||||||
|
/** Number-specific matchers */
|
||||||
|
public get number(): NumberMatchers<M> {
|
||||||
|
return new NumberMatchers<M>(this as Assertion<number, M>);
|
||||||
|
}
|
||||||
|
/** Boolean-specific matchers */
|
||||||
|
public get boolean(): BooleanMatchers<M> {
|
||||||
|
return new BooleanMatchers<M>(this as Assertion<boolean, M>);
|
||||||
|
}
|
||||||
|
/** Object-specific matchers */
|
||||||
|
public get object(): ObjectMatchers<any, M> {
|
||||||
|
return new ObjectMatchers<any, M>(this as Assertion<object, M>);
|
||||||
|
}
|
||||||
|
/** Function-specific matchers */
|
||||||
|
public get function(): FunctionMatchers<M> {
|
||||||
|
return new FunctionMatchers<M>(this as Assertion<Function, M>);
|
||||||
|
}
|
||||||
|
/** Date-specific matchers */
|
||||||
|
public get date(): DateMatchers<M> {
|
||||||
|
return new DateMatchers<M>(this as Assertion<Date, M>);
|
||||||
|
}
|
||||||
|
/** Type-based matchers */
|
||||||
|
public get type(): TypeMatchers<M> {
|
||||||
|
return new TypeMatchers<M>(this as Assertion<any, M>);
|
||||||
|
}
|
||||||
}
|
}
|
13
ts/types.ts
Normal file
13
ts/types.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Common types for smartexpect
|
||||||
|
*/
|
||||||
|
/** Execution mode: sync or async */
|
||||||
|
export type TExecutionType = 'sync' | 'async';
|
||||||
|
/**
|
||||||
|
* Definition of a custom matcher function.
|
||||||
|
* Should return an object with `pass` and optional `message`.
|
||||||
|
*/
|
||||||
|
export type TMatcher = (
|
||||||
|
received: any,
|
||||||
|
...args: any[]
|
||||||
|
) => { pass: boolean; message?: string | (() => string) };
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user