mirror of
https://github.com/servo/servo.git
synced 2025-10-10 05:20:19 +01:00
298 lines
11 KiB
HTML
298 lines
11 KiB
HTML
<!doctype html>
|
|
<title>Navigating to a text fragment directive</title>
|
|
<meta charset=utf-8>
|
|
<link rel="help" href="https://wicg.github.io/ScrollToTextFragment/">
|
|
<meta name="timeout" content="long">
|
|
<script src="/resources/testharness.js"></script>
|
|
<script src="/resources/testharnessreport.js"></script>
|
|
<script src="/resources/testdriver.js"></script>
|
|
<script src="/resources/testdriver-vendor.js"></script>
|
|
<script>
|
|
let test_cases = [
|
|
// Test non-text fragment directives
|
|
{
|
|
fragment: '#',
|
|
expect_position: 'top',
|
|
description: 'Empty hash should scroll to top'
|
|
},
|
|
{
|
|
fragment: '#:~:text=this,is,test,page',
|
|
expect_position: 'top',
|
|
description: 'Text directive with invalid syntax (context terms without "-") should not parse as a text directive'
|
|
},
|
|
{
|
|
fragment: '#element:~:directive',
|
|
expect_position: 'element',
|
|
description: 'Generic fragment directive with existing element fragment should scroll to element'
|
|
},
|
|
{
|
|
fragment: '#:~:TEXT=test',
|
|
expect_position: 'top',
|
|
description: 'Uppercase TEXT directive should not parse as a text directive'
|
|
},
|
|
// Test exact text matching, with all combinations of context terms
|
|
{
|
|
fragment: '#:~:text=test',
|
|
expect_position: 'text',
|
|
description: 'Exact text with no context should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=this is a-,test',
|
|
expect_position: 'text',
|
|
description: 'Exact text with prefix should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=test,-page',
|
|
expect_position: 'text',
|
|
description: 'Exact text with suffix should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=this is a-,test,-page',
|
|
expect_position: 'text',
|
|
description: 'Exact text with prefix and suffix should match text'
|
|
},
|
|
// Test text range matching, with all combinations of context terms
|
|
{
|
|
fragment: '#:~:text=this,page',
|
|
expect_position: 'text',
|
|
description: 'Text range with no context should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=this-,is,test',
|
|
expect_position: 'text',
|
|
description: 'Text range with prefix should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=this,test,-page',
|
|
expect_position: 'text',
|
|
description: 'Text range with suffix should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=this-,is,test,-page',
|
|
expect_position: 'text',
|
|
description: 'Text range with prefix and suffix should match text'
|
|
},
|
|
// Test partially non-matching text ranges
|
|
{
|
|
fragment: '#:~:text=this,none',
|
|
expect_position: 'top',
|
|
description: 'Text range with non-matching endText should not match'
|
|
},
|
|
{
|
|
fragment: '#:~:text=none,page',
|
|
expect_position: 'top',
|
|
description: 'Text range with non-matching startText should not match'
|
|
},
|
|
// Test non-matching context terms
|
|
{
|
|
fragment: '#:~:text=this-,is,page,-none',
|
|
expect_position: 'top',
|
|
description: 'Text range with prefix and nonmatching suffix should not match'
|
|
},
|
|
{
|
|
fragment: '#:~:text=none-,this,test,-page',
|
|
expect_position: 'top',
|
|
description: 'Text range with nonmatching prefix and matching suffix should not match'
|
|
},
|
|
// Test percent encoded characters
|
|
{
|
|
fragment: '#:~:text=this%20is%20a%20test%20page',
|
|
expect_position: 'text',
|
|
description: 'Exact text with percent encoded spaces should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=test%20pag',
|
|
expect_position: 'top',
|
|
description: 'Non-whole-word exact text with spaces should not match'
|
|
},
|
|
{
|
|
fragment: '#:~:text=%26%2C%2D',
|
|
expect_position: 'text',
|
|
description: 'Fragment directive with percent encoded syntactical characters "&,-" should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=%E3%83%8D%E3%82%B3',
|
|
expect_position: 'text',
|
|
description: 'Fragment directive with percent encoded non-ASCII unicode character should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=!$\'()*+./:;=?@_~',
|
|
expect_position: 'text',
|
|
description: 'Fragment directive with all TextMatchChars should match text'
|
|
},
|
|
// Test multiple text directives
|
|
{
|
|
fragment: '#:~:text=this&text=test,page',
|
|
expect_position: 'text',
|
|
description: 'Multiple matching exact texts should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=tes&text=age',
|
|
expect_position: 'top',
|
|
description: 'Multiple non-whole-word exact texts should not match'
|
|
},
|
|
{
|
|
fragment: '#:~:text=none&text=test%20page',
|
|
expect_position: 'text',
|
|
description: 'A non-matching text directive followed by a matching text directive should match and scroll into view the second text directive'
|
|
},
|
|
{
|
|
fragment: '#:~:text=test%20page&directive',
|
|
expect_position: 'text',
|
|
description: 'Text directive followed by non-text directive should match text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=test&directive&text=page',
|
|
expect_position: 'text',
|
|
description: 'Multiple text directives and a non-text directive should match text'
|
|
},
|
|
// Test text directive behavior when there's an element fragment identifier
|
|
{
|
|
fragment: '#element:~:text=test',
|
|
expect_position: 'text',
|
|
description: 'Text directive with existing element fragment should match and scroll into view text'
|
|
},
|
|
{
|
|
fragment: '#pagestate:~:text=test',
|
|
expect_position: 'text',
|
|
description: 'Text directive with nonexistent element fragment should match and scroll into view text'
|
|
},
|
|
{
|
|
fragment: '#element:~:text=nomatch',
|
|
expect_position: 'element',
|
|
description: 'Non-matching text directive with existing element fragment should scroll to element'
|
|
},
|
|
{
|
|
fragment: '#pagestate:~:text=nomatch',
|
|
expect_position: 'top',
|
|
description: 'Non-matching text directive with nonexistent element fragment should not match and not scroll'
|
|
},
|
|
// Test ambiguous text matches disambiguated by context terms
|
|
{
|
|
fragment: '#:~:text=more-,test%20page',
|
|
expect_position: 'more-text',
|
|
description: 'Multiple match text directive disambiguated by prefix should match the prefixed text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=test%20page,-text',
|
|
expect_position: 'more-text',
|
|
description: 'Multiple match text directive disambiguated by suffix should match the suffixed text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=more-,test%20page,-text',
|
|
expect_position: 'more-text',
|
|
description: 'Multiple match text directive disambiguated by prefix and suffix should match the text with the given context'
|
|
},
|
|
// Test context terms separated by node boundaries
|
|
{
|
|
fragment: '#:~:text=prefix-,test%20page,-suffix',
|
|
expect_position: 'cross-node-context',
|
|
description: 'Text directive should match when context terms are separated by node boundaries'
|
|
},
|
|
// Test text directive within shadow DOM
|
|
{
|
|
fragment: '#:~:text=shadow%20text',
|
|
expect_position: 'shadow-parent',
|
|
description: 'Text directive should match text within shadow DOM'
|
|
},
|
|
// Test text directive within hidden and display none elements. These cases should not scroll into
|
|
// view, but still "match" in that they should be highlighted or otherwise visibly indicated
|
|
// if they were to become visible.
|
|
{
|
|
fragment: '#:~:text=hidden%20text',
|
|
expect_position: 'top',
|
|
description: 'Text directive should not scroll to hidden text'
|
|
},
|
|
{
|
|
fragment: '#:~:text=display%20none',
|
|
expect_position: 'top',
|
|
description: 'Text directive should not scroll to display none text'
|
|
},
|
|
// Test horizontal scroll into view
|
|
{
|
|
fragment: '#:~:text=horizontally%20scrolled%20text',
|
|
expect_position: 'horizontal-scroll',
|
|
description: 'Text directive should horizontally scroll into view'
|
|
}
|
|
];
|
|
|
|
for (const test_case of test_cases) {
|
|
promise_test(t => new Promise(resolve => {
|
|
let channel = new BroadcastChannel('scroll-to-text-fragment');
|
|
channel.addEventListener("message", e => {
|
|
resolve(e.data);
|
|
}, {once: true});
|
|
|
|
test_driver.bless('Open a URL with a text fragment directive', () => {
|
|
window.open('scroll-to-text-fragment-target.html' + test_case.fragment, '_blank', 'noopener');
|
|
});
|
|
}).then(data => {
|
|
assert_equals(data.href.indexOf(':~:'), -1, 'Expected fragment directive to be stripped from the URL.');
|
|
assert_equals(data.scrollPosition, test_case.expect_position,
|
|
`Expected ${test_case.fragment} (${test_case.description}) to scroll to ${test_case.expect_position}.`);
|
|
}), `Test navigation with fragment: ${test_case.description}.`);
|
|
}
|
|
|
|
promise_test(t => new Promise(resolve => {
|
|
let channel = new BroadcastChannel('scroll-to-text-fragment');
|
|
channel.addEventListener("message", e => {
|
|
assert_equals(e.data.href.indexOf(':~:'), -1, 'Expected fragment directive to be stripped from the URL.');
|
|
|
|
// The first navigation has no user activation.
|
|
assert_equals(e.data.scrollPosition, 'top', 'Expected window.open() with no user activation to not activate text fragment directive.');
|
|
|
|
// Now ensure that a navigation with a user activation does activate the text fragment directive.
|
|
test_driver.bless('Open a URL with a text fragment directive', () => {
|
|
window.open('scroll-to-text-fragment-target.html#:~:text=test', '_blank', 'noopener');
|
|
});
|
|
channel.addEventListener("message", e => {
|
|
resolve(e.data.scrollPosition);
|
|
}, {once: true});
|
|
}, {once: true});
|
|
|
|
window.open('scroll-to-text-fragment-target.html#:~:text=test', '_blank', 'noopener');
|
|
}).then(scrollPosition => {
|
|
assert_equals(scrollPosition, 'text', 'Expected window.open() with a user activation to scroll to text.');
|
|
}), 'Test that a text fragment directive is not activated without a user activation');
|
|
|
|
promise_test(t => new Promise(resolve => {
|
|
let channel = new BroadcastChannel('scroll-to-text-fragment');
|
|
channel.addEventListener("message", e => {
|
|
assert_equals(e.data.href.indexOf(':~:'), -1, 'Expected fragment directive to be stripped from the URL.');
|
|
|
|
// The first navigation has an opener.
|
|
assert_equals(e.data.scrollPosition, 'top', 'Expected window.open() with opener to not activate text fragment directive.');
|
|
|
|
// Now ensure that a navigation with noopener does activate the text fragment directive.
|
|
test_driver.bless('Open a URL with a text fragment directive', () => {
|
|
window.open('scroll-to-text-fragment-target.html#:~:text=test', '_blank', 'noopener');
|
|
});
|
|
channel.addEventListener("message", e => {
|
|
resolve(e.data.scrollPosition);
|
|
}, {once: true});
|
|
}, {once: true});
|
|
|
|
test_driver.bless('Open a URL with a text fragment directive', () => {
|
|
window.open('scroll-to-text-fragment-target.html#:~:text=test', '_blank');
|
|
});
|
|
}).then(scrollPosition => {
|
|
assert_equals(scrollPosition, 'text', 'Expected window.open() with noopener to scroll to text.');
|
|
}), 'Test that a text fragment directive is not activated when there is a window opener.');
|
|
|
|
promise_test(t => new Promise(resolve => {
|
|
let channel = new BroadcastChannel('scroll-to-text-fragment');
|
|
channel.addEventListener("message", e => {
|
|
resolve(e.data);
|
|
}, {once: true});
|
|
|
|
let frame = document.createElement('iframe');
|
|
document.body.appendChild(frame);
|
|
|
|
test_driver.bless('Navigate the iframe with a text fragment directive', () => {
|
|
frame.src = 'scroll-to-text-fragment-target.html#:~:text=test';
|
|
});
|
|
}).then(data => {
|
|
assert_equals(data.href.indexOf(':~:'), -1, 'Expected fragment directive to be stripped from the URL.');
|
|
assert_equals(data.scrollPosition, 'top', 'Expected iframe navigation to not activate text fragment directive.');
|
|
}), 'Test that a text fragment directive is not activated within an iframe.');
|
|
</script>
|