684 lines
26 KiB
JavaScript
684 lines
26 KiB
JavaScript
const STORE_URL = '/fenced-frame/resources/key-value-store.py';
|
|
const BEACON_URL = '/fenced-frame/resources/beacon-store.py';
|
|
const REMOTE_EXECUTOR_URL = '/fenced-frame/resources/remote-context-executor.https.html';
|
|
|
|
// If your test needs to modify FLEDGE bidding or decision logic, you should
|
|
// update the generated JS in the corresponding handler below.
|
|
const FLEDGE_BIDDING_URL = '/fenced-frame/resources/fledge-bidding-logic.py';
|
|
const FLEDGE_DECISION_URL = '/fenced-frame/resources/fledge-decision-logic.py';
|
|
|
|
// Creates a URL that includes a list of stash key UUIDs that are being used
|
|
// in the test. This allows us to generate UUIDs on the fly and let anything
|
|
// (iframes, fenced frames, pop-ups, etc...) that wouldn't have access to the
|
|
// original UUID variable know what the UUIDs are.
|
|
// @param {string} href - The base url of the page being navigated to
|
|
// @param {string list} keylist - The list of key UUIDs to be used. Note that
|
|
// order matters when extracting the keys
|
|
function generateURL(href, keylist) {
|
|
const ret_url = new URL(href, location.href);
|
|
ret_url.searchParams.append("keylist", keylist.join(','));
|
|
return ret_url;
|
|
}
|
|
|
|
function getRemoteContextURL(origin) {
|
|
return new URL(REMOTE_EXECUTOR_URL, origin);
|
|
}
|
|
|
|
async function runSelectRawURL(
|
|
href, resolve_to_config = false, register_beacon = false) {
|
|
try {
|
|
await sharedStorage.worklet.addModule(
|
|
"/shared-storage/resources/simple-module.js");
|
|
} catch (e) {
|
|
// Shared Storage needs to have a module added before we can operate on it.
|
|
// It is generated on the fly with this call, and since there's no way to
|
|
// tell through the API if a module already exists, wrap the addModule call
|
|
// in a try/catch so that if it runs a second time in a test, it will
|
|
// gracefully fail rather than bring the whole test down.
|
|
}
|
|
let operation = {url: href};
|
|
if (register_beacon) {
|
|
operation.reportingMetadata = {
|
|
'reserved.top_navigation_start':
|
|
BEACON_URL + '?type=reserved.top_navigation_start',
|
|
'reserved.top_navigation_commit':
|
|
BEACON_URL + '?type=reserved.top_navigation_commit',
|
|
};
|
|
}
|
|
return await sharedStorage.selectURL(
|
|
'test-url-selection-operation', [operation], {
|
|
data: {'mockResult': 0},
|
|
resolveToConfig: resolve_to_config,
|
|
keepAlive: true,
|
|
});
|
|
}
|
|
|
|
// Similar to generateURL, but creates
|
|
// 1. An urn:uuid if `resolve_to_config` is false.
|
|
// 2. A fenced frame config object if `resolve_to_config` is true.
|
|
// This relies on a mock Shared Storage auction, since it is the simplest
|
|
// WP-exposed way to turn a url into an urn:uuid or a fenced frame config.
|
|
// Note: this function, unlike generateURL, is asynchronous and needs to be
|
|
// called with an await operator.
|
|
// @param {string} href - The base url of the page being navigated to
|
|
// @param {string list} keylist - The list of key UUIDs to be used. Note that
|
|
// order matters when extracting the keys
|
|
// @param {boolean} [resolve_to_config = false] - Determines whether the result
|
|
// of `sharedStorage.selectURL()`
|
|
// is an urn:uuid or a fenced
|
|
// frame config.
|
|
// Note:
|
|
// 1. There is a limit of 3 calls per origin per pageload for
|
|
// `sharedStorage.selectURL()`, so `runSelectURL()` must also respect this
|
|
// limit.
|
|
// 2. If `resolve_to_config` is true, blink feature `FencedFramesAPIChanges`
|
|
// needs to be enabled for `selectURL()` to return a fenced frame config.
|
|
// Otherwise `selectURL()` will fall back to the old behavior that returns an
|
|
// urn:uuid.
|
|
async function runSelectURL(
|
|
href, keylist = [], resolve_to_config = false, register_beacon = false) {
|
|
const full_url = generateURL(href, keylist);
|
|
return await runSelectRawURL(full_url, resolve_to_config, register_beacon);
|
|
}
|
|
|
|
async function generateURNFromFledgeRawURL(
|
|
href, nested_urls, resolve_to_config = false, ad_with_size = false,
|
|
requested_size = null, register_beacon = false) {
|
|
const bidding_token = token();
|
|
const seller_token = token();
|
|
|
|
const ad_components_list = nested_urls.map((url) => {
|
|
return ad_with_size ?
|
|
{ renderURL: url, sizeGroup: "group1" } :
|
|
{ renderURL: url }
|
|
});
|
|
|
|
let interestGroup = {
|
|
name: 'testAd1',
|
|
owner: location.origin,
|
|
biddingLogicURL: new URL(FLEDGE_BIDDING_URL, location.origin),
|
|
ads: [{
|
|
renderURL: href,
|
|
bid: 1,
|
|
allowedReportingOrigins: [location.origin],
|
|
}],
|
|
userBiddingSignals: {biddingToken: bidding_token},
|
|
trustedBiddingSignalsKeys: ['key1'],
|
|
adComponents: ad_components_list,
|
|
};
|
|
|
|
let biddingURLParams =
|
|
new URLSearchParams(interestGroup.biddingLogicURL.search);
|
|
if (requested_size)
|
|
biddingURLParams.set(
|
|
'requested-size', requested_size[0] + '-' + requested_size[1]);
|
|
if (ad_with_size)
|
|
biddingURLParams.set('ad-with-size', 1);
|
|
if (register_beacon)
|
|
biddingURLParams.set('beacon', 1);
|
|
interestGroup.biddingLogicURL.search = biddingURLParams;
|
|
|
|
if (ad_with_size) {
|
|
interestGroup.ads[0].sizeGroup = 'group1';
|
|
interestGroup.adSizes = {'size1': {width: '100px', height: '50px'}};
|
|
interestGroup.sizeGroups = {'group1': ['size1']};
|
|
}
|
|
|
|
// Pick an arbitrarily high duration to guarantee that we never leave the
|
|
// ad interest group while the test runs.
|
|
navigator.joinAdInterestGroup(interestGroup, /*durationSeconds=*/3000000);
|
|
|
|
let auctionConfig = {
|
|
seller: location.origin,
|
|
interestGroupBuyers: [location.origin],
|
|
decisionLogicURL: new URL(FLEDGE_DECISION_URL, location.origin),
|
|
auctionSignals: {biddingToken: bidding_token, sellerToken: seller_token},
|
|
resolveToConfig: resolve_to_config
|
|
};
|
|
|
|
if (requested_size) {
|
|
let decisionURLParams =
|
|
new URLSearchParams(auctionConfig.decisionLogicURL.search);
|
|
decisionURLParams.set(
|
|
'requested-size', requested_size[0] + '-' + requested_size[1]);
|
|
auctionConfig.decisionLogicURL.search = decisionURLParams;
|
|
|
|
auctionConfig['requestedSize'] = {width: requested_size[0], height: requested_size[1]};
|
|
}
|
|
|
|
return navigator.runAdAuction(auctionConfig);
|
|
}
|
|
|
|
// Similar to runSelectURL, but uses FLEDGE instead of Shared Storage as the
|
|
// auctioning tool.
|
|
// Note: this function, unlike generateURL, is asynchronous and needs to be
|
|
// called with an await operator. @param {string} href - The base url of the
|
|
// page being navigated to @param {string list} keylist - The list of key UUIDs
|
|
// to be used. Note that order matters when extracting the keys
|
|
// @param {string} href - The base url of the page being navigated to
|
|
// @param {string list} keylist - The list of key UUIDs to be used. Note that
|
|
// order matters when extracting the keys
|
|
// @param {string list} nested_urls - A list of urls that will eventually become
|
|
// the nested configs/ad components
|
|
// @param {boolean} [resolve_to_config = false] - Determines whether the result
|
|
// of `navigator.runAdAuction()`
|
|
// is an urn:uuid or a fenced
|
|
// frame config.
|
|
// @param {boolean} [ad_with_size = false] - Determines whether the auction is
|
|
// run with ad sizes specified.
|
|
// @param {boolean} [register_beacon = false] - If true, FLEDGE logic will
|
|
// register reporting beacons after
|
|
// completion.
|
|
async function generateURNFromFledge(
|
|
href, keylist, nested_urls = [], resolve_to_config = false,
|
|
ad_with_size = false, requested_size = null, register_beacon = false) {
|
|
const full_url = generateURL(href, keylist);
|
|
return generateURNFromFledgeRawURL(
|
|
full_url, nested_urls, resolve_to_config, ad_with_size, requested_size,
|
|
register_beacon);
|
|
}
|
|
|
|
// Extracts a list of UUIDs from the from the current page's URL.
|
|
// @returns {string list} - The list of UUIDs extracted from the page. This can
|
|
// be read into multiple variables using the
|
|
// [key1, key2, etc...] = parseKeyList(); pattern.
|
|
function parseKeylist() {
|
|
const url = new URL(location.href);
|
|
const keylist = url.searchParams.get("keylist");
|
|
return keylist.split(',');
|
|
}
|
|
|
|
// Converts a same-origin URL to a cross-origin URL
|
|
// @param {URL} url - The URL object whose origin is being converted
|
|
// @param {boolean} [https=true] - Whether or not to use the HTTPS origin
|
|
//
|
|
// @returns {URL} The new cross-origin URL
|
|
function getRemoteOriginURL(url, https=true) {
|
|
const same_origin = location.origin;
|
|
const cross_origin = https ? get_host_info().HTTPS_REMOTE_ORIGIN
|
|
: get_host_info().HTTP_REMOTE_ORIGIN;
|
|
return new URL(url.toString().replace(same_origin, cross_origin));
|
|
}
|
|
|
|
// Builds a URL to be used as a remote context executor.
|
|
function generateRemoteContextURL(headers, origin) {
|
|
// Generate the unique id for the parent/child channel.
|
|
const uuid = token();
|
|
|
|
// Use the absolute path of the remote context executor source file, so that
|
|
// nested contexts will work.
|
|
const url = getRemoteContextURL(origin ? origin : location.origin);
|
|
url.searchParams.append('uuid', uuid);
|
|
|
|
// Add the header to allow loading in a fenced frame.
|
|
headers.push(["Supports-Loading-Mode", "fenced-frame"]);
|
|
|
|
// Transform the headers into the expected format.
|
|
// https://web-platform-tests.org/writing-tests/server-pipes.html#headers
|
|
function escape(s) {
|
|
return s.replace('(', '\\(').replace(')', '\\)').replace(',', '\\,');
|
|
}
|
|
const formatted_headers = headers.map((header) => {
|
|
return `header(${escape(header[0])}, ${escape(header[1])})`;
|
|
});
|
|
url.searchParams.append('pipe', formatted_headers.join('|'));
|
|
|
|
return [uuid, url];
|
|
}
|
|
|
|
function buildRemoteContextForObject(object, uuid, html) {
|
|
// https://github.com/web-platform-tests/wpt/blob/master/common/dispatcher/README.md
|
|
const context = new RemoteContext(uuid);
|
|
if (html) {
|
|
context.execute_script(
|
|
(html_source) => {
|
|
document.body.insertAdjacentHTML('beforebegin', html_source);
|
|
},
|
|
[html]);
|
|
}
|
|
|
|
// We need a little bit of boilerplate in the handlers because Proxy doesn't
|
|
// work so nicely with HTML elements.
|
|
const handler = {
|
|
get: (target, key) => {
|
|
if (key == "execute") {
|
|
return context.execute_script;
|
|
}
|
|
if (key == "element") {
|
|
return object;
|
|
}
|
|
if (key in target) {
|
|
return target[key];
|
|
}
|
|
return context[key];
|
|
},
|
|
set: (target, key, value) => {
|
|
target[key] = value;
|
|
return value;
|
|
}
|
|
};
|
|
|
|
// If `object` is null (e.g. a window created with noopener), set it to a
|
|
// dummy value so that the Proxy constructor won't fail.
|
|
if (object == null) {
|
|
object = {};
|
|
}
|
|
const proxy = new Proxy(object, handler);
|
|
return proxy;
|
|
}
|
|
|
|
// Attaches an object that waits for scripts to execute from RemoteContext.
|
|
// (In practice, this is either a frame or a window.)
|
|
// Returns a proxy for the object that first resolves to the object itself,
|
|
// then resolves to the RemoteContext if the property isn't found.
|
|
// The proxy also has an extra attribute `execute`, which is an alias for the
|
|
// remote context's `execute_script(fn, args=[])`.
|
|
function attachContext(object_constructor, html, headers, origin) {
|
|
const [uuid, url] = generateRemoteContextURL(headers, origin);
|
|
const object = object_constructor(url);
|
|
return buildRemoteContextForObject(object, uuid, html);
|
|
}
|
|
|
|
// TODO(crbug.com/1347953): Update this function to also test
|
|
// `sharedStorage.selectURL()` that returns a fenced frame config object.
|
|
// This should be done after fixing the following flaky tests that use this
|
|
// function.
|
|
// 1. crbug.com/1372536: resize-lock-input.https.html
|
|
// 2. crbug.com/1394559: unfenced-top.https.html
|
|
async function attachOpaqueContext(
|
|
generator_api, resolve_to_config, ad_with_size, requested_size,
|
|
register_beacon, object_constructor, html, headers, origin,
|
|
component_origin, num_components) {
|
|
const [uuid, url] = generateRemoteContextURL(headers, origin);
|
|
|
|
let components_list = [];
|
|
for (let i = 0; i < num_components; i++) {
|
|
let [component_uuid, component_url] =
|
|
generateRemoteContextURL(headers, component_origin);
|
|
// This field will be read by attachComponentFrameContext() in order to
|
|
// know what uuid to point to when building the remote context.
|
|
html += '<input type=\'hidden\' id=\'component_uuid_' + i + '\' value=\'' +
|
|
component_uuid + '\'>';
|
|
components_list.push(component_url);
|
|
}
|
|
|
|
const id = await (
|
|
generator_api == 'fledge' ?
|
|
generateURNFromFledge(
|
|
url, [], components_list, resolve_to_config, ad_with_size,
|
|
requested_size, register_beacon) :
|
|
runSelectURL(url, [], resolve_to_config, register_beacon));
|
|
const object = object_constructor(id);
|
|
return buildRemoteContextForObject(object, uuid, html);
|
|
}
|
|
|
|
function attachPotentiallyOpaqueContext(
|
|
generator_api, resolve_to_config, ad_with_size, requested_size,
|
|
register_beacon, frame_constructor, html, headers, origin,
|
|
component_origin, num_components) {
|
|
generator_api = generator_api.toLowerCase();
|
|
if (generator_api == 'fledge' || generator_api == 'sharedstorage') {
|
|
return attachOpaqueContext(
|
|
generator_api, resolve_to_config, ad_with_size, requested_size,
|
|
register_beacon, frame_constructor, html, headers, origin,
|
|
component_origin, num_components);
|
|
} else {
|
|
return attachContext(frame_constructor, html, headers, origin);
|
|
}
|
|
}
|
|
|
|
function attachFrameContext(
|
|
element_name, generator_api, resolve_to_config, ad_with_size,
|
|
requested_size, register_beacon, html, headers, attributes, origin,
|
|
component_origin, num_components) {
|
|
frame_constructor = (id) => {
|
|
frame = document.createElement(element_name);
|
|
attributes.forEach(attribute => {
|
|
frame.setAttribute(attribute[0], attribute[1]);
|
|
});
|
|
if (element_name == "iframe") {
|
|
frame.src = id;
|
|
} else if (id instanceof FencedFrameConfig) {
|
|
frame.config = id;
|
|
} else {
|
|
const config = new FencedFrameConfig(id);
|
|
frame.config = config;
|
|
}
|
|
document.body.append(frame);
|
|
return frame;
|
|
};
|
|
return attachPotentiallyOpaqueContext(
|
|
generator_api, resolve_to_config, ad_with_size, requested_size,
|
|
register_beacon, frame_constructor, html, headers, origin,
|
|
component_origin, num_components);
|
|
}
|
|
|
|
// Performs a content-initiated navigation of a frame proxy. This navigated page
|
|
// uses a new urn:uuid as its communication channel to prevent potential clashes
|
|
// with the currently loaded document.
|
|
async function navigateFrameContext(frame_proxy, {headers = [], origin = ''}) {
|
|
const [uuid, url] = generateRemoteContextURL(headers, origin);
|
|
frame_proxy.execute((url) => {
|
|
window.executor.suspend(() => {
|
|
window.location = url;
|
|
});
|
|
}, [url])
|
|
frame_proxy.context_id = uuid;
|
|
}
|
|
|
|
function replaceFrameContext(frame_proxy, {
|
|
generator_api = '',
|
|
resolve_to_config = false,
|
|
ad_with_size = false,
|
|
requested_size = null,
|
|
register_beacon = false,
|
|
html = '',
|
|
headers = [],
|
|
origin = ''
|
|
} = {}) {
|
|
frame_constructor = (id) => {
|
|
if (frame_proxy.element.nodeName == "IFRAME") {
|
|
frame_proxy.element.src = id;
|
|
} else if (id instanceof FencedFrameConfig) {
|
|
frame_proxy.element.config = id;
|
|
} else {
|
|
const config = new FencedFrameConfig(id);
|
|
frame_proxy.element.config = config;
|
|
}
|
|
return frame_proxy.element;
|
|
};
|
|
return attachPotentiallyOpaqueContext(
|
|
generator_api, resolve_to_config, ad_with_size, requested_size,
|
|
register_beacon, frame_constructor, html, headers, origin);
|
|
}
|
|
|
|
// Attach a fenced frame that waits for scripts to execute. Takes as input a(n
|
|
// optional) dictionary of configs:
|
|
// - generator_api: the name of the API that should generate the urn/config.
|
|
// Supports (case-insensitive) "fledge" and "sharedstorage", or any other
|
|
// value as a default. If you generate a urn, then you need to await the
|
|
// result of this function.
|
|
// - resolve_to_config: whether a config should be used. (currently only works
|
|
// for FLEDGE and sharedStorage generator_api)
|
|
// - ad_with_size: whether an ad auction is run with size specified for the ads
|
|
// and ad components. (currently only works for FLEDGE)
|
|
// - requested_size: A 2-element list with the width and height for
|
|
// requestedSize in the FLEDGE auction config. This is different from
|
|
// ad_with_size, which refers to size information provided alongside the ads
|
|
// themselves.
|
|
// - register_beacon: If true and generator_api = "fledge", an automatic beacon
|
|
// and a destination URL reportEvent() beacon will be registered after the
|
|
// FLEDGE auction completes.
|
|
// - html: extra HTML source code to inject into the loaded frame
|
|
// - headers: an array of header pairs [[key, value], ...]
|
|
// - attributes: an array of attribute pairs to set on the frame [[key, value],
|
|
// ...]
|
|
// - origin: origin of the url, default to location.origin if not set. Returns a
|
|
// proxy that acts like the frame HTML element, but with an extra function
|
|
// `execute`. See `attachFrameContext` or the README for more details.
|
|
function attachFencedFrameContext({
|
|
generator_api = '',
|
|
resolve_to_config = false,
|
|
ad_with_size = false,
|
|
requested_size = null,
|
|
register_beacon = false,
|
|
html = '',
|
|
headers = [],
|
|
attributes = [],
|
|
origin = '',
|
|
component_origin = '',
|
|
num_components = 0
|
|
} = {}) {
|
|
return attachFrameContext(
|
|
'fencedframe', generator_api, resolve_to_config, ad_with_size,
|
|
requested_size, register_beacon, html, headers, attributes, origin,
|
|
component_origin, num_components);
|
|
}
|
|
|
|
// Attach an iframe that waits for scripts to execute.
|
|
// See `attachFencedFrameContext` for more details.
|
|
function attachIFrameContext({
|
|
generator_api = '',
|
|
register_beacon = false,
|
|
html = '',
|
|
headers = [],
|
|
attributes = [],
|
|
origin = '',
|
|
component_origin = '',
|
|
num_components = 0
|
|
} = {}) {
|
|
return attachFrameContext(
|
|
'iframe', generator_api, resolve_to_config = false, ad_with_size = false,
|
|
requested_size = null, register_beacon, html, headers, attributes, origin,
|
|
component_origin, num_components);
|
|
}
|
|
|
|
// Open a window that waits for scripts to execute.
|
|
// Returns a proxy that acts like the window object, but with an extra
|
|
// function `execute`. See `attachContext` for more details.
|
|
function attachWindowContext({target="_blank", html="", headers=[], origin=""}={}) {
|
|
window_constructor = (url) => {
|
|
return window.open(url, target);
|
|
}
|
|
|
|
return attachContext(window_constructor, html, headers, origin);
|
|
}
|
|
|
|
// Attaches an ad component in a fenced frame. For this to work, this must be
|
|
// called in a frame that was generated with attachFrameContext() using the
|
|
// Protected Audience API (generator_api: 'fledge').
|
|
function attachComponentFencedFrameContext(
|
|
index = 0, {attributes = [], html = ''} = {}) {
|
|
const urn = window.fence.getNestedConfigs()[index];
|
|
return attachComponentFrameContext(
|
|
index, 'fencedframe', urn, attributes, html);
|
|
}
|
|
|
|
// Same as attachComponentFencedFrameContext, but in a urn iframe.
|
|
function attachComponentIFrameContext(
|
|
index = 0, {attributes = [], html = ''} = {}) {
|
|
const urn = navigator.adAuctionComponents(index + 1)[index];
|
|
return attachComponentFrameContext(index, 'iframe', urn, attributes, html);
|
|
}
|
|
|
|
function attachComponentFrameContext(
|
|
index, element_name, urn, attributes, html) {
|
|
assert_not_equals(
|
|
document.getElementById('component_uuid_' + index), null,
|
|
'Component frames can only be attached to frames loaded with ' +
|
|
'attach*FrameContext() with `num_components` set to at least ' +
|
|
(index + 1) + '.');
|
|
|
|
let frame = document.createElement(element_name);
|
|
attributes.forEach(attribute => {
|
|
frame.setAttribute(attribute[0], attribute[1]);
|
|
});
|
|
if (element_name == 'iframe') {
|
|
frame.src = urn;
|
|
} else {
|
|
frame.config = urn;
|
|
}
|
|
document.body.append(frame);
|
|
const context_uuid = document.getElementById('component_uuid_' + index).value;
|
|
return buildRemoteContextForObject(frame, context_uuid, html);
|
|
}
|
|
|
|
// Converts a key string into a key uuid using a cryptographic hash function.
|
|
// This function only works in secure contexts (HTTPS).
|
|
async function stringToStashKey(string) {
|
|
// Compute a SHA-256 hash of the input string, and convert it to hex.
|
|
const data = new TextEncoder().encode(string);
|
|
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
const digest_array = Array.from(new Uint8Array(digest));
|
|
const digest_as_hex = digest_array.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
|
|
// UUIDs are structured as 8X-4X-4X-4X-12X.
|
|
// Use the first 32 hex digits and ignore the rest.
|
|
const digest_slices = [digest_as_hex.slice(0,8),
|
|
digest_as_hex.slice(8,12),
|
|
digest_as_hex.slice(12,16),
|
|
digest_as_hex.slice(16,20),
|
|
digest_as_hex.slice(20,32)];
|
|
return digest_slices.join('-');
|
|
}
|
|
|
|
// Create a fenced frame. Then navigate it using the given `target`, which can
|
|
// be either an urn:uuid or a fenced frame config object.
|
|
function attachFencedFrame(target) {
|
|
if (window.test_driver) {
|
|
assert_implements(
|
|
window.HTMLFencedFrameElement,
|
|
'The HTMLFencedFrameElement should be exposed on the window object');
|
|
}
|
|
|
|
const fenced_frame = document.createElement('fencedframe');
|
|
|
|
if (target instanceof FencedFrameConfig) {
|
|
fenced_frame.config = target;
|
|
} else {
|
|
const config = new FencedFrameConfig(target);
|
|
fenced_frame.config = config;
|
|
}
|
|
|
|
document.body.append(fenced_frame);
|
|
return fenced_frame;
|
|
}
|
|
|
|
function attachIFrame(url) {
|
|
const iframe = document.createElement('iframe');
|
|
iframe.src = url;
|
|
document.body.append(iframe);
|
|
return iframe;
|
|
}
|
|
|
|
// Reads the value specified by `key` from the key-value store on the server.
|
|
async function readValueFromServer(key) {
|
|
// Resolve the key if it is a Promise.
|
|
key = await key;
|
|
|
|
const serverURL = `${STORE_URL}?key=${key}`;
|
|
const response = await fetch(serverURL);
|
|
if (!response.ok)
|
|
throw new Error('An error happened in the server');
|
|
const value = await response.text();
|
|
|
|
// The value is not stored in the server.
|
|
if (value === "<Not set>")
|
|
return { status: false };
|
|
|
|
return { status: true, value: value };
|
|
}
|
|
|
|
// Convenience wrapper around the above getter that will wait until a value is
|
|
// available on the server.
|
|
async function nextValueFromServer(key) {
|
|
// Resolve the key if it is a Promise.
|
|
key = await key;
|
|
|
|
while (true) {
|
|
// Fetches the test result from the server.
|
|
const { status, value } = await readValueFromServer(key);
|
|
if (!status) {
|
|
// The test result has not been stored yet. Retry after a while.
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
continue;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
|
|
// Checks the beacon data server to see if it has received a beacon with a given
|
|
// event type and body.
|
|
async function readBeaconDataFromServer(event_type, expected_body) {
|
|
let serverURL = `${BEACON_URL}`;
|
|
const response = await fetch(serverURL + "?" + new URLSearchParams({
|
|
type: event_type,
|
|
expected_body: expected_body,
|
|
}));
|
|
if (!response.ok)
|
|
throw new Error('An error happened in the server ' + response.status);
|
|
const value = await response.text();
|
|
|
|
// The value is not stored in the server.
|
|
if (value === "<Not set>")
|
|
return { status: false };
|
|
|
|
return { status: true, value: value };
|
|
}
|
|
|
|
// Convenience wrapper around the above getter that will wait until a value is
|
|
// available on the server. The server uses a hash of the concatenated event
|
|
// type and beacon data as the key when storing the beacon in the database. To
|
|
// retrieve it, we need to supply the endpoint with both pieces of information.
|
|
async function nextBeacon(event_type, expected_body) {
|
|
while (true) {
|
|
// Fetches the test result from the server.
|
|
const {status, value} =
|
|
await readBeaconDataFromServer(event_type, expected_body);
|
|
if (!status) {
|
|
// The test result has not been stored yet. Retry after a while.
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
continue;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
|
|
// Writes `value` for `key` in the key-value store on the server.
|
|
async function writeValueToServer(key, value, origin = '') {
|
|
// Resolve the key if it is a Promise.
|
|
key = await key;
|
|
|
|
const serverURL = `${origin}${STORE_URL}?key=${key}&value=${value}`;
|
|
await fetch(serverURL, {"mode": "no-cors"});
|
|
}
|
|
|
|
// Fenced frames are always put in the public IP address space which is the
|
|
// least privileged. In case a navigation to a local data: URL or blob: URL
|
|
// resource is allowed, they would only be able to fetch things that are *also*
|
|
// in the public IP address space. So for the document described by these local
|
|
// URLs, we'll set them up to only communicate back to the outer page via
|
|
// resources obtained in the public address space.
|
|
function createLocalSource(key, url) {
|
|
return `
|
|
<head>
|
|
<script src="${url}"><\/script>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
writeValueToServer("${key}", "LOADED", /*origin=*/"${url.origin}");
|
|
<\/script>
|
|
</body>
|
|
`;
|
|
}
|
|
|
|
function setupCSP(csp, second_csp=null) {
|
|
let headers = [];
|
|
|
|
headers.push(["Content-Security-Policy", "fenced-frame-src " + csp]);
|
|
if (second_csp != null) {
|
|
headers.push(["Content-Security-Policy", "frame-src " + second_csp]);
|
|
}
|
|
|
|
const iframe = attachIFrameContext({headers: headers});
|
|
|
|
return iframe;
|
|
}
|
|
|
|
// Clicking in WPT tends to be flaky (https://crbug.com/1066891), so you may
|
|
// need to click multiple times to have an effect. This function clicks at
|
|
// coordinates `{x, y}` relative to `click_origin`, by default 3 times. Should
|
|
// not be used for tests where multiple clicks have distinct impact on the state
|
|
// of the page, but rather to bruteforce through flakes that rely on only one
|
|
// click.
|
|
async function multiClick(x, y, click_origin, times = 3) {
|
|
for (let i = 0; i < times; i++) {
|
|
let actions = new test_driver.Actions();
|
|
await actions.pointerMove(x, y, {origin: click_origin})
|
|
.pointerDown()
|
|
.pointerUp()
|
|
.send();
|
|
}
|
|
}
|