After putting off testing for a while now due to Cypress not allowing visiting chrome://
urls, I decided to finally understand how to unit/integration test my extension – TabMerger. This comes after the many times that I had to manually test the ever growing functionality and in some cases forgot to check a thing or two. Having automated testing will certainly speed up the process and help me be more at peace when adding new functionality.
To do this, I chose Jest since my extension was made with React (CRA). I also used React Testing Library (@testing-library/react
) to render all React components for testing.
As I recently made TabMerger open source, the full testing script can be found here
Here is the test case that I want to focus on for this question:
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import * as TabFunc from "../src/Tab/Tab_functions";
import Tab from "../src/Tab/Tab";
var init_groups = {
"group-0": {
color: "#d6ffe0",
created: "11/12/2020 @ 22:13:24",
tabs: [
{
title:
"Stack Overflow - Where Developers Learn, Share, & Build Careersaaaaaaaaaaaaaaaaaaaaaa",
url: "https://stackoverflow.com/",
},
{
title: "lichess.org • Free Online Chess",
url: "https://lichess.org/",
},
{
title: "Chess.com - Play Chess Online - Free Games",
url: "https://www.chess.com/",
},
],
title: "Chess",
},
"group-1": {
color: "#c7eeff",
created: "11/12/2020 @ 22:15:11",
tabs: [
{
title: "Twitch",
url: "https://www.twitch.tv/",
},
{
title: "reddit: the front page of the internet",
url: "https://www.reddit.com/",
},
],
title: "Social",
},
};
describe("removeTab", () => {
it("correctly adjusts groups and counts when a tab is removed", () => {
var tabs = init_groups["group-0"].tabs;
const { container } = render(<Tab init_tabs={tabs} />);
expect(container.getElementsByClassName("draggable").length).toEqual(3);
var removeTabSpy = jest.spyOn(TabFunc, "removeTab");
fireEvent.click(container.querySelector(".close-tab"));
expect(removeTabSpy).toHaveBeenCalledTimes(1);
expect(container.getElementsByClassName("draggable").length).toEqual(2); // fails (does not remove the tab for some reason)
});
});
I mocked the Chrome API according to my needs, but feel that something is missing. To mock the Chrome API I followed this post (along with many others, even for other test runners like Jasmine): testing chrome.storage.local.set with jest.
Even though the Chrome storage API is mocked, I think the issue lies in this function which gets called upon initial render. That is, I think the chrome.storage.local.get
is not actually being executed, but am not sure why.
// ./src/Tab/Tab_functions.js
/**
* Sets the initial tabs based on Chrome's local storage upon initial render.
* If Chrome's local storage is empty, this is set to an empty array.
* @param {function} setTabs For re-rendering the group's tabs
* @param {string} id Used to get the correct group tabs
*/
export function setInitTabs(setTabs, id) {
chrome.storage.local.get("groups", (local) => {
var groups = local.groups;
setTabs((groups && groups[id] && groups[id].tabs) || []);
});
}
The reason I think the mocked Chrome storage API is not working properly is because when I manually set it in my tests, the number of tabs does not increase from 0. Which forced me to pass a prop (props.init_tabs
) to my Tab
component for testing purposes (https://github.com/lbragile/TabMerger/blob/f78a2694786d11e8270454521f92e679d182b577/src/Tab/Tab.js#L33-L35) – something I want to avoid if possible via setting local storage.
Can someone point me in the right direction? I would like to avoid using libraries like jest-chrome
since they abstract too much and make it harder for me to understand what is going on in my tests.
Advertisement
Answer
I think I have a solution for this now, so I will share with others.
I made proper mocks for my chrome storage API to use localStorage:
// __mocks__/chromeMock.js
storage: {
local: {
,
get: function (key, cb) {
const item = JSON.parse(localStorage.getItem(key));
cb({ [key]: item });
},
,
set: function (obj, cb) {
const key = Object.keys(obj)[0];
localStorage.setItem(key, JSON.stringify(obj[key]));
cb();
},
},
},
Also, to simulate the tab settings on initial render, I have a beforeEach
hook which sets my localStorage
using the above mock:
// __tests__/Tab.spec.js
var init_ls_entry, init_tabs, mockSet;
beforeEach(() => {
chrome.storage.local.set({ groups: init_groups }, () => {});
init_ls_entry = JSON.parse(localStorage.getItem("groups"));
init_tabs = init_ls_entry["group-0"].tabs;
mockSet = jest.fn(); // mock for setState hooks
});
AND most importantly, when I render(<Tab/>)
, I noticed that I wasn’t supplying the id
prop which caused nothing to render (in terms of tabs from localStorage
), so now I have this:
// __tests__/Tab.spec.js
describe("removeTab", () => {
it("correctly adjusts storage when a tab is removed", async () => {
const { container } = render(
<Tab id="group-0" setTabTotal={mockSet} setGroups={mockSet} />
);
var removeTabSpy = jest.spyOn(TabFunc, "removeTab");
var chromeSetSpy = jest.spyOn(chrome.storage.local, "set");
fireEvent.click(container.querySelector(".close-tab"));
await waitFor(() => {
expect(chromeSetSpy).toHaveBeenCalled();
});
chrome.storage.local.get("groups", (local) => {
expect(init_tabs.length).toEqual(3);
expect(local.groups["group-0"].tabs.length).toEqual(2);
expect(removeTabSpy).toHaveBeenCalledTimes(1);
});
expect.assertions(4);
});
});
Which passes!!
Now on to drag and drop testing 😊