-
-
Notifications
You must be signed in to change notification settings - Fork 481
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
flushPromises
from vue-test-utils
must be called multiple times
#1163
Comments
flushPromises
from vue-test-utils
must be called multiple times to wait for component render
Hey, @vincerubinetti. Thanks for converting this into an issue. Could you please remove the use of Please, if somebody has time to investigate this I'd be thankful. I don't currently have the capacity to look into this. |
flushPromises
from vue-test-utils
must be called multiple times to wait for component renderflushPromises
from vue-test-utils
must be called multiple times
However I've removed it anyway by simply I wanted to keep it in as proof that other solutions worked as expected. Now if you want that you will have to go back in the commit history. |
@vincerubinetti Couldn't recreate your issue from your repo. It wasn't working even with two Here is the complete code that works for me: import { rest } from "msw";
import { setupServer } from "msw/node";
import App from "@/App.vue";
import {render, screen, waitFor} from '@testing-library/vue'
import '@testing-library/jest-dom'
const handler = rest.get("https://reqres.in/api/users", (req, res, ctx) =>
res(ctx.status(200), ctx.json({ foo: "bar" }))
);
const server = setupServer(handler);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test("Double flushPromises", async () => {
render(App);
await waitFor(() => {
expect(screen.getByText('success')).toBeInTheDocument()
})
}); |
What OS are you on? I've tested this on MacOS Monterey and Windows 10 (and Linux on GitHub Actions). Also I'm using Node v16.
I could probably write my own |
It's worth noting that it's unlikely that the issue can be solved by increasing the number of times one calls It does look like the issue is in constructing the proper test. Specifically, awaiting asynchronous elements in the test. As I suggest you look into how |
I appreciate that you're very busy and have limited time to look into this, but this has already be discussed at length and made clear in the original discussion. You yourself already looked at the implementation of
This insinuates this issue is user error, or a problem with The issue is some peculiarity with timing/promises/something async in I don't think this is super urgent -- it seems like a rare edge case -- but I've already done as much as I can. I've done my due diligence, and it's really up to someone in the I tried look at what I think is the relevant code, but I just don't have enough knowledge of the code base or node servers, and I've already spent too much "company time" on a small, hard-to-debug edge-case in one 3rd party library of many. |
I tested it on Node v16 and MacOS Monterey. I did notice that the test("Single flushPromises", async () => {
const wrapper = mount(App);
await flushPromises();
await new Promise(res => setTimeout(res, 0))
expect(wrapper.text()).toMatch("success");
});
test("Double flushPromises", async () => {
const wrapper = mount(App);
await flushPromises();
await new Promise(res => setTimeout(res, 0))
expect(wrapper.text()).toMatch("success");
}); The other method was to use setTimeout in a slightly different manner: test("Double flushPromises", async (done) => {
const wrapper = mount(App);
setTimeout(async () => {
expect(wrapper.text()).toMatch("success");
done()
}, 50)
}); Both approach are essentially allowing the unresolved promises to run the .then and .finally handlers. I hope that helps in someway! |
Okay... so... you were able to reproduce? But only on MacOS I guess? You said before both failed. What OS was that?
Again... these are both equivalent. This is the same as calling
This is quite hacky. An arbitrary timeout will be fragile/inconsistent, and we shouldn't have to rely on it. Nonetheless, I have also done something like this in my case: export const flush = async () => {
await flushPromises(); // per vue-test-utils
await flushPromises(); // per msw issue #1163
await new Promise(resolve => window.setTimeout(resolve, 100)); // for "good measure"/safety
}; But I don't like this. |
I probably made some other error when running the tests. Once I recreated the test again from the repo without any changes, I managed to recreate it on my MacOS. The solutions are indeed a bit hacky. I tried the flush-promises package and that still has the same issue. So it may well be a bug in |
Apologies, I cannot keep track of the number of issues I'm looking into, investigating, and solving every day. Things will inevitably repeat, and I will inevitably ask repetitive questions. If that's indeed what Just as I suspected, including multiple But why does it work without MSW?I assume when you say "without MSW" you imply you're using another API mocking library. In either way, it likely works as a false positive. MSW executes much more of the request client logic than an average API mocking library. The benefit of that is more reliable tests. Another benefit is that one correct decision forces you to make correct decisions everywhere across your stack. This seems like the case. Because MSW constructs and performs requests similar to your production app, it may include actual asynchronous code (instead of common mocked requests that resolve immediately)—the code you must properly await in your tests. What we are discussing at length here is the way to correctly write test assertions for asynchronous code. This is a general practice and it is outside of this library's scope. If you happen to find an issue in MSW, please don't hesitate to post it in this thread, I'll gladly look into it. Until then, the issue is closed. |
To give insights as to why I find Proper awaiting in tests is often focused around assertions ( test('fetches user' async () => {
render(<VueComponent />)
await waitFor(() => {
expect(ui.getByText('Hello world')).toBeVisible()
})
}) I understand that different frameworks come with different sets of ecosystem tools and general recommendations in regards to various things. Constructing tests may be one of those things. Yet awaiting asynchronous operations is a general pattern that lies outside of any particular framework, and it is not achieved by skipping an event loop iteration. |
For a race condition, it has been extraordinarily consistent, across multiple OS's, new/old devices, and 100+ runs. I asked the
Again yes,
Ultimately it seems to come down to this. I'm begging for I can see that you disagree with |
Yes, I think that's the case. I've answered you at length in the linked issue but let me summarize briefly: I'm not against adding mentions to the documentation. But the mention here is not the solution. So instead of looking for a solution, you're just rushing me into documenting workarounds, which will confuse even more people. I don't see anything wrong with MSW constructing a Promise around the response resolution logic. If anything, that is a natural thing to do to emphasize the asynchronous nature of the response. But what I do see is that Incompatibility with other tools is not a reason to conclude there's an issue. Somehow that extra Promise has no effect on all requests clients and many testing frameworks I've tried. I assume |
I disagree. Your library is not implemented in the same way as other similar libraries, as that other thread pointed out. Consistency with developer expectations is something that should be considered, and executed when possible. I think most devs would expect there to be no promise wrapper if the delay is 0. And yes, I agree, But still, I don't see that you lose barely anything by just conforming to other mocking libraries and not promise-ifying when delay is 0. But I concede that may unexpectedly break things for other people using msw. Regardless, thanks for taking all the time to respond, though I wish you were more receptive to suggestion as well. I suspect I still wont change your mind though, and it's your library so it's fine. The vue people will save other people's headaches with their doc updates. Edit: Revisiting this a year later. I think I pulled my punches too much. I'm shocked and dismayed by the attitude and arguments by the maintainer.
Very much disagree. But let's assume this is true. Then the corollary is that msw must not wrap something in a promise when there is no reason to.
This is wishy washy at best. The response is not asynchronous (unless the user is using Would you consider it acceptable if some math library randomly wrapped an This is still objectively a bug and the only valid reason to keep it here is to not break existing dependent code. If that's why you wont change it then fine. If it's something else, then in my view you are being a stereotypically inflexible maintainer.
It is not for you to impose your purism on how other people write tests, especially so forcefully and dismissively.
I have never used such forceful language in a GitHub issue before, and I do not like doing so, but I feel I have to. I feel that you are being forceful and not listening. I don't maintain any project this big, but my biggest has a few hundred stars though, and I would be ashamed if I acted this way in response to a user issue. |
Summary, if I may, for anyone stumbling upon this in the future:
Feel free to edit this comment if any of this is incorrect. |
Something to note here- If you use |
I've just written a new Vue + MSW integration example and I highly recommend anybody reading this to use import waitFor from 'wait-for-expect'
it('my test', async () => {
await waitFor(() => {
expect(someAsyncState).toBe(expectedResult)
})
}) |
any good solution for this problem? |
I wouldn't call them "good", but you can patch your node_modules with this fix or you can add two |
hihi, I came to here because of a scenario: Testing GraphQL Queries with React Hooks and MSWIn this scenario, I have a React hook that queries data using GraphQL and is mocked with MSW. When testing with the The Challenge:While using let's see an example: describe('given a `useTodosData` hook to query TODOs data, and tested with renderHook', {
let result: {
current: ITodo;
};
beforeEach(() => {
const utils = renderHook(() => useTodosData());
result = utils.result;
});
}); then I can use test('it should provide TODOs data from MSW', async () => {
await waitFor(() => expect(result.current.data).toEqual(DATA_FROM_MSW));
}); but when I want to add more tests based on data from MSW, one describe('when go to next tick', () => {
beforeEach(async () => {
await flushPromises();
});
test('then it should provide TODOs data from MSW, but not really', () => {
expect(result.current.data).toEqual(DATA_FROM_MSW);
});
// add more tests
}); The Solution:To address this, I introduced a const hookHasChanged = async <
T extends {
current: unknown;
},
>(
result: T,
timeLimit = 1000,
) => {
const now = Date.now();
const currentValue = result.current;
while (Date.now() - now < timeLimit) {
await flushPromises();
if (result.current !== currentValue) {
return true;
}
}
return false;
}; Usage Example:describe('when hook function has done', () => {
beforeEach(async () => {
await hookHasChanged(result, 500);
});
test('then it should provide TODOs data from MSW', () => {
expect(result.current.data).toEqual(DATA_FROM_MSW);
});
// Additional tests can now be added seamlessly.
}); Note:
|
please do not use the above code, later I come out a new version, added a type Options = {
field?: string;
timeLimit?: number;
};
export const hookHasChanged = async <
T extends {
current: any;
},
>(
result: T,
option?: Options,
) => {
const now = Date.now();
const currentValue = result.current;
const { field, timeLimit = 1000 } = option || {};
if (field && !(field in result.current)) {
throw new Error(`field [${field}] not found in result.current`);
}
while (Date.now() - now < timeLimit) {
await delay();
if (result.current !== currentValue) {
if (!field) {
return true;
}
if (!isEqual(result.current[field], currentValue[field])) {
return true;
} else {
continue;
}
}
}
return false;
}; example: beforeEach(async () => {
await hookHasChanged(result, { field: 'some-field-in-hook-return-value' });
}); |
Discussed in #988
Please read discussion first for full details.
Originally posted by vincerubinetti November 17, 2021
I have a Vue and vue-test-utils project that I'm trying to use MSW with. I have unit tests for components that call api functions when mounted. Vue exports a function called
flushPromises
, which is intended to be a simple function you can await to make sure the api calls have returned and the component has finished rendering before continuing with the test.The component that I'm testing looks like this:
For some reason, with MSW, I need two
flushPromises
calls for my tests to pass. The odd thing is, I was usingaxios-mock-adapter
before trying out MSW, and it was working with just one call. Can anyone think of any reason why MSW might be different?Another funny thing is that MSW is working in my e2e tests in Cypress (setupWorker), but the problem I describe above is happening in my unit tests in Jest (setupServer), which is setup like this:
Minimum reproducible example
Here is a barebones Vue CLI project with jest unit tests and cypress e2e tests set up:
https://github.com/vincerubinetti/msw-test (archived for posterity)
You can run the unit tests with
yarn test:unit
and the e2e (with gui) tests withyarn test:e2e
. This demonstrates the behavior thatmsw
needs two calls toflushPromises
whereasaxios-mock-adapter
only needs one.You can verify that
msw
andaxios-mock-adapter
are both correctly mocking the call by addingexpect(wrapper.text()).toMatch("foo"); expect(wrapper.text()).toMatch("bar");
.The text was updated successfully, but these errors were encountered: