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