import test from 'ava' import sinon from 'ts-sinon' import { RpcClient, RpcError, RemoteError, injectRpcInfo } from '../src/RpcClient' function mockBrowser() { return { runtime: { sendMessage: sinon.spy(async (...args) => {}), }, tabs: { sendMessage: sinon.spy(async (...args) => {}), }, } } let browser = mockBrowser() test.beforeEach(() => { // We mock the browser globally. Note we therefore need to test serially to prevent the tests from // interfering with each other. global.browser = browser = mockBrowser() }) test.serial('should create a function', t => { const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc') t.is(remoteFunc.name, 'remoteFunc_RPC') t.is(typeof remoteFunc, 'function') }) test.serial('should throw an error when unable to sendMessage', async t => { const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc') browser.tabs.sendMessage = async () => { throw new Error() } await t.throwsAsync(remoteFunc, { instanceOf: RpcError, message: `Got no response when trying to call 'remoteFunc'. Did you enable RPC in the tab's content script?`, }) }) test.serial('should call the browser.tabs function when tabId is given', async t => { const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc') try { await remoteFunc() } catch (e) {} t.true(browser.tabs.sendMessage.calledOnce) t.true(browser.runtime.sendMessage.notCalled) }) test.serial('should call the browser.runtime function when tabId is undefined', async t => { const remoteFunc = new RpcClient().func('remoteFunc') try { await remoteFunc() } catch (e) {} t.true(browser.tabs.sendMessage.notCalled) t.true(browser.runtime.sendMessage.calledOnce) }) test.serial('should call the browser.tabs function when tabId is overridden', async t => { const remoteFunc = new RpcClient().func('remoteFunc', { tabId: 123 }) try { await remoteFunc() } catch (e) {} t.true(browser.tabs.sendMessage.calledOnce) t.true(browser.runtime.sendMessage.notCalled) }) test.serial('should call the browser.runtime function when tabId is overridden as null', async t => { const remoteFunc = new RpcClient({ tabId: 123 }).func('remoteFunc', { tabId: null }) try { await remoteFunc() } catch (e) {} t.true(browser.tabs.sendMessage.notCalled) t.true(browser.runtime.sendMessage.calledOnce) }) test.serial('should send the call message correctly', async t => { const remoteFunc = new RpcClient().func('remoteFunc') try { await remoteFunc('a', 'b', 'c', 'd') } catch {} t.true(browser.runtime.sendMessage.calledOnce) t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{ __WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__', funcName: 'remoteFunc', args: ['a', 'b', 'c', 'd'], addRpcInfoAsArgument: false, }]) }) test.serial('should handle the RpcInfoSymbol', async t => { const remoteFunc = new RpcClient().func('remoteFunc') try { await remoteFunc('a', 'b', injectRpcInfo, 'd') } catch {} t.true(browser.runtime.sendMessage.calledOnce) t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{ __WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__', funcName: 'remoteFunc', args: ['a', 'b', null, 'd'], addRpcInfoAsArgument: 2, }]) }) test.serial('should throw an "interfering listener" error if response is unrecognised', async t => { browser.tabs.sendMessage = async () => 'some unexpected return value' const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc') await t.throwsAsync(remoteFunc, { instanceOf: RpcError, message: /RPC got a response from an interfering listener/, }) }) test.serial('should throw a "no response" error if response is undefined', async t => { // It seems we can get back undefined when the tab is closed before the response is sent. // In such cases 'no response' seems a better error message than 'interfering listener'. browser.tabs.sendMessage = async () => undefined const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc') await t.throwsAsync(remoteFunc, { instanceOf: RpcError, message: /Got no response/, }) }) test.serial('should throw RemoteError if the response contains an error message', async t => { browser.tabs.sendMessage = async () => ({ __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__', errorMessage: 'Remote function error', }) const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc') await t.throwsAsync(remoteFunc, { instanceOf: RemoteError, message: 'Remote function error', }) }) test.serial('should return the value contained in the response', async t => { browser.tabs.sendMessage = async () => ({ __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__', returnValue: 'Remote function return value', }) const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc') t.is(await remoteFunc(), 'Remote function return value') })