Skip to content

Commit

Permalink
feat(messaging): Add support for frameId (#88)
Browse files Browse the repository at this point in the history
Co-authored-by: Aaron <aaronklinker1@gmail.com>
  • Loading branch information
Mehdi-Hp and aklinker1 authored Jan 10, 2025
1 parent 7c4ba67 commit 63f961c
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 25 deletions.
5 changes: 4 additions & 1 deletion docs/content/messaging/0.installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 });
```

::
Expand Down
30 changes: 27 additions & 3 deletions docs/content/messaging/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ See [`@webext-core/messaging`](/messaging/installation/)
```ts
interface BaseMessagingConfig {
logger?: Logger;
breakError?: boolean;
}
```

Expand All @@ -22,6 +23,8 @@ Shared configuration between all the different messengers.

- ***`logger?: Logger`*** (default: `console`)<br/>The logger to use when logging messages. Set to `null` to disable logging.

- ***`breakError?: boolean`*** (default: `undefined`)<br/>Whether to break an error when an invalid message is received.

## `CustomEventMessage`

```ts
Expand Down Expand Up @@ -87,6 +90,8 @@ websiteMessenger.sendMessage("initInjectedScript", ...);
websiteMessenger.onMessage("initInjectedScript", (...) => {
// ...
})

*
```

## `defineExtensionMessaging`
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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`***<br/>The tab to send a message to

- ***`frameId?: number`***<br/>The frame to send a message to. 0 represents the main frame.

## `WindowMessagingConfig`

```ts
Expand Down
6 changes: 3 additions & 3 deletions packages/messaging/src/extension.test-d.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -73,7 +73,7 @@ describe('Messenger Typing', () => {

expectTypeOf(sendMessage).parameter(0).toMatchTypeOf<'ping'>();
expectTypeOf(sendMessage).parameter(1).toBeUndefined();
expectTypeOf(sendMessage).parameter(2).toEqualTypeOf<number | undefined>();
expectTypeOf(sendMessage).parameter(2).toEqualTypeOf<number | undefined | SendMessageOptions>();
});

it('should require passing undefined to sendMessage when there is no arguments in a function definition', () => {
Expand All @@ -83,6 +83,6 @@ describe('Messenger Typing', () => {

expectTypeOf(sendMessage).parameter(0).toMatchTypeOf<'ping'>();
expectTypeOf(sendMessage).parameter(1).toBeUndefined();
expectTypeOf(sendMessage).parameter(2).toEqualTypeOf<number | undefined>();
expectTypeOf(sendMessage).parameter(2).toEqualTypeOf<number | undefined | SendMessageOptions>();
});
});
42 changes: 35 additions & 7 deletions packages/messaging/src/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProtocolMap>();
const input = 'test';
const tabId = 0;
Expand All @@ -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<ProtocolMap>();
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 () => {
Expand Down
45 changes: 34 additions & 11 deletions packages/messaging/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -51,9 +62,21 @@ export function defineExtensionMessaging<
>(config?: ExtensionMessagingConfig): ExtensionMessenger<TProtocolMap> {
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) => {
Expand Down

0 comments on commit 63f961c

Please sign in to comment.