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

webextension-rpc/test/ RpcClient.ts
144 lines
4.7 KiB

  1. import test from 'ava'
  2. import sinon from 'ts-sinon'
  3. import { RpcClient, RpcError, RemoteError, injectRpcInfo } from '../src/RpcClient'
  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 = new RpcClient({ tabId: 1 }).func('remoteFunc')
  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 = new RpcClient({ tabId: 1 }).func('remoteFunc')
  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 = new RpcClient({ tabId: 1 }).func('remoteFunc')
  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 = new RpcClient().func('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 call the browser.tabs function when tabId is overridden', async t => {
  50. const remoteFunc = new RpcClient().func('remoteFunc', { tabId: 123 })
  51. try {
  52. await remoteFunc()
  53. } catch (e) {}
  54. t.true(browser.tabs.sendMessage.calledOnce)
  55. t.true(browser.runtime.sendMessage.notCalled)
  56. })
  57. test.serial('should call the browser.runtime function when tabId is overridden as null', async t => {
  58. const remoteFunc = new RpcClient({ tabId: 123 }).func('remoteFunc', { tabId: null })
  59. try {
  60. await remoteFunc()
  61. } catch (e) {}
  62. t.true(browser.tabs.sendMessage.notCalled)
  63. t.true(browser.runtime.sendMessage.calledOnce)
  64. })
  65. test.serial('should send the call message correctly', async t => {
  66. const remoteFunc = new RpcClient().func('remoteFunc')
  67. try {
  68. await remoteFunc('a', 'b', 'c', 'd')
  69. } catch {}
  70. t.true(browser.runtime.sendMessage.calledOnce)
  71. t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{
  72. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__',
  73. funcName: 'remoteFunc',
  74. args: ['a', 'b', 'c', 'd'],
  75. addRpcInfoAsArgument: false,
  76. }])
  77. })
  78. test.serial('should handle the RpcInfoSymbol', async t => {
  79. const remoteFunc = new RpcClient().func('remoteFunc')
  80. try {
  81. await remoteFunc('a', 'b', injectRpcInfo, 'd')
  82. } catch {}
  83. t.true(browser.runtime.sendMessage.calledOnce)
  84. t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{
  85. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__',
  86. funcName: 'remoteFunc',
  87. args: ['a', 'b', null, 'd'],
  88. addRpcInfoAsArgument: 2,
  89. }])
  90. })
  91. test.serial('should throw an "interfering listener" error if response is unrecognised', async t => {
  92. browser.tabs.sendMessage = async () => 'some unexpected return value'
  93. const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
  94. await t.throwsAsync(remoteFunc, {
  95. instanceOf: RpcError,
  96. message: /RPC got a response from an interfering listener/,
  97. })
  98. })
  99. test.serial('should throw a "no response" error if response is undefined', async t => {
  100. // It seems we can get back undefined when the tab is closed before the response is sent.
  101. // In such cases 'no response' seems a better error message than 'interfering listener'.
  102. browser.tabs.sendMessage = async () => undefined
  103. const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
  104. await t.throwsAsync(remoteFunc, {
  105. instanceOf: RpcError,
  106. message: /Got no response/,
  107. })
  108. })
  109. test.serial('should throw RemoteError if the response contains an error message', async t => {
  110. browser.tabs.sendMessage = async () => ({
  111. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__',
  112. errorMessage: 'Remote function error',
  113. })
  114. const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
  115. await t.throwsAsync(remoteFunc, {
  116. instanceOf: RemoteError,
  117. message: 'Remote function error',
  118. })
  119. })
  120. test.serial('should return the value contained in the response', async t => {
  121. browser.tabs.sendMessage = async () => ({
  122. __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__',
  123. returnValue: 'Remote function return value',
  124. })
  125. const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
  126. t.is(await remoteFunc(), 'Remote function return value')
  127. })