browser_handle_command_retry.js (12820B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { isInitialDocument } = ChromeUtils.importESModule( 7 "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs" 8 ); 9 const { RootMessageHandler } = ChromeUtils.importESModule( 10 "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs" 11 ); 12 13 // We are forcing the actors to shutdown while queries are unresolved. 14 const { PromiseTestUtils } = ChromeUtils.importESModule( 15 "resource://testing-common/PromiseTestUtils.sys.mjs" 16 ); 17 PromiseTestUtils.allowMatchingRejectionsGlobally( 18 /Actor 'MessageHandlerFrame' destroyed before query 'MessageHandlerFrameParent:sendCommand' was resolved/ 19 ); 20 21 // The tests in this file assert the retry behavior for MessageHandler commands. 22 // We call "blocked" commands from resources/modules/windowglobal/retry.sys.mjs 23 // and then trigger reload and navigations to simulate AbortErrors and force the 24 // MessageHandler to retry the commands, when possible. 25 26 // If no retryOnAbort argument is provided, the framework will retry automatically. 27 add_task(async function test_default_retry() { 28 let tab = BrowserTestUtils.addTab( 29 gBrowser, 30 "https://example.com/document-builder.sjs?html=tab" 31 ); 32 33 let rootMessageHandler = createRootMessageHandler("session-id-retry"); 34 35 try { 36 const initialBrowsingContext = tab.linkedBrowser.browsingContext; 37 ok( 38 isInitialDocument(initialBrowsingContext), 39 "Module method needs to run in the initial document" 40 ); 41 42 info("Call a module method which will throw"); 43 const onBlockedOneTime = rootMessageHandler.handleCommand({ 44 moduleName: "retry", 45 commandName: "blockedOneTime", 46 destination: { 47 type: WindowGlobalMessageHandler.type, 48 id: initialBrowsingContext.id, 49 }, 50 }); 51 52 await onBlockedOneTime; 53 54 ok( 55 !isInitialDocument(tab.linkedBrowser.browsingContext), 56 "module method to be successful" 57 ); 58 } finally { 59 await cleanup(rootMessageHandler, tab, false); 60 } 61 62 // Now try again with a normal navigation which has to retry as well. 63 64 tab = BrowserTestUtils.addTab( 65 gBrowser, 66 "https://example.com/document-builder.sjs?html=tab" 67 ); 68 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 69 70 try { 71 rootMessageHandler = createRootMessageHandler("session-id-no-retry"); 72 const browsingContext = tab.linkedBrowser.browsingContext; 73 74 info("Call a module method which will throw"); 75 const onBlockedOneTime = rootMessageHandler.handleCommand({ 76 moduleName: "retry", 77 commandName: "blockedOneTime", 78 destination: { 79 type: WindowGlobalMessageHandler.type, 80 id: browsingContext.id, 81 }, 82 }); 83 84 // Reloading the tab will reject the pending query with an AbortError. 85 await BrowserTestUtils.reloadTab(tab); 86 87 await onBlockedOneTime; 88 } finally { 89 await cleanup(rootMessageHandler, tab); 90 } 91 }); 92 93 // Test that without retry behavior, a pending command rejects when the 94 // underlying JSWindowActor pair is destroyed. 95 add_task(async function test_forced_no_retry() { 96 const tab = BrowserTestUtils.addTab( 97 gBrowser, 98 "https://example.com/document-builder.sjs?html=tab" 99 ); 100 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 101 const browsingContext = tab.linkedBrowser.browsingContext; 102 103 const rootMessageHandler = createRootMessageHandler("session-id-no-retry"); 104 105 try { 106 info("Call a module method which will throw"); 107 const onBlockedOneTime = rootMessageHandler.handleCommand({ 108 moduleName: "retry", 109 commandName: "blockedOneTime", 110 destination: { 111 type: WindowGlobalMessageHandler.type, 112 id: browsingContext.id, 113 }, 114 retryOnAbort: false, 115 }); 116 117 // Reloading the tab will reject the pending query with an AbortError. 118 await BrowserTestUtils.reloadTab(tab); 119 120 await Assert.rejects( 121 onBlockedOneTime, 122 e => e.name == "DiscardedBrowsingContextError", 123 "Caught the expected error when reloading" 124 ); 125 } finally { 126 await cleanup(rootMessageHandler, tab); 127 } 128 }); 129 130 // Test that without retry behavior, a pending command rejects when the 131 // underlying browsing context is discarded. 132 add_task(async function test_forced_no_retry_cross_group() { 133 const tab = BrowserTestUtils.addTab( 134 gBrowser, 135 "https://example.com/document-builder.sjs?html=COM" + 136 // Attach an unload listener to prevent the page from going into bfcache, 137 // so that pending queries will be rejected with an AbortError. 138 "<script type='text/javascript'>window.onunload = function() {};</script>" 139 ); 140 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 141 const browsingContext = tab.linkedBrowser.browsingContext; 142 143 const rootMessageHandler = createRootMessageHandler("session-id-no-retry"); 144 145 try { 146 const onBlockedOneTime = rootMessageHandler.handleCommand({ 147 moduleName: "retry", 148 commandName: "blockedOneTime", 149 destination: { 150 type: WindowGlobalMessageHandler.type, 151 id: browsingContext.id, 152 }, 153 retryOnAbort: false, 154 }); 155 156 // This command will return when the old browsing context was discarded. 157 const onDiscarded = rootMessageHandler.handleCommand({ 158 moduleName: "retry", 159 commandName: "waitForDiscardedBrowsingContext", 160 destination: { 161 type: RootMessageHandler.type, 162 }, 163 params: { 164 browsingContext, 165 retryOnAbort: false, 166 }, 167 }); 168 169 ok( 170 !(await hasPromiseResolved(onBlockedOneTime)), 171 "blockedOneTime should not have resolved yet" 172 ); 173 ok( 174 !(await hasPromiseResolved(onDiscarded)), 175 "waitForDiscardedBrowsingContext should not have resolved yet" 176 ); 177 178 info( 179 "Navigate to example.net with COOP headers to destroy browsing context" 180 ); 181 await loadURL( 182 tab.linkedBrowser, 183 "https://example.net/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=NET" 184 ); 185 186 await Assert.rejects( 187 onBlockedOneTime, 188 e => e.name == "DiscardedBrowsingContextError", 189 "Caught the expected error when navigating" 190 ); 191 192 await Assert.rejects( 193 onDiscarded, 194 e => e.name == "DiscardedBrowsingContextError", 195 "Caught the expected error when navigating" 196 ); 197 } finally { 198 await cleanup(rootMessageHandler, tab); 199 } 200 }); 201 202 // Test various commands, which all need a different number of "retries" to 203 // succeed. Check that they only resolve when the expected number of "retries" 204 // was reached. For commands which require more "retries" than we allow, check 205 // that we still fail with an AbortError once all the attempts are consumed. 206 add_task(async function test_forced_retry() { 207 const tab = BrowserTestUtils.addTab( 208 gBrowser, 209 "https://example.com/document-builder.sjs?html=tab" 210 ); 211 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 212 const browsingContext = tab.linkedBrowser.browsingContext; 213 214 const rootMessageHandler = createRootMessageHandler("session-id-retry"); 215 216 try { 217 // This command will return if called twice. 218 const onBlockedOneTime = rootMessageHandler.handleCommand({ 219 moduleName: "retry", 220 commandName: "blockedOneTime", 221 destination: { 222 type: WindowGlobalMessageHandler.type, 223 id: browsingContext.id, 224 }, 225 params: { 226 foo: "bar", 227 }, 228 retryOnAbort: true, 229 }); 230 231 // This command will return if called three times. 232 const onBlockedTenTimes = rootMessageHandler.handleCommand({ 233 moduleName: "retry", 234 commandName: "blockedTenTimes", 235 destination: { 236 type: WindowGlobalMessageHandler.type, 237 id: browsingContext.id, 238 }, 239 params: { 240 foo: "baz", 241 }, 242 retryOnAbort: true, 243 }); 244 245 // This command will return if called twelve times, which is greater than the 246 // maximum amount of retries allowed. 247 const onBlockedElevenTimes = rootMessageHandler.handleCommand({ 248 moduleName: "retry", 249 commandName: "blockedElevenTimes", 250 destination: { 251 type: WindowGlobalMessageHandler.type, 252 id: browsingContext.id, 253 }, 254 retryOnAbort: true, 255 }); 256 257 info("Reload one time"); 258 await BrowserTestUtils.reloadTab(tab); 259 260 info("blockedOneTime should resolve on the first retry"); 261 let { callsToCommand, foo } = await onBlockedOneTime; 262 is( 263 callsToCommand, 264 2, 265 "The command was called twice (initial call + 1 retry)" 266 ); 267 is(foo, "bar", "The parameter was sent when the command was retried"); 268 269 // We already reloaded 1 time. Reload 9 more times to unblock blockedTenTimes. 270 for (let i = 2; i < 11; i++) { 271 info("blockedTenTimes/blockedElevenTimes should not have resolved yet"); 272 ok(!(await hasPromiseResolved(onBlockedTenTimes))); 273 ok(!(await hasPromiseResolved(onBlockedElevenTimes))); 274 275 info(`Reload the tab (time: ${i})`); 276 await BrowserTestUtils.reloadTab(tab); 277 } 278 279 info("blockedTenTimes should resolve on the 10th reload"); 280 ({ callsToCommand, foo } = await onBlockedTenTimes); 281 is( 282 callsToCommand, 283 11, 284 "The command was called 11 times (initial call + 10 retry)" 285 ); 286 is(foo, "baz", "The parameter was sent when the command was retried"); 287 288 info("Reload one more time"); 289 await BrowserTestUtils.reloadTab(tab); 290 291 info( 292 "The call to blockedElevenTimes now exceeds the maximum attempts allowed" 293 ); 294 await Assert.rejects( 295 onBlockedElevenTimes, 296 e => e.name == "DiscardedBrowsingContextError", 297 "Caught the expected error when reloading" 298 ); 299 } finally { 300 await cleanup(rootMessageHandler, tab); 301 } 302 }); 303 304 // Test cross-group navigations to check that the retry mechanism will 305 // transparently switch to the new Browsing Context created by the cross-group 306 // navigation. 307 add_task(async function test_retry_cross_group() { 308 const tab = BrowserTestUtils.addTab( 309 gBrowser, 310 "https://example.com/document-builder.sjs?html=COM" + 311 // Attach an unload listener to prevent the page from going into bfcache, 312 // so that pending queries will be rejected with an AbortError. 313 "<script type='text/javascript'>window.onunload = function() {};</script>" 314 ); 315 await BrowserTestUtils.browserLoaded(tab.linkedBrowser); 316 const browsingContext = tab.linkedBrowser.browsingContext; 317 318 const rootMessageHandler = createRootMessageHandler( 319 "session-id-retry-cross-group" 320 ); 321 322 try { 323 // This command hangs and only returns if the current domain is example.net. 324 // We send the command while on example.com, perform a series of reload and 325 // navigations, and the retry mechanism should allow onBlockedOnNetDomain to 326 // resolve. 327 const onBlockedOnNetDomain = rootMessageHandler.handleCommand({ 328 moduleName: "retry", 329 commandName: "blockedOnNetDomain", 330 destination: { 331 type: WindowGlobalMessageHandler.type, 332 id: browsingContext.id, 333 }, 334 params: { 335 foo: "bar", 336 }, 337 retryOnAbort: true, 338 }); 339 340 // This command will return when the old browsing context was discarded. 341 const onDiscarded = rootMessageHandler.handleCommand({ 342 moduleName: "retry", 343 commandName: "waitForDiscardedBrowsingContext", 344 destination: { 345 type: RootMessageHandler.type, 346 }, 347 params: { 348 browsingContext, 349 retryOnAbort: true, 350 }, 351 }); 352 353 info("Reload one time"); 354 await BrowserTestUtils.reloadTab(tab); 355 356 info("blockedOnNetDomain should not have resolved yet"); 357 ok(!(await hasPromiseResolved(onBlockedOnNetDomain))); 358 359 info("waitForDiscardedBrowsingContext should not have resolved yet"); 360 ok(!(await hasPromiseResolved(onDiscarded))); 361 362 info( 363 "Navigate to example.net with COOP headers to destroy browsing context" 364 ); 365 await loadURL( 366 tab.linkedBrowser, 367 "https://example.net/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=NET" 368 ); 369 370 info("blockedOnNetDomain should resolve now"); 371 let { foo } = await onBlockedOnNetDomain; 372 is(foo, "bar", "The parameter was sent when the command was retried"); 373 374 info("waitForDiscardedBrowsingContext should resolve now"); 375 await onDiscarded; 376 } finally { 377 await cleanup(rootMessageHandler, tab); 378 } 379 }); 380 381 async function cleanup(rootMessageHandler, tab) { 382 const browsingContext = tab.linkedBrowser.browsingContext; 383 // Cleanup global JSM state in the test module. 384 await rootMessageHandler.handleCommand({ 385 moduleName: "retry", 386 commandName: "cleanup", 387 destination: { 388 type: WindowGlobalMessageHandler.type, 389 id: browsingContext.id, 390 }, 391 }); 392 393 rootMessageHandler.destroy(); 394 gBrowser.removeTab(tab); 395 }