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

vue: dynamic component imports are not resolved #1328

Closed
6 tasks done
cexbrayat opened this issue May 17, 2022 · 15 comments
Closed
6 tasks done

vue: dynamic component imports are not resolved #1328

cexbrayat opened this issue May 17, 2022 · 15 comments

Comments

@cexbrayat
Copy link
Contributor

Describe the bug

With Vue, you can define async components with a dynamic import like the following:

<template>
  <button @click="showHello = true">Show Hello</button>
  <div v-if="showHello">
    <div>Async component</div>
    <Hello :count="1"></Hello>
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';

const Hello = defineAsyncComponent(() => import('./Hello.vue'));
const showHello = ref(false);
</script>

Hello.vue is then loaded only when the user clicks on the button Show Hello

But a unit test checking this behavior fails with Vitest:

import { mount, flushPromises } from '@vue/test-utils';
import AsyncComp from '../components/AsyncComp.vue';

test('mount component', async () => {
  const wrapper = mount(AsyncComp);

  expect(wrapper.text()).toContain('Show Hello');

  await wrapper.get('button').trigger('click');
  expect(wrapper.vm.showHello).toBe(true);
  await flushPromises();
  // I suppose this should work but does not
  expect(wrapper.text()).toContain('1 x 2');
});

Maybe I'm missing something obvious in the test setup, but it looks like the promise is never resolved.

Reproduction

https://stackblitz.com/edit/vitest-dev-vitest-bmfn8b

System Info

System:
    OS: macOS 12.3.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 16.90 GB / 64.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 16.15.0 - ~/.volta/tools/image/node/16.15.0/bin/node
    Yarn: 1.22.17 - ~/.volta/tools/image/yarn/1.22.17/bin/yarn
    npm: 6.14.17 - ~/.volta/tools/image/npm/6.14.17/bin/npm
  Browsers:
    Chrome: 101.0.4951.64
    Firefox: 100.0
    Safari: 15.4
  npmPackages:
    @vitejs/plugin-vue: 2.3.3 => 2.3.3
    vite: 2.9.9 => 2.9.9
    vitest: 0.12.6 => 0.12.6

Used Package Manager

yarn

Validations

@sheremet-va
Copy link
Member

If I add this:

  return new Promise((resolve) => {
    setInterval(() => {
      console.log(wrapper.html());
      if (wrapper.text().includes('1 x 2')) {
        resolve();
      }
    });
  });

Promise is resolved on the second tick 🤔

@tinobino
Copy link

I experienced the same issue. Asked a question 2 days ago here: #1322

There you can find a link to a codesandbox example.

@sheremet-va
Copy link
Member

sheremet-va commented May 21, 2022

I am not sure this is how it should work in Vitest. In Jest this worked because the code was essentially:

const Hello = defineAsyncComponent(() => Promise.resolve(require('./path')))

Meaning, since it's synchronous we can just queue a microtask and wait for it to end, but in Vitest this is transformed to this:

const Hello = defineAsyncComponent(() => __vite_import__('./path'))
// __vite_import__ is asynchronous

And so, since __vite_import__ is asynchronous, we cannot just queue a microtask, and we need to actually wait for it to load (when it happens is not predictable like with synchronous code). In my tests, if I use async component with Vitest, I use findBy* from Vue Testing Library, although I've seen in CI it can take more that 1000ms to load, which is kind of weird.

I do not know, if it can be fixed on Vitest side (maybe, we can provide utility like vi.waitForModulesToLoad or something like that). Maybe, if we had some kind of nextTick implementation in Vue, but for loading async component, you could use that.

@sheremet-va
Copy link
Member

sheremet-va commented May 21, 2022

I found a way to load it, using Vitest internals:

  await flushPromises() // this is needed for vue to start importing
  const worker = globalThis.__vitest_worker__
  const promises = [...worker.moduleCache.values()]
    .filter(m => !m.exports && m.promise)
    .map(m => m.promise)
  await Promise.all(promises) // this is needed for us to wait until module is loaded
  await flushPromises() // this is needed for vue to rerender component

To break it down,

  1. We clicked "Show Hello"
  2. Vue initiated loading, but haven't called loader yet
  3. We are waiting until the end of the even loop, because Vitest haven't started importing yet
  4. When Vitest imports modules it puts it inside moduleCache, so we can use it to guarantee that module is loaded
  5. We need to wait until the end of the event loop for Vue to rerender component

I found a bug that Vitest stores some modules twice, but it is out of scope of this issue. I am not sure what else I can do to help you, @cexbrayat

@cexbrayat
Copy link
Contributor Author

@sheremet-va Thanks for the workaround, that works 👍

Do you think this internal machinery could be exposed on a utility function by Vitest (like vi.runAllTicks())?

@sheremet-va
Copy link
Member

I think we can expose waiting for modules to load, but I don't see how flushes fit there, so you would need to also make a wrapper in test-utils.

@cexbrayat
Copy link
Contributor Author

Yeah, I think we can either offer a wrapper in VTU, or at least document how to build it once a utility is available 👍

@cexbrayat
Copy link
Contributor Author

Now that dynamicImportSettled is released, we can close this issue. Thanks @sheremet-va !

@binarious
Copy link

@cexbrayat Should dynamicImportSettled also work for Svelte components? As this doesn't seem to be the case for me. Or should I open a new issue?

@cexbrayat
Copy link
Contributor Author

@binarious I actually don't know sorry: I'm part of the Vue team but not of the Svelte one, so I don't know what Svelte does in that case. You should probably open a dedicated issue 👍

@krystof-k
Copy link

I'm having troubles when trying to load nested async components (first async components loads another async component). When using just one, it works fine. See the example. Any idea what am I doing wrong or the dynamicImportSettled doesn't wait for the nested import?

@krystof-k
Copy link

OK, looks like the "solution" is to call the dynamicImportSettled twice. However, it could be probably handled somehow better. Any ideas @sheremet-va?

@sheremet-va
Copy link
Member

Any idea what am I doing wrong or the dynamicImportSettled doesn't wait for the nested import?

Yes, I don't think it works for nested imports. When it's called it looks if there are any modules that are not loaded and waits until it's loaded. But it doesn't check if there are any not loaded modules after the first call. We should probably do this, PR welcome.

@krystof-k
Copy link

I'm afraid this is above my skill set. All I can do is to create a new issue or update the docs that it needs to be called multiple times when handling multiple imports.

@AckermannJan
Copy link

AckermannJan commented May 4, 2023

Hey, sadly for me the issue still exists. In our vue component we are importing another component from a file which is importing and exporting all existing components within this component-lib. I still have to call dynamicImportSettled multiple times and even then its flaky. When I call it like 5+ times then it starts working all the time.

What I found out so far is that this utility is not even recognising that this is a dynamic export that it should wait for.

This is using:
Vue2.7
Vitest 0.31.0
@vue/test-utils 1.3.5
flush-promises 1.0.2

// index.js from component lib
const component1 = () => import(component1.vue)
const component2 = () => import(component2.vue)
const component3 = () => import(component3.vue)

export {
  component1,
  component2,
  component3,
}
// vue component we are testing
<template>
        <Component1>hello world</Component1>
</template>

<script>
    import { component1 } from '@external/component-lib';

    export default {
        name: 'NewComponent',
        components: { component1 },
        ...
// spec
describe('NewComponent', async () => {
    let wrapper;

    beforeEach(async () => {
        wrapper = await mount(NewComponent);
        await flushPromises;
        await vi.dynamicImportSettled();
    });

    it('test', async () => {
        console.log(wrapper.html());
    });
});

@github-actions github-actions bot locked and limited conversation to collaborators Jun 3, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants