util.js (9618B)
1 const kValidAvailabilities = 2 ['unavailable', 'downloadable', 'downloading', 'available']; 3 const kAvailableAvailabilities = ['downloadable', 'downloading', 'available']; 4 5 const kTestPrompt = 'Please write a sentence in English.'; 6 const kTestContext = 'This is a test; this is only a test.'; 7 8 const getId = (() => { 9 let idCount = 0; 10 return () => idCount++; 11 })(); 12 13 // Takes an array of dictionaries mapping keys to value arrays, e.g.: 14 // [ {Shape: ["Square", "Circle", undefined]}, {Count: [1, 2]} ] 15 // Returns an array of dictionaries with all value combinations, i.e.: 16 // [ {Shape: "Square", Count: 1}, {Shape: "Square", Count: 2}, 17 // {Shape: "Circle", Count: 1}, {Shape: "Circle", Count: 2}, 18 // {Shape: undefined, Count: 1}, {Shape: undefined, Count: 2} ] 19 // Omits dictionary members when the value is undefined; supports array values. 20 function generateOptionCombinations(optionsSpec) { 21 // 1. Extract keys from the input specification. 22 const keys = optionsSpec.map(o => Object.keys(o)[0]); 23 // 2. Extract the arrays of possible values for each key. 24 const valueArrays = optionsSpec.map(o => Object.values(o)[0]); 25 // 3. Compute the Cartesian product of the value arrays using reduce. 26 const valueCombinations = valueArrays.reduce((accumulator, currentValues) => { 27 // Init the empty accumulator (first iteration), with single-element 28 // arrays. 29 if (accumulator.length === 0) { 30 return currentValues.map(value => [value]); 31 } 32 // Otherwise, expand existing combinations with current values. 33 return accumulator.flatMap( 34 existingCombo => currentValues.map( 35 currentValue => [...existingCombo, currentValue])); 36 }, []); 37 38 // 4. Map each value combination to a result dictionary, skipping 39 // undefined. 40 return valueCombinations.map(combination => { 41 const result = {}; 42 keys.forEach((key, index) => { 43 if (combination[index] !== undefined) { 44 result[key] = combination[index]; 45 } 46 }); 47 return result; 48 }); 49 } 50 51 // The method should take the AbortSignal as an option and return a promise. 52 async function testAbortPromise(t, method) { 53 // Test abort signal without custom error. 54 { 55 const controller = new AbortController(); 56 const promise = method(controller.signal); 57 controller.abort(); 58 await promise_rejects_dom(t, 'AbortError', promise); 59 60 // Using the same aborted controller will get the `AbortError` as well. 61 const anotherPromise = method(controller.signal); 62 await promise_rejects_dom(t, 'AbortError', anotherPromise); 63 } 64 65 // Test abort signal with custom error. 66 { 67 const err = new Error('test'); 68 const controller = new AbortController(); 69 const promise = method(controller.signal); 70 controller.abort(err); 71 await promise_rejects_exactly(t, err, promise); 72 73 // Using the same aborted controller will get the same error as well. 74 const anotherPromise = method(controller.signal); 75 await promise_rejects_exactly(t, err, anotherPromise); 76 } 77 }; 78 79 async function testCreateMonitorWithAbortAt( 80 t, loadedToAbortAt, method, options = {}) { 81 const {promise: eventPromise, resolve} = Promise.withResolvers(); 82 let hadEvent = false; 83 function monitor(m) { 84 m.addEventListener('downloadprogress', e => { 85 if (e.loaded != loadedToAbortAt) { 86 return; 87 } 88 89 if (hadEvent) { 90 assert_unreached( 91 'This should never be reached since LanguageDetector.create() was aborted.'); 92 return; 93 } 94 95 resolve(); 96 hadEvent = true; 97 }); 98 } 99 100 const controller = new AbortController(); 101 102 const createPromise = 103 method({...options, monitor, signal: controller.signal}); 104 105 await eventPromise; 106 107 const err = new Error('test'); 108 controller.abort(err); 109 await promise_rejects_exactly(t, err, createPromise); 110 } 111 112 async function testCreateMonitorWithAbort(t, method, options = {}) { 113 await testCreateMonitorWithAbortAt(t, 0, method, options); 114 await testCreateMonitorWithAbortAt(t, 1, method, options); 115 } 116 117 // The method should take the AbortSignal as an option and return a 118 // ReadableStream. 119 async function testAbortReadableStream(t, method) { 120 // Test abort signal without custom error. 121 { 122 const controller = new AbortController(); 123 const stream = method(controller.signal); 124 controller.abort(); 125 let writableStream = new WritableStream(); 126 await promise_rejects_dom(t, 'AbortError', stream.pipeTo(writableStream)); 127 128 // Using the same aborted controller will get the `AbortError` as well. 129 await promise_rejects_dom(t, 'AbortError', new Promise(() => { 130 method(controller.signal); 131 })); 132 } 133 134 // Test abort signal with custom error. 135 { 136 const error = new DOMException('test', 'VersionError'); 137 const controller = new AbortController(); 138 const stream = method(controller.signal); 139 controller.abort(error); 140 let writableStream = new WritableStream(); 141 await promise_rejects_exactly(t, error, stream.pipeTo(writableStream)); 142 143 // Using the same aborted controller will get the same error. 144 await promise_rejects_exactly(t, error, new Promise(() => { 145 method(controller.signal); 146 })); 147 } 148 }; 149 150 async function testMonitor(createFunc, options = {}) { 151 let created = false; 152 const progressEvents = []; 153 function monitor(m) { 154 m.addEventListener('downloadprogress', e => { 155 // No progress events should be fired after `createFunc` resolves. 156 assert_false(created); 157 158 progressEvents.push(e); 159 }); 160 } 161 162 result = await createFunc({...options, monitor}); 163 created = true; 164 165 assert_greater_than_equal(progressEvents.length, 2); 166 assert_equals(progressEvents.at(0).loaded, 0); 167 assert_equals(progressEvents.at(-1).loaded, 1); 168 169 let lastProgressEventLoaded = -1; 170 for (const progressEvent of progressEvents) { 171 assert_equals(progressEvent.lengthComputable, true); 172 assert_equals(progressEvent.total, 1); 173 assert_less_than_equal(progressEvent.loaded, progressEvent.total); 174 175 // `loaded` must be rounded to the nearest 0x10000th. 176 assert_equals(progressEvent.loaded % (1 / 0x10000), 0); 177 178 // Progress events should have monotonically increasing `loaded` values. 179 assert_greater_than(progressEvent.loaded, lastProgressEventLoaded); 180 lastProgressEventLoaded = progressEvent.loaded; 181 } 182 return result; 183 } 184 185 async function testCreateMonitorCallbackThrowsError( 186 t, createFunc, options = {}) { 187 const error = new Error('CreateMonitorCallback threw an error'); 188 function monitor(m) { 189 m.addEventListener('downloadprogress', e => { 190 assert_unreached( 191 'This should never be reached since monitor throws an error.'); 192 }); 193 throw error; 194 } 195 196 await promise_rejects_exactly(t, error, createFunc({...options, monitor})); 197 } 198 199 function run_iframe_test(iframe, test_name) { 200 const id = getId(); 201 iframe.contentWindow.postMessage({id, type: test_name}, '*'); 202 const {promise, resolve, reject} = Promise.withResolvers(); 203 window.onmessage = message => { 204 if (message.data.id !== id) { 205 return; 206 } 207 if (message.data.success) { 208 resolve(message.data.success); 209 } else { 210 reject(message.data.err) 211 } 212 }; 213 return promise; 214 } 215 216 function load_iframe(src, permission_policy) { 217 let iframe = document.createElement('iframe'); 218 const {promise, resolve} = Promise.withResolvers(); 219 iframe.onload = () => { 220 resolve(iframe); 221 }; 222 iframe.src = src; 223 iframe.allow = permission_policy; 224 document.body.appendChild(iframe); 225 return promise; 226 } 227 228 async function createLanguageModel(options = {}) { 229 await test_driver.bless(); 230 return LanguageModel.create(options); 231 } 232 233 async function createSummarizer(options = {}) { 234 await test_driver.bless(); 235 return await Summarizer.create(options); 236 } 237 238 async function createWriter(options = {}) { 239 await test_driver.bless(); 240 return await Writer.create(options); 241 } 242 243 async function createRewriter(options = {}) { 244 await test_driver.bless(); 245 return await Rewriter.create(options); 246 } 247 248 async function createProofreader(options = {}) { 249 await test_driver.bless(); 250 return await Proofreader.create(options); 251 } 252 253 async function ensureLanguageModel(options = {}) { 254 assert_true(!!LanguageModel); 255 const availability = await LanguageModel.availability(options); 256 assert_in_array(availability, kValidAvailabilities); 257 // Yield PRECONDITION_FAILED if the API is unavailable on this device. 258 assert_implements_optional(availability != 'unavailable', 'API unavailable'); 259 }; 260 261 async function testDestroy(t, createMethod, options, instanceMethods) { 262 const instance = await createMethod(options); 263 264 const promises = instanceMethods.map(method => method(instance)); 265 266 instance.destroy(); 267 268 promises.push(...instanceMethods.map(method => method(instance))); 269 270 for (const promise of promises) { 271 await promise_rejects_dom(t, 'AbortError', promise); 272 } 273 } 274 275 async function testCreateAbort(t, createMethod, options, instanceMethods) { 276 const controller = new AbortController(); 277 const instance = await createMethod({...options, signal: controller.signal}); 278 279 const promises = instanceMethods.map(method => method(instance)); 280 281 const error = new Error('The create abort signal was aborted.'); 282 controller.abort(error); 283 284 promises.push(...instanceMethods.map(method => method(instance))); 285 286 for (const promise of promises) { 287 await promise_rejects_exactly(t, error, promise); 288 } 289 } 290 291 function consumeTransientUserActivation() { 292 const win = window.open('about:blank', '_blank'); 293 if (win) 294 win.close(); 295 }