test_ext_native_messaging_geckoview.js (12600B)
1 "use strict"; 2 3 const server = createHttpServer({ hosts: ["example.com"] }); 4 server.registerPathHandler("/", (request, response) => { 5 response.setStatusLine(request.httpVersion, 200, "OK"); 6 response.setHeader("Content-Type", "text/html; charset=utf-8", false); 7 response.write("<!DOCTYPE html><html></html>"); 8 }); 9 10 ChromeUtils.defineESModuleGetters(this, { 11 GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", 12 }); 13 14 // Save reference to original implementations to restore later. 15 const { sendMessage, onConnect } = GeckoViewConnection.prototype; 16 add_setup(async () => { 17 // This file replaces the implementation of GeckoViewConnection; 18 // make sure that it is restored upon test completion. 19 registerCleanupFunction(() => { 20 GeckoViewConnection.prototype.sendMessage = sendMessage; 21 GeckoViewConnection.prototype.onConnect = onConnect; 22 }); 23 }); 24 25 // Mock the embedder communication port 26 class EmbedderPort { 27 constructor(portId, messenger) { 28 this.id = portId; 29 this.messenger = messenger; 30 } 31 close() { 32 Assert.ok(false, "close not expected to be called"); 33 } 34 onPortDisconnect() { 35 Assert.ok(false, "onPortDisconnect not expected to be called"); 36 } 37 onPortMessage() { 38 Assert.ok(false, "onPortMessage not expected to be called"); 39 } 40 triggerPortDisconnect() { 41 this.messenger.sendPortDisconnect(this.id); 42 } 43 } 44 45 function stubConnectNative() { 46 let port; 47 const firstCallPromise = new Promise(resolve => { 48 let callCount = 0; 49 GeckoViewConnection.prototype.onConnect = (portId, messenger) => { 50 Assert.equal(++callCount, 1, "onConnect called once"); 51 port = new EmbedderPort(portId, messenger); 52 resolve(); 53 return port; 54 }; 55 }); 56 const triggerPortDisconnect = () => { 57 if (!port) { 58 Assert.ok(false, "Undefined port, connection must be established first"); 59 } 60 port.triggerPortDisconnect(); 61 }; 62 const restore = () => { 63 GeckoViewConnection.prototype.onConnect = onConnect; 64 }; 65 return { firstCallPromise, triggerPortDisconnect, restore }; 66 } 67 68 function stubSendNativeMessage() { 69 let sendResponse; 70 const returnPromise = new Promise(resolve => { 71 sendResponse = resolve; 72 }); 73 const firstCallPromise = new Promise(resolve => { 74 let callCount = 0; 75 GeckoViewConnection.prototype.sendMessage = data => { 76 Assert.equal(++callCount, 1, "sendMessage called once"); 77 resolve(data); 78 return returnPromise; 79 }; 80 }); 81 const restore = () => { 82 GeckoViewConnection.prototype.sendMessage = sendMessage; 83 }; 84 return { firstCallPromise, sendResponse, restore }; 85 } 86 87 function promiseExtensionEvent(wrapper, event) { 88 return new Promise(resolve => { 89 wrapper.extension.once(event, (...args) => resolve(args)); 90 }); 91 } 92 93 // verify that when background sends a native message, 94 // the background will not be terminated to allow native messaging 95 add_task(async function test_sendNativeMessage_event_page() { 96 const extension = ExtensionTestUtils.loadExtension({ 97 isPrivileged: true, 98 manifest: { 99 permissions: ["geckoViewAddons", "nativeMessaging"], 100 background: { persistent: false }, 101 }, 102 async background() { 103 const res = await browser.runtime.sendNativeMessage("fake", "msg"); 104 browser.test.assertEq("myResp", res, "expected response"); 105 browser.test.sendMessage("done"); 106 browser.runtime.onSuspend.addListener(async () => { 107 browser.test.assertFail("unexpected onSuspend"); 108 }); 109 }, 110 }); 111 112 const stub = stubSendNativeMessage(); 113 await extension.startup(); 114 info("Wait for sendNativeMessage to be received"); 115 Assert.equal( 116 (await stub.firstCallPromise).deserialize({}), 117 "msg", 118 "expected message" 119 ); 120 121 info("Trigger background script idle timeout and expect to be reset"); 122 const promiseResetIdle = promiseExtensionEvent( 123 extension, 124 "background-script-reset-idle" 125 ); 126 await extension.terminateBackground({ expectStopped: false }); 127 info("Wait for 'background-script-reset-idle' event to be emitted"); 128 await promiseResetIdle; 129 130 stub.sendResponse("myResp"); 131 132 info("Wait for extension to verify sendNativeMessage response"); 133 await extension.awaitMessage("done"); 134 await extension.unload(); 135 136 stub.restore(); 137 }); 138 139 // verify that when an extension tab sends a native message, 140 // the background will terminate as expected 141 add_task(async function test_sendNativeMessage_tab() { 142 const extension = ExtensionTestUtils.loadExtension({ 143 isPrivileged: true, 144 manifest: { 145 permissions: ["geckoViewAddons", "nativeMessaging"], 146 background: { persistent: false }, 147 }, 148 async background() { 149 browser.runtime.onSuspend.addListener(async () => { 150 browser.test.sendMessage("onSuspend_called"); 151 }); 152 }, 153 files: { 154 "tab.html": ` 155 <!DOCTYPE html><meta charset="utf-8"> 156 <script src="tab.js"></script> 157 `, 158 "tab.js": async () => { 159 const res = await browser.runtime.sendNativeMessage("fake", "msg"); 160 browser.test.assertEq("myResp", res, "expected response"); 161 browser.test.sendMessage("content_done"); 162 }, 163 }, 164 }); 165 166 const stub = stubSendNativeMessage(); 167 await extension.startup(); 168 169 const tab = await ExtensionTestUtils.loadContentPage( 170 `moz-extension://${extension.uuid}/tab.html?tab`, 171 { extension } 172 ); 173 174 info("Wait for sendNativeMessage to be received"); 175 Assert.equal( 176 (await stub.firstCallPromise).deserialize({}), 177 "msg", 178 "expected message" 179 ); 180 181 info("Terminate extension"); 182 await extension.terminateBackground(); 183 await extension.awaitMessage("onSuspend_called"); 184 185 stub.sendResponse("myResp"); 186 187 info("Wait for extension to verify sendNativeMessage response"); 188 await extension.awaitMessage("content_done"); 189 await tab.close(); 190 await extension.unload(); 191 192 stub.restore(); 193 }); 194 195 // verify that when a content script sends a native message, 196 // the background will terminate as expected 197 add_task(async function test_sendNativeMessage_content_script() { 198 const extension = ExtensionTestUtils.loadExtension({ 199 isPrivileged: true, 200 manifest: { 201 permissions: [ 202 "geckoViewAddons", 203 "nativeMessaging", 204 "nativeMessagingFromContent", 205 ], 206 background: { persistent: false }, 207 content_scripts: [ 208 { 209 run_at: "document_end", 210 js: ["test.js"], 211 matches: ["http://example.com/"], 212 }, 213 ], 214 }, 215 files: { 216 "test.js": async () => { 217 const res = await browser.runtime.sendNativeMessage("fake", "msg"); 218 browser.test.assertEq("myResp", res, "expected response"); 219 browser.test.sendMessage("content_done"); 220 }, 221 }, 222 async background() { 223 browser.runtime.onSuspend.addListener(async () => { 224 browser.test.sendMessage("onSuspend_called"); 225 }); 226 }, 227 }); 228 229 const stub = stubSendNativeMessage(); 230 await extension.startup(); 231 232 info("Load content page"); 233 const page = await ExtensionTestUtils.loadContentPage("http://example.com/"); 234 235 info("Wait for message from extension"); 236 Assert.equal( 237 (await stub.firstCallPromise).deserialize({}), 238 "msg", 239 "expected message" 240 ); 241 242 info("Terminate extension"); 243 await extension.terminateBackground(); 244 await extension.awaitMessage("onSuspend_called"); 245 246 stub.sendResponse("myResp"); 247 248 info("Wait for extension to verify sendNativeMessage response"); 249 await extension.awaitMessage("content_done"); 250 await page.close(); 251 await extension.unload(); 252 253 stub.restore(); 254 }); 255 256 // verify that when native messaging ports are open, the background will not be terminated 257 // and once the ports disconnect, onSuspend can be called 258 add_task(async function test_connectNative_event_page() { 259 const extension = ExtensionTestUtils.loadExtension({ 260 isPrivileged: true, 261 manifest: { 262 permissions: ["geckoViewAddons", "nativeMessaging"], 263 background: { persistent: false }, 264 }, 265 async background() { 266 const port = browser.runtime.connectNative("test"); 267 port.onDisconnect.addListener(() => { 268 browser.test.assertEq( 269 null, 270 port.error, 271 "port should be disconnected without errors" 272 ); 273 browser.test.sendMessage("port_disconnected"); 274 }); 275 276 browser.runtime.onSuspend.addListener(async () => { 277 browser.test.sendMessage("onSuspend_called"); 278 }); 279 }, 280 }); 281 282 const stub = stubConnectNative(); 283 await extension.startup(); 284 info("Waiting for connectNative request"); 285 await stub.firstCallPromise; 286 287 info("Trigger background script idle timeout and expect to be reset"); 288 const promiseResetIdle = promiseExtensionEvent( 289 extension, 290 "background-script-reset-idle" 291 ); 292 293 await extension.terminateBackground({ expectStopped: false }); 294 info("Wait for 'background-script-reset-idle' event to be emitted"); 295 await promiseResetIdle; 296 297 info("Trigger port disconnect, terminate background, and expect onSuspend()"); 298 stub.triggerPortDisconnect(); 299 await extension.awaitMessage("port_disconnected"); 300 301 info("Terminate extension"); 302 await extension.terminateBackground(); 303 await extension.awaitMessage("onSuspend_called"); 304 305 await extension.unload(); 306 stub.restore(); 307 }); 308 309 // verify that when an extension tab opens native messaging ports, 310 // the background will terminate as expected 311 add_task(async function test_connectNative_tab() { 312 const extension = ExtensionTestUtils.loadExtension({ 313 isPrivileged: true, 314 manifest: { 315 permissions: ["geckoViewAddons", "nativeMessaging"], 316 background: { persistent: false }, 317 }, 318 async background() { 319 browser.runtime.onSuspend.addListener(async () => { 320 browser.test.sendMessage("onSuspend_called"); 321 }); 322 }, 323 files: { 324 "tab.html": ` 325 <!DOCTYPE html><meta charset="utf-8"> 326 <script src="tab.js"></script> 327 `, 328 "tab.js": async () => { 329 const port = browser.runtime.connectNative("test"); 330 port.onDisconnect.addListener(() => { 331 browser.test.assertEq( 332 null, 333 port.error, 334 "port should be disconnected without errors" 335 ); 336 browser.test.sendMessage("port_disconnected"); 337 }); 338 browser.test.sendMessage("content_done"); 339 }, 340 }, 341 }); 342 343 const stub = stubConnectNative(); 344 await extension.startup(); 345 346 const tab = await ExtensionTestUtils.loadContentPage( 347 `moz-extension://${extension.uuid}/tab.html?tab`, 348 { extension } 349 ); 350 await extension.awaitMessage("content_done"); 351 await stub.firstCallPromise; 352 353 info("Terminate extension"); 354 await extension.terminateBackground(); 355 await extension.awaitMessage("onSuspend_called"); 356 357 stub.triggerPortDisconnect(); 358 await extension.awaitMessage("port_disconnected"); 359 await tab.close(); 360 await extension.unload(); 361 362 stub.restore(); 363 }); 364 365 // verify that when a content script opens native messaging ports, 366 // the background will terminate as expected 367 add_task(async function test_connectNative_content_script() { 368 const extension = ExtensionTestUtils.loadExtension({ 369 isPrivileged: true, 370 manifest: { 371 permissions: [ 372 "geckoViewAddons", 373 "nativeMessaging", 374 "nativeMessagingFromContent", 375 ], 376 background: { persistent: false }, 377 content_scripts: [ 378 { 379 run_at: "document_end", 380 js: ["test.js"], 381 matches: ["http://example.com/"], 382 }, 383 ], 384 }, 385 files: { 386 "test.js": async () => { 387 const port = browser.runtime.connectNative("test"); 388 port.onDisconnect.addListener(() => { 389 browser.test.assertEq( 390 null, 391 port.error, 392 "port should be disconnected without errors" 393 ); 394 browser.test.sendMessage("port_disconnected"); 395 }); 396 browser.test.sendMessage("content_done"); 397 }, 398 }, 399 async background() { 400 browser.runtime.onSuspend.addListener(async () => { 401 browser.test.sendMessage("onSuspend_called"); 402 }); 403 }, 404 }); 405 406 const stub = stubConnectNative(); 407 await extension.startup(); 408 409 info("Load content page"); 410 const page = await ExtensionTestUtils.loadContentPage("http://example.com/"); 411 await extension.awaitMessage("content_done"); 412 await stub.firstCallPromise; 413 414 info("Terminate extension"); 415 await extension.terminateBackground(); 416 await extension.awaitMessage("onSuspend_called"); 417 418 stub.triggerPortDisconnect(); 419 await extension.awaitMessage("port_disconnected"); 420 await page.close(); 421 await extension.unload(); 422 423 stub.restore(); 424 });