Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toEqual throws a TypeError when using getter to private field of class #10167

Closed
InExtremaRes opened this issue Jun 14, 2020 · 9 comments · Fixed by #14007
Closed

toEqual throws a TypeError when using getter to private field of class #10167

InExtremaRes opened this issue Jun 14, 2020 · 9 comments · Fixed by #14007

Comments

@InExtremaRes
Copy link
Contributor

🐛 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

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

  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 
@defunctzombie
Copy link

@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.

@InExtremaRes
Copy link
Contributor Author

@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:

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.

@piranna
Copy link
Contributor

piranna commented Jul 4, 2021

I've got the same problem. Is there any update?

@jacksongross
Copy link

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.

@Rochet2
Copy link

Rochet2 commented Oct 6, 2022

Having this issue also on node: "16.17.1", jest: "29.1.2".

@PSanetra
Copy link

This issue is still reproducible with jest 29.2.2. See https://stackblitz.com/edit/node-p9xadj?file=index.spec.js

@BenceSzalai
Copy link
Contributor

BenceSzalai commented Mar 14, 2023

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 is called, which in turn calls replaceMatchedToAsymmetricMatcher, which calls deepCyclicCopyReplaceable which ends up at deepCyclicCopyObject. 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 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 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 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 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 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

  const descriptors: {
    [x: string]: PropertyDescriptor;
  } = Object.getOwnPropertyDescriptors(object);

in deepCyclicCopyReplaceable.ts with:

  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!

@piranna
Copy link
Contributor

piranna commented Mar 14, 2023

Let me know if PR is welcome!

Go for it!!! :-D

@github-actions
Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 20, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants