utils.js (26582B)
1 const STORE_URL = '/fenced-frame/resources/key-value-store.py'; 2 const BEACON_URL = '/fenced-frame/resources/beacon-store.py'; 3 const REMOTE_EXECUTOR_URL = '/fenced-frame/resources/remote-context-executor.https.html'; 4 5 // If your test needs to modify FLEDGE bidding or decision logic, you should 6 // update the generated JS in the corresponding handler below. 7 const FLEDGE_BIDDING_URL = '/fenced-frame/resources/fledge-bidding-logic.py'; 8 const FLEDGE_DECISION_URL = '/fenced-frame/resources/fledge-decision-logic.py'; 9 10 // Creates a URL that includes a list of stash key UUIDs that are being used 11 // in the test. This allows us to generate UUIDs on the fly and let anything 12 // (iframes, fenced frames, pop-ups, etc...) that wouldn't have access to the 13 // original UUID variable know what the UUIDs are. 14 // @param {string} href - The base url of the page being navigated to 15 // @param {string list} keylist - The list of key UUIDs to be used. Note that 16 // order matters when extracting the keys 17 function generateURL(href, keylist) { 18 const ret_url = new URL(href, location.href); 19 ret_url.searchParams.append("keylist", keylist.join(',')); 20 return ret_url; 21 } 22 23 function getRemoteContextURL(origin) { 24 return new URL(REMOTE_EXECUTOR_URL, origin); 25 } 26 27 async function runSelectRawURL( 28 href, resolve_to_config = false, register_beacon = false) { 29 try { 30 await sharedStorage.worklet.addModule( 31 "/shared-storage/resources/simple-module.js"); 32 } catch (e) { 33 // Shared Storage needs to have a module added before we can operate on it. 34 // It is generated on the fly with this call, and since there's no way to 35 // tell through the API if a module already exists, wrap the addModule call 36 // in a try/catch so that if it runs a second time in a test, it will 37 // gracefully fail rather than bring the whole test down. 38 } 39 let operation = {url: href}; 40 if (register_beacon) { 41 operation.reportingMetadata = { 42 'reserved.top_navigation_start': 43 BEACON_URL + '?type=reserved.top_navigation_start', 44 'reserved.top_navigation_commit': 45 BEACON_URL + '?type=reserved.top_navigation_commit', 46 }; 47 } 48 return await sharedStorage.selectURL( 49 'test-url-selection-operation', [operation], { 50 data: {'mockResult': 0}, 51 resolveToConfig: resolve_to_config, 52 keepAlive: true, 53 }); 54 } 55 56 // Similar to generateURL, but creates 57 // 1. An urn:uuid if `resolve_to_config` is false. 58 // 2. A fenced frame config object if `resolve_to_config` is true. 59 // This relies on a mock Shared Storage auction, since it is the simplest 60 // WP-exposed way to turn a url into an urn:uuid or a fenced frame config. 61 // Note: this function, unlike generateURL, is asynchronous and needs to be 62 // called with an await operator. 63 // @param {string} href - The base url of the page being navigated to 64 // @param {string list} keylist - The list of key UUIDs to be used. Note that 65 // order matters when extracting the keys 66 // @param {boolean} [resolve_to_config = false] - Determines whether the result 67 // of `sharedStorage.selectURL()` 68 // is an urn:uuid or a fenced 69 // frame config. 70 // Note: 71 // 1. There is a limit of 3 calls per origin per pageload for 72 // `sharedStorage.selectURL()`, so `runSelectURL()` must also respect this 73 // limit. 74 // 2. If `resolve_to_config` is true, blink feature `FencedFramesAPIChanges` 75 // needs to be enabled for `selectURL()` to return a fenced frame config. 76 // Otherwise `selectURL()` will fall back to the old behavior that returns an 77 // urn:uuid. 78 async function runSelectURL( 79 href, keylist = [], resolve_to_config = false, register_beacon = false) { 80 const full_url = generateURL(href, keylist); 81 return await runSelectRawURL(full_url, resolve_to_config, register_beacon); 82 } 83 84 async function generateURNFromFledgeRawURL( 85 href, nested_urls, resolve_to_config = false, ad_with_size = false, 86 requested_size = null, register_beacon = false) { 87 const bidding_token = token(); 88 const seller_token = token(); 89 90 const ad_components_list = nested_urls.map((url) => { 91 return ad_with_size ? 92 { renderURL: url, sizeGroup: "group1" } : 93 { renderURL: url } 94 }); 95 96 let interestGroup = { 97 name: 'testAd1', 98 owner: location.origin, 99 biddingLogicURL: new URL(FLEDGE_BIDDING_URL, location.origin), 100 ads: [{ 101 renderURL: href, 102 bid: 1, 103 allowedReportingOrigins: [location.origin], 104 }], 105 userBiddingSignals: {biddingToken: bidding_token}, 106 trustedBiddingSignalsKeys: ['key1'], 107 adComponents: ad_components_list, 108 }; 109 110 let biddingURLParams = 111 new URLSearchParams(interestGroup.biddingLogicURL.search); 112 if (requested_size) 113 biddingURLParams.set( 114 'requested-size', requested_size[0] + '-' + requested_size[1]); 115 if (ad_with_size) 116 biddingURLParams.set('ad-with-size', 1); 117 if (register_beacon) 118 biddingURLParams.set('beacon', 1); 119 interestGroup.biddingLogicURL.search = biddingURLParams; 120 121 if (ad_with_size) { 122 interestGroup.ads[0].sizeGroup = 'group1'; 123 interestGroup.adSizes = {'size1': {width: '100px', height: '50px'}}; 124 interestGroup.sizeGroups = {'group1': ['size1']}; 125 } 126 127 // Pick an arbitrarily high duration to guarantee that we never leave the 128 // ad interest group while the test runs. 129 navigator.joinAdInterestGroup(interestGroup, /*durationSeconds=*/3000000); 130 131 let auctionConfig = { 132 seller: location.origin, 133 interestGroupBuyers: [location.origin], 134 decisionLogicURL: new URL(FLEDGE_DECISION_URL, location.origin), 135 auctionSignals: {biddingToken: bidding_token, sellerToken: seller_token}, 136 resolveToConfig: resolve_to_config 137 }; 138 139 if (requested_size) { 140 let decisionURLParams = 141 new URLSearchParams(auctionConfig.decisionLogicURL.search); 142 decisionURLParams.set( 143 'requested-size', requested_size[0] + '-' + requested_size[1]); 144 auctionConfig.decisionLogicURL.search = decisionURLParams; 145 146 auctionConfig['requestedSize'] = {width: requested_size[0], height: requested_size[1]}; 147 } 148 149 return navigator.runAdAuction(auctionConfig); 150 } 151 152 // Similar to runSelectURL, but uses FLEDGE instead of Shared Storage as the 153 // auctioning tool. 154 // Note: this function, unlike generateURL, is asynchronous and needs to be 155 // called with an await operator. @param {string} href - The base url of the 156 // page being navigated to @param {string list} keylist - The list of key UUIDs 157 // to be used. Note that order matters when extracting the keys 158 // @param {string} href - The base url of the page being navigated to 159 // @param {string list} keylist - The list of key UUIDs to be used. Note that 160 // order matters when extracting the keys 161 // @param {string list} nested_urls - A list of urls that will eventually become 162 // the nested configs/ad components 163 // @param {boolean} [resolve_to_config = false] - Determines whether the result 164 // of `navigator.runAdAuction()` 165 // is an urn:uuid or a fenced 166 // frame config. 167 // @param {boolean} [ad_with_size = false] - Determines whether the auction is 168 // run with ad sizes specified. 169 // @param {boolean} [register_beacon = false] - If true, FLEDGE logic will 170 // register reporting beacons after 171 // completion. 172 async function generateURNFromFledge( 173 href, keylist, nested_urls = [], resolve_to_config = false, 174 ad_with_size = false, requested_size = null, register_beacon = false) { 175 const full_url = generateURL(href, keylist); 176 return generateURNFromFledgeRawURL( 177 full_url, nested_urls, resolve_to_config, ad_with_size, requested_size, 178 register_beacon); 179 } 180 181 // Extracts a list of UUIDs from the from the current page's URL. 182 // @returns {string list} - The list of UUIDs extracted from the page. This can 183 // be read into multiple variables using the 184 // [key1, key2, etc...] = parseKeyList(); pattern. 185 function parseKeylist() { 186 const url = new URL(location.href); 187 const keylist = url.searchParams.get("keylist"); 188 return keylist.split(','); 189 } 190 191 // Converts a same-origin URL to a cross-origin URL 192 // @param {URL} url - The URL object whose origin is being converted 193 // @param {boolean} [https=true] - Whether or not to use the HTTPS origin 194 // 195 // @returns {URL} The new cross-origin URL 196 function getRemoteOriginURL(url, https=true) { 197 const same_origin = location.origin; 198 const cross_origin = https ? get_host_info().HTTPS_REMOTE_ORIGIN 199 : get_host_info().HTTP_REMOTE_ORIGIN; 200 return new URL(url.toString().replace(same_origin, cross_origin)); 201 } 202 203 // Builds a URL to be used as a remote context executor. 204 function generateRemoteContextURL(headers, origin) { 205 // Generate the unique id for the parent/child channel. 206 const uuid = token(); 207 208 // Use the absolute path of the remote context executor source file, so that 209 // nested contexts will work. 210 const url = getRemoteContextURL(origin ? origin : location.origin); 211 url.searchParams.append('uuid', uuid); 212 213 // Add the header to allow loading in a fenced frame. 214 headers.push(["Supports-Loading-Mode", "fenced-frame"]); 215 216 // Transform the headers into the expected format. 217 // https://web-platform-tests.org/writing-tests/server-pipes.html#headers 218 function escape(s) { 219 return s.replace('(', '\\(').replace(')', '\\)').replace(',', '\\,'); 220 } 221 const formatted_headers = headers.map((header) => { 222 return `header(${escape(header[0])}, ${escape(header[1])})`; 223 }); 224 url.searchParams.append('pipe', formatted_headers.join('|')); 225 226 return [uuid, url]; 227 } 228 229 function buildRemoteContextForObject(object, uuid, html) { 230 // https://github.com/web-platform-tests/wpt/blob/master/common/dispatcher/README.md 231 const context = new RemoteContext(uuid); 232 if (html) { 233 context.execute_script( 234 (html_source) => { 235 document.body.insertAdjacentHTML('beforebegin', html_source); 236 }, 237 [html]); 238 } 239 240 // We need a little bit of boilerplate in the handlers because Proxy doesn't 241 // work so nicely with HTML elements. 242 const handler = { 243 get: (target, key) => { 244 if (key == "execute") { 245 return context.execute_script; 246 } 247 if (key == "element") { 248 return object; 249 } 250 if (key in target) { 251 return target[key]; 252 } 253 return context[key]; 254 }, 255 set: (target, key, value) => { 256 target[key] = value; 257 return value; 258 } 259 }; 260 261 // If `object` is null (e.g. a window created with noopener), set it to a 262 // dummy value so that the Proxy constructor won't fail. 263 if (object == null) { 264 object = {}; 265 } 266 const proxy = new Proxy(object, handler); 267 return proxy; 268 } 269 270 // Attaches an object that waits for scripts to execute from RemoteContext. 271 // (In practice, this is either a frame or a window.) 272 // Returns a proxy for the object that first resolves to the object itself, 273 // then resolves to the RemoteContext if the property isn't found. 274 // The proxy also has an extra attribute `execute`, which is an alias for the 275 // remote context's `execute_script(fn, args=[])`. 276 function attachContext(object_constructor, html, headers, origin) { 277 const [uuid, url] = generateRemoteContextURL(headers, origin); 278 const object = object_constructor(url); 279 return buildRemoteContextForObject(object, uuid, html); 280 } 281 282 // TODO(crbug.com/1347953): Update this function to also test 283 // `sharedStorage.selectURL()` that returns a fenced frame config object. 284 // This should be done after fixing the following flaky tests that use this 285 // function. 286 // 1. crbug.com/1372536: resize-lock-input.https.html 287 // 2. crbug.com/1394559: unfenced-top.https.html 288 async function attachOpaqueContext( 289 generator_api, resolve_to_config, ad_with_size, requested_size, 290 register_beacon, object_constructor, html, headers, origin, 291 component_origin, num_components) { 292 const [uuid, url] = generateRemoteContextURL(headers, origin); 293 294 let components_list = []; 295 for (let i = 0; i < num_components; i++) { 296 let [component_uuid, component_url] = 297 generateRemoteContextURL(headers, component_origin); 298 // This field will be read by attachComponentFrameContext() in order to 299 // know what uuid to point to when building the remote context. 300 html += '<input type=\'hidden\' id=\'component_uuid_' + i + '\' value=\'' + 301 component_uuid + '\'>'; 302 components_list.push(component_url); 303 } 304 305 const id = await ( 306 generator_api == 'fledge' ? 307 generateURNFromFledge( 308 url, [], components_list, resolve_to_config, ad_with_size, 309 requested_size, register_beacon) : 310 runSelectURL(url, [], resolve_to_config, register_beacon)); 311 const object = object_constructor(id); 312 return buildRemoteContextForObject(object, uuid, html); 313 } 314 315 function attachPotentiallyOpaqueContext( 316 generator_api, resolve_to_config, ad_with_size, requested_size, 317 register_beacon, frame_constructor, html, headers, origin, 318 component_origin, num_components) { 319 generator_api = generator_api.toLowerCase(); 320 if (generator_api == 'fledge' || generator_api == 'sharedstorage') { 321 return attachOpaqueContext( 322 generator_api, resolve_to_config, ad_with_size, requested_size, 323 register_beacon, frame_constructor, html, headers, origin, 324 component_origin, num_components); 325 } else { 326 return attachContext(frame_constructor, html, headers, origin); 327 } 328 } 329 330 function attachFrameContext( 331 element_name, generator_api, resolve_to_config, ad_with_size, 332 requested_size, register_beacon, html, headers, attributes, origin, 333 component_origin, num_components) { 334 frame_constructor = (id) => { 335 frame = document.createElement(element_name); 336 attributes.forEach(attribute => { 337 frame.setAttribute(attribute[0], attribute[1]); 338 }); 339 if (element_name == "iframe") { 340 frame.src = id; 341 } else if (id instanceof FencedFrameConfig) { 342 frame.config = id; 343 } else { 344 const config = new FencedFrameConfig(id); 345 frame.config = config; 346 } 347 document.body.append(frame); 348 return frame; 349 }; 350 return attachPotentiallyOpaqueContext( 351 generator_api, resolve_to_config, ad_with_size, requested_size, 352 register_beacon, frame_constructor, html, headers, origin, 353 component_origin, num_components); 354 } 355 356 // Performs a content-initiated navigation of a frame proxy. This navigated page 357 // uses a new urn:uuid as its communication channel to prevent potential clashes 358 // with the currently loaded document. 359 async function navigateFrameContext(frame_proxy, {headers = [], origin = ''}) { 360 const [uuid, url] = generateRemoteContextURL(headers, origin); 361 frame_proxy.execute((url) => { 362 window.executor.suspend(() => { 363 window.location = url; 364 }); 365 }, [url]) 366 frame_proxy.context_id = uuid; 367 } 368 369 function replaceFrameContext(frame_proxy, { 370 generator_api = '', 371 resolve_to_config = false, 372 ad_with_size = false, 373 requested_size = null, 374 register_beacon = false, 375 html = '', 376 headers = [], 377 origin = '' 378 } = {}) { 379 frame_constructor = (id) => { 380 if (frame_proxy.element.nodeName == "IFRAME") { 381 frame_proxy.element.src = id; 382 } else if (id instanceof FencedFrameConfig) { 383 frame_proxy.element.config = id; 384 } else { 385 const config = new FencedFrameConfig(id); 386 frame_proxy.element.config = config; 387 } 388 return frame_proxy.element; 389 }; 390 return attachPotentiallyOpaqueContext( 391 generator_api, resolve_to_config, ad_with_size, requested_size, 392 register_beacon, frame_constructor, html, headers, origin); 393 } 394 395 // Attach a fenced frame that waits for scripts to execute. Takes as input a(n 396 // optional) dictionary of configs: 397 // - generator_api: the name of the API that should generate the urn/config. 398 // Supports (case-insensitive) "fledge" and "sharedstorage", or any other 399 // value as a default. If you generate a urn, then you need to await the 400 // result of this function. 401 // - resolve_to_config: whether a config should be used. (currently only works 402 // for FLEDGE and sharedStorage generator_api) 403 // - ad_with_size: whether an ad auction is run with size specified for the ads 404 // and ad components. (currently only works for FLEDGE) 405 // - requested_size: A 2-element list with the width and height for 406 // requestedSize in the FLEDGE auction config. This is different from 407 // ad_with_size, which refers to size information provided alongside the ads 408 // themselves. 409 // - register_beacon: If true and generator_api = "fledge", an automatic beacon 410 // and a destination URL reportEvent() beacon will be registered after the 411 // FLEDGE auction completes. 412 // - html: extra HTML source code to inject into the loaded frame 413 // - headers: an array of header pairs [[key, value], ...] 414 // - attributes: an array of attribute pairs to set on the frame [[key, value], 415 // ...] 416 // - origin: origin of the url, default to location.origin if not set. Returns a 417 // proxy that acts like the frame HTML element, but with an extra function 418 // `execute`. See `attachFrameContext` or the README for more details. 419 function attachFencedFrameContext({ 420 generator_api = '', 421 resolve_to_config = false, 422 ad_with_size = false, 423 requested_size = null, 424 register_beacon = false, 425 html = '', 426 headers = [], 427 attributes = [], 428 origin = '', 429 component_origin = '', 430 num_components = 0 431 } = {}) { 432 return attachFrameContext( 433 'fencedframe', generator_api, resolve_to_config, ad_with_size, 434 requested_size, register_beacon, html, headers, attributes, origin, 435 component_origin, num_components); 436 } 437 438 // Attach an iframe that waits for scripts to execute. 439 // See `attachFencedFrameContext` for more details. 440 function attachIFrameContext({ 441 generator_api = '', 442 register_beacon = false, 443 html = '', 444 headers = [], 445 attributes = [], 446 origin = '', 447 component_origin = '', 448 num_components = 0 449 } = {}) { 450 return attachFrameContext( 451 'iframe', generator_api, resolve_to_config = false, ad_with_size = false, 452 requested_size = null, register_beacon, html, headers, attributes, origin, 453 component_origin, num_components); 454 } 455 456 // Open a window that waits for scripts to execute. 457 // Returns a proxy that acts like the window object, but with an extra 458 // function `execute`. See `attachContext` for more details. 459 function attachWindowContext({target="_blank", html="", headers=[], origin=""}={}) { 460 window_constructor = (url) => { 461 return window.open(url, target); 462 } 463 464 return attachContext(window_constructor, html, headers, origin); 465 } 466 467 // Attaches an ad component in a fenced frame. For this to work, this must be 468 // called in a frame that was generated with attachFrameContext() using the 469 // Protected Audience API (generator_api: 'fledge'). 470 function attachComponentFencedFrameContext( 471 index = 0, {attributes = [], html = ''} = {}) { 472 const urn = window.fence.getNestedConfigs()[index]; 473 return attachComponentFrameContext( 474 index, 'fencedframe', urn, attributes, html); 475 } 476 477 // Same as attachComponentFencedFrameContext, but in a urn iframe. 478 function attachComponentIFrameContext( 479 index = 0, {attributes = [], html = ''} = {}) { 480 const urn = navigator.adAuctionComponents(index + 1)[index]; 481 return attachComponentFrameContext(index, 'iframe', urn, attributes, html); 482 } 483 484 function attachComponentFrameContext( 485 index, element_name, urn, attributes, html) { 486 assert_not_equals( 487 document.getElementById('component_uuid_' + index), null, 488 'Component frames can only be attached to frames loaded with ' + 489 'attach*FrameContext() with `num_components` set to at least ' + 490 (index + 1) + '.'); 491 492 let frame = document.createElement(element_name); 493 attributes.forEach(attribute => { 494 frame.setAttribute(attribute[0], attribute[1]); 495 }); 496 if (element_name == 'iframe') { 497 frame.src = urn; 498 } else { 499 frame.config = urn; 500 } 501 document.body.append(frame); 502 const context_uuid = document.getElementById('component_uuid_' + index).value; 503 return buildRemoteContextForObject(frame, context_uuid, html); 504 } 505 506 // Converts a key string into a key uuid using a cryptographic hash function. 507 // This function only works in secure contexts (HTTPS). 508 async function stringToStashKey(string) { 509 // Compute a SHA-256 hash of the input string, and convert it to hex. 510 const data = new TextEncoder().encode(string); 511 const digest = await crypto.subtle.digest('SHA-256', data); 512 const digest_array = Array.from(new Uint8Array(digest)); 513 const digest_as_hex = digest_array.map(b => b.toString(16).padStart(2, '0')).join(''); 514 515 // UUIDs are structured as 8X-4X-4X-4X-12X. 516 // Use the first 32 hex digits and ignore the rest. 517 const digest_slices = [digest_as_hex.slice(0,8), 518 digest_as_hex.slice(8,12), 519 digest_as_hex.slice(12,16), 520 digest_as_hex.slice(16,20), 521 digest_as_hex.slice(20,32)]; 522 return digest_slices.join('-'); 523 } 524 525 // Create a fenced frame. Then navigate it using the given `target`, which can 526 // be either an urn:uuid or a fenced frame config object. 527 function attachFencedFrame(target) { 528 if (window.test_driver) { 529 assert_implements( 530 window.HTMLFencedFrameElement, 531 'The HTMLFencedFrameElement should be exposed on the window object'); 532 } 533 534 const fenced_frame = document.createElement('fencedframe'); 535 536 if (target instanceof FencedFrameConfig) { 537 fenced_frame.config = target; 538 } else { 539 const config = new FencedFrameConfig(target); 540 fenced_frame.config = config; 541 } 542 543 document.body.append(fenced_frame); 544 return fenced_frame; 545 } 546 547 function attachIFrame(url) { 548 const iframe = document.createElement('iframe'); 549 iframe.src = url; 550 document.body.append(iframe); 551 return iframe; 552 } 553 554 // Reads the value specified by `key` from the key-value store on the server. 555 async function readValueFromServer(key) { 556 // Resolve the key if it is a Promise. 557 key = await key; 558 559 const serverURL = `${STORE_URL}?key=${key}`; 560 const response = await fetch(serverURL); 561 if (!response.ok) 562 throw new Error('An error happened in the server'); 563 const value = await response.text(); 564 565 // The value is not stored in the server. 566 if (value === "<Not set>") 567 return { status: false }; 568 569 return { status: true, value: value }; 570 } 571 572 // Convenience wrapper around the above getter that will wait until a value is 573 // available on the server. 574 async function nextValueFromServer(key) { 575 // Resolve the key if it is a Promise. 576 key = await key; 577 578 while (true) { 579 // Fetches the test result from the server. 580 const { status, value } = await readValueFromServer(key); 581 if (!status) { 582 // The test result has not been stored yet. Retry after a while. 583 await new Promise(resolve => setTimeout(resolve, 20)); 584 continue; 585 } 586 587 return value; 588 } 589 } 590 591 // Checks the beacon data server to see if it has received a beacon with a given 592 // event type and body. 593 async function readBeaconDataFromServer(event_type, expected_body) { 594 let serverURL = `${BEACON_URL}`; 595 const response = await fetch(serverURL + "?" + new URLSearchParams({ 596 type: event_type, 597 expected_body: expected_body, 598 })); 599 if (!response.ok) 600 throw new Error('An error happened in the server ' + response.status); 601 const value = await response.text(); 602 603 // The value is not stored in the server. 604 if (value === "<Not set>") 605 return { status: false }; 606 607 return { status: true, value: value }; 608 } 609 610 // Convenience wrapper around the above getter that will wait until a value is 611 // available on the server. The server uses a hash of the concatenated event 612 // type and beacon data as the key when storing the beacon in the database. To 613 // retrieve it, we need to supply the endpoint with both pieces of information. 614 async function nextBeacon(event_type, expected_body) { 615 while (true) { 616 // Fetches the test result from the server. 617 const {status, value} = 618 await readBeaconDataFromServer(event_type, expected_body); 619 if (!status) { 620 // The test result has not been stored yet. Retry after a while. 621 await new Promise(resolve => setTimeout(resolve, 20)); 622 continue; 623 } 624 625 return value; 626 } 627 } 628 629 // Writes `value` for `key` in the key-value store on the server. 630 async function writeValueToServer(key, value, origin = '') { 631 // Resolve the key if it is a Promise. 632 key = await key; 633 634 const serverURL = `${origin}${STORE_URL}?key=${key}&value=${value}`; 635 await fetch(serverURL, {"mode": "no-cors"}); 636 } 637 638 // Fenced frames are always put in the public IP address space which is the 639 // least privileged. In case a navigation to a local data: URL or blob: URL 640 // resource is allowed, they would only be able to fetch things that are *also* 641 // in the public IP address space. So for the document described by these local 642 // URLs, we'll set them up to only communicate back to the outer page via 643 // resources obtained in the public address space. 644 function createLocalSource(key, url) { 645 return ` 646 <head> 647 <script src="${url}"><\/script> 648 </head> 649 <body> 650 <script> 651 writeValueToServer("${key}", "LOADED", /*origin=*/"${url.origin}"); 652 <\/script> 653 </body> 654 `; 655 } 656 657 function setupCSP(csp, second_csp=null) { 658 let headers = []; 659 660 headers.push(["Content-Security-Policy", "fenced-frame-src " + csp]); 661 if (second_csp != null) { 662 headers.push(["Content-Security-Policy", "frame-src " + second_csp]); 663 } 664 665 const iframe = attachIFrameContext({headers: headers}); 666 667 return iframe; 668 } 669 670 // Clicking in WPT tends to be flaky (https://crbug.com/1066891), so you may 671 // need to click multiple times to have an effect. This function clicks at 672 // coordinates `{x, y}` relative to `click_origin`, by default 3 times. Should 673 // not be used for tests where multiple clicks have distinct impact on the state 674 // of the page, but rather to bruteforce through flakes that rely on only one 675 // click. 676 async function multiClick(x, y, click_origin, times = 3) { 677 for (let i = 0; i < times; i++) { 678 let actions = new test_driver.Actions(); 679 await actions.pointerMove(x, y, {origin: click_origin}) 680 .pointerDown() 681 .pointerUp() 682 .send(); 683 } 684 }