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

webextension-rpc/src/ webextensionRPC.test.js
95 lines
3.9 KiB

  1. /* eslint-env jest */
  2. import { remoteFunction } from './webextensionRPC'
  3. describe('remoteFunction', () => {
  4. beforeEach(() => {
  5. browser.runtime = {
  6. sendMessage: jest.fn(() => Promise.resolve()),
  7. }
  8. browser.tabs = {
  9. sendMessage: jest.fn(() => Promise.resolve()),
  10. }
  11. })
  12. test('should create a function', () => {
  13. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  14. expect(remoteFunc.name).toBe('remoteFunc_RPC')
  15. expect(typeof remoteFunc).toBe('function')
  16. })
  17. test('should throw an error when unable to sendMessage', async () => {
  18. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  19. browser.tabs.sendMessage.mockImplementationOnce(() => { throw new Error() })
  20. await expect(remoteFunc()).rejects.toMatchObject({
  21. message: `Got no response when trying to call 'remoteFunc'. Did you enable RPC in the tab's content script?`,
  22. })
  23. })
  24. test('should call the browser.tabs function when tabId is given', async () => {
  25. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  26. try {
  27. await remoteFunc()
  28. } catch (e) {}
  29. expect(browser.tabs.sendMessage).toHaveBeenCalledTimes(1)
  30. expect(browser.runtime.sendMessage).toHaveBeenCalledTimes(0)
  31. })
  32. test('should call the browser.runtime function when tabId is undefined', async () => {
  33. const remoteFunc = remoteFunction('remoteFunc')
  34. try {
  35. await remoteFunc()
  36. } catch (e) {}
  37. expect(browser.tabs.sendMessage).toHaveBeenCalledTimes(0)
  38. expect(browser.runtime.sendMessage).toHaveBeenCalledTimes(1)
  39. })
  40. test('should throw an "interfering listener" error if response is unrecognised', async () => {
  41. browser.tabs.sendMessage.mockReturnValueOnce('some unexpected return value')
  42. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  43. await expect(remoteFunc()).rejects.toMatchObject({
  44. message: expect.stringContaining('RPC got a response from an interfering listener'),
  45. })
  46. })
  47. test('should throw a "no response" error if sending the message fails', async () => {
  48. browser.tabs.sendMessage.mockReturnValueOnce(Promise.reject(new Error()))
  49. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  50. await expect(remoteFunc()).rejects.toMatchObject({
  51. message: expect.stringContaining('Got no response'),
  52. })
  53. })
  54. test('should throw a "no response" error if response is undefined', async () => {
  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.mockReturnValueOnce(undefined)
  58. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  59. await expect(remoteFunc()).rejects.toMatchObject({
  60. message: expect.stringContaining('Got no response'),
  61. })
  62. })
  63. test('should throw an error if the response contains an error message', async () => {
  64. browser.tabs.sendMessage.mockReturnValueOnce({
  65. __RPC_RESPONSE__: '__RPC_RESPONSE__',
  66. errorMessage: 'Remote function error',
  67. })
  68. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  69. await expect(remoteFunc()).rejects.toMatchObject({
  70. message: 'Remote function error',
  71. })
  72. })
  73. test('should return the value contained in the response', async () => {
  74. browser.tabs.sendMessage.mockReturnValueOnce({
  75. __RPC_RESPONSE__: '__RPC_RESPONSE__',
  76. returnValue: 'Remote function return value',
  77. })
  78. const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
  79. await expect(remoteFunc()).resolves.toBe('Remote function return value')
  80. })
  81. })
  82. // TODO Test behaviour of executing side.