test-helpers.sub.js (10485B)
1 // Adapter for testharness.js-style tests with Service Workers 2 3 /** 4 * @param options an object that represents RegistrationOptions except for scope. 5 * @param options.type a WorkerType. 6 * @param options.updateViaCache a ServiceWorkerUpdateViaCache. 7 * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions 8 */ 9 function service_worker_unregister_and_register(test, url, scope, options) { 10 if (!scope || scope.length == 0) 11 return Promise.reject(new Error('tests must define a scope')); 12 13 if (options && options.scope) 14 return Promise.reject(new Error('scope must not be passed in options')); 15 16 options = Object.assign({ scope: scope }, options); 17 return service_worker_unregister(test, scope) 18 .then(function() { 19 return navigator.serviceWorker.register(url, options); 20 }) 21 .catch(unreached_rejection(test, 22 'unregister and register should not fail')); 23 } 24 25 // This unregisters the registration that precisely matches scope. Use this 26 // when unregistering by scope. If no registration is found, it just resolves. 27 function service_worker_unregister(test, scope) { 28 var absoluteScope = (new URL(scope, window.location).href); 29 return navigator.serviceWorker.getRegistration(scope) 30 .then(function(registration) { 31 if (registration && registration.scope === absoluteScope) 32 return registration.unregister(); 33 }) 34 .catch(unreached_rejection(test, 'unregister should not fail')); 35 } 36 37 function service_worker_unregister_and_done(test, scope) { 38 return service_worker_unregister(test, scope) 39 .then(test.done.bind(test)); 40 } 41 42 function unreached_fulfillment(test, prefix) { 43 return test.step_func(function(result) { 44 var error_prefix = prefix || 'unexpected fulfillment'; 45 assert_unreached(error_prefix + ': ' + result); 46 }); 47 } 48 49 // Rejection-specific helper that provides more details 50 function unreached_rejection(test, prefix) { 51 return test.step_func(function(error) { 52 var reason = error.message || error.name || error; 53 var error_prefix = prefix || 'unexpected rejection'; 54 assert_unreached(error_prefix + ': ' + reason); 55 }); 56 } 57 58 /** 59 * Adds an iframe to the document and returns a promise that resolves to the 60 * iframe when it finishes loading. The caller is responsible for removing the 61 * iframe later if needed. 62 * 63 * @param {string} url 64 * @returns {HTMLIFrameElement} 65 */ 66 function with_iframe(url) { 67 return new Promise(function(resolve) { 68 var frame = document.createElement('iframe'); 69 frame.className = 'test-iframe'; 70 frame.src = url; 71 frame.onload = function() { resolve(frame); }; 72 document.body.appendChild(frame); 73 }); 74 } 75 76 function normalizeURL(url) { 77 return new URL(url, self.location).toString().replace(/#.*$/, ''); 78 } 79 80 function wait_for_update(test, registration) { 81 if (!registration || registration.unregister == undefined) { 82 return Promise.reject(new Error( 83 'wait_for_update must be passed a ServiceWorkerRegistration')); 84 } 85 86 return new Promise(test.step_func(function(resolve) { 87 var handler = test.step_func(function() { 88 registration.removeEventListener('updatefound', handler); 89 resolve(registration.installing); 90 }); 91 registration.addEventListener('updatefound', handler); 92 })); 93 } 94 95 // Return true if |state_a| is more advanced than |state_b|. 96 function is_state_advanced(state_a, state_b) { 97 if (state_b === 'installing') { 98 switch (state_a) { 99 case 'installed': 100 case 'activating': 101 case 'activated': 102 case 'redundant': 103 return true; 104 } 105 } 106 107 if (state_b === 'installed') { 108 switch (state_a) { 109 case 'activating': 110 case 'activated': 111 case 'redundant': 112 return true; 113 } 114 } 115 116 if (state_b === 'activating') { 117 switch (state_a) { 118 case 'activated': 119 case 'redundant': 120 return true; 121 } 122 } 123 124 if (state_b === 'activated') { 125 switch (state_a) { 126 case 'redundant': 127 return true; 128 } 129 } 130 return false; 131 } 132 133 function wait_for_state(test, worker, state) { 134 if (!worker || worker.state == undefined) { 135 return Promise.reject(new Error( 136 'wait_for_state needs a ServiceWorker object to be passed.')); 137 } 138 if (worker.state === state) 139 return Promise.resolve(state); 140 141 if (is_state_advanced(worker.state, state)) { 142 return Promise.reject(new Error( 143 `Waiting for ${state} but the worker is already ${worker.state}.`)); 144 } 145 return new Promise(test.step_func(function(resolve, reject) { 146 worker.addEventListener('statechange', test.step_func(function() { 147 if (worker.state === state) 148 resolve(state); 149 150 if (is_state_advanced(worker.state, state)) { 151 reject(new Error( 152 `The state of the worker becomes ${worker.state} while waiting` + 153 `for ${state}.`)); 154 } 155 })); 156 })); 157 } 158 159 // Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url| 160 // is the service worker script URL. This function: 161 // - Instantiates a new test with the description specified in |description|. 162 // The test will succeed if the specified service worker can be successfully 163 // registered and installed. 164 // - Creates a new ServiceWorker registration with a scope unique to the current 165 // document URL. Note that this doesn't allow more than one 166 // service_worker_test() to be run from the same document. 167 // - Waits for the new worker to begin installing. 168 // - Imports tests results from tests running inside the ServiceWorker. 169 function service_worker_test(url, description) { 170 // If the document URL is https://example.com/document and the script URL is 171 // https://example.com/script/worker.js, then the scope would be 172 // https://example.com/script/scope/document. 173 var scope = new URL('scope' + window.location.pathname, 174 new URL(url, window.location)).toString(); 175 promise_test(function(test) { 176 return service_worker_unregister_and_register(test, url, scope) 177 .then(function(registration) { 178 add_completion_callback(function() { 179 registration.unregister(); 180 }); 181 return wait_for_update(test, registration) 182 .then(function(worker) { 183 return fetch_tests_from_worker(worker); 184 }); 185 }); 186 }, description); 187 } 188 189 function base_path() { 190 return location.pathname.replace(/\/[^\/]*$/, '/'); 191 } 192 193 function test_login(test, origin, username, password, cookie) { 194 return new Promise(function(resolve, reject) { 195 with_iframe( 196 origin + base_path() + 197 'resources/fetch-access-control-login.html') 198 .then(test.step_func(function(frame) { 199 var channel = new MessageChannel(); 200 channel.port1.onmessage = test.step_func(function() { 201 frame.remove(); 202 resolve(); 203 }); 204 frame.contentWindow.postMessage( 205 {username: username, password: password, cookie: cookie}, 206 origin, [channel.port2]); 207 })); 208 }); 209 } 210 211 function test_websocket(test, frame, url) { 212 return new Promise(function(resolve, reject) { 213 var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']); 214 var openCalled = false; 215 ws.addEventListener('open', test.step_func(function(e) { 216 assert_equals(ws.readyState, 1, "The WebSocket should be open"); 217 openCalled = true; 218 ws.close(); 219 }), true); 220 221 ws.addEventListener('close', test.step_func(function(e) { 222 assert_true(openCalled, "The WebSocket should be closed after being opened"); 223 resolve(); 224 }), true); 225 226 ws.addEventListener('error', reject); 227 }); 228 } 229 230 function login_https(test) { 231 var host_info = get_host_info(); 232 return test_login(test, host_info.HTTPS_REMOTE_ORIGIN, 233 'username1s', 'password1s', 'cookie1') 234 .then(function() { 235 return test_login(test, host_info.HTTPS_ORIGIN, 236 'username2s', 'password2s', 'cookie2'); 237 }); 238 } 239 240 function websocket(test, frame) { 241 return test_websocket(test, frame, get_websocket_url()); 242 } 243 244 function get_websocket_url() { 245 return 'wss://{{host}}:{{ports[wss][0]}}/echo'; 246 } 247 248 // The navigator.serviceWorker.register() method guarantees that the newly 249 // installing worker is available as registration.installing when its promise 250 // resolves. However some tests test installation using a <link> element where 251 // it is possible for the installing worker to have already become the waiting 252 // or active worker. So this method is used to get the newest worker when these 253 // tests need access to the ServiceWorker itself. 254 function get_newest_worker(registration) { 255 if (registration.installing) 256 return registration.installing; 257 if (registration.waiting) 258 return registration.waiting; 259 if (registration.active) 260 return registration.active; 261 } 262 263 function register_using_link(script, options) { 264 var scope = options.scope; 265 var link = document.createElement('link'); 266 link.setAttribute('rel', 'serviceworker'); 267 link.setAttribute('href', script); 268 link.setAttribute('scope', scope); 269 document.getElementsByTagName('head')[0].appendChild(link); 270 return new Promise(function(resolve, reject) { 271 link.onload = resolve; 272 link.onerror = reject; 273 }) 274 .then(() => navigator.serviceWorker.getRegistration(scope)); 275 } 276 277 function with_sandboxed_iframe(url, sandbox) { 278 return new Promise(function(resolve) { 279 var frame = document.createElement('iframe'); 280 frame.sandbox = sandbox; 281 frame.src = url; 282 frame.onload = function() { resolve(frame); }; 283 document.body.appendChild(frame); 284 }); 285 } 286 287 // Registers, waits for activation, then unregisters on a sample scope. 288 // 289 // This can be used to wait for a period of time needed to register, 290 // activate, and then unregister a service worker. When checking that 291 // certain behavior does *NOT* happen, this is preferable to using an 292 // arbitrary delay. 293 async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) { 294 const script = '/service-workers/service-worker/resources/empty-worker.js'; 295 const scope = 'resources/there/is/no/there/there?' + Date.now(); 296 let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope }); 297 await wait_for_state(t, registration.installing, 'activated'); 298 await registration.unregister(); 299 }