diff options
Diffstat (limited to 'test/test-document-util.js')
-rw-r--r-- | test/test-document-util.js | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/test/test-document-util.js b/test/test-document-util.js new file mode 100644 index 00000000..93ce1669 --- /dev/null +++ b/test/test-document-util.js @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); +const {JSDOM} = require('jsdom'); +const {testMain} = require('../dev/util'); +const {VM} = require('../dev/vm'); + + +// DOMRect class definition +class DOMRect { + /** + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + */ + constructor(x, y, width, height) { + /** @type {number} */ + this._x = x; + /** @type {number} */ + this._y = y; + /** @type {number} */ + this._width = width; + /** @type {number} */ + this._height = height; + } + + /** @type {number} */ + get x() { return this._x; } + /** @type {number} */ + get y() { return this._y; } + /** @type {number} */ + get width() { return this._width; } + /** @type {number} */ + get height() { return this._height; } + /** @type {number} */ + get left() { return this._x + Math.min(0, this._width); } + /** @type {number} */ + get right() { return this._x + Math.max(0, this._width); } + /** @type {number} */ + get top() { return this._y + Math.min(0, this._height); } + /** @type {number} */ + get bottom() { return this._y + Math.max(0, this._height); } + /** @returns {string} */ + toJSON() { return '<not implemented>'; } +} + + +/** + * @param {string} fileName + * @returns {JSDOM} + */ +function createJSDOM(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + const dom = new JSDOM(domSource); + const document = dom.window.document; + const window = dom.window; + + // Define innerText setter as an alias for textContent setter + Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', { + set(value) { this.textContent = value; } + }); + + // Placeholder for feature detection + document.caretRangeFromPoint = () => null; + + return dom; +} + +/** + * @param {Element} element + * @param {string|undefined} selector + * @returns {?Element} + */ +function querySelectorChildOrSelf(element, selector) { + return selector ? element.querySelector(selector) : element; +} + +/** + * @param {JSDOM} dom + * @param {?Node} node + * @returns {?Text|Node} + */ +function getChildTextNodeOrSelf(dom, node) { + if (node === null) { return null; } + const Node = dom.window.Node; + const childNode = node.firstChild; + return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node); +} + +/** + * @param {unknown} value + * @returns {unknown} + */ +function getPrototypeOfOrNull(value) { + try { + return Object.getPrototypeOf(value); + } catch (e) { + return null; + } +} + +/** + * @param {Document} document + * @returns {?Element} + */ +function findImposterElement(document) { + // Finds the imposter element based on it's z-index style + return document.querySelector('div[style*="2147483646"]>*'); +} + + +/** */ +async function testDocument1() { + const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-document1.html')); + const window = dom.window; + const document = window.document; + const Node = window.Node; + const Range = window.Range; + + const vm = new VM({document, window, Range, Node}); + vm.execute([ + 'js/data/sandbox/string-util.js', + 'js/dom/dom-text-scanner.js', + 'js/dom/text-source-range.js', + 'js/dom/text-source-element.js', + 'js/dom/document-util.js' + ]); + /** @type {[DOMTextScanner: typeof DOMTextScanner, TextSourceRange: typeof TextSourceRange, TextSourceElement: typeof TextSourceElement, DocumentUtil: typeof DocumentUtil]} */ + const [DOMTextScanner2, TextSourceRange2, TextSourceElement2, DocumentUtil2] = vm.get([ + 'DOMTextScanner', + 'TextSourceRange', + 'TextSourceElement', + 'DocumentUtil' + ]); + + try { + await testDocumentTextScanningFunctions(dom, {DocumentUtil: DocumentUtil2, TextSourceRange: TextSourceRange2, TextSourceElement: TextSourceElement2}); + await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner: DOMTextScanner2}); + } finally { + window.close(); + } +} + +/** + * @param {JSDOM} dom + * @param {{DocumentUtil: typeof DocumentUtil, TextSourceRange: typeof TextSourceRange, TextSourceElement: typeof TextSourceElement}} details + */ +async function testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}) { + const document = dom.window.document; + + for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.test[data-test-type=scan]'))) { + // Get test parameters + const { + elementFromPointSelector, + caretRangeFromPointSelector, + startNodeSelector, + startOffset, + endNodeSelector, + endOffset, + resultType, + sentenceScanExtent, + sentence, + hasImposter, + terminateAtNewlines + } = testElement.dataset; + + const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector); + const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector); + const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector)); + const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector)); + + const startOffset2 = parseInt(/** @type {string} */ (startOffset), 10); + const endOffset2 = parseInt(/** @type {string} */ (endOffset), 10); + const sentenceScanExtent2 = parseInt(/** @type {string} */ (sentenceScanExtent), 10); + const terminateAtNewlines2 = (terminateAtNewlines !== 'false'); + + assert.notStrictEqual(elementFromPointValue, null); + assert.notStrictEqual(caretRangeFromPointValue, null); + assert.notStrictEqual(startNode, null); + assert.notStrictEqual(endNode, null); + + // Setup functions + document.elementFromPoint = () => elementFromPointValue; + + document.caretRangeFromPoint = (x, y) => { + const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document)); + assert.strictEqual(!!imposter, hasImposter === 'true'); + + const range = document.createRange(); + range.setStart(/** @type {Node} */ (imposter ? imposter : startNode), startOffset2); + range.setEnd(/** @type {Node} */ (imposter ? imposter : startNode), endOffset2); + + // Override getClientRects to return a rect guaranteed to contain (x, y) + range.getClientRects = () => { + /** @type {import('test/document-types').PseudoDOMRectList} */ + const domRectList = Object.assign( + [new DOMRect(x - 1, y - 1, 2, 2)], + { + /** + * @this {DOMRect[]} + * @param {number} index + * @returns {DOMRect} + */ + item: function item(index) { return this[index]; } + } + ); + return domRectList; + }; + return range; + }; + + // Test docRangeFromPoint + const source = DocumentUtil.getRangeFromPoint(0, 0, { + deepContentScan: false, + normalizeCssZoom: true + }); + switch (resultType) { + case 'TextSourceRange': + assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype); + break; + case 'TextSourceElement': + assert.strictEqual(getPrototypeOfOrNull(source), TextSourceElement.prototype); + break; + case 'null': + assert.strictEqual(source, null); + break; + default: + assert.ok(false); + break; + } + if (source === null) { continue; } + + // Sentence info + const terminatorString = '…。..??!!'; + const terminatorMap = new Map(); + for (const char of terminatorString) { + terminatorMap.set(char, [false, true]); + } + const quoteArray = [['「', '」'], ['『', '』'], ['\'', '\''], ['"', '"']]; + const forwardQuoteMap = new Map(); + const backwardQuoteMap = new Map(); + for (const [char1, char2] of quoteArray) { + forwardQuoteMap.set(char1, [char2, false]); + backwardQuoteMap.set(char2, [char1, false]); + } + + // Test docSentenceExtract + const sentenceActual = DocumentUtil.extractSentence( + source, + false, + sentenceScanExtent2, + terminateAtNewlines2, + terminatorMap, + forwardQuoteMap, + backwardQuoteMap + ).text; + assert.strictEqual(sentenceActual, sentence); + + // Clean + source.cleanup(); + } +} + +/** + * @param {JSDOM} dom + * @param {{DOMTextScanner: typeof DOMTextScanner}} details + */ +async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) { + const document = dom.window.document; + + for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.test[data-test-type=text-source-range-seek]'))) { + // Get test parameters + const { + seekNodeSelector, + seekNodeIsText, + seekOffset, + seekLength, + seekDirection, + expectedResultNodeSelector, + expectedResultNodeIsText, + expectedResultOffset, + expectedResultContent + } = testElement.dataset; + + const seekOffset2 = parseInt(/** @type {string} */ (seekOffset), 10); + const seekLength2 = parseInt(/** @type {string} */ (seekLength), 10); + const expectedResultOffset2 = parseInt(/** @type {string} */ (expectedResultOffset), 10); + + /** @type {?Node} */ + let seekNode = testElement.querySelector(/** @type {string} */ (seekNodeSelector)); + if (seekNodeIsText === 'true' && seekNode !== null) { + seekNode = seekNode.firstChild; + } + + /** @type {?Node} */ + let expectedResultNode = testElement.querySelector(/** @type {string} */ (expectedResultNodeSelector)); + if (expectedResultNodeIsText === 'true' && expectedResultNode !== null) { + expectedResultNode = expectedResultNode.firstChild; + } + + const {node, offset, content} = ( + seekDirection === 'forward' ? + new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(seekLength2) : + new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(-seekLength2) + ); + + assert.strictEqual(node, expectedResultNode); + assert.strictEqual(offset, expectedResultOffset2); + assert.strictEqual(content, expectedResultContent); + } +} + + +/** */ +async function main() { + await testDocument1(); +} + + +if (require.main === module) { testMain(main); } |