blob: 76dc83ecace0eb702936f322040eb8a4792beb06 [file] [edit]
<!DOCTYPE html><!-- webkit-test-runner [ SpeculationRulesPrefetchEnabled=true ] -->
<meta charset="utf-8">
<meta name="timeout" content="long">
<title>Conservative document rules lazy matching on interaction</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<body>
<script>
setup(() => {
assert_implements(
'supports' in HTMLScriptElement,
'HTMLScriptElement.supports must be supported');
assert_implements(
HTMLScriptElement.supports('speculationrules'),
'<script type="speculationrules"> must be supported');
});
const PREFETCH_RESOURCE_URL = new URL('/speculation-rules/prefetch/resources/prefetch.py', location.href);
function getPrefetchUrl(extra_params = {}) {
let params = new URLSearchParams({ uuid: token(), ...extra_params });
return new URL(`${PREFETCH_RESOURCE_URL}?${params}`);
}
async function isUrlPrefetched(url) {
let response = await fetch(url, { redirect: 'follow' });
return response.json();
}
// Poll until prefetch arrives or timeout. Use for positive assertions instead
// of fixed delays.
async function waitForPrefetch(url, timeout = 5000) {
const start = performance.now();
while (performance.now() - start < timeout) {
if (await isUrlPrefetched(url) > 0)
return true;
await new Promise(r => setTimeout(r, 50));
}
return false;
}
// Yield to the event loop so that any synchronously-initiated prefetch
// network requests have a chance to reach the server. Two round-trips:
// one to flush the event loop, one to let the server process the request.
async function flushNetworking() {
await fetch('/resources/blank.html');
await fetch('/resources/blank.html');
}
function insertSpeculationRules(body) {
let script = document.createElement('script');
script.type = 'speculationrules';
script.textContent = JSON.stringify(body);
document.head.appendChild(script);
return script;
}
function addLink(href, className, parent = document.body) {
const a = document.createElement('a');
a.href = href;
a.textContent = 'link';
if (className)
a.className = className;
parent.appendChild(a);
return a;
}
function triggerPointerDown(element) {
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
element.dispatchEvent(new PointerEvent('pointerdown', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y, button: 0, pointerType: 'mouse'
}));
element.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y, button: 0
}));
}
function triggerHover(element) {
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
element.dispatchEvent(new PointerEvent('pointerover', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y, pointerType: 'mouse'
}));
element.dispatchEvent(new MouseEvent('mouseover', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y
}));
element.dispatchEvent(new PointerEvent('pointermove', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y, pointerType: 'mouse'
}));
element.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true, cancelable: true, view: window,
clientX: x, clientY: y
}));
}
// Test: Conservative document rule with selector_matches is lazily matched
// on pointerdown (not during style recalc / anchor scan).
promise_test(async t => {
const matchUrl = getPrefetchUrl({ test: 'selector-match' });
const noMatchUrl = getPrefetchUrl({ test: 'selector-no-match' });
const matchLink = addLink(matchUrl, 'prefetch-me');
const noMatchLink = addLink(noMatchUrl, 'do-not-prefetch');
matchLink.addEventListener('click', e => e.preventDefault());
noMatchLink.addEventListener('click', e => e.preventDefault());
t.add_cleanup(() => { matchLink.remove(); noMatchLink.remove(); });
const rules = insertSpeculationRules({
prefetch: [{
source: 'document',
eagerness: 'conservative',
where: { selector_matches: '.prefetch-me' }
}]
});
t.add_cleanup(() => rules.remove());
// With the optimization, the anchor scan is skipped for conservative-only
// rules, so m_prefetchEagerness stays "none" instead of being set to
// "conservative" by the scan.
if (window.internals)
assert_equals(internals.anchorPrefetchEagerness(matchLink), 'none', 'eagerness should be none (scan skipped for conservative-only rules)');
// Hover should not trigger conservative prefetch.
triggerHover(matchLink);
await flushNetworking();
assert_equals(await isUrlPrefetched(matchUrl), 0, 'matching link should not be prefetched on hover');
assert_equals(await isUrlPrefetched(noMatchUrl), 0, 'non-matching link should not be prefetched on hover');
// Pointerdown on matching link should trigger lazy match and prefetch.
triggerPointerDown(matchLink);
assert_true(await waitForPrefetch(matchUrl), 'matching link should be prefetched after pointerdown');
// Pointerdown on non-matching link should NOT trigger prefetch.
triggerPointerDown(noMatchLink);
await flushNetworking();
assert_equals(await isUrlPrefetched(noMatchUrl), 0, 'non-matching link should not be prefetched after pointerdown');
}, 'Conservative document rule with selector_matches lazily matches on pointerdown');
// Test: Conservative document rule with href_matches pattern lazily matches.
promise_test(async t => {
const url = getPrefetchUrl({ test: 'href-pattern' });
const link = addLink(url);
link.addEventListener('click', e => e.preventDefault());
t.add_cleanup(() => link.remove());
const rules = insertSpeculationRules({
prefetch: [{
source: 'document',
eagerness: 'conservative',
where: { href_matches: '/speculation-rules/prefetch/resources/prefetch.py*' }
}]
});
t.add_cleanup(() => rules.remove());
if (window.internals)
assert_equals(internals.anchorPrefetchEagerness(link), 'none', 'eagerness should be none (scan skipped)');
// Hover should not trigger conservative prefetch.
triggerHover(link);
await flushNetworking();
assert_equals(await isUrlPrefetched(url), 0, 'should not be prefetched on hover');
triggerPointerDown(link);
assert_true(await waitForPrefetch(url), 'should be prefetched after pointerdown');
}, 'Conservative document rule with href_matches pattern lazily matches on pointerdown');
// Test: Dynamically added link is lazily matched on pointerdown without
// needing a style recalc anchor scan.
promise_test(async t => {
const url = getPrefetchUrl({ test: 'dynamic-link' });
// Insert rules first, before the link exists.
const rules = insertSpeculationRules({
prefetch: [{
source: 'document',
eagerness: 'conservative',
where: { selector_matches: '.late-addition' }
}]
});
t.add_cleanup(() => rules.remove());
// Add link after rules are already in place.
const link = addLink(url, 'late-addition');
link.addEventListener('click', e => e.preventDefault());
t.add_cleanup(() => link.remove());
if (window.internals)
assert_equals(internals.anchorPrefetchEagerness(link), 'none', 'dynamically added link eagerness should be none');
// Hover should not trigger conservative prefetch.
triggerHover(link);
await flushNetworking();
assert_equals(await isUrlPrefetched(url), 0, 'dynamically added link should not be prefetched on hover');
triggerPointerDown(link);
assert_true(await waitForPrefetch(url), 'dynamically added link should be prefetched after pointerdown');
}, 'Dynamically added link with conservative document rule lazily matches on pointerdown');
</script>
</body>