Open matanshukry opened 4 years ago
How does your functional child use the prop? Presumably to render in the DOM (that's all functional components do).
You can test it by using mount
and assert against the rendered DOM, not the passed props/attrs.
I tried to do that, but then I need to check the "href" attribute of the result; which means the route need to be resolved and I also need to put the app routes on it.
I tried to go that way as well, but I can't get it to work either; one of the nested components (BLink from vue-bootstrap specifically) uses this.$router , which is undefined when installing VueRouter on the localVue.
It works if I'm installing VueRouter on the global Vue, but this mess up mocking $route for other tests (as you probably know).
Code on using local vue that ends up with an undefined this.$router:
import VueRouter from "vue-router";
import router from "@/router";
const localVue = createLocalVue();
localVue.use(VueRouter);
// import Vue from 'vue';
// Vue.use(VueRouter);
const wrapper = mount(MyComponent, { localVue, router });
console.log("Html: ", wrapper.html());
As said, only when the 2 commented out lines are "commented-in" the result .html() will contain a valid href (and not just "#")
I managed to create a small sandbox code that shows the problem with the global vue (using mount like you suggested @lmiller1990 ): https://codesandbox.io/s/vue-template-g6wqb
Currently if you run the test it will fail. If you go to MyComp.test.js
you'll see a commented line (installing VueRouter globally). Uncomment it and the test will succeed.
This should work. I will debug this now.
I looked into this. I tried <b-nav-item :to="{ name: 'home' }">home</b-nav-item>
using Jest locally and in an actual browser and both give "#" as the route. I don't think this is a bug in VTU, but the bootstrap nav component (or the usage).
I verified this here: https://gist.github.com/lmiller1990/f6b9f6c5fa28e4e6cb4089b5765cb4c4
If you run that in a browser you will see the normal <router-link>
works fine, but <b-nav-link>
Original example from here: https://router.vuejs.org/guide/essentials/named-routes.html
You may need to investigate BootstrapVue and how it works with the router. On first look, their link component does not seem to check if the to
prop is an object: https://github.com/bootstrap-vue/bootstrap-vue/blob/8241644477b174042bb163ba1741c3066165d9f9/src/components/link/link.js#L101
@lmiller1990 I'm confused - you do realize the codesandbox link I gave does work if you use VueRouter globally, even with VTU? That is, it does NOT give "#".
I haven't actually tried your gist, but I'm pretty sure it's not working due to case sensitive.
That is, the route is 'home' (lowercase 'h')
{ path: '/', name: 'home', component: Home },
while the nav item name is Home (uppercase 'H')
<b-nav-item :to="{ name: 'Home' }">Home</b-nav-item>
Unlike the router links which all uses lowercase, matching the routes.
Regarding vue bootstrap link: the important part (regarding my issue) is actually computeTag
and not computeHref
. That is, computeTag
will return the acutal tag used. If the tag is 'router-link' (which it should be), it will pass the to
prop to it and there's no problem.
However, when installing VueRouter locally (unlike globally), the $router is undefined and hence the computeTag
will return 'a' rather than 'router-link'. Line:
p.s. To clarify, I know all that by debugging locally
I think I missed that you had the global usage commented out. I see what you are explaining now with the global router.
One you you forgot was localVue.use(BootstrapVue)
. Your localVue doesn't know about the bootstrap components.
I'll investigate some more. Do you have a real repo by any chance? It is very difficult to debug on code sandbox - constantly I get random errors like "record is undefined" for no reason. It not, no problem, I can copy paste from the sandbox.
This is so weird. Any non :to
prop renders as object Object
. What is going on with :to
?
I dug deeper.
This is incorrectly returning when using localVue. For some reason BootstrapVue is not finding the locally installed router? Is it using the global vue instance instead maybe? There is a function in BoostrapVue that does isRouterLink(tag)
and returns true
if the tag
is a
- which it is in this example.
I think this particular bug is unique to BoostrapVue. As you demonstrated, regular functional components receive their props (as object Object).
BootstrapVue seems to do a bunch of stuff to integrate with VueRouter, something is going on there. This will require some more digging into BootstrapVue, I think. Do you know their codebase well? Maybe we can work together on this.
@lmiller1990 First let me say thanks for looking into it!
And I have a code base but it's only local on my computer and it's not something I can share. I can debug any place if there's something specific you want me to.
from my debugging, the code you referred to in isRouterLink
is not the problem. The problem is the tag that's passed to it.
The problem is with computeTag
. Specifically the part where it's checking thisOrParent.$router
which is undefined, which results in a the ANCHOR_TAG rather than the router-link
tag.
I can debug a bit more to see why the $router is null, but I'm not sure when it's suppose to be "filled". One thing that could be is that when installing Vue-Router locally, this.$router
is null at the render() hook method, which is where computeTag
is being used. Not sure how to check ti though
Sorry, by code base I meant the BootstrapVue code base.
Something weird is definitely happening where BootstrapVue doesn't know about the mock router. I don't know if I can look too much more into it, but if anything comes to mind I will post it here 🤔
The fact it works w/ the global router is super weird to me. My guess is BootstrapVue does import Vue
somewhere and it is getting the non local vue, but global one.
@lmiller1990 The BootstrapVue code base is open source on github: https://github.com/bootstrap-vue/bootstrap-vue
Also, the code that calls computeTag() is simply passing "this", where this.$router
is undefined as well
@lmiller1990 Got a simpler code for you to check:
https://codesandbox.io/s/vue-template-bj9he
I pretty much copied the 'BLink' and 'BNavItem' code in there, but reduced a lot of code that's unrelated.
So now a router-link will always be created, but without the global router the path will either be "#" (incorrect) or "/hello" (correct).
^ Yep thanks, I ended up making my own case just like this. I think we need to do a deep dive in the bootstrap-vue codebase and explore how it is using vue router.
@lmiller1990 What? are you sure you looked at the code I provided?
To be clear, the code I provided doesn't use Bootstrap-vue at all. There are leftovers in there, but it's not part of the test.
Here, I even removed the import lines and the package.json usage, so now there's no mention to bootstrap-vue other than comments: https://codesandbox.io/s/vue-template-2upde
👀
I'm silly, I assumed something incorrectly without properly looking at the code - I was looking at some left overs from previously.
This is very good minimal repro. I'll make a repo locally with this and debug a bit - sandbox does not appear to expose node_modules :(
I guess we are doing something incorrectly with functional components?
@lmiller1990 all good :)
I'm not too familiar with functional components to be honest. I suspected as well, which is why I've tried to make MyNav non functional, but since I'm not familiar with it enough it threw some errors I didn't know how to handle. I might give it another try tomorrow
I don't see anywhere in our codebase where we do anything special with functional components except for the shallowMount hacks. This reproduction is very good though, let's dig a bit deeper, probably with a log console.logs and debug statements. Thanks @matanshukry!
@lmiller1990 Made it even smaller now:
https://codesandbox.io/s/vue-template-v2urw
Changes:
functional
keyword in template functional
in MyNav2.vue, it will passSo it's either something with functional or something with the short code in MyNav.js. Hopefully it helps more!
if functional components are treated like or are actually DOM nodes in a shallow mount scenario, then it's not possible to store objects in DOM node attributes.
I've tried this in the browser with a DOM node:
so maybe need to use another approach to store the attributes ? dataset ?
possible workaround, use a stub like this:
const wrapper = shallowMount(ComponentToTest, {
...
stubs: {
TheInnerComponent: {
props: {
stringArgs: {
type: String,
},
objectArgs: {
type: Object,
},
},
template: '<div/>',
},
},
})
...
console.log(wrapper.findComponent(TheInnerComponent).props)
In this case you can simply use "props".
Drawback is that you need to redefine all props/attributes that you want to test against.
I guess if a fix is possible in vue-test-utils it would need to do something similar internally for functional components.
Probably best to just use mount
at this point, depending on what you are trying to test.
Happy to accept a PR if you find something - I've spend a decent amount of time on this, and have a ton of other issues to triage, so I can't look into this one right now.
hope this helps somebody
Component
<template> <oc-button id="files-list-not-found-button-reload-link" type="router-link" appearance="raw" :to="publicLinkRoute" <translate>Reload public link</translate> </oc-button> </template> <script> export default { .. computed: { publicLinkRoute() { const item = this.$route.params.item.replace(/^\/+/, '') return { name: 'files-public-list', params: { item: item.split('/')[0] } } } } } </script>
Spec File
function getMountedWrapper(route) { return mount(NotFoundMessage, { localVue, store: store, stubs: stubs, mocks: { $route: route } }) } function publicLinkRoute(item) { return { name: 'files-public-list', params: { item: item } } } describe("component", () => { it('should have property route to files public list', () => { // remember to use mounted wrapper const wrapper = getMountedWrapper(publicLinkRoute('parent/sub')) const reloadLinkButton = wrapper.find(selectors.reloadLinkButton) // if you log reloadLinkButton HTML: o/p is like // <oc-button-stub type="router-link" size="medium" to="[object Object]" variation="passive" appearance="raw" justifycontent="center" gapsize="medium" id="files-list-not-found-button-reload-link"><translate-stub>Reload public link</translate-stub></oc-button-stub>
// but you can access the `[object Object]` value using `props()` method.
expect(reloadLinkButton.props().to.name).toBe('files-public-list')
expect(reloadLinkButton.props().to.params.item).toBe('parent')
})
})
Version
1.0.0-beta.33
Reproduction link
https://codesandbox.io/s/vue-template-yy6q9
Steps to reproduce
What is expected?
To have a way to test the value passed as an object property to a child functional component
What is actually happening?
There is no way to test the value passed as an object property to a child functional component