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

Recommendations for testing with vue-test-utils? #28

Closed
danielbachhuber opened this issue Nov 24, 2020 · 20 comments
Closed

Recommendations for testing with vue-test-utils? #28

danielbachhuber opened this issue Nov 24, 2020 · 20 comments

Comments

@danielbachhuber
Copy link
Contributor

Hi,

Do you have any recommendations for how to test for paginated data with vue-test-utils?

My typical test looks like this:

it('mounts', async () => {
    expect.assertions(1);
    mock.onGet('/api-uri')
        .reply(200, JSON.stringify([
            firstItem,
            secondItem,
        ]), {'x-total-count': 2});
    wrapper = mount(
        Component,
        {
            localVue,
            router,
            store,
            mocks: {
                $route,
            },
            stubs: ['router-link', 'router-view']
        },
    );
    await flushPromises();
    await wrapper.vm.$nextTick();
});

However, the computed createInstance() seems stuck in the .loading state, even after I've flushed the promises and waited for a re-render.

Any ideas on what I might be doing wrong?

Thanks!

@danielbachhuber
Copy link
Contributor Author

@MaxGfeller Any ideas here? Thanks!

@MaxGfeller
Copy link
Contributor

Hi @danielbachhuber, sorry for only getting back to you now.

That's a good question. Does the component actually get mounted? Because the Vue plugin does the wiring in the beforeMount hook.

And where do you set up the store?

@danielbachhuber
Copy link
Contributor Author

That's a good question. Does the component actually get mounted? Because the Vue plugin does the wiring in the beforeMount hook.

Yep, the component is mounted. I can see the other HTML rendered by the component, but not the the HTML that would be rendered if Vuex Pagination items were present.

And where do you set up the store?

In beforeEach:

beforeEach(() => {
    mock = new MockAdapter(window.axios);
    product = importJsonMock('models/Product/slack');
    store = new Vuex.Store({
        state: {
            product,
        },
        actions,
        mutations,
    });
});

Also, I've added an initializeResources() to the test, which calls createResource():

it('mounts', async () => {
    expect.assertions(1);
    mock.onGet('/api-uri')
        .reply(200, JSON.stringify([
            firstItem,
            secondItem,
        ]), {'x-total-count': 2});
    initializeResources();
    wrapper = mount(
        Component,
        {
            localVue,
            router,
            store,
            mocks: {
                $route,
            },
            stubs: ['router-link', 'router-view']
        },
    );
    await flushPromises();
    await wrapper.vm.$nextTick();
});

@danielbachhuber
Copy link
Contributor Author

Interestingly, I added some debugging inside of setInRegistry and can see the items being set.

I also added some debug to the component itself. When loading is set to false, the items exist on the object as expected:

export default {
    computed: {
        ...mapState({
            product: state => state.product,
        }),
        themes: createInstance('themes', {
            pageSize: 200,
            args() {
                return {
                    product: this.product,
                    search: this.filter.search,
                    status: this.filter.status,
                };
            }
        }),
        themesIsLoading() {
            return this.themes.loading;
        }
    },
    watch: {
        themesIsLoading(val) {
            console.log('themesIsLoading');
            console.log(val);
            console.log(this.themes.items);
        }
    }
}

So maybe the component isn't re-rendering when its computed state is changing?

@danielbachhuber
Copy link
Contributor Author

So maybe the component isn't re-rendering when its computed state is changing?

Yep, this must be it. Force updating causes the test to pass.

it('mounts', async () => {
    expect.assertions(1);
    mock.onGet('/api-uri')
        .reply(200, JSON.stringify([
            firstItem,
            secondItem,
        ]), {'x-total-count': 2});
    initializeResources();
    wrapper = mount(
        Component,
        {
            localVue,
            router,
            store,
            mocks: {
                $route,
            },
            stubs: ['router-link', 'router-view']
        },
    );
    await flushPromises();
    await wrapper.vm.$nextTick();
    await wrapper.vm.$forceUpdate();
});

@MaxGfeller
Copy link
Contributor

I remember vaguely that i once had a similar problem... 🤔 I think you can also access wrapper.vm.themes.loading directly in your test and it should be correct.

@danielbachhuber
Copy link
Contributor Author

Yep, this must be it. Force updating causes the test to pass.

Hm, force updating worked once and now it no longer works 😦

@MaxGfeller
Copy link
Contributor

That sounds like a race condition. Does it work with directly checking wrapper.vm.themes.loading?

@danielbachhuber
Copy link
Contributor Author

That sounds like a race condition.

Yeah, or some object mutation issue.

Does it work with directly checking wrapper.vm.themes.loading

Oh, good catch. wrapper.vm.themes.loading is still true immediately prior to the failed assertion.

I thought all of my await statements would be sufficient? Do you think there's something else I should try?

@MaxGfeller
Copy link
Contributor

You can try more await statements🙈 Sorry, not sure how many ticks are required.

@danielbachhuber
Copy link
Contributor Author

You can try more await statements🙈 Sorry, not sure how many ticks are required.

Hm, this doesn't seem to work either:

it('mounts', async () => {
    expect.assertions(1);
    mock.onGet('/api-uri')
        .reply(200, JSON.stringify([
            firstItem,
            secondItem,
        ]), {'x-total-count': 2});
    initializeResources();
    wrapper = mount(
        Component,
        {
            localVue,
            router,
            store,
            mocks: {
                $route,
            },
            stubs: ['router-link', 'router-view']
        },
    );
    await flushPromises();
    await new Promise(r => setTimeout(r, 1000));
    await wrapper.vm.$nextTick();
    await wrapper.vm.$nextTick();
    await wrapper.vm.$nextTick();
    await wrapper.vm.$nextTick();
    await wrapper.vm.$forceUpdate();
});

@danielbachhuber
Copy link
Contributor Author

@MaxGfeller Here's a repository that reproduces the issue: https://github.com/danielbachhuber/vuex-pagination-28

If you run npm install && npm tdd, you'll see output like this:

image

Notably, here's how the console.log() debug ends up:

image

@MaxGfeller
Copy link
Contributor

Thanks for the reproduction @danielbachhuber. I just had a look at it and got it to run.

image

Here's what i did:

  • There was a bug in the Component.vue, in the v-for it must be themes.items instead of themes
  • The flushPromises() and wrapper.vm.$forceUpdate() are not necessary
  • The magic number (for me at least) seems to be 7 await wrapper.vm.$nextTick() (i have no idea why it requires exactly that many ticks and you'd have to have an extensive look at how test-utils does the computation and rendering to find that out)
  • I realize this is not very clean but it could be replaced with a await new Promise(resolve => setTimeout(resolve, 50)), too

Hope this helps.

@MaxGfeller
Copy link
Contributor

Even better solution: update to 1.3.9 and in your test replace all the awaits with await wrapper.vm.$vuexPaginationInitialized. 🎉

Will document this in the README soon-ish.

@danielbachhuber
Copy link
Contributor Author

Thanks Max! I’ll check this out when I’m back at the computer in the next couple of days.

I thought I tried the setTimeout approach but maybe I missed something. My reproduction example was from scratch, so there might be a nuance of my main application that it missed. If it doesn’t fix, I’ll send you a short screencast of some further debugging.

Thanks again for your help!

@MaxGfeller
Copy link
Contributor

Sure thing! Problem was that the Vuex actions were not completed yet. That's why i added the $vuexPaginationInitialized property in the plugin: https://github.com/cyon/vuex-pagination/blob/master/src/index.js#L48

Please let me know if it works in your project, too.

@danielbachhuber
Copy link
Contributor Author

@MaxGfeller The plot thickens! I've found the real source of the problem I'm experiencing in my project though.

A component using Vuex pagination passes the assertions in the first it() test within describe() block. However, it will fail the same assertions in a second it() test in the same describe() block.

The reproduction repo is updated to document this behavior: https://github.com/danielbachhuber/vuex-pagination-28/commit/2e7d42ba4a70655d41bd5d07a9040e17acd3cc1d

I don't fully understand your store registration code is doing, but my guess is that initializeStore isn't getting reset:

if (this.$store && !initializedStore) initializeStore(this.$store)

@MaxGfeller
Copy link
Contributor

@danielbachhuber You are exactly right! In this case the store does not get reset.

I added a reset() function in 1.3.10, which needs to called in the afterEach() block:

https://github.com/cyon/vuex-pagination/blob/master/src/index.js#L115-L119

Hope that helps!

@danielbachhuber
Copy link
Contributor Author

Thanks @MaxGfeller ! reset() works great 🎉

@MaxGfeller
Copy link
Contributor

@danielbachhuber Awesome! I'm closing this issue then and provide documentation for those two small additions in a later version 👍🏻

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

No branches or pull requests

2 participants