diff --git a/changelog.md b/changelog.md index 37a96e0..8000205 100644 --- a/changelog.md +++ b/changelog.md @@ -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. diff --git a/readme.plan.md b/readme.plan.md deleted file mode 100644 index d1dfa5e..0000000 --- a/readme.plan.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/test/test.keys.ts b/test/test.keys.ts new file mode 100644 index 0000000..7b55f3c --- /dev/null +++ b/test/test.keys.ts @@ -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(); \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 4483ce9..bdb0e57 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -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.' } diff --git a/ts/namespaces/object.ts b/ts/namespaces/object.ts index 0a1d723..438ac12 100644 --- a/ts/namespaces/object.ts +++ b/ts/namespaces/object.ts @@ -130,4 +130,30 @@ export class ObjectMatchers { `\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)}` + ); + } } \ No newline at end of file