Skip to content

Commit

Permalink
test: Expand test coverage of background.ts to 100% coverage (#1449)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Erek Speed <[email protected]>
  • Loading branch information
tora-pan and melink14 authored Dec 22, 2024
1 parent 20521ec commit 28805cb
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 34 deletions.
7 changes: 5 additions & 2 deletions extension/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => {
});

chrome.runtime.onMessage.addListener((request, sender, response) => {
void (async () => {
testOnlyPromiseHolder.onMessagePromise = (async () => {
const rcxMain = await rcxMainPromise;
switch (request.type) {
case 'enable?':
console.log('enable?');
if (sender.tab === undefined) {
throw TypeError('sender.tab is always defined here.');
throw new TypeError('sender.tab is always defined here.');
}
rcxMain.onTabSelect(sender.tab.id);
break;
Expand Down Expand Up @@ -91,4 +91,7 @@ chrome.runtime.onMessage.addListener((request, sender, response) => {
return true;
});

// This allows us to await the anonymouse promise in tests.
const testOnlyPromiseHolder = { onMessagePromise: Promise.resolve() };
export { testOnlyPromiseHolder };
export { rcxMainPromise as TestOnlyRxcMainPromise };
212 changes: 182 additions & 30 deletions extension/test/background_test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import chrome from 'sinon-chrome';

import { Config } from '../configuration';
import { DictEntryData } from '../data';
import { RcxMain } from '../rikaichan';
import { stubbedChrome as chrome } from './chrome_stubs';

use(sinonChai);
use(chaiAsPromised);

let rcxMain: RcxMain;
let onMessagePromiseHolder: { onMessagePromise: Promise<void> };

describe('background.ts', function () {
// Increase timeout from 2000ms since data tests can take longer.
Expand All @@ -21,21 +25,30 @@ describe('background.ts', function () {
chrome.storage.sync.get.yields({ kanjiInfo: [] });
chrome.storage.local.get.returns(Promise.resolve({ enabled: false }));
// Imports only run once so run in `before` to make it deterministic.
rcxMain = await (await import('../background')).TestOnlyRxcMainPromise;
const { TestOnlyRxcMainPromise, testOnlyPromiseHolder } = await import(
'../background'
);
onMessagePromiseHolder = testOnlyPromiseHolder;
rcxMain = await TestOnlyRxcMainPromise;
});

beforeEach(function () {
// Only reset the spies we're using since we need to preserve
// the state of `chrome.runtime.onMessage.addListener` for invoking
// the core functionality of background.ts.
chrome.tabs.sendMessage.reset();
chrome.runtime.sendMessage.reset();
});

afterEach(function () {
sinon.restore();
});

describe('when sent enable? message', function () {
it('should send "enable" message to tab', async function () {
rcxMain.enabled = true;

await sendMessageToBackground({ type: 'enable?' });
await sendMessageToBackground({ request: { type: 'enable?' } });

expect(chrome.tabs.sendMessage).to.have.been.calledWithMatch(
/* tabId= */ sinon.match.any,
Expand All @@ -49,19 +62,28 @@ describe('background.ts', function () {
rcxMain.enabled = true;
const tabId = 10;

await sendMessageToBackground({ tabId: tabId, type: 'enable?' });
await sendMessageToBackground({ tabId, request: { type: 'enable?' } });

expect(chrome.tabs.sendMessage).to.have.been.calledWithMatch(
tabId,
/* message= */ sinon.match.any
);
});

it('should throw an error if sender.tab is undefined', async function () {
await expect(
sendMessageToBackground({
tabId: null,
request: { type: 'enable?' },
})
).to.be.rejectedWith(TypeError, 'sender.tab is always defined here.');
});

it('should send config in message to tab', async function () {
rcxMain.enabled = true;
rcxMain.config = { copySeparator: 'testValue' } as Config;

await sendMessageToBackground({ type: 'enable?' });
await sendMessageToBackground({ request: { type: 'enable?' } });

expect(chrome.tabs.sendMessage).to.have.been.calledWithMatch(
/* tabId= */ sinon.match.any,
Expand All @@ -71,64 +93,194 @@ describe('background.ts', function () {
});

describe('when sent xsearch message', function () {
afterEach(function () {
sinon.restore();
});

it('should call rcxMain.search with the value', async function () {
const expectedText = 'theText';
const searchStub = sinon.stub(rcxMain, 'search');
it('should call response callback with the value returned by rcxMain.search', async function () {
const request = {
type: 'xsearch',
text: 'testXsearch',
dictOption: '-10',
};
sinon
.stub(rcxMain, 'search')
.withArgs(request.text, request.dictOption)
.returns({ title: 'theText' } as DictEntryData);
const response = sinon.spy();

await sendMessageToBackground({
type: 'xsearch',
text: expectedText,
request,
responseCallback: response,
});

expect(searchStub).to.have.been.calledOnceWith(
expectedText,
sinon.match.any
);
expect(response).to.have.been.calledWithMatch({ title: 'theText' });
});

it('should not call rcxMain.search if request.text is an empty string', async function () {
const searchStub = sinon.stub(rcxMain, 'search');

await sendMessageToBackground({
type: 'xsearch',
text: '',
request: { type: 'xsearch', text: '' },
});

expect(searchStub).to.not.have.been.called;
});
});

describe('when sent resetDict message', function () {
it('should call rcxMain.resetDict', async function () {
const resetStub = sinon.stub(rcxMain, 'resetDict');

await sendMessageToBackground({
request: { type: 'resetDict' },
});

expect(resetStub).to.be.called;
});
});

describe('when sent translate message', function () {
it('should call response callback with result of rcxMain.data.translate', async function () {
const request = {
type: 'translate',
title: 'た',
};
sinon
.stub(rcxMain.dict, 'translate')
.withArgs(request.title)
.returns({
title: 'translateTitle',
textLen: 4,
} as DictEntryData & { textLen: number });
const response = sinon.spy();

await sendMessageToBackground({ request, responseCallback: response });

expect(response).to.be.calledWithMatch({
title: 'translateTitle',
textLen: 4,
});
});
});

describe('when sent makeHtml message', function () {
it('should call response callback with the result of rcxMain.dict.makeHtml', async function () {
const request = {
type: 'makehtml',
entry: { title: 'htmlTest' } as DictEntryData,
};
sinon
.stub(rcxMain.dict, 'makeHtml')
.withArgs(request.entry)
.returns('myTestHtml');
const response = sinon.spy();

await sendMessageToBackground({
request,
responseCallback: response,
});

expect(response).to.be.calledWithMatch('myTestHtml');
});
});

describe('when sent switchOnlyReading message', function () {
it('should toggle the config value of onlyReading in chrome.storage.sync', async function () {
const request = { type: 'switchOnlyReading' };
rcxMain.config = { onlyreading: false } as Config;

await sendMessageToBackground({ request });

expect(chrome.storage.sync.set).to.be.calledWith({ onlyreading: true });
});
});

describe('when sent copyToClip message', function () {
it('should call copyToClip with given tab and entry', async function () {
const copyStub = sinon.stub(rcxMain, 'copyToClip');
const request = {
type: 'copyToClip',
entry: { title: 'copyTest' } as DictEntryData,
};

await sendMessageToBackground({ request, tabId: 12 });

expect(copyStub).to.be.calledWith({ id: 12 }, request.entry);
});
});

describe('when sent playTTS message', function () {
it('should call setupOffscreenDocument', async function () {
await sendMessageToBackground({ request: { type: 'playTTS', text: '' } });

expect(chrome.offscreen.createDocument).to.be.called;
});

it('should send a message to the offscreen document to play TTS of text', async function () {
const request = {
type: 'playTTS',
text: 'textToPlay',
};

await sendMessageToBackground({ request });

expect(chrome.runtime.sendMessage).to.be.calledWith({
target: 'offscreen',
type: 'playTtsOffscreen',
text: request.text,
});
});

it('should rethrow any errors that happen while offscreen doc is playing TTS', async function () {
const request = {
type: 'playTTS',
};
const expectedError = new Error('testError');
chrome.runtime.sendMessage.rejects(expectedError);

await expect(sendMessageToBackground({ request }))
.to.be.rejectedWith('Error while having offscreen doc play TTS')
.and.eventually.have.property('cause')
.that.deep.equals(expectedError);
});
});

describe('when sent unhandled message', function () {
it('should log informational message with request', async function () {
const logger = sinon.stub(console, 'log');
const request = {
type: 'noMatch',
};

await sendMessageToBackground({ request });

expect(logger).to.be.calledWithMatch(sinon.match('Unknown background'));
expect(logger).to.be.calledWithMatch(sinon.match(request));
logger.restore();
});
});
});

type Payload = {
tabId?: number;
text?: string;
type: string;
tabId?: number | null;
request: object;
responseCallback?: (response: unknown) => void;
};

async function sendMessageToBackground({
tabId = 0,
type,
text,
request = {},
responseCallback = () => {
// Do nothing by default.
},
}: Payload): Promise<void> {
const request: { type: string; text?: string } = {
type,
text,
};
// Allow a null tabId to denote it should be undefined (for testing error cases);
const sender = { tab: tabId !== null ? { id: tabId } : undefined };

// In background.ts, a promise is passed to `addListener` so we can await it here.
// eslint-disable-next-line @typescript-eslint/await-thenable
await chrome.runtime.onMessage.addListener.yield(
request,
{ tab: { id: tabId } },
sender,
responseCallback
);
await onMessagePromiseHolder.onMessagePromise;
return;
}
Loading

0 comments on commit 28805cb

Please sign in to comment.