Remote Procedure Call implementation for WebExtensions, to easily call functions across content scripts and background script.

webextension-rpc.js 3.2 KiB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import test from 'ava'
  2. import { spy } from 'sinon'
  3. import { remoteFunction, RpcError, RemoteError } from '../src/webextension-rpc.js'
  4. test.beforeEach(() => {
  5. // We mock the browser globally. Note we therefore need to test serially to prevent the tests from
  6. // interfering with each other.
  7. global.browser = {
  8. runtime: {
  9. sendMessage: spy(async () => {}),
  10. },
  11. tabs: {
  12. sendMessage: spy(async () => {}),
  13. },
  14. }
  15. })
  16. test.serial('should create a function', t => {
  17. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  18. t.is(remoteFunc.name, 'remoteFunc_RPC')
  19. t.is(typeof remoteFunc, 'function')
  20. })
  21. test.serial('should throw an error when unable to sendMessage', async t => {
  22. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  23. browser.tabs.sendMessage = async () => { throw new Error() }
  24. await t.throwsAsync(remoteFunc, {
  25. instanceOf: RpcError,
  26. message: `Got no response when trying to call 'remoteFunc'. Did you enable RPC in the tab's content script?`,
  27. })
  28. })
  29. test.serial('should call the browser.tabs function when tabId is given', async t => {
  30. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  31. try {
  32. await remoteFunc()
  33. } catch (e) {}
  34. console.log(browser.tabs.sendMessage.callCount)
  35. t.true(browser.tabs.sendMessage.calledOnce)
  36. t.true(browser.runtime.sendMessage.notCalled)
  37. })
  38. test.serial('should call the browser.runtime function when tabId is undefined', async t => {
  39. const remoteFunc = remoteFunction('remoteFunc')
  40. try {
  41. await remoteFunc()
  42. } catch (e) {}
  43. t.true(browser.tabs.sendMessage.notCalled)
  44. t.true(browser.runtime.sendMessage.calledOnce)
  45. })
  46. test.serial('should throw an "interfering listener" error if response is unrecognised', async t => {
  47. browser.tabs.sendMessage = async () => 'some unexpected return value'
  48. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  49. await t.throwsAsync(remoteFunc, {
  50. instanceOf: RpcError,
  51. message: /RPC got a response from an interfering listener/,
  52. })
  53. })
  54. test.serial('should throw a "no response" error if response is undefined', async t => {
  55. // It seems we can get back undefined when the tab is closed before the response is sent.
  56. // In such cases 'no response' seems a better error message than 'interfering listener'.
  57. browser.tabs.sendMessage = async () => undefined
  58. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  59. await t.throwsAsync(remoteFunc, {
  60. instanceOf: RpcError,
  61. message: /Got no response/,
  62. })
  63. })
  64. test.serial('should throw RemoteError if the response contains an error message', async t => {
  65. browser.tabs.sendMessage = async () => ({
  66. __RPC_RESPONSE__: '__RPC_RESPONSE__',
  67. errorMessage: 'Remote function error',
  68. })
  69. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  70. await t.throwsAsync(remoteFunc, {
  71. instanceOf: RemoteError,
  72. message: 'Remote function error',
  73. })
  74. })
  75. test.serial('should return the value contained in the response', async t => {
  76. browser.tabs.sendMessage = async () => ({
  77. __RPC_RESPONSE__: '__RPC_RESPONSE__',
  78. returnValue: 'Remote function return value',
  79. })
  80. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  81. t.is(await remoteFunc(), 'Remote function return value')
  82. })
  83. // TODO Test behaviour of executing side.