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

Helpers: Suspense (and others?) #108

Closed
lmiller1990 opened this issue May 4, 2020 · 36 comments
Closed

Helpers: Suspense (and others?) #108

lmiller1990 opened this issue May 4, 2020 · 36 comments

Comments

@lmiller1990
Copy link
Member

#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:

  • we call flushPromises for the user. Any possible side-effects/unexpected behavior?
  • does this belong in this library? Or should we mention in the docs and let users write their own?
  • this sets a precedent. One we have one helper, it may be expected others are included. This is not a bad thing, just something to keep in mind.
  • do we also then need a shallowMountSuspense? Someone will almost certainly ask for this.
    • if we drop shallowMount as a stand-alone function (which I think we should) and instead have a shallow: true mounting option, this would not be a problem.
@dobromir-hristov
Copy link
Contributor

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
Copy link
Member Author

lmiller1990 commented May 5, 2020

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
Copy link
Contributor

@lmiller1990
Copy link
Member Author

Should work for named slots too, right?

Would this help with #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.

  • suspense
  • slots
  • anything else come to mind?

@dobromir-hristov
Copy link
Contributor

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
Copy link
Member Author

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

@dobromir-hristov
Copy link
Contributor

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
Copy link
Member Author

lmiller1990 commented May 6, 2020

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
Copy link
Contributor

dobromir-hristov commented May 9, 2020

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
Copy link
Member Author

Interesting!

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

@lmiller1990
Copy link
Member Author

lmiller1990 commented May 10, 2020

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
Copy link
Contributor

dobromir-hristov commented May 10, 2020

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
Copy link
Member Author

lmiller1990 commented May 10, 2020

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
Copy link
Contributor

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
Copy link
Member Author

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
Copy link
Member

afontcu commented Jun 8, 2020

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
Copy link
Member Author

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
Copy link
Contributor

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
Copy link
Member Author

lmiller1990 commented Jul 8, 2020

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
Copy link
Contributor

TheJaredWilcurt commented Jul 17, 2020

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
Copy link
Member Author

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

@lmiller1990
Copy link
Member Author

lmiller1990 commented Sep 25, 2020

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
Copy link

Fergmux commented Oct 1, 2020

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
Copy link
Member

afontcu commented Oct 1, 2020

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 :) #102

@Fergmux
Copy link

Fergmux commented Oct 1, 2020

@afontcu Brilliant, thanks!

@rikbrowning
Copy link

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
Copy link

sand4rt commented Jul 23, 2021

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
Copy link
Member Author

@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
Copy link

@lmiller1990 I put together the feature I am talking about in this commit rikbrowning@d9309d4
The feature would also fix #773

@alejandroclaro
Copy link

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
Copy link
Member Author

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
Copy link

alejandroclaro commented Jun 27, 2022

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
Copy link

@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
Copy link

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
Copy link

@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
Copy link

@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
}

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

No branches or pull requests