I'm trying to write a vitest
test around a composable that reads and writes postMessage()
for communication between iframes.
The Vue docs say that if the composable relies on lifecycle hooks to mount it in a test app. So I have added test-helper.js
import { createApp } from 'vue'
export function withSetup(composable) {
let result
const app = createApp({
setup() {
result = composable()
// suppress missing template warning
return () => {}
}
})
app.mount(document.createElement('div'))
// return the result and the app instance
// for testing provide/unmount
return [result, app]
}
The test creates a mock messageHandler
which should be executed when the event listener is triggered.
Then I call the postMessageFromIframe()
method in my composable and test, but I'm getting some unexpected behaviour.
import { usePostMessage } from '@/composables/usePostMessage.ts';
import { vi} from "vitest";
import { withSetup } from './test-helper'
import { nextTick } from 'vue';
describe('usePostMessage', () => {
it("should deliver a post message successfully", async () => {
//ARRANGE - create a mock
const expectedPayload = { action: 'testAction' };
const messageHandler = vi.fn().mockImplementation(event => {
// ASSERT within the event handler
console.log('I was run');
expect(event.data.from).toBe('usePostMessage');
expect(event.data.payload.action).toBe(expectedPayload.action);
});
//listener
window.addEventListener('message', messageHandler);
//ACT
//launch within app
const [result,app] = withSetup(() => {
const { postMessageFromIframe } = usePostMessage();
postMessageFromIframe({action:'testAction'});
return { postMessageFromIframe };
});
//ASSERT
// Wait for next tick to allow the event to be processed
await nextTick();
expect(messageHandler).toHaveBeenCalled();
// Cleanup
window.removeEventListener('message', messageHandler);
app.unmount();
});
});
If I comment out the last expect()
then the test runs fine but messageHandler
is never executed. However if I uncomment the last expect()
the tests fail but the messageHandler
is executed. It seems that the last expect()
is actually executing messageHandler
. I think I am misunderstanding something here...
So it seems that nextTick()
wasn't enough to wait for all the asynchronous tasks to finish.
I tried adding:
const [result,app] = withSetup(async() => {
const { postMessageFromIframe } = usePostMessage();
await postMessageFromIframe(expectedPayload);
return { postMessageFromIframe };
});
but still no luck.
Final answer was to create a Promise with a 0ms timeout:
it("should deliver a post message successfully", async () => {
//ARRANGE - create a mock
const expectedPayload = { action: 'testAction' };
const messageHandler = vi.fn();
window.addEventListener('message', messageHandler);
//ACT
//launch within app
const [result,app] = withSetup(() => {
const { postMessageFromIframe } = usePostMessage();
postMessageFromIframe(expectedPayload);
return { postMessageFromIframe };
});
// Wait for next tick to allow the event to be processed
await new Promise(resolve => setTimeout(resolve));
//ASSERT
expect(messageHandler).toHaveBeenCalled();
const event = messageHandler.mock.calls[0][0];
expect(event.data.from).toBe('usePostMessage');
expect(event.data.payload.action).toBe(expectedPayload.action);
// Cleanup
app.unmount();
window.removeEventListener('message', messageHandler);
});