testHelpers.js (8290B)
1 // META: script=/resources/testharness.js 2 3 /** 4 * Awaitable which returns when error occurs or the test sends back its name. 5 * @param {*} testId - name to distinguish the test 6 * @returns promise which resolves if test sends back its name, otherwise rejects. 7 */ 8 const waitOutcomesForNames = async nameList => { 9 return Promise.allSettled( 10 nameList.map( 11 name => 12 new Promise((resolve, reject) => { 13 const bc = new BroadcastChannel(name); 14 bc.onmessage = e => { 15 try { 16 if (e.data.message.startsWith(name)) { 17 resolve("ok"); 18 } else { 19 reject(JSON.stringify(e.data)); 20 } 21 } catch (err) { 22 reject(err.message); 23 } 24 }; 25 }) 26 ) 27 ); 28 }; 29 30 const expectNamesForTestWindow = (names, windowPath) => { 31 return () => { 32 return new Promise((resolve, reject) => { 33 try { 34 waitOutcomesForNames(names).then(res => { 35 try { 36 if ( 37 res.every( 38 elem => elem.status === "fulfilled" && elem.value == "ok" 39 ) 40 ) { 41 resolve(); 42 } else { 43 reject(res.find(elem => elem.status === "rejected").reason); 44 } 45 } catch (err) { 46 reject(err.message); 47 } 48 }); 49 window.open(windowPath); 50 } catch (err) { 51 reject(err.message); 52 } 53 }); 54 }; 55 }; 56 57 /** 58 * Tests whether the event is an automated summary from a known window. 59 * 60 * @param {*} e - event 61 * @returns true if event seems to be an automated summary with a recognized name, otherwise false 62 */ 63 const isSummary = e => { 64 const wrappers = ["Write wrapper", "Read wrapper"]; 65 const hasName = 66 !!e.data.tests && 1 === e.data.tests.length && !!e.data.tests[0].name; 67 if (!hasName) { 68 return false; 69 } 70 return wrappers.includes(e.data.tests[0].name); 71 }; 72 73 /** 74 * Returns expected error message when access to a storage API is denied. 75 * 76 * @param {*} api Shorthand for the storage API type to be tested 77 * @returns Expected error message when access is denied. 78 */ 79 const getAccessErrorForAPI = (api, testId) => { 80 if (api === "IDB") { 81 const storeName = "testObjectStore" + testId; 82 return ( 83 "IDBDatabase.transaction: '" + 84 storeName + 85 "' is not a known object store name" 86 ); 87 } else if (api === "FS") { 88 return "Entry not found"; //"Security error when calling GetDirectory"; 89 } 90 throw Error("Unknown API!"); 91 }; 92 93 const childListeners = new Map(); 94 const readPromises = new Map(); 95 const readListeners = new Map(); 96 97 function createMotherListener( 98 defaultHandler = e => { 99 childListeners.values().next().value(e); 100 const msg = 101 "Unexpectedly, default handler called: " + JSON.stringify(e.data); 102 console.log(msg); 103 } 104 ) { 105 // The core 'messageHub' listener that checks for a matching child ID 106 function messageHubListener(event) { 107 console.log("We got message with data " + JSON.stringify(event.data)); 108 const id = event.data.id; 109 if (id) { 110 if (event.data.message == "read loaded") { 111 if (readListeners.has(id)) { 112 if (!readPromises.has(id)) { 113 throw new Error("Read window lifecycle issue"); 114 } 115 readListeners.get(id)(event); 116 } else { 117 readPromises[id] = new Promise(resolve => resolve()); 118 } 119 return; 120 } else if (childListeners.has(id)) { 121 childListeners.get(id)(event); 122 return; 123 } 124 } 125 defaultHandler(event); 126 } 127 128 // Start listening to window messages right away 129 window.addEventListener("message", messageHubListener); 130 131 // Return an API to register new child listeners 132 return { 133 registerWindow(t, testId, testAPI, expectation, setup) { 134 if (childListeners.has(testId)) { 135 throw new Error(`Window ID "${testId}" is already registered.`); 136 } 137 const handler = getWindowTestListener( 138 t, 139 testAPI, 140 testId, 141 expectation, 142 "window", 143 setup 144 ); 145 childListeners.set(testId, handler); 146 console.log("Registered window id " + testId); 147 }, 148 registerWorker(t, testId, testAPI, expectation, setup) { 149 if (childListeners.has(testId)) { 150 throw new Error(`Worker ID "${testId}" is already registered.`); 151 } 152 const handler = getWindowTestListener( 153 t, 154 testAPI, 155 testId, 156 expectation, 157 "worker", 158 setup 159 ); 160 childListeners.set(testId, handler); 161 console.log("Registered worker id " + testId); 162 }, 163 registerReadWindow(testId) { 164 if (readPromises.has(testId)) { 165 return; 166 } 167 readPromises.set( 168 testId, 169 new Promise((resolve, reject) => { 170 readListeners.set(testId, e => { 171 if (e.data.id != testId) { 172 reject("Expected read id " + testId + ", actual " + e.data.id); 173 } 174 resolve(); 175 }); 176 }) 177 ); 178 console.log("Registered read window id " + testId); 179 }, 180 async getReadWindow(testId) { 181 if (!readPromises.has(testId)) { 182 throw new Error("Read window lifecycle issue"); 183 } 184 185 return readPromises.get(testId); 186 }, 187 }; 188 } 189 190 /** 191 * Requires that writeWindows and readWindows are defined in the calling context. 192 * @param {*} t - test object provided by the wpt test harness 193 * @param {*} testId - name to distinguish the test 194 * @param {*} expectation - final message from the tested iframe 195 * @returns test step function to be used as a window listener 196 */ 197 const getWindowTestListener = ( 198 t, 199 testAPI, 200 testId, 201 expectation, 202 contextType, 203 setup 204 ) => { 205 const bc = new BroadcastChannel(testId); 206 207 assert_true(["window", "worker"].includes(contextType)); 208 209 const readFrame = "read-frame-" + contextType; 210 211 assert_true(["allow", "deny"].includes(expectation)); 212 213 const expectedMessage = 214 expectation == "allow" ? testId : getAccessErrorForAPI(testAPI, testId); 215 216 const ownedReadWindow = (s => { 217 if (s.readWindows) { 218 return s.readWindows.get(testId); 219 } 220 return null; 221 })(setup); 222 223 const ownedWriteWindows = setup.writeWindows; 224 225 return t.step_func(e => { 226 const here = {}; 227 228 try { 229 console.log("Test listener received " + JSON.stringify(e.data)); 230 here.ownedReadWindow = ownedReadWindow; 231 here.writeWindows = ownedWriteWindows; 232 here.bc = bc; 233 here.api = testAPI; 234 here.id = testId; 235 here.expected = expectedMessage; 236 here.readFrame = readFrame; 237 238 // Test summary is automatically sent to parent window 239 if (isSummary(e)) { 240 const maybeError = e.data.tests[0].message; 241 if (maybeError) { 242 here.bc.postMessage({ id: here.id, message: maybeError }); 243 throw new Error(maybeError); 244 } 245 } else { 246 // Otherwise it should follow this protocol. 247 if (e.data.id !== here.id) { 248 const msg = "id " + here.id + " ignores message for id " + e.data.id; 249 console.log(msg); 250 here.bc.postMessage({ id: here.id, message: msg }); 251 throw new Error(msg); 252 } 253 254 assert_true(!!e.data.message); 255 if (e.data.message === "write loaded") { 256 const msg = { id: here.id, message: here.id, type: here.api }; 257 here.writeWindows.get(here.id).postMessage(msg, "*"); 258 } else if (e.data.message === "write done") { 259 assert_true(!!e.data.expected); // What we wrote to the database 260 const msg = { 261 id: here.id, 262 message: here.id, 263 expected: e.data.expected, // What was written to storage 264 outcome: here.expected, // What the iframe should send to its parent 265 frame: here.readFrame, // Should it go to the worker or window iframe? 266 type: here.api, // Which storage API should be tested? 267 }; 268 269 here.ownedReadWindow.postMessage(msg, "*"); 270 } else { 271 assert_equals(e.data.message, here.expected); 272 here.bc.postMessage({ id: here.id, message: here.id }); 273 t.done(); 274 } 275 } 276 } catch (err) { 277 here.bc.postMessage({ id: here.id, message: err.message }); 278 throw err; 279 } 280 }); 281 };