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

Skip undefined props when comparing nodes #662

Merged
merged 2 commits into from
Sep 26, 2017

Conversation

vadimdemedes
Copy link
Contributor

Fixes #656.

This PR ensures null and undefined props are skipped when comparing nodes:

expect(nodeEqual(
   <div className={null} id={undefined} />,
   <div />
)).to.equal(true);

A helper function cleanProps removes all null and undefined props. I wanted to do that in propsOfNode(), but there are multiple return statements, which would result in a lot of repetition.

Let me know if this PR needs modification or anything else ;)

Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think doing this in propsOfNode is the proper way, since a lot of places call that - repetition inside an abstraction is OK.

Tests LGTM tho!

src/Utils.js Outdated
for (let i = 0; i < leftKeys.length; i += 1) {
const prop = leftKeys[i];
// we will check children later
if (prop === 'children') {
if (prop === 'children' || !left[prop]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be left[prop] == null, since 0 is falsy too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, leftover from debugging.

src/Utils.js Outdated
@@ -1,5 +1,6 @@
/* eslint no-use-before-define:0 */
import isEqual from 'lodash/isEqual';
import isNil from 'lodash/isNil';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure why we need a dependency for == null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed the linter won't like non-strict equality check, I should've checked, my bad!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a non-strict check against null is the sole exception :-)

src/Utils.js Outdated
@@ -114,7 +115,7 @@ export function nodeEqual(a, b, lenComp = is) {
}

if (!isTextualNode(a)) {
const rightKeys = Object.keys(right);
const rightKeys = Object.keys(right).filter(key => !isNil(right[key]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's just do key => right[key] != null - it's clearer and built-in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, agree.

@vadimdemedes
Copy link
Contributor Author

vadimdemedes commented Nov 6, 2016

Apparently, moving cleanProps() to propsOfNode() breaks these tests:

  1. https://github.com/airbnb/enzyme/blob/master/test/ShallowTraversal-spec.jsx#L134
  2. https://github.com/airbnb/enzyme/blob/master/test/ShallowWrapper-spec.jsx#L577

I guess cleanProps() should stay inside nodeEqual(). @ljharb What do you think?

@ljharb
Copy link
Member

ljharb commented Nov 6, 2016

If we're going to ignore null, we need to ignore it consistently. In other words, searching for a value of null should be the same as a value of undefined, or as "absence".

In other words, if we're asking if a node has property "foo" equal to null, and it doesn't have the property "foo", then I'd expect it to pass.

@vadimdemedes
Copy link
Contributor Author

Ok, I see your point!

Regarding this test: https://github.com/airbnb/enzyme/blob/master/test/ShallowWrapper-spec.jsx#L577, let me clarify one last thing. If there are nodes with props equal to null or undefined and we're looking for them (like in the test), these nodes shouldn't be returned, because we treat those props as missing.

expect(wrapper.find({ prop: undefined })).to.have.length(0);
expect(wrapper.find({ prop: null })).to.have.length(0);

@ljharb
Copy link
Member

ljharb commented Nov 6, 2016

yeah, that's a good point/wrinkle. It seems like the options are, when finding { foo: null, bar: 3 }, to find anything that omits foo and has bar equal to 3 - or, to simply find anything that has bar equal to 3.

If we choose the former path, .find({ prop: null }) will find everything. If we choose the latter, .find({ prop: null }) will find nothing. I'm not sure if either makes sense.

@vadimdemedes
Copy link
Contributor Author

I think there's a 3rd option. Check props in find() and throw if there are null or undefined values.

find(props) {
  const hasNilProps = Object.keys(props).some(key => props[key] == null);

  if (hasNilProps) {
    throw new TypeError('Props can\'t have `null` or `undefined` values.');
  }

  // ...
}

What do you think?

@ljharb
Copy link
Member

ljharb commented Nov 6, 2016

I think that's probably best, but then that means that it's a breaking change.

@lelandrichardson @aweary @blainekasten @nfcampos what do you think?

@nfcampos
Copy link
Collaborator

nfcampos commented Nov 6, 2016

this 3rd option is definitely the best of the three. this change should be accompanied with a note on the docs for find() saying that if you want to find nodes whose props are null you can use findWhere. maybe even the error message should mention this. what do you think?

@vadimdemedes
Copy link
Contributor Author

I think @nfcampos's proposal is reasonable and makes this change clear to users.

@aweary
Copy link
Collaborator

aweary commented Nov 7, 2016

I think @nfcampos's proposal is reasonable and makes this change clear to users.

Agreed 👍

@vadimdemedes
Copy link
Contributor Author

Glad we agree, I'll update the PR!

@ljharb
Copy link
Member

ljharb commented Nov 8, 2016

ok, so it looks like React ignores undefined but not null - http://stackoverflow.com/questions/34809178/react-js-default-prop-is-not-used-with-null-is-passed - so we should change this PR to continue to treat null as a valid, existing value.

@vadimdemedes
Copy link
Contributor Author

@ljharb Good catch, will do!

@vadimdemedes vadimdemedes changed the title Skip null or undefined props when comparing nodes Skip undefined props when comparing nodes Nov 8, 2016
@vadimdemedes vadimdemedes force-pushed the skip-null-or-undefined-props branch from 379ea7c to 034cc0c Compare November 8, 2016 13:33
 - `.find` with an object with `undefined` values should throw

Fixes enzymejs#656.
@vadimdemedes
Copy link
Contributor Author

PR updated, addressing all of the feedback above. Thanks to everyone!

One question: I put the warning about an error when using .find() and having some undefined props in the docs/selector.md. Should I put it in API docs for .find() or leave it there?

@vadimdemedes vadimdemedes force-pushed the skip-null-or-undefined-props branch from 34a6d8c to 886e90d Compare November 10, 2016 14:29
nfcampos
nfcampos previously approved these changes Nov 12, 2016
Copy link
Collaborator

@nfcampos nfcampos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great @vdemedes just please add the missing trailing commas to appease the linter and make travis green :)

@@ -32,4 +32,4 @@ expect(wrapper.equals(<div className="foo bar" />)).to.equal(true);
when you are calling it you are calling it with a ReactElement or a JSX expression.
- Keep in mind that this method determines equality based on the equality of the node's children as
well.

- Following React's behavior, `.equals()` ignores properties, whose values are `undefined`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove the comma after 'properties'

@vadimdemedes vadimdemedes force-pushed the skip-null-or-undefined-props branch from 886e90d to 4827455 Compare November 12, 2016 12:19
@vadimdemedes
Copy link
Contributor Author

All good now, thanks guys for the feedback ;)

@@ -78,6 +78,13 @@ export default class ComplexSelector {
return this.handleSelectors(selectors, wrapper);
}

if (typeof selector === 'object' && selector !== null) {
const hasUndefinedValues = Object.keys(selector).some(key => selector[key] === undefined);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import values from 'object.values' and you can do values(selector).some(value => value === undefined)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, updated!

@ljharb ljharb added semver: major Breaking changes and removed semver: patch labels Nov 12, 2016
@vadimdemedes vadimdemedes force-pushed the skip-null-or-undefined-props branch from 4827455 to 41b0c8a Compare November 12, 2016 16:47
src/Utils.js Outdated
newProps[key] = props[key];
});

return newProps;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternatively:

return entries(props)
  .filter(([, value]) => value !== undefined)
  .reduce((acc, [key, value]) => assign({}, acc, { [key]: value }), {});

@vadimdemedes vadimdemedes force-pushed the skip-null-or-undefined-props branch from 41b0c8a to ae25d12 Compare November 13, 2016 13:29
@nfcampos
Copy link
Collaborator

@ljharb should we get this in for v3?

@ljharb ljharb force-pushed the skip-null-or-undefined-props branch from e0c3c45 to eca06ae Compare September 23, 2017 15:37
@ljharb
Copy link
Member

ljharb commented Sep 23, 2017

I've rebased this on top of latest master; @aweary @nfcampos would you mind rereviewing?

@ljharb ljharb dismissed nfcampos’s stale review September 23, 2017 15:37

needs a rereview

@ljharb ljharb requested review from aweary and a team September 23, 2017 15:38
Copy link
Collaborator

@nfcampos nfcampos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments

@@ -141,6 +142,10 @@ export function buildPredicate(selector) {
// If the selector is an non-empty object, treat the keys/values as props
if (typeof selector === 'object') {
if (!Array.isArray(selector) && selector !== null && !isEmpty(selector)) {
const hasUndefinedValues = values(selector).some(value => value === undefined);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this one a test of the value itself but the others are testing typeof?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My habit is to always use typeof for safety; babel makes this unnecessary. I'll make them all be the same.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's now consistent in the project.

return (node && node.props) || {};
return entries((node && node.props) || {})
.filter(([, value]) => typeof value !== 'undefined')
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is rather ineffecient, creating as many objects as you have keys, I know this is a testing library, but if you have a lot of tests it starts to add up

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I could mutate the accumulator since the overall reduce is pure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

export function nodeMatchesObjectProps(node, props) {
return isSubset(propsOfNode(node), props);
return isSubset(propsOfNode(node), replaceUndefinedValues(props));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t understand why we need this change. Don’t we throw if the received props contain undefined values?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In find, yes, but I don't think that's the only place this is called.

@ljharb ljharb force-pushed the skip-null-or-undefined-props branch from eca06ae to 9bb8a91 Compare September 26, 2017 03:31
@ljharb ljharb merged commit efb3913 into enzymejs:master Sep 26, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants