diff options
author | Alex Yatskov <alex@foosoft.net> | 2020-03-14 13:14:03 -0700 |
---|---|---|
committer | Alex Yatskov <alex@foosoft.net> | 2020-03-14 13:14:03 -0700 |
commit | b77e2afe3a8ef9e96a53dd8ca97d8b913941244b (patch) | |
tree | 818a2f25169845a72b3424b7400b5b07f0f7cacf /test | |
parent | d32f4def0eeed1599857bc04c973337a2a13dd8b (diff) | |
parent | 98afe7adae80c6bc9de0c4b996e6f6cb0a5df49d (diff) |
Merge branch 'master' into testing
Diffstat (limited to 'test')
-rw-r--r-- | test/data/html/test-document1.html | 264 | ||||
-rw-r--r-- | test/data/html/test-stylesheet.css | 32 | ||||
-rw-r--r-- | test/dictionary-validate.js | 8 | ||||
-rw-r--r-- | test/lint/global-declarations.js | 105 | ||||
-rw-r--r-- | test/schema-validate.js | 6 | ||||
-rw-r--r-- | test/test-database.js | 46 | ||||
-rw-r--r-- | test/test-document.js | 240 | ||||
-rw-r--r-- | test/test-schema.js | 12 | ||||
-rw-r--r-- | test/yomichan-test.js | 34 | ||||
-rw-r--r-- | test/yomichan-vm.js | 174 |
10 files changed, 877 insertions, 44 deletions
diff --git a/test/data/html/test-document1.html b/test/data/html/test-document1.html new file mode 100644 index 00000000..0754a314 --- /dev/null +++ b/test/data/html/test-document1.html @@ -0,0 +1,264 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan Tests</title> + <link rel="icon" type="image/gif" href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw==" /> + <link rel="stylesheet" href="test-stylesheet.css" /> + </head> +<body> + + <h1>Yomichan Tests</h1> + + <div + class="test" + data-test-type="scan" + data-element-from-point-selector="span" + data-caret-range-from-point-selector="span" + data-start-node-selector="span" + data-start-offset="0" + data-end-node-selector="span" + data-end-offset="0" + data-result-type="TextSourceRange", + data-sentence-extent="100" + data-sentence="真白「心配してくださって、ありがとございます」" + > + <span>真白「心配してくださって、ありがとございます」</span> + </div> + + <div + class="test" + data-test-type="scan" + data-element-from-point-selector="span" + data-caret-range-from-point-selector="span" + data-start-node-selector="span" + data-start-offset="5" + data-end-node-selector="span" + data-end-offset="5" + data-result-type="TextSourceRange", + data-sentence-extent="100" + data-sentence="心配してくださって、ありがとございます" + > + <span>真白「心配してくださって、ありがとございます」</span> + </div> + + <div + class="test" + data-test-type="scan" + data-element-from-point-selector="input" + data-caret-range-from-point-selector="input" + data-start-node-selector="input" + data-start-offset="0" + data-end-node-selector="input" + data-end-offset="0" + data-result-type="TextSourceRange", + data-sentence-extent="100" + data-sentence="真白「心配してくださって、ありがとございます」" + data-has-imposter="true" + > + <input type="text" value="真白「心配してくださって、ありがとございます」" style="width: 100%; box-sizing: border-box; font-family: inherit; font-size: inherit; border: 1px solid #d8d8d8; padding: 0.2em;" /> + </div> + + <div + class="test" + data-test-type="scan" + data-element-from-point-selector="textarea" + data-caret-range-from-point-selector="textarea" + data-start-node-selector="textarea" + data-start-offset="0" + data-end-node-selector="textarea" + data-end-offset="0" + data-result-type="TextSourceRange", + data-sentence-extent="100" + data-sentence="真白「心配してくださって、ありがとございます」" + data-has-imposter="true" + > + <textarea style="width: 100%; height: 3em; box-sizing: border-box; font-family: inherit; font-size: inherit; border: 1px solid #d8d8d8; padding: 0.2em;">真白「心配してくださって、ありがとございます」</textarea> + </div> + + <div + class="test" + data-test-type="scan" + data-element-from-point-selector="button" + data-caret-range-from-point-selector="button" + data-start-node-selector="button" + data-start-offset="0" + data-end-node-selector="button" + data-end-offset="0" + data-result-type="TextSourceElement", + data-sentence-extent="100" + data-sentence="よみちゃん" + > + <button style="width: 100%; box-sizing: border-box; font-family: inherit; font-size: inherit; border: 1px solid #d8d8d8; background-color: #f0f0f0; padding: 0.2em;">よみちゃん</button> + </div> + + <div + class="test" + data-test-type="scan" + data-element-from-point-selector="img" + data-caret-range-from-point-selector="img" + data-start-node-selector="img" + data-start-offset="0" + data-end-node-selector="img" + data-end-offset="0" + data-result-type="TextSourceElement" + data-sentence="よみちゃん" + > + <img src="data:image/gif;base64,R0lGODdhBwAHAIABAAAAAP///ywAAAAABwAHAAACDIRvEaC32FpCbEkKCgA7" alt="よみちゃん" title="よみちゃん" style="width: 70px; height: 70px; image-rendering: crisp-edges; image-rendering: pixelated; display: block;" /> + </div> + + <div + class="test" + data-test-type="text-source-range-seek" + data-seek-node-selector="span:nth-of-type(1)" + data-seek-node-is-text="true" + data-seek-offset="0" + data-seek-length="149" + data-seek-direction="forward" + data-expected-result-node-selector="span:nth-of-type(1)" + data-expected-result-node-is-text="true" + data-expected-result-offset="149" + data-expected-result-content=" + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + " + > + <span> + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + </span><span>trailing content</span> + </div> + + <div + class="test" + data-test-type="text-source-range-seek" + data-seek-node-selector="span:nth-of-type(1)" + data-seek-node-is-text="true" + data-seek-offset="149" + data-seek-length="149" + data-seek-direction="backward" + data-expected-result-node-selector="span:nth-of-type(1)" + data-expected-result-node-is-text="true" + data-expected-result-offset="0" + data-expected-result-content=" + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + " + > + <span> + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + </span><span>trailing content</span> + </div> + + <div + class="test" + data-test-type="text-source-range-seek" + data-seek-node-selector="span:nth-of-type(1)" + data-seek-node-is-text="true" + data-seek-offset="0" + data-seek-length="150" + data-seek-direction="forward" + data-expected-result-node-selector="span:nth-of-type(2)" + data-expected-result-node-is-text="true" + data-expected-result-offset="1" + data-expected-result-content=" + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + t" + > + <span> + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + </span><span>trailing content</span> + </div> + + <div + class="test" + data-test-type="text-source-range-seek" + data-seek-node-selector="span:nth-of-type(2)" + data-seek-node-is-text="true" + data-seek-offset="1" + data-seek-length="150" + data-seek-direction="backward" + data-expected-result-node-selector="span:nth-of-type(1)" + data-expected-result-node-is-text="true" + data-expected-result-offset="0" + data-expected-result-content=" + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + t" + > + <span> + あいうえお + かきくけこ + さしすせそ + たちつてと + なにぬねの + はひふへほ + まみむめも + や ゆ よ + らりるれろ + わゐ ゑを + </span><span>trailing content</span> + </div> + +</body> +</html>
\ No newline at end of file diff --git a/test/data/html/test-stylesheet.css b/test/data/html/test-stylesheet.css new file mode 100644 index 00000000..ab25732e --- /dev/null +++ b/test/data/html/test-stylesheet.css @@ -0,0 +1,32 @@ +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + max-width: 680px; + padding: 0 1em; + box-sizing: border-box; + margin: 0 auto; + background-color: #f8f8f8; + counter-reset: test-id; +} + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +.test { + background-color: #ffffff; + margin: 1em 0; + padding: 0.5em; + box-shadow: rgba(64, 64, 64, 0.3) 0px 1px 2px 0px, rgba(64, 64, 64, 0.15) 0px 1px 3px 1px; + border-radius: 4px; +} + +.test:before { + content: "Test " counter(test-id); + display: block; + counter-increment: test-id; + margin-bottom: 0.5em; + border-bottom: 1px solid #d8d8d8; + font-weight: bold; +} diff --git a/test/dictionary-validate.js b/test/dictionary-validate.js index 14eee2ed..6496f2ac 100644 --- a/test/dictionary-validate.js +++ b/test/dictionary-validate.js @@ -18,10 +18,12 @@ const fs = require('fs'); const path = require('path'); -const yomichanTest = require('./yomichan-test'); +const {JSZip} = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); -const JSZip = yomichanTest.JSZip; -const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']); +const vm = new VM(); +vm.execute('bg/js/json-schema.js'); +const JsonSchema = vm.get('JsonSchema'); function readSchema(relativeFileName) { diff --git a/test/lint/global-declarations.js b/test/lint/global-declarations.js new file mode 100644 index 00000000..2629cc5e --- /dev/null +++ b/test/lint/global-declarations.js @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 {getAllFiles} = require('../yomichan-test'); + + +function countOccurences(string, pattern) { + return (string.match(pattern) || []).length; +} + +function getNewline(string) { + const count1 = countOccurences(string, /(?:^|[^\r])\n/g); + const count2 = countOccurences(string, /\r\n/g); + const count3 = countOccurences(string, /\r(?:[^\n]|$)/g); + if (count2 > count1) { + return (count3 > count2) ? '\r' : '\r\n'; + } else { + return (count3 > count1) ? '\r' : '\n'; + } +} + + +function validateGlobals(fileName, fix) { + const pattern = /\/\*\s*global\s+([\w\W]*?)\*\//g; + const trimPattern = /^[\s,*]+|[\s,*]+$/g; + const splitPattern = /[\s,*]+/; + const source = fs.readFileSync(fileName, {encoding: 'utf8'}); + let match; + let first = true; + let endIndex = 0; + let newSource = ''; + const newline = getNewline(source); + while ((match = pattern.exec(source)) !== null) { + if (!first) { + console.error(`Encountered more than one global declaration in ${fileName}`); + return false; + } + first = false; + + const parts = match[1].replace(trimPattern, '').split(splitPattern); + parts.sort(); + + const actual = match[0]; + const expected = `/* global${parts.map((v) => `${newline} * ${v}`).join('')}${newline} */`; + + try { + assert.strictEqual(actual, expected); + } catch (e) { + console.error(`Global declaration error encountered in ${fileName}:`); + console.error(e.message); + if (!fix) { + return false; + } + } + + newSource += source.substring(0, match.index); + newSource += expected; + endIndex = match.index + match[0].length; + } + + newSource += source.substring(endIndex); + + if (fix) { + fs.writeFileSync(fileName, newSource, {encoding: 'utf8'}); + } + + return true; +} + + +function main() { + const fix = (process.argv.length >= 2 && process.argv[2] === '--fix'); + const directory = path.resolve(__dirname, '..', '..', 'ext'); + const pattern = /\.js$/; + const ignorePattern = /[\\/]ext[\\/]mixed[\\/]lib[\\/]/; + const fileNames = getAllFiles(directory, (f) => pattern.test(f) && !ignorePattern.test(f)); + for (const fileName of fileNames) { + if (!validateGlobals(fileName, fix)) { + process.exit(-1); + return; + } + } + process.exit(0); +} + + +if (require.main === module) { main(); } diff --git a/test/schema-validate.js b/test/schema-validate.js index a4f2d94c..eb31aa8d 100644 --- a/test/schema-validate.js +++ b/test/schema-validate.js @@ -17,9 +17,11 @@ */ const fs = require('fs'); -const yomichanTest = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); -const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']); +const vm = new VM(); +vm.execute('bg/js/json-schema.js'); +const JsonSchema = vm.get('JsonSchema'); function main() { diff --git a/test/test-database.js b/test/test-database.js index c2317881..833aa75d 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -21,6 +21,7 @@ const url = require('url'); const path = require('path'); const assert = require('assert'); const yomichanTest = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); require('fake-indexeddb/auto'); const chrome = { @@ -30,6 +31,9 @@ const chrome = { }, getURL(path2) { return url.pathToFileURL(path.join(__dirname, '..', 'ext', path2.replace(/^\//, ''))); + }, + sendMessage() { + // NOP } } }; @@ -88,24 +92,24 @@ class XMLHttpRequest { } } -const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']); -const {dictFieldSplit, dictTagSanitize} = yomichanTest.requireScript('ext/bg/js/dictionary.js', ['dictFieldSplit', 'dictTagSanitize']); -const {stringReverse, hasOwn} = yomichanTest.requireScript('ext/mixed/js/core.js', ['stringReverse', 'hasOwn'], {chrome}); -const {requestJson} = yomichanTest.requireScript('ext/bg/js/request.js', ['requestJson'], {XMLHttpRequest}); -const databaseGlobals = { +const vm = new VM({ chrome, - JsonSchema, - requestJson, - stringReverse, - hasOwn, - dictFieldSplit, - dictTagSanitize, + XMLHttpRequest, indexedDB: global.indexedDB, + IDBKeyRange: global.IDBKeyRange, JSZip: yomichanTest.JSZip -}; -databaseGlobals.window = databaseGlobals; -const {Database} = yomichanTest.requireScript('ext/bg/js/database.js', ['Database'], databaseGlobals); +}); +vm.context.window = vm.context; + +vm.execute([ + 'bg/js/json-schema.js', + 'bg/js/dictionary.js', + 'mixed/js/core.js', + 'bg/js/request.js', + 'bg/js/database.js' +]); +const Database = vm.get('Database'); function countTermsWithExpression(terms, expression) { @@ -213,20 +217,20 @@ async function testDatabase1() { }, {prefixWildcardsSupported: true} ); - assert.deepStrictEqual(errors, []); - assert.deepStrictEqual(result, expectedSummary); + vm.assert.deepStrictEqual(errors, []); + vm.assert.deepStrictEqual(result, expectedSummary); assert.ok(progressEvent); // Get info summary const info = await database.getDictionaryInfo(); - assert.deepStrictEqual(info, [expectedSummary]); + vm.assert.deepStrictEqual(info, [expectedSummary]); // Get counts const counts = await database.getDictionaryCounts( info.map((v) => v.title), true ); - assert.deepStrictEqual(counts, { + vm.assert.deepStrictEqual(counts, { counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}], total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12} }); @@ -249,10 +253,10 @@ async function testDatabase1() { async function testDatabaseEmpty1(database) { const info = await database.getDictionaryInfo(); - assert.deepStrictEqual(info, []); + vm.assert.deepStrictEqual(info, []); const counts = await database.getDictionaryCounts([], true); - assert.deepStrictEqual(counts, { + vm.assert.deepStrictEqual(counts, { counts: [], total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0} }); @@ -825,7 +829,7 @@ async function testFindTagForTitle1(database, title) { for (const {inputs, expectedResults} of data) { for (const {name} of inputs) { const result = await database.findTagForTitle(name, title); - assert.deepStrictEqual(result, expectedResults.value); + vm.assert.deepStrictEqual(result, expectedResults.value); } } } diff --git a/test/test-document.js b/test/test-document.js new file mode 100644 index 00000000..80b9719d --- /dev/null +++ b/test/test-document.js @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 {VM} = require('./yomichan-vm'); + + +// DOMRect class definition +class DOMRect { + constructor(x, y, width, height) { + this._x = x; + this._y = y; + this._width = width; + this._height = height; + } + + get x() { return this._x; } + get y() { return this._y; } + get width() { return this._width; } + get height() { return this._height; } + get left() { return this._x + Math.min(0, this._width); } + get right() { return this._x + Math.max(0, this._width); } + get top() { return this._y + Math.min(0, this._height); } + get bottom() { return this._y + Math.max(0, this._height); } +} + + +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; +} + +function querySelectorChildOrSelf(element, selector) { + return selector ? element.querySelector(selector) : element; +} + +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); +} + +function getPrototypeOfOrNull(value) { + try { + return Object.getPrototypeOf(value); + } catch (e) { + return null; + } +} + +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([ + 'mixed/js/dom.js', + 'fg/js/source.js', + 'fg/js/document.js' + ]); + const [TextSourceRange, TextSourceElement, docRangeFromPoint, docSentenceExtract] = vm.get([ + 'TextSourceRange', + 'TextSourceElement', + 'docRangeFromPoint', + 'docSentenceExtract' + ]); + + try { + await testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}); + await testTextSourceRangeSeekFunctions(dom, {TextSourceRange}); + } finally { + window.close(); + } +} + +async function testDocumentTextScanningFunctions(dom, {docRangeFromPoint, docSentenceExtract, TextSourceRange, TextSourceElement}) { + const document = dom.window.document; + + for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) { + // Get test parameters + let { + elementFromPointSelector, + caretRangeFromPointSelector, + startNodeSelector, + startOffset, + endNodeSelector, + endOffset, + resultType, + sentenceExtent, + sentence, + hasImposter + } = 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)); + + startOffset = parseInt(startOffset, 10); + endOffset = parseInt(endOffset, 10); + sentenceExtent = parseInt(sentenceExtent, 10); + + 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(imposter ? imposter : startNode, startOffset); + range.setEnd(imposter ? imposter : startNode, endOffset); + + // Override getClientRects to return a rect guaranteed to contain (x, y) + range.getClientRects = () => [new DOMRect(x - 1, y - 1, 2, 2)]; + return range; + }; + + // Test docRangeFromPoint + const source = docRangeFromPoint(0, 0, false); + 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; } + + // Test docSentenceExtract + const sentenceActual = docSentenceExtract(source, sentenceExtent).text; + assert.strictEqual(sentenceActual, sentence); + + // Clean + source.cleanup(); + } +} + +async function testTextSourceRangeSeekFunctions(dom, {TextSourceRange}) { + const document = dom.window.document; + + for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) { + // Get test parameters + let { + seekNodeSelector, + seekNodeIsText, + seekOffset, + seekLength, + seekDirection, + expectedResultNodeSelector, + expectedResultNodeIsText, + expectedResultOffset, + expectedResultContent + } = testElement.dataset; + + seekOffset = parseInt(seekOffset, 10); + seekLength = parseInt(seekLength, 10); + expectedResultOffset = parseInt(expectedResultOffset, 10); + + let seekNode = testElement.querySelector(seekNodeSelector); + if (seekNodeIsText === 'true') { + seekNode = seekNode.firstChild; + } + + let expectedResultNode = testElement.querySelector(expectedResultNodeSelector); + if (expectedResultNodeIsText === 'true') { + expectedResultNode = expectedResultNode.firstChild; + } + + const {node, offset, content} = ( + seekDirection === 'forward' ? + TextSourceRange.seekForward(seekNode, seekOffset, seekLength) : + TextSourceRange.seekBackward(seekNode, seekOffset, seekLength) + ); + + assert.strictEqual(node, expectedResultNode); + assert.strictEqual(offset, expectedResultOffset); + assert.strictEqual(content, expectedResultContent); + } +} + + +async function main() { + await testDocument1(); +} + + +if (require.main === module) { main(); } diff --git a/test/test-schema.js b/test/test-schema.js index f4612f86..5f9915fd 100644 --- a/test/test-schema.js +++ b/test/test-schema.js @@ -17,9 +17,11 @@ */ const assert = require('assert'); -const yomichanTest = require('./yomichan-test'); +const {VM} = require('./yomichan-vm'); -const {JsonSchema} = yomichanTest.requireScript('ext/bg/js/json-schema.js', ['JsonSchema']); +const vm = new VM(); +vm.execute('bg/js/json-schema.js'); +const JsonSchema = vm.get('JsonSchema'); function testValidate1() { @@ -138,7 +140,7 @@ function testGetValidValueOrDefault1() { for (const [value, expected] of testData) { const actual = JsonSchema.getValidValueOrDefault(schema, value); - assert.deepStrictEqual(actual, expected); + vm.assert.deepStrictEqual(actual, expected); } } @@ -177,7 +179,7 @@ function testGetValidValueOrDefault2() { for (const [value, expected] of testData) { const actual = JsonSchema.getValidValueOrDefault(schema, value); - assert.deepStrictEqual(actual, expected); + vm.assert.deepStrictEqual(actual, expected); } } @@ -235,7 +237,7 @@ function testGetValidValueOrDefault3() { for (const [value, expected] of testData) { const actual = JsonSchema.getValidValueOrDefault(schema, value); - assert.deepStrictEqual(actual, expected); + vm.assert.deepStrictEqual(actual, expected); } } diff --git a/test/yomichan-test.js b/test/yomichan-test.js index 78bfb9c6..5fa7730b 100644 --- a/test/yomichan-test.js +++ b/test/yomichan-test.js @@ -22,18 +22,6 @@ const path = require('path'); let JSZip = null; -function requireScript(fileName, exportNames, variables) { - const absoluteFileName = path.join(__dirname, '..', fileName); - const source = fs.readFileSync(absoluteFileName, {encoding: 'utf8'}); - const exportNamesString = Array.isArray(exportNames) ? exportNames.join(',') : ''; - const variablesArgumentName = '__variables__'; - let variableString = ''; - if (typeof variables === 'object' && variables !== null) { - variableString = Object.keys(variables).join(','); - variableString = `const {${variableString}} = ${variablesArgumentName};`; - } - return Function(variablesArgumentName, `'use strict';${variableString}${source}\n;return {${exportNamesString}};`)(variables); -} function getJSZip() { if (JSZip === null) { @@ -62,9 +50,29 @@ function createTestDictionaryArchive(dictionary, dictionaryName) { return archive; } +function getAllFiles(baseDirectory, predicate=null) { + const results = []; + const directories = [path.resolve(baseDirectory)]; + while (directories.length > 0) { + const directory = directories.shift(); + for (const fileName of fs.readdirSync(directory)) { + const fullFileName = path.resolve(directory, fileName); + const stats = fs.statSync(fullFileName); + if (stats.isFile()) { + if (typeof predicate !== 'function' || predicate(fullFileName, directory, baseDirectory)) { + results.push(fullFileName); + } + } else if (stats.isDirectory()) { + directories.push(fullFileName); + } + } + } + return results; +} + module.exports = { - requireScript, createTestDictionaryArchive, + getAllFiles, get JSZip() { return getJSZip(); } }; diff --git a/test/yomichan-vm.js b/test/yomichan-vm.js new file mode 100644 index 00000000..ff478844 --- /dev/null +++ b/test/yomichan-vm.js @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 vm = require('vm'); +const path = require('path'); +const assert = require('assert'); + + +function getContextEnvironmentRecords(context, names) { + // Enables export of values from the declarative environment record + if (!Array.isArray(names) || names.length === 0) { + return []; + } + + let scriptSource = '(() => {\n "use strict";\n const results = [];'; + for (const name of names) { + scriptSource += `\n try { results.push(${name}); } catch (e) { results.push(void 0); }`; + } + scriptSource += '\n return results;\n})();'; + + const script = new vm.Script(scriptSource, {filename: 'getContextEnvironmentRecords'}); + + const contextHasNames = Object.prototype.hasOwnProperty.call(context, 'names'); + const contextNames = context.names; + context.names = names; + + const results = script.runInContext(context, {}); + + if (contextHasNames) { + context.names = contextNames; + } else { + delete context.names; + } + + return Array.from(results); +} + +function isDeepStrictEqual(val1, val2) { + if (val1 === val2) { return true; } + + if (Array.isArray(val1)) { + if (Array.isArray(val2)) { + return isArrayDeepStrictEqual(val1, val2); + } + } else if (typeof val1 === 'object' && val1 !== null) { + if (typeof val2 === 'object' && val2 !== null) { + return isObjectDeepStrictEqual(val1, val2); + } + } + + return false; +} + +function isArrayDeepStrictEqual(val1, val2) { + const ii = val1.length; + if (ii !== val2.length) { return false; } + + for (let i = 0; i < ii; ++i) { + if (!isDeepStrictEqual(val1[i], val2[i])) { + return false; + } + } + + return true; +} + +function isObjectDeepStrictEqual(val1, val2) { + const keys1 = Object.keys(val1); + const keys2 = Object.keys(val2); + + if (keys1.length !== keys2.length) { return false; } + + const keySet = new Set(keys1); + for (const key of keys2) { + if (!keySet.delete(key)) { return false; } + } + + for (const key of keys1) { + if (!isDeepStrictEqual(val1[key], val2[key])) { + return false; + } + } + + const tag1 = Object.prototype.toString.call(val1); + const tag2 = Object.prototype.toString.call(val2); + if (tag1 !== tag2) { return false; } + + return true; +} + +function deepStrictEqual(actual, expected) { + try { + // This will fail on prototype === comparison on cross context objects + assert.deepStrictEqual(actual, expected); + } catch (e) { + if (!isDeepStrictEqual(actual, expected)) { + throw e; + } + } +} + + +class VM { + constructor(context={}) { + this._context = vm.createContext(context); + this._assert = { + deepStrictEqual + }; + } + + get context() { + return this._context; + } + + get assert() { + return this._assert; + } + + get(names) { + if (typeof names === 'string') { + return getContextEnvironmentRecords(this._context, [names])[0]; + } else if (Array.isArray(names)) { + return getContextEnvironmentRecords(this._context, names); + } else { + throw new Error('Invalid argument'); + } + } + + set(values) { + if (typeof values === 'object' && values !== null) { + Object.assign(this._context, values); + } else { + throw new Error('Invalid argument'); + } + } + + execute(fileNames) { + const single = !Array.isArray(fileNames); + if (single) { + fileNames = [fileNames]; + } + + const results = []; + for (const fileName of fileNames) { + const absoluteFileName = path.resolve(__dirname, '..', 'ext', fileName); + const source = fs.readFileSync(absoluteFileName, {encoding: 'utf8'}); + const script = new vm.Script(source, {filename: absoluteFileName}); + results.push(script.runInContext(this._context, {})); + } + + return single ? results[0] : results; + } +} + + +module.exports = { + VM +}; |