browser_devtools_serviceworker_interception.js (7955B)
1 "use strict"; 2 3 const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; 4 const emptyDoc = BASE_URI + "empty.html"; 5 const fakeDoc = BASE_URI + "fake.html"; 6 const helloDoc = BASE_URI + "hello.html"; 7 8 const CROSS_URI = "http://example.com/browser/dom/serviceworkers/test/"; 9 const crossRedirect = CROSS_URI + "redirect"; 10 const crossHelloDoc = CROSS_URI + "hello.html"; 11 12 const sw = BASE_URI + "fetch.js"; 13 14 async function checkObserver(aInput) { 15 let interceptedChannel = null; 16 17 // We always get two channels which receive the "http-on-stop-request" 18 // notification if the service worker hijacks the request and respondWith an 19 // another fetch. One is for the "outer" window request when the other one is 20 // for the "inner" service worker request. Therefore, distinguish them by the 21 // order. 22 let waitForSecondOnStopRequest = aInput.intercepted; 23 24 let promiseResolve; 25 26 function observer(aSubject) { 27 let channel = aSubject.QueryInterface(Ci.nsIChannel); 28 // Since we cannot make sure that the network event triggered by the fetch() 29 // in this testcase is the very next event processed by ObserverService, we 30 // have to wait until we catch the one we want. 31 if (!channel.URI.spec.includes(aInput.expectedURL)) { 32 return; 33 } 34 35 if (waitForSecondOnStopRequest) { 36 waitForSecondOnStopRequest = false; 37 return; 38 } 39 40 // Wait for the service worker to intercept the request if it's expected to 41 // be intercepted 42 if (aInput.intercepted && interceptedChannel === null) { 43 return; 44 } else if (interceptedChannel) { 45 ok( 46 aInput.intercepted, 47 "Service worker intercepted the channel as expected" 48 ); 49 } else { 50 ok(!aInput.intercepted, "The channel doesn't be intercepted"); 51 } 52 53 var tc = interceptedChannel 54 ? interceptedChannel.QueryInterface(Ci.nsITimedChannel) 55 : aSubject.QueryInterface(Ci.nsITimedChannel); 56 57 // Check service worker related timings. 58 var serviceWorkerTimings = [ 59 { 60 start: tc.launchServiceWorkerStartTime, 61 end: tc.launchServiceWorkerEndTime, 62 }, 63 { 64 start: tc.dispatchFetchEventStartTime, 65 end: tc.dispatchFetchEventEndTime, 66 }, 67 { start: tc.handleFetchEventStartTime, end: tc.handleFetchEventEndTime }, 68 ]; 69 if (!aInput.swPresent) { 70 serviceWorkerTimings.forEach(aTimings => { 71 is(aTimings.start, 0, "SW timings should be 0."); 72 is(aTimings.end, 0, "SW timings should be 0."); 73 }); 74 } 75 76 // Check network related timings. 77 var networkTimings = [ 78 tc.domainLookupStartTime, 79 tc.domainLookupEndTime, 80 tc.connectStartTime, 81 tc.connectEndTime, 82 tc.requestStartTime, 83 tc.responseStartTime, 84 tc.responseEndTime, 85 ]; 86 if (aInput.fetch) { 87 networkTimings.reduce((aPreviousTiming, aCurrentTiming) => { 88 Assert.lessOrEqual( 89 aPreviousTiming, 90 aCurrentTiming, 91 "Checking network timings" 92 ); 93 return aCurrentTiming; 94 }); 95 } else { 96 networkTimings.forEach(aTiming => 97 is(aTiming, 0, "Network timings should be 0.") 98 ); 99 } 100 101 interceptedChannel = null; 102 Services.obs.removeObserver(observer, topic); 103 promiseResolve(); 104 } 105 106 function addInterceptedChannel(aSubject) { 107 let channel = aSubject.QueryInterface(Ci.nsIChannel); 108 if (!channel.URI.spec.includes(aInput.url)) { 109 return; 110 } 111 112 // Hold the interceptedChannel until checking timing information. 113 // Note: It's a interceptedChannel in the type of httpChannel 114 interceptedChannel = channel; 115 Services.obs.removeObserver(addInterceptedChannel, topic_SW); 116 } 117 118 const topic = "http-on-stop-request"; 119 const topic_SW = "service-worker-synthesized-response"; 120 121 Services.obs.addObserver(observer, topic); 122 if (aInput.intercepted) { 123 Services.obs.addObserver(addInterceptedChannel, topic_SW); 124 } 125 126 await new Promise(resolve => { 127 promiseResolve = resolve; 128 }); 129 } 130 131 async function contentFetch(aURL) { 132 if (aURL.includes("redirect")) { 133 await content.window.fetch(aURL, { mode: "no-cors" }); 134 return; 135 } 136 await content.window.fetch(aURL); 137 } 138 139 // The observer topics are fired in the parent process in parent-intercept 140 // and the content process in child-intercept. This function will handle running 141 // the check in the correct process. Note that it will block until the observers 142 // are notified. 143 async function fetchAndCheckObservers( 144 aFetchBrowser, 145 aObserverBrowser, 146 aTestCase 147 ) { 148 let promise = null; 149 150 promise = checkObserver(aTestCase); 151 152 await SpecialPowers.spawn(aFetchBrowser, [aTestCase.url], contentFetch); 153 await promise; 154 } 155 156 async function registerSWAndWaitForActive(aServiceWorker) { 157 let swr = await content.navigator.serviceWorker.register(aServiceWorker, { 158 scope: "empty.html", 159 }); 160 await new Promise(resolve => { 161 let worker = swr.installing || swr.waiting || swr.active; 162 if (worker.state === "activated") { 163 resolve(); 164 return; 165 } 166 167 worker.addEventListener("statechange", () => { 168 if (worker.state === "activated") { 169 resolve(); 170 } 171 }); 172 }); 173 174 await new Promise(resolve => { 175 if (content.navigator.serviceWorker.controller) { 176 resolve(); 177 return; 178 } 179 180 content.navigator.serviceWorker.addEventListener( 181 "controllerchange", 182 resolve, 183 { once: true } 184 ); 185 }); 186 } 187 188 async function unregisterSW() { 189 let swr = await content.navigator.serviceWorker.getRegistration(); 190 swr.unregister(); 191 } 192 193 add_task(async function test_serivce_worker_interception() { 194 info("Setting the prefs to having e10s enabled"); 195 await SpecialPowers.pushPrefEnv({ 196 set: [ 197 // Make sure observer and testing function run in the same process 198 ["dom.ipc.processCount", 1], 199 ["dom.serviceWorkers.enabled", true], 200 ["dom.serviceWorkers.testing.enabled", true], 201 ], 202 }); 203 204 waitForExplicitFinish(); 205 206 info("Open the tab"); 207 let tab = BrowserTestUtils.addTab(gBrowser, emptyDoc); 208 let tabBrowser = gBrowser.getBrowserForTab(tab); 209 await BrowserTestUtils.browserLoaded(tabBrowser); 210 211 info("Open the tab for observing"); 212 let tab_observer = BrowserTestUtils.addTab(gBrowser, emptyDoc); 213 let tabBrowser_observer = gBrowser.getBrowserForTab(tab_observer); 214 await BrowserTestUtils.browserLoaded(tabBrowser_observer); 215 216 let testcases = [ 217 { 218 url: helloDoc, 219 expectedURL: helloDoc, 220 swPresent: false, 221 intercepted: false, 222 fetch: true, 223 }, 224 { 225 url: fakeDoc, 226 expectedURL: helloDoc, 227 swPresent: true, 228 intercepted: true, 229 fetch: false, // should use HTTP cache 230 }, 231 { 232 // Bypass http cache 233 url: helloDoc + "?ForBypassingHttpCache=" + Date.now(), 234 expectedURL: helloDoc, 235 swPresent: true, 236 intercepted: false, 237 fetch: true, 238 }, 239 { 240 // no-cors mode redirect to no-cors mode (trigger internal redirect) 241 url: crossRedirect + "?url=" + crossHelloDoc + "&mode=no-cors", 242 expectedURL: crossHelloDoc, 243 swPresent: true, 244 redirect: "hello.html", 245 intercepted: true, 246 fetch: true, 247 }, 248 ]; 249 250 info("Test 1: Verify simple fetch"); 251 await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[0]); 252 253 info("Register a service worker"); 254 await SpecialPowers.spawn(tabBrowser, [sw], registerSWAndWaitForActive); 255 256 info("Test 2: Verify simple hijack"); 257 await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[1]); 258 259 info("Test 3: Verify fetch without using http cache"); 260 await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[2]); 261 262 info("Test 4: make a internal redirect"); 263 await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[3]); 264 265 info("Clean up"); 266 await SpecialPowers.spawn(tabBrowser, [undefined], unregisterSW); 267 268 gBrowser.removeTab(tab); 269 gBrowser.removeTab(tab_observer); 270 });