-
Notifications
You must be signed in to change notification settings - Fork 779
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
[2.19.2] Regression with deepEqual
on hashes
#1706
Comments
@boris-petrov Thanks for filing this issue. I agree it would be inappropiate in a patch release and not intended indeed. I'm not yet able to reproduce the issue in isolation. QUnit.module('example', function () {
QUnit.test('test', function (assert) {
const hash = { a: 1 };
const prox = new Proxy(hash, {});
assert.deepEqual(prox, hash);
assert.deepEqual(hash, prox);
});
});
|
@boris-petrov Could you share the CLI output that shows the failure? I'm hoping that the way it describes the difference between actual/expected might point to where this is going wrong. |
@Krinkle thanks for taking the time to look into the issue! And sorry I didn't add a complete reproduction. It wasn't only the proxy part: class ProxyObject {
static of(obj) {
return new Proxy(obj, {
getPrototypeOf() {
return ProxyObject.prototype;
},
});
}
}
QUnit.module('example', function() {
QUnit.test('test', function(assert) {
const hash = { a: 1 };
const prox = ProxyObject.of(hash);
assert.deepEqual(prox, hash);
assert.deepEqual(hash, prox);
});
}); This reproduces the problem. It's because of |
@boris-petrov Thanks. I found the problem, quite subtle. I failed to notice the difference during code review of #1700. It's intentional and documented that assert.deepEqual requires A and B to be instances of the same exact class. In other words, being an instance of a different class (even a subclass) is considered not equal. QUnit does offer easy assertion of own properties only, which lets you compare a complex object using an object literal which we effectively downcast to plain object for comparison purposes. This is known as assert.propEqual and maybe useful for your test case in the long-term. Having said all that, this is still an unforeseen regression and we'll fix it as such. Below is how we compared inherited prototypes ("the class") before, upto 2.19.1 (simplified from source) (a, b) {
let protoA = Object.getPrototypeOf(a);
let protoB = Object.getPrototypeOf(b);
// Special case: Consider null-prototype object from Object.create(null) equal to plain object
if (…) {
return true;
}
return (a.constructor === b.constructor);
} And after, in 2.19.2 (simplified from source): (a, b) {
let protoA = Object.getPrototypeOf(a);
let protoB = Object.getPrototypeOf(b);
// Special case: Handle null-prototype case
if (…) {
return true;
}
return (protoA.constructor === protoB.constructor);
} We're now comparing The constructor property, while conventionally a way to signal how to create similar objects and what name to use for logging purposes, doesn't really factor into any this. It doesn't control inheritence, doesn't decide which constructor function runs during In QUnit 3.0 (\cc @izelnakri), after a bit of research into what other assertion libraries do, I think we might want to go for comparing strictly the result of getProto and not look at the constructor property. We can afford to not compare the constructor property explicitly, as it'll naturally come up when we compare properties of A and B — after establishing that they are instances of the same prototype — which is indeed possible (example below). For now I'll restore what we did before, whilst trying to keep the performance optimisation that 2.19.2 introduced. handling we had before. The below example shows all these silly edge cases at the same time: class Foo1{}
class Foo2{}
class Foo3{}
class Quux {
constructor() {
this.constructor = (Math.random() < 0.5 ? Foo1 : Foo2);
}
}
console.log(Quux.prototype.constructor); // Quux
Quux.prototype.constructor = Foo3;
x = new Quux();
console.log(x instanceof Foo1); // false
console.log(x instanceof Foo2); // false
console.log(x instanceof Foo3); // false
console.log(x instanceof Quux); // true
console.log(x.constructor); // Foo1 or Foo2 |
@Krinkle thank you for the awesome support! I managed to fix my tests using |
@boris-petrov In general, if you're not concerned with re-creating the exact inheritence for your If you expect there to be an inherited prototype chain that you want to validate as well, then Which one you pick is up to you. Sometimes it's obvious which one to pick. Other times it may be a trade-off between what's convenient to reproduce in a test context, and what your applications' promised behaviour is. For example, in the below example, let's say function Coord(x, y) {
this.x = x;
this.y = y;
}
function getHomeCoord() {
return new Coord(4, 10);
}
assert.deepEqual(getHomeCoord(), new Coord(4, 10)); On the other hand, if Coord is a private class, then it's a little hard to do this. Perhaps the only documented API is that assert.propEqual(getHomeCoord(), { x: 4, y: 10 }); Another reason to use propEqual might be if the class in question is difficult to instantiate where it can be a quicker alternative to elaborate dependency mocking. There is also assert.propContains() if you need to skip certain internal properties that don't need to be asserted. /** @private */
function Position(grid, player, x, y, secret) {
this.x = x;
this.y = y;
}
function getSpecialPos() {
return new Position(private.grid, private.player, 4, 10, private.token);
}
const pos = getSpecialPos();
assert.propEqual(pos, { x: 4, y: 10 }); Long story short, I think for your case, E.g. you might one day decide to promote an object literal to a class, or introduce a proxy to help with deprecation warnings. That shouldn't be observable to normal code, but would be observable to the test if you use deepEqual. Some developers will prefer a failing test if behaviour changes, and then often change tests to match new code. Others prefer to have the test only assert the public contract, and thus only fail or require changes in tests if a semver-breaking change is made. If you prefer the former of those two, then you might want to use deepEqual more often. |
Thank you again for the detailed explanations! I'll go with |
Tell us about your runtime:
ember-cli
Node v18.11.0
What are you trying to do?
Code that reproduces the problem:
What did you expect to happen?
The assert to pass
What actually happened?
It doesn't.
That's a change since 2.19.1. I'm not sure it's a bad change - that is, perhaps that's "correct", but it is a breaking change which I'm not sure you wanted to introduce. Just wanted to mention it.
The text was updated successfully, but these errors were encountered: