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

Bug: Built-in <component /> functionality seems altered when shallow: true #1277

Closed
phobetron opened this issue Jan 25, 2022 · 11 comments
Closed
Labels
bug Something isn't working needs reproduction

Comments

@phobetron
Copy link
Contributor

Describe the bug
When shallow-mounting a component that contains the built-in <component /> component, its behavior seems to be altered.

To Reproduce
In my current case, I'm testing an SFC with script setup that has <component :is="someComponent" /> in its template. someComponent is a computed property that returns a value of the type Component.

When testing with shallow: true, a component named <some-component /> is rendered. It seems to match the name of the computed property.

Expected behavior
When testing with shallow: true, a component named <custom-component /> is rendered. It should match the return value of the computed property.

Related information:

  • @vue/test-utils version: 2.0.0-rc.18
  • Vue version: 3.2.26
  • node version: 16.13.0
  • npm (or yarn) version: yarn 1.22.17

Additional context
When using shallow: false, or running normally in the browser, the built-in <component /> component functions as expected, and <custom-component /> is rendered.

I believe it is not possible to stub the functionality of <component /> as it is a built-in component.

The components that are rendered using <component /> are fairly complex, and are tested separately, so we are hoping to merely inspect the values passed to those components instead of reaching through their hierarchies and inspecting their behaviors.

@phobetron phobetron added the bug Something isn't working label Jan 25, 2022
@phobetron
Copy link
Contributor Author

phobetron commented Jan 25, 2022

I've typed it up based on what's in my codebase, but haven't actually run it. Hopefully, it will help explain the issue I'm running into, either way.

OneOrOtherComponent.vue

<template>
  <component :is="someComponent" />
</template>

<script setup>
import { computed } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

interface Props {
  isB?: boolean
}

const props = defineProps<Props>()

const someComponent = computed(() => {
  return isB ? ComponentB : ComponentA
})
</script>

OneOrOtherComponent.spec.ts

import { mount } from '@vue/test-utils'
import { OneOrOtherComponent } from './'

describe('OneOrOtherComponent', () => {
  it('renders component A by default', () => {
    const wrapper = mount(OneOrOtherComponent, { shallow: true })

    // This outputs "<some-component />"
    console.log(wrapper.html())

    // This fails
    expect(wrapper.findComponent({ name: 'component-a' }).exists()).toBe(true)
  })
})

@lmiller1990
Copy link
Member

Thanks for the great report - I will try get some 👀 on this soon.

@freakzlike
Copy link
Collaborator

freakzlike commented Jan 26, 2022

I know the name resolution a bit, so I took a look at this.

VTU searchs the component within the exposed script setup (vue internal setupState). ComponentA and ComponentB are not directly used in the template, so vue does not expose these in the setupState. Even if so, VTU would use the first occurens of the component which could either be someComponent or ComponentB.

Workarounds

Currently I have no solution for this but some workarounds:

  1. Use the component itself for findComponent instead of the name.
    The .html() will still be <some-component-stub></some-component-stub>
expect(wrapper.findComponent(ComponentA).exists()).toBe(true)
  1. Provide a name for ComponentA and ComponentB Edit: Does not work see comment
  2. Avoid the computed property and evaluate directly in template
<template>
  <component :is="isB ? ComponentB : ComponentA" />
</template>

@phobetron
Copy link
Contributor Author

@freakzlike Thanks for the ideas!

  1. Use the component itself for findComponent instead of the name.

I'll give this one a try and report back.

  1. Provide a name for ComponentA and ComponentB

Do you mean, in a stub component passed to mount?

  1. Avoid the computed property and evaluate directly in template

This one would be a bit ugly to evaluate in the template, since the actual implementation I'm working on chooses between about 5 different components based on a key. I'll see what I can do that could maybe trick the VTU.

@freakzlike
Copy link
Collaborator

@freakzlike Thanks for the ideas!

  1. Use the component itself for findComponent instead of the name.

I'll give this one a try and report back.

  1. Provide a name for ComponentA and ComponentB

Do you mean, in a stub component passed to mount?

  1. Avoid the computed property and evaluate directly in template

This one would be a bit ugly to evaluate in the template, since the actual implementation I'm working on chooses between about 5 different components based on a key. I'll see what I can do that could maybe trick the VTU.

On 2 I mean this:

ComponentA.vue

<template>
  <!-- ... -->
</template>

<script>
export default {
  name: 'ComponentA'
}
</script>

ComponentB.vue

<template>
  <!-- ... -->
</template>

<script>
export default {
  name: 'ComponentB'
}
</script>

@phobetron
Copy link
Contributor Author

phobetron commented Jan 27, 2022

Workaround 1

This seems to work in most cases. However, there are a few caveats remaining:

  • Whether content is passed to a named slot (not default) can not be tested unless a stub component is used
  • If a stub component is used, there is no match, because the actual component is the one that is expected
  • If the actual component is passed to global.stubs, named slots can not be tested for some reason

I think the first two points make sense under the circumstances, but the third point was a bit of a surprise.

Workaround 2

This did not seem to have an effect. I could try poking around at it more.

Workaround 3

This works, but only when the computed is also removed from the component. It is not enough for the reference to be removed from the template.

From poking at it a bit, it seems that the name of the first computed that returns the same component that is in <component :is="..." /> is what is rendered as the component:

  • if :is receives ComponentA, and a computed anyComputedName returns ComponentA, the output is any-computed-name-stub
  • if :is receives ComponentA, and a computed anyComputedName returns ComponentB, the output is component-a-stub

This seems to be the VTU behavior @freakzlike mentioned.

@freakzlike
Copy link
Collaborator

Well named slots are never rendered with the auto stub from VTU:
https://next.vue-test-utils.vuejs.org/guide/advanced/stubs-shallow-mount.html#default-slots-and-shallow

What I tried to explain is exactly this logic:

const getComponentNameInSetup = (
instance: any | null,
type: VNodeTypes
): string | undefined =>
Object.keys(instance?.setupState || {}).find(
(key) => instance.setupState[key] === type
)

And the arguments at this scenario

type = ComponentA
setupState = {
  someComponent: ComponentA,
  ComponentA: ComponentA,
  ComponentB: ComponentB
}

So .find will find someComponent first

Update for Workaround 2

I noticed, that the name of the component is used as fallback after the script setup. So unfortunally this workaround is not working. Sorry for that :(

@phobetron
Copy link
Contributor Author

@freakzlike

Regarding named slots, the important note is the third point, that even with component stub objects registered in global.stubs, named slots still seem inaccessible, which is different from v1 stub behavior. I'm not sure that is intended behavior, but it's likely out of scope for this issue either way. Such caveats are for anyone else who may run into this issue and is looking for a workaround.

Regarding .find returning someComponent as ComponentA's component name due to the logic in getComponentNameInSetup, is that intended behavior, or is that the target for a possible fix? I could try looking into that when I have some time.

@cexbrayat
Copy link
Member

@phobetron This issue is a bit long and I'm getting lost in the details.
Would you mind setting up failing test cases that you would expect to work? You can try to build the repro directly in this repo by adding a test, or provide a github repo based on create-vue for example. That would be great to help us investigate!

@phobetron
Copy link
Contributor Author

@cexbrayat I'll see what I can do over the weekend 👍🏻

@phobetron
Copy link
Contributor Author

@cexbrayat I've added minimal tests in a PR: #1312

I'm not sure whether the tests are appropriately arranged, but let me know and I can make changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working needs reproduction
Projects
None yet
Development

No branches or pull requests

4 participants