pilotguy
pilotguy

Reputation: 687

How do I mock Audio API in Jest properly?

I'm writing a test that needs to simulate and provide accessor facilities for validating my Audio implementation is correct. I successfully implemented my setup method like so:

import "regenerator-runtime/runtime";
import { AudioState } from '../src';

export class MockAudio extends Audio {
  src: string;
  state: AudioState;
  duration: number;
  playing: boolean;
  constructor() {
    super();
    this.playing = false;
    this.duration = NaN;
    this.state = AudioState.STOPPED;
    this.src = '';
  }
  fastSeek = (time: number): Promise<void> => {
    this.currentTime = time;
    return Promise.resolve();
  }
  play = (): Promise<void> => {
    this.playing = true;
    this.state = AudioState.PLAYING;
    return super.play();
  }
  pause = (): void => {
    this.playing = false;
    this.state = AudioState.PAUSED;
    return super.pause();
  }
}

HTMLMediaElement.prototype.pause = () => Promise.resolve();
HTMLMediaElement.prototype.play = () => Promise.resolve();

window.Audio = MockAudio;

Object.defineProperty(window, 'MediaSource', {
  writable: true,
  value: jest.fn().mockImplementation((params) => ({
    // MediaSource implementation goes here
    addEventListener: jest.fn(),
  })),
});

As you can see I'm setting the methods up so that they can effect the object properties in a way that simulates the Audio API. I'm wondering how I can do this properly with mockImplementation and defineProperty?

Upvotes: 4

Views: 5272

Answers (3)

cgvalayev
cgvalayev

Reputation: 33

You are correct about play returning Promise. However, i still needed mocks in order to see if method was called. Crazy thing, this doesn't work:

export const mocks = {
  Audio: {
    pause: jest.fn(),
    play: jest.fn(() => Promise.resolve()),
  },
}

The code breaks saying that in file.play().catch(...) returns: TypeError: Cannot read property 'catch' of undefined

Somehow returned promise gets lost and play method always returns undefined. I tried all possible jest APIs and everything gives result. But this works play: () => Promise.resolve() but then i cannot test if it was called or not...

Upvotes: 0

Lo&#239;c Baron
Lo&#239;c Baron

Reputation: 68

It seems Audio.play() returns a Promise: HTMLMediaElement.play(): Promise<void>

see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play

This worked best for me using Typescript

global.Audio = jest.fn().mockImplementation(() => ({
  pause: jest.fn(),
  play: jest.fn(() => Promise.resolve()),
}));

Upvotes: 0

Robert Pazurek
Robert Pazurek

Reputation: 116

You can solve this problem (or mocking any other inbuild API classes) using what is described here: https://jestjs.io/docs/es6-class-mocks#manual-mock

What I like to do is to first define my mocks for later use in a test-utils.js like this:

export const mocks = {
  Audio: {
    pause: jest.fn(),
    play: jest.fn(),
  },
}

Now I mock the actual class in my jest.setup.js (as defined in the jest config):

import { mocks } from "./test-utils"

// Audio mock
global.Audio = jest.fn().mockImplementation(() => ({
  pause: mocks.Audio.pause,
  play: mocks.Audio.play,
}))

and finally I can expect against the mocks in my test:

import { mocks } from "./test-utils"

describe("Something", () => {
  it("should work", () => {
    // run your test here
    expect(mocks.Audio.pause).toHaveBeenCalled()
  })
})

of course remember to clean up the mocks as needed.

And the relevant part of the jest config in my package.json to trigger the setup file:

  "jest": {
    ...,
    "setupFilesAfterEnv": [
      "<rootDir>/jest.setup.js"
    ],
    ...
  },

Upvotes: 4

Related Questions