From 63f961c89a30ae0b53cab773978901f0cb7dbf12 Mon Sep 17 00:00:00 2001 From: Mehdi Hoseini Date: Fri, 10 Jan 2025 20:57:43 +0330 Subject: [PATCH] feat(messaging): Add support for `frameId` (#88) Co-authored-by: Aaron --- docs/content/messaging/0.installation.md | 5 ++- docs/content/messaging/api.md | 30 +++++++++++++-- packages/messaging/src/extension.test-d.ts | 6 +-- packages/messaging/src/extension.test.ts | 42 ++++++++++++++++---- packages/messaging/src/extension.ts | 45 ++++++++++++++++------ 5 files changed, 103 insertions(+), 25 deletions(-) diff --git a/docs/content/messaging/0.installation.md b/docs/content/messaging/0.installation.md index ea1353d..acfbf6c 100644 --- a/docs/content/messaging/0.installation.md +++ b/docs/content/messaging/0.installation.md @@ -91,7 +91,9 @@ console.log(length); // 11 ### Sending Messages to Tabs -You can also send messages from your background script to a tab, but you need to know the `tabId`. +You can also send messages from your background script to a tab, but you need to know the `tabId`. This would send the message to all frames in the tab. + +If you want to send a message to a specific frame, you can pass an object to `sendMessage` with the `tabId` and `frameId`. ::code-group @@ -107,6 +109,7 @@ onMessage('getStringLength', message => { import { sendMessage } from './messaging'; const length = await sendMessage('getStringLength', 'hello world', tabId); +const length = await sendMessage('getStringLength', 'hello world', { tabId, frameId }); ``` :: diff --git a/docs/content/messaging/api.md b/docs/content/messaging/api.md index f104986..14d0a4d 100644 --- a/docs/content/messaging/api.md +++ b/docs/content/messaging/api.md @@ -13,6 +13,7 @@ See [`@webext-core/messaging`](/messaging/installation/) ```ts interface BaseMessagingConfig { logger?: Logger; + breakError?: boolean; } ``` @@ -22,6 +23,8 @@ Shared configuration between all the different messengers. - ***`logger?: Logger`*** (default: `console`)
The logger to use when logging messages. Set to `null` to disable logging. +- ***`breakError?: boolean`*** (default: `undefined`)
Whether to break an error when an invalid message is received. + ## `CustomEventMessage` ```ts @@ -87,6 +90,8 @@ websiteMessenger.sendMessage("initInjectedScript", ...); websiteMessenger.onMessage("initInjectedScript", (...) => { // ... }) + +* ``` ## `defineExtensionMessaging` @@ -173,11 +178,13 @@ Messenger returned by `defineExtensionMessaging`. ## `ExtensionSendMessageArgs` ```ts -type ExtensionSendMessageArgs = [tabId?: number]; +type ExtensionSendMessageArgs = [arg?: number | SendMessageOptions]; ``` -Send messsage accepts an additional, optional argument `tabId`. Pass it to send a message to a -specific tab from the background script. +Send message accepts either: +- No arguments to send to background +- A tabId number to send to a specific tab +- A SendMessageOptions object to target a specific tab and frame You cannot message between tabs directly. It must go through the background script. @@ -374,6 +381,23 @@ Call to ensure an active listener has been removed. If the listener has already been removed with `Messenger.removeAllListeners`, this is a noop. +## `SendMessageOptions` + +```ts +interface SendMessageOptions { + tabId: number; + frameId?: number; +} +``` + +Options for sending a message to a specific tab/frame + +### Properties + +- ***`tabId: number`***
The tab to send a message to + +- ***`frameId?: number`***
The frame to send a message to. 0 represents the main frame. + ## `WindowMessagingConfig` ```ts diff --git a/packages/messaging/src/extension.test-d.ts b/packages/messaging/src/extension.test-d.ts index 3f896b3..f548580 100644 --- a/packages/messaging/src/extension.test-d.ts +++ b/packages/messaging/src/extension.test-d.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest'; import { MaybePromise, ProtocolWithReturn } from './types'; -import { defineExtensionMessaging } from './extension'; +import { defineExtensionMessaging, SendMessageOptions } from './extension'; describe('Messenger Typing', () => { it('should use any for data and return type when a protocol map is not passed', () => { @@ -73,7 +73,7 @@ describe('Messenger Typing', () => { expectTypeOf(sendMessage).parameter(0).toMatchTypeOf<'ping'>(); expectTypeOf(sendMessage).parameter(1).toBeUndefined(); - expectTypeOf(sendMessage).parameter(2).toEqualTypeOf(); + expectTypeOf(sendMessage).parameter(2).toEqualTypeOf(); }); it('should require passing undefined to sendMessage when there is no arguments in a function definition', () => { @@ -83,6 +83,6 @@ describe('Messenger Typing', () => { expectTypeOf(sendMessage).parameter(0).toMatchTypeOf<'ping'>(); expectTypeOf(sendMessage).parameter(1).toBeUndefined(); - expectTypeOf(sendMessage).parameter(2).toEqualTypeOf(); + expectTypeOf(sendMessage).parameter(2).toEqualTypeOf(); }); }); diff --git a/packages/messaging/src/extension.test.ts b/packages/messaging/src/extension.test.ts index 24e24e9..40a0591 100644 --- a/packages/messaging/src/extension.test.ts +++ b/packages/messaging/src/extension.test.ts @@ -34,7 +34,7 @@ describe('Messaging Wrapper', () => { expect(actual).toBe(expected); }); - it('should send messages to tabs', async () => { + it('should send messages to tabs with only tabId', async () => { const { sendMessage } = defineExtensionMessaging(); const input = 'test'; const tabId = 0; @@ -45,12 +45,40 @@ describe('Messaging Wrapper', () => { expect(actual).toBe(expected); expect(fakeBrowser.tabs.sendMessage).toBeCalledTimes(1); - expect(fakeBrowser.tabs.sendMessage).toBeCalledWith(tabId, { - id: expect.any(Number), - timestamp: expect.any(Number), - type: 'getLength', - data: input, - }); + expect(fakeBrowser.tabs.sendMessage).toBeCalledWith( + tabId, + { + id: expect.any(Number), + timestamp: expect.any(Number), + type: 'getLength', + data: input, + }, + undefined, + ); + }); + + it('should send messages to tabs with tabId and frameId', async () => { + const { sendMessage } = defineExtensionMessaging(); + const input = 'test'; + const tabId = 0; + const frameId = 0; + const expected = 4; + vi.spyOn(fakeBrowser.tabs, 'sendMessage').mockResolvedValueOnce({ res: expected }); + + const actual = await sendMessage('getLength', input, { tabId, frameId }); + + expect(actual).toBe(expected); + expect(fakeBrowser.tabs.sendMessage).toBeCalledTimes(1); + expect(fakeBrowser.tabs.sendMessage).toBeCalledWith( + tabId, + { + id: expect.any(Number), + timestamp: expect.any(Number), + type: 'getLength', + data: input, + }, + { frameId }, + ); }); it('should handle errors', async () => { diff --git a/packages/messaging/src/extension.ts b/packages/messaging/src/extension.ts index 5d48fad..e867594 100644 --- a/packages/messaging/src/extension.ts +++ b/packages/messaging/src/extension.ts @@ -19,17 +19,28 @@ export interface ExtensionMessage { } /** - * Send messsage accepts an additional, optional argument `tabId`. Pass it to send a message to a - * specific tab from the background script. - * - * You cannot message between tabs directly. It must go through the background script. + * Options for sending a message to a specific tab/frame */ -export type ExtensionSendMessageArgs = [ +export interface SendMessageOptions { + /** + * The tab to send a message to + */ + tabId: number; /** - * The tab to send a message to. + * The frame to send a message to. 0 represents the main frame. */ - tabId?: number, -]; + frameId?: number; +} + +/** + * Send message accepts either: + * - No arguments to send to background + * - A tabId number to send to a specific tab + * - A SendMessageOptions object to target a specific tab and frame + * + * You cannot message between tabs directly. It must go through the background script. + */ +export type ExtensionSendMessageArgs = [arg?: number | SendMessageOptions]; /** * Messenger returned by `defineExtensionMessaging`. @@ -51,9 +62,21 @@ export function defineExtensionMessaging< >(config?: ExtensionMessagingConfig): ExtensionMessenger { return defineGenericMessanging({ ...config, - sendMessage(message, tabId) { - if (tabId == null) return Browser.runtime.sendMessage(message); - return Browser.tabs.sendMessage(tabId, message); + sendMessage(message, arg) { + // No args - send to background + if (arg == null) { + return Browser.runtime.sendMessage(message); + } + + // Handle both number and options object + const options: SendMessageOptions = typeof arg === 'number' ? { tabId: arg } : arg; + + return Browser.tabs.sendMessage( + options.tabId, + message, + // Pass frameId if specified + options.frameId != null ? { frameId: options.frameId } : undefined, + ); }, addRootListener(processMessage) { const listener = (message: any, sender: Runtime.MessageSender) => {