summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2020-03-14 13:14:03 -0700
committerAlex Yatskov <alex@foosoft.net>2020-03-14 13:14:03 -0700
commitb77e2afe3a8ef9e96a53dd8ca97d8b913941244b (patch)
tree818a2f25169845a72b3424b7400b5b07f0f7cacf /test
parentd32f4def0eeed1599857bc04c973337a2a13dd8b (diff)
parent98afe7adae80c6bc9de0c4b996e6f6cb0a5df49d (diff)
Merge branch 'master' into testing
Diffstat (limited to 'test')
-rw-r--r--test/data/html/test-document1.html264
-rw-r--r--test/data/html/test-stylesheet.css32
-rw-r--r--test/dictionary-validate.js8
-rw-r--r--test/lint/global-declarations.js105
-rw-r--r--test/schema-validate.js6
-rw-r--r--test/test-database.js46
-rw-r--r--test/test-document.js240
-rw-r--r--test/test-schema.js12
-rw-r--r--test/yomichan-test.js34
-rw-r--r--test/yomichan-vm.js174
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="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ "
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </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="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ "
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </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="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ t"
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </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="
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ t"
+ >
+ <span>
+ あいうえお
+ かきくけこ
+ さしすせそ
+ たちつてと
+ なにぬねの
+ はひふへほ
+ まみむめも
+ や&#x3000;ゆ&#x3000;よ
+ らりるれろ
+ わゐ&#x3000;ゑを
+ </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
+};