feat(object-matchers): Add object key matchers: toHaveKeys and toHaveOwnKeys; remove obsolete roadmap plan file
This commit is contained in:
parent
6ea5d643db
commit
d1969ab658
@ -1,5 +1,13 @@
|
||||
# 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.
|
||||
|
||||
|
@ -1,55 +0,0 @@
|
||||
# Plan for Improving @push.rocks/smartexpect API
|
||||
|
||||
This document captures the roadmap for evolving the `expect` / `expectAsync` API.
|
||||
|
||||
## Phase 1: Unify Sync + Async
|
||||
- [x] Consolidate `expect` and `expectAsync` into a single `expect()` entry point.
|
||||
- [x] Introduce `.resolves` and `.rejects` chainable helpers for Promises.
|
||||
- [x] Deprecate `expectAsync`, provide migration guidance.
|
||||
|
||||
## Phase 2: Timeout Helper
|
||||
- [x] Rename or wrap the existing `.timeout(ms)` to a more intuitive `.withTimeout(ms)`.
|
||||
|
||||
## Phase 3: Custom Matchers
|
||||
- [x] Implement `expect.extend()` API for user-defined matchers.
|
||||
|
||||
## Phase 4: TypeScript Typings
|
||||
- [ ] Enhance generic matcher types to infer narrow types after `.property()` / `.arrayItem()`.
|
||||
- [ ] Provide matcher overloads for primitive categories (string, number, array, etc.).
|
||||
|
||||
## Phase 5: Namespaced Matchers
|
||||
- [ ] Group matchers under `.string`, `.array`, `.number`, etc. for discoverability.
|
||||
|
||||
## Phase 6: Jest-Style Convenience
|
||||
- [x] Add `expect.any()` and `expect.anything()` matchers for use in `.toMatchObject()` patterns
|
||||
(Snapshot matchers still TBD)
|
||||
|
||||
The next items to tackle:
|
||||
|
||||
3. Improve negation (`.not`) messaging
|
||||
- Today `.not` simply flips pass/fail, but the failure message isn’t very descriptive. We should capture positive/negative message templates so e.g.
|
||||
> expect(5).not.toBeGreaterThan(3)
|
||||
emits:
|
||||
"Expected 5 not to be greater than 3"
|
||||
|
||||
4. Richer error output for objects/arrays
|
||||
- Integrate a diff library (or extend `fast-deep-equal`) to show unified diffs between expected and actual values
|
||||
|
||||
5. More built-in matchers
|
||||
- toBeNaN(), toBeFinite()
|
||||
- toBeWithinRange(min, max)
|
||||
- toHaveKeys(...), toHaveOwnKeys(...)
|
||||
- toThrowErrorMatching(/regex/), toThrowErrorWithMessage('…')
|
||||
- string/array: toBeEmpty() alias
|
||||
- object: toHaveOwnProperty() shorthand
|
||||
|
||||
6. TypeScript-friendliness
|
||||
- Enhance `.d.ts` so editors autocomplete namespace methods (e.g. `expect(x).string.`)
|
||||
- Statically type matcher arguments to catch wrong types at compile time
|
||||
|
||||
7. Async assertions and timeouts improvements
|
||||
- Support `.not.resolves`, `.rejects.toThrow()`
|
||||
- Provide clearer timeout errors (e.g. "Expected promise to resolve within …")
|
||||
|
||||
8. Plugin/extension API
|
||||
- Formalize `Assertion.extend()` plugin API for shipping matcher bundles
|
51
test/test.keys.ts
Normal file
51
test/test.keys.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { tap, expect as tExpect } from '@push.rocks/tapbundle';
|
||||
import * as smartexpect from '../dist_ts/index.js';
|
||||
|
||||
// Positive: toHaveKeys should match both own and inherited properties
|
||||
tap.test('toHaveKeys includes own and inherited properties', async () => {
|
||||
class Parent { a = 1; }
|
||||
class Child extends Parent { b = 2; }
|
||||
const child = new Child();
|
||||
smartexpect.expect(child).object.toHaveKeys(['a', 'b']);
|
||||
});
|
||||
|
||||
// Positive: toHaveOwnKeys should match only own properties
|
||||
tap.test('toHaveOwnKeys includes only own properties', async () => {
|
||||
class Parent { a = 1; }
|
||||
class Child extends Parent { b = 2; }
|
||||
const child = new Child();
|
||||
smartexpect.expect(child).object.toHaveOwnKeys(['b']);
|
||||
});
|
||||
|
||||
// Negative: toHaveKeys should fail if any key is missing
|
||||
tap.test('toHaveKeys fails when keys are missing', async () => {
|
||||
const obj = { a: 1 };
|
||||
try {
|
||||
smartexpect.expect(obj).object.toHaveKeys(['a', 'b']);
|
||||
throw new Error('Assertion did not throw');
|
||||
} catch (err: any) {
|
||||
tExpect(err.message.includes('Expected object to have keys')).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
// Negative: toHaveOwnKeys should fail when own keys are missing (inherited keys not counted)
|
||||
tap.test('toHaveOwnKeys fails when own keys are missing', async () => {
|
||||
// 'a' is inherited via the prototype, not an own property
|
||||
class Parent {}
|
||||
Parent.prototype.a = 1;
|
||||
class Child extends Parent {
|
||||
constructor() {
|
||||
super();
|
||||
this.b = 2;
|
||||
}
|
||||
}
|
||||
const child = new Child();
|
||||
try {
|
||||
smartexpect.expect(child).object.toHaveOwnKeys(['a', 'b']);
|
||||
throw new Error('Assertion did not throw');
|
||||
} catch (err: any) {
|
||||
tExpect(err.message.includes('Expected object to have own keys')).toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartexpect',
|
||||
version: '2.2.2',
|
||||
version: '2.3.0',
|
||||
description: 'A testing library to manage expectations in code, offering both synchronous and asynchronous assertion methods.'
|
||||
}
|
||||
|
@ -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)}`
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user