Can't mock 'webextension-polyfill' for jest unit tests
Asked Answered
S

1

6

I am writing a browser extension for Firefox and Chrome. I use the browser.* commands and Mozilla's 'webextension-polyfill' module to make browser work in chrome. I have a file called browser.ts that contains a single line:

export const browser = require('webextension-polyfill');

which then gets used in each file like so

import {browser} from '../path/to/browser.ts'.

I want to make it so I can write unit tests, but the line require('webextension-polyfill') is causing me a headache as any test with browser throws This script should only be loaded in a browser extension and references the previous require statement.

I have tried using rewire, proxyquire, and jest mocks to avoid the require statement from being called in the unit tests, but I am not able to override it successfully. The only way I have been able to avoid this error is with a try-catch and returning a mock object on the exception, but that seems hacky and messy.

What is the best way to get around this and fix it? I see packages like mockzilla which look helpful for mocking, but I think I would use them after I can first use the require line for webextension-polyfill.

Saltation answered 22/9, 2022 at 3:25 Comment(0)
N
4

Unfortunately you will have to create the entire mock class. Or at least a class with the methods you use. I'll now explain what you can do with jest to solve this issue

1. Mock Class

Let's say you have a file browser.ts (you can use javascript) under __mocks__ folder containing

import { jest } from "@jest/globals";

const mockBrowser = {
    // Define mock implementations for browser APIs you use
    storage: {
        local: {
            get: jest.fn(),
            set: jest.fn(),
        },
    },
    // Add more mock APIs as needed
};

export default mockBrowser;

2. Update jest config

On the jest config jest.config.js, you need to add the moduleNameWrapper. For instance I have this

/** @type {import('ts-jest').JestConfigWithTsJest} */

const path = require('path');

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/extension/src/**/*.test.ts'],
  "moduleNameMapper": {
    "^webextension-polyfill$": path.join(process.cwd(), "src", "__mocks__", "browser.ts"),
    // Add other mocks if needed
  }
};

3. Test file

You can now create a class with the mock calls. For this example, I will directly execute the browser operations. But this could be called on the tested functions

import { describe, expect, test } from "@jest/globals";
import mockBrowser from "../__mocks__/browser";
test("Browser Mock", async () => {
    const getStorage = async (data: null | string | string[] | Record<string, any>): Promise<Record<string, any>> => {
        return { key: data };
    };

    mockBrowser.storage.local.get.mockImplementation(getStorage);

    const testMock = await mockBrowser.storage.local.get("dataX");
    expect(testMock).toEqual({ key: "dataX" });
});

As you may see I'm specifying the types of the get operation. But that's not needed unless you use typescript. On this example you can also see how the parameter can be used for the output.

I hope you find this answer helpful.

Normandnormandy answered 27/10, 2023 at 13:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.