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

Automatically stub nested components with their names instead of <!----> #410

Closed
orels1 opened this issue Feb 3, 2018 · 23 comments · Fixed by #606
Closed

Automatically stub nested components with their names instead of <!----> #410

orels1 opened this issue Feb 3, 2018 · 23 comments · Fixed by #606

Comments

@orels1
Copy link

orels1 commented Feb 3, 2018

What problem does this feature solve?

In my workflow it is a pretty common use case to have a unit test that checks if some nested component is being rendered conditionally.

For example i have a component with a template like this (I use pug for templating, just so there won't be any confusion):

.container
  MyCustomComponent(v-if="condition" :title="cog.title")

And I want to have a unit test that makes sure that MyCustomComponent is only rendered when that condition is satisfied.

I'm coming from React background, where enzyme's shallow render passes component's display name by default. So if I would shallow render the setup above - the wrapper.html() would look something like this (if the v-if condition was met):

<div class="container">
  <MyCustomComponent title="something from cog.title" />
</div>

But in vue-test-utils the result would actually look like this:

<div class="container">
  <!---->
</div>

There is currently an option to supply a stubs object to shallow render method, but that requires you to manually write a list of components and their respective stubs, which is not time-efficient.

Currently I wrote a simple stub-generator, which acts somewhat like identity-object-proxy, and basically looks at the list of components in options of the component we want to render, and generates an object of stubs.

That allows me to achieve a result like this:

<div class="container">
  <mycustomcomponent title="something from cog.title" />
</div>

Which is basically what I need.

I can see more people that might want this kind of behaviour to speed up the unit testing, as this is a pretty common approach in other systems.

My stubs generator code for context:

// Using custom proxy to automatically stub with proper component names
const idObj = new Proxy({}, {
  get: function getter(target, key) {
    return `<${key} />`;
  },
});

// Generate the object of stubs with proxy
const genStubs = component => Object.keys(component.options.components).reduce((acc, comp) => {
  acc[comp] = idObj[comp];
  return acc;
}, {});

Pretty basic, but it allows me to simply use wrapper.find('<component name>'); to conditionally check if something was rendered, without manually supplying the stubs object every time, since I have a custom wrapper around shallow that passes my generator to the stubs option.

What does the proposed API look like?

I'm not sure if this should be the default behaviour, since people might rely on current way of stubbing in the existing code, but I can see something like a flag "useNamedStubs" for shallow options, or something similar.

@eddyerburgh
Copy link
Member

eddyerburgh commented Feb 4, 2018

This is a great idea! We could make it an option, and also have it as an option you can set in the config, so you only need to set it once:

VueTestUtils.config.shallowStubsAsText = true

Would you like to add a PR? I'm happy to if you're not able to

By the way, are you aware you can check a component has been rendered with shallow?

import MyCustomComponent from '~/MyCustomComponent.vue'

const wrapper = shallow(TestComponent)
wrapper.find(MyCustomComponent).exists()

This is sort of a clone of #28. When we add this feature, we can close #28

@orels1
Copy link
Author

orels1 commented Feb 4, 2018

Sadly, I have too much on my hands atm to make a PR.

I was unaware that you can check if a component is included that way, but I think the text stubs will still help when you’re debugging stuff (logging out wrapper.html()) and it will make it a generally more streamlined experience.

Thanks for a quick response! :)

@orels1
Copy link
Author

orels1 commented Feb 16, 2018

Actually, I think I'll look into it. Have some more time this week. Shouldn't be that complicated

@orels1
Copy link
Author

orels1 commented Mar 5, 2018

So, yeah, that took more than a week :) I played around with the concept, and I'm a bit concerned about all the warnings in the console about using non-registered components with an approach like that. Was unable to get around them.

I will try to make a PR sometime this week, so maybe someone else can take a look as well.

@iztsv
Copy link
Contributor

iztsv commented Mar 10, 2018

This feature will be useful for snapshot testing also.
@orels1 could you please explain in more details how you use your "stubs generator" for render stubs as string?

@orels1
Copy link
Author

orels1 commented Mar 10, 2018

Hey @ilyaztsv you want me to share my current solution?
If so, you can find it used here: https://github.com/orels1/v3.cogs.red/blob/feature/%2326/unit-tests/test/unit/utils.js

The above is a set of handy utils I use to have better time testing. This approach drops a lot of warnings about "unregistered components", but if those are not a big deal for you - this setup works quite well. I was emulating the way shallow rendering works in enzyme

@eddyerburgh
Copy link
Member

@orels1 You can ignore custom elements by adding them to the ignoredElements array—https://vuejs.org/v2/api/#ignoredElements. That should stop warnings

@iztsv
Copy link
Contributor

iztsv commented Mar 12, 2018

@orels1 thanks for your solution. I'll try it with jest today.

@iztsv
Copy link
Contributor

iztsv commented Mar 12, 2018

@orels1 @eddyerburgh Unfortunately, "stub-generator" solution does not work for me. May be because of I use jest + vue-test-utils.
I've already created the issue for that #465

@orels1
Copy link
Author

orels1 commented Mar 12, 2018

@ilyaztsv my solution uses jest as well

@iztsv
Copy link
Contributor

iztsv commented Mar 13, 2018

@orels1 yes. I was wrong about jest. I wrote about the reason here #465

@AlbertBrand
Copy link

AlbertBrand commented Mar 24, 2018

I've written a stub generator that works with jest and vue-test-utils:

export function generateStubs(cmp) {
  return Object.values(cmp.components).reduce((stubs, stubCmp) => {
    const dashName = stubCmp.name
      .replace(/([a-z])([A-Z])/g, '$1-$2')
      .toLowerCase();
    stubs[dashName] = {
      render(createElement) {
        return createElement(dashName, this.$slots.default);
      },
    };
    return stubs;
  }, {});
}

This requires all components to have their name option set, be it in camel case or kebab case (see https://vuejs.org/v2/api/#name). It creates a stub component render function that renders the element and its children (if you're passing children you can see those in your snapshot as well).

Use it like this:

// components
export default {
  name: 'MyComponent',
  components: {
    FooBar,
  },
}

export default {
  name: 'FooBar',
}

// test
const component = shallow(MyComponent, {
  stubs: generateStubs(MyComponent),
});

@iztsv
Copy link
Contributor

iztsv commented Mar 26, 2018

@AlbertBrand unfortunately, it is still cause RangeError: Maximum call stack size exceeded for the case in https://github.com/ilyaztsv/jest-and-vue-test-utils-stubs repo.

you can see #465 for more details.

@AlbertBrand
Copy link

@ilyaztsv Interesting, I tried some things in your repo and found the issue. If you change line 13 in my-component.js to ChildComponent (so don't give it a dashed key), your test succeeds. It probably has to do with a clash of names.

@iztsv
Copy link
Contributor

iztsv commented Mar 26, 2018

@AlbertBrand thanks for the reply! It did help :)

@DrSensor
Copy link
Contributor

Seems like it also possible to auto-generate stubs when registering plugin like Vuetify.

@lambdalisue
Copy link

lambdalisue commented Mar 29, 2018

@AlbertBrand Thanks for sharing your tips. Unfortunately, #410 (comment) did not work in my environment while components for typescript user is wrapped with Vue.extend().
Additionally, the tag name generated from the function was different from what I specified to the components attribute of a parent component.

So I modified it as below and works well. In case someone faced the same problem.

export function generateStubs(component: any) {
  const children = component instanceof Function
    ? component.extendOptions.components
    : component.components;
  const reducer = (accumulator: {[key: string]: any}, value: string) => {
    const lhs = value[0];
    const rhs = value.substr(1);
    const name = (lhs + rhs.replace(/([A-Z])/g, '-$1')).toLowerCase();
    accumulator[name] = {
      render(createElement) {
        return createElement(name, this.$slots.default);
      },
    };
    return accumulator;
  };
  return !!children ? Object.keys(children).reduce(reducer, {}) : undefined;
}

Tested with

@Component
export default class MaterialIconView extends Vue {
  // ...
}

@Component({
  components: {
    'x-material-icon': MaterialIconView,
  },
})
export default class AccountMenuView extends Vue {
  // ...
}

const wrapper = shallow(AccountMenuView, {
  stubs: generateStubs(AccountMenuView),
});

expect(wrapper.html()).toMatchSnapshot();
// Produce below
//
// <div class="account-menu">
//   <div class="label"><span>[email protected]</span>
//     <x-material-icon icon="person"></x-material-icon>
//   </div>
//   <!---->
// </div>
//

@lambdalisue
Copy link

Unfortunately, above stub seal events, attributes, or etc so it is really difficult to test the behavior.

@trollepierre
Copy link
Contributor

Is someone working on this actually?

@eddyerburgh
Copy link
Member

Not currently.

If you'd like to work on it, I would be happy to review a PR :)

@trollepierre
Copy link
Contributor

@AlbertBrand 's solution looks well to me.

@kfischer-okarin
Copy link

I personally have been using a generalized mock-component that just displays a fake element and renders all its slots and children and most of the props and attrs as normal DOM attributes and it works quite well for snapshots when used in combination with jest-serializer-html.

Here is the gist

Maybe it's helpful for someone

@eddyerburgh
Copy link
Member

I've made a PR: #606

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants