chromium/third_party/blink/web_tests/external/wpt/speculation-rules/prefetch/resources/utils.sub.js

/**
 * Utilities for initiating prefetch via speculation rules.
 */

// Resolved URL to find this script.
const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI);
// Hostname for cross origin urls.
const PREFETCH_PROXY_BYPASS_HOST = "{{hosts[alt][]}}";

class PrefetchAgent extends RemoteContext {
  constructor(uuid, t) {
    super(uuid);
    this.t = t;
  }

  getExecutorURL(options = {}) {
    let {hostname, username, password, protocol, executor, ...extra} = options;
    let params = new URLSearchParams({uuid: this.context_id, ...extra});
    if(executor === undefined) {
      executor = "executor.sub.html";
    }
    let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL);
    if(hostname !== undefined) {
      url.hostname = hostname;
    }
    if(username !== undefined) {
      url.username = username;
    }
    if(password !== undefined) {
      url.password = password;
    }
    if(protocol !== undefined) {
      url.protocol = protocol;
      url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}";
    }
    return url;
  }

  // Requests prefetch via speculation rules.
  //
  // In the future, this should also use browser hooks to force the prefetch to
  // occur despite heuristic matching, etc., and await the completion of the
  // prefetch.
  async forceSinglePrefetch(url, extra = {}, wait_for_completion = true) {
    await this.execute_script((url, extra) => {
      insertSpeculationRules({ prefetch: [{source: 'list', urls: [url], ...extra}] });
    }, [url, extra]);
    if (!wait_for_completion) {
      return Promise.resolve();
    }
    return new Promise(resolve => this.t.step_timeout(resolve, 2000));
  }

  // `url` is the URL to navigate.
  //
  // `expectedDestinationUrl` is the expected URL after navigation.
  // When omitted, `url` is used. When explicitly null, the destination URL is
  // not validated.
  async navigate(url, {expectedDestinationUrl} = {}) {
    await this.execute_script((url) => {
      window.executor.suspend(() => {
        location.href = url;
      });
    }, [url]);
    if (expectedDestinationUrl === undefined) {
      expectedDestinationUrl = url;
    }
    if (expectedDestinationUrl) {
      expectedDestinationUrl.username = '';
      expectedDestinationUrl.password = '';
      assert_equals(
          await this.execute_script(() => location.href),
          expectedDestinationUrl.toString(),
          "expected navigation to reach destination URL");
    }
    await this.execute_script(() => {});
  }

  async getRequestHeaders() {
    return this.execute_script(() => requestHeaders);
  }

  async getResponseCookies() {
    return this.execute_script(() => {
      let cookie = {};
      document.cookie.split(/\s*;\s*/).forEach((kv)=>{
        let [key, value] = kv.split(/\s*=\s*/);
        cookie[key] = value;
      });
      return cookie;
    });
  }

  async getRequestCookies() {
    return this.execute_script(() => window.requestCookies);
  }

  async getRequestCredentials() {
    return this.execute_script(() => window.requestCredentials);
  }

  async setReferrerPolicy(referrerPolicy) {
    return this.execute_script(referrerPolicy => {
      const meta = document.createElement("meta");
      meta.name = "referrer";
      meta.content = referrerPolicy;
      document.head.append(meta);
    }, [referrerPolicy]);
  }

  async getDeliveryType(){
    return this.execute_script(() => {
      return performance.getEntriesByType("navigation")[0].deliveryType;
    });
  }
}

// Produces a URL with a UUID which will record when it's prefetched.
// |extra_params| can be specified to add extra search params to the generated
// URL.
function getPrefetchUrl(extra_params={}) {
  let params = new URLSearchParams({ uuid: token(), ...extra_params });
  return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL);
}

// Produces n URLs with unique UUIDs which will record when they are prefetched.
function getPrefetchUrlList(n) {
  return Array.from({ length: n }, () => getPrefetchUrl());
}

async function isUrlPrefetched(url) {
  let response = await fetch(url, {redirect: 'follow'});
  assert_true(response.ok);
  return response.json();
}

// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
async function spawnWindowWithReference(t, options = {}, uuid = token()) {
  let agent = new PrefetchAgent(uuid, t);
  let w = window.open(agent.getExecutorURL(options), '_blank', options);
  t.add_cleanup(() => w.close());
  return {"agent":agent, "window":w};
}

// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
async function spawnWindow(t, options = {}, uuid = token()) {
  let agent_window_pair = await spawnWindowWithReference(t, options, uuid);
  return agent_window_pair.agent;
}

function insertSpeculationRules(body) {
  let script = document.createElement('script');
  script.type = 'speculationrules';
  script.textContent = JSON.stringify(body);
  document.head.appendChild(script);
}

// Creates and appends <a href=|href|> to |insertion point|. If
// |insertion_point| is not specified, document.body is used.
function addLink(href, insertion_point=document.body) {
  const a = document.createElement('a');
  a.href = href;
  insertion_point.appendChild(a);
  return a;
}

// Inserts a prefetch document rule with |predicate|. |predicate| can be
// undefined, in which case the default predicate will be used (i.e. all links
// in document will match).
function insertDocumentRule(predicate, extra_options={}) {
  insertSpeculationRules({
    prefetch: [{
      source: 'document',
      eagerness: 'eager',
      where: predicate,
      ...extra_options
    }]
  });
}

function assert_prefetched (requestHeaders, description) {
  assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
  assert_in_array(requestHeaders.sec_purpose,
                  ["prefetch", "prefetch;anonymous-client-ip"], description);
}

function assert_not_prefetched (requestHeaders, description){
  assert_equals(requestHeaders.purpose, "", description);
  assert_equals(requestHeaders.sec_purpose, "", description);
}

// Use nvs_header query parameter to ask the wpt server
// to populate No-Vary-Search response header.
function addNoVarySearchHeaderUsingQueryParam(url, value){
  if(value){
    url.searchParams.append("nvs_header", value);
  }
}