Source code

Revision control

Copy as Markdown

Other Tools

'use strict';
const ROOT_NAME = 'fetch/fetch-later';
function parallelPromiseTest(func, description) {
async_test((t) => {
Promise.resolve(func(t)).then(() => t.done()).catch(t.step_func((e) => {
throw e;
}));
}, description);
}
/** @enum {string} */
const BeaconDataType = {
String: 'String',
ArrayBuffer: 'ArrayBuffer',
FormData: 'FormData',
URLSearchParams: 'URLSearchParams',
Blob: 'Blob',
File: 'File',
};
/** @enum {string} */
const BeaconDataTypeToSkipCharset = {
String: '',
ArrayBuffer: '',
FormData: '\n\r', // CRLF characters will be normalized by FormData
URLSearchParams: ';,/?:@&=+$', // reserved URI characters
Blob: '',
File: '',
};
const BEACON_PAYLOAD_KEY = 'payload';
// Creates beacon data of the given `dataType` from `data`.
// @param {string} data - A string representation of the beacon data. Note that
// it cannot contain UTF-16 surrogates for all `BeaconDataType` except BLOB.
// @param {BeaconDataType} dataType - must be one of `BeaconDataType`.
// @param {string} contentType - Request Content-Type.
function makeBeaconData(data, dataType, contentType) {
switch (dataType) {
case BeaconDataType.String:
return data;
case BeaconDataType.ArrayBuffer:
return new TextEncoder().encode(data).buffer;
case BeaconDataType.FormData:
const formData = new FormData();
if (data.length > 0) {
formData.append(BEACON_PAYLOAD_KEY, data);
}
return formData;
case BeaconDataType.URLSearchParams:
if (data.length > 0) {
return new URLSearchParams(`${BEACON_PAYLOAD_KEY}=${data}`);
}
return new URLSearchParams();
case BeaconDataType.Blob: {
const options = {type: contentType || undefined};
return new Blob([data], options);
}
case BeaconDataType.File: {
const options = {type: contentType || 'text/plain'};
return new File([data], 'file.txt', options);
}
default:
throw Error(`Unsupported beacon dataType: ${dataType}`);
}
}
// Create a string of `end`-`begin` characters, with characters starting from
// UTF-16 code unit `begin` to `end`-1.
function generateSequentialData(begin, end, skip) {
const codeUnits = Array(end - begin).fill().map((el, i) => i + begin);
if (skip) {
return String.fromCharCode(
...codeUnits.filter(c => !skip.includes(String.fromCharCode(c))));
}
return String.fromCharCode(...codeUnits);
}
function generatePayload(size) {
if (size == 0) {
return '';
}
const prefix = String(size) + ':';
if (size < prefix.length) {
return Array(size).fill('*').join('');
}
if (size == prefix.length) {
return prefix;
}
return prefix + Array(size - prefix.length).fill('*').join('');
}
function generateSetBeaconURL(uuid, options) {
const host = (options && options.host) || '';
let url = `${host}/${ROOT_NAME}/resources/set_beacon.py?uuid=${uuid}`;
if (options) {
if (options.expectOrigin !== undefined) {
url = `${url}&expectOrigin=${options.expectOrigin}`;
}
if (options.expectPreflight !== undefined) {
url = `${url}&expectPreflight=${options.expectPreflight}`;
}
if (options.expectCredentials !== undefined) {
url = `${url}&expectCredentials=${options.expectCredentials}`;
}
if (options.useRedirectHandler) {
const redirect = `${host}/common/redirect.py` +
`?location=${encodeURIComponent(url)}`;
url = redirect;
}
}
return url;
}
async function poll(asyncFunc, expected) {
const maxRetries = 30;
const waitInterval = 100; // milliseconds.
const delay = ms => new Promise(res => setTimeout(res, ms));
let result = {data: []};
for (let i = 0; i < maxRetries; i++) {
result = await asyncFunc();
if (!expected(result)) {
await delay(waitInterval);
continue;
}
return result;
}
return result;
}
// Waits until the `options.count` number of beacon data available from the
// server. Defaults to 1.
// If `options.data` is set, it will be used to compare with the data from the
// response.
async function expectBeacon(uuid, options) {
const expectedCount =
(options && options.count !== undefined) ? options.count : 1;
const res = await poll(
async () => {
const res = await fetch(
`/${ROOT_NAME}/resources/get_beacon.py?uuid=${uuid}`,
{cache: 'no-store'});
return await res.json();
},
(res) => {
if (expectedCount == 0) {
// If expecting no beacon, we should try to wait as long as possible.
// So always returning false here until `poll()` decides to terminate
// itself.
return false;
}
return res.data.length == expectedCount;
});
if (!options || !options.data) {
assert_equals(
res.data.length, expectedCount,
'Number of sent beacons does not match expected count:');
return;
}
if (expectedCount == 0) {
assert_equals(
res.data.length, 0,
'Number of sent beacons does not match expected count:');
return;
}
const decoder = options && options.percentDecoded ? (s) => {
// application/x-www-form-urlencoded serializer encodes space as '+'
s = s.replace(/\+/g, '%20');
return decodeURIComponent(s);
} : (s) => s;
assert_equals(
res.data.length, options.data.length,
`The size of beacon data ${
res.data.length} from server does not match expected value ${
options.data.length}.`);
for (let i = 0; i < options.data.length; i++) {
assert_equals(
decoder(res.data[i]), options.data[i],
'The beacon data does not match expected value.');
}
}
function generateHTML(script) {
return `<!DOCTYPE html><body><script>${script}</script></body>`;
}
// Loads `script` into an iframe and appends it to the current document.
// Returns the loaded iframe element.
async function loadScriptAsIframe(script) {
const iframe = document.createElement('iframe');
iframe.srcdoc = generateHTML(script);
const iframeLoaded = new Promise(resolve => iframe.onload = resolve);
document.body.appendChild(iframe);
await iframeLoaded;
return iframe;
}