utils.js (7224B)
1 function waitForState(worker, state, context) { 2 return new Promise(resolve => { 3 function onStateChange() { 4 if (worker.state === state) { 5 worker.removeEventListener("statechange", onStateChange); 6 resolve(context); 7 } 8 } 9 10 // First add an event listener, so we won't miss any change that happens 11 // before we check the current state. 12 worker.addEventListener("statechange", onStateChange); 13 14 // Now check if the worker is already in the desired state. 15 onStateChange(); 16 }); 17 } 18 19 /** 20 * Helper for browser tests to issue register calls from the content global and 21 * wait for the SW to progress to the active state, as most tests desire. 22 * From the ContentTask.spawn, use via 23 * `content.wrappedJSObject.registerAndWaitForActive`. 24 */ 25 async function registerAndWaitForActive(script, maybeScope) { 26 console.log("...calling register"); 27 let opts = undefined; 28 if (maybeScope) { 29 opts = { scope: maybeScope }; 30 } 31 const reg = await navigator.serviceWorker.register(script, opts); 32 // Unless registration resurrection happens, the SW should be in the 33 // installing slot. 34 console.log("...waiting for activation"); 35 await waitForState(reg.installing, "activated", reg); 36 console.log("...activated!"); 37 return reg; 38 } 39 40 /** 41 * Helper to create an iframe with the given URL and return the first 42 * postMessage payload received. This is intended to be used when creating 43 * cross-origin iframes. 44 * 45 * A promise will be returned that resolves with the payload of the postMessage 46 * call. 47 */ 48 function createIframeAndWaitForMessage(url) { 49 const iframe = document.createElement("iframe"); 50 document.body.appendChild(iframe); 51 return new Promise(resolve => { 52 window.addEventListener( 53 "message", 54 event => { 55 resolve(event.data); 56 }, 57 { once: true } 58 ); 59 iframe.src = url; 60 }); 61 } 62 63 /** 64 * Helper to create a nested iframe into the iframe created by 65 * createIframeAndWaitForMessage(). 66 * 67 * A promise will be returned that resolves with the payload of the postMessage 68 * call. 69 */ 70 function createNestedIframeAndWaitForMessage(url) { 71 const iframe = document.getElementsByTagName("iframe")[0]; 72 iframe.contentWindow.postMessage("create nested iframe", "*"); 73 return new Promise(resolve => { 74 window.addEventListener( 75 "message", 76 event => { 77 resolve(event.data); 78 }, 79 { once: true } 80 ); 81 }); 82 } 83 84 async function unregisterAll() { 85 const registrations = await navigator.serviceWorker.getRegistrations(); 86 for (const reg of registrations) { 87 await reg.unregister(); 88 } 89 } 90 91 /** 92 * Make a blob that contains random data and therefore shouldn't compress all 93 * that well. 94 */ 95 function makeRandomBlob(size) { 96 const arr = new Uint8Array(size); 97 let offset = 0; 98 /** 99 * getRandomValues will only provide a maximum of 64k of data at a time and 100 * will error if we ask for more, so using a while loop for get a random value 101 * which much larger than 64k. 102 * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#exceptions 103 */ 104 while (offset < size) { 105 const nextSize = Math.min(size - offset, 65536); 106 window.crypto.getRandomValues(new Uint8Array(arr.buffer, offset, nextSize)); 107 offset += nextSize; 108 } 109 return new Blob([arr], { type: "application/octet-stream" }); 110 } 111 112 async function fillStorage(cacheBytes, idbBytes) { 113 // ## Fill Cache API Storage 114 const cache = await caches.open("filler"); 115 await cache.put("fill", new Response(makeRandomBlob(cacheBytes))); 116 117 // ## Fill IDB 118 const storeName = "filler"; 119 let db = await new Promise((resolve, reject) => { 120 let openReq = indexedDB.open("filler", 1); 121 openReq.onerror = event => { 122 reject(event.target.error); 123 }; 124 openReq.onsuccess = event => { 125 resolve(event.target.result); 126 }; 127 openReq.onupgradeneeded = event => { 128 const useDB = event.target.result; 129 useDB.onerror = error => { 130 reject(error); 131 }; 132 const store = useDB.createObjectStore(storeName); 133 store.put({ blob: makeRandomBlob(idbBytes) }, "filler-blob"); 134 }; 135 }); 136 } 137 138 const messagingChannels = {}; 139 140 // This method should ideally be called during a setup phase of the test to make 141 // sure our BroadcastChannel is fully connected before anything that could cause 142 // something to send a message to the channel can happen. Because IPC ordering 143 // is more predictable these days (single channel per process pair), this is 144 // primarily an issue of: 145 // - Helping you not have to worry about there being a race here at all. 146 // - Potentially be able to refactor this to run on the WPT infrastructure in 147 // the future which likely cannot provide the same ordering guarantees. 148 function setupMessagingChannel(name) { 149 if (messagingChannels[name]) { 150 return; 151 } 152 153 messagingChannels[name] = new BroadcastChannel(name); 154 } 155 156 function waitForBroadcastMessage(channelName, messageToWaitFor) { 157 if (!messagingChannels[channelName]) { 158 throw new Error(`You forgot to call setupMessagingChannel(${channelName})`); 159 } 160 return new Promise((resolve, reject) => { 161 const channel = messagingChannels[channelName]; 162 const listener = evt => { 163 // Add `--setpref="devtools.console.stdout.content=true"` to your mach 164 // invocation to get this to stdout for extra debugging. 165 console.log("Helper seeing message", evt.data, "on channel", channelName); 166 if (evt.data === messageToWaitFor) { 167 resolve(); 168 channel.removeEventListener("message", listener); 169 } else if (evt.data?.error) { 170 // Anything reporting an error means we should fail fast. 171 reject(evt.data); 172 channel.removeEventListener("message", listener); 173 } 174 }; 175 channel.addEventListener("message", listener); 176 }); 177 } 178 179 async function postMessageScopeAndWaitFor( 180 channelName, 181 scope, 182 messageToSend, 183 messageToWaitFor 184 ) { 185 // This will throw for us if the channel does not exist. 186 const waitPromise = waitForBroadcastMessage(channelName, messageToWaitFor); 187 const channel = messagingChannels[channelName]; 188 189 const reg = await navigator.serviceWorker.getRegistration(scope); 190 if (!reg) { 191 throw new Error(`Unable to find registration for scope: ${scope}`); 192 } 193 if (!reg.active) { 194 throw new Error(`There is no active SW on the reg for scope: ${scope}`); 195 } 196 reg.active.postMessage(messageToSend); 197 198 await waitPromise; 199 } 200 201 async function broadcastAndWaitFor( 202 channelName, 203 messageToBroadcast, 204 messageToWaitFor 205 ) { 206 // This will throw for us if the channel does not exist. 207 const waitPromise = waitForBroadcastMessage(channelName, messageToWaitFor); 208 const channel = messagingChannels[channelName]; 209 210 channel.postMessage(messageToBroadcast); 211 212 await waitPromise; 213 } 214 215 async function updateScopeAndWaitFor(channelName, scope, messageToWaitFor) { 216 // This will throw for us if the channel does not exist. 217 const waitPromise = waitForBroadcastMessage(channelName, messageToWaitFor); 218 const channel = messagingChannels[channelName]; 219 220 const reg = await navigator.serviceWorker.getRegistration(scope); 221 if (!reg) { 222 throw new Error(`Unable to find registration for scope: ${scope}`); 223 } 224 reg.update(); 225 226 await waitPromise; 227 }