More tests — some failing
Three tests fail because of an opinionated expectation: I expect a match
at the start of a the text node to have that textNode as start container
rather than its parent element (although technically such a Range should
be equivalent).
Two tests fail because we really mess up when the ancestorContainer of
the scope contains text before its startContainer. Ouch.
diff --git a/packages/dom/test/text-quote-match.ts b/packages/dom/test/text-quote-match.ts
index 835a3dd..e348dbe 100644
--- a/packages/dom/test/text-quote-match.ts
+++ b/packages/dom/test/text-quote-match.ts
@@ -55,18 +55,48 @@
},
]
},
+ 'first characters': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'lorem ipsum',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 0,
+ endContainerXPath: '//b/text()',
+ endOffset: 11,
+ },
+ ]
+ },
+ 'last characters': {
+ html: '<b>lorem ipsum dolor amet yada yada</b>',
+ selector: {
+ type: 'TextQuoteSelector',
+ exact: 'yada yada',
+ },
+ expected: [
+ {
+ startContainerXPath: '//b/text()',
+ startOffset: 23,
+ endContainerXPath: '//b/text()',
+ endOffset: 32,
+ },
+ ]
+ },
'across elements': {
- html: '<b>lorem <i>ipsum dolor</i> amet yada yada</b>',
+ html: '<b>lorem <i>ipsum</i> dolor <u>amet</u> yada yada</b>',
selector: {
type: 'TextQuoteSelector',
exact: 'dolor am',
},
expected: [
{
- startContainerXPath: '//i/text()',
- startOffset: 6,
- endContainerXPath: '//b/text()[2]',
- endOffset: 3,
+ startContainerXPath: '//b/text()[2]',
+ startOffset: 1,
+ endContainerXPath: '//u/text()',
+ endOffset: 2,
},
]
},
@@ -151,6 +181,115 @@
await testMatcher(doc, doc, selector, expected);
});
}
+
+ it('handles adjacent text nodes', async () => {
+ const { html, selector } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+ const textNode = evaluateXPath(doc, '//b/text()') as Text;
+
+ for (let index = textNode.length - 1; index > 0; index--)
+ textNode.splitText(index);
+ // console.log([...textNode.parentNode.childNodes].map(node => node.textContent))
+ // → 'l', 'o', 'r', 'e', 'm', …
+
+ await testMatcher(doc, doc, selector, [
+ {
+ startContainerXPath: '//b/text()[13]',
+ startOffset: 0,
+ endContainerXPath: '//b/text()[21]',
+ endOffset: 0,
+ },
+ ]);
+ });
+
+ it('handles empty text nodes', async () => {
+ const { html, selector } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const textNode = evaluateXPath(doc, '//b/text()') as Text;
+ textNode.splitText(textNode.length);
+ textNode.splitText(20);
+ textNode.splitText(20);
+ textNode.splitText(17);
+ textNode.splitText(17);
+ textNode.splitText(12);
+ textNode.splitText(12);
+ textNode.splitText(0);
+ // console.log([...textNode.parentNode.childNodes].map(node => node.textContent))
+ // → '', 'lorem ipsum ', '', 'dolor', '', ' am', '', 'et yada yada', ''
+
+ await testMatcher(doc, doc, selector, [
+ {
+ startContainerXPath: '//b/text()[4]', // "dolor"
+ startOffset: 0,
+ endContainerXPath: '//b/text()[8]', // "et yada yada"
+ endOffset: 0,
+ },
+ ]);
+ });
+
+ it('works with parent of text as scope', async () => {
+ const { html, selector, expected } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ await testMatcher(doc, evaluateXPath(doc, '//b'), selector, expected);
+ });
+
+ it('works with parent of text as scope, when matching its first characters', async () => {
+ const { html, selector, expected } = testCases['first characters'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ await testMatcher(doc, evaluateXPath(doc, '//b'), selector, expected);
+ });
+
+ it('works with parent of text as scope, when matching its first characters, with an empty text node', async () => {
+ const { html, selector } = testCases['first characters'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const textNode = evaluateXPath(doc, '//b/text()') as Text;
+ textNode.splitText(0);
+
+ await testMatcher(doc, evaluateXPath(doc, '//b'), selector, [
+ {
+ startContainerXPath: '//b/text()[2]',
+ startOffset: 0,
+ endContainerXPath: '//b/text()[2]',
+ endOffset: 11,
+ },
+ ]);
+ });
+
+ it('works when scope is a Range within one text node', async () => {
+ const { html, selector, expected } = testCases['simple'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ // Use the substring ‘ipsum dolor amet’ as scope.
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b/text()'), 6);
+ scope.setEnd(evaluateXPath(doc, '//b/text()'), 22);
+ await testMatcher(doc, scope, selector, expected);
+ });
+
+ it('works when scope is a Range with both ends inside text nodes', async () => {
+ const { html, selector, expected } = testCases['across elements'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ // Use the substring ‘sum dolor am’ as scope.
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//i/text()'), 2);
+ scope.setEnd(evaluateXPath(doc, '//u/text()'), 2);
+ await testMatcher(doc, scope, selector, expected);
+ });
+
+ it('works when scope is a Range with both ends inside elements', async () => {
+ const { html, selector, expected } = testCases['across elements'];
+ const doc = domParser.parseFromString(html, 'text/html');
+
+ const scope = document.createRange();
+ scope.setStart(evaluateXPath(doc, '//b'), 1); // before the <i>
+ scope.setEnd(evaluateXPath(doc, '//b'), 4); // before the " yada yada"
+ await testMatcher(doc, scope, selector, expected);
+ });
});
async function testMatcher(