Repository: bahmutov/cypress-map (js)

Issue:
possess assertion with a predicate
to allow checking a property value without changing the subject
```js
cy.wrap(S)
  .should('possess', 'age', (years) => years >= 0 && years < 100)
```

Pull request description:
feat: add predicate support to the possess assertion
- closes #246 

Patch:
diff --git a/README.md b/README.md
index 9d6265b..83d0990 100644
--- a/README.md
+++ b/README.md
@@ -687,6 +687,14 @@ cy.wrap({ users: [{ name: 'Joe' }, { name: 'Jane' }] })
   .and('possess', 'users.1.name', 'Jane') // simple dot notation
 ```
 
+You can also pass a predicate function instead of the value to check the property value against it.
+
+```js
+const isDrinkingAge = (years) => years > 21
+cy.wrap({ age: 42 }).should('possess', 'age', isDrinkingAge)
+// yields the original subject
+```
+
 ## cy.invoke vs cy.map vs cy.mapInvoke
 
 Here are a few examples to clarify the different between the `cy.invoke`, `cy.map`, and `cy.mapInvoke` query commands, see [diff.cy.js](./cypress/e2e/diff.cy.js)
diff --git a/src/commands/assertions.js b/src/commands/assertions.js
index 4f05c10..340e372 100644
--- a/src/commands/assertions.js
+++ b/src/commands/assertions.js
@@ -39,7 +39,7 @@ chai.use((_chai) => {
   }
   _chai.Assertion.addMethod('read', readAssertion)
 
-  function possesAssertion(propertyName, maybeValue) {
+  function possesAssertion(propertyName, maybeValueOrPredicate) {
     if (typeof propertyName !== 'string') {
       throw new Error(
         `possess assertion: Expected a string, but got ${typeof propertyName}`,
@@ -60,11 +60,21 @@ chai.use((_chai) => {
       )
     }
     if (arguments.length === 2) {
-      return this.assert(
-        Cypress._.get(this._obj, propertyName) === maybeValue,
-        `expected ${subjectShort} to possess property ${propertyName}=${maybeValue}`,
-        `expected ${subjectShort} to not possess property ${propertyName}=${maybeValue}`,
-      )
+      const value = Cypress._.get(this._obj, propertyName)
+      if (typeof maybeValueOrPredicate === 'function') {
+        const functionName = maybeValueOrPredicate.name || 'function'
+        return this.assert(
+          maybeValueOrPredicate(value),
+          `expected ${subjectShort} to pass the ${functionName} predicate`,
+          `expected ${subjectShort} to not pass the ${functionName} predicate`,
+        )
+      } else {
+        return this.assert(
+          value === maybeValueOrPredicate,
+          `expected ${subjectShort} to possess property ${propertyName}=${maybeValueOrPredicate}`,
+          `expected ${subjectShort} to not possess property ${propertyName}=${maybeValueOrPredicate}`,
+        )
+      }
     }
 
     throw new Error(
diff --git a/src/commands/index.d.ts b/src/commands/index.d.ts
index 6763315..fc28c92 100644
--- a/src/commands/index.d.ts
+++ b/src/commands/index.d.ts
@@ -416,6 +416,25 @@ declare namespace Cypress {
      */
     (chainer: 'possess', propertyName: string): Chainable<Subject>
 
+    /**
+     * Checks the presence of the given property with the given value
+     * in the current subject object against the given predicate function.
+     * Yields the original subject.
+     * Supports deep nested properties using dot notation.
+     * Supports arrays and array indexes, brackets are optional
+     *
+     * @example
+     *  const isDrinkingAge = (years) => years > 21
+     *  cy.wrap({ age: 42 }).should('possess', 'age', isDrinkingAge)
+     *
+     * @see https://github.com/bahmutov/cypress-map
+     */
+    (
+      chainer: 'possess',
+      propertyName: string,
+      value: Function,
+    ): Chainable<Subject>
+
     /**
      * Checks the presence of the given property with the given value
      * in the current subject object.


Test patch:
diff --git a/cypress/e2e/assertions/possess.cy.js b/cypress/e2e/assertions/possess.cy.js
index a9e5865..dd26de2 100644
--- a/cypress/e2e/assertions/possess.cy.js
+++ b/cypress/e2e/assertions/possess.cy.js
@@ -85,4 +85,19 @@ describe('should possess assertion', () => {
       .should('possess', 'foo', 'bar', 123)
       .should('deep.equal', { foo: 'bar' })
   })
+
+  context('with a predicate', () => {
+    it('checks the property value against the predicate function', () => {
+      cy.wrap({ foo: 'bar' })
+        .should('possess', 'foo', (val) => val === 'bar')
+        // keeps the subject
+        .and('deep.equal', { foo: 'bar' })
+    })
+
+    it('negates the predicate function', () => {
+      const isDrinkingAge = (years) => years > 21
+      cy.wrap({ age: 42 }).should('possess', 'age', isDrinkingAge)
+      cy.wrap({ age: 1 }).should('not.possess', 'age', isDrinkingAge)
+    })
+  })
 })