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

index.ts 4.1 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import test from 'ava'
  2. import sinon from 'ts-sinon'
  3. import { remoteFunction, RpcError, RemoteError, injectRpcInfo } from '../src/index'
  4. function mockBrowser() {
  5. return {
  6. runtime: {
  7. sendMessage: sinon.spy(async (...args) => {}),
  8. },
  9. tabs: {
  10. sendMessage: sinon.spy(async (...args) => {}),
  11. },
  12. }
  13. }
  14. let browser = mockBrowser()
  15. test.beforeEach(() => {
  16. // We mock the browser globally. Note we therefore need to test serially to prevent the tests from
  17. // interfering with each other.
  18. global.browser = browser = mockBrowser()
  19. })
  20. test.serial('should create a function', t => {
  21. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  22. t.is(remoteFunc.name, 'remoteFunc_RPC')
  23. t.is(typeof remoteFunc, 'function')
  24. })
  25. test.serial('should throw an error when unable to sendMessage', async t => {
  26. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  27. browser.tabs.sendMessage = async () => { throw new Error() }
  28. await t.throwsAsync(remoteFunc, {
  29. instanceOf: RpcError,
  30. message: `Got no response when trying to call 'remoteFunc'. Did you enable RPC in the tab's content script?`,
  31. })
  32. })
  33. test.serial('should call the browser.tabs function when tabId is given', async t => {
  34. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  35. try {
  36. await remoteFunc()
  37. } catch (e) {}
  38. t.true(browser.tabs.sendMessage.calledOnce)
  39. t.true(browser.runtime.sendMessage.notCalled)
  40. })
  41. test.serial('should call the browser.runtime function when tabId is undefined', async t => {
  42. const remoteFunc = remoteFunction('remoteFunc')
  43. try {
  44. await remoteFunc()
  45. } catch (e) {}
  46. t.true(browser.tabs.sendMessage.notCalled)
  47. t.true(browser.runtime.sendMessage.calledOnce)
  48. })
  49. test.serial('should send the call message correctly', async t => {
  50. const remoteFunc = remoteFunction('remoteFunc')
  51. try {
  52. await remoteFunc('a', 'b', 'c', 'd')
  53. } catch {}
  54. t.true(browser.runtime.sendMessage.calledOnce)
  55. t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{
  56. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__',
  57. funcName: 'remoteFunc',
  58. args: ['a', 'b', 'c', 'd'],
  59. addRpcInfoAsArgument: false,
  60. }])
  61. })
  62. test.serial('should handle the RpcInfoSymbol', async t => {
  63. const remoteFunc = remoteFunction('remoteFunc')
  64. try {
  65. await remoteFunc('a', 'b', injectRpcInfo, 'd')
  66. } catch {}
  67. t.true(browser.runtime.sendMessage.calledOnce)
  68. t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{
  69. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__',
  70. funcName: 'remoteFunc',
  71. args: ['a', 'b', injectRpcInfo, 'd'],
  72. addRpcInfoAsArgument: 2,
  73. }])
  74. })
  75. test.serial('should throw an "interfering listener" error if response is unrecognised', async t => {
  76. browser.tabs.sendMessage = async () => 'some unexpected return value'
  77. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  78. await t.throwsAsync(remoteFunc, {
  79. instanceOf: RpcError,
  80. message: /RPC got a response from an interfering listener/,
  81. })
  82. })
  83. test.serial('should throw a "no response" error if response is undefined', async t => {
  84. // It seems we can get back undefined when the tab is closed before the response is sent.
  85. // In such cases 'no response' seems a better error message than 'interfering listener'.
  86. browser.tabs.sendMessage = async () => undefined
  87. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  88. await t.throwsAsync(remoteFunc, {
  89. instanceOf: RpcError,
  90. message: /Got no response/,
  91. })
  92. })
  93. test.serial('should throw RemoteError if the response contains an error message', async t => {
  94. browser.tabs.sendMessage = async () => ({
  95. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__',
  96. errorMessage: 'Remote function error',
  97. })
  98. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  99. await t.throwsAsync(remoteFunc, {
  100. instanceOf: RemoteError,
  101. message: 'Remote function error',
  102. })
  103. })
  104. test.serial('should return the value contained in the response', async t => {
  105. browser.tabs.sendMessage = async () => ({
  106. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__',
  107. returnValue: 'Remote function return value',
  108. })
  109. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  110. t.is(await remoteFunc(), 'Remote function return value')
  111. })
  112. // TODO Test behaviour of executing side.