Different results after creating two JS objects from (seemingly) identical JSON - javascript

I'll try to frame this in a framework-agnostic manner, but my use case is specific to react/redux/redux-form.
I first get a thing out of the store. This is important because if I define this object in place, things work fine. It makes it really difficult to reacreate the error, though. So, I'll go through a couple examples. The first one works, the rest don't. Let's say it has the following structure:
thing = {
foo: 'foo',
bar: ['bar'],
baz: 'baz'
}
This works fine
function mapStateToProps(state) {
const { thing } = state.thing;
// thing = {
// foo: 'foo',
// bar: ['bar'],
// baz: 'baz'
// }
// I don't care about `baz`, and I don't want to send it downstream
// If I hardcode this, everything works fine
// (except that my data is dynamic, so
// this is not a real solution)
const workingObj = { foo: 'foo', bar: ['bar'] }
return (
{ initialValues: workingObj }
);
}
This doesn't work, however
function mapStateToProps(state) {
const { thing } = state.thing;
const brokenObj = { foo: thing.foo, bar: thing.bar };
return (
{ initialValues: brokenObj }
);
}
I figured I was having some issue with references losing scope or something. Let's try a deep copy:
function mapStateToProps(state) {
const { thing } = state.thing;
const brokenObj = JSON.parse(JSON.stringify(
{ foo: thing.foo, bar: thing.bar }
);
return (
{ initialValues: brokenObj }
);
}
Here's that same one in more detail... I have no idea how workingObjStr and brokenObjStr could be parsed into anything other than identical objects.
function mapStateToProps(state) {
const { thing } = state.thing;
const workingObj = { foo: 'foo', bar: ['bar'] };
const brokenObj = { foo: thing.foo, bar: thing.bar };
const workingObjStr = JSON.stringify(workingObj);
const brokenObjStr = JSON.stringify(brokenObj);
console.debug(workingObjStr === brokenObjStr);
// => true
return (
{ initialValues: JSON.parse(brokenObjStr) }
);
}
So, my question in general terms is: how could two identical string objects produce different objects after being parsed? Might there be some optimization voodoo going on behind the scenes, or am I missing something fundamental? I can't imagine how anything downstream of this return would even know that these objects have been produced through different means, so I'm completely stumped.
I'll also note again that I can't really think of how you could reproduce this... it seems somehow tied to the redux environment, because if I define the thing object inside the function scope things work fine.
I'll also say that I know the initialValues object is properly formatted and is propagating downstream because I can see it inside of my form... but redux-form doesn't pick it up unless it is created manually inside of the mapStateToProps function scope. Again, I have no idea how it would even be able to tell the difference, but hopefully I'm just missing something silly...
Thanks,
Thomas

Related

Spying on property changes with Sinon

I am stubbing properties with sinon. I have trouble understanding how the related spying works. Most of the sinon spy methods seem to be related to function calls and not to access of object properties. As a workaround I created a variable for tracking the property access and set it manually when the related getter function is called. See the code example below. This seems to work just fine but I feel like I am probably duplicating some functionality that already exists in sinon. What would be a more idiomatic way of achieving the same result?
const sinon = require('sinon');
const lib = require('./lib');
const app = require('./app');
const sandbox = sinon.createSandbox();
let fooAccessed = false;
sandbox.stub(lib, 'foo').get(() => {
fooAccessed = true;
return 123
});
app();
expect(fooAccessed).toEqual(true);
I know this is probably not the answer you want, because i do not use "sinon", but i will answer anyway. Perhaps this helps you in any way.
You can use a Proxy object to intercept accessing a object.
const obj = {
foo: "bar",
baz: true
};
let accessed = false;
const proxied = new Proxy(obj, {
get(target, prop) {
if (prop === "foo") {
accessed = true;
}
return target[prop];
}
});
console.log(proxied, accessed);
console.log(proxied.foo);
console.log(proxied, accessed);
Result:
{ foo: 'bar', baz: true } false
bar
{ foo: 'bar', baz: true } true

Create array with an additional property

I'm using an existing 3rd party API over which I have no control and which requires me to pass an array with an additional property. Something like the following:
type SomeArgument = string[] & { foo: string };
doSomething (argument: SomeArgument);
Now I find it quite clumsy and verbose, to create SomeArgument while keeping the compiler satisfied.
This one works, but no type safety at all:
const customArray: any = ['baz'];
customArray.foo = 'bar';
doSomething(customArray);
Another option which feels cleaner, but is quite verbose (I will need different subclasses with different properties) is to subclass Array:
class SomeArgumentImpl extends Array<string> {
constructor (public foo: string, content?: Array<string>) {
super(...content);
}
}
doSomething(new SomeArgumentImpl('bar', ['baz']));
Is there any better, one-liner-style way? I was hoping for something along doSomething({ ...['baz'], foo: 'bar' }); (this one does not work, obviously).
Suppose this is the function you want to call:
function doSomething (argument: string[] & { foo: string }) {
argument.push("a");
argument.foo = "4";
console.log(argument);
}
Then you can call it like this:
// Works
doSomething(Object.assign(["a", "b"], { foo: "c" }));
// Error
doSomething(Object.assign(["a", 2], { foo: "c" }));
// Error
doSomething(Object.assign(["a", 2], { foo: 4 }));

Jest mock function arguments don't match when using immutable data structures

When trying to test the arguments passed to the function with Jest's .toHaveBeenCalledWith() method, the test fails if I am treating with immutable data structures using ImmutableJS library. The test fails with the message similar to this:
Expected mock function to have been called with:
[{"foo": true, "bar": "baz"}]
But it was called with:
[{"foo": true, "bar": "baz"}]
The test looks similar to this:
const expectedArgs = Map({
foo: true,
bar: 'baz'
});
const foo = jest.fn();
bar();
expect(foo).toHaveBeenCalledWith(expectedArgs);
And the function similar to this:
const bar = () => {
const baz = Map({});
const bazModified = baz.set('foo', true).set('bar', 'baz');
foo(bazModified);
}
I realized that if I pass the arguments in this manner everything works fine:
const bar = () => {
const baz = Map({
foo: true,
bar: 'baz'
});
foo(baz);
}
The problem is that this is a big simplification of my function's logic and I have to use .set to construct the object. Does anybody have an idea why the approach with .set is failing to evaluate correctly?
So your test is failing because toHaveBeenCalledWith only passes if the instances of the entities are exactly the same. It is similar to the following, which also fails:
const expectedArgs = Map({
foo: true,
bar: 'baz'
});
const input = Map({
foo: false,
bar: 'baz'
});
const result = input.set('foo', true);
expect(result).toBe(expectedArgs);
This, on the other hand, does work:
expect(result).toEqual(expectedArgs);
Because that performs a deep equal.
There is no way you will be able to test equality with toHaveBeenCalledWith, because baz.set('foo', true) will always return a new instance (which is the point of using immutable data).
I don't think there is a way to make toHaveBeenCalledWith behave like toEqual, so I guess the way to go is to manually test the mock call with toEqual:
const expectedArgs = Map({
foo: true,
bar: 'baz'
});
const foo = jest.fn();
bar();
// foo.mock.calls[0][0] returns the first argument of the first call to foo
expect(foo.mock.calls[0][0]).toEqual(expectedArgs);
See the documentation on toBe, toEqual, and mock calls

Lodash isEqual fails because of constructor defined by angular

I am using Lodash _.isEqual to deep-compare a local javascript object with another javascript object retrieved through angular $get.
This code says that the objects are different:
$get({...}, function (data) {
cleanupAngularProps(data);
if (_.isEqual(data, {name: 'Someone'}) {
...
}
});
but chaging it a little as follows it now says that they are equal (as expected):
$get({...}, function (data) {
cleanupAngularProps(data);
if (_.isEqual(JSON.parse(JSON.stringify(data)), {name: 'Someone'}) {
...
}
});
I debugged into the Lodash code and it seems to fail because both objects have different constructors.
How can I solve this without cloning the data?
I know this question is three years old at this point, but I have found what I believe is a more acceptable answer. Simply exclude the prototype comparison between the two objects:
var result = _.isEqual(
_.omit(data, ['__proto__']),
_.omit({name: 'Someone'}, ['__proto__'])
);
I am not sure if this is a good practice but I solved it by resetting the prototype, hence the constructor.
$get({...}, function (data) {
cleanupAngularProps(data);
data.__proto__ = Object.prototype;
if (_.isEqual(data, {name: 'Someone'}) {
...
}
});
If you need deep comparison and ignore constructors, then you can use this code:
const customizer = (a: any, b: any, key: any) => {
if (_.isObject(a) && _.isObject(b)) {
// #ts-ignore
const aProto = a.__proto__.constructor.name
// #ts-ignore
const bProto = b.__proto__.constructor.name
if (aProto != bProto) {
return _.isEqualWith(_.omit(a, ['__proto__']), _.omit(b, ['__proto__']), customizer)
}
}
return undefined
}
_.isEqualWith({foo:1}, {foo:1}, customizer)

Has been called with object assertion

Function I'm spying on, receives object as an argument. I need to assert that the function been called with certain properties of the object.
e.g: my SUT has:
function kaboom() {
fn({
foo: 'foo',
bar: 'bar',
zap: function() { ... },
dap: true
});
}
and in my test I can do this:
fnStub = sinon.stub();
kaboom();
expect(fnStub).to.have.been.called;
and that works (it's good to know that fn's been called). Now I need to make sure that the right object has been passed into the function. I care about only foo and bar properties, i.e. I have to set match for specific properties of the argument. How?
upd: sinon.match() seems to work for simple objects. Let's raise the bar, shall we?
What if I want to include zap function into assertion? How do I make that work?
Assuming you're using sinon-chai, you can use calledWith along with sinon.match to achieve this
expect(fnStub).to.have.been.calledWith(sinon.match({
foo: 'foo',
bar: 'bar'
}));
To achieve called.with partial object check, you can also use chai-spies-augment package (https://www.npmjs.com/package/chai-spies-augment) :
Usage (please, notice to .default after importing):
const chaiSpies = require('chai-spies');
const chaiSpiesAugment = require('chai-spies-augment').default;
chai.use(chaiSpies);
chai.use(chaiSpiesAugment);
usage :
const myStub = { myFunc: () => true };
const spy1 = chai.spy.on(myStub, 'myFunc');
myStub.myFunc({ a: 1, b: 2 });
expect(spy1).to.have.been.called.with.objectContaining({ a: 1 });

Categories

Resources