<!DOCTYPE html>
<title>fragmentDirective.createSelectorDirective</title>
<meta charset="utf-8">
<link rel="help" href="https://wicg.github.io/ScrollToTextFragment/issues/160">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
.causeScrolling {
height: 200vh;
}
</style>
<script>
function getRange(startNode, startOffset, endNode, endOffset) {
const range = startNode.ownerDocument.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);
return range;
}
function rAF() {
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
}
// Performs a same-document navigation to a text directive whose content is
// `directive` so that the page attempts to scroll to it. Navigation is
// performed in the given window `win`, if null uses main frame.
async function scrollToSelectorDirective(directive, test, win) {
let w = window;
if (typeof win != 'undefined')
w = win;
w.location.hash = `:~:${directive}`;
await test.step_wait(() => window.scrollY > 0, "Wait for scroll");
// Needed since the text fragment will be kept in view until
// scriptable actions are performed in the next frame.
await rAF();
}
function reset() {
window.scrollTo(0, 0);
document.getElementById('iframe').contentWindow.scrollTo(0, 0);
}
// Returns true if the center of `element` is within the visual viewport.
function isInViewport(element) {
const viewportRect = {
left: visualViewport.offsetLeft,
top: visualViewport.offsetTop,
right: visualViewport.offsetLeft + visualViewport.width,
bottom: visualViewport.offsetTop + visualViewport.height
};
const elementRect = element.getBoundingClientRect();
const elementCenter = {
x: elementRect.left + elementRect.width / 2,
y: elementRect.top + elementRect.height / 2
};
return elementCenter.x > viewportRect.left &&
elementCenter.x < viewportRect.right &&
elementCenter.y > viewportRect.top &&
elementCenter.y < viewportRect.bottom;
}
</script>
<body>
<div class="causeScrolling"></div>
<p id="p1">The quick brown fox...</p>
<img width="100" height="100" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAABVJREFUeNpiZPjPgAyYGBgo4gMEGABPkgEJUvO9mgAAAABJRU5ErkJggg==">
<p id="p2">...jumped over the lazy dog</p>
<p id="japanese">このテストへようこそ</p>
<p id="empty"></p>
<div class="causeScrolling"></div>
<p id="p3">quick is an ambiguous word</p>
<div class="causeScrolling"></div>
<p id="p4">this sentence & words, contain percent-encoded characters</p>
<div class="causeScrolling"></div>
<p id="p5">Here's some whitespace: and it's done.</p>
<div class="causeScrolling"></div>
<iframe id='iframe' src="resources/simple-frame.html"></iframe>
<script>
const p1 = document.getElementById("p1");
const p2 = document.getElementById("p2");
const p3 = document.getElementById("p3");
const p4 = document.getElementById("p4");
const japanese = document.getElementById("japanese");
const empty = document.getElementById("empty");
const initialIFrameURL = document.getElementById('iframe').contentWindow.location.href;
const test_cases = [
{
// Simple sentence within a block.
range: getRange(p1.firstChild, 0, p1.firstChild, 19),
range_text: 'The quick brown fox',
description: 'Simple text run'
},
{
// Range spans multiple blocks
range: getRange(p1.firstChild, 10, p2.firstChild, 14),
range_text: 'brown fox...\n \n ...jumped over',
description: 'Cross-block text'
},
{
// Ensure the generator can correctly disambiguate a range that occurs
// more than once.
range: getRange(p1.firstChild, 4, p1.firstChild, 9),
range_text: 'quick',
description: 'Ambiguous text - first instance'
},
{
// Same as above but this time the range is the other instance, to make
// sure the generator doesn't just get lucky.
range: getRange(p3.firstChild, 0, p3.firstChild, 5),
range_text: 'quick',
description: 'Ambiguous text - last instance'
},
{
// Ensure the generator correctly percent encodes characters that are
// part of the text fragment syntax: ampersand, comma, and hyphen.
range: getRange(p4.firstChild, 5, p4.firstChild, 46),
range_text: 'sentence & words, contain percent-encoded',
description: 'Percent encoded set'
},
{
// Ensure the generator expands a text directive that doesn't start/end
// on a text boundary.
range: getRange(p3.firstChild, 13, p3.firstChild, 19),
range_text: 'mbiguo',
description: 'Non-word boundary'
},
{
// Ensure the generator expands a text directive that doesn't start/end
// on a text boundary in a non-space broken language like Japanese.
// (And that the unicode is correctly percent-encoded).
range: getRange(japanese.firstChild, 6, japanese.firstChild, 9),
range_text: 'ようこ',
description: 'Non-word boundary Japanese'
},
{
// Ensure that using getSelection instead of a range works as well.
range: getRange(p1.firstChild, 0, p1.firstChild, 19),
range_text: 'The quick brown fox',
description: 'window.getSelection()',
use_selection: true
},
{
// Ensure passing a null range rejects the returned promise.
range: null,
range_text: null,
description: 'Null range.',
rejects_js: TypeError
},
{
// Ensure passing a collapsed range rejects the returned promise.
range: getRange(p1.firstChild, 1, p1.firstChild, 1),
range_text: '',
description: 'Collapsed range.',
rejects_dom: 'NotSupportedError'
},
{
// Ensure passing a range of just whitespace rejects.
range: getRange(p5.firstChild, 6, p5.firstChild, 7),
range_text: ' ',
description: 'White space.',
rejects_dom: 'OperationError'
},
{
// Ensure passing a range of just non-breaking whitespace rejects.
range: getRange(p5.firstChild, 23, p5.firstChild, 26),
range_text: '\xa0\xa0\xa0',
description: 'Non-breaking white space.',
rejects_dom: 'OperationError'
},
];
onload = async () => {
for (let test of test_cases) {
promise_test(async (t) => {
reset();
let range = test.range;
let start_element;
if (range) {
start_element = (range.startContainer.nodeType == Node.TEXT_NODE)
? range.startContainer.parentElement
: range.startContainer;
assert_false(isInViewport(start_element), 'PRECONDITION: Target is not in view.');
assert_equals(range.toString(), test.range_text, 'PRECONDITION: Correct range.');
}
if (Boolean(test['use_selection'])) {
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
range = selection;
}
if ('rejects_js' in test) {
return promise_rejects_js(t, test.rejects_js, document.fragmentDirective
.createSelectorDirective(range));
}
const result_promise = document.fragmentDirective.createSelectorDirective(range);
if ('rejects_dom' in test) {
return promise_rejects_dom(t, test.rejects_dom, result_promise);
} else {
const result = await result_promise;
await scrollToSelectorDirective(result, t);
assert_true(isInViewport(start_element), '`p1 is scrolled into view.');
}
}, test.description);
}
// IFrame tests
{
function elemP() {
return frames[0].document.getElementById('iframetext');
}
// Simple test that createSelectorDirective works from inside an iframe document.
promise_test(async (t) => {
reset();
const iframeDoc = frames[0].document;
assert_equals(iframeDoc.scrollingElement.scrollTop, 0, 'PRECONDITION: Iframe unscrolled');
const range = getRange(elemP().firstChild, 0, elemP().firstChild, 21);
assert_equals(range.toString(), 'Text inside an iframe', 'PRECONDITION: Correct range.');
const result = await iframeDoc.fragmentDirective.createSelectorDirective(range);
await scrollToSelectorDirective(result, t, frames[0]);
assert_greater_than(iframeDoc.scrollingElement.scrollTop, 100, 'iframe text scrolled into view');
}, 'Generate selector within iframe');
// Ensure that the range passed to createSelectorDirective must be from
// the same document as the fragmentDirective being used.
promise_test(async (t) => {
reset();
const range = getRange(elemP().firstChild, 0, elemP().firstChild, 21);
assert_equals(range.toString(), 'Text inside an iframe', 'PRECONDITION: Correct range.');
// Try creating a selector in the top document using the iframe range.
const result_promise = document.fragmentDirective.createSelectorDirective(range);
return promise_rejects_dom(t, "WrongDocumentError", result_promise);
}, 'Range from outside document');
// Ensure that createSelectorDirective doesn't work on a document that
// isn't attached to a frame.
promise_test(async (t) => {
reset();
let doc = document.implementation.createHTMLDocument("New Document");
let p = doc.createElement("p");
p.textContent = "This is a document without a frame.";
doc.body.appendChild(p);
const range = getRange(p.firstChild, 0, p.firstChild, 7);
assert_equals(range.toString(), 'This is', 'PRECONDITION: Correct range.');
// Try creating a selector in the unattached document.
const result_promise = doc.fragmentDirective.createSelectorDirective(range);
return promise_rejects_dom(t, "InvalidStateError", result_promise);
}, 'Detached document');
// Ensure that createSelectorDirective doesn't work on a document that
// has been replaced.
promise_test(async (t) => {
reset();
const iframeDoc = frames[0].document;
assert_equals(iframeDoc.scrollingElement.scrollTop, 0, 'PRECONDITION: Iframe unscrolled');
const range = getRange(elemP().firstChild, 0, elemP().firstChild, 21);
assert_equals(range.toString(), 'Text inside an iframe', 'PRECONDITION: Correct range.');
frames[0].location = 'about:blank';
await t.step_wait(() => frames[0].document != iframeDoc, "Wait for about:blank");
const result = await iframeDoc.fragmentDirective.createSelectorDirective(range);
assert_equals(result, undefined, 'createSelectorDirective returns undefined');
}, 'Execution context destroyed');
}
};
</script>
</body>