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

add support for object property selector #110

Merged
merged 1 commit into from
Jan 20, 2016
Merged

Conversation

hartzis
Copy link
Contributor

@hartzis hartzis commented Jan 11, 2016

@blainekasten, thanks for quoting me in #105, gave me the drive to attempt to add the support. Let me know what you guys think.

I also just happened to notice while I tried to follow code patterns adding functionality that there seems to be some opportunity to reuse some code between MountedTraversal and ShallowTraversal. There was definitely some copy & paste I did :)

@@ -95,6 +96,10 @@ export function nodeHasType(node, type) {
return node.type.name === type || node.type.displayName === type;
}

export function nodeHasProps(node, props) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function name clashes quite a lot with the nodeHasProperty function. I think it should probably be more explicit so it's known that it's meant for the object selector.

Something like nodeMatchesObjectProps ?

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 like that name a lot more!

@hartzis
Copy link
Contributor Author

hartzis commented Jan 12, 2016

Updated with suggested edits.

export function instMatchesObjectProps(inst, props) {
if (!isDOMComponent(inst)) return false;
const node = getNode(inst);
return !isEmpty(props) && isMatch(propsOfNode(node), props);
Copy link
Contributor

Choose a reason for hiding this comment

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

the !isEmpty check seems redudant. On L318, you are ensuring that props can't be null.

So it seems like the isEmpty call is just a perf concern to make sure users aren't doing find({}), but that should be the minority case.

So you are actually just adding complexity for the typical case of find({foo: bar}).

Ultimately, I think the isEmpty call is superflous and negatively affecting performance in a very small way.

Copy link
Member

Choose a reason for hiding this comment

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

I don't see a line 318 in this diff.

what does isMatch(propsOfNode(node), {}) do?

Copy link
Contributor

Choose a reason for hiding this comment

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

L218. My bad.

isMatch compares keys and values of two objects to be equal

Copy link
Member

Choose a reason for hiding this comment

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

right, so, if propsOfNode(node) are empty, and props are empty, then as-is, it returns false - with your change, it returns true.

Which is desired?

Copy link
Contributor

Choose a reason for hiding this comment

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

That is true. But how often is somebody going to look for something with an empty object? That just seems like a design against the wrong use-case. And I guess maybe somebody does want to search for nodes that don't have any props? which find({}) could support? feels awkward though. I guess I could see either way here.

Copy link
Member

Choose a reason for hiding this comment

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

That seems reasonable (to throw an error). @lelandrichardson, thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if they are actually passing in an empty object, then matching all nodes actually seems like the reasonable response to me?

I agree this doesn't seem like a normal use case, although I can imagine some code that builds up some object dynamically, and in some cases might build up to nothing...

I don't feel strongly about this, but I don't see the harm in having {} match all nodes. It seems like the expected behavior, despite being a "corner-case" type input?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree this doesn't seem like a normal use case, although I can imagine some code that builds up some object dynamically, and in some cases might build up to nothing...

Exactly. But I would say that's a bug in their code. If they are dynamically building an object, and it gets to an empty state. An error should be thrown, so they can make sure they are testing what they want.

I don't feel strongly about this, but I don't see the harm in having {} match all nodes. It seems like the expected behavior, despite being a "corner-case" type input?

Would you also want to see find('[]') to match every node? Because that is the same API effectively.

Comparing it to the '[]' api, I would think it should only find components with props that match the specified key/values.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems its 3-against-1, so I'm happy to go with the approach of throwing an error for this case.

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'll update this after we discuss the best solution to the nested objects below.

@lelandrichardson
Copy link
Collaborator

This looks great. Quick question, though:

isMatch makes a strict equality check on the props of the object passed in. This means that the following will not work:

const wrapper = shallow(<div style={{ height: 20 }} />);
expect(wrapper.is({ style: { height: 20 } })).to.equal(true);

Do we potentially want to implement our own matcher? We may even want to have special flattening for the style prop.

@hartzis
Copy link
Contributor Author

hartzis commented Jan 15, 2016

Yeah style poses an interesting problem. Also just general nested objects { prop: { id: 1 } }.

isMatch was used because underscore is already included in the library.

is-subset is used by skin-deep to compare prop objects. What about using is-subset?

@ljharb
Copy link
Member

ljharb commented Jan 15, 2016

I also made is-equal which works a bit more reliably than assert.deepEqual and is used by expect. It doesn't work for subsets though.

@hartzis
Copy link
Contributor Author

hartzis commented Jan 17, 2016

@ljharb Just took a look at is-equal, very nice library, but i think you're right, since it doesn't work for subsets I decided to give is-subset a whirl.

Let me know how you guys feel about this possible solution.

expect(wrapper.find({ more: { a: 1 } })).to.have.length(0);
expect(wrapper.find({ more: [{ id: 1 }] })).to.have.length(2);
expect(wrapper.find({ more: { item: { id: 1 } } })).to.have.length(1);
expect(wrapper.find({ style: { height: 20 } })).to.have.length(1);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lelandrichardson This works now with is-subset, what do you think?

@lelandrichardson
Copy link
Collaborator

I was trying to think about what the most sensible API for this is... I think there are 2 questions:

  1. Since we are effectively doing "subset" matching behavior one level down for props, should we continue to match things as subsets at the sub-prop level?
  2. Since style has special built-in behavior, do we want to handle it differently by pre-flattening the styles array for matching?

Question 2 is sort of a special case of Question 1... it just goes a bit further. At the moment I feel like the answer to 2 is "yes", and if 2 is "yes" I think 1 has to be yes or else the API becomes inconsistent.

As a result, I think the following API would be ideal:

  • { foo: 'bar' } matches <Foo foo="bar" />
  • { user: { id: 1 } } matches <Foo user={{ id: 1, name: 'Bob' }} />
  • { foo: 'bar', qoo: 'qux' } matches <Foo foo="bar" qoo="qux" />
  • { style: { width: 20 } } matches <Foo style={{ width: 20, height: 20 }} />
  • { style: { width: 20 } } matches <div style={[{ width: 20 }, { height: 20 }]} />
  • { foo: 'bar', qoo: 'qux' } does not match <Foo foo="bar" />
  • { style: { width: 20 } } does not match <Foo style={[{ width: 20 }, { height: 20 }]} />

Let me know if any of you guys disagree, but I feel like that last rule is probably the right behavior. Basically, I'm thinking we will only "flatten" styles when the node type is a string (ie, it's a DOM node).

This could end up kind of annoying though, because some people could have a test set up where their render method somewhere along the lines has something like:

<input style={[styles.input, error && styles.inputError]} ... />

Then they have a test somewhere that asserts that the inputError styles are applied to the input. With the proposed API above, this works swimmingly BUT... at some point there might be a refactor like the following:

<Input style={[styles.input, error && styles.inputError]} ... />

At this point, this test will end up failing (assuming a shallow render), but its still behaving properly. Now writing the same test becomes a lot more difficult.

I guess the question comes down to whether or not we want to treat the style prop as a special case for all components, or just for DOM components where we know that they behave in this way? I think there are arguments for both cases...

@ljharb
Copy link
Member

ljharb commented Jan 17, 2016

If it matches subsets all the way down, how would I go about not matching a subset, ie, only doing strict matching?

I think that the only special cases should ever be on DOM components - style is just a random prop on anything else.

@hartzis
Copy link
Contributor Author

hartzis commented Jan 17, 2016

I think that the only special cases should ever be on DOM components - style is just a random prop on anything else.

I agree with this. @ljharb. I also haven't thought about strict matching. Maybe hold off till users are asking for it? Then maybe an options object passed as second argument with the selector?

@lelandrichardson I'm having trouble understanding <div style={[{ width: 20 }, { height: 20 }]} />. This doesn't seem to currently be a thing in v0.14, it looks like it might be something someday?

But the array for style is currently working for React Native Components, but they have to be Components <Text/> not dom elements/nodes <div/>. And then the trouble is in 'regular react' passing that array for style just becomes a prop of style whose value is an array.

example fiddle https://jsfiddle.net/n38pwsdc/

So i could see this:

  • { style: { width: 20 } } matches <Foo style={[{ width: 20 }, { height: 20 }]} />
  • { style: { width: 20 } } does not match <div style={[{ width: 20 }, { height: 20 }]} />

but even this is weird because the top one truly only works for react native.

If we just keep it as is just looking for subsets of props, then the user has to know what to expect since they are testing. No flattening needed on our end.

@lelandrichardson
Copy link
Collaborator

LOL. @hartzis you're completely right. I've just been working way too much in react native recently... we don't need to do anything specific for style then at this point.

As far as strict matching goes... I agree that maybe we just wait on this. As a result, I think the current state of this PR is what we all agree on?

@hartzis
Copy link
Contributor Author

hartzis commented Jan 18, 2016

no worries. let me know about those errors and i'll get it updated asap to get this merged in

@blainekasten
Copy link
Contributor

LGTM.

1 similar comment
@lelandrichardson
Copy link
Collaborator

LGTM.

ljharb added a commit that referenced this pull request Jan 20, 2016
[semver-minor] add support for object property selector
@ljharb ljharb merged commit 3feb0ca into enzymejs:master Jan 20, 2016
@lelandrichardson lelandrichardson added the semver: minor New stuff. label Feb 2, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver: minor New stuff.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants