| <!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> |