Compare commits

...

6 Commits

8 changed files with 193 additions and 60 deletions

View File

@ -1,5 +1,26 @@
# Changelog
## 2025-04-30 - 2.3.0 - feat(object-matchers)
Add object key matchers: toHaveKeys and toHaveOwnKeys; remove obsolete roadmap plan file
- Implemented toHaveKeys matcher to check for both own and inherited properties
- Implemented toHaveOwnKeys matcher to check for own properties only
- Added tests for key matchers in test/test.keys.ts
- Removed readme.plan.md as it is now obsolete
## 2025-04-29 - 2.2.2 - fix(license-files)
Remove legacy license file and add license.md to update file naming.
- Removed the old 'license' file.
- Added 'license.md' with updated file structure.
## 2025-04-29 - 2.2.1 - fix(readme)
Update usage examples and full matcher reference in README
- Removed deprecated 'expectAsync' from import example
- Fixed formatting in documentation
- Added comprehensive full matcher reference section
## 2025-04-29 - 2.2.0 - feat(generics)
Improve assertion and matcher type definitions by adding execution mode generics for better async/sync support

View File

@ -1,4 +1,4 @@
Copyright (c) 2022 Lossless GmbH (hello@lossless.com)
Copyright (c) 2022 Task Venture Capital GmbH (hello@lossless.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartexpect",
"version": "2.2.0",
"version": "2.3.0",
"private": false,
"description": "A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.",
"main": "dist_ts/index.js",

View File

@ -17,10 +17,10 @@ This will add `@push.rocks/smartexpect` to your project's dependencies. Make sur
### Getting Started
First, import `@push.rocks/smartexpect` into your TypeScript file:
First, import `@push.rocks/smartexpect` into your TypeScript file:
```typescript
import { expect, expectAsync } from '@push.rocks/smartexpect';
import { expect } from '@push.rocks/smartexpect';
```
### Synchronous Expectations
@ -254,6 +254,96 @@ expect(4).not.toBeOdd();
- Matcher functions receive the value under test (`received`) plus any arguments.
- Must return an object with `pass` (boolean) and `message` (string or function) for failure messages.
### Full Matcher Reference
Below is a comprehensive list of all matchers and utility functions available in `@push.rocks/smartexpect`.
#### Modifiers and Utilities
- `.not` Negates the next matcher in the chain.
- `.resolves` Switches to async mode, expecting the promise to resolve.
- `.rejects` Switches to async mode, expecting the promise to reject.
- `.withTimeout(ms)` Sets a timeout (in milliseconds) for async assertions.
- `.timeout(ms)` (deprecated) Alias for `.withTimeout(ms)`.
- `.property(name)` Drill into a property of an object.
- `.arrayItem(index)` Drill into an array element by index.
- `.log()` Logs the current value and assertion path for debugging.
- `.setFailMessage(message)` Override the failure message for the current assertion.
- `.setSuccessMessage(message)` Override the success message for the current assertion.
- `.customAssertion(fn, message)` Execute a custom assertion function with a message.
- `expect.extend(matchers)` Register custom matchers globally.
- `expect.any(constructor)` Matcher for values that are instances of the given constructor.
- `expect.anything()` Matcher for any defined value (not null or undefined).
#### Basic Matchers
- `.toEqual(expected)` Deep (or strict for primitives) equality.
- `.toBeTrue()` Value is strictly `true`.
- `.toBeFalse()` Value is strictly `false`.
- `.toBeTruthy()` Value is truthy.
- `.toBeFalsy()` Value is falsy.
- `.toBeNull()` Value is `null`.
- `.toBeUndefined()` Value is `undefined`.
- `.toBeNullOrUndefined()` Value is `null` or `undefined`.
- `.toBeDefined()` Value is not `undefined`.
#### Number Matchers
- `.toBeGreaterThan(value)`
- `.toBeLessThan(value)`
- `.toBeGreaterThanOrEqual(value)`
- `.toBeLessThanOrEqual(value)`
- `.toBeCloseTo(value, precision?)`
- `.toEqual(value)` Strict equality for numbers.
- `.toBeNaN()`
- `.toBeFinite()`
- `.toBeWithinRange(min, max)`
#### String Matchers
- `.toStartWith(prefix)`
- `.toEndWith(suffix)`
- `.toInclude(substring)`
- `.toMatch(regex)`
- `.toBeOneOf(arrayOfValues)`
- `.toHaveLength(length)`
- `.toBeEmpty()` Alias for empty string.
#### Array Matchers
- `.toBeArray()`
- `.toHaveLength(length)`
- `.toContain(value)`
- `.toContainEqual(value)`
- `.toContainAll(arrayOfValues)`
- `.toExclude(value)`
- `.toBeEmptyArray()`
- `.toBeEmpty()` Alias for empty array.
- `.toHaveLengthGreaterThan(length)`
- `.toHaveLengthLessThan(length)`
#### Object Matchers
- `.toEqual(expected)` Deep equality for objects.
- `.toMatchObject(partialObject)` Partial deep matching (supports `expect.any` and `expect.anything`).
- `.toBeInstanceOf(constructor)`
- `.toHaveProperty(propertyName, value?)`
- `.toHaveDeepProperty(pathArray)`
- `.toHaveOwnProperty(propertyName, value?)`
- `.toBeNull()`
- `.toBeUndefined()`
- `.toBeNullOrUndefined()`
#### Function Matchers
- `.toThrow(expectedError?)`
- `.toThrowErrorMatching(regex)`
- `.toThrowErrorWithMessage(message)`
#### Date Matchers
- `.toBeDate()`
- `.toBeBeforeDate(date)`
- `.toBeAfterDate(date)`
#### Type Matchers
- `.toBeTypeofString()`
- `.toBeTypeofNumber()`
- `.toBeTypeofBoolean()`
- `.toBeTypeOf(typeName)`
## Best Practices
- **Human-readable assertions**: The fluent API is designed to create tests that read like natural language sentences.

View File

@ -1,55 +0,0 @@
# Plan for Improving @push.rocks/smartexpect API
This document captures the roadmap for evolving the `expect` / `expectAsync` API.
## Phase 1: Unify Sync + Async
- [x] Consolidate `expect` and `expectAsync` into a single `expect()` entry point.
- [x] Introduce `.resolves` and `.rejects` chainable helpers for Promises.
- [x] Deprecate `expectAsync`, provide migration guidance.
## Phase 2: Timeout Helper
- [x] Rename or wrap the existing `.timeout(ms)` to a more intuitive `.withTimeout(ms)`.
## Phase 3: Custom Matchers
- [x] Implement `expect.extend()` API for user-defined matchers.
## Phase 4: TypeScript Typings
- [ ] Enhance generic matcher types to infer narrow types after `.property()` / `.arrayItem()`.
- [ ] Provide matcher overloads for primitive categories (string, number, array, etc.).
## Phase 5: Namespaced Matchers
- [ ] Group matchers under `.string`, `.array`, `.number`, etc. for discoverability.
## Phase 6: Jest-Style Convenience
- [x] Add `expect.any()` and `expect.anything()` matchers for use in `.toMatchObject()` patterns
(Snapshot matchers still TBD)
The next items to tackle:
3. Improve negation (`.not`) messaging
- Today `.not` simply flips pass/fail, but the failure message isnt very descriptive. We should capture positive/negative message templates so e.g.
> expect(5).not.toBeGreaterThan(3)
emits:
"Expected 5 not to be greater than 3"
4. Richer error output for objects/arrays
- Integrate a diff library (or extend `fast-deep-equal`) to show unified diffs between expected and actual values
5. More built-in matchers
- toBeNaN(), toBeFinite()
- toBeWithinRange(min, max)
- toHaveKeys(...), toHaveOwnKeys(...)
- toThrowErrorMatching(/regex/), toThrowErrorWithMessage('…')
- string/array: toBeEmpty() alias
- object: toHaveOwnProperty() shorthand
6. TypeScript-friendliness
- Enhance `.d.ts` so editors autocomplete namespace methods (e.g. `expect(x).string.`)
- Statically type matcher arguments to catch wrong types at compile time
7. Async assertions and timeouts improvements
- Support `.not.resolves`, `.rejects.toThrow()`
- Provide clearer timeout errors (e.g. "Expected promise to resolve within …")
8. Plugin/extension API
- Formalize `Assertion.extend()` plugin API for shipping matcher bundles

51
test/test.keys.ts Normal file
View File

@ -0,0 +1,51 @@
import { tap, expect as tExpect } from '@push.rocks/tapbundle';
import * as smartexpect from '../dist_ts/index.js';
// Positive: toHaveKeys should match both own and inherited properties
tap.test('toHaveKeys includes own and inherited properties', async () => {
class Parent { a = 1; }
class Child extends Parent { b = 2; }
const child = new Child();
smartexpect.expect(child).object.toHaveKeys(['a', 'b']);
});
// Positive: toHaveOwnKeys should match only own properties
tap.test('toHaveOwnKeys includes only own properties', async () => {
class Parent { a = 1; }
class Child extends Parent { b = 2; }
const child = new Child();
smartexpect.expect(child).object.toHaveOwnKeys(['b']);
});
// Negative: toHaveKeys should fail if any key is missing
tap.test('toHaveKeys fails when keys are missing', async () => {
const obj = { a: 1 };
try {
smartexpect.expect(obj).object.toHaveKeys(['a', 'b']);
throw new Error('Assertion did not throw');
} catch (err: any) {
tExpect(err.message.includes('Expected object to have keys')).toBeTrue();
}
});
// Negative: toHaveOwnKeys should fail when own keys are missing (inherited keys not counted)
tap.test('toHaveOwnKeys fails when own keys are missing', async () => {
// 'a' is inherited via the prototype, not an own property
class Parent {}
Parent.prototype.a = 1;
class Child extends Parent {
constructor() {
super();
this.b = 2;
}
}
const child = new Child();
try {
smartexpect.expect(child).object.toHaveOwnKeys(['a', 'b']);
throw new Error('Assertion did not throw');
} catch (err: any) {
tExpect(err.message.includes('Expected object to have own keys')).toBeTrue();
}
});
export default tap.start();

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartexpect',
version: '2.2.0',
version: '2.3.0',
description: 'A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.'
}

View File

@ -130,4 +130,30 @@ export class ObjectMatchers<T extends object, M extends TExecutionType> {
`\nReceived: ${JSON.stringify(v, null, 2)}`
);
}
/**
* Assert object has the given keys (including inherited properties).
* @param keys Array of keys to check for presence.
*/
public toHaveKeys(keys: string[]) {
return this.assertion.customAssertion(
(v) => keys.every((key) => key in (v as any)),
(v) =>
`Expected object to have keys ${JSON.stringify(keys)}` +
`\nReceived keys: ${JSON.stringify(Object.keys(v), null, 2)}`
);
}
/**
* Assert object has the given own keys (excluding inherited properties).
* @param keys Array of own keys to check.
*/
public toHaveOwnKeys(keys: string[]) {
return this.assertion.customAssertion(
(v) => keys.every((key) => Object.prototype.hasOwnProperty.call(v as any, key)),
(v) =>
`Expected object to have own keys ${JSON.stringify(keys)}` +
`\nReceived own keys: ${JSON.stringify(Object.keys(v), null, 2)}`
);
}
}