Repository: jestjs/jest (TypeScript)

Issue:
toEqual throws a TypeError when using getter to private field of class
## 🐛 Bug Report

Jest throws a `TypeError` when using `toEqual` or `toMatchObject` in a class with private fields (those starting by `#`) and a getter that exposes the value. I guess other similar matchers like `toStrictEqual` also fails but didn't test. I tested this using jest-circus and fails the same way. 

The class under test has a private field `#foo` and a getter `get foo()` that returns the value of `#foo`. Using `toMatchObject` with the name of the getter (something like `.toMatchObject({ foo: 0 })`) works when the value is equal but throws a `TypeError` when are distinct (I bet when trying to show the differences, but I haven't debugged).

## To Reproduce

```js
class X {
  #foo = 42;
  get foo() {
    return this.#foo;
  }
}

test("it works and test pass", () => {
  const x = new X();
  expect(x.foo).toEqual(42);
});

test("it also works and test pass", () => {
  const x = new X();
  expect(x).toMatchObject({ foo: 42 });
});

test("it works, the test fail since values are distinct", () => {
  const x = new X();
  expect(x.foo).toEqual(0);
  // Expected: 0, Received: 42
});

test("it doesn't work, matcher throws a TypeError", () => {
  const x = new X();
  expect(x).toMatchObject({ foo: 0 });
  // TypeError: Cannot read private member #foo from an object whose class did not declare it
});

test("also doesn't work, matcher throws a TypeError", () => {
  const x = new X();
  expect(x).toEqual({ foo: 0 });
  // TypeError: Cannot read private member #foo from an object whose class did not declare it
});
```

## Expected behavior

Last two test fail properly since property `foo` are distinct between expected and received objects, showing the right report.

## envinfo

```plain
  System:
    OS: Linux 5.6 Arch Linux
    CPU: (8) x64 Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
  Binaries:
    Node: 12.17.0 - /usr/bin/node
    npm: 6.14.5 - /usr/bin/npm
  npmPackages:
    jest: ^26.0.1 => 26.0.1 
```



Hints:
@InExtremaRes did you ever find a fix or workaround for this? I've too run into this where I am trying to check the equality of a class's getters with a jest expect.
@defunctzombie No, not really. I started creating a plain object with the properties I want to compare, but that's far from ideal. I made a little utility `pick` to take some properties from a given object:

```js
const x = new X(); // suppose this have props `foo` and `bar`
expect(pick(x, ['foo', 'bar'])).toEqual({ foo: 1, bar: 2 });
```
I hope to see some progress on this, the way I'm doing it is very tedious.
I've got the same problem. Is there any update?
I have the same problem as @defunctzombie, but using `toMatchObject` instead of `toEqual` seems to make the tests pass as a workaround. It would be nice to get this fixed though as the error is really misleading and requires knowledge of the underlying class to know to use `toMatchObject` vs `toEqual`.
Having this issue also on `node: "16.17.1", jest: "29.1.2"`.
This issue is still reproducible with jest `29.2.2`. See https://stackblitz.com/edit/node-p9xadj?file=index.spec.js
Jest is able to determine properly, that the expected value does not match the received value, so it starts to build the corresponding console output for the difference. For this [`printDiffOrStringify`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/index.ts#L318) is called, which in turn calls [`replaceMatchedToAsymmetricMatcher`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/index.ts#L410), which calls [`deepCyclicCopyReplaceable`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts#L37) which ends up at [`deepCyclicCopyObject`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts#L58). Now this method enumerates the properties of the received value, which is an instance of `X` containing `#foo` and inheriting `get foo()`. However the inherited `get foo()` is not enough for this method to identify `foo` as a property, i.e. [`newDescriptors`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts#L66) ends up as an empty object. If the getter for `foo` was identified, the value of `foo` would have been copied into the new object, replacing the getter with an actual value, however this does not happen, so [`_replaceMatchedToAsymmetricMatcher`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/index.ts#L424) receives an object as the `replacedReceived` parameter with the `foo` property missing. Naturally when later this method tries to print out the value of `foo` on `replacedReceived` using the [`receivedReplaceable.get`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/index.ts#L448) method instead of printing the value as would happened if the value of `foo` was copied, it [ends up accessing `foo` on an object with no such property](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/Replaceable.ts#L59) which in turn calls the getter defined on the parent (prototype of class `X`), however since the private property `#foo` was not copied either to `replacedReceived` from the original one, the getter now fails with `Cannot read private member #foo from an object whose class did not declare it`.

So the problem here has two parts: `deepCyclicCopyObject` is:
1. not replacing the getter defined on the prototype with the value,
2. nor able to copy the private fields over.

Therefore the getter will be called when the rest of the code expects it to be already replaced with a value, and the getter in turn tries to access the private field, which is also missing by this time (working with the partially copied object).

Now obviously copying the private fields is not achievable, so 2.) is not fixable, so we can only expect 1.) to be fixed.

The properties copied over are determined by [`Object.getOwnPropertyDescriptors`](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts#L60) which includes both enumerable and non-enumerable properties, where properties are in the wider sense, so they include getters, setters etc. However only own, nothing inherited. And since `class X` defines the getter for `foo` on the class, from the perspective of `x` it is an inherited getter, therefore it is skipped by `Object.getOwnPropertyDescriptors`.

As far as i know there is no built in method that could get the inherited descriptors together with own, so the naive solution is to iterate the prototype chain and collect the descriptors for each level (excluding the root object model).

According to that a naive fix can be to replace
```ts
  const descriptors: {
    [x: string]: PropertyDescriptor;
  } = Object.getOwnPropertyDescriptors(object);
```
in [deepCyclicCopyReplaceable.ts](https://github.com/facebook/jest/blob/main/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts#L60-L62) with:
```ts
  let descriptors: Record<string, PropertyDescriptor> = {};
  let obj = object;
  do {
    descriptors = Object.assign({}, Object.getOwnPropertyDescriptors(obj), descriptors)
  } while ( (obj = Object.getPrototypeOf( obj )) && obj !== Object.getPrototypeOf({}) );
```
 
With this in place all the test cases from the top of the issue pass or fail gracefully with proper comparison output in the console.

Let me know if PR is welcome!
> Let me know if PR is welcome!

Go for it!!! :-D

Patch:
diff --git a/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts b/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts
--- a/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts
+++ b/packages/jest-matcher-utils/src/deepCyclicCopyReplaceable.ts
@@ -57,9 +57,18 @@ export default function deepCyclicCopyReplaceable<T>(
 
 function deepCyclicCopyObject<T>(object: T, cycles: WeakMap<any, unknown>): T {
   const newObject = Object.create(Object.getPrototypeOf(object));
-  const descriptors: {
-    [x: string]: PropertyDescriptor;
-  } = Object.getOwnPropertyDescriptors(object);
+  let descriptors: Record<string, PropertyDescriptor> = {};
+  let obj = object;
+  do {
+    descriptors = Object.assign(
+      {},
+      Object.getOwnPropertyDescriptors(obj),
+      descriptors,
+    );
+  } while (
+    (obj = Object.getPrototypeOf(obj)) &&
+    obj !== Object.getPrototypeOf({})
+  );
 
   cycles.set(object, newObject);
 


Test patch:
diff --git a/packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts b/packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts
--- a/packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts
+++ b/packages/jest-matcher-utils/src/__tests__/deepCyclicCopyReplaceable.test.ts
@@ -43,7 +43,7 @@ test('convert accessor descriptor into value descriptor', () => {
   });
 });
 
-test('should not skips non-enumerables', () => {
+test('should not skip non-enumerables', () => {
   const obj = {};
   Object.defineProperty(obj, 'foo', {enumerable: false, value: 'bar'});
 
@@ -66,6 +66,18 @@ test('copies symbols', () => {
   expect(deepCyclicCopyReplaceable(obj)[symbol]).toBe(42);
 });
 
+test('copies value of inherited getters', () => {
+  class Foo {
+    #foo = 42;
+    get foo() {
+      return this.#foo;
+    }
+  }
+  const obj = new Foo();
+
+  expect(deepCyclicCopyReplaceable(obj).foo).toBe(42);
+});
+
 test('copies arrays as array objects', () => {
   const array = [null, 42, 'foo', 'bar', [], {}];