| 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) { | |
| 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. | |
| } | |
| return await sharedStorage.selectURL( | |
| 'test-url-selection-operation', [{url: href, | |
| reportingMetadata: { | |
| 'reserved.top_navigation_start': BEACON_URL + | |
| "?type=reserved.top_navigation_start", | |
| 'reserved.top_navigation_commit': BEACON_URL + | |
| "?type=reserved.top_navigation_commit", | |
| }}], { | |
| 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) { | |
| const full_url = generateURL(href, keylist); | |
| return await runSelectRawURL(full_url, resolve_to_config); | |
| } | |
| 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)); | |
| 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) { | |
| 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"}); | |
| } | |
| // Simulates a user gesture. | |
| async function simulateGesture() { | |
| // Wait until the window size is initialized. | |
| while (window.innerWidth == 0) { | |
| await new Promise(resolve => requestAnimationFrame(resolve)); | |
| } | |
| await test_driver.bless('simulate gesture'); | |
| } | |
| // 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(); | |
| } | |
| } |