Runs: Chrome 158.0.8110.0 (wpt@850b36a56, 2026-10-18) | Firefox 160.0a1 (wpt@b58866519, 2026-10-18) | Safari 254 preview (wpt@850b36a56, 2026-10-18) | Ladybird 1.0-e7b21 (wpt@850b36a56, 2026-10-18) | Servo Servo 0.7 (wpt@df642c414, 2026-10-17) | Blitz fb8d6c15e (wpt@cd1d8aa5b, 2026-10-17)
| Chrome | Firefox | Safari | Ladybird | Servo | Blitz | |
|---|---|---|---|---|---|---|
| Total | 1/1 | 1/1 | 1/1 | 0/1 | 0/1 | NOT RUN |
| Subtest | Chrome | Firefox | Safari | Ladybird | Servo | Blitz |
|---|---|---|---|---|---|---|
| test that matching link in a shadow tree is prefetched | PASS | PASS | PASS | — | — | — |
/speculation-rules/prefetch/document-rules.https.html
<!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.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"> <meta name="variant" content="?include=linkHrefChanged"> <meta name="variant" content="?include=newRuleSetAdded"> <meta name="variant" content="?include=selectorMatches"> <meta name="variant" content="?include=selectorMatchesScopingRoot"> <meta name="variant" content="?include=selectorMatchesDisplayNone"> <meta name="variant" content="?include=selectorMatchesDisplayLocked"> <meta name="variant" content="?include=unslottedLink"> <meta name="variant" content="?include=immediateMutation"> <meta name="variant" content="?include=baseURLChangedBySameDocumentNavigation"> <meta name="variant" content="?include=baseURLChangedByBaseElement"> <meta name="variant" content="?include=linkToSelfFragment"> <body> <script> setup(() => assertSpeculationRulesIsSupported()); subsetTestByKey('defaultPredicate', promise_test, async t => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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'); subsetTestByKey('linkHrefChanged', promise_test, async t => { insertDocumentRule({href_matches: "*\\?*foo=bar*"}); const url = getPrefetchUrl(); const link = addLink(url); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 0); const matching_url = getPrefetchUrl({foo: 'bar'}); link.href = matching_url; await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(matching_url), 1); }, 'test that changing the href of an invalid link to a matching value triggers a prefetch'); subsetTestByKey('newRuleSetAdded', promise_test, async t => { insertDocumentRule({href_matches: "*\\?*foo=bar*"}); const url = getPrefetchUrl({fizz: "buzz"}); addLink(url); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 0); insertDocumentRule({href_matches: "*\\?*fizz=buzz*"}); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 1); }, 'test that adding a second rule set triggers prefetch'); subsetTestByKey('selectorMatches', promise_test, async t => { insertDocumentRule({ selector_matches: 'a.important-link' }); const url_1 = getPrefetchUrl({foo: 'bar'}); const importantLink = addLink(url_1); importantLink.className = 'important-link'; 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 selector_matches document rule'); subsetTestByKey('selectorMatchesScopingRoot', promise_test, async t => { insertDocumentRule({ selector_matches: ':root > body > a' }); const url_1 = getPrefetchUrl({ foo: 'bar' }); addLink(url_1); const url_2 = getPrefetchUrl({ foo: 'buzz' }); const extraContainer = document.createElement('div'); document.body.appendChild(extraContainer); addLink(url_2, extraContainer); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url_1), 1); assert_equals(await isUrlPrefetched(url_2), 0); }, 'test selector_matches with :root'); subsetTestByKey('selectorMatchesDisplayNone', promise_test, async t => { const style = document.createElement('style'); style.innerText = ".important-section { display: none; }"; document.head.appendChild(style); insertDocumentRule(); const importantSection = document.createElement('div'); importantSection.className = 'important-section'; document.body.appendChild(importantSection); const url = getPrefetchUrl(); addLink(url, importantSection); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 0); style.remove(); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 1); }, 'test selector_matches with link inside display:none container'); subsetTestByKey('selectorMatchesDisplayLocked', promise_test, async t => { const style = document.createElement('style'); style.innerText = ".important-section { content-visibility: hidden; }"; document.head.appendChild(style); insertDocumentRule({ selector_matches: '.important-section a' }); const importantSection = document.createElement('div'); importantSection.className = 'important-section'; document.body.appendChild(importantSection); const url = getPrefetchUrl(); addLink(url, importantSection); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 0); style.remove(); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 1); }, 'test selector_matches with link inside display locked container'); subsetTestByKey('unslottedLink', promise_test, async t => { insertDocumentRule(); // Create shadow root. const shadowHost = document.createElement('div'); document.body.appendChild(shadowHost); const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); // Add unslotted link. const url = getPrefetchUrl(); addLink(url, shadowHost); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 0); }, 'test that unslotted link never matches document rule'); subsetTestByKey('immediateMutation', promise_test, async t => { // Add a link and allow it to get its style computed. // (Double RAF lets this happen normally.) const url = getPrefetchUrl(); const link = addLink(url, document.body); await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); // Add a document rule and then immediately change the DOM to make it match. insertDocumentRule({ selector_matches: '.late-class *' }); document.body.className = 'late-class'; await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 1); }, 'test that selector_matches predicates respect changes immediately'); const baseURLChangedTestFixture = (testName, modifyBaseURLFunc) => { return subsetTestByKey(testName, promise_test, async t => { const url = getPrefetchUrl(); const link = addLink(url); const url_pattern_string = `prefetch.py${url.search}`; // Insert a document rule with a url pattern predicate that uses a // relative URL which will not match with |url|, due to |document.baseURI| // being different from |url|'s path. assert_false((new URLPattern(url_pattern_string, document.baseURI)).test(url)); insertDocumentRule({ href_matches: url_pattern_string }); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 0); // Change the baseURL of the document to |url|. |url| should now be // prefetched. modifyBaseURLFunc(url); assert_true((new URLPattern(url_pattern_string, document.baseURI)).test(url)); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 1); }); } baseURLChangedTestFixture('baseURLChangedBySameDocumentNavigation', url => { history.pushState({}, "", url); }); baseURLChangedTestFixture('baseURLChangedByBaseElement', url => { const base = document.createElement('base'); base.href = url; document.head.appendChild(base); }); subsetTestByKey('linkToSelfFragment', promise_test, async t => { const url = getPrefetchUrl(); history.pushState({}, "", url); addLink(new URL('#fragment', url)); insertDocumentRule(); await new Promise(resolve => t.step_timeout(resolve, 2000)); assert_equals(await isUrlPrefetched(url), 0); }, 'test that a fragment link to the current document does not prefetch'); </script> </body>