This is an automated email from the ASF dual-hosted git repository. gerben pushed a commit to branch tweak-highlighter in repository https://gitbox.apache.org/repos/asf/incubator-annotator.git
commit 992e3855909b1c665e8579c6f5286529b0ab2d0b Author: Gerben <[email protected]> AuthorDate: Thu Jul 23 14:16:49 2020 +0200 Create tests for highlighter --- .../test/highlight-range/highlight-range.test.ts | 199 +++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/packages/dom/test/highlight-range/highlight-range.test.ts b/packages/dom/test/highlight-range/highlight-range.test.ts new file mode 100644 index 0000000..387fd7b --- /dev/null +++ b/packages/dom/test/highlight-range/highlight-range.test.ts @@ -0,0 +1,199 @@ +/** + * @license + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { assert } from 'chai'; +import { highlightRange } from '../../src/highlight-range'; +import { RangeInfo, hydrateRange, evaluateXPath } from '../utils'; + +const domParser = new window.DOMParser(); + +const testCases: { + [name: string]: { + inputHtml: string, + range: RangeInfo, + tagName?: string, + attributes?: Record<string, string>, + expectedHtml: string, + } +} = { + 'single text node': { + inputHtml: '<b>lorem ipsum dolor amet yada yada</b>', + range: { + startContainerXPath: '//b/text()', + startOffset: 12, + endContainerXPath: '//b/text()', + endOffset: 20, + }, + expectedHtml: '<b>lorem ipsum <mark>dolor am</mark>et yada yada</b>', + }, + 'across elements': { + inputHtml: '<b>lorem <i>ipsum</i> dolor <u>amet</u> yada yada</b>', + range: { + startContainerXPath: '//b/text()[2]', + startOffset: 1, + endContainerXPath: '//u/text()', + endOffset: 2, + }, + expectedHtml: '<b>lorem <i>ipsum</i> <mark>dolor </mark><u><mark>am</mark>et</u> yada yada</b>', + }, + 'collapsed range': { + inputHtml: '<b>lorem ipsum dolor amet yada yada</b>', + range: { + startContainerXPath: '//b/text()', + startOffset: 12, + endContainerXPath: '//b/text()', + endOffset: 12, + }, + expectedHtml: '<b>lorem ipsum <mark></mark>dolor amet yada yada</b>', + }, + 'custom tag name': { + inputHtml: '<b>lorem ipsum dolor amet yada yada</b>', + range: { + startContainerXPath: '//b/text()', + startOffset: 12, + endContainerXPath: '//b/text()', + endOffset: 20, + }, + tagName: 'span', + expectedHtml: '<b>lorem ipsum <span>dolor am</span>et yada yada</b>', + }, + 'custom attributes': { + inputHtml: '<b>lorem ipsum dolor amet yada yada</b>', + range: { + startContainerXPath: '//b/text()', + startOffset: 12, + endContainerXPath: '//b/text()', + endOffset: 20, + }, + attributes: { + class: 'red', + }, + expectedHtml: '<b>lorem ipsum <mark class="red">dolor am</mark>et yada yada</b>', + }, + 'overlapping highlight': { + // Starts off from the result of the 'single text node' case. + inputHtml: '<b>lorem ipsum <mark>dolor am</mark>et yada yada</b>', + range: { + startContainerXPath: '//mark/text()', + startOffset: 6, + endContainerXPath: '//b/text()[2]', + endOffset: 7, + }, + tagName: 'mark2', + expectedHtml: '<b>lorem ipsum <mark>dolor <mark2>am</mark2></mark><mark2>et yada</mark2> yada</b>', + }, +}; + +describe('highlightRange', () => { + for (const [name, { inputHtml, range, tagName, attributes, expectedHtml }] of Object.entries(testCases)) { + it(`works for case: ${name}`, () => { + const doc = domParser.parseFromString(inputHtml, 'text/html'); + + // Invoke highlightRange for the specified Range, and check the result. + const removeHighlights = highlightRange(hydrateRange(range, doc), tagName, attributes); + assert.equal(doc.body.innerHTML, expectedHtml); + + // Remove the highlight again and check that we end up exactly how we started. + removeHighlights(); + assert.equal(doc.body.innerHTML, inputHtml); + }); + } + + it('works on adjacent text nodes', () => { + const inputHtml = '<b>lorem ipsum dolor amet yada yada</b>'; + const doc = domParser.parseFromString(inputHtml, 'text/html'); + + const textNode = evaluateXPath(doc, '//b/text()') as Text; + textNode.splitText(15); // after 'dol' + + const range = doc.createRange(); + range.setStart(evaluateXPath(doc, '//b/text()[1]'), 12); // before 'dolor am' + range.setEnd(evaluateXPath(doc, '//b/text()[2]'), 20 - 15); // after 'dolor am' + + const removeHighlights = highlightRange(range); + const expectedHtml = '<b>lorem ipsum <mark>dol</mark><mark>or am</mark>et yada yada</b>'; + assert.equal(doc.body.innerHTML, expectedHtml); + + removeHighlights(); + assert.equal(doc.body.innerHTML, inputHtml); + }); + + it('also marks empty text nodes', () => { + const inputHtml = '<b>lorem ipsum dolor amet yada yada</b>'; + const doc = domParser.parseFromString(inputHtml, 'text/html'); + + const textNode = evaluateXPath(doc, '//b/text()') as Text; + textNode.splitText(15); + textNode.splitText(15); // Split the node twice to create an empty text node. + + const range = doc.createRange(); + range.setStart(evaluateXPath(doc, '//b/text()[1]'), 12); // before 'dolor am' + range.setEnd(evaluateXPath(doc, '//b/text()[3]'), 20 - 15); // after 'dolor am' + + const removeHighlights = highlightRange(range); + const expectedHtml = '<b>lorem ipsum <mark>dol</mark><mark></mark><mark>or am</mark>et yada yada</b>'; + assert.equal(doc.body.innerHTML, expectedHtml); + + removeHighlights(); + assert.equal(doc.body.innerHTML, inputHtml); + }); + + it('ignores a range that does not contain Text nodes', () => { + const inputHtml = `<b>Try highlighting this image: <img> — would that work?</b>` + const doc = domParser.parseFromString(inputHtml, 'text/html'); + + const range = document.createRange(); + range.selectNode(evaluateXPath(doc, '//img')); + + const removeHighlights = highlightRange(range); + assert.equal(doc.body.innerHTML, inputHtml); + + removeHighlights(); + assert.equal(doc.body.innerHTML, inputHtml); + }); + + it('correctly removes multiple highlights (fifo order)', () => { + const { inputHtml, range } = testCases['single text node']; + const { range: range2, expectedHtml } = testCases['overlapping highlight']; + const doc = domParser.parseFromString(inputHtml, 'text/html'); + + const removeHighlights1 = highlightRange(hydrateRange(range, doc)); + const removeHighlights2 = highlightRange(hydrateRange(range2, doc), 'mark2'); + assert.equal(doc.body.innerHTML, expectedHtml); + + removeHighlights1(); + removeHighlights2(); + assert.equal(doc.body.innerHTML, inputHtml); + }); + + it('correctly removes multiple highlights (lifo order)', () => { + const { inputHtml, range } = testCases['single text node']; + const { range: range2, expectedHtml } = testCases['overlapping highlight']; + const doc = domParser.parseFromString(inputHtml, 'text/html'); + + const removeHighlights1 = highlightRange(hydrateRange(range, doc)); + const removeHighlights2 = highlightRange(hydrateRange(range2, doc), 'mark2'); + assert.equal(doc.body.innerHTML, expectedHtml); + + removeHighlights2(); + removeHighlights1(); + assert.equal(doc.body.innerHTML, inputHtml); + }); +});
