vuejs3vue-test-utilsvitest

Can't get mock to execute in vitest


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


Solution

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