From 0351da2878945daaf754e402585abe00a6b6d324 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Wed, 30 Apr 2025 18:24:28 +0000 Subject: [PATCH] feat(object): add toHaveOwnProperty method and improve property-path matching in object assertions --- changelog.md | 8 ++++++++ scratch.js | 16 ++++++++++++++++ test/{test.expectAny.ts => test.expectany.ts} | 0 test/test.propertypath.ts | 6 ++++-- ts/00_commitinfo_data.ts | 2 +- ts/namespaces/object.ts | 10 +++++++++- ts/smartexpect.classes.assertion.ts | 1 + 7 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 scratch.js rename test/{test.expectAny.ts => test.expectany.ts} (100%) diff --git a/changelog.md b/changelog.md index a984341..92215f5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-04-30 - 2.4.0 - feat(object) +add toHaveOwnProperty method and improve property-path matching in object assertions + +- Added 'toHaveOwnProperty' as a direct method on Assertion to check for own properties +- Enhanced property path evaluation in 'toHaveProperty' to handle nested keys more robustly +- Renamed test file to maintain consistent naming for expect.any tests +- Introduced scratch.js for manual testing and debugging of property matchers + ## 2025-04-30 - 2.3.3 - fix(tests) Fix test file naming inconsistencies diff --git a/scratch.js b/scratch.js new file mode 100644 index 0000000..97ca204 --- /dev/null +++ b/scratch.js @@ -0,0 +1,16 @@ +import * as smartexpect from './dist_ts/index.js'; +class Foo { constructor(){ this.foo='bar'; } } +console.log('foo in instance:', 'foo' in new Foo()); +console.log('hasOwn foo:', Object.prototype.hasOwnProperty.call(new Foo(), 'foo')); +try { + smartexpect.expect(new Foo()).object.toHaveProperty('foo'); + console.log('toHaveProperty passed'); +} catch (err) { + console.error('toHaveProperty failed:', err.message); +} +try { + smartexpect.expect(new Foo()).object.toHaveOwnProperty('foo'); + console.log('toHaveOwnProperty passed'); +} catch (err) { + console.error('toHaveOwnProperty failed:', err.message); +} diff --git a/test/test.expectAny.ts b/test/test.expectany.ts similarity index 100% rename from test/test.expectAny.ts rename to test/test.expectany.ts diff --git a/test/test.propertypath.ts b/test/test.propertypath.ts index a88260f..a0239bb 100644 --- a/test/test.propertypath.ts +++ b/test/test.propertypath.ts @@ -2,7 +2,9 @@ import { tap, expect as tExpect } from '@push.rocks/tapbundle'; import * as smartexpect from '../dist_ts/index.js'; tap.test('toHaveProperty nested path via dot notation', async () => { - const testObject = { level1: { level2: { level3: 'value' } } }; + const testObject = { level1: { level2: { level3: 'value' }}, publicTest: 'hi' }; + + smartexpect.expect(testObject).object.toHaveProperty('publicTest'); // Existence check smartexpect.expect(testObject).object.toHaveProperty('level1.level2.level3'); // Value check @@ -11,4 +13,4 @@ tap.test('toHaveProperty nested path via dot notation', async () => { smartexpect.expect(testObject).not.object.toHaveProperty('level1.level2.missing'); }); -export default tap.start(); \ No newline at end of file +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 1e96d3a..17214d9 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.3.3', + version: '2.4.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 45a0995..1108acb 100644 --- a/ts/namespaces/object.ts +++ b/ts/namespaces/object.ts @@ -62,13 +62,21 @@ export class ObjectMatchers { return this.assertion.customAssertion( (v) => { const obj = v as any; + // first check for a literal property (including inherited) + if (property in obj) { + if (arguments.length === 2) { + return plugins.fastDeepEqual(obj[property], value); + } + return true; + } + // no direct key, try nested path via dot notation const path = property.split('.'); let current = obj; for (const key of path) { if (current == null || !(key in current)) { return false; } - current = current[key]; + current = (current as any)[key]; } if (arguments.length === 2) { return plugins.fastDeepEqual(current, value); diff --git a/ts/smartexpect.classes.assertion.ts b/ts/smartexpect.classes.assertion.ts index 1b6bdfd..7518aab 100644 --- a/ts/smartexpect.classes.assertion.ts +++ b/ts/smartexpect.classes.assertion.ts @@ -356,6 +356,7 @@ export class Assertion { 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 toHaveOwnProperty(property: string, value?: any) { return this.object.toHaveOwnProperty(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); }