I have a function which extracts meta data like width and height from video element as following:
export async function getVideoMetadata(
videoBlobUrl: string,
videoElement: HTMLVideoElement,
): Promise<{ width: number; height: number }> {
// Trigger video load
return new Promise<{ width: number; height: number }>((resolve, reject) => {
videoElement.onloadedmetadata = () => {
videoElement.width = videoElement.videoWidth;
videoElement.height = videoElement.videoHeight;
videoElement.currentTime = videoElement.duration * 0.25;
};
videoElement.onseeked = () => {
resolve({ width: videoElement.videoWidth, height: videoElement.videoHeight });
};
videoElement.onerror = () => {
reject(`Error loading video`);
};
videoElement.src = videoBlobUrl;
});
}
On a running environment, the code runs perfectly and provides desired result with the correct height and width of video. I am using this function in another function to draw the video on canvas to generate thumbnail of the video on client side.
But when I am trying to write a test in jest for the same as the following function:
it('should return video metadata', async () => {
// Mock the videoBlobUrl
const videoBlobUrl = 'https://example.com/mock-video-url.mp4';
// Mock the HTMLVideoElement
const videoElementMock: HTMLVideoElement = {
onloadedmetadata: null,
onseeked: null,
onerror: null,
width: 0,
height: 0,
videoWidth: 640, // Mock video width
videoHeight: 480, // Mock video height
duration: 10, // Mock video duration
currentTime: 0,
src: '',
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
play: jest.fn(),
pause: jest.fn(),
load: jest.fn(),
};
// Spy on addEventListener to capture the event handlers
const addEventListenerSpy = jest.spyOn(videoElementMock, 'addEventListener');
// Call the function with the mocked data
const result = await getVideoMetadata(videoBlobUrl, videoElementMock);
// Assertions
expect(videoElementMock.src).toBe(videoBlobUrl);
expect(addEventListenerSpy).toHaveBeenCalledWith('loadedmetadata', expect.any(Function));
expect(addEventListenerSpy).toHaveBeenCalledWith('seeked', expect.any(Function));
// Trigger the loadedmetadata event
const loadedMetadataHandler = (addEventListenerSpy as jest.Mock).mock.calls.find(
(call) => call[0] === 'loadedmetadata',
)[1] as EventListener;
loadedMetadataHandler(new Event('loadedmetadata'));
// Trigger the seeked event
const seekedHandler = (addEventListenerSpy as jest.Mock).mock.calls.find(
(call) => call[0] === 'seeked',
)[1] as EventListener;
seekedHandler(new Event('seeked'));
// Validate the result
expect(result).toEqual({width:640, height:480});
});
The test case always fails throwing out as soon as I call getVideoMetadata
function in the test case without proceeding to the next line.
thrown: "Exceeded timeout of 5000 ms for a test.
Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."
I have tried increasing the timeout of the test but that did not help. As soon as the test code videoElement.src = videoBlobUrl;
reaches and the next step it fails throwing error.
You attached the event handlers to the video element by its properties .onloadedmetadata
, .onseeked
, and .onerror
. They are all anonymous functions rather than jestjs mock functions. So jest.spyOn(videoElementMock, 'addEventListener')
and expect(addEventListenerSpy).toHaveBeenCalledWith('loadedmetadata', expect.any(Function))
will not work.
Since you passed the videoElement
to getVideoMetadata
function, modify the video element's properties when the event happens, and you can get the properties(event handlers) of the video element in the tests. Then trigger them manually.
E.g.
getVideoMetadata.test.ts
:
import { expect, it } from '@jest/globals';
import { getVideoMetadata } from './getVideoMetadata';
it('should return video metadata', async () => {
const videoBlobUrl = 'https://example.com/mock-video-url.mp4';
const videoElementMock = {
width: 0,
height: 0,
videoWidth: 640,
videoHeight: 480,
duration: 10,
currentTime: 0,
src: '',
onloadedmetadata: () => {},
onseeked: () => {},
};
const promise = getVideoMetadata(videoBlobUrl, videoElementMock as unknown as HTMLVideoElement);
expect(videoElementMock.src).toBe(videoBlobUrl);
// Trigger the loadedmetadata event
videoElementMock.onloadedmetadata();
// Trigger the seeked event
videoElementMock.onseeked();
// Validate the result
const result = await promise;
expect(result).toEqual({ width: 640, height: 480 });
expect(videoElementMock.src).toBe(videoBlobUrl);
});
Test result:
PASS stackoverflow/77550452/getVideoMetadata.test.ts
✓ should return video metadata (1 ms)
---------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------|---------|----------|---------|---------|-------------------
All files | 90 | 100 | 80 | 90 |
getVideoMetadata.ts | 90 | 100 | 80 | 90 | 15
---------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.553 s