webext-messenger 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/distribution/receiver.js +6 -5
- package/distribution/sender.js +108 -78
- package/distribution/sender.test.d.ts +6 -0
- package/distribution/sender.test.js +160 -0
- package/package.json +4 -5
package/distribution/receiver.js
CHANGED
|
@@ -42,7 +42,7 @@ function onMessageListener(message, sender, sendResponse) {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
const { type, target, args, options = {} } = message;
|
|
45
|
-
const { trace = [], seq } = options;
|
|
45
|
+
const { trace = [], seq, retry } = options;
|
|
46
46
|
if (action === "forward") {
|
|
47
47
|
log.debug(type, seq, "🔀 forwarded", { sender, target });
|
|
48
48
|
}
|
|
@@ -57,7 +57,7 @@ function onMessageListener(message, sender, sendResponse) {
|
|
|
57
57
|
(async () => {
|
|
58
58
|
try {
|
|
59
59
|
trace.push(sender);
|
|
60
|
-
const value = await prepareResponse(message, action, { trace });
|
|
60
|
+
const value = await prepareResponse(message, action, { trace, retry });
|
|
61
61
|
log.debug(type, seq, "↗️ responding", { value });
|
|
62
62
|
sendResponse({ __webextMessenger, value });
|
|
63
63
|
}
|
|
@@ -87,17 +87,18 @@ function onMessageExternalListener(message, sender, sendResponse) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
/** Generates the value or error to return to the sender; does not include further messaging logic */
|
|
90
|
-
async function prepareResponse(message, action,
|
|
90
|
+
async function prepareResponse(message, action, options) {
|
|
91
91
|
const { type, target, args } = message;
|
|
92
92
|
if (action === "forward") {
|
|
93
|
-
return messenger(type,
|
|
93
|
+
return messenger(type, options, target, ...args);
|
|
94
94
|
}
|
|
95
95
|
const localHandler = handlers.get(type);
|
|
96
96
|
if (localHandler) {
|
|
97
97
|
if ("extensionId" in target && !externalMethods.has(type)) {
|
|
98
98
|
throw new MessengerError(`The ${type} handler is registered in ${getContextName()} for internal use only`);
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
const { trace = [] } = options;
|
|
101
|
+
return localHandler.apply({ trace }, args);
|
|
101
102
|
}
|
|
102
103
|
if (didUserRegisterMethods()) {
|
|
103
104
|
throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
|
package/distribution/sender.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isBackground, isExtensionContext } from "webext-detect";
|
|
1
|
+
import { isExtensionContext } from "webext-detect";
|
|
3
2
|
import { deserializeError } from "serialize-error";
|
|
4
3
|
import { isObject, MessengerError, ExtensionNotFoundError, __webextMessenger, } from "./shared.js";
|
|
5
4
|
import { log } from "./logging.js";
|
|
6
5
|
import { handlers } from "./handlers.js";
|
|
7
6
|
import { events } from "./events.js";
|
|
7
|
+
import { compareTargets } from "./targetLogic.js";
|
|
8
|
+
import { thisTarget } from "./thisTarget.js";
|
|
8
9
|
const _errorNonExistingTarget = "Could not establish connection. Receiving end does not exist.";
|
|
9
10
|
const _errorTargetClosedEarly = "A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received";
|
|
10
11
|
export const errorTargetClosedEarly = "The target was closed before receiving a response";
|
|
@@ -20,6 +21,28 @@ function attemptLog(attemptCount) {
|
|
|
20
21
|
function wasContextInvalidated() {
|
|
21
22
|
return !chrome.runtime?.id;
|
|
22
23
|
}
|
|
24
|
+
function getErrorMessage(error) {
|
|
25
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
26
|
+
return String(error.message);
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function shouldRetryError(error, target) {
|
|
31
|
+
const message = getErrorMessage(error);
|
|
32
|
+
// Don't retry sending to the background page unless it really hasn't loaded yet
|
|
33
|
+
if (target.page !== "background" && error instanceof MessengerError) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
// Page or its content script not yet loaded
|
|
37
|
+
if (message === _errorNonExistingTarget) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// `registerMethods` not yet loaded
|
|
41
|
+
if (message?.startsWith("No handlers registered in ")) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
23
46
|
function makeMessage(type, args, target, options) {
|
|
24
47
|
return {
|
|
25
48
|
__webextMessenger,
|
|
@@ -39,102 +62,108 @@ function manageConnection(type, { seq, isNotification, retry }, target, sendMess
|
|
|
39
62
|
});
|
|
40
63
|
}
|
|
41
64
|
async function manageMessage(type, target, seq, retry, sendMessage) {
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
const maxRetryTime = 4000;
|
|
67
|
+
const minTimeout = 100;
|
|
68
|
+
const factor = 1.3;
|
|
69
|
+
// Safety cap to avoid infinite loops, generally stops at 11 with current setting
|
|
70
|
+
// MUST BE UPDATED if maxRetryTime, minTimeout or factor are changed
|
|
71
|
+
const maxRetryCount = 15;
|
|
72
|
+
let attemptCount = 0;
|
|
73
|
+
let currentTimeout = minTimeout;
|
|
74
|
+
while (attemptCount < maxRetryCount) {
|
|
75
|
+
attemptCount++;
|
|
76
|
+
try {
|
|
77
|
+
// eslint-disable-next-line no-await-in-loop -- Necessary for retry logic
|
|
78
|
+
const response = await sendMessage(attemptCount);
|
|
79
|
+
if (isMessengerResponse(response)) {
|
|
80
|
+
if ("error" in response) {
|
|
81
|
+
log.debug(type, seq, "↘️ replied with error", response.error);
|
|
82
|
+
throw deserializeError(response.error);
|
|
83
|
+
}
|
|
84
|
+
log.debug(type, seq, "↘️ replied successfully", response.value);
|
|
85
|
+
return response.value;
|
|
86
|
+
}
|
|
87
|
+
// If no one answers, `response` will be `undefined`
|
|
88
|
+
// If the target does not have any `onMessage` listener at all, it will throw
|
|
89
|
+
// Possible:
|
|
90
|
+
// - Any target exists and has onMessage handler, but never handled the message
|
|
91
|
+
// - Extension page exists and has Messenger, but never handled the message (Messenger in Runtime ignores messages when the target isn't found)
|
|
92
|
+
// Not possible:
|
|
93
|
+
// - Tab exists and has Messenger, but never handled the message (Messenger in CS always handles messages)
|
|
94
|
+
// - Any target exists, but Messenger didn't have the specific Type handler (The receiving Messenger will throw an error)
|
|
95
|
+
// - No targets exist (the browser immediately throws "Could not establish connection. Receiving end does not exist.")
|
|
96
|
+
if (response === undefined) {
|
|
97
|
+
if ("page" in target) {
|
|
98
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
|
99
|
+
}
|
|
100
|
+
throw new MessengerError(`Messenger was not available in the target ${JSON.stringify(target)} for ${type}`);
|
|
60
101
|
}
|
|
61
|
-
|
|
102
|
+
// Possible:
|
|
103
|
+
// - Non-Messenger handler responded
|
|
104
|
+
throw new MessengerError(`Conflict: The message ${type} was handled by a third-party listener`);
|
|
62
105
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw new MessengerError(`Conflict: The message ${type} was handled by a third-party listener`);
|
|
66
|
-
}, {
|
|
67
|
-
minTimeout: 100,
|
|
68
|
-
factor: 1.3,
|
|
69
|
-
// Do not set this to undefined or Infinity, it doesn't work the same way
|
|
70
|
-
...(retry ? {} : { retries: 0 }),
|
|
71
|
-
maxRetryTime: 4000,
|
|
72
|
-
async onFailedAttempt(error) {
|
|
106
|
+
catch (error) {
|
|
107
|
+
const errorMessage = getErrorMessage(error);
|
|
73
108
|
events.dispatchEvent(new CustomEvent("failed-attempt", {
|
|
74
109
|
detail: {
|
|
75
110
|
type,
|
|
76
111
|
seq,
|
|
77
112
|
target,
|
|
78
113
|
error,
|
|
79
|
-
attemptCount
|
|
114
|
+
attemptCount,
|
|
80
115
|
},
|
|
81
116
|
}));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// The extension is not available and it will not be. Do not retry.
|
|
117
|
+
// Check for non-retryable errors
|
|
118
|
+
if ("extensionId" in target && errorMessage === _errorNonExistingTarget) {
|
|
85
119
|
throw new ExtensionNotFoundError(errorExtensionNotFound.replace("$ID", target.extensionId));
|
|
86
120
|
}
|
|
87
121
|
if (isExtensionContext() && wasContextInvalidated()) {
|
|
88
|
-
// The error matches the native context invalidated error
|
|
89
|
-
// *.sendMessage() might fail with a message-specific error that is less useful,
|
|
90
|
-
// like "Sender closed without responding"
|
|
91
122
|
throw new Error("Extension context invalidated.");
|
|
92
123
|
}
|
|
93
|
-
if (
|
|
124
|
+
if (errorMessage === _errorTargetClosedEarly) {
|
|
94
125
|
throw new Error(errorTargetClosedEarly);
|
|
95
126
|
}
|
|
96
|
-
if (!(
|
|
97
|
-
// If NONE of these conditions is true, stop retrying
|
|
98
|
-
// Don't retry sending to the background page unless it really hasn't loaded yet
|
|
99
|
-
((target.page !== "background" &&
|
|
100
|
-
error instanceof MessengerError) ||
|
|
101
|
-
// Page or its content script not yet loaded
|
|
102
|
-
error.message === _errorNonExistingTarget ||
|
|
103
|
-
// `registerMethods` not yet loaded
|
|
104
|
-
String(error.message).startsWith("No handlers registered in ")))) {
|
|
127
|
+
if (!shouldRetryError(error, target)) {
|
|
105
128
|
throw error;
|
|
106
129
|
}
|
|
130
|
+
// Check if tab is still valid
|
|
107
131
|
if (chrome.tabs && typeof target.tabId === "number") {
|
|
132
|
+
let tabInfo;
|
|
108
133
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
throw new Error(errorTabWasDiscarded);
|
|
112
|
-
}
|
|
134
|
+
// eslint-disable-next-line no-await-in-loop -- Necessary to check tab status during retry
|
|
135
|
+
tabInfo = await chrome.tabs.get(target.tabId);
|
|
113
136
|
}
|
|
114
137
|
catch {
|
|
115
138
|
throw new Error(errorTabDoesntExist);
|
|
116
139
|
}
|
|
140
|
+
if (tabInfo.discarded) {
|
|
141
|
+
throw new Error(errorTabWasDiscarded);
|
|
142
|
+
}
|
|
117
143
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
// Check if we should stop retrying
|
|
145
|
+
const elapsedTime = Date.now() - startTime;
|
|
146
|
+
if (!retry || elapsedTime >= maxRetryTime) {
|
|
147
|
+
if (errorMessage === _errorNonExistingTarget) {
|
|
148
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
|
149
|
+
}
|
|
150
|
+
events.dispatchEvent(new CustomEvent("attempts-exhausted", {
|
|
151
|
+
detail: { type, seq, target, error },
|
|
152
|
+
}));
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
log.debug(type, seq, "will retry", attemptLog(attemptCount));
|
|
156
|
+
// Wait before retrying with exponential backoff
|
|
157
|
+
const waitTime = currentTimeout;
|
|
158
|
+
// eslint-disable-next-line no-await-in-loop -- Necessary for retry delay
|
|
159
|
+
await new Promise((resolve) => {
|
|
160
|
+
setTimeout(resolve, waitTime);
|
|
161
|
+
});
|
|
162
|
+
currentTimeout = Math.floor(currentTimeout * factor);
|
|
126
163
|
}
|
|
127
|
-
events.dispatchEvent(new CustomEvent("attempts-exhausted", {
|
|
128
|
-
detail: { type, seq, target, error },
|
|
129
|
-
}));
|
|
130
|
-
throw error;
|
|
131
|
-
});
|
|
132
|
-
if ("error" in response) {
|
|
133
|
-
log.debug(type, seq, "↘️ replied with error", response.error);
|
|
134
|
-
throw deserializeError(response.error);
|
|
135
164
|
}
|
|
136
|
-
|
|
137
|
-
|
|
165
|
+
// If you reach this, refer to note above `maxRetryCount` definition
|
|
166
|
+
throw new MessengerError("Exceeded maximum retry attempts. This suggests a low `maxRetryCount`");
|
|
138
167
|
}
|
|
139
168
|
// Not a UID nor a truly global sequence. Signal / console noise compromise.
|
|
140
169
|
// The time part is a pseudo-random number between 0 and 99 that helps visually
|
|
@@ -157,16 +186,17 @@ function messenger(type, options, target, ...args) {
|
|
|
157
186
|
};
|
|
158
187
|
return manageConnection(type, options, target, sendMessage);
|
|
159
188
|
}
|
|
189
|
+
// Use local methods if the target matches the current context
|
|
190
|
+
if (compareTargets(target, thisTarget)) {
|
|
191
|
+
const handler = handlers.get(type);
|
|
192
|
+
if (handler) {
|
|
193
|
+
log.warn(type, seq, "is being handled locally");
|
|
194
|
+
return handler.apply({ trace: [] }, args);
|
|
195
|
+
}
|
|
196
|
+
throw new MessengerError("No handler registered locally for " + type);
|
|
197
|
+
}
|
|
160
198
|
// Message goes to extension page
|
|
161
199
|
if ("page" in target) {
|
|
162
|
-
if (target.page === "background" && isBackground()) {
|
|
163
|
-
const handler = handlers.get(type);
|
|
164
|
-
if (handler) {
|
|
165
|
-
log.warn(type, seq, "is being handled locally");
|
|
166
|
-
return handler.apply({ trace: [] }, args);
|
|
167
|
-
}
|
|
168
|
-
throw new MessengerError("No handler registered locally for " + type);
|
|
169
|
-
}
|
|
170
200
|
const sendMessage = async (attemptCount) => {
|
|
171
201
|
log.debug(type, seq, "↗️ sending message to runtime", attemptLog(attemptCount));
|
|
172
202
|
return chrome.runtime.sendMessage(makeMessage(type, args, target, options));
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/naming-convention */
|
|
2
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
import { handlers } from "./handlers.js";
|
|
4
|
+
import * as thisTargetModule from "./thisTarget.js";
|
|
5
|
+
// Mock dependencies
|
|
6
|
+
vi.mock("webext-detect", () => ({
|
|
7
|
+
isBackground: vi.fn(() => false),
|
|
8
|
+
isExtensionContext: vi.fn(() => true),
|
|
9
|
+
isOffscreenDocument: vi.fn(() => false),
|
|
10
|
+
isContentScript: vi.fn(() => true),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("./logging.js", () => ({
|
|
13
|
+
log: {
|
|
14
|
+
debug: vi.fn(),
|
|
15
|
+
warn: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("./events.js", () => ({
|
|
19
|
+
events: {
|
|
20
|
+
dispatchEvent: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
// Mock chrome APIs - simulate content script environment (no chrome.tabs)
|
|
24
|
+
globalThis.chrome = {
|
|
25
|
+
runtime: {
|
|
26
|
+
id: "test-extension-id",
|
|
27
|
+
sendMessage: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
// Note: chrome.tabs is undefined in content scripts
|
|
30
|
+
};
|
|
31
|
+
describe("messenger with tab targets and local methods", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
handlers.clear();
|
|
35
|
+
});
|
|
36
|
+
test("should use local method when targeting same tab and frame", async () => {
|
|
37
|
+
// Setup: Set thisTarget to have tabId: 1, frameId: 0
|
|
38
|
+
const mockThisTarget = {
|
|
39
|
+
tabId: 1,
|
|
40
|
+
frameId: 0,
|
|
41
|
+
page: "test",
|
|
42
|
+
};
|
|
43
|
+
vi.spyOn(thisTargetModule, "thisTarget", "get").mockReturnValue(mockThisTarget);
|
|
44
|
+
// Register a local handler
|
|
45
|
+
const mockHandler = vi.fn(async () => "local result");
|
|
46
|
+
handlers.set("testMethod", mockHandler);
|
|
47
|
+
// Import messenger after mocks are set up
|
|
48
|
+
const { messenger } = await import("./sender.js");
|
|
49
|
+
// Test: Send message to same tab and frame
|
|
50
|
+
const result = await messenger("testMethod", {}, { tabId: 1, frameId: 0 });
|
|
51
|
+
// Verify: Handler was called locally
|
|
52
|
+
expect(mockHandler).toHaveBeenCalledOnce();
|
|
53
|
+
expect(mockHandler).toHaveBeenCalledWith();
|
|
54
|
+
expect(result).toBe("local result");
|
|
55
|
+
// Verify: No message was sent via chrome.runtime.sendMessage
|
|
56
|
+
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
test("should send message when targeting different tab", async () => {
|
|
59
|
+
// Setup: Set thisTarget to have tabId: 1, frameId: 0
|
|
60
|
+
const mockThisTarget = {
|
|
61
|
+
tabId: 1,
|
|
62
|
+
frameId: 0,
|
|
63
|
+
page: "test",
|
|
64
|
+
};
|
|
65
|
+
vi.spyOn(thisTargetModule, "thisTarget", "get").mockReturnValue(mockThisTarget);
|
|
66
|
+
// Register a local handler (should not be used)
|
|
67
|
+
const mockHandler = vi.fn(async () => "local result");
|
|
68
|
+
handlers.set("testMethod", mockHandler);
|
|
69
|
+
// Mock chrome.runtime.sendMessage to return a messenger response
|
|
70
|
+
vi.mocked(chrome.runtime.sendMessage).mockResolvedValue({
|
|
71
|
+
__webextMessenger: true,
|
|
72
|
+
value: "remote result",
|
|
73
|
+
});
|
|
74
|
+
// Import messenger after mocks are set up
|
|
75
|
+
const { messenger } = await import("./sender.js");
|
|
76
|
+
// Test: Send message to different tab
|
|
77
|
+
const result = await messenger("testMethod", {}, { tabId: 2, frameId: 0 });
|
|
78
|
+
// Verify: Message was sent via chrome.runtime.sendMessage
|
|
79
|
+
expect(chrome.runtime.sendMessage).toHaveBeenCalledOnce();
|
|
80
|
+
expect(result).toBe("remote result");
|
|
81
|
+
// Verify: Local handler was not called
|
|
82
|
+
expect(mockHandler).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
test("should send message when targeting different frame in same tab", async () => {
|
|
85
|
+
// Setup: Set thisTarget to have tabId: 1, frameId: 0
|
|
86
|
+
const mockThisTarget = {
|
|
87
|
+
tabId: 1,
|
|
88
|
+
frameId: 0,
|
|
89
|
+
page: "test",
|
|
90
|
+
};
|
|
91
|
+
vi.spyOn(thisTargetModule, "thisTarget", "get").mockReturnValue(mockThisTarget);
|
|
92
|
+
// Register a local handler (should not be used)
|
|
93
|
+
const mockHandler = vi.fn(async () => "local result");
|
|
94
|
+
handlers.set("testMethod", mockHandler);
|
|
95
|
+
// Mock chrome.runtime.sendMessage to return a messenger response
|
|
96
|
+
vi.mocked(chrome.runtime.sendMessage).mockResolvedValue({
|
|
97
|
+
__webextMessenger: true,
|
|
98
|
+
value: "remote result",
|
|
99
|
+
});
|
|
100
|
+
// Import messenger after mocks are set up
|
|
101
|
+
const { messenger } = await import("./sender.js");
|
|
102
|
+
// Test: Send message to different frame in same tab
|
|
103
|
+
const result = await messenger("testMethod", {}, { tabId: 1, frameId: 1 });
|
|
104
|
+
// Verify: Message was sent via chrome.runtime.sendMessage
|
|
105
|
+
expect(chrome.runtime.sendMessage).toHaveBeenCalledOnce();
|
|
106
|
+
expect(result).toBe("remote result");
|
|
107
|
+
// Verify: Local handler was not called
|
|
108
|
+
expect(mockHandler).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
test("should send message when targeting allFrames", async () => {
|
|
111
|
+
// Setup: Set thisTarget to have tabId: 1, frameId: 0
|
|
112
|
+
const mockThisTarget = {
|
|
113
|
+
tabId: 1,
|
|
114
|
+
frameId: 0,
|
|
115
|
+
page: "test",
|
|
116
|
+
};
|
|
117
|
+
vi.spyOn(thisTargetModule, "thisTarget", "get").mockReturnValue(mockThisTarget);
|
|
118
|
+
// Register a local handler (should not be used for allFrames)
|
|
119
|
+
const mockHandler = vi.fn(async () => "local result");
|
|
120
|
+
handlers.set("testMethod", mockHandler);
|
|
121
|
+
// Mock chrome.runtime.sendMessage to return a messenger response
|
|
122
|
+
vi.mocked(chrome.runtime.sendMessage).mockResolvedValue({
|
|
123
|
+
__webextMessenger: true,
|
|
124
|
+
value: "remote result",
|
|
125
|
+
});
|
|
126
|
+
// Import messenger after mocks are set up
|
|
127
|
+
const { messenger } = await import("./sender.js");
|
|
128
|
+
// Test: Send message to allFrames in same tab
|
|
129
|
+
const result = await messenger("testMethod", {}, { tabId: 1, frameId: "allFrames" });
|
|
130
|
+
// Verify: Message was sent via chrome.runtime.sendMessage
|
|
131
|
+
expect(chrome.runtime.sendMessage).toHaveBeenCalledOnce();
|
|
132
|
+
expect(result).toBe("remote result");
|
|
133
|
+
// Verify: Local handler was not called
|
|
134
|
+
expect(mockHandler).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
test("should throw error when no local handler registered for same tab/frame", async () => {
|
|
137
|
+
// Setup: Set thisTarget to have tabId: 1, frameId: 0
|
|
138
|
+
const mockThisTarget = {
|
|
139
|
+
tabId: 1,
|
|
140
|
+
frameId: 0,
|
|
141
|
+
page: "test",
|
|
142
|
+
};
|
|
143
|
+
vi.spyOn(thisTargetModule, "thisTarget", "get").mockReturnValue(mockThisTarget);
|
|
144
|
+
// No handler registered
|
|
145
|
+
// Import messenger and MessengerError after mocks are set up
|
|
146
|
+
const { messenger } = await import("./sender.js");
|
|
147
|
+
const { MessengerError } = await import("./shared.js");
|
|
148
|
+
// Test: Send message to same tab and frame without handler
|
|
149
|
+
try {
|
|
150
|
+
await messenger("testMethod", {}, { tabId: 1, frameId: 0 });
|
|
151
|
+
expect.fail("Should have thrown an error");
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
expect(error).toBeInstanceOf(MessengerError);
|
|
155
|
+
expect(error.message).toBe("No handler registered locally for testMethod");
|
|
156
|
+
}
|
|
157
|
+
// Verify: No message was sent via chrome.runtime.sendMessage
|
|
158
|
+
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webext-messenger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Browser Extension component messaging framework",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": "pixiebrix/webext-messenger",
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"one-event": "^4.3.0",
|
|
31
|
-
"p-retry": "^6.2.1",
|
|
32
31
|
"serialize-error": "^12.0.0",
|
|
33
32
|
"type-fest": "^5.0.1",
|
|
34
33
|
"webext-detect": "^5.3.2"
|
|
@@ -37,16 +36,16 @@
|
|
|
37
36
|
"packageExports": true
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
|
-
"@parcel/config-webextension": "^2.
|
|
39
|
+
"@parcel/config-webextension": "^2.16.0",
|
|
41
40
|
"@sindresorhus/tsconfig": "^8.0.1",
|
|
42
|
-
"@types/chrome": "^0.1.
|
|
41
|
+
"@types/chrome": "^0.1.22",
|
|
43
42
|
"@types/tape": "^5.8.1",
|
|
44
43
|
"buffer": "^6.0.3",
|
|
45
44
|
"eslint": "^8.57.0",
|
|
46
45
|
"eslint-config-pixiebrix": "^0.41.1",
|
|
47
46
|
"events": "^3.3.0",
|
|
48
47
|
"npm-run-all": "^4.1.5",
|
|
49
|
-
"parcel": "^2.
|
|
48
|
+
"parcel": "^2.16.0",
|
|
50
49
|
"path-browserify": "^1.0.1",
|
|
51
50
|
"process": "^0.11.10",
|
|
52
51
|
"stream-browserify": "^3.0.0",
|