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 3: Async component with suspense wrapper #230

Open
sand4rt opened this issue May 14, 2021 · 11 comments
Open

Vue 3: Async component with suspense wrapper #230

sand4rt opened this issue May 14, 2021 · 11 comments
Labels
bug Something isn't working

Comments

@sand4rt
Copy link
Contributor

sand4rt commented May 14, 2021

Struggling to test a async component. I created a suspense wrapper component with defineComponent which i think should work but it doesn't:

it("renders a async component with a suspense wrapper", () => {
  const Component = defineComponent({
    async setup() {
      return () => h("div", "AsyncSetup");
    },
  });

  const { getByText } = render(
    defineComponent({
      render() {
        return h(Suspense, null, {
          default: h(Component),
          fallback: h("div", "fallback"),
        });
      },
    })
  );

  expect(getByText("AsyncSetup")).toBeInTheDocument(); // fails
});

Also created a repository for reproducing the issue.

@sand4rt sand4rt added the bug Something isn't working label May 14, 2021
@sand4rt
Copy link
Contributor Author

sand4rt commented Jul 23, 2021

Related to: vuejs/test-utils#108

@sand4rt
Copy link
Contributor Author

sand4rt commented Aug 16, 2021

Has anyone gotten this to work yet? It seems that there is currently no way to test an asynchronous component with vue testing library.

@afontcu
Copy link
Member

afontcu commented Aug 18, 2021

Hi!

this is most likely an upstream issue in vue-test-utils-next (as seen in the issue you linked a few days ago)

@sand4rt
Copy link
Contributor Author

sand4rt commented Sep 25, 2021

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

@afontcu
Copy link
Member

afontcu commented Sep 27, 2021

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!

@sand4rt
Copy link
Contributor Author

sand4rt commented Sep 28, 2021

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.

@sand4rt
Copy link
Contributor Author

sand4rt commented Dec 2, 2021

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..

@afontcu afontcu mentioned this issue Jan 7, 2022
6 tasks
@Ragura
Copy link

Ragura commented Sep 18, 2022

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.

@kalvenschraut
Copy link

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

@dwin0
Copy link

dwin0 commented Apr 6, 2023

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()

@mutoe
Copy link

mutoe commented Sep 6, 2023

@dwin0 Works fine for me, thank you ♥️

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

No branches or pull requests

7 participants