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); }  |