DocumentRules: Add WPTs

Bug: 1371522
Change-Id: Iffbef05ee295b73b7ab6eff1078684df64dba2e7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4048640
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Reviewed-by: Stephen Chenney <schenney@chromium.org>
Commit-Queue: Adithya Srinivasan <adithyas@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1076377}
diff --git a/speculation-rules/prefetch/document-rules.https.html b/speculation-rules/prefetch/document-rules.https.html
new file mode 100644
index 0000000..a5030f6
--- /dev/null
+++ b/speculation-rules/prefetch/document-rules.https.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script src="/common/subset-tests-by-key.js"></script>
+
+<meta name="variant" content="?include=defaultPredicate">
+<meta name="variant" content="?include=hrefMatches">
+<meta name="variant" content="?include=and">
+<meta name="variant" content="?include=or">
+<meta name="variant" content="?include=not">
+<meta name="variant" content="?include=invalidPredicate">
+<meta name="variant" content="?include=linkInShadowTree">
+
+<body>
+<script>
+  subsetTestByKey('defaultPredicate', promise_test, async t => {
+    assert_implements(HTMLScriptElement.supports('speculationrules'),
+      'Speculation Rules not supported');
+
+    const url = getPrefetchUrl();
+    addLink(url);
+    insertDocumentRule();
+    await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+    assert_equals(await isUrlPrefetched(url), 1);
+  }, 'test document rule with no predicate');
+
+  subsetTestByKey('hrefMatches', promise_test, async t => {
+    assert_implements(HTMLScriptElement.supports('speculationrules'),
+      'Speculation Rules not supported');
+
+    insertDocumentRule({ href_matches: '*\\?uuid=*&foo=bar' });
+
+    const url_1 = getPrefetchUrl({foo: 'bar'});
+    addLink(url_1);
+    const url_2 = getPrefetchUrl({foo: 'buzz'});
+    addLink(url_2)
+    await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+    assert_equals(await isUrlPrefetched(url_1), 1);
+    assert_equals(await isUrlPrefetched(url_2), 0);
+  }, 'test href_matches document rule');
+
+  subsetTestByKey('and', promise_test, async t => {
+    assert_implements(HTMLScriptElement.supports('speculationrules'),
+      'Speculation Rules not supported');
+
+    insertDocumentRule({
+      'and': [
+        { href_matches: '*\\?*foo=bar*' },
+        { href_matches: '*\\?*fizz=buzz*' }]
+    });
+
+    const url_1 = getPrefetchUrl({foo: 'bar'});
+    const url_2 = getPrefetchUrl({fizz: 'buzz'});
+    const url_3 = getPrefetchUrl({foo: 'bar', fizz: 'buzz'});
+    [url_1, url_2, url_3].forEach(url => addLink(url));
+    await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+    assert_equals(await isUrlPrefetched(url_1), 0);
+    assert_equals(await isUrlPrefetched(url_2), 0);
+    assert_equals(await isUrlPrefetched(url_3), 1);
+  }, 'test document rule with conjunction predicate');
+
+  subsetTestByKey('or', promise_test, async t => {
+    assert_implements(HTMLScriptElement.supports('speculationrules'),
+      'Speculation Rules not supported');
+
+    insertDocumentRule({
+      'or': [
+        { href_matches: '*\\?*foo=bar*' },
+        { href_matches: '*\\?*fizz=buzz*' }]
+    });
+
+    const url_1 = getPrefetchUrl({ foo: 'buzz' });
+    const url_2 = getPrefetchUrl({ fizz: 'buzz' });
+    const url_3 = getPrefetchUrl({ foo: 'bar'});
+    [url_1, url_2, url_3].forEach(url => addLink(url));
+    await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+    assert_equals(await isUrlPrefetched(url_1), 0);
+    assert_equals(await isUrlPrefetched(url_2), 1);
+    assert_equals(await isUrlPrefetched(url_3), 1);
+  }, 'test document rule with disjunction predicate');
+
+  subsetTestByKey('not', promise_test, async t => {
+    assert_implements(HTMLScriptElement.supports('speculationrules'),
+      "Speculation Rules not supported");
+
+    insertDocumentRule({ not: { href_matches: '*\\?uuid=*&foo=bar' } });
+
+    const url_1 = getPrefetchUrl({foo: 'bar'});
+    addLink(url_1);
+    const url_2 = getPrefetchUrl({foo: 'buzz'});
+    addLink(url_2)
+    await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+    assert_equals(await isUrlPrefetched(url_1), 0);
+    assert_equals(await isUrlPrefetched(url_2), 1);
+  }, 'test document rule with negation predicate');
+
+  subsetTestByKey('invalidPredicate', promise_test, async t => {
+    assert_implements(HTMLScriptElement.supports('speculationrules'),
+      'Speculation Rules not supported');
+
+    const url = getPrefetchUrl();
+    addLink(url);
+    insertDocumentRule({invalid: 'predicate'});
+    await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+    assert_equals(await isUrlPrefetched(url), 0);
+  }, 'invalid predicate should not throw error or start prefetch');
+
+  subsetTestByKey('linkInShadowTree', promise_test, async t => {
+    assert_implements(HTMLScriptElement.supports('speculationrules'),
+      'Speculation Rules not supported');
+
+    insertDocumentRule();
+
+    // Create shadow root.
+    const shadowHost = document.createElement('div');
+    document.body.appendChild(shadowHost);
+    const shadowRoot = shadowHost.attachShadow({mode: 'open'});
+
+    const url = getPrefetchUrl();
+    addLink(url, shadowRoot);
+    await new Promise(resolve => t.step_timeout(resolve, 2000));
+
+    assert_equals(await isUrlPrefetched(url), 1);
+  }, 'test that matching link in a shadow tree is prefetched');
+
+</script>
+</body>
diff --git a/speculation-rules/prefetch/referrer-policy-from-rules.https.html b/speculation-rules/prefetch/referrer-policy-from-rules.https.html
index 01e32ba..ce3db0f 100644
--- a/speculation-rules/prefetch/referrer-policy-from-rules.https.html
+++ b/speculation-rules/prefetch/referrer-policy-from-rules.https.html
@@ -6,7 +6,8 @@
 <meta name="variant" content="?2-2">
 <meta name="variant" content="?3-3">
 <meta name="variant" content="?4-4">
-<meta name="variant" content="?5-last">
+<meta name="variant" content="?5-5">
+<meta name="variant" content="?6-last">
 
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
@@ -38,6 +39,25 @@
   assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
 
   const agent = await spawnWindow(t);
+  const next_url = agent.getExecutorURL({ page: 2 });
+  await agent.execute_script((url) => {
+    const a = addLink(url);
+    a.referrerPolicy = 'no-referrer';
+    insertDocumentRule(undefined, { referrer_policy: 'strict-origin' });
+  }, [next_url]);
+  await new Promise(resolve => t.step_timeout(resolve, 2000));
+  await agent.navigate(next_url);
+
+  const headers = await agent.getRequestHeaders();
+  assert_prefetched(headers, 'must be prefetched');
+  const expected_referrer = next_url.origin + '/';
+  assert_equals(headers.referer, expected_referrer, 'must send the origin as the referrer');
+}, 'with "strict-origin" referrer policy in rule set override "no-referrer" of link');
+
+subsetTest(promise_test, async t => {
+  assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+  const agent = await spawnWindow(t);
   await agent.setReferrerPolicy("unsafe-url");
 
   const nextURL = agent.getExecutorURL({ page: 2 });
diff --git a/speculation-rules/prefetch/referrer-policy.https.html b/speculation-rules/prefetch/referrer-policy.https.html
index aaeae16..1987d2e 100644
--- a/speculation-rules/prefetch/referrer-policy.https.html
+++ b/speculation-rules/prefetch/referrer-policy.https.html
@@ -4,7 +4,8 @@
 <!--Split test cases due to the use of timeouts in speculation rules test utilities.-->
 <meta name="variant" content="?1-1">
 <meta name="variant" content="?2-2">
-<meta name="variant" content="?3-last">
+<meta name="variant" content="?3-3">
+<meta name="variant" content="?4-last">
 
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
@@ -63,4 +64,25 @@
   assert_equals(headers.referer, '', "must send no referrer");
 }, 'with "no-referrer" referrer policy');
 
+subsetTest(promise_test, async t => {
+  assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+  const agent = await spawnWindow(t);
+  await agent.setReferrerPolicy("no-referrer");
+
+  const next_url = agent.getExecutorURL({ page: 2 });
+  await agent.execute_script((url) => {
+    const a = addLink(url);
+    a.referrerPolicy = 'strict-origin';
+    insertDocumentRule();
+  }, [next_url]);
+  await new Promise(resolve => t.step_timeout(resolve, 2000));
+  await agent.navigate(next_url);
+
+  const headers = await agent.getRequestHeaders();
+  const expected_referrer = next_url.origin + '/';
+  assert_prefetched(headers, 'must be prefetched');
+  assert_equals(headers.referer, expected_referrer);
+}, 'with "strict-origin" link referrer policy overriding "no-referrer" of referring page');
+
 </script>
diff --git a/speculation-rules/prefetch/resources/utils.sub.js b/speculation-rules/prefetch/resources/utils.sub.js
index f532f35..b29dd6b 100644
--- a/speculation-rules/prefetch/resources/utils.sub.js
+++ b/speculation-rules/prefetch/resources/utils.sub.js
@@ -96,14 +96,17 @@
   }
 }
 
+// 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) {
-  let urls = [];
-  for (let i=0; i<n; i++) {
-    let params = new URLSearchParams({uuid: token()});
-    urls.push(new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL));
-  }
-  return urls;
+  return Array.from({ length: n }, () => getPrefetchUrl());
 }
 
 function getRedirectUrl() {
@@ -132,6 +135,28 @@
   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',
+      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_equals(requestHeaders.sec_purpose, "prefetch", description);