browser_bug1058164.js (7534B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 SpecialPowers.pushPrefEnv({ 8 set: [["security.allow_eval_with_system_principal", true]], 9 }); 10 11 const PAGE = 12 "data:text/html,<html><body>A%20regular,%20everyday,%20normal%20page."; 13 14 let gListenerId = 0; 15 16 /** 17 * Adds a content event listener on the given browser element. NOTE: this test 18 * is checking the behavior of pageshow and pagehide as seen by frame scripts, 19 * so it is specifically implemented using the message message manager. 20 * Similar to BrowserTestUtils.waitForContentEvent, but the listener will fire 21 * until it is removed. A callable object is returned that, when called, removes 22 * the event listener. Note that this function works even if the browser's 23 * frameloader is swapped. 24 * 25 * @param {xul:browser} browser 26 * The browser element to listen for events in. 27 * @param {string} eventName 28 * Name of the event to listen to. 29 * @param {function} listener 30 * Function to call in parent process when event fires. 31 * Not passed any arguments. 32 * @param {function} checkFn [optional] 33 * Called with the Event object as argument, should return true if the 34 * event is the expected one, or false if it should be ignored and 35 * listening should continue. If not specified, the first event with 36 * the specified name resolves the returned promise. This is called 37 * within the content process and can have no closure environment. 38 * 39 * @returns function 40 * If called, the return value will remove the event listener. 41 */ 42 function addContentEventListenerWithMessageManager( 43 browser, 44 eventName, 45 listener, 46 checkFn 47 ) { 48 let id = gListenerId++; 49 let checkFnSource = checkFn 50 ? encodeURIComponent(escape(checkFn.toSource())) 51 : ""; 52 53 // To correctly handle frameloader swaps, we load a frame script 54 // into all tabs but ignore messages from the ones not related to 55 // |browser|. 56 57 /* eslint-disable no-eval */ 58 function frameScript(innerId, innerEventName, innerCheckFnSource) { 59 let innerCheckFn; 60 if (innerCheckFnSource) { 61 innerCheckFn = eval(`(() => (${unescape(innerCheckFnSource)}))()`); 62 } 63 64 function innerListener(event) { 65 if (innerCheckFn && !innerCheckFn(event)) { 66 return; 67 } 68 sendAsyncMessage("ContentEventListener:Run", innerId); 69 } 70 function removeListener(msg) { 71 if (msg.data == innerId) { 72 removeMessageListener("ContentEventListener:Remove", removeListener); 73 removeEventListener(innerEventName, innerListener); 74 } 75 } 76 addMessageListener("ContentEventListener:Remove", removeListener); 77 addEventListener(innerEventName, innerListener); 78 } 79 /* eslint-enable no-eval */ 80 81 let frameScriptSource = `data:,(${frameScript.toString()})(${id}, "${eventName}", 82 "${checkFnSource}")`; 83 84 let mm = Services.mm; 85 86 function runListener(msg) { 87 if (msg.data == id && msg.target == browser) { 88 listener(); 89 } 90 } 91 mm.addMessageListener("ContentEventListener:Run", runListener); 92 93 let needCleanup = true; 94 95 let unregisterFunction = function () { 96 if (!needCleanup) { 97 return; 98 } 99 needCleanup = false; 100 mm.removeMessageListener("ContentEventListener:Run", runListener); 101 mm.broadcastAsyncMessage("ContentEventListener:Remove", id); 102 mm.removeDelayedFrameScript(frameScriptSource); 103 }; 104 105 function cleanupObserver(subject, topic, data) { 106 if (subject == browser.messageManager) { 107 unregisterFunction(); 108 } 109 } 110 111 mm.loadFrameScript(frameScriptSource, true); 112 113 return unregisterFunction; 114 } 115 116 /** 117 * Returns a Promise that resolves when it sees a pageshow and 118 * pagehide events in a particular order, where each event must 119 * have the persisted property set to true. Will cause a test 120 * failure to be logged if it sees an event out of order. 121 * 122 * @param browser (<xul:browser>) 123 * The browser to expect the events from. 124 * @param expectedOrder (array) 125 * An array of strings describing what order the pageshow 126 * and pagehide events should be in. 127 * Example: 128 * ["pageshow", "pagehide", "pagehide", "pageshow"] 129 * @returns Promise 130 */ 131 function prepareForVisibilityEvents(browser, expectedOrder) { 132 return new Promise(resolve => { 133 let order = []; 134 135 let rmvHide, rmvShow; 136 137 let checkSatisfied = () => { 138 if (order.length < expectedOrder.length) { 139 // We're still waiting... 140 return; 141 } 142 rmvHide(); 143 rmvShow(); 144 145 for (let i = 0; i < expectedOrder.length; ++i) { 146 is(order[i], expectedOrder[i], "Got expected event"); 147 } 148 resolve(); 149 }; 150 151 let eventListener = type => { 152 order.push(type); 153 checkSatisfied(); 154 }; 155 156 let checkFn = e => e.persisted; 157 158 rmvHide = addContentEventListenerWithMessageManager( 159 browser, 160 "pagehide", 161 () => eventListener("pagehide"), 162 checkFn 163 ); 164 rmvShow = addContentEventListenerWithMessageManager( 165 browser, 166 "pageshow", 167 () => eventListener("pageshow"), 168 checkFn 169 ); 170 }); 171 } 172 173 /** 174 * Tests that frame scripts get pageshow / pagehide events when 175 * swapping browser frameloaders (which occurs when moving a tab 176 * into a different window). 177 */ 178 add_task(async function test_swap_frameloader_pagevisibility_events() { 179 // Disable window occlusion. Bug 1733955 180 if (navigator.platform.indexOf("Win") == 0) { 181 await SpecialPowers.pushPrefEnv({ 182 set: [["widget.windows.window_occlusion_tracking.enabled", false]], 183 }); 184 } 185 186 // Load a new tab that we'll tear out... 187 let tab = BrowserTestUtils.addTab(gBrowser, PAGE); 188 gBrowser.selectedTab = tab; 189 let firstBrowser = tab.linkedBrowser; 190 await BrowserTestUtils.browserLoaded(firstBrowser); 191 192 // Swap the browser out to a new window 193 let newWindow = gBrowser.replaceTabWithWindow(tab); 194 195 // We have to wait for the window to load so we can get the selected browser 196 // to listen to. 197 await BrowserTestUtils.waitForEvent(newWindow, "DOMContentLoaded"); 198 let newWindowBrowser = newWindow.gBrowser.selectedBrowser; 199 200 // Wait for the expected pagehide and pageshow events on the initial browser 201 await prepareForVisibilityEvents(newWindowBrowser, ["pagehide", "pageshow"]); 202 203 // Now let's send the browser back to the original window 204 205 // First, create a new, empty browser tab to replace the window with 206 let newTab = BrowserTestUtils.addTab(gBrowser); 207 gBrowser.selectedTab = newTab; 208 let emptyBrowser = newTab.linkedBrowser; 209 210 // Wait for that initial doc to be visible because if its pageshow hasn't 211 // happened we don't confuse it with the other expected events. 212 await ContentTask.spawn(emptyBrowser, null, async () => { 213 if (content.document.visibilityState === "hidden") { 214 info("waiting for hidden emptyBrowser to be visible"); 215 await ContentTaskUtils.waitForEvent( 216 content.document, 217 "visibilitychange", 218 {} 219 ); 220 } 221 }); 222 223 info("emptyBrowser is shown now."); 224 225 // The empty tab we just added show now fire a pagehide as its replaced, 226 // and a pageshow once the swap is finished. 227 let emptyBrowserPromise = prepareForVisibilityEvents(emptyBrowser, [ 228 "pagehide", 229 "pageshow", 230 ]); 231 232 gBrowser.swapBrowsersAndCloseOther(newTab, newWindow.gBrowser.selectedTab); 233 234 await emptyBrowserPromise; 235 236 gBrowser.removeTab(gBrowser.selectedTab); 237 });