Open sand4rt opened 3 years ago
Hi!
this is most likely an upstream issue in vue-test-utils-next (as seen in the issue you linked a few days ago)
@afontcu After some debugging i found out that the wrapper on line 27 needs to be resolved first, e.g. with a await flushPromises()
like on line 32 before calling the getQueriesForElement(baseElement) on line 51
Here is a test that demonstrates this.
Are you sure this should be fixed in @vue/vue-test-utils?
Hi!
Thank you for taking the time to looking into this! Looks like you're right about the problem, too. Have you tried stubbing Suspense
and make it simply render #default
? It's far from ideal, but having an async render method is, too.
Thanks!
Thanks for your reply!
Could you elaborate on what you mean by stubbing Suspense
? A component with an async setup()
must be wrapped with Suspense
right? Otherwise the component will not resolve.
As workaround i'm using a modified version of the render
function called renderAsync
for now. The code can be viewed here.
After many problems with JSDOM/HappyDOM and issues like this i decided not to use VTL anymore. Playwright component testing has a almost identical API, is fast as well and runs in a real browser..
Any updates on this? We have embraced Suspense and async components very heavily in our project and this is really holding us back in writing tests. I've noticed it's the last part of making this library fully Vue 3 compatible.
After a lot of debugging and trial and error finally got something that works with the current code base and isn't async. If you do pass an async component to this function then you just have to do some findBy*
after the mount call. Alternative is making a separate async function that will flush promises as others have stated, I find one universal function more appealing. I just wrap the below in another async function when I test an async component.
Note below I left in where I setup quasar, pinia, and vue router mocks/stubs. Also the test runner I use is vitest
import { createTestingPinia } from '@pinia/testing';
import getQuasarOptions from '@rtvision/configs/quasar';
import { render } from '@testing-library/vue';
import { config as vueTestUtilsConfig, RouterLinkStub } from '@vue/test-utils';
import { Quasar } from 'quasar';
import { vi } from 'vitest';
import { defineComponent, reactive } from 'vue';
// Note need the wrapping div around suspense otherwise won't load correctly
// due to this line https://github.com/sand4rt/suspense-test/blob/master/tests/unit/renderAsync.js#L36
const SUSPENSE_TEST_TEMPLATE = `
<div id="TestRoot">
<suspense>
<async-component v-bind="$attrs" v-on="emitListeners">
<template v-for="(_, slot) of $slots" :key="slot" #[slot]="scope">
<slot key="" :name="slot" v-bind="scope" />
</template>
</async-component>
<template #fallback>
Suspense Fallback
</template>
</suspense>
</div>
`;
function getSuspenseWrapper(component) {
return defineComponent({
setup(_props, {
emit
}) {
const emitListeners = reactive({});
if ('emits' in component && Array.isArray(component.emits)) {
for (const emitName of component.emits) {
emitListeners[emitName] = (...args) => {
emit(emitName, ...args);
};
}
}
return {
emitListeners
};
},
emits: 'emits' in component && Array.isArray(component.emits) ? component.emits : [],
components: {
AsyncComponent: component
},
inheritAttrs: false,
template: SUSPENSE_TEST_TEMPLATE
});
}
/**
* @param initialState Used to set initial set of pinia stores
**/
export function mountComponent(
component,
{ props, initialState, slots } = {}
) {
return render(getSuspenseWrapper(component), {
props,
slots,
global: {
plugins: [
[Quasar, getQuasarOptions()],
createTestingPinia({
createSpy: vi.fn,
initialState
})
],
provide: {
...vueTestUtilsConfig.global.provide
},
components: {
AsyncComponent: component
},
stubs: {
icon: true,
RouterLink: RouterLinkStub
}
}
});
}
Example of usage with async component
// CommonTable.spec.ts
async function mountCommonTable(props, slots = {}) {
const tableWrapper = mountComponent(CommonTable, { props, slots });
await tableWrapper.findByRole('table');
return tableWrapper;
}
This will probably not cover all the cases, but by copying together code, this seems to work:
helper functions:
const scheduler = typeof setImmediate === 'function' ? setImmediate : setTimeout
export function flushPromises(): Promise<void> {
return new Promise((resolve) => {
scheduler(resolve, 0)
})
}
export function wrapInSuspense(
component: ReturnType<typeof defineComponent>,
{ props }: { props: object },
): ReturnType<typeof defineComponent> {
return defineComponent({
render() {
return h(
'div',
{ id: 'root' },
h(Suspense, null, {
default() {
return h(component, props)
},
fallback: h('div', 'fallback'),
}),
)
},
})
}
tests:
render(wrapInSuspense(MyAsyncComponent, { props: { } }))
await flushPromises()
expect(screen.getByText('text in component')).toBeVisible()
@dwin0 Works fine for me, thank you ♥️
Struggling to test a async component. I created a suspense wrapper component with
defineComponent
which i think should work but it doesn't:Also created a repository for reproducing the issue.