vuejs / test-utils

Vue Test Utils for Vue 3
https://test-utils.vuejs.org
MIT License
1.04k stars 245 forks source link

Helpers: Suspense (and others?) #108

Closed lmiller1990 closed 4 years ago

lmiller1990 commented 4 years ago

105 had some great discussion around helpers - the example raised there was for components with async setup functions (used in <Suspense>). Testing those alone won't work, since if you have a async setup, Vue expected a <Suspense> wrapper.

We could provide a helper (this works)

  test('uses a helper to mount a component with async setup', async () => {
    const Comp = defineComponent({
      async setup() {
        return () => h('div', 'Async Setup')
      }
    })

    const mountSuspense = async (component: new () => ComponentPublicInstance, options) => {
      const wrapper = mount(defineComponent({
        render() {
          return h(Suspense, null, {
            default: h(component),
            fallback: h('div', 'fallback')
          })
        }
      })
      ...options)
      await flushPromises()
      return wrapper
    }

    const wrapper = mountSuspense(Comp)
    console.log(wrapper.html()) //=> <div>Async Setup</div>
  })

Some thoughts:

dobromir-hristov commented 4 years ago

I was thinking about such helpers too. It is super convenient to have them at your disposal, officially. I also made a similar one for testing stub scoped and named slots.

Maybe we can add all of these to the vtu-extended package we all have been mentioning here and there :D

lmiller1990 commented 4 years ago

We could just include them in core. If people will basically install the package every time, why not? Kind of like flush-promises - it's in basically every VTU project ever, it almost feels silly not to include it.

Can you share your helpers here?

dobromir-hristov commented 4 years ago

https://github.com/vuejs/vue-test-utils-next/compare/dhristov/renderSlots-config...feature/stub-slot-utility

lmiller1990 commented 4 years ago

Should work for named slots too, right?

Would this help with https://github.com/vuejs/vue-test-utils-next/issues/2

I am starting to link the idea of including helpers - if we are going to mention them in the guide, we may as well.

dobromir-hristov commented 4 years ago

This works for any type of slot, but its only to generate a minimal stub, that renders and provides just enough for you to test your slot content.

It will not help with testing slots properly on the mounted component. That is what I will be doing next day or so. I have started a branch locally with cases :)

I agree, we will have some edge cases that may need some some helpers :)

lmiller1990 commented 4 years ago

Great, sounds good. I will work on my suspense helper a bit too.

dobromir-hristov commented 4 years ago

I wonder, how are we going to test suspense content? Finding things inside Suspense, while its loading/errored? I could not make findComponent work, I did not test if find managed to find something.

lmiller1990 commented 4 years ago

Seems to work with find. No reason why it wouldn't - you just need flush-promises to make the async setup resolve. Is this what you mean?

That's what I am using the tests/features directory for - just testing various "real world" scenarios that are not specific to any method in VTU. I think we can add Vuetify etc here, too.

dobromir-hristov commented 4 years ago

So while writing the ScopesSlots PR I realized this is a pretty nifty way of testing scoped slot params. Deff one I would have loved to have available, a while back.

I think its a great addition to the utilities.

      const assertScopedSlotData = () => {
        let assertion = ref(null)
        const slot = (params) => {
          assertion.value = params
          return ''
        }
        return [assertion, slot]
      }

      const [params, slot] = assertScopedSlotData()

      const wrapper = mount(ComponentWithSlots, {
        slots: {
          scoped: slot,
        }
      })

      expect(params.value).toEqual({
        boolean: true,
        string: 'string',
        object: { foo: 'foo' }
      })
lmiller1990 commented 4 years ago

Interesting!

The more I think about it, the more I like the idea of including these utilities. Let's do it.

lmiller1990 commented 4 years ago

Should assertScopedSlotData return an object you destructure? Either way is fine - just an idea.

Also, how does assertScopedSlotData get a reference to the current component/wrapper? Do you use it like wrapper.assertScopedSlotData?

dobromir-hristov commented 4 years ago

So how I invisioned using it was as just an external function, that you use as I have shown in the example:

const [namedParams, named] = assertScopedSlotData()
const [defaultParams, default] = assertScopedSlotData()

const wrapper = mount(Comp, { slots: { named, default } })

expect(namedParams).toEqual({})
expect(defaultParams).toEqual({})

This way I think its much easier to name the exported objects, then to destructure and object and re-assign names. But then again, in most cases you test only one slot at a time.... But overall I did not intend this to be in the wrapper as it would have to attach special hidden properties to the instance, which we dont want :) and augment the slot, which I did not want :)

lmiller1990 commented 4 years ago

Oh, I see. I was confused by returning return [assertion, slot] but then renaming the first variable to params when you call the function.

This is very cool - what an interesting way to use Vue's reactivity. I am starting to see this decoupled reactivity is actually a huge improvement to Vue overall.

Don't have a strong opinion for/against including this. I personally would still make my assertions against the DOM as opposed the the props the slot receives... this still seems like a fancy way to test an implementation detail (what props are passed) instead of a behavior (what the component actually does with the props).

That said, I think you encounter much more complex uses of slots in your day to day usage - would this make an improvement to the code-bases you work on? Do you think other people would also find this useful? If you think so, I don't have a strong opinion against including this - people who want to use it can, people who don't need not.

dobromir-hristov commented 4 years ago

Lets decide where/how do we add these extra bits? Do we make a vtu-extended package, that exports helpers and extra VTU convenience methods, like getByTestId(), name() or that overview method, that was added recently? Or do we keep helpers to VTU and leave community to make their own extra plugins?

lmiller1990 commented 4 years ago

I think anything that can be done via a plugin is fine to leave out. These helpers (slots, suspense) can be in core, imo - doesn't hurt.

Did you want to make a branch with your slot helpers? Happy to review and push my suspense helper (can't find it, might just remake it it :) )

afontcu commented 4 years ago

Lets decide where/how do we add these extra bits? Do we make a vtu-extended package, that exports helpers and extra VTU convenience methods, like getByTestId(), name() or that overview method, that was added recently?

Could they be implemented as plugins? If so, we could have some "official" plugins, meaning plugins that we've developed or that we consider valuable, so they earn the "official" umbrella.

That said, I think you encounter much more complex uses of slots in your day to day usage - would this make an improvement to the code-bases you work on? Do you think other people would also find this useful?

These are important questions – I'd stick to testing against the DOM so I can't really say anything meaningful here. Maybe we could check against other people that manage tests on large codebases?

If you think so, I don't have a strong opinion against including this - people who want to use it can, people who don't need not.

Agree. Yet we need to be careful – we know people might not need this, but newcomers always have a hard time trying to grasp what they need and what they can ignore for a while. I think I'm just saying that we need to be extra careful when adding new stuff to the API, and make sure docs are in sync 👍

lmiller1990 commented 4 years ago

The main one I'm thinking about is a Suspense helper.

Happy to let this sit and add things as people ask for them.

TheJaredWilcurt commented 4 years ago

Can we get an official helper function to stub out a component, while still showing its slot content.

For example, I have a component like this:

<div class="parent-component">
  <BaseTable :data="data">
    <template v-if="thing">
      <label for="thing">Thing</label>
      <input type="checkbox" id="thing">
    </template>
  </BaseTable>
</div>

All I care about is testing the logic being passed into the slot. Currently if I did a snapshot of the component 99% of it would be cruft that I don't care about:

<div class="parent-component">
  <div class="base-table-wrapper">
    <!-- ...80 lines of code... -->
    <div class="custom-table-filters-slot">
      <label for="thing">Thing</label>
      <input type="checkbox" id="thing">
    </div>
    <!-- ...3000 lines of code, mostly from a 3rd party table component displaying data... -->
  </div>
</div>

But if we stubbed out the child component, while still rendering its slots, we'd get a much better snapshot. Something closer to this:

<div class="parent-component">
  <div stub="base-table">
    <div class="custom-table-filters-slot">
      <label for="thing">Thing</label>
      <input type="checkbox" id="thing">
    </div>
  </div>
</div>

This is a huge improvement, I shouldn't have to load 3000 lines of code in a giant DOM tree just to test a checkbox.

See: #69 for how this is done in VTU 1 + Vue 2.

lmiller1990 commented 4 years ago

I think we have have something like this @TheJaredWilcurt (docs for VTU Next are still a WIP...) but as a (global) config, we could reuse that logic.

https://github.com/vuejs/vue-test-utils-next/blob/5248066289c188091881d7ca32554692cee7471a/src/config.ts#L11

If this was a mounting option, would that solve your problem? This feels like something you would want on a global config, not test by test (correct me if I am wrong, I don't use shallow or snapshots often).

For example:

shallowMount(Foo, {
  renderDefaultSlot: true
})
TheJaredWilcurt commented 4 years ago

Having both would be good (a global defaulting to true that can be overridden on a per-test basis). But if I could only choose one I'd want the per-test control.

lmiller1990 commented 4 years ago

Yes, I agree, we should add this as a global config too.

lmiller1990 commented 4 years ago

This (renderStubDefaultSlot mounting option) will go out with #212.

As for helpers, I think we can revisit them when/if there is more demand. So far no-one issues really - we will document how to test things like <Suspense> etc. If there is more demand, we can consider adding them. Most are trivial to implement anyway. For now I will close this issue.

Fergmux commented 4 years ago

Hey @lmiller1990 the renderStubDefaultSlot option is a great idea and would help me out a lot! When do you think it might make it into a release?

afontcu commented 4 years ago

Hey @lmiller1990 the renderStubDefaultSlot option is a great idea and would help me out a lot! When do you think it might make it into a release?

it's already there :) https://github.com/vuejs/vue-test-utils-next/pull/102

Fergmux commented 4 years ago

@afontcu Brilliant, thanks!

rikbrowning commented 3 years ago

Is there a plan to extend the render to named slots rather than just default? I have a similarly scenario to @TheJaredWilcurt were I use named slots and the default slot to compose a component together. Whilst doing a full render would solve this I really only want to focus on the component functionality at this level. Assuming other unit tests cover other components.

sand4rt commented 3 years ago

What about checking if the setup function is async like (not sure if there is a better way without calling the function first): Component.setup.constructor.name === 'AsyncFunction'

And then return something like the mountSuspense promise @lmiller1990 posted earlier so that the mount function can be optionally awaited when the component has a an async setup?

It seems to me that a lot of people will run into this. As far as i know there is no off-the-shelf way to test this right now while this a common task.

lmiller1990 commented 3 years ago

@rikbrowning Might be worth making a new issue if you have a feature proposal. Not 100% clear on what you want, but I think I understand and I think the reason we don't have that is a technical blocker (could be wrong, would need to see a more fleshed out example of your proposal).

@sand4rt I think including some helpers is a good idea, if you want to make an issue with a proposal we could go over it. If there's no technical blocker/edge cases, we could add it. What I've generally been doing is just something like this which seems okay. What do you think?

rikbrowning commented 3 years ago

@lmiller1990 I put together the feature I am talking about in this commit https://github.com/rikbrowning/vue-test-utils-next/commit/d9309d48264031ec83a9211c8baced8f5e87a088 The feature would also fix https://github.com/vuejs/vue-test-utils-next/issues/773

alejandroclaro commented 2 years ago

My two cents goes towards including a helper function in this library. I've been struggling to get this to work with components that need props and react to changes to those props.

I think I finally have a solution, but I'm still not sure if it's the right way. You probably know better.

This is my current solution in case it could help someone:

/**
 * Creates a Wrapper that contains the mounted and rendered Vue component.
 *
 * @param component The asynchronous component to mount using suspense.
 * @param options   The mount options.
 *
 * @returns The mounted component wrapper.
 */
async function mountWithSuspense<Component extends ComponentPublicInstance, Props>(
  component: new () => Component,
  options: MountingOptions<Props>
): Promise<VueWrapper<ComponentPublicInstance>> {
  const wrapper = defineComponent({
    'components': { [component.name]: component },
    'props': Object.keys(options.props ?? {}),
    'template': `<suspense><${component.name} v-bind="$props" /></suspense>`
  });

  const result = mount(wrapper, options);

  await flushPromises();

  return result;
}
lmiller1990 commented 2 years ago

Neat helper. Personally I like to keep the scope of this library small, and encourage building helpers/integrations, mainly to keep maintenance easy. No doubt this helper is great for you, but someone else might need to tweak it - then we end up with a helper with many different options.

We could have. a page in the docs with some snippets/helpers - kind of like recipes - what do you think?

Alternatively, if you have a blog, you could write about how it works and we could backlink to you from the docs - good traffic/publicity for your content.

alejandroclaro commented 2 years ago

I think it's worth considering.

<suspense> is very intrusive. Once a component is asynchronous, the rest of that component's clients must react to this in some way. Most of the time, to me, it seems like the best policy is NOT to 'suspense' until you really need to. That has been long chains of components.

We have a lot of packages that depend on each other, and trying to encapsulate this in some sort of package hasn't been easy. For example, recently, it had a problem with the config object being different between packages due to bundle issues and version differences.

This makes me think that it's actually a variation of mount, like shallowMount for when you're aware of the need for suspense.

It would be even better if this could be autodetected by mount.

jeffpohlmeyer commented 5 months ago

@alejandroclaro first things first, thank you for your contribution here; it's been extraordinarily helpful. That said, I'm running into a problem trying to access elements of the component itself after it has mounted. With a synchronous component I can simply call wrapper.vm.<methodName> or wrapper.vm.<dataElement> to get access to relevant functionality in the component, but using this method it doesn't work.

I've provided a minimal example here: https://stackblitz.com/~/github.com/jeffpohlmeyer/vue-testing-async where I try to call the helloWorld method on the component but it doesn't recognize it as a function. You can see the error if you open a new terminal and just run vitest.

ckuetbach commented 4 months ago

Can this issue be reopened?

I tried to find a solution for testing a simple Component with async setup code and was unable to find a solution. In my opinion async/await should be easy to use.

Bo-Yang-PDL commented 3 weeks ago

@alejandroclaro first things first, thank you for your contribution here; it's been extraordinarily helpful. That said, I'm running into a problem trying to access elements of the component itself after it has mounted. With a synchronous component I can simply call wrapper.vm.<methodName> or wrapper.vm.<dataElement> to get access to relevant functionality in the component, but using this method it doesn't work.

I've provided a minimal example here: https://stackblitz.com/~/github.com/jeffpohlmeyer/vue-testing-async where I try to call the helloWorld method on the component but it doesn't recognize it as a function. You can see the error if you open a new terminal and just run vitest.

I have found a workaround to access wrapper.vm, hope it helps

wrapper.getComponent(<component>).vm.<dataElement>
BegeMode commented 3 weeks ago

@Bo-Yang-PDL try to do it like this:

async function mountWithSuspense<Component extends ComponentPublicInstance, Props>(
    component: new () => Component,
    options: MountingOptions<Props>
): Promise<VueWrapper<ComponentPublicInstance>> {
    const wrapper = defineComponent({
        components: { [component.name]: component },
        props: Object.keys(options.props ?? {}),
        template: `<suspense><${component.name} v-bind="$props" /></suspense>`
    })

    const result = mount(wrapper, options)

    await flushPromises()

    const childWrapper = result.findComponent(component)
    return childWrapper
}