summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorDarius Jahandarie <djahandarie@gmail.com>2023-11-09 13:30:31 +0000
committerGitHub <noreply@github.com>2023-11-09 13:30:31 +0000
commit5c5a167b4792af379cdacf633513cebf20728cd2 (patch)
tree5b6be3620a557d0b9177047003f6d742d9d2a32d /test
parentb64f51c3b13a46af4dd7f1e43048ac19c781ca7b (diff)
parent0f4d36938fd0d844f548aa5a7f7e7842df8dfb41 (diff)
Merge pull request #307 from themoeway/modernize
Modernize codebase
Diffstat (limited to 'test')
-rw-r--r--test/anki-note-builder.test.js224
-rw-r--r--test/cache-map.test.js128
-rw-r--r--test/core.test.js288
-rw-r--r--test/css-json.test.js (renamed from test/test-css-json.js)26
-rw-r--r--test/database.test.js853
-rw-r--r--test/deinflector.test.js (renamed from test/test-deinflector.js)49
-rw-r--r--test/dictionary.test.js (renamed from test/test-dictionary.js)37
-rw-r--r--test/document-util.test.js259
-rw-r--r--test/dom-text-scanner.test.js (renamed from test/test-dom-text-scanner.js)139
-rw-r--r--test/hotkey-util.test.js164
-rw-r--r--test/japanese-util.test.js905
-rw-r--r--test/jsdom.test.js (renamed from test/test-jsdom.js)27
-rw-r--r--test/json-schema.test.js1009
-rw-r--r--test/object-property-accessor.test.js438
-rw-r--r--test/options-util.test.js (renamed from test/test-options-util.js)402
-rw-r--r--test/profile-conditions-util.test.js1090
-rw-r--r--test/test-all.js67
-rw-r--r--test/test-anki-note-builder.js308
-rw-r--r--test/test-build-libs.js42
-rw-r--r--test/test-cache-map.js132
-rw-r--r--test/test-core.js292
-rw-r--r--test/test-database.js887
-rw-r--r--test/test-document-util.js270
-rw-r--r--test/test-hotkey-util.js173
-rw-r--r--test/test-japanese-util.js881
-rw-r--r--test/test-json-schema.js1011
-rw-r--r--test/test-manifest.js44
-rw-r--r--test/test-object-property-accessor.js416
-rw-r--r--test/test-profile-conditions-util.js1099
-rw-r--r--test/test-text-source-map.js235
-rw-r--r--test/test-translator.js94
-rw-r--r--test/test-workers.js137
-rw-r--r--test/text-source-map.test.js237
-rw-r--r--test/translator.test.js83
34 files changed, 5993 insertions, 6453 deletions
diff --git a/test/anki-note-builder.test.js b/test/anki-note-builder.test.js
new file mode 100644
index 00000000..90bb3cbe
--- /dev/null
+++ b/test/anki-note-builder.test.js
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2021-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/>.
+ */
+
+import 'fake-indexeddb/auto';
+import fs from 'fs';
+import {fileURLToPath} from 'node:url';
+import path from 'path';
+import url from 'url';
+import {describe, test, vi} from 'vitest';
+import {TranslatorVM} from '../dev/translator-vm.js';
+import {AnkiNoteBuilder} from '../ext/js/data/anki-note-builder.js';
+import {JapaneseUtil} from '../ext/js/language/sandbox/japanese-util.js';
+
+vi.stubGlobal('fetch', async (url2) => {
+ const extDir = path.join(__dirname, '..', 'ext');
+ let filePath;
+ try {
+ filePath = url.fileURLToPath(url2);
+ } catch (e) {
+ filePath = path.resolve(extDir, url2.replace(/^[/\\]/, ''));
+ }
+ await Promise.resolve();
+ const content = fs.readFileSync(filePath, {encoding: null});
+ return {
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: async () => Promise.resolve(content.toString('utf8')),
+ json: async () => Promise.resolve(JSON.parse(content.toString('utf8')))
+ };
+});
+vi.mock('../ext/js/templates/template-renderer-proxy.js');
+
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+
+async function createVM() {
+ const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', 'valid-dictionary1');
+ const vm = new TranslatorVM();
+
+ await vm.prepare(dictionaryDirectory, 'Test Dictionary 2');
+
+ return vm;
+}
+
+function getFieldMarkers(type) {
+ switch (type) {
+ case 'terms':
+ return [
+ 'audio',
+ 'clipboard-image',
+ 'clipboard-text',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'conjugation',
+ 'dictionary',
+ 'document-title',
+ 'expression',
+ 'frequencies',
+ 'furigana',
+ 'furigana-plain',
+ 'glossary',
+ 'glossary-brief',
+ 'glossary-no-dictionary',
+ 'part-of-speech',
+ 'pitch-accents',
+ 'pitch-accent-graphs',
+ 'pitch-accent-positions',
+ 'reading',
+ 'screenshot',
+ 'search-query',
+ 'selection-text',
+ 'sentence',
+ 'sentence-furigana',
+ 'tags',
+ 'url'
+ ];
+ case 'kanji':
+ return [
+ 'character',
+ 'clipboard-image',
+ 'clipboard-text',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'document-title',
+ 'glossary',
+ 'kunyomi',
+ 'onyomi',
+ 'screenshot',
+ 'search-query',
+ 'selection-text',
+ 'sentence',
+ 'sentence-furigana',
+ 'stroke-count',
+ 'tags',
+ 'url'
+ ];
+ default:
+ return [];
+ }
+}
+
+async function getRenderResults(dictionaryEntries, type, mode, template, expect) {
+ const markers = getFieldMarkers(type);
+ const fields = [];
+ for (const marker of markers) {
+ fields.push([marker, `{${marker}}`]);
+ }
+
+ const japaneseUtil = new JapaneseUtil(null);
+ const clozePrefix = 'cloze-prefix';
+ const clozeSuffix = 'cloze-suffix';
+ const results = [];
+ for (const dictionaryEntry of dictionaryEntries) {
+ let source = '';
+ switch (dictionaryEntry.type) {
+ case 'kanji':
+ source = dictionaryEntry.character;
+ break;
+ case 'term':
+ if (dictionaryEntry.headwords.length > 0 && dictionaryEntry.headwords[0].sources.length > 0) {
+ source = dictionaryEntry.headwords[0].sources[0].originalText;
+ }
+ break;
+ }
+ const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil});
+ const context = {
+ url: 'url:',
+ sentence: {
+ text: `${clozePrefix}${source}${clozeSuffix}`,
+ offset: clozePrefix.length
+ },
+ documentTitle: 'title',
+ query: 'query',
+ fullQuery: 'fullQuery'
+ };
+ const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote({
+ dictionaryEntry,
+ mode: null,
+ context,
+ template,
+ deckName: 'deckName',
+ modelName: 'modelName',
+ fields,
+ tags: ['yomichan'],
+ checkForDuplicates: true,
+ duplicateScope: 'collection',
+ duplicateScopeCheckAllModels: false,
+ resultOutputMode: mode,
+ glossaryLayoutMode: 'default',
+ compactTags: false
+ });
+ for (const error of errors) {
+ console.error(error);
+ }
+ expect(errors.length).toStrictEqual(0);
+ results.push(noteFields);
+ }
+
+ return results;
+}
+
+
+async function main() {
+ const vm = await createVM();
+
+ const testInputsFilePath = path.join(dirname, 'data', 'translator-test-inputs.json');
+ const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'}));
+
+ const testResults1FilePath = path.join(dirname, 'data', 'anki-note-builder-test-results.json');
+ const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'}));
+ const actualResults1 = [];
+
+ const template = fs.readFileSync(path.join(dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'});
+
+ describe.concurrent('AnkiNoteBuilder', () => {
+ for (let i = 0, ii = tests.length; i < ii; ++i) {
+ const t = tests[i];
+ test(`${t.name}`, async ({expect}) => {
+ const expected1 = expectedResults1[i];
+ switch (t.func) {
+ case 'findTerms':
+ {
+ const {name, mode, text} = t;
+ const options = vm.buildOptions(optionsPresets, t.options);
+ const {dictionaryEntries} = structuredClone(await vm.translator.findTerms(mode, text, options));
+ const results = mode !== 'simple' ? structuredClone(await getRenderResults(dictionaryEntries, 'terms', mode, template, expect)) : null;
+ actualResults1.push({name, results});
+ expect(results).toStrictEqual(expected1.results);
+ }
+ break;
+ case 'findKanji':
+ {
+ const {name, text} = t;
+ const options = vm.buildOptions(optionsPresets, t.options);
+ const dictionaryEntries = structuredClone(await vm.translator.findKanji(text, options));
+ const results = structuredClone(await getRenderResults(dictionaryEntries, 'kanji', null, template, expect));
+ actualResults1.push({name, results});
+ expect(results).toStrictEqual(expected1.results);
+ }
+ break;
+ }
+ });
+ }
+ });
+}
+await main();
diff --git a/test/cache-map.test.js b/test/cache-map.test.js
new file mode 100644
index 00000000..9d10a719
--- /dev/null
+++ b/test/cache-map.test.js
@@ -0,0 +1,128 @@
+/*
+ * 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/>.
+ */
+
+import {expect, test} from 'vitest';
+import {CacheMap} from '../ext/js/general/cache-map.js';
+
+function testConstructor() {
+ test('constructor', () => {
+ const data = [
+ [false, () => new CacheMap(0)],
+ [false, () => new CacheMap(1)],
+ [false, () => new CacheMap(Number.MAX_VALUE)],
+ [true, () => new CacheMap(-1)],
+ [true, () => new CacheMap(1.5)],
+ [true, () => new CacheMap(Number.NaN)],
+ [true, () => new CacheMap(Number.POSITIVE_INFINITY)],
+ [true, () => new CacheMap('a')]
+ ];
+
+ for (const [throws, create] of data) {
+ if (throws) {
+ expect(create).toThrowError();
+ } else {
+ expect(create).not.toThrowError();
+ }
+ }
+ });
+}
+
+function testApi() {
+ test('api', () => {
+ const data = [
+ {
+ maxSize: 1,
+ expectedSize: 0,
+ calls: []
+ },
+ {
+ maxSize: 10,
+ expectedSize: 1,
+ calls: [
+ {func: 'get', args: ['a1-b-c'], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: false},
+ {func: 'set', args: ['a1-b-c', 32], returnValue: void 0},
+ {func: 'get', args: ['a1-b-c'], returnValue: 32},
+ {func: 'has', args: ['a1-b-c'], returnValue: true}
+ ]
+ },
+ {
+ maxSize: 10,
+ expectedSize: 2,
+ calls: [
+ {func: 'set', args: ['a1-b-c', 32], returnValue: void 0},
+ {func: 'get', args: ['a1-b-c'], returnValue: 32},
+ {func: 'set', args: ['a1-b-c', 64], returnValue: void 0},
+ {func: 'get', args: ['a1-b-c'], returnValue: 64},
+ {func: 'set', args: ['a2-b-c', 96], returnValue: void 0},
+ {func: 'get', args: ['a2-b-c'], returnValue: 96}
+ ]
+ },
+ {
+ maxSize: 2,
+ expectedSize: 2,
+ calls: [
+ {func: 'has', args: ['a1-b-c'], returnValue: false},
+ {func: 'has', args: ['a2-b-c'], returnValue: false},
+ {func: 'has', args: ['a3-b-c'], returnValue: false},
+ {func: 'set', args: ['a1-b-c', 1], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: true},
+ {func: 'has', args: ['a2-b-c'], returnValue: false},
+ {func: 'has', args: ['a3-b-c'], returnValue: false},
+ {func: 'set', args: ['a2-b-c', 2], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: true},
+ {func: 'has', args: ['a2-b-c'], returnValue: true},
+ {func: 'has', args: ['a3-b-c'], returnValue: false},
+ {func: 'set', args: ['a3-b-c', 3], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: false},
+ {func: 'has', args: ['a2-b-c'], returnValue: true},
+ {func: 'has', args: ['a3-b-c'], returnValue: true}
+ ]
+ }
+ ];
+
+ for (const {maxSize, expectedSize, calls} of data) {
+ const cache = new CacheMap(maxSize);
+ expect(cache.maxSize).toStrictEqual(maxSize);
+ for (const call of calls) {
+ const {func, args} = call;
+ let returnValue;
+ switch (func) {
+ case 'get': returnValue = cache.get(...args); break;
+ case 'set': returnValue = cache.set(...args); break;
+ case 'has': returnValue = cache.has(...args); break;
+ case 'clear': returnValue = cache.clear(...args); break;
+ }
+ if (Object.prototype.hasOwnProperty.call(call, 'returnValue')) {
+ const {returnValue: expectedReturnValue} = call;
+ expect(returnValue).toStrictEqual(expectedReturnValue);
+ }
+ }
+ expect(cache.size).toStrictEqual(expectedSize);
+ }
+ });
+}
+
+
+function main() {
+ testConstructor();
+ testApi();
+}
+
+
+main();
diff --git a/test/core.test.js b/test/core.test.js
new file mode 100644
index 00000000..203460f4
--- /dev/null
+++ b/test/core.test.js
@@ -0,0 +1,288 @@
+/*
+ * 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/>.
+ */
+
+import {describe, expect, test} from 'vitest';
+import {DynamicProperty, deepEqual} from '../ext/js/core.js';
+
+function testDynamicProperty() {
+ test('DynamicProperty', () => {
+ const data = [
+ {
+ initialValue: 0,
+ operations: [
+ {
+ operation: null,
+ expectedDefaultValue: 0,
+ expectedValue: 0,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'set.defaultValue',
+ args: [1],
+ expectedDefaultValue: 1,
+ expectedValue: 1,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'set.defaultValue',
+ args: [1],
+ expectedDefaultValue: 1,
+ expectedValue: 1,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'set.defaultValue',
+ args: [0],
+ expectedDefaultValue: 0,
+ expectedValue: 0,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'setOverride',
+ args: [8],
+ expectedDefaultValue: 0,
+ expectedValue: 8,
+ expectedOverrideCount: 1,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'setOverride',
+ args: [16],
+ expectedDefaultValue: 0,
+ expectedValue: 8,
+ expectedOverrideCount: 2,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'setOverride',
+ args: [32, 1],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 3,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'setOverride',
+ args: [64, -1],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 4,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'clearOverride',
+ args: [-4],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 3,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'clearOverride',
+ args: [-3],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 2,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'clearOverride',
+ args: [-2],
+ expectedDefaultValue: 0,
+ expectedValue: 64,
+ expectedOverrideCount: 1,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'clearOverride',
+ args: [-1],
+ expectedDefaultValue: 0,
+ expectedValue: 0,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: true
+ }
+ ]
+ }
+ ];
+
+ for (const {initialValue, operations} of data) {
+ const property = new DynamicProperty(initialValue);
+ const overrideTokens = [];
+ let eventOccurred = false;
+ const onChange = () => { eventOccurred = true; };
+ property.on('change', onChange);
+ for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) {
+ eventOccurred = false;
+ switch (operation) {
+ case 'set.defaultValue': property.defaultValue = args[0]; break;
+ case 'setOverride': overrideTokens.push(property.setOverride(...args)); break;
+ case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break;
+ }
+ expect(eventOccurred).toStrictEqual(expeectedEventOccurred);
+ expect(property.defaultValue).toStrictEqual(expectedDefaultValue);
+ expect(property.value).toStrictEqual(expectedValue);
+ expect(property.overrideCount).toStrictEqual(expectedOverrideCount);
+ }
+ property.off('change', onChange);
+ }
+ });
+}
+
+function testDeepEqual() {
+ describe('deepEqual', () => {
+ const data = [
+ // Simple tests
+ {
+ value1: 0,
+ value2: 0,
+ expected: true
+ },
+ {
+ value1: null,
+ value2: null,
+ expected: true
+ },
+ {
+ value1: 'test',
+ value2: 'test',
+ expected: true
+ },
+ {
+ value1: true,
+ value2: true,
+ expected: true
+ },
+ {
+ value1: 0,
+ value2: 1,
+ expected: false
+ },
+ {
+ value1: null,
+ value2: false,
+ expected: false
+ },
+ {
+ value1: 'test1',
+ value2: 'test2',
+ expected: false
+ },
+ {
+ value1: true,
+ value2: false,
+ expected: false
+ },
+
+ // Simple object tests
+ {
+ value1: {},
+ value2: {},
+ expected: true
+ },
+ {
+ value1: {},
+ value2: [],
+ expected: false
+ },
+ {
+ value1: [],
+ value2: [],
+ expected: true
+ },
+ {
+ value1: {},
+ value2: null,
+ expected: false
+ },
+
+ // Complex object tests
+ {
+ value1: [1],
+ value2: [],
+ expected: false
+ },
+ {
+ value1: [1],
+ value2: [1],
+ expected: true
+ },
+ {
+ value1: [1],
+ value2: [2],
+ expected: false
+ },
+
+ {
+ value1: {},
+ value2: {test: 1},
+ expected: false
+ },
+ {
+ value1: {test: 1},
+ value2: {test: 1},
+ expected: true
+ },
+ {
+ value1: {test: 1},
+ value2: {test: {test2: false}},
+ expected: false
+ },
+ {
+ value1: {test: {test2: true}},
+ value2: {test: {test2: false}},
+ expected: false
+ },
+ {
+ value1: {test: {test2: [true]}},
+ value2: {test: {test2: [true]}},
+ expected: true
+ },
+
+ // Recursive
+ {
+ value1: (() => { const x = {}; x.x = x; return x; })(),
+ value2: (() => { const x = {}; x.x = x; return x; })(),
+ expected: false
+ }
+ ];
+
+ let index = 0;
+ for (const {value1, value2, expected} of data) {
+ test(`${index}`, () => {
+ const actual1 = deepEqual(value1, value2);
+ expect(actual1).toStrictEqual(expected);
+
+ const actual2 = deepEqual(value2, value1);
+ expect(actual2).toStrictEqual(expected);
+ });
+ ++index;
+ }
+ });
+}
+
+
+function main() {
+ testDynamicProperty();
+ testDeepEqual();
+}
+
+main();
diff --git a/test/test-css-json.js b/test/css-json.test.js
index ddeee6bd..0aaf7d10 100644
--- a/test/test-css-json.js
+++ b/test/css-json.test.js
@@ -16,22 +16,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-const fs = require('fs');
-const assert = require('assert');
-const {testMain} = require('../dev/util');
-const {formatRulesJson, generateRules} = require('../dev/css-to-json-util');
-const {getTargets} = require('../dev/generate-css-json');
-
+import fs from 'fs';
+import {expect, test} from 'vitest';
+import {formatRulesJson, generateRules, getTargets} from '../dev/generate-css-json';
function main() {
- for (const {cssFile, overridesCssFile, outputPath} of getTargets()) {
- const actual = fs.readFileSync(outputPath, {encoding: 'utf8'});
- const expected = formatRulesJson(generateRules(cssFile, overridesCssFile));
- assert.deepStrictEqual(actual, expected);
- }
+ test('css-json', () => {
+ for (const {cssFile, overridesCssFile, outputPath} of getTargets()) {
+ const actual = fs.readFileSync(outputPath, {encoding: 'utf8'});
+ const expected = formatRulesJson(generateRules(cssFile, overridesCssFile));
+ expect(actual).toStrictEqual(expected);
+ }
+ });
}
-
-if (require.main === module) {
- testMain(main, process.argv.slice(2));
-}
+main();
diff --git a/test/database.test.js b/test/database.test.js
new file mode 100644
index 00000000..b53d0e65
--- /dev/null
+++ b/test/database.test.js
@@ -0,0 +1,853 @@
+/*
+ * 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/>.
+ */
+
+import {IDBFactory, IDBKeyRange} from 'fake-indexeddb';
+import path from 'path';
+import {beforeEach, describe, expect, test, vi} from 'vitest';
+import {createDictionaryArchive} from '../dev/util.js';
+import {DictionaryDatabase} from '../ext/js/language/dictionary-database.js';
+import {DictionaryImporterMediaLoader} from '../ext/js/language/dictionary-importer-media-loader.js';
+import {DictionaryImporter} from '../ext/js/language/dictionary-importer.js';
+
+vi.stubGlobal('IDBKeyRange', IDBKeyRange);
+
+vi.mock('../ext/js/language/dictionary-importer-media-loader.js');
+
+function createTestDictionaryArchive(dictionary, dictionaryName) {
+ const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary);
+ return createDictionaryArchive(dictionaryDirectory, dictionaryName);
+}
+
+
+function createDictionaryImporter(onProgress) {
+ const dictionaryImporterMediaLoader = new DictionaryImporterMediaLoader();
+ return new DictionaryImporter(dictionaryImporterMediaLoader, (...args) => {
+ const {stepIndex, stepCount, index, count} = args[0];
+ expect(stepIndex < stepCount).toBe(true);
+ expect(index <= count).toBe(true);
+ if (typeof onProgress === 'function') {
+ onProgress(...args);
+ }
+ });
+}
+
+
+function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) {
+ return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0);
+}
+
+function countDictionaryDatabaseEntriesWithReading(dictionaryDatabaseEntries, reading) {
+ return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0);
+}
+
+function countMetasWithMode(metas, mode) {
+ return metas.reduce((i, v) => (i + (v.mode === mode ? 1 : 0)), 0);
+}
+
+function countKanjiWithCharacter(kanji, character) {
+ return kanji.reduce((i, v) => (i + (v.character === character ? 1 : 0)), 0);
+}
+
+
+
+async function testDatabase1() {
+ test('Database1', async () => { // Load dictionary data
+ const testDictionary = createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup iteration data
+ const iterations = [
+ {
+ cleanup: async () => {
+ // Test purge
+ await dictionaryDatabase.purge();
+ await testDatabaseEmpty1(dictionaryDatabase);
+ }
+ },
+ {
+ cleanup: async () => {
+ // Test deleteDictionary
+ let progressEvent = false;
+ await dictionaryDatabase.deleteDictionary(
+ title,
+ 1000,
+ () => {
+ progressEvent = true;
+ }
+ );
+ expect(progressEvent).toBe(true);
+
+ await testDatabaseEmpty1(dictionaryDatabase);
+ }
+ },
+ {
+ cleanup: async () => {}
+ }
+ ];
+
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase();
+ await dictionaryDatabase.prepare();
+
+ for (const {cleanup} of iterations) {
+ const expectedSummary = {
+ title,
+ revision: 'test',
+ sequenced: true,
+ version: 3,
+ importDate: 0,
+ prefixWildcardsSupported: true,
+ counts: {
+ kanji: {total: 2},
+ kanjiMeta: {total: 6, freq: 6},
+ media: {total: 4},
+ tagMeta: {total: 15},
+ termMeta: {total: 38, freq: 31, pitch: 7},
+ terms: {total: 21}
+ }
+ };
+
+ // Import data
+ let progressEvent = false;
+ const dictionaryImporter = createDictionaryImporter(() => { progressEvent = true; });
+ const {result, errors} = await dictionaryImporter.importDictionary(
+ dictionaryDatabase,
+ testDictionarySource,
+ {prefixWildcardsSupported: true}
+ );
+ expectedSummary.importDate = result.importDate;
+ expect(errors).toStrictEqual([]);
+ expect(result).toStrictEqual(expectedSummary);
+ expect(progressEvent).toBe(true);
+
+ // Get info summary
+ const info = await dictionaryDatabase.getDictionaryInfo();
+ expect(info).toStrictEqual([expectedSummary]);
+
+ // Get counts
+ const counts = await dictionaryDatabase.getDictionaryCounts(
+ info.map((v) => v.title),
+ true
+ );
+ expect(counts).toStrictEqual({
+ counts: [{kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}],
+ total: {kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}
+ });
+
+ // Test find* functions
+ await testFindTermsBulkTest1(dictionaryDatabase, titles);
+ await testTindTermsExactBulk1(dictionaryDatabase, titles);
+ await testFindTermsBySequenceBulk1(dictionaryDatabase, title);
+ await testFindTermMetaBulk1(dictionaryDatabase, titles);
+ await testFindKanjiBulk1(dictionaryDatabase, titles);
+ await testFindKanjiMetaBulk1(dictionaryDatabase, titles);
+ await testFindTagForTitle1(dictionaryDatabase, title);
+
+ // Cleanup
+ await cleanup();
+ }
+
+ await dictionaryDatabase.close();
+ });
+}
+
+async function testDatabaseEmpty1(database) {
+ test('DatabaseEmpty1', async () => {
+ const info = await database.getDictionaryInfo();
+ expect(info).toStrictEqual([]);
+
+ const counts = await database.getDictionaryCounts([], true);
+ expect(counts).toStrictEqual({
+ counts: [],
+ total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0, media: 0}
+ });
+ });
+}
+
+async function testFindTermsBulkTest1(database, titles) {
+ test('FindTermsBulkTest1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ matchType: null,
+ termList: ['打', '打つ', '打ち込む']
+ },
+ {
+ matchType: null,
+ termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ']
+ },
+ {
+ matchType: 'prefix',
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 10,
+ terms: [
+ ['打', 2],
+ ['打つ', 4],
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 2],
+ ['ぶつ', 2],
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: null,
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: 'suffix',
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: null,
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList, matchType} of inputs) {
+ const results = await database.findTermsBulk(termList, titles, matchType);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testTindTermsExactBulk1(database, titles) {
+ test('TindTermsExactBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打', reading: 'だ'},
+ {term: '打つ', reading: 'うつ'},
+ {term: '打ち込む', reading: 'うちこむ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 5,
+ terms: [
+ ['打', 1],
+ ['打つ', 2],
+ ['打ち込む', 2]
+ ],
+ readings: [
+ ['だ', 1],
+ ['うつ', 2],
+ ['うちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打', reading: 'だ?'},
+ {term: '打つ', reading: 'うつ?'},
+ {term: '打ち込む', reading: 'うちこむ?'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打つ', reading: 'うつ'},
+ {term: '打つ', reading: 'ぶつ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打つ', 4]
+ ],
+ readings: [
+ ['うつ', 2],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打つ', reading: 'うちこむ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermsExactBulk(termList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindTermsBySequenceBulk1(database, mainDictionary) {
+ test('FindTermsBySequenceBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ sequenceList: [1, 2, 3, 4, 5]
+ }
+ ],
+ expectedResults: {
+ total: 11,
+ terms: [
+ ['打', 2],
+ ['打つ', 4],
+ ['打ち込む', 4],
+ ['画像', 1]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 2],
+ ['ぶつ', 2],
+ ['うちこむ', 2],
+ ['ぶちこむ', 2],
+ ['がぞう', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [1]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['打', 1]
+ ],
+ readings: [
+ ['だ', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [2]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['打', 1]
+ ],
+ readings: [
+ ['ダース', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [3]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打つ', 4]
+ ],
+ readings: [
+ ['うつ', 2],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [4]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [5]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['画像', 1]
+ ],
+ readings: [
+ ['がぞう', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [-1]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {sequenceList} of inputs) {
+ const results = await database.findTermsBySequenceBulk(sequenceList.map((query) => ({query, dictionary: mainDictionary})));
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindTermMetaBulk1(database, titles) {
+ test('FindTermMetaBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 11,
+ modes: [
+ ['freq', 11]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ']
+ }
+ ],
+ expectedResults: {
+ total: 10,
+ modes: [
+ ['freq', 10]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打ち込む']
+ }
+ ],
+ expectedResults: {
+ total: 12,
+ modes: [
+ ['freq', 10],
+ ['pitch', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermMetaBulk(termList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ expect(countMetasWithMode(results, mode)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindKanjiBulk1(database, titles) {
+ test('FindKanjiBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['打', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['込', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ kanji: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiBulk(kanjiList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [kanji, count] of expectedResults.kanji) {
+ expect(countKanjiWithCharacter(results, kanji)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindKanjiMetaBulk1(database, titles) {
+ test('FindKanjiMetaBulk1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 3,
+ modes: [
+ ['freq', 3]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 3,
+ modes: [
+ ['freq', 3]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiMetaBulk(kanjiList, titles);
+ expect(results.length).toStrictEqual(expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ expect(countMetasWithMode(results, mode)).toStrictEqual(count);
+ }
+ }
+ }
+ });
+}
+
+async function testFindTagForTitle1(database, title) {
+ test('FindTagForTitle1', async () => {
+ const data = [
+ {
+ inputs: [
+ {
+ name: 'E1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'default', dictionary: title, name: 'E1', notes: 'example tag 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'K1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'default', dictionary: title, name: 'K1', notes: 'example kanji tag 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'kstat1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'class', dictionary: title, name: 'kstat1', notes: 'kanji stat 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'invalid'
+ }
+ ],
+ expectedResults: {
+ value: null
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {name} of inputs) {
+ const result = await database.findTagForTitle(name, title);
+ expect(result).toStrictEqual(expectedResults.value);
+ }
+ }
+ });
+}
+
+
+async function testDatabase2() {
+ test('Database2', async () => { // Load dictionary data
+ const testDictionary = createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase();
+
+ // Database not open
+ await expect(dictionaryDatabase.deleteDictionary(title, 1000)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermsBulk(['?'], titles, null)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermsExactBulk([{term: '?', reading: '?'}], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermsBySequenceBulk([{query: 1, dictionary: title}])).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermMetaBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTermMetaBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findKanjiBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findKanjiMetaBulk(['?'], titles)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.findTagForTitle('tag', title)).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.getDictionaryInfo()).rejects.toThrow('Database not open');
+ await expect(dictionaryDatabase.getDictionaryCounts(titles, true)).rejects.toThrow('Database not open');
+ await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Database is not ready');
+
+ await dictionaryDatabase.prepare();
+
+ // already prepared
+ await expect(dictionaryDatabase.prepare()).rejects.toThrow('Database already open');
+
+ await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {});
+
+ // dictionary already imported
+ await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Dictionary is already imported');
+
+ await dictionaryDatabase.close();
+ });
+}
+
+
+async function testDatabase3() {
+ const invalidDictionaries = [
+ 'invalid-dictionary1',
+ 'invalid-dictionary2',
+ 'invalid-dictionary3',
+ 'invalid-dictionary4',
+ 'invalid-dictionary5',
+ 'invalid-dictionary6'
+ ];
+
+
+ describe('Database3', () => {
+ for (const invalidDictionary of invalidDictionaries) {
+ test(`${invalidDictionary}`, async () => {
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase();
+ await dictionaryDatabase.prepare();
+
+ const testDictionary = createTestDictionaryArchive(invalidDictionary);
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+
+ await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Dictionary has invalid data');
+ await dictionaryDatabase.close();
+ });
+ }
+ });
+}
+
+
+async function main() {
+ beforeEach(async () => {
+ globalThis.indexedDB = new IDBFactory();
+ });
+ await testDatabase1();
+ await testDatabase2();
+ await testDatabase3();
+}
+
+await main();
diff --git a/test/test-deinflector.js b/test/deinflector.test.js
index a20cfc95..edb85833 100644
--- a/test/test-deinflector.js
+++ b/test/deinflector.test.js
@@ -16,14 +16,12 @@
* 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 {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
+import fs from 'fs';
+import path from 'path';
+import {describe, expect, test} from 'vitest';
+import {Deinflector} from '../ext/js/language/deinflector.js';
-
-function hasTermReasons(Deinflector, deinflector, source, expectedTerm, expectedRule, expectedReasons) {
+function hasTermReasons(deinflector, source, expectedTerm, expectedRule, expectedReasons) {
for (const {term, reasons, rules} of deinflector.deinflect(source, source)) {
if (term !== expectedTerm) { continue; }
if (typeof expectedRule !== 'undefined') {
@@ -917,30 +915,27 @@ function testDeinflections() {
}
];
- const vm = new VM();
- vm.execute(['js/language/deinflector.js']);
- const [Deinflector] = vm.get(['Deinflector']);
-
const deinflectionReasons = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/deinflect.json')));
const deinflector = new Deinflector(deinflectionReasons);
- for (const {valid, tests} of data) {
- for (const {source, term, rule, reasons} of tests) {
- const {has, reasons: actualReasons} = hasTermReasons(Deinflector, deinflector, source, term, rule, reasons);
- let message = `${source} ${valid ? 'does not have' : 'has'} term candidate ${JSON.stringify(term)}`;
- if (typeof rule !== 'undefined') {
- message += ` with rule ${JSON.stringify(rule)}`;
- }
- if (typeof reasons !== 'undefined') {
- message += (typeof rule !== 'undefined' ? ' and' : ' with');
- message += ` reasons ${JSON.stringify(reasons)}`;
- }
- if (actualReasons !== null) {
- message += ` (actual reasons: ${JSON.stringify(actualReasons)})`;
+ describe('deinflections', () => {
+ for (const {valid, tests} of data) {
+ for (const {source, term, rule, reasons} of tests) {
+ const {has} = hasTermReasons(deinflector, source, term, rule, reasons);
+ let message = `${source} ${valid ? 'has' : 'does not have'} term candidate ${JSON.stringify(term)}`;
+ if (typeof rule !== 'undefined') {
+ message += ` with rule ${JSON.stringify(rule)}`;
+ }
+ if (typeof reasons !== 'undefined') {
+ message += (typeof rule !== 'undefined' ? ' and' : ' with');
+ message += ` reasons ${JSON.stringify(reasons)}`;
+ }
+ test(`${message}`, () => {
+ expect(has).toStrictEqual(valid);
+ });
}
- assert.strictEqual(has, valid, message);
}
- }
+ });
}
@@ -949,4 +944,4 @@ function main() {
}
-if (require.main === module) { testMain(main); }
+main();
diff --git a/test/test-dictionary.js b/test/dictionary.test.js
index d4390e19..8f160bc1 100644
--- a/test/test-dictionary.js
+++ b/test/dictionary.test.js
@@ -16,13 +16,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-const path = require('path');
-const {createDictionaryArchive, testMain} = require('../dev/util');
-const dictionaryValidate = require('../dev/dictionary-validate');
+import {fileURLToPath} from 'node:url';
+import path from 'path';
+import {expect, test} from 'vitest';
+import * as dictionaryValidate from '../dev/dictionary-validate.js';
+import {createDictionaryArchive} from '../dev/util.js';
+const dirname = path.dirname(fileURLToPath(import.meta.url));
function createTestDictionaryArchive(dictionary, dictionaryName) {
- const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary);
+ const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', dictionary);
return createDictionaryArchive(dictionaryDirectory, dictionaryName);
}
@@ -41,26 +44,16 @@ async function main() {
const schemas = dictionaryValidate.getSchemas();
for (const {name, valid} of dictionaries) {
- const archive = createTestDictionaryArchive(name);
+ test(`${name} is ${valid ? 'valid' : 'invalid'}`, async () => {
+ const archive = createTestDictionaryArchive(name);
- let error = null;
- try {
- await dictionaryValidate.validateDictionary(null, archive, schemas);
- } catch (e) {
- error = e;
- }
-
- if (valid) {
- if (error !== null) {
- throw error;
- }
- } else {
- if (error === null) {
- throw new Error(`Expected dictionary ${name} to be invalid`);
+ if (valid) {
+ await expect(dictionaryValidate.validateDictionary(null, archive, schemas)).resolves.not.toThrow();
+ } else {
+ await expect(dictionaryValidate.validateDictionary(null, archive, schemas)).rejects.toThrow();
}
- }
+ });
}
}
-
-if (require.main === module) { testMain(main); }
+await main();
diff --git a/test/document-util.test.js b/test/document-util.test.js
new file mode 100644
index 00000000..f2552f78
--- /dev/null
+++ b/test/document-util.test.js
@@ -0,0 +1,259 @@
+/*
+ * 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/>.
+ */
+
+import fs from 'fs';
+import {JSDOM} from 'jsdom';
+import {fileURLToPath} from 'node:url';
+import path from 'path';
+import {expect, test} from 'vitest';
+import {DocumentUtil} from '../ext/js/dom/document-util.js';
+import {DOMTextScanner} from '../ext/js/dom/dom-text-scanner.js';
+import {TextSourceElement} from '../ext/js/dom/text-source-element.js';
+import {TextSourceRange} from '../ext/js/dom/text-source-range.js';
+
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+
+// 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;
+
+ try {
+ await testDocumentTextScanningFunctions(dom);
+ await testTextSourceRangeSeekFunctions(dom);
+ } finally {
+ window.close();
+ }
+}
+
+async function testDocumentTextScanningFunctions(dom) {
+ const document = dom.window.document;
+
+ test('DocumentTextScanningFunctions', () => {
+ for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) {
+ // Get test parameters
+ let {
+ 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));
+
+ startOffset = parseInt(startOffset, 10);
+ endOffset = parseInt(endOffset, 10);
+ sentenceScanExtent = parseInt(sentenceScanExtent, 10);
+ terminateAtNewlines = (terminateAtNewlines !== 'false');
+
+ expect(elementFromPointValue).not.toStrictEqual(null);
+ expect(caretRangeFromPointValue).not.toStrictEqual(null);
+ expect(startNode).not.toStrictEqual(null);
+ expect(endNode).not.toStrictEqual(null);
+
+ // Setup functions
+ document.elementFromPoint = () => elementFromPointValue;
+
+ document.caretRangeFromPoint = (x, y) => {
+ const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document));
+ expect(!!imposter).toStrictEqual(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 = DocumentUtil.getRangeFromPoint(0, 0, {
+ deepContentScan: false,
+ normalizeCssZoom: true
+ });
+ switch (resultType) {
+ case 'TextSourceRange':
+ expect(getPrototypeOfOrNull(source)).toStrictEqual(TextSourceRange.prototype);
+ break;
+ case 'TextSourceElement':
+ expect(getPrototypeOfOrNull(source)).toStrictEqual(TextSourceElement.prototype);
+ break;
+ case 'null':
+ expect(source).toStrictEqual(null);
+ break;
+ default:
+ expect.unreachable();
+ 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,
+ sentenceScanExtent,
+ terminateAtNewlines,
+ terminatorMap,
+ forwardQuoteMap,
+ backwardQuoteMap
+ ).text;
+ expect(sentenceActual).toStrictEqual(sentence);
+
+ // Clean
+ source.cleanup();
+ }
+ });
+}
+
+async function testTextSourceRangeSeekFunctions(dom) {
+ const document = dom.window.document;
+
+ test('TextSourceRangeSeekFunctions', async () => {
+ 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' ?
+ new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) :
+ new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength)
+ );
+
+ expect(node).toStrictEqual(expectedResultNode);
+ expect(offset).toStrictEqual(expectedResultOffset);
+ expect(content).toStrictEqual(expectedResultContent);
+ }
+ });
+}
+
+
+async function main() {
+ await testDocument1();
+}
+
+await main();
diff --git a/test/test-dom-text-scanner.js b/test/dom-text-scanner.test.js
index 37017b01..d1b31276 100644
--- a/test/test-dom-text-scanner.js
+++ b/test/dom-text-scanner.test.js
@@ -16,13 +16,11 @@
* 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');
-
+import fs from 'fs';
+import {JSDOM} from 'jsdom';
+import path from 'path';
+import {expect, test} from 'vitest';
+import {DOMTextScanner} from '../ext/js/dom/dom-text-scanner.js';
function createJSDOM(fileName) {
const domSource = fs.readFileSync(fileName, {encoding: 'utf8'});
@@ -87,74 +85,77 @@ function createAbsoluteGetComputedStyle(window) {
}
-async function testDomTextScanner(dom, {DOMTextScanner}) {
+async function testDomTextScanner(dom) {
const document = dom.window.document;
- for (const testElement of document.querySelectorAll('y-test')) {
- let testData = JSON.parse(testElement.dataset.testData);
- if (!Array.isArray(testData)) {
- testData = [testData];
- }
- for (const testDataItem of testData) {
- let {
- node,
- offset,
- length,
- forcePreserveWhitespace,
- generateLayoutContent,
- reversible,
- expected: {
- node: expectedNode,
- offset: expectedOffset,
- content: expectedContent,
- remainder: expectedRemainder
- }
- } = testDataItem;
-
- node = querySelectorTextNode(testElement, node);
- expectedNode = querySelectorTextNode(testElement, expectedNode);
- // Standard test
- {
- const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent);
- scanner.seek(length);
-
- const {node: actualNode1, offset: actualOffset1, content: actualContent1, remainder: actualRemainder1} = scanner;
- assert.strictEqual(actualContent1, expectedContent);
- assert.strictEqual(actualOffset1, expectedOffset);
- assert.strictEqual(actualNode1, expectedNode);
- assert.strictEqual(actualRemainder1, expectedRemainder || 0);
+ test('DomTextScanner', () => {
+ for (const testElement of document.querySelectorAll('y-test')) {
+ let testData = JSON.parse(testElement.dataset.testData);
+ if (!Array.isArray(testData)) {
+ testData = [testData];
}
+ for (const testDataItem of testData) {
+ let {
+ node,
+ offset,
+ length,
+ forcePreserveWhitespace,
+ generateLayoutContent,
+ reversible,
+ expected: {
+ node: expectedNode,
+ offset: expectedOffset,
+ content: expectedContent,
+ remainder: expectedRemainder
+ }
+ } = testDataItem;
+
+ node = querySelectorTextNode(testElement, node);
+ expectedNode = querySelectorTextNode(testElement, expectedNode);
+
+ // Standard test
+ {
+ const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent);
+ scanner.seek(length);
+
+ const {node: actualNode1, offset: actualOffset1, content: actualContent1, remainder: actualRemainder1} = scanner;
+ expect(actualContent1).toStrictEqual(expectedContent);
+ expect(actualOffset1).toStrictEqual(expectedOffset);
+ expect(actualNode1).toStrictEqual(expectedNode);
+ expect(actualRemainder1).toStrictEqual(expectedRemainder || 0);
+ }
- // Substring tests
- for (let i = 1; i <= length; ++i) {
- const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent);
- scanner.seek(length - i);
+ // Substring tests
+ for (let i = 1; i <= length; ++i) {
+ const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent);
+ scanner.seek(length - i);
- const {content: actualContent} = scanner;
- assert.strictEqual(actualContent, expectedContent.substring(0, expectedContent.length - i));
- }
+ const {content: actualContent} = scanner;
+ expect(actualContent).toStrictEqual(expectedContent.substring(0, expectedContent.length - i));
+ }
- if (reversible === false) { continue; }
+ if (reversible === false) { continue; }
- // Reversed test
- {
- const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent);
- scanner.seek(-length);
+ // Reversed test
+ {
+ const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent);
+ scanner.seek(-length);
- const {content: actualContent} = scanner;
- assert.strictEqual(actualContent, expectedContent);
- }
+ const {content: actualContent} = scanner;
+ expect(actualContent).toStrictEqual(expectedContent);
+ }
- // Reversed substring tests
- for (let i = 1; i <= length; ++i) {
- const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent);
- scanner.seek(-(length - i));
+ // Reversed substring tests
+ for (let i = 1; i <= length; ++i) {
+ const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent);
+ scanner.seek(-(length - i));
- const {content: actualContent} = scanner;
- assert.strictEqual(actualContent, expectedContent.substring(i));
+ const {content: actualContent} = scanner;
+ expect(actualContent).toStrictEqual(expectedContent.substring(i));
+ }
}
}
- }
+ });
}
@@ -162,17 +163,8 @@ async function testDocument1() {
const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-dom-text-scanner.html'));
const window = dom.window;
try {
- const {document, Node, Range} = window;
-
window.getComputedStyle = createAbsoluteGetComputedStyle(window);
- const vm = new VM({document, window, Range, Node});
- vm.execute([
- 'js/data/sandbox/string-util.js',
- 'js/dom/dom-text-scanner.js'
- ]);
- const DOMTextScanner = vm.get('DOMTextScanner');
-
await testDomTextScanner(dom, {DOMTextScanner});
} finally {
window.close();
@@ -184,5 +176,4 @@ async function main() {
await testDocument1();
}
-
-if (require.main === module) { testMain(main); }
+await main();
diff --git a/test/hotkey-util.test.js b/test/hotkey-util.test.js
new file mode 100644
index 00000000..8666b98b
--- /dev/null
+++ b/test/hotkey-util.test.js
@@ -0,0 +1,164 @@
+/*
+ * 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/>.
+ */
+
+import {expect, test} from 'vitest';
+import {HotkeyUtil} from '../ext/js/input/hotkey-util.js';
+
+function testCommandConversions() {
+ test('CommandConversions', () => {
+ const data = [
+ {os: 'win', command: 'Alt+F', expectedCommand: 'Alt+F', expectedInput: {key: 'KeyF', modifiers: ['alt']}},
+ {os: 'win', command: 'F1', expectedCommand: 'F1', expectedInput: {key: 'F1', modifiers: []}},
+
+ {os: 'win', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'win', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'win', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
+
+ {os: 'mac', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
+ {os: 'mac', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'MacCtrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'mac', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
+
+ {os: 'linux', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'linux', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'linux', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}
+ ];
+
+ const hotkeyUtil = new HotkeyUtil();
+ for (const {command, os, expectedInput, expectedCommand} of data) {
+ hotkeyUtil.os = os;
+ const input = structuredClone(hotkeyUtil.convertCommandToInput(command));
+ expect(input).toStrictEqual(expectedInput);
+ const command2 = hotkeyUtil.convertInputToCommand(input.key, input.modifiers);
+ expect(command2).toStrictEqual(expectedCommand);
+ }
+ });
+}
+
+function testDisplayNames() {
+ test('DisplayNames', () => {
+ const data = [
+ {os: 'win', key: null, modifiers: [], expected: ''},
+ {os: 'win', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'win', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'win', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'win', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'win', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'win', key: null, modifiers: ['alt'], expected: 'Alt'},
+ {os: 'win', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
+ {os: 'win', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
+ {os: 'win', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'win', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'win', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'win', key: null, modifiers: ['meta'], expected: 'Windows'},
+ {os: 'win', key: 'KeyF', modifiers: ['meta'], expected: 'Windows + F'},
+ {os: 'win', key: 'F1', modifiers: ['meta'], expected: 'Windows + F1'},
+ {os: 'win', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'win', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'win', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
+
+ {os: 'mac', key: null, modifiers: [], expected: ''},
+ {os: 'mac', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'mac', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'mac', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'mac', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'mac', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'mac', key: null, modifiers: ['alt'], expected: 'Opt'},
+ {os: 'mac', key: 'KeyF', modifiers: ['alt'], expected: 'Opt + F'},
+ {os: 'mac', key: 'F1', modifiers: ['alt'], expected: 'Opt + F1'},
+ {os: 'mac', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'mac', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'mac', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'mac', key: null, modifiers: ['meta'], expected: 'Cmd'},
+ {os: 'mac', key: 'KeyF', modifiers: ['meta'], expected: 'Cmd + F'},
+ {os: 'mac', key: 'F1', modifiers: ['meta'], expected: 'Cmd + F1'},
+ {os: 'mac', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'mac', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'mac', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
+
+ {os: 'linux', key: null, modifiers: [], expected: ''},
+ {os: 'linux', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'linux', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'linux', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'linux', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'linux', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'linux', key: null, modifiers: ['alt'], expected: 'Alt'},
+ {os: 'linux', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
+ {os: 'linux', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
+ {os: 'linux', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'linux', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'linux', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'linux', key: null, modifiers: ['meta'], expected: 'Super'},
+ {os: 'linux', key: 'KeyF', modifiers: ['meta'], expected: 'Super + F'},
+ {os: 'linux', key: 'F1', modifiers: ['meta'], expected: 'Super + F1'},
+ {os: 'linux', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'linux', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'linux', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
+
+ {os: 'unknown', key: null, modifiers: [], expected: ''},
+ {os: 'unknown', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'unknown', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'unknown', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'unknown', key: null, modifiers: ['alt'], expected: 'Alt'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
+ {os: 'unknown', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'unknown', key: null, modifiers: ['meta'], expected: 'Meta'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['meta'], expected: 'Meta + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['meta'], expected: 'Meta + F1'},
+ {os: 'unknown', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}
+ ];
+
+ const hotkeyUtil = new HotkeyUtil();
+ for (const {os, key, modifiers, expected} of data) {
+ hotkeyUtil.os = os;
+ const displayName = hotkeyUtil.getInputDisplayValue(key, modifiers);
+ expect(displayName).toStrictEqual(expected);
+ }
+ });
+}
+
+function testSortModifiers() {
+ test('SortModifiers', () => {
+ const data = [
+ {modifiers: [], expected: []},
+ {modifiers: ['shift', 'alt', 'ctrl', 'mouse4', 'meta', 'mouse1', 'mouse0'], expected: ['meta', 'ctrl', 'alt', 'shift', 'mouse0', 'mouse1', 'mouse4']}
+ ];
+
+ const hotkeyUtil = new HotkeyUtil();
+ for (const {modifiers, expected} of data) {
+ const modifiers2 = hotkeyUtil.sortModifiers(modifiers);
+ expect(modifiers2).toStrictEqual(modifiers);
+ expect(modifiers2).toStrictEqual(expected);
+ }
+ });
+}
+
+
+function main() {
+ testCommandConversions();
+ testDisplayNames();
+ testSortModifiers();
+}
+
+main();
diff --git a/test/japanese-util.test.js b/test/japanese-util.test.js
new file mode 100644
index 00000000..47da4ccb
--- /dev/null
+++ b/test/japanese-util.test.js
@@ -0,0 +1,905 @@
+/*
+ * 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/>.
+ */
+
+import {expect, test} from 'vitest';
+import {TextSourceMap} from '../ext/js/general/text-source-map.js';
+import {JapaneseUtil} from '../ext/js/language/sandbox/japanese-util.js';
+import * as wanakana from '../ext/lib/wanakana.js';
+
+const jp = new JapaneseUtil(wanakana);
+
+function testIsCodePointKanji() {
+ test('isCodePointKanji', () => {
+ const data = [
+ ['力方', true],
+ ['\u53f1\u{20b9f}', true],
+ ['かたカタ々kata、。?,.?', false],
+ ['逸逸', true]
+ ];
+
+ for (const [characters, expected] of data) {
+ for (const character of characters) {
+ const codePoint = character.codePointAt(0);
+ const actual = jp.isCodePointKanji(codePoint);
+ expect(actual).toStrictEqual(expected); // `isCodePointKanji failed for ${character} (\\u{${codePoint.toString(16)}})`
+ }
+ }
+ });
+}
+
+function testIsCodePointKana() {
+ test('isCodePointKana', () => {
+ const data = [
+ ['かたカタ', true],
+ ['力方々kata、。?,.?', false],
+ ['\u53f1\u{20b9f}', false]
+ ];
+
+ for (const [characters, expected] of data) {
+ for (const character of characters) {
+ const codePoint = character.codePointAt(0);
+ const actual = jp.isCodePointKana(codePoint);
+ expect(actual).toStrictEqual(expected); // `isCodePointKana failed for ${character} (\\u{${codePoint.toString(16)}})`
+ }
+ }
+ });
+}
+
+function testIsCodePointJapanese() {
+ test('isCodePointJapanese', () => {
+ const data = [
+ ['かたカタ力方々、。?', true],
+ ['\u53f1\u{20b9f}', true],
+ ['kata,.?', false],
+ ['逸逸', true]
+ ];
+
+ for (const [characters, expected] of data) {
+ for (const character of characters) {
+ const codePoint = character.codePointAt(0);
+ const actual = jp.isCodePointJapanese(codePoint);
+ expect(actual).toStrictEqual(expected); // `isCodePointJapanese failed for ${character} (\\u{${codePoint.toString(16)}})`
+ }
+ }
+ });
+}
+
+function testIsStringEntirelyKana() {
+ test('isStringEntirelyKana', () => {
+ const data = [
+ ['かたかな', true],
+ ['カタカナ', true],
+ ['ひらがな', true],
+ ['ヒラガナ', true],
+ ['カタカナひらがな', true],
+ ['かたカタ力方々、。?', false],
+ ['\u53f1\u{20b9f}', false],
+ ['kata,.?', false],
+ ['かたカタ力方々、。?invalid', false],
+ ['\u53f1\u{20b9f}invalid', false],
+ ['kata,.?かた', false]
+ ];
+
+ for (const [string, expected] of data) {
+ expect(jp.isStringEntirelyKana(string)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testIsStringPartiallyJapanese() {
+ test('isStringPartiallyJapanese', () => {
+ const data = [
+ ['かたかな', true],
+ ['カタカナ', true],
+ ['ひらがな', true],
+ ['ヒラガナ', true],
+ ['カタカナひらがな', true],
+ ['かたカタ力方々、。?', true],
+ ['\u53f1\u{20b9f}', true],
+ ['kata,.?', false],
+ ['かたカタ力方々、。?invalid', true],
+ ['\u53f1\u{20b9f}invalid', true],
+ ['kata,.?かた', true],
+ ['逸逸', true]
+ ];
+
+ for (const [string, expected] of data) {
+ expect(jp.isStringPartiallyJapanese(string)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testConvertKatakanaToHiragana() {
+ test('convertKatakanaToHiragana', () => {
+ const data = [
+ ['かたかな', 'かたかな'],
+ ['ひらがな', 'ひらがな'],
+ ['カタカナ', 'かたかな'],
+ ['ヒラガナ', 'ひらがな'],
+ ['カタカナかたかな', 'かたかなかたかな'],
+ ['ヒラガナひらがな', 'ひらがなひらがな'],
+ ['chikaraちからチカラ力', 'chikaraちからちから力'],
+ ['katakana', 'katakana'],
+ ['hiragana', 'hiragana'],
+ ['カーナー', 'かあなあ'],
+ ['カーナー', 'かーなー', true]
+ ];
+
+ for (const [string, expected, keepProlongedSoundMarks=false] of data) {
+ expect(jp.convertKatakanaToHiragana(string, keepProlongedSoundMarks)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testConvertHiraganaToKatakana() {
+ test('ConvertHiraganaToKatakana', () => {
+ const data = [
+ ['かたかな', 'カタカナ'],
+ ['ひらがな', 'ヒラガナ'],
+ ['カタカナ', 'カタカナ'],
+ ['ヒラガナ', 'ヒラガナ'],
+ ['カタカナかたかな', 'カタカナカタカナ'],
+ ['ヒラガナひらがな', 'ヒラガナヒラガナ'],
+ ['chikaraちからチカラ力', 'chikaraチカラチカラ力'],
+ ['katakana', 'katakana'],
+ ['hiragana', 'hiragana']
+ ];
+
+ for (const [string, expected] of data) {
+ expect(jp.convertHiraganaToKatakana(string)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testConvertToRomaji() {
+ test('ConvertToRomaji', () => {
+ const data = [
+ ['かたかな', 'katakana'],
+ ['ひらがな', 'hiragana'],
+ ['カタカナ', 'katakana'],
+ ['ヒラガナ', 'hiragana'],
+ ['カタカナかたかな', 'katakanakatakana'],
+ ['ヒラガナひらがな', 'hiraganahiragana'],
+ ['chikaraちからチカラ力', 'chikarachikarachikara力'],
+ ['katakana', 'katakana'],
+ ['hiragana', 'hiragana']
+ ];
+
+ for (const [string, expected] of data) {
+ expect(jp.convertToRomaji(string)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testConvertNumericToFullWidth() {
+ test('ConvertNumericToFullWidth', () => {
+ const data = [
+ ['0123456789', '0123456789'],
+ ['abcdefghij', 'abcdefghij'],
+ ['カタカナ', 'カタカナ'],
+ ['ひらがな', 'ひらがな']
+ ];
+
+ for (const [string, expected] of data) {
+ expect(jp.convertNumericToFullWidth(string)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testConvertHalfWidthKanaToFullWidth() {
+ test('ConvertHalfWidthKanaToFullWidth', () => {
+ const data = [
+ ['0123456789', '0123456789'],
+ ['abcdefghij', 'abcdefghij'],
+ ['カタカナ', 'カタカナ'],
+ ['ひらがな', 'ひらがな'],
+ ['カキ', 'カキ', [1, 1]],
+ ['ガキ', 'ガキ', [2, 1]],
+ ['ニホン', 'ニホン', [1, 1, 1]],
+ ['ニッポン', 'ニッポン', [1, 1, 2, 1]]
+ ];
+
+ for (const [string, expected, expectedSourceMapping] of data) {
+ const sourceMap = new TextSourceMap(string);
+ const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null);
+ const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap);
+ expect(actual1).toStrictEqual(expected);
+ expect(actual2).toStrictEqual(expected);
+ if (typeof expectedSourceMapping !== 'undefined') {
+ expect(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))).toBe(true);
+ }
+ }
+ });
+}
+
+function testConvertAlphabeticToKana() {
+ test('ConvertAlphabeticToKana', () => {
+ const data = [
+ ['0123456789', '0123456789'],
+ ['abcdefghij', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]],
+ ['ABCDEFGHIJ', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], // wanakana.toHiragana converts text to lower case
+ ['カタカナ', 'カタカナ'],
+ ['ひらがな', 'ひらがな'],
+ ['chikara', 'ちから', [3, 2, 2]],
+ ['CHIKARA', 'ちから', [3, 2, 2]]
+ ];
+
+ for (const [string, expected, expectedSourceMapping] of data) {
+ const sourceMap = new TextSourceMap(string);
+ const actual1 = jp.convertAlphabeticToKana(string, null);
+ const actual2 = jp.convertAlphabeticToKana(string, sourceMap);
+ expect(actual1).toStrictEqual(expected);
+ expect(actual2).toStrictEqual(expected);
+ if (typeof expectedSourceMapping !== 'undefined') {
+ expect(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))).toBe(true);
+ }
+ }
+ });
+}
+
+function testDistributeFurigana() {
+ test('DistributeFurigana', () => {
+ const data = [
+ [
+ ['有り難う', 'ありがとう'],
+ [
+ {text: '有', reading: 'あ'},
+ {text: 'り', reading: ''},
+ {text: '難', reading: 'がと'},
+ {text: 'う', reading: ''}
+ ]
+ ],
+ [
+ ['方々', 'かたがた'],
+ [
+ {text: '方々', reading: 'かたがた'}
+ ]
+ ],
+ [
+ ['お祝い', 'おいわい'],
+ [
+ {text: 'お', reading: ''},
+ {text: '祝', reading: 'いわ'},
+ {text: 'い', reading: ''}
+ ]
+ ],
+ [
+ ['美味しい', 'おいしい'],
+ [
+ {text: '美味', reading: 'おい'},
+ {text: 'しい', reading: ''}
+ ]
+ ],
+ [
+ ['食べ物', 'たべもの'],
+ [
+ {text: '食', reading: 'た'},
+ {text: 'べ', reading: ''},
+ {text: '物', reading: 'もの'}
+ ]
+ ],
+ [
+ ['試し切り', 'ためしぎり'],
+ [
+ {text: '試', reading: 'ため'},
+ {text: 'し', reading: ''},
+ {text: '切', reading: 'ぎ'},
+ {text: 'り', reading: ''}
+ ]
+ ],
+ // Ambiguous
+ [
+ ['飼い犬', 'かいいぬ'],
+ [
+ {text: '飼い犬', reading: 'かいいぬ'}
+ ]
+ ],
+ [
+ ['長い間', 'ながいあいだ'],
+ [
+ {text: '長い間', reading: 'ながいあいだ'}
+ ]
+ ],
+ // Same/empty reading
+ [
+ ['飼い犬', ''],
+ [
+ {text: '飼い犬', reading: ''}
+ ]
+ ],
+ [
+ ['かいいぬ', 'かいいぬ'],
+ [
+ {text: 'かいいぬ', reading: ''}
+ ]
+ ],
+ [
+ ['かいぬ', 'かいぬ'],
+ [
+ {text: 'かいぬ', reading: ''}
+ ]
+ ],
+ // Misc
+ [
+ ['月', 'か'],
+ [
+ {text: '月', reading: 'か'}
+ ]
+ ],
+ [
+ ['月', 'カ'],
+ [
+ {text: '月', reading: 'カ'}
+ ]
+ ],
+ // Mismatched kana readings
+ [
+ ['有り難う', 'アリガトウ'],
+ [
+ {text: '有', reading: 'ア'},
+ {text: 'り', reading: 'リ'},
+ {text: '難', reading: 'ガト'},
+ {text: 'う', reading: 'ウ'}
+ ]
+ ],
+ [
+ ['ありがとう', 'アリガトウ'],
+ [
+ {text: 'ありがとう', reading: 'アリガトウ'}
+ ]
+ ],
+ // Mismatched kana readings (real examples)
+ [
+ ['カ月', 'かげつ'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '月', reading: 'げつ'}
+ ]
+ ],
+ [
+ ['序ノ口', 'じょのくち'],
+ [
+ {text: '序', reading: 'じょ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '口', reading: 'くち'}
+ ]
+ ],
+ [
+ ['スズメの涙', 'すずめのなみだ'],
+ [
+ {text: 'スズメ', reading: 'すずめ'},
+ {text: 'の', reading: ''},
+ {text: '涙', reading: 'なみだ'}
+ ]
+ ],
+ [
+ ['二カ所', 'にかしょ'],
+ [
+ {text: '二', reading: 'に'},
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['八ツ橋', 'やつはし'],
+ [
+ {text: '八', reading: 'や'},
+ {text: 'ツ', reading: 'つ'},
+ {text: '橋', reading: 'はし'}
+ ]
+ ],
+ [
+ ['八ツ橋', 'やつはし'],
+ [
+ {text: '八', reading: 'や'},
+ {text: 'ツ', reading: 'つ'},
+ {text: '橋', reading: 'はし'}
+ ]
+ ],
+ [
+ ['一カ月', 'いっかげつ'],
+ [
+ {text: '一', reading: 'いっ'},
+ {text: 'カ', reading: 'か'},
+ {text: '月', reading: 'げつ'}
+ ]
+ ],
+ [
+ ['一カ所', 'いっかしょ'],
+ [
+ {text: '一', reading: 'いっ'},
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['カ所', 'かしょ'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['数カ月', 'すうかげつ'],
+ [
+ {text: '数', reading: 'すう'},
+ {text: 'カ', reading: 'か'},
+ {text: '月', reading: 'げつ'}
+ ]
+ ],
+ [
+ ['くノ一', 'くのいち'],
+ [
+ {text: 'く', reading: ''},
+ {text: 'ノ', reading: 'の'},
+ {text: '一', reading: 'いち'}
+ ]
+ ],
+ [
+ ['くノ一', 'くのいち'],
+ [
+ {text: 'く', reading: ''},
+ {text: 'ノ', reading: 'の'},
+ {text: '一', reading: 'いち'}
+ ]
+ ],
+ [
+ ['数カ国', 'すうかこく'],
+ [
+ {text: '数', reading: 'すう'},
+ {text: 'カ', reading: 'か'},
+ {text: '国', reading: 'こく'}
+ ]
+ ],
+ [
+ ['数カ所', 'すうかしょ'],
+ [
+ {text: '数', reading: 'すう'},
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['壇ノ浦の戦い', 'だんのうらのたたかい'],
+ [
+ {text: '壇', reading: 'だん'},
+ {text: 'ノ', reading: 'の'},
+ {text: '浦', reading: 'うら'},
+ {text: 'の', reading: ''},
+ {text: '戦', reading: 'たたか'},
+ {text: 'い', reading: ''}
+ ]
+ ],
+ [
+ ['壇ノ浦の戦', 'だんのうらのたたかい'],
+ [
+ {text: '壇', reading: 'だん'},
+ {text: 'ノ', reading: 'の'},
+ {text: '浦', reading: 'うら'},
+ {text: 'の', reading: ''},
+ {text: '戦', reading: 'たたかい'}
+ ]
+ ],
+ [
+ ['序ノ口格', 'じょのくちかく'],
+ [
+ {text: '序', reading: 'じょ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '口格', reading: 'くちかく'}
+ ]
+ ],
+ [
+ ['二カ国語', 'にかこくご'],
+ [
+ {text: '二', reading: 'に'},
+ {text: 'カ', reading: 'か'},
+ {text: '国語', reading: 'こくご'}
+ ]
+ ],
+ [
+ ['カ国', 'かこく'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '国', reading: 'こく'}
+ ]
+ ],
+ [
+ ['カ国語', 'かこくご'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '国語', reading: 'こくご'}
+ ]
+ ],
+ [
+ ['壇ノ浦の合戦', 'だんのうらのかっせん'],
+ [
+ {text: '壇', reading: 'だん'},
+ {text: 'ノ', reading: 'の'},
+ {text: '浦', reading: 'うら'},
+ {text: 'の', reading: ''},
+ {text: '合戦', reading: 'かっせん'}
+ ]
+ ],
+ [
+ ['一タ偏', 'いちたへん'],
+ [
+ {text: '一', reading: 'いち'},
+ {text: 'タ', reading: 'た'},
+ {text: '偏', reading: 'へん'}
+ ]
+ ],
+ [
+ ['ル又', 'るまた'],
+ [
+ {text: 'ル', reading: 'る'},
+ {text: '又', reading: 'また'}
+ ]
+ ],
+ [
+ ['ノ木偏', 'のぎへん'],
+ [
+ {text: 'ノ', reading: 'の'},
+ {text: '木偏', reading: 'ぎへん'}
+ ]
+ ],
+ [
+ ['一ノ貝', 'いちのかい'],
+ [
+ {text: '一', reading: 'いち'},
+ {text: 'ノ', reading: 'の'},
+ {text: '貝', reading: 'かい'}
+ ]
+ ],
+ [
+ ['虎ノ門事件', 'とらのもんじけん'],
+ [
+ {text: '虎', reading: 'とら'},
+ {text: 'ノ', reading: 'の'},
+ {text: '門事件', reading: 'もんじけん'}
+ ]
+ ],
+ [
+ ['教育ニ関スル勅語', 'きょういくにかんするちょくご'],
+ [
+ {text: '教育', reading: 'きょういく'},
+ {text: 'ニ', reading: 'に'},
+ {text: '関', reading: 'かん'},
+ {text: 'スル', reading: 'する'},
+ {text: '勅語', reading: 'ちょくご'}
+ ]
+ ],
+ [
+ ['二カ年', 'にかねん'],
+ [
+ {text: '二', reading: 'に'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['三カ年', 'さんかねん'],
+ [
+ {text: '三', reading: 'さん'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['四カ年', 'よんかねん'],
+ [
+ {text: '四', reading: 'よん'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['五カ年', 'ごかねん'],
+ [
+ {text: '五', reading: 'ご'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['六カ年', 'ろっかねん'],
+ [
+ {text: '六', reading: 'ろっ'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['七カ年', 'ななかねん'],
+ [
+ {text: '七', reading: 'なな'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['八カ年', 'はちかねん'],
+ [
+ {text: '八', reading: 'はち'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['九カ年', 'きゅうかねん'],
+ [
+ {text: '九', reading: 'きゅう'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['十カ年', 'じゅうかねん'],
+ [
+ {text: '十', reading: 'じゅう'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['鏡ノ間', 'かがみのま'],
+ [
+ {text: '鏡', reading: 'かがみ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '間', reading: 'ま'}
+ ]
+ ],
+ [
+ ['鏡ノ間', 'かがみのま'],
+ [
+ {text: '鏡', reading: 'かがみ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '間', reading: 'ま'}
+ ]
+ ],
+ [
+ ['ページ違反', 'ぺーじいはん'],
+ [
+ {text: 'ペ', reading: 'ぺ'},
+ {text: 'ー', reading: ''},
+ {text: 'ジ', reading: 'じ'},
+ {text: '違反', reading: 'いはん'}
+ ]
+ ],
+ // Mismatched kana
+ [
+ ['サボる', 'サボル'],
+ [
+ {text: 'サボ', reading: ''},
+ {text: 'る', reading: 'ル'}
+ ]
+ ],
+ // Reading starts with term, but has remainder characters
+ [
+ ['シック', 'シック・ビルしょうこうぐん'],
+ [
+ {text: 'シック', reading: 'シック・ビルしょうこうぐん'}
+ ]
+ ],
+ // Kanji distribution tests
+ [
+ ['逸らす', 'そらす'],
+ [
+ {text: '逸', reading: 'そ'},
+ {text: 'らす', reading: ''}
+ ]
+ ],
+ [
+ ['逸らす', 'そらす'],
+ [
+ {text: '逸', reading: 'そ'},
+ {text: 'らす', reading: ''}
+ ]
+ ]
+ ];
+
+ for (const [[term, reading], expected] of data) {
+ const actual = jp.distributeFurigana(term, reading);
+ expect(actual).toStrictEqual(expected);
+ }
+ });
+}
+
+function testDistributeFuriganaInflected() {
+ test('DistributeFuriganaInflected', () => {
+ const data = [
+ [
+ ['美味しい', 'おいしい', '美味しかた'],
+ [
+ {text: '美味', reading: 'おい'},
+ {text: 'しかた', reading: ''}
+ ]
+ ],
+ [
+ ['食べる', 'たべる', '食べた'],
+ [
+ {text: '食', reading: 'た'},
+ {text: 'べた', reading: ''}
+ ]
+ ],
+ [
+ ['迄に', 'までに', 'までに'],
+ [
+ {text: 'までに', reading: ''}
+ ]
+ ],
+ [
+ ['行う', 'おこなう', 'おこなわなかった'],
+ [
+ {text: 'おこなわなかった', reading: ''}
+ ]
+ ],
+ [
+ ['いい', 'いい', 'イイ'],
+ [
+ {text: 'イイ', reading: ''}
+ ]
+ ],
+ [
+ ['否か', 'いなか', '否カ'],
+ [
+ {text: '否', reading: 'いな'},
+ {text: 'カ', reading: 'か'}
+ ]
+ ]
+ ];
+
+ for (const [[term, reading, source], expected] of data) {
+ const actual = jp.distributeFuriganaInflected(term, reading, source);
+ expect(actual).toStrictEqual(expected);
+ }
+ });
+}
+
+function testCollapseEmphaticSequences() {
+ test('CollapseEmphaticSequences', () => {
+ const data = [
+ [['かこい', false], ['かこい', [1, 1, 1]]],
+ [['かこい', true], ['かこい', [1, 1, 1]]],
+ [['かっこい', false], ['かっこい', [1, 1, 1, 1]]],
+ [['かっこい', true], ['かこい', [2, 1, 1]]],
+ [['かっっこい', false], ['かっこい', [1, 2, 1, 1]]],
+ [['かっっこい', true], ['かこい', [3, 1, 1]]],
+ [['かっっっこい', false], ['かっこい', [1, 3, 1, 1]]],
+ [['かっっっこい', true], ['かこい', [4, 1, 1]]],
+
+ [['こい', false], ['こい', [1, 1]]],
+ [['こい', true], ['こい', [1, 1]]],
+ [['っこい', false], ['っこい', [1, 1, 1]]],
+ [['っこい', true], ['こい', [2, 1]]],
+ [['っっこい', false], ['っこい', [2, 1, 1]]],
+ [['っっこい', true], ['こい', [3, 1]]],
+ [['っっっこい', false], ['っこい', [3, 1, 1]]],
+ [['っっっこい', true], ['こい', [4, 1]]],
+
+ [['すごい', false], ['すごい', [1, 1, 1]]],
+ [['すごい', true], ['すごい', [1, 1, 1]]],
+ [['すごーい', false], ['すごーい', [1, 1, 1, 1]]],
+ [['すごーい', true], ['すごい', [1, 2, 1]]],
+ [['すごーーい', false], ['すごーい', [1, 1, 2, 1]]],
+ [['すごーーい', true], ['すごい', [1, 3, 1]]],
+ [['すっごーい', false], ['すっごーい', [1, 1, 1, 1, 1]]],
+ [['すっごーい', true], ['すごい', [2, 2, 1]]],
+ [['すっっごーーい', false], ['すっごーい', [1, 2, 1, 2, 1]]],
+ [['すっっごーーい', true], ['すごい', [3, 3, 1]]],
+
+ [['', false], ['', []]],
+ [['', true], ['', []]],
+ [['っ', false], ['っ', [1]]],
+ [['っ', true], ['', [1]]],
+ [['っっ', false], ['っ', [2]]],
+ [['っっ', true], ['', [2]]],
+ [['っっっ', false], ['っ', [3]]],
+ [['っっっ', true], ['', [3]]]
+ ];
+
+ for (const [[text, fullCollapse], [expected, expectedSourceMapping]] of data) {
+ const sourceMap = new TextSourceMap(text);
+ const actual1 = jp.collapseEmphaticSequences(text, fullCollapse, null);
+ const actual2 = jp.collapseEmphaticSequences(text, fullCollapse, sourceMap);
+ expect(actual1).toStrictEqual(expected);
+ expect(actual2).toStrictEqual(expected);
+ if (typeof expectedSourceMapping !== 'undefined') {
+ expect(sourceMap.equals(new TextSourceMap(text, expectedSourceMapping))).toBe(true);
+ }
+ }
+ });
+}
+
+function testIsMoraPitchHigh() {
+ test('IsMoraPitchHigh', () => {
+ const data = [
+ [[0, 0], false],
+ [[1, 0], true],
+ [[2, 0], true],
+ [[3, 0], true],
+
+ [[0, 1], true],
+ [[1, 1], false],
+ [[2, 1], false],
+ [[3, 1], false],
+
+ [[0, 2], false],
+ [[1, 2], true],
+ [[2, 2], false],
+ [[3, 2], false],
+
+ [[0, 3], false],
+ [[1, 3], true],
+ [[2, 3], true],
+ [[3, 3], false],
+
+ [[0, 4], false],
+ [[1, 4], true],
+ [[2, 4], true],
+ [[3, 4], true]
+ ];
+
+ for (const [[moraIndex, pitchAccentDownstepPosition], expected] of data) {
+ const actual = jp.isMoraPitchHigh(moraIndex, pitchAccentDownstepPosition);
+ expect(actual).toStrictEqual(expected);
+ }
+ });
+}
+
+function testGetKanaMorae() {
+ test('GetKanaMorae', () => {
+ const data = [
+ ['かこ', ['か', 'こ']],
+ ['かっこ', ['か', 'っ', 'こ']],
+ ['カコ', ['カ', 'コ']],
+ ['カッコ', ['カ', 'ッ', 'コ']],
+ ['コート', ['コ', 'ー', 'ト']],
+ ['ちゃんと', ['ちゃ', 'ん', 'と']],
+ ['とうきょう', ['と', 'う', 'きょ', 'う']],
+ ['ぎゅう', ['ぎゅ', 'う']],
+ ['ディスコ', ['ディ', 'ス', 'コ']]
+ ];
+
+ for (const [text, expected] of data) {
+ const actual = jp.getKanaMorae(text);
+ expect(actual).toStrictEqual(expected);
+ }
+ });
+}
+
+
+function main() {
+ testIsCodePointKanji();
+ testIsCodePointKana();
+ testIsCodePointJapanese();
+ testIsStringEntirelyKana();
+ testIsStringPartiallyJapanese();
+ testConvertKatakanaToHiragana();
+ testConvertHiraganaToKatakana();
+ testConvertToRomaji();
+ testConvertNumericToFullWidth();
+ testConvertHalfWidthKanaToFullWidth();
+ testConvertAlphabeticToKana();
+ testDistributeFurigana();
+ testDistributeFuriganaInflected();
+ testCollapseEmphaticSequences();
+ testIsMoraPitchHigh();
+ testGetKanaMorae();
+}
+
+main();
diff --git a/test/test-jsdom.js b/test/jsdom.test.js
index 5078c240..c53f374e 100644
--- a/test/test-jsdom.js
+++ b/test/jsdom.test.js
@@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-const assert = require('assert');
-const {testMain} = require('../dev/util');
+import {JSDOM} from 'jsdom';
+import {expect, test} from 'vitest';
/**
* This function tests the following bug:
@@ -25,17 +25,18 @@ const {testMain} = require('../dev/util');
* - https://github.com/dperini/nwsapi/issues/48
*/
function testJSDOMSelectorBug() {
+ test('JSDOMSelectorBug', () => {
// nwsapi is used by JSDOM
- const {JSDOM} = require('jsdom');
- const dom = new JSDOM();
- const {document} = dom.window;
- const div = document.createElement('div');
- div.innerHTML = '<div class="b"><div class="c"></div></div>';
- const c = div.querySelector('.c');
- assert.doesNotThrow(() => { c.matches('.a:nth-last-of-type(1) .b .c'); });
+ const dom = new JSDOM();
+ const {document} = dom.window;
+ const div = document.createElement('div');
+ div.innerHTML = '<div class="b"><div class="c"></div></div>';
+ const c = div.querySelector('.c');
+ expect(() => c.matches('.a:nth-last-of-type(1) .b .c')).not.toThrow();
+ });
}
-function testJSDOM() {
+export function testJSDOM() {
testJSDOMSelectorBug();
}
@@ -43,8 +44,4 @@ function main() {
testJSDOM();
}
-module.exports = {
- testJSDOM
-};
-
-if (require.main === module) { testMain(main); }
+main();
diff --git a/test/json-schema.test.js b/test/json-schema.test.js
new file mode 100644
index 00000000..5370e8da
--- /dev/null
+++ b/test/json-schema.test.js
@@ -0,0 +1,1009 @@
+/*
+ * 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/>.
+ */
+
+import {expect, test} from 'vitest';
+import {JsonSchema} from '../ext/js/data/json-schema.js';
+
+function schemaValidate(schema, value) {
+ return new JsonSchema(schema).isValid(value);
+}
+
+function getValidValueOrDefault(schema, value) {
+ return new JsonSchema(schema).getValidValueOrDefault(value);
+}
+
+function createProxy(schema, value) {
+ return new JsonSchema(schema).createProxy(value);
+}
+
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+
+function testValidate1() {
+ test('Validate1', () => {
+ const schema = {
+ allOf: [
+ {
+ type: 'number'
+ },
+ {
+ anyOf: [
+ {minimum: 10, maximum: 100},
+ {minimum: -100, maximum: -10}
+ ]
+ },
+ {
+ oneOf: [
+ {multipleOf: 3},
+ {multipleOf: 5}
+ ]
+ },
+ {
+ not: [
+ {multipleOf: 20}
+ ]
+ }
+ ]
+ };
+
+ const jsValidate = (value) => {
+ return (
+ typeof value === 'number' &&
+ (
+ (value >= 10 && value <= 100) ||
+ (value >= -100 && value <= -10)
+ ) &&
+ (
+ (
+ (value % 3) === 0 ||
+ (value % 5) === 0
+ ) &&
+ (value % 15) !== 0
+ ) &&
+ (value % 20) !== 0
+ );
+ };
+
+ for (let i = -111; i <= 111; i++) {
+ const actual = schemaValidate(schema, i);
+ const expected = jsValidate(i);
+ expect(actual).toStrictEqual(expected);
+ }
+ });
+}
+
+function testValidate2() {
+ test('Validate2', () => {
+ const data = [
+ // String tests
+ {
+ schema: {
+ type: 'string'
+ },
+ inputs: [
+ {expected: false, value: null},
+ {expected: false, value: void 0},
+ {expected: false, value: 0},
+ {expected: false, value: {}},
+ {expected: false, value: []},
+ {expected: true, value: ''}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ minLength: 2
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: false, value: '1'},
+ {expected: true, value: '12'},
+ {expected: true, value: '123'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ maxLength: 2
+ },
+ inputs: [
+ {expected: true, value: ''},
+ {expected: true, value: '1'},
+ {expected: true, value: '12'},
+ {expected: false, value: '123'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: 'test'
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: true, value: 'test'},
+ {expected: false, value: 'TEST'},
+ {expected: true, value: 'ABCtestDEF'},
+ {expected: false, value: 'ABCTESTDEF'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '^test$'
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: true, value: 'test'},
+ {expected: false, value: 'TEST'},
+ {expected: false, value: 'ABCtestDEF'},
+ {expected: false, value: 'ABCTESTDEF'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '^test$',
+ patternFlags: 'i'
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: true, value: 'test'},
+ {expected: true, value: 'TEST'},
+ {expected: false, value: 'ABCtestDEF'},
+ {expected: false, value: 'ABCTESTDEF'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '*'
+ },
+ inputs: [
+ {expected: false, value: ''}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '.',
+ patternFlags: '?'
+ },
+ inputs: [
+ {expected: false, value: ''}
+ ]
+ },
+
+ // Const tests
+ {
+ schema: {
+ const: 32
+ },
+ inputs: [
+ {expected: true, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: '32'
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: true, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: null
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: true, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: {a: 'b'}
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: [1, 2, 3]
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+
+ // Array contains tests
+ {
+ schema: {
+ type: 'array',
+ contains: {const: 32}
+ },
+ inputs: [
+ {expected: false, value: []},
+ {expected: true, value: [32]},
+ {expected: true, value: [1, 32]},
+ {expected: true, value: [1, 32, 1]},
+ {expected: false, value: [33]},
+ {expected: false, value: [1, 33]},
+ {expected: false, value: [1, 33, 1]}
+ ]
+ },
+
+ // Number limits tests
+ {
+ schema: {
+ type: 'number',
+ minimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: true, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'number',
+ exclusiveMinimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: false, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'number',
+ maximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: true, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'number',
+ exclusiveMaximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: false, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+
+ // Integer limits tests
+ {
+ schema: {
+ type: 'integer',
+ minimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: true, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ exclusiveMinimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: false, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ maximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: true, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ exclusiveMaximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: false, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ multipleOf: 2
+ },
+ inputs: [
+ {expected: true, value: -2},
+ {expected: false, value: -1},
+ {expected: true, value: 0},
+ {expected: false, value: 1},
+ {expected: true, value: 2}
+ ]
+ },
+
+ // Numeric type tests
+ {
+ schema: {
+ type: 'number'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: true, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: false, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+
+ // Reference tests
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'number'
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: true, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'integer'
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: false, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ test: {
+ $ref: '#/definitions/example'
+ }
+ }
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ {expected: false, value: 0},
+ {expected: false, value: 0.5},
+ {expected: false, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: true, value: {}},
+ {expected: false, value: {test: 0}},
+ {expected: false, value: {test: 0.5}},
+ {expected: false, value: {test: 1}},
+ {expected: false, value: {test: '0'}},
+ {expected: false, value: {test: null}},
+ {expected: false, value: {test: []}},
+ {expected: true, value: {test: {}}},
+ {expected: true, value: {test: {test: {}}}},
+ {expected: true, value: {test: {test: {test: {}}}}}
+ ]
+ }
+ ];
+
+ for (const {schema, inputs} of data) {
+ for (const {expected, value} of inputs) {
+ const actual = schemaValidate(schema, value);
+ expect(actual).toStrictEqual(expected);
+ }
+ }
+ });
+}
+
+
+function testGetValidValueOrDefault1() {
+ test('GetValidValueOrDefault1', () => {
+ const data = [
+ // Test value defaulting on objects with additionalProperties=false
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: false
+ },
+ inputs: [
+ [
+ void 0,
+ {test: 'default'}
+ ],
+ [
+ null,
+ {test: 'default'}
+ ],
+ [
+ 0,
+ {test: 'default'}
+ ],
+ [
+ '',
+ {test: 'default'}
+ ],
+ [
+ [],
+ {test: 'default'}
+ ],
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value'}
+ ]
+ ]
+ },
+
+ // Test value defaulting on objects with additionalProperties=true
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: true
+ },
+ inputs: [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 'value2'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 'value2'}
+ ]
+ ]
+ },
+
+ // Test value defaulting on objects with additionalProperties={schema}
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: {
+ type: 'number',
+ default: 10
+ }
+ },
+ inputs: [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 10}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 10}
+ ],
+ [
+ {test2: 2},
+ {test: 'default', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2},
+ {test: 'value', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2, test3: null},
+ {test: 'value', test2: 2, test3: 10}
+ ],
+ [
+ {test: 'value', test2: 2, test3: void 0},
+ {test: 'value', test2: 2, test3: 10}
+ ]
+ ]
+ },
+
+ // Test value defaulting where hasOwnProperty is false
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ },
+ inputs: [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ Object.create({test: 'value'}),
+ {test: 'default'}
+ ]
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['toString'],
+ properties: {
+ toString: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ },
+ inputs: [
+ [
+ {},
+ {toString: 'default'}
+ ],
+ [
+ {toString: 'value'},
+ {toString: 'value'}
+ ],
+ [
+ Object.create({toString: 'value'}),
+ {toString: 'default'}
+ ]
+ ]
+ },
+
+ // Test enum
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'value1',
+ enum: ['value1', 'value2', 'value3']
+ }
+ }
+ },
+ inputs: [
+ [
+ {test: 'value1'},
+ {test: 'value1'}
+ ],
+ [
+ {test: 'value2'},
+ {test: 'value2'}
+ ],
+ [
+ {test: 'value3'},
+ {test: 'value3'}
+ ],
+ [
+ {test: 'value4'},
+ {test: 'value1'}
+ ]
+ ]
+ },
+
+ // Test valid vs invalid default
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'integer',
+ default: 2,
+ minimum: 1
+ }
+ }
+ },
+ inputs: [
+ [
+ {test: -1},
+ {test: 2}
+ ]
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'integer',
+ default: 1,
+ minimum: 2
+ }
+ }
+ },
+ inputs: [
+ [
+ {test: -1},
+ {test: -1}
+ ]
+ ]
+ },
+
+ // Test references
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'number',
+ default: 0
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ [
+ 1,
+ 1
+ ],
+ [
+ null,
+ 0
+ ],
+ [
+ 'test',
+ 0
+ ],
+ [
+ {test: 'value'},
+ 0
+ ]
+ ]
+ },
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ test: {
+ $ref: '#/definitions/example'
+ }
+ }
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ [
+ 1,
+ {}
+ ],
+ [
+ null,
+ {}
+ ],
+ [
+ 'test',
+ {}
+ ],
+ [
+ {},
+ {}
+ ],
+ [
+ {test: {}},
+ {test: {}}
+ ],
+ [
+ {test: 'value'},
+ {test: {}}
+ ],
+ [
+ {test: {test: {}}},
+ {test: {test: {}}}
+ ]
+ ]
+ }
+ ];
+
+ for (const {schema, inputs} of data) {
+ for (const [value, expected] of inputs) {
+ const actual = getValidValueOrDefault(schema, value);
+ expect(actual).toStrictEqual(expected);
+ }
+ }
+ });
+}
+
+
+function testProxy1() {
+ test('Proxy1', () => {
+ const data = [
+ // Object tests
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ additionalProperties: false,
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ },
+ tests: [
+ {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
+ {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
+ {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
+ {error: true, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
+ {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ additionalProperties: true,
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ },
+ tests: [
+ {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
+ {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
+ {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
+ {error: false, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
+ {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['test1'],
+ additionalProperties: false,
+ properties: {
+ test1: {
+ type: 'object',
+ required: ['test2'],
+ additionalProperties: false,
+ properties: {
+ test2: {
+ type: 'object',
+ required: ['test3'],
+ additionalProperties: false,
+ properties: {
+ test3: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ tests: [
+ {error: false, action: (value) => { value.test1.test2.test3 = 'string'; }},
+ {error: true, action: (value) => { value.test1.test2.test3 = null; }},
+ {error: true, action: (value) => { delete value.test1.test2.test3; }},
+ {error: true, action: (value) => { value.test1.test2 = null; }},
+ {error: true, action: (value) => { value.test1 = null; }},
+ {error: true, action: (value) => { value.test4 = 'string'; }},
+ {error: false, action: (value) => { delete value.test4; }}
+ ]
+ },
+
+ // Array tests
+ {
+ schema: {
+ type: 'array',
+ items: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ tests: [
+ {error: false, value: ['default'], action: (value) => { value[0] = 'string'; }},
+ {error: true, value: ['default'], action: (value) => { value[0] = null; }},
+ {error: false, value: ['default'], action: (value) => { delete value[0]; }},
+ {error: false, value: ['default'], action: (value) => { value[1] = 'string'; }},
+ {error: false, value: ['default'], action: (value) => {
+ value[1] = 'string';
+ if (value.length !== 2) { throw new Error(`Invalid length; expected=2; actual=${value.length}`); }
+ if (typeof value.push !== 'function') { throw new Error(`Invalid push; expected=function; actual=${typeof value.push}`); }
+ }}
+ ]
+ },
+
+ // Reference tests
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ test: {
+ $ref: '#/definitions/example'
+ }
+ }
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ tests: [
+ {error: false, value: {}, action: (value) => { value.test = {}; }},
+ {error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }},
+ {error: false, value: {}, action: (value) => { value.test = {test: {}}; }},
+ {error: true, value: {}, action: (value) => { value.test = null; }},
+ {error: true, value: {}, action: (value) => { value.test = 'string'; }},
+ {error: true, value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }},
+ {error: true, value: {}, action: (value) => { value.test = {test: 'string'}; }}
+ ]
+ }
+ ];
+
+ for (const {schema, tests} of data) {
+ for (let {error, value, action} of tests) {
+ if (typeof value === 'undefined') { value = getValidValueOrDefault(schema, void 0); }
+ value = clone(value);
+ expect(schemaValidate(schema, value)).toBe(true);
+ const valueProxy = createProxy(schema, value);
+ if (error) {
+ expect(() => action(valueProxy)).toThrow();
+ } else {
+ expect(() => action(valueProxy)).not.toThrow();
+ }
+ }
+ }
+ });
+}
+
+
+function main() {
+ testValidate1();
+ testValidate2();
+ testGetValidValueOrDefault1();
+ testProxy1();
+}
+
+
+main();
diff --git a/test/object-property-accessor.test.js b/test/object-property-accessor.test.js
new file mode 100644
index 00000000..a8730093
--- /dev/null
+++ b/test/object-property-accessor.test.js
@@ -0,0 +1,438 @@
+/*
+ * 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/>.
+ */
+
+import {expect, test} from 'vitest';
+import {ObjectPropertyAccessor} from '../ext/js/general/object-property-accessor.js';
+
+function createTestObject() {
+ return {
+ 0: null,
+ value1: {
+ value2: {},
+ value3: [],
+ value4: null
+ },
+ value5: [
+ {},
+ [],
+ null
+ ]
+ };
+}
+
+
+function testGet1() {
+ test('Get1', () => {
+ const data = [
+ [[], (object) => object],
+ [['0'], (object) => object['0']],
+ [['value1'], (object) => object.value1],
+ [['value1', 'value2'], (object) => object.value1.value2],
+ [['value1', 'value3'], (object) => object.value1.value3],
+ [['value1', 'value4'], (object) => object.value1.value4],
+ [['value5'], (object) => object.value5],
+ [['value5', 0], (object) => object.value5[0]],
+ [['value5', 1], (object) => object.value5[1]],
+ [['value5', 2], (object) => object.value5[2]]
+ ];
+
+ for (const [pathArray, getExpected] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+ const expected = getExpected(object);
+
+ expect(accessor.get(pathArray)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testGet2() {
+ test('Get2', () => {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ const data = [
+ [[0], 'Invalid path: [0]'],
+ [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
+ [['invalid'], 'Invalid path: invalid'],
+ [['value1', 'invalid'], 'Invalid path: value1.invalid'],
+ [['value1', 'value2', 'invalid'], 'Invalid path: value1.value2.invalid'],
+ [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
+ [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
+ [['value1', 'value3', 0], 'Invalid path: value1.value3[0]'],
+ [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
+ [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
+ [['value5', 'length'], 'Invalid path: value5.length'],
+ [['value5', 0, 'invalid'], 'Invalid path: value5[0].invalid'],
+ [['value5', 0, 0], 'Invalid path: value5[0][0]'],
+ [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
+ [['value5', 1, 0], 'Invalid path: value5[1][0]'],
+ [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
+ [['value5', 2, 0], 'Invalid path: value5[2][0]'],
+ [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
+ [['value5', 2.5], 'Invalid index']
+ ];
+
+ for (const [pathArray, message] of data) {
+ expect(() => accessor.get(pathArray)).toThrow(message);
+ }
+ });
+}
+
+
+function testSet1() {
+ test('Set1', () => {
+ const testValue = {};
+ const data = [
+ ['0'],
+ ['value1', 'value2'],
+ ['value1', 'value3'],
+ ['value1', 'value4'],
+ ['value1'],
+ ['value5', 0],
+ ['value5', 1],
+ ['value5', 2],
+ ['value5']
+ ];
+
+ for (const pathArray of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ accessor.set(pathArray, testValue);
+ expect(accessor.get(pathArray)).toStrictEqual(testValue);
+ }
+ });
+}
+
+function testSet2() {
+ test('Set2', () => {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ const testValue = {};
+ const data = [
+ [[], 'Invalid path'],
+ [[0], 'Invalid path: [0]'],
+ [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
+ [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
+ [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
+ [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
+ [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
+ [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
+ [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
+ [['value5', 2, 0], 'Invalid path: value5[2][0]'],
+ [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
+ [['value5', 2.5], 'Invalid index']
+ ];
+
+ for (const [pathArray, message] of data) {
+ expect(() => accessor.set(pathArray, testValue)).toThrow(message);
+ }
+ });
+}
+
+
+function testDelete1() {
+ test('Delete1', () => {
+ const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property);
+
+ const data = [
+ [['0'], (object) => !hasOwn(object, '0')],
+ [['value1', 'value2'], (object) => !hasOwn(object.value1, 'value2')],
+ [['value1', 'value3'], (object) => !hasOwn(object.value1, 'value3')],
+ [['value1', 'value4'], (object) => !hasOwn(object.value1, 'value4')],
+ [['value1'], (object) => !hasOwn(object, 'value1')],
+ [['value5'], (object) => !hasOwn(object, 'value5')]
+ ];
+
+ for (const [pathArray, validate] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ accessor.delete(pathArray);
+ expect(validate(object)).toBe(true);
+ }
+ });
+}
+
+function testDelete2() {
+ test('Delete2', () => {
+ const data = [
+ [[], 'Invalid path'],
+ [[0], 'Invalid path: [0]'],
+ [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
+ [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
+ [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
+ [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
+ [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
+ [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
+ [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
+ [['value5', 2, 0], 'Invalid path: value5[2][0]'],
+ [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
+ [['value5', 2.5], 'Invalid index'],
+ [['value5', 0], 'Invalid type'],
+ [['value5', 1], 'Invalid type'],
+ [['value5', 2], 'Invalid type']
+ ];
+
+ for (const [pathArray, message] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ expect(() => accessor.delete(pathArray)).toThrow(message);
+ }
+ });
+}
+
+
+function testSwap1() {
+ test('Swap1', () => {
+ const data = [
+ [['0'], true],
+ [['value1', 'value2'], true],
+ [['value1', 'value3'], true],
+ [['value1', 'value4'], true],
+ [['value1'], false],
+ [['value5', 0], true],
+ [['value5', 1], true],
+ [['value5', 2], true],
+ [['value5'], false]
+ ];
+
+ for (const [pathArray1, compareValues1] of data) {
+ for (const [pathArray2, compareValues2] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ const value1a = accessor.get(pathArray1);
+ const value2a = accessor.get(pathArray2);
+
+ accessor.swap(pathArray1, pathArray2);
+
+ if (!compareValues1 || !compareValues2) { continue; }
+
+ const value1b = accessor.get(pathArray1);
+ const value2b = accessor.get(pathArray2);
+
+ expect(value1a).toStrictEqual(value2b);
+ expect(value2a).toStrictEqual(value1b);
+ }
+ }
+ });
+}
+
+function testSwap2() {
+ test('Swap2', () => {
+ const data = [
+ [[], [], false, 'Invalid path 1'],
+ [['0'], [], false, 'Invalid path 2'],
+ [[], ['0'], false, 'Invalid path 1'],
+ [[0], ['0'], false, 'Invalid path 1: [0]'],
+ [['0'], [0], false, 'Invalid path 2: [0]']
+ ];
+
+ for (const [pathArray1, pathArray2, checkRevert, message] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor(object);
+
+ let value1a;
+ let value2a;
+ if (checkRevert) {
+ try {
+ value1a = accessor.get(pathArray1);
+ value2a = accessor.get(pathArray2);
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ expect(() => accessor.swap(pathArray1, pathArray2)).toThrow(message);
+
+ if (!checkRevert) { continue; }
+
+ const value1b = accessor.get(pathArray1);
+ const value2b = accessor.get(pathArray2);
+
+ expect(value1a).toStrictEqual(value1b);
+ expect(value2a).toStrictEqual(value2b);
+ }
+ });
+}
+
+
+function testGetPathString1() {
+ test('GetPathString1', () => {
+ const data = [
+ [[], ''],
+ [[0], '[0]'],
+ [['escape\\'], '["escape\\\\"]'],
+ [['\'quote\''], '["\'quote\'"]'],
+ [['"quote"'], '["\\"quote\\""]'],
+ [['part1', 'part2'], 'part1.part2'],
+ [['part1', 'part2', 3], 'part1.part2[3]'],
+ [['part1', 'part2', '3'], 'part1.part2["3"]'],
+ [['part1', 'part2', '3part'], 'part1.part2["3part"]'],
+ [['part1', 'part2', '3part', 'part4'], 'part1.part2["3part"].part4'],
+ [['part1', 'part2', '3part', '4part'], 'part1.part2["3part"]["4part"]']
+ ];
+
+ for (const [pathArray, expected] of data) {
+ expect(ObjectPropertyAccessor.getPathString(pathArray)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testGetPathString2() {
+ test('GetPathString2', () => {
+ const data = [
+ [[1.5], 'Invalid index'],
+ [[null], 'Invalid type: object']
+ ];
+
+ for (const [pathArray, message] of data) {
+ expect(() => ObjectPropertyAccessor.getPathString(pathArray)).toThrow(message);
+ }
+ });
+}
+
+
+function testGetPathArray1() {
+ test('GetPathArray1', () => {
+ const data = [
+ ['', []],
+ ['[0]', [0]],
+ ['["escape\\\\"]', ['escape\\']],
+ ['["\'quote\'"]', ['\'quote\'']],
+ ['["\\"quote\\""]', ['"quote"']],
+ ['part1.part2', ['part1', 'part2']],
+ ['part1.part2[3]', ['part1', 'part2', 3]],
+ ['part1.part2["3"]', ['part1', 'part2', '3']],
+ ['part1.part2[\'3\']', ['part1', 'part2', '3']],
+ ['part1.part2["3part"]', ['part1', 'part2', '3part']],
+ ['part1.part2[\'3part\']', ['part1', 'part2', '3part']],
+ ['part1.part2["3part"].part4', ['part1', 'part2', '3part', 'part4']],
+ ['part1.part2[\'3part\'].part4', ['part1', 'part2', '3part', 'part4']],
+ ['part1.part2["3part"]["4part"]', ['part1', 'part2', '3part', '4part']],
+ ['part1.part2[\'3part\'][\'4part\']', ['part1', 'part2', '3part', '4part']]
+ ];
+
+ for (const [pathString, expected] of data) {
+ expect(ObjectPropertyAccessor.getPathArray(pathString)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testGetPathArray2() {
+ test('GetPathArray2', () => {
+ const data = [
+ ['?', 'Unexpected character: ?'],
+ ['.', 'Unexpected character: .'],
+ ['0', 'Unexpected character: 0'],
+ ['part1.[0]', 'Unexpected character: ['],
+ ['part1?', 'Unexpected character: ?'],
+ ['[part1]', 'Unexpected character: p'],
+ ['[0a]', 'Unexpected character: a'],
+ ['["part1"x]', 'Unexpected character: x'],
+ ['[\'part1\'x]', 'Unexpected character: x'],
+ ['["part1"]x', 'Unexpected character: x'],
+ ['[\'part1\']x', 'Unexpected character: x'],
+ ['part1..part2', 'Unexpected character: .'],
+
+ ['[', 'Path not terminated correctly'],
+ ['part1.', 'Path not terminated correctly'],
+ ['part1[', 'Path not terminated correctly'],
+ ['part1["', 'Path not terminated correctly'],
+ ['part1[\'', 'Path not terminated correctly'],
+ ['part1[""', 'Path not terminated correctly'],
+ ['part1[\'\'', 'Path not terminated correctly'],
+ ['part1[0', 'Path not terminated correctly'],
+ ['part1[0].', 'Path not terminated correctly']
+ ];
+
+ for (const [pathString, message] of data) {
+ expect(() => ObjectPropertyAccessor.getPathArray(pathString)).toThrow(message);
+ }
+ });
+}
+
+
+function testHasProperty() {
+ test('HasProperty', () => {
+ const data = [
+ [{}, 'invalid', false],
+ [{}, 0, false],
+ [{valid: 0}, 'valid', true],
+ [{null: 0}, null, false],
+ [[], 'invalid', false],
+ [[], 0, false],
+ [[0], 0, true],
+ [[0], null, false],
+ ['string', 0, false],
+ ['string', 'length', false],
+ ['string', null, false]
+ ];
+
+ for (const [object, property, expected] of data) {
+ expect(ObjectPropertyAccessor.hasProperty(object, property)).toStrictEqual(expected);
+ }
+ });
+}
+
+function testIsValidPropertyType() {
+ test('IsValidPropertyType', () => {
+ const data = [
+ [{}, 'invalid', true],
+ [{}, 0, false],
+ [{valid: 0}, 'valid', true],
+ [{null: 0}, null, false],
+ [[], 'invalid', false],
+ [[], 0, true],
+ [[0], 0, true],
+ [[0], null, false],
+ ['string', 0, false],
+ ['string', 'length', false],
+ ['string', null, false]
+ ];
+
+ for (const [object, property, expected] of data) {
+ expect(ObjectPropertyAccessor.isValidPropertyType(object, property)).toStrictEqual(expected);
+ }
+ });
+}
+
+
+function main() {
+ testGet1();
+ testGet2();
+ testSet1();
+ testSet2();
+ testDelete1();
+ testDelete2();
+ testSwap1();
+ testSwap2();
+ testGetPathString1();
+ testGetPathString2();
+ testGetPathArray1();
+ testGetPathArray2();
+ testHasProperty();
+ testIsValidPropertyType();
+}
+
+
+main();
diff --git a/test/test-options-util.js b/test/options-util.test.js
index d94028c0..9f49eb28 100644
--- a/test/test-options-util.js
+++ b/test/options-util.test.js
@@ -16,53 +16,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-const fs = require('fs');
-const url = require('url');
-const path = require('path');
-const assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-
-function createVM(extDir) {
- const chrome = {
- runtime: {
- getURL(path2) {
- return url.pathToFileURL(path.join(extDir, path2.replace(/^\//, ''))).href;
- }
- }
+import fs from 'fs';
+import url, {fileURLToPath} from 'node:url';
+import path from 'path';
+import {expect, test, vi} from 'vitest';
+import {OptionsUtil} from '../ext/js/data/options-util.js';
+import {TemplatePatcher} from '../ext/js/templates/template-patcher.js';
+
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+vi.stubGlobal('fetch', async function fetch(url2) {
+ const filePath = url.fileURLToPath(url2);
+ await Promise.resolve();
+ const content = fs.readFileSync(filePath, {encoding: null});
+ return {
+ ok: true,
+ status: 200,
+ statusText: 'OK',
+ text: async () => Promise.resolve(content.toString('utf8')),
+ json: async () => Promise.resolve(JSON.parse(content.toString('utf8')))
};
-
- async function fetch(url2) {
- const filePath = url.fileURLToPath(url2);
- await Promise.resolve();
- const content = fs.readFileSync(filePath, {encoding: null});
- return {
- ok: true,
- status: 200,
- statusText: 'OK',
- text: async () => Promise.resolve(content.toString('utf8')),
- json: async () => Promise.resolve(JSON.parse(content.toString('utf8')))
- };
+});
+vi.stubGlobal('chrome', {
+ runtime: {
+ getURL: (path2) => {
+ return url.pathToFileURL(path.join(dirname, '..', 'ext', path2.replace(/^\//, ''))).href;
+ }
}
-
- const vm = new VM({chrome, fetch});
- vm.execute([
- 'js/core.js',
- 'js/general/cache-map.js',
- 'js/data/json-schema.js',
- 'js/templates/template-patcher.js',
- 'js/data/options-util.js'
- ]);
-
- return vm;
-}
-
-
-function clone(value) {
- return JSON.parse(JSON.stringify(value));
-}
-
+});
function createProfileOptionsTestData1() {
return {
@@ -632,131 +612,130 @@ function createOptionsUpdatedTestData1() {
}
-async function testUpdate(extDir) {
- const vm = createVM(extDir);
- const [OptionsUtil] = vm.get(['OptionsUtil']);
- const optionsUtil = new OptionsUtil();
- await optionsUtil.prepare();
+async function testUpdate() {
+ test('Update', async () => {
+ const optionsUtil = new OptionsUtil();
+ await optionsUtil.prepare();
- const options = createOptionsTestData1();
- const optionsUpdated = clone(await optionsUtil.update(options));
- const optionsExpected = createOptionsUpdatedTestData1();
- assert.deepStrictEqual(optionsUpdated, optionsExpected);
+ const options = createOptionsTestData1();
+ const optionsUpdated = structuredClone(await optionsUtil.update(options));
+ const optionsExpected = createOptionsUpdatedTestData1();
+ expect(optionsUpdated).toStrictEqual(optionsExpected);
+ });
}
-async function testDefault(extDir) {
- const data = [
- (options) => options,
- (options) => {
- delete options.profiles[0].options.audio.autoPlay;
- },
- (options) => {
- options.profiles[0].options.audio.autoPlay = void 0;
- }
- ];
+async function testDefault() {
+ test('Default', async () => {
+ const data = [
+ (options) => options,
+ (options) => {
+ delete options.profiles[0].options.audio.autoPlay;
+ },
+ (options) => {
+ options.profiles[0].options.audio.autoPlay = void 0;
+ }
+ ];
- const vm = createVM(extDir);
- const [OptionsUtil] = vm.get(['OptionsUtil']);
- const optionsUtil = new OptionsUtil();
- await optionsUtil.prepare();
+ const optionsUtil = new OptionsUtil();
+ await optionsUtil.prepare();
- for (const modify of data) {
- const options = optionsUtil.getDefault();
+ for (const modify of data) {
+ const options = optionsUtil.getDefault();
- const optionsModified = clone(options);
- modify(optionsModified);
+ const optionsModified = structuredClone(options);
+ modify(optionsModified);
- const optionsUpdated = await optionsUtil.update(clone(optionsModified));
- assert.deepStrictEqual(clone(optionsUpdated), clone(options));
- }
+ const optionsUpdated = await optionsUtil.update(structuredClone(optionsModified));
+ expect(structuredClone(optionsUpdated)).toStrictEqual(structuredClone(options));
+ }
+ });
}
-async function testFieldTemplatesUpdate(extDir) {
- const vm = createVM(extDir);
- const [OptionsUtil, TemplatePatcher] = vm.get(['OptionsUtil', 'TemplatePatcher']);
- const optionsUtil = new OptionsUtil();
- await optionsUtil.prepare();
+async function testFieldTemplatesUpdate() {
+ test('FieldTemplatesUpdate', async () => {
+ const optionsUtil = new OptionsUtil();
+ await optionsUtil.prepare();
- const templatePatcher = new TemplatePatcher();
- const loadDataFile = (fileName) => {
- const content = fs.readFileSync(path.join(extDir, fileName), {encoding: 'utf8'});
- return templatePatcher.parsePatch(content).addition;
- };
- const updates = [
- {version: 2, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v2.handlebars')},
- {version: 4, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v4.handlebars')},
- {version: 6, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')},
- {version: 8, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')},
- {version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.handlebars')},
- {version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')},
- {version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')},
- {version: 21, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v21.handlebars')}
- ];
- const getUpdateAdditions = (startVersion, targetVersion) => {
- let value = '';
- for (const {version, changes} of updates) {
- if (version <= startVersion || version > targetVersion || changes.length === 0) { continue; }
- if (value.length > 0) { value += '\n'; }
- value += changes;
- }
- return value;
- };
+ const templatePatcher = new TemplatePatcher();
+ const loadDataFile = (fileName) => {
+ const content = fs.readFileSync(path.join(dirname, '..', 'ext', fileName), {encoding: 'utf8'});
+ return templatePatcher.parsePatch(content).addition;
+ };
+ const updates = [
+ {version: 2, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v2.handlebars')},
+ {version: 4, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v4.handlebars')},
+ {version: 6, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')},
+ {version: 8, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')},
+ {version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.handlebars')},
+ {version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')},
+ {version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')},
+ {version: 21, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v21.handlebars')}
+ ];
+ const getUpdateAdditions = (startVersion, targetVersion) => {
+ let value = '';
+ for (const {version, changes} of updates) {
+ if (version <= startVersion || version > targetVersion || changes.length === 0) { continue; }
+ if (value.length > 0) { value += '\n'; }
+ value += changes;
+ }
+ return value;
+ };
- const data = [
+ const data = [
// Standard format
- {
- oldVersion: 0,
- newVersion: 12,
- old: `
+ {
+ oldVersion: 0,
+ newVersion: 12,
+ old: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // Non-standard marker format
- {
- oldVersion: 0,
- newVersion: 12,
- old: `
+ },
+ // Non-standard marker format
+ {
+ oldVersion: 0,
+ newVersion: 12,
+ old: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{~> (lookup . "marker2") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "character"}}
{{~definition.character~}}
{{/inline}}
{{~> (lookup . "marker2") ~}}
<<<UPDATE-ADDITIONS>>>`.trimStart()
- },
- // Empty test
- {
- oldVersion: 0,
- newVersion: 12,
- old: `
+ },
+ // Empty test
+ {
+ oldVersion: 0,
+ newVersion: 12,
+ old: `
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // Definition tags update
- {
- oldVersion: 0,
- newVersion: 12,
- old: `
+ },
+ // Definition tags update
+ {
+ oldVersion: 0,
+ newVersion: 12,
+ old: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}
@@ -779,7 +758,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}
`.trimStart(),
- expected: `
+ expected: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
@@ -824,12 +803,12 @@ async function testFieldTemplatesUpdate(extDir) {
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}
`.trimStart()
- },
- // glossary and glossary-brief update
- {
- oldVersion: 7,
- newVersion: 12,
- old: `
+ },
+ // glossary and glossary-brief update
+ {
+ oldVersion: 7,
+ newVersion: 12,
+ old: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
@@ -895,7 +874,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
@@ -964,12 +943,12 @@ async function testFieldTemplatesUpdate(extDir) {
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // formatGlossary update
- {
- oldVersion: 12,
- newVersion: 13,
- old: `
+ },
+ // formatGlossary update
+ {
+ oldVersion: 12,
+ newVersion: 13,
+ old: `
{{#*inline "example"}}
{{~#if (op "<=" glossary.length 1)~}}
{{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}}
@@ -982,7 +961,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "example"}}
{{~#if (op "<=" glossary.length 1)~}}
{{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}}
@@ -995,12 +974,12 @@ async function testFieldTemplatesUpdate(extDir) {
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // hasMedia/getMedia update
- {
- oldVersion: 12,
- newVersion: 13,
- old: `
+ },
+ // hasMedia/getMedia update
+ {
+ oldVersion: 12,
+ newVersion: 13,
+ old: `
{{#*inline "audio"}}
{{~#if definition.audioFileName~}}
[sound:{{definition.audioFileName}}]
@@ -1023,7 +1002,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "audio"}}
{{~#if (hasMedia "audio")~}}
[sound:{{#getMedia "audio"}}{{/getMedia}}]
@@ -1048,12 +1027,12 @@ async function testFieldTemplatesUpdate(extDir) {
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // hasMedia/getMedia update
- {
- oldVersion: 12,
- newVersion: 13,
- old: `
+ },
+ // hasMedia/getMedia update
+ {
+ oldVersion: 12,
+ newVersion: 13,
+ old: `
{{! Pitch Accents }}
{{#*inline "pitch-accent-item-downstep-notation"}}
{{~#scope~}}
@@ -1166,7 +1145,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{! Pitch Accents }}
{{#*inline "pitch-accent-item"}}
{{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}}
@@ -1216,12 +1195,12 @@ async function testFieldTemplatesUpdate(extDir) {
<<<UPDATE-ADDITIONS>>>
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // block helper update: furigana and furiganaPlain
- {
- oldVersion: 20,
- newVersion: 21,
- old: `
+ },
+ // block helper update: furigana and furiganaPlain
+ {
+ oldVersion: 20,
+ newVersion: 21,
+ old: `
{{#*inline "furigana"}}
{{~#if merge~}}
{{~#each definition.expressions~}}
@@ -1263,7 +1242,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "furigana"}}
{{~#if merge~}}
{{~#each definition.expressions~}}
@@ -1304,12 +1283,12 @@ async function testFieldTemplatesUpdate(extDir) {
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // block helper update: formatGlossary
- {
- oldVersion: 20,
- newVersion: 21,
- old: `
+ },
+ // block helper update: formatGlossary
+ {
+ oldVersion: 20,
+ newVersion: 21,
+ old: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
@@ -1344,7 +1323,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "glossary-single"}}
{{~#unless brief~}}
{{~#scope~}}
@@ -1378,12 +1357,12 @@ async function testFieldTemplatesUpdate(extDir) {
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // block helper update: set and get
- {
- oldVersion: 20,
- newVersion: 21,
- old: `
+ },
+ // block helper update: set and get
+ {
+ oldVersion: 20,
+ newVersion: 21,
+ old: `
{{#*inline "pitch-accent-item-disambiguation"}}
{{~#scope~}}
{{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}}
@@ -1432,7 +1411,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "pitch-accent-item-disambiguation"}}
{{~#scope~}}
{{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}}
@@ -1480,12 +1459,12 @@ async function testFieldTemplatesUpdate(extDir) {
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // block helper update: hasMedia and getMedia
- {
- oldVersion: 20,
- newVersion: 21,
- old: `
+ },
+ // block helper update: hasMedia and getMedia
+ {
+ oldVersion: 20,
+ newVersion: 21,
+ old: `
{{#*inline "audio"}}
{{~#if (hasMedia "audio")~}}
[sound:{{#getMedia "audio"}}{{/getMedia}}]
@@ -1524,7 +1503,7 @@ async function testFieldTemplatesUpdate(extDir) {
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "audio"}}
{{~#if (hasMedia "audio")~}}
[sound:{{getMedia "audio"}}]
@@ -1562,48 +1541,47 @@ async function testFieldTemplatesUpdate(extDir) {
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart()
- },
- // block helper update: pronunciation
- {
- oldVersion: 20,
- newVersion: 21,
- old: `
+ },
+ // block helper update: pronunciation
+ {
+ oldVersion: 20,
+ newVersion: 21,
+ old: `
{{#*inline "pitch-accent-item"}}
{{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}}
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart(),
- expected: `
+ expected: `
{{#*inline "pitch-accent-item"}}
{{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}
{{/inline}}
{{~> (lookup . "marker") ~}}`.trimStart()
- }
- ];
+ }
+ ];
- const updatesPattern = /<<<UPDATE-ADDITIONS>>>/g;
- for (const {old, expected, oldVersion, newVersion} of data) {
- const options = createOptionsTestData1();
- options.profiles[0].options.anki.fieldTemplates = old;
- options.version = oldVersion;
+ const updatesPattern = /<<<UPDATE-ADDITIONS>>>/g;
+ for (const {old, expected, oldVersion, newVersion} of data) {
+ const options = createOptionsTestData1();
+ options.profiles[0].options.anki.fieldTemplates = old;
+ options.version = oldVersion;
- const expected2 = expected.replace(updatesPattern, getUpdateAdditions(oldVersion, newVersion));
+ const expected2 = expected.replace(updatesPattern, getUpdateAdditions(oldVersion, newVersion));
- const optionsUpdated = clone(await optionsUtil.update(options, newVersion));
- const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates;
- assert.deepStrictEqual(fieldTemplatesActual, expected2);
- }
+ const optionsUpdated = structuredClone(await optionsUtil.update(options, newVersion));
+ const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates;
+ expect(fieldTemplatesActual).toStrictEqual(expected2);
+ }
+ });
}
async function main() {
- const extDir = path.join(__dirname, '..', 'ext');
- await testUpdate(extDir);
- await testDefault(extDir);
- await testFieldTemplatesUpdate(extDir);
+ await testUpdate();
+ await testDefault();
+ await testFieldTemplatesUpdate();
}
-
-if (require.main === module) { testMain(main); }
+await main();
diff --git a/test/profile-conditions-util.test.js b/test/profile-conditions-util.test.js
new file mode 100644
index 00000000..ca8b00ef
--- /dev/null
+++ b/test/profile-conditions-util.test.js
@@ -0,0 +1,1090 @@
+/*
+ * 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/>.
+ */
+
+import {expect, test} from 'vitest';
+import {ProfileConditionsUtil} from '../ext/js/background/profile-conditions-util.js';
+
+function testNormalizeContext() {
+ test('NormalizeContext', () => {
+ const data = [
+ // Empty
+ {
+ context: {},
+ expected: {flags: []}
+ },
+
+ // Domain normalization
+ {
+ context: {url: ''},
+ expected: {url: '', flags: []}
+ },
+ {
+ context: {url: 'http://example.com/'},
+ expected: {url: 'http://example.com/', domain: 'example.com', flags: []}
+ },
+ {
+ context: {url: 'http://example.com:1234/'},
+ expected: {url: 'http://example.com:1234/', domain: 'example.com', flags: []}
+ },
+ {
+ context: {url: 'http://user@example.com:1234/'},
+ expected: {url: 'http://user@example.com:1234/', domain: 'example.com', flags: []}
+ }
+ ];
+
+ for (const {context, expected} of data) {
+ const profileConditionsUtil = new ProfileConditionsUtil();
+ const actual = profileConditionsUtil.normalizeContext(context);
+ expect(actual).toStrictEqual(expected);
+ }
+ });
+}
+
+function testSchemas() {
+ test('Schemas', () => {
+ const data = [
+ // Empty
+ {
+ conditionGroups: [],
+ expectedSchema: {},
+ inputs: [
+ {expected: true, context: {url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {conditions: []}
+ ],
+ expectedSchema: {},
+ inputs: [
+ {expected: true, context: {url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {conditions: []},
+ {conditions: []}
+ ],
+ expectedSchema: {},
+ inputs: [
+ {expected: true, context: {url: 'http://example.com/'}}
+ ]
+ },
+
+ // popupLevel tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'equal',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {const: 0}
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'notEqual',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: [
+ {
+ properties: {
+ depth: {const: 0}
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'lessThanOrEqual',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ maximum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThanOrEqual',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ minimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+
+ // url tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'url',
+ operator: 'matchDomain',
+ value: 'example.com'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ domain: {
+ oneOf: [
+ {const: 'example.com'}
+ ]
+ }
+ },
+ required: ['domain']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example1.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example2.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}},
+ {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'url',
+ operator: 'matchDomain',
+ value: 'example.com, example1.com, example2.com'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ domain: {
+ oneOf: [
+ {const: 'example.com'},
+ {const: 'example1.com'},
+ {const: 'example2.com'}
+ ]
+ }
+ },
+ required: ['domain']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example1.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example2.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example3.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}},
+ {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'url',
+ operator: 'matchRegExp',
+ value: '^http://example\\d?\\.com/[\\w\\W]*$'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ url: {
+ type: 'string',
+ pattern: '^http://example\\d?\\.com/[\\w\\W]*$',
+ patternFlags: 'i'
+ }
+ },
+ required: ['url']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example1.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example2.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example3.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/example'}},
+ {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}},
+ {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}},
+ {expected: false, context: {depth: 0, url: 'http://example-1.com/'}}
+ ]
+ },
+
+ // modifierKeys tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'are',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'are',
+ value: 'Alt, Shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'Alt'}},
+ {contains: {const: 'Shift'}}
+ ]
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'areNot',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: [
+ {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ },
+ required: ['modifierKeys']
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'areNot',
+ value: 'Alt, Shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: [
+ {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'Alt'}},
+ {contains: {const: 'Shift'}}
+ ]
+ }
+ },
+ required: ['modifierKeys']
+ }
+ ]
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'include',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ minItems: 0
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'include',
+ value: 'Alt, Shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'Alt'}},
+ {contains: {const: 'Shift'}}
+ ]
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'notInclude',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array'
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'notInclude',
+ value: 'Alt, Shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ not: [
+ {contains: {const: 'Alt'}},
+ {contains: {const: 'Shift'}}
+ ]
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
+ ]
+ },
+
+ // flags tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'are',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {}},
+ {expected: true, context: {flags: []}},
+ {expected: false, context: {flags: ['test1']}},
+ {expected: false, context: {flags: ['test1', 'test2']}},
+ {expected: false, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'are',
+ value: 'test1, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'test1'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ },
+ inputs: [
+ {expected: false, context: {}},
+ {expected: false, context: {flags: []}},
+ {expected: false, context: {flags: ['test1']}},
+ {expected: true, context: {flags: ['test1', 'test2']}},
+ {expected: false, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'areNot',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: [
+ {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ }
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {}},
+ {expected: false, context: {flags: []}},
+ {expected: true, context: {flags: ['test1']}},
+ {expected: true, context: {flags: ['test1', 'test2']}},
+ {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'areNot',
+ value: 'test1, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: [
+ {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'test1'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ }
+ ]
+ },
+ inputs: [
+ {expected: true, context: {}},
+ {expected: true, context: {flags: []}},
+ {expected: true, context: {flags: ['test1']}},
+ {expected: false, context: {flags: ['test1', 'test2']}},
+ {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'include',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ minItems: 0
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {}},
+ {expected: true, context: {flags: []}},
+ {expected: true, context: {flags: ['test1']}},
+ {expected: true, context: {flags: ['test1', 'test2']}},
+ {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'include',
+ value: 'test1, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'test1'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ },
+ inputs: [
+ {expected: false, context: {}},
+ {expected: false, context: {flags: []}},
+ {expected: false, context: {flags: ['test1']}},
+ {expected: true, context: {flags: ['test1', 'test2']}},
+ {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'notInclude',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array'
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {}},
+ {expected: true, context: {flags: []}},
+ {expected: true, context: {flags: ['test1']}},
+ {expected: true, context: {flags: ['test1', 'test2']}},
+ {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'notInclude',
+ value: 'test1, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ not: [
+ {contains: {const: 'test1'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {}},
+ {expected: true, context: {flags: []}},
+ {expected: false, context: {flags: ['test1']}},
+ {expected: false, context: {flags: ['test1', 'test2']}},
+ {expected: false, context: {flags: ['test1', 'test2', 'test3']}}
+ ]
+ },
+
+ // Multiple conditions tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '3'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 3
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: -2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 3, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '3'
+ }
+ ]
+ },
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'equal',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ anyOf: [
+ {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 3
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ {
+ properties: {
+ depth: {const: 0}
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: -2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 3, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '3'
+ }
+ ]
+ },
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'lessThanOrEqual',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'greaterThanOrEqual',
+ value: '-1'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ anyOf: [
+ {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 3
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ maximum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ minimum: -1
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: -2, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 3, url: 'http://example.com/'}}
+ ]
+ }
+ ];
+
+ for (const {conditionGroups, expectedSchema, inputs} of data) {
+ const profileConditionsUtil = new ProfileConditionsUtil();
+ const schema = profileConditionsUtil.createSchema(conditionGroups);
+ if (typeof expectedSchema !== 'undefined') {
+ expect(schema.schema).toStrictEqual(expectedSchema);
+ }
+ if (Array.isArray(inputs)) {
+ for (const {expected, context} of inputs) {
+ const normalizedContext = profileConditionsUtil.normalizeContext(context);
+ const actual = schema.isValid(normalizedContext);
+ expect(actual).toStrictEqual(expected);
+ }
+ }
+ }
+ });
+}
+
+
+function main() {
+ testNormalizeContext();
+ testSchemas();
+}
+
+main();
diff --git a/test/test-all.js b/test/test-all.js
deleted file mode 100644
index d187879a..00000000
--- a/test/test-all.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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 {spawnSync} = require('child_process');
-const {getArgs} = require('../dev/util');
-
-
-function main() {
- const args = getArgs(process.argv.slice(2), new Map([
- ['skip', []],
- [null, []]
- ]));
- const directories = args.get(null);
- const skip = new Set([__filename, ...args.get('skip')].map((value) => path.resolve(value)));
-
- const node = process.execPath;
- const fileNamePattern = /\.js$/i;
-
- let first = true;
- for (const directory of directories) {
- const fileNames = fs.readdirSync(directory);
- for (const fileName of fileNames) {
- if (!fileNamePattern.test(fileName)) { continue; }
-
- const fullFileName = path.resolve(path.join(directory, fileName));
- if (skip.has(fullFileName)) { continue; }
-
- const stats = fs.lstatSync(fullFileName);
- if (!stats.isFile()) { continue; }
-
- process.stdout.write(`${first ? '' : '\n'}Running ${fileName}...\n`);
- first = false;
-
- const {error, status} = spawnSync(node, [fileName], {cwd: directory, stdio: 'inherit'});
-
- if (status !== null && status !== 0) {
- process.exit(status);
- return;
- }
- if (error) {
- throw error;
- }
- }
- }
-
- process.exit(0);
-}
-
-
-if (require.main === module) { main(); }
diff --git a/test/test-anki-note-builder.js b/test/test-anki-note-builder.js
deleted file mode 100644
index ba937302..00000000
--- a/test/test-anki-note-builder.js
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Copyright (C) 2023 Yomitan Authors
- * Copyright (C) 2021-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 {TranslatorVM} = require('../dev/translator-vm');
-
-
-function clone(value) {
- return JSON.parse(JSON.stringify(value));
-}
-
-async function createVM() {
- const dom = new JSDOM();
- const {Node, NodeFilter, document} = dom.window;
-
- const vm = new TranslatorVM({
- Node,
- NodeFilter,
- document,
- location: new URL('https://yomichan.test/')
- });
-
- const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1');
- await vm.prepare(dictionaryDirectory, 'Test Dictionary 2');
-
- vm.execute([
- 'js/data/anki-note-builder.js',
- 'js/data/anki-util.js',
- 'js/dom/sandbox/css-style-applier.js',
- 'js/display/sandbox/pronunciation-generator.js',
- 'js/display/sandbox/structured-content-generator.js',
- 'js/templates/sandbox/anki-template-renderer.js',
- 'js/templates/sandbox/anki-template-renderer-content-manager.js',
- 'js/templates/sandbox/template-renderer.js',
- 'js/templates/sandbox/template-renderer-media-provider.js',
- 'lib/handlebars.min.js'
- ]);
-
- const [
- JapaneseUtil,
- AnkiNoteBuilder,
- AnkiTemplateRenderer
- ] = vm.get([
- 'JapaneseUtil',
- 'AnkiNoteBuilder',
- 'AnkiTemplateRenderer'
- ]);
-
- class TemplateRendererProxy {
- constructor() {
- this._preparePromise = null;
- this._ankiTemplateRenderer = new AnkiTemplateRenderer();
- }
-
- async render(template, data, type) {
- await this._prepare();
- return await this._ankiTemplateRenderer.templateRenderer.render(template, data, type);
- }
-
- async renderMulti(items) {
- await this._prepare();
- return await this._serializeMulti(this._ankiTemplateRenderer.templateRenderer.renderMulti(items));
- }
-
- _prepare() {
- if (this._preparePromise === null) {
- this._preparePromise = this._prepareInternal();
- }
- return this._preparePromise;
- }
-
- async _prepareInternal() {
- await this._ankiTemplateRenderer.prepare();
- }
-
- _serializeError(error) {
- try {
- if (typeof error === 'object' && error !== null) {
- const result = {
- name: error.name,
- message: error.message,
- stack: error.stack
- };
- if (Object.prototype.hasOwnProperty.call(error, 'data')) {
- result.data = error.data;
- }
- return result;
- }
- } catch (e) {
- // NOP
- }
- return {
- value: error,
- hasValue: true
- };
- }
-
- _serializeMulti(array) {
- for (let i = 0, ii = array.length; i < ii; ++i) {
- const value = array[i];
- const {error} = value;
- if (typeof error !== 'undefined') {
- value.error = this._serializeError(error);
- }
- }
- return array;
- }
- }
- vm.set({TemplateRendererProxy});
-
- return {vm, AnkiNoteBuilder, JapaneseUtil};
-}
-
-function getFieldMarkers(type) {
- switch (type) {
- case 'terms':
- return [
- 'audio',
- 'clipboard-image',
- 'clipboard-text',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'conjugation',
- 'dictionary',
- 'document-title',
- 'expression',
- 'frequencies',
- 'furigana',
- 'furigana-plain',
- 'glossary',
- 'glossary-brief',
- 'glossary-no-dictionary',
- 'part-of-speech',
- 'pitch-accents',
- 'pitch-accent-graphs',
- 'pitch-accent-positions',
- 'reading',
- 'screenshot',
- 'search-query',
- 'selection-text',
- 'sentence',
- 'sentence-furigana',
- 'tags',
- 'url'
- ];
- case 'kanji':
- return [
- 'character',
- 'clipboard-image',
- 'clipboard-text',
- 'cloze-body',
- 'cloze-prefix',
- 'cloze-suffix',
- 'dictionary',
- 'document-title',
- 'glossary',
- 'kunyomi',
- 'onyomi',
- 'screenshot',
- 'search-query',
- 'selection-text',
- 'sentence',
- 'sentence-furigana',
- 'stroke-count',
- 'tags',
- 'url'
- ];
- default:
- return [];
- }
-}
-
-async function getRenderResults(dictionaryEntries, type, mode, template, AnkiNoteBuilder, JapaneseUtil, write) {
- const markers = getFieldMarkers(type);
- const fields = [];
- for (const marker of markers) {
- fields.push([marker, `{${marker}}`]);
- }
-
- const japaneseUtil = new JapaneseUtil(null);
- const clozePrefix = 'cloze-prefix';
- const clozeSuffix = 'cloze-suffix';
- const results = [];
- for (const dictionaryEntry of dictionaryEntries) {
- let source = '';
- switch (dictionaryEntry.type) {
- case 'kanji':
- source = dictionaryEntry.character;
- break;
- case 'term':
- if (dictionaryEntry.headwords.length > 0 && dictionaryEntry.headwords[0].sources.length > 0) {
- source = dictionaryEntry.headwords[0].sources[0].originalText;
- }
- break;
- }
- const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil});
- const context = {
- url: 'url:',
- sentence: {
- text: `${clozePrefix}${source}${clozeSuffix}`,
- offset: clozePrefix.length
- },
- documentTitle: 'title',
- query: 'query',
- fullQuery: 'fullQuery'
- };
- const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote({
- dictionaryEntry,
- mode: null,
- context,
- template,
- deckName: 'deckName',
- modelName: 'modelName',
- fields,
- tags: ['yomichan'],
- checkForDuplicates: true,
- duplicateScope: 'collection',
- duplicateScopeCheckAllModels: false,
- resultOutputMode: mode,
- glossaryLayoutMode: 'default',
- compactTags: false
- });
- if (!write) {
- for (const error of errors) {
- console.error(error);
- }
- assert.strictEqual(errors.length, 0);
- }
- results.push(noteFields);
- }
-
- return results;
-}
-
-
-async function main() {
- const write = (process.argv[2] === '--write');
-
- const {vm, AnkiNoteBuilder, JapaneseUtil} = await createVM();
-
- const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json');
- const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'}));
-
- const testResults1FilePath = path.join(__dirname, 'data', 'anki-note-builder-test-results.json');
- const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'}));
- const actualResults1 = [];
-
- const template = fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'});
-
- for (let i = 0, ii = tests.length; i < ii; ++i) {
- const test = tests[i];
- const expected1 = expectedResults1[i];
- switch (test.func) {
- case 'findTerms':
- {
- const {name, mode, text} = test;
- const options = vm.buildOptions(optionsPresets, test.options);
- const {dictionaryEntries} = clone(await vm.translator.findTerms(mode, text, options));
- const results = mode !== 'simple' ? clone(await getRenderResults(dictionaryEntries, 'terms', mode, template, AnkiNoteBuilder, JapaneseUtil, write)) : null;
- actualResults1.push({name, results});
- if (!write) {
- assert.deepStrictEqual(results, expected1.results);
- }
- }
- break;
- case 'findKanji':
- {
- const {name, text} = test;
- const options = vm.buildOptions(optionsPresets, test.options);
- const dictionaryEntries = clone(await vm.translator.findKanji(text, options));
- const results = clone(await getRenderResults(dictionaryEntries, 'kanji', null, template, AnkiNoteBuilder, JapaneseUtil, write));
- actualResults1.push({name, results});
- if (!write) {
- assert.deepStrictEqual(results, expected1.results);
- }
- }
- break;
- }
- }
-
- if (write) {
- // Use 2 indent instead of 4 to save a bit of file size
- fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'});
- }
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-build-libs.js b/test/test-build-libs.js
deleted file mode 100644
index 496f43f8..00000000
--- a/test/test-build-libs.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2023 Yomitan Authors
- * Copyright (C) 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 assert = require('assert');
-const {getBuildTargets} = require('../dev/build-libs');
-
-async function main() {
- try {
- for (const {path: path2, build} of getBuildTargets()) {
- let expectedContent = await build();
- if (typeof expectedContent !== 'string') {
- // Buffer
- expectedContent = expectedContent.toString('utf8');
- }
- const actualContent = fs.readFileSync(path2, {encoding: 'utf8'});
- assert.strictEqual(actualContent, expectedContent);
- }
- } catch (e) {
- console.error(e);
- process.exit(-1);
- return;
- }
- process.exit(0);
-}
-
-if (require.main === module) { main(); }
diff --git a/test/test-cache-map.js b/test/test-cache-map.js
deleted file mode 100644
index 863c6ace..00000000
--- a/test/test-cache-map.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-const vm = new VM({console});
-vm.execute([
- 'js/general/cache-map.js'
-]);
-const CacheMap = vm.get('CacheMap');
-
-
-function testConstructor() {
- const data = [
- [false, () => new CacheMap(0)],
- [false, () => new CacheMap(1)],
- [false, () => new CacheMap(Number.MAX_VALUE)],
- [true, () => new CacheMap(-1)],
- [true, () => new CacheMap(1.5)],
- [true, () => new CacheMap(Number.NaN)],
- [true, () => new CacheMap(Number.POSITIVE_INFINITY)],
- [true, () => new CacheMap('a')]
- ];
-
- for (const [throws, create] of data) {
- if (throws) {
- assert.throws(create);
- } else {
- assert.doesNotThrow(create);
- }
- }
-}
-
-function testApi() {
- const data = [
- {
- maxSize: 1,
- expectedSize: 0,
- calls: []
- },
- {
- maxSize: 10,
- expectedSize: 1,
- calls: [
- {func: 'get', args: ['a1-b-c'], returnValue: void 0},
- {func: 'has', args: ['a1-b-c'], returnValue: false},
- {func: 'set', args: ['a1-b-c', 32], returnValue: void 0},
- {func: 'get', args: ['a1-b-c'], returnValue: 32},
- {func: 'has', args: ['a1-b-c'], returnValue: true}
- ]
- },
- {
- maxSize: 10,
- expectedSize: 2,
- calls: [
- {func: 'set', args: ['a1-b-c', 32], returnValue: void 0},
- {func: 'get', args: ['a1-b-c'], returnValue: 32},
- {func: 'set', args: ['a1-b-c', 64], returnValue: void 0},
- {func: 'get', args: ['a1-b-c'], returnValue: 64},
- {func: 'set', args: ['a2-b-c', 96], returnValue: void 0},
- {func: 'get', args: ['a2-b-c'], returnValue: 96}
- ]
- },
- {
- maxSize: 2,
- expectedSize: 2,
- calls: [
- {func: 'has', args: ['a1-b-c'], returnValue: false},
- {func: 'has', args: ['a2-b-c'], returnValue: false},
- {func: 'has', args: ['a3-b-c'], returnValue: false},
- {func: 'set', args: ['a1-b-c', 1], returnValue: void 0},
- {func: 'has', args: ['a1-b-c'], returnValue: true},
- {func: 'has', args: ['a2-b-c'], returnValue: false},
- {func: 'has', args: ['a3-b-c'], returnValue: false},
- {func: 'set', args: ['a2-b-c', 2], returnValue: void 0},
- {func: 'has', args: ['a1-b-c'], returnValue: true},
- {func: 'has', args: ['a2-b-c'], returnValue: true},
- {func: 'has', args: ['a3-b-c'], returnValue: false},
- {func: 'set', args: ['a3-b-c', 3], returnValue: void 0},
- {func: 'has', args: ['a1-b-c'], returnValue: false},
- {func: 'has', args: ['a2-b-c'], returnValue: true},
- {func: 'has', args: ['a3-b-c'], returnValue: true}
- ]
- }
- ];
-
- for (const {maxSize, expectedSize, calls} of data) {
- const cache = new CacheMap(maxSize);
- assert.strictEqual(cache.maxSize, maxSize);
- for (const call of calls) {
- const {func, args} = call;
- let returnValue;
- switch (func) {
- case 'get': returnValue = cache.get(...args); break;
- case 'set': returnValue = cache.set(...args); break;
- case 'has': returnValue = cache.has(...args); break;
- case 'clear': returnValue = cache.clear(...args); break;
- }
- if (Object.prototype.hasOwnProperty.call(call, 'returnValue')) {
- const {returnValue: expectedReturnValue} = call;
- assert.deepStrictEqual(returnValue, expectedReturnValue);
- }
- }
- assert.strictEqual(cache.size, expectedSize);
- }
-}
-
-
-function main() {
- testConstructor();
- testApi();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-core.js b/test/test-core.js
deleted file mode 100644
index b42f8cf2..00000000
--- a/test/test-core.js
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-const vm = new VM();
-vm.execute([
- 'js/core.js'
-]);
-const [DynamicProperty, deepEqual] = vm.get(['DynamicProperty', 'deepEqual']);
-
-
-function testDynamicProperty() {
- const data = [
- {
- initialValue: 0,
- operations: [
- {
- operation: null,
- expectedDefaultValue: 0,
- expectedValue: 0,
- expectedOverrideCount: 0,
- expeectedEventOccurred: false
- },
- {
- operation: 'set.defaultValue',
- args: [1],
- expectedDefaultValue: 1,
- expectedValue: 1,
- expectedOverrideCount: 0,
- expeectedEventOccurred: true
- },
- {
- operation: 'set.defaultValue',
- args: [1],
- expectedDefaultValue: 1,
- expectedValue: 1,
- expectedOverrideCount: 0,
- expeectedEventOccurred: false
- },
- {
- operation: 'set.defaultValue',
- args: [0],
- expectedDefaultValue: 0,
- expectedValue: 0,
- expectedOverrideCount: 0,
- expeectedEventOccurred: true
- },
- {
- operation: 'setOverride',
- args: [8],
- expectedDefaultValue: 0,
- expectedValue: 8,
- expectedOverrideCount: 1,
- expeectedEventOccurred: true
- },
- {
- operation: 'setOverride',
- args: [16],
- expectedDefaultValue: 0,
- expectedValue: 8,
- expectedOverrideCount: 2,
- expeectedEventOccurred: false
- },
- {
- operation: 'setOverride',
- args: [32, 1],
- expectedDefaultValue: 0,
- expectedValue: 32,
- expectedOverrideCount: 3,
- expeectedEventOccurred: true
- },
- {
- operation: 'setOverride',
- args: [64, -1],
- expectedDefaultValue: 0,
- expectedValue: 32,
- expectedOverrideCount: 4,
- expeectedEventOccurred: false
- },
- {
- operation: 'clearOverride',
- args: [-4],
- expectedDefaultValue: 0,
- expectedValue: 32,
- expectedOverrideCount: 3,
- expeectedEventOccurred: false
- },
- {
- operation: 'clearOverride',
- args: [-3],
- expectedDefaultValue: 0,
- expectedValue: 32,
- expectedOverrideCount: 2,
- expeectedEventOccurred: false
- },
- {
- operation: 'clearOverride',
- args: [-2],
- expectedDefaultValue: 0,
- expectedValue: 64,
- expectedOverrideCount: 1,
- expeectedEventOccurred: true
- },
- {
- operation: 'clearOverride',
- args: [-1],
- expectedDefaultValue: 0,
- expectedValue: 0,
- expectedOverrideCount: 0,
- expeectedEventOccurred: true
- }
- ]
- }
- ];
-
- for (const {initialValue, operations} of data) {
- const property = new DynamicProperty(initialValue);
- const overrideTokens = [];
- let eventOccurred = false;
- const onChange = () => { eventOccurred = true; };
- property.on('change', onChange);
- for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) {
- eventOccurred = false;
- switch (operation) {
- case 'set.defaultValue': property.defaultValue = args[0]; break;
- case 'setOverride': overrideTokens.push(property.setOverride(...args)); break;
- case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break;
- }
- assert.strictEqual(eventOccurred, expeectedEventOccurred);
- assert.strictEqual(property.defaultValue, expectedDefaultValue);
- assert.strictEqual(property.value, expectedValue);
- assert.strictEqual(property.overrideCount, expectedOverrideCount);
- }
- property.off('change', onChange);
- }
-}
-
-function testDeepEqual() {
- const data = [
- // Simple tests
- {
- value1: 0,
- value2: 0,
- expected: true
- },
- {
- value1: null,
- value2: null,
- expected: true
- },
- {
- value1: 'test',
- value2: 'test',
- expected: true
- },
- {
- value1: true,
- value2: true,
- expected: true
- },
- {
- value1: 0,
- value2: 1,
- expected: false
- },
- {
- value1: null,
- value2: false,
- expected: false
- },
- {
- value1: 'test1',
- value2: 'test2',
- expected: false
- },
- {
- value1: true,
- value2: false,
- expected: false
- },
-
- // Simple object tests
- {
- value1: {},
- value2: {},
- expected: true
- },
- {
- value1: {},
- value2: [],
- expected: false
- },
- {
- value1: [],
- value2: [],
- expected: true
- },
- {
- value1: {},
- value2: null,
- expected: false
- },
-
- // Complex object tests
- {
- value1: [1],
- value2: [],
- expected: false
- },
- {
- value1: [1],
- value2: [1],
- expected: true
- },
- {
- value1: [1],
- value2: [2],
- expected: false
- },
-
- {
- value1: {},
- value2: {test: 1},
- expected: false
- },
- {
- value1: {test: 1},
- value2: {test: 1},
- expected: true
- },
- {
- value1: {test: 1},
- value2: {test: {test2: false}},
- expected: false
- },
- {
- value1: {test: {test2: true}},
- value2: {test: {test2: false}},
- expected: false
- },
- {
- value1: {test: {test2: [true]}},
- value2: {test: {test2: [true]}},
- expected: true
- },
-
- // Recursive
- {
- value1: (() => { const x = {}; x.x = x; return x; })(),
- value2: (() => { const x = {}; x.x = x; return x; })(),
- expected: false
- }
- ];
-
- let index = 0;
- for (const {value1, value2, expected} of data) {
- const actual1 = deepEqual(value1, value2);
- assert.strictEqual(actual1, expected, `Failed for test ${index}`);
-
- const actual2 = deepEqual(value2, value1);
- assert.strictEqual(actual2, expected, `Failed for test ${index}`);
-
- ++index;
- }
-}
-
-
-function main() {
- testDynamicProperty();
- testDeepEqual();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-database.js b/test/test-database.js
deleted file mode 100644
index c4cd504a..00000000
--- a/test/test-database.js
+++ /dev/null
@@ -1,887 +0,0 @@
-/*
- * 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 path = require('path');
-const assert = require('assert');
-const {createDictionaryArchive, testMain} = require('../dev/util');
-const {DatabaseVM, DatabaseVMDictionaryImporterMediaLoader} = require('../dev/database-vm');
-
-
-const vm = new DatabaseVM();
-vm.execute([
- 'js/core.js',
- 'js/general/cache-map.js',
- 'js/data/json-schema.js',
- 'js/media/media-util.js',
- 'js/language/dictionary-importer.js',
- 'js/data/database.js',
- 'js/language/dictionary-database.js'
-]);
-const DictionaryImporter = vm.get('DictionaryImporter');
-const DictionaryDatabase = vm.get('DictionaryDatabase');
-
-
-function createTestDictionaryArchive(dictionary, dictionaryName) {
- const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary);
- return createDictionaryArchive(dictionaryDirectory, dictionaryName);
-}
-
-
-function createDictionaryImporter(onProgress) {
- const dictionaryImporterMediaLoader = new DatabaseVMDictionaryImporterMediaLoader();
- return new DictionaryImporter(dictionaryImporterMediaLoader, (...args) => {
- const {stepIndex, stepCount, index, count} = args[0];
- assert.ok(stepIndex < stepCount);
- assert.ok(index <= count);
- if (typeof onProgress === 'function') {
- onProgress(...args);
- }
- });
-}
-
-
-function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) {
- return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0);
-}
-
-function countDictionaryDatabaseEntriesWithReading(dictionaryDatabaseEntries, reading) {
- return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0);
-}
-
-function countMetasWithMode(metas, mode) {
- return metas.reduce((i, v) => (i + (v.mode === mode ? 1 : 0)), 0);
-}
-
-function countKanjiWithCharacter(kanji, character) {
- return kanji.reduce((i, v) => (i + (v.character === character ? 1 : 0)), 0);
-}
-
-
-function clearDatabase(timeout) {
- return new Promise((resolve, reject) => {
- let timer = setTimeout(() => {
- timer = null;
- reject(new Error(`clearDatabase failed to resolve after ${timeout}ms`));
- }, timeout);
-
- (async () => {
- const indexedDB = vm.indexedDB;
- for (const {name} of await indexedDB.databases()) {
- await new Promise((resolve2, reject2) => {
- const request = indexedDB.deleteDatabase(name);
- request.onerror = (e) => reject2(e);
- request.onsuccess = () => resolve2();
- });
- }
- if (timer !== null) {
- clearTimeout(timer);
- }
- resolve();
- })();
- });
-}
-
-
-async function testDatabase1() {
- // Load dictionary data
- const testDictionary = createTestDictionaryArchive('valid-dictionary1');
- const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
- const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
-
- const title = testDictionaryIndex.title;
- const titles = new Map([
- [title, {priority: 0, allowSecondarySearches: false}]
- ]);
-
- // Setup iteration data
- const iterations = [
- {
- cleanup: async () => {
- // Test purge
- await dictionaryDatabase.purge();
- await testDatabaseEmpty1(dictionaryDatabase);
- }
- },
- {
- cleanup: async () => {
- // Test deleteDictionary
- let progressEvent = false;
- await dictionaryDatabase.deleteDictionary(
- title,
- 1000,
- () => {
- progressEvent = true;
- }
- );
- assert.ok(progressEvent);
-
- await testDatabaseEmpty1(dictionaryDatabase);
- }
- },
- {
- cleanup: async () => {}
- }
- ];
-
- // Setup database
- const dictionaryDatabase = new DictionaryDatabase();
- await dictionaryDatabase.prepare();
-
- for (const {cleanup} of iterations) {
- const expectedSummary = {
- title,
- revision: 'test',
- sequenced: true,
- version: 3,
- importDate: 0,
- prefixWildcardsSupported: true,
- counts: {
- kanji: {total: 2},
- kanjiMeta: {total: 6, freq: 6},
- media: {total: 4},
- tagMeta: {total: 15},
- termMeta: {total: 38, freq: 31, pitch: 7},
- terms: {total: 21}
- }
- };
-
- // Import data
- let progressEvent = false;
- const dictionaryImporter = createDictionaryImporter(() => { progressEvent = true; });
- const {result, errors} = await dictionaryImporter.importDictionary(
- dictionaryDatabase,
- testDictionarySource,
- {prefixWildcardsSupported: true}
- );
- expectedSummary.importDate = result.importDate;
- vm.assert.deepStrictEqual(errors, []);
- vm.assert.deepStrictEqual(result, expectedSummary);
- assert.ok(progressEvent);
-
- // Get info summary
- const info = await dictionaryDatabase.getDictionaryInfo();
- vm.assert.deepStrictEqual(info, [expectedSummary]);
-
- // Get counts
- const counts = await dictionaryDatabase.getDictionaryCounts(
- info.map((v) => v.title),
- true
- );
- vm.assert.deepStrictEqual(counts, {
- counts: [{kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}],
- total: {kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}
- });
-
- // Test find* functions
- await testFindTermsBulkTest1(dictionaryDatabase, titles);
- await testTindTermsExactBulk1(dictionaryDatabase, titles);
- await testFindTermsBySequenceBulk1(dictionaryDatabase, title);
- await testFindTermMetaBulk1(dictionaryDatabase, titles);
- await testFindKanjiBulk1(dictionaryDatabase, titles);
- await testFindKanjiMetaBulk1(dictionaryDatabase, titles);
- await testFindTagForTitle1(dictionaryDatabase, title);
-
- // Cleanup
- await cleanup();
- }
-
- await dictionaryDatabase.close();
-}
-
-async function testDatabaseEmpty1(database) {
- const info = await database.getDictionaryInfo();
- vm.assert.deepStrictEqual(info, []);
-
- const counts = await database.getDictionaryCounts([], true);
- vm.assert.deepStrictEqual(counts, {
- counts: [],
- total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0, media: 0}
- });
-}
-
-async function testFindTermsBulkTest1(database, titles) {
- const data = [
- {
- inputs: [
- {
- matchType: null,
- termList: ['打', '打つ', '打ち込む']
- },
- {
- matchType: null,
- termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ']
- },
- {
- matchType: 'prefix',
- termList: ['打']
- }
- ],
- expectedResults: {
- total: 10,
- terms: [
- ['打', 2],
- ['打つ', 4],
- ['打ち込む', 4]
- ],
- readings: [
- ['だ', 1],
- ['ダース', 1],
- ['うつ', 2],
- ['ぶつ', 2],
- ['うちこむ', 2],
- ['ぶちこむ', 2]
- ]
- }
- },
- {
- inputs: [
- {
- matchType: null,
- termList: ['込む']
- }
- ],
- expectedResults: {
- total: 0,
- terms: [],
- readings: []
- }
- },
- {
- inputs: [
- {
- matchType: 'suffix',
- termList: ['込む']
- }
- ],
- expectedResults: {
- total: 4,
- terms: [
- ['打ち込む', 4]
- ],
- readings: [
- ['うちこむ', 2],
- ['ぶちこむ', 2]
- ]
- }
- },
- {
- inputs: [
- {
- matchType: null,
- termList: []
- }
- ],
- expectedResults: {
- total: 0,
- terms: [],
- readings: []
- }
- }
- ];
-
- for (const {inputs, expectedResults} of data) {
- for (const {termList, matchType} of inputs) {
- const results = await database.findTermsBulk(termList, titles, matchType);
- assert.strictEqual(results.length, expectedResults.total);
- for (const [term, count] of expectedResults.terms) {
- assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count);
- }
- for (const [reading, count] of expectedResults.readings) {
- assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count);
- }
- }
- }
-}
-
-async function testTindTermsExactBulk1(database, titles) {
- const data = [
- {
- inputs: [
- {
- termList: [
- {term: '打', reading: 'だ'},
- {term: '打つ', reading: 'うつ'},
- {term: '打ち込む', reading: 'うちこむ'}
- ]
- }
- ],
- expectedResults: {
- total: 5,
- terms: [
- ['打', 1],
- ['打つ', 2],
- ['打ち込む', 2]
- ],
- readings: [
- ['だ', 1],
- ['うつ', 2],
- ['うちこむ', 2]
- ]
- }
- },
- {
- inputs: [
- {
- termList: [
- {term: '打', reading: 'だ?'},
- {term: '打つ', reading: 'うつ?'},
- {term: '打ち込む', reading: 'うちこむ?'}
- ]
- }
- ],
- expectedResults: {
- total: 0,
- terms: [],
- readings: []
- }
- },
- {
- inputs: [
- {
- termList: [
- {term: '打つ', reading: 'うつ'},
- {term: '打つ', reading: 'ぶつ'}
- ]
- }
- ],
- expectedResults: {
- total: 4,
- terms: [
- ['打つ', 4]
- ],
- readings: [
- ['うつ', 2],
- ['ぶつ', 2]
- ]
- }
- },
- {
- inputs: [
- {
- termList: [
- {term: '打つ', reading: 'うちこむ'}
- ]
- }
- ],
- expectedResults: {
- total: 0,
- terms: [],
- readings: []
- }
- },
- {
- inputs: [
- {
- termList: []
- }
- ],
- expectedResults: {
- total: 0,
- terms: [],
- readings: []
- }
- }
- ];
-
- for (const {inputs, expectedResults} of data) {
- for (const {termList} of inputs) {
- const results = await database.findTermsExactBulk(termList, titles);
- assert.strictEqual(results.length, expectedResults.total);
- for (const [term, count] of expectedResults.terms) {
- assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count);
- }
- for (const [reading, count] of expectedResults.readings) {
- assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count);
- }
- }
- }
-}
-
-async function testFindTermsBySequenceBulk1(database, mainDictionary) {
- const data = [
- {
- inputs: [
- {
- sequenceList: [1, 2, 3, 4, 5]
- }
- ],
- expectedResults: {
- total: 11,
- terms: [
- ['打', 2],
- ['打つ', 4],
- ['打ち込む', 4],
- ['画像', 1]
- ],
- readings: [
- ['だ', 1],
- ['ダース', 1],
- ['うつ', 2],
- ['ぶつ', 2],
- ['うちこむ', 2],
- ['ぶちこむ', 2],
- ['がぞう', 1]
- ]
- }
- },
- {
- inputs: [
- {
- sequenceList: [1]
- }
- ],
- expectedResults: {
- total: 1,
- terms: [
- ['打', 1]
- ],
- readings: [
- ['だ', 1]
- ]
- }
- },
- {
- inputs: [
- {
- sequenceList: [2]
- }
- ],
- expectedResults: {
- total: 1,
- terms: [
- ['打', 1]
- ],
- readings: [
- ['ダース', 1]
- ]
- }
- },
- {
- inputs: [
- {
- sequenceList: [3]
- }
- ],
- expectedResults: {
- total: 4,
- terms: [
- ['打つ', 4]
- ],
- readings: [
- ['うつ', 2],
- ['ぶつ', 2]
- ]
- }
- },
- {
- inputs: [
- {
- sequenceList: [4]
- }
- ],
- expectedResults: {
- total: 4,
- terms: [
- ['打ち込む', 4]
- ],
- readings: [
- ['うちこむ', 2],
- ['ぶちこむ', 2]
- ]
- }
- },
- {
- inputs: [
- {
- sequenceList: [5]
- }
- ],
- expectedResults: {
- total: 1,
- terms: [
- ['画像', 1]
- ],
- readings: [
- ['がぞう', 1]
- ]
- }
- },
- {
- inputs: [
- {
- sequenceList: [-1]
- }
- ],
- expectedResults: {
- total: 0,
- terms: [],
- readings: []
- }
- },
- {
- inputs: [
- {
- sequenceList: []
- }
- ],
- expectedResults: {
- total: 0,
- terms: [],
- readings: []
- }
- }
- ];
-
- for (const {inputs, expectedResults} of data) {
- for (const {sequenceList} of inputs) {
- const results = await database.findTermsBySequenceBulk(sequenceList.map((query) => ({query, dictionary: mainDictionary})));
- assert.strictEqual(results.length, expectedResults.total);
- for (const [term, count] of expectedResults.terms) {
- assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count);
- }
- for (const [reading, count] of expectedResults.readings) {
- assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count);
- }
- }
- }
-}
-
-async function testFindTermMetaBulk1(database, titles) {
- const data = [
- {
- inputs: [
- {
- termList: ['打']
- }
- ],
- expectedResults: {
- total: 11,
- modes: [
- ['freq', 11]
- ]
- }
- },
- {
- inputs: [
- {
- termList: ['打つ']
- }
- ],
- expectedResults: {
- total: 10,
- modes: [
- ['freq', 10]
- ]
- }
- },
- {
- inputs: [
- {
- termList: ['打ち込む']
- }
- ],
- expectedResults: {
- total: 12,
- modes: [
- ['freq', 10],
- ['pitch', 2]
- ]
- }
- },
- {
- inputs: [
- {
- termList: ['?']
- }
- ],
- expectedResults: {
- total: 0,
- modes: []
- }
- }
- ];
-
- for (const {inputs, expectedResults} of data) {
- for (const {termList} of inputs) {
- const results = await database.findTermMetaBulk(termList, titles);
- assert.strictEqual(results.length, expectedResults.total);
- for (const [mode, count] of expectedResults.modes) {
- assert.strictEqual(countMetasWithMode(results, mode), count);
- }
- }
- }
-}
-
-async function testFindKanjiBulk1(database, titles) {
- const data = [
- {
- inputs: [
- {
- kanjiList: ['打']
- }
- ],
- expectedResults: {
- total: 1,
- kanji: [
- ['打', 1]
- ]
- }
- },
- {
- inputs: [
- {
- kanjiList: ['込']
- }
- ],
- expectedResults: {
- total: 1,
- kanji: [
- ['込', 1]
- ]
- }
- },
- {
- inputs: [
- {
- kanjiList: ['?']
- }
- ],
- expectedResults: {
- total: 0,
- kanji: []
- }
- }
- ];
-
- for (const {inputs, expectedResults} of data) {
- for (const {kanjiList} of inputs) {
- const results = await database.findKanjiBulk(kanjiList, titles);
- assert.strictEqual(results.length, expectedResults.total);
- for (const [kanji, count] of expectedResults.kanji) {
- assert.strictEqual(countKanjiWithCharacter(results, kanji), count);
- }
- }
- }
-}
-
-async function testFindKanjiMetaBulk1(database, titles) {
- const data = [
- {
- inputs: [
- {
- kanjiList: ['打']
- }
- ],
- expectedResults: {
- total: 3,
- modes: [
- ['freq', 3]
- ]
- }
- },
- {
- inputs: [
- {
- kanjiList: ['込']
- }
- ],
- expectedResults: {
- total: 3,
- modes: [
- ['freq', 3]
- ]
- }
- },
- {
- inputs: [
- {
- kanjiList: ['?']
- }
- ],
- expectedResults: {
- total: 0,
- modes: []
- }
- }
- ];
-
- for (const {inputs, expectedResults} of data) {
- for (const {kanjiList} of inputs) {
- const results = await database.findKanjiMetaBulk(kanjiList, titles);
- assert.strictEqual(results.length, expectedResults.total);
- for (const [mode, count] of expectedResults.modes) {
- assert.strictEqual(countMetasWithMode(results, mode), count);
- }
- }
- }
-}
-
-async function testFindTagForTitle1(database, title) {
- const data = [
- {
- inputs: [
- {
- name: 'E1'
- }
- ],
- expectedResults: {
- value: {category: 'default', dictionary: title, name: 'E1', notes: 'example tag 1', order: 0, score: 0}
- }
- },
- {
- inputs: [
- {
- name: 'K1'
- }
- ],
- expectedResults: {
- value: {category: 'default', dictionary: title, name: 'K1', notes: 'example kanji tag 1', order: 0, score: 0}
- }
- },
- {
- inputs: [
- {
- name: 'kstat1'
- }
- ],
- expectedResults: {
- value: {category: 'class', dictionary: title, name: 'kstat1', notes: 'kanji stat 1', order: 0, score: 0}
- }
- },
- {
- inputs: [
- {
- name: 'invalid'
- }
- ],
- expectedResults: {
- value: null
- }
- }
- ];
-
- for (const {inputs, expectedResults} of data) {
- for (const {name} of inputs) {
- const result = await database.findTagForTitle(name, title);
- vm.assert.deepStrictEqual(result, expectedResults.value);
- }
- }
-}
-
-
-async function testDatabase2() {
- // Load dictionary data
- const testDictionary = createTestDictionaryArchive('valid-dictionary1');
- const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
- const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
-
- const title = testDictionaryIndex.title;
- const titles = new Map([
- [title, {priority: 0, allowSecondarySearches: false}]
- ]);
-
- // Setup database
- const dictionaryDatabase = new DictionaryDatabase();
-
- // Error: not prepared
- await assert.rejects(async () => await dictionaryDatabase.deleteDictionary(title, 1000));
- await assert.rejects(async () => await dictionaryDatabase.findTermsBulk(['?'], titles, null));
- await assert.rejects(async () => await dictionaryDatabase.findTermsExactBulk([{term: '?', reading: '?'}], titles));
- await assert.rejects(async () => await dictionaryDatabase.findTermsBySequenceBulk([{query: 1, dictionary: title}]));
- await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles));
- await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles));
- await assert.rejects(async () => await dictionaryDatabase.findKanjiBulk(['?'], titles));
- await assert.rejects(async () => await dictionaryDatabase.findKanjiMetaBulk(['?'], titles));
- await assert.rejects(async () => await dictionaryDatabase.findTagForTitle('tag', title));
- await assert.rejects(async () => await dictionaryDatabase.getDictionaryInfo());
- await assert.rejects(async () => await dictionaryDatabase.getDictionaryCounts(titles, true));
- await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {}));
-
- await dictionaryDatabase.prepare();
-
- // Error: already prepared
- await assert.rejects(async () => await dictionaryDatabase.prepare());
-
- await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {});
-
- // Error: dictionary already imported
- await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {}));
-
- await dictionaryDatabase.close();
-}
-
-
-async function testDatabase3() {
- const invalidDictionaries = [
- 'invalid-dictionary1',
- 'invalid-dictionary2',
- 'invalid-dictionary3',
- 'invalid-dictionary4',
- 'invalid-dictionary5',
- 'invalid-dictionary6'
- ];
-
- // Setup database
- const dictionaryDatabase = new DictionaryDatabase();
- await dictionaryDatabase.prepare();
-
- for (const invalidDictionary of invalidDictionaries) {
- const testDictionary = createTestDictionaryArchive(invalidDictionary);
- const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
-
- let error = null;
- try {
- await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {});
- } catch (e) {
- error = e;
- }
-
- if (error === null) {
- assert.ok(false, `Expected an error while importing ${invalidDictionary}`);
- } else {
- const prefix = 'Dictionary has invalid data';
- const message = error.message;
- assert.ok(typeof message, 'string');
- assert.ok(message.startsWith(prefix), `Expected error message to start with '${prefix}': ${message}`);
- }
- }
-
- await dictionaryDatabase.close();
-}
-
-
-async function main() {
- const clearTimeout = 5000;
- try {
- await testDatabase1();
- await clearDatabase(clearTimeout);
-
- await testDatabase2();
- await clearDatabase(clearTimeout);
-
- await testDatabase3();
- await clearDatabase(clearTimeout);
- } catch (e) {
- console.log(e);
- process.exit(-1);
- throw e;
- }
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-document-util.js b/test/test-document-util.js
deleted file mode 100644
index a2458b61..00000000
--- a/test/test-document-util.js
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * 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 {
- 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([
- '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'
- ]);
- const [DOMTextScanner, TextSourceRange, TextSourceElement, DocumentUtil] = vm.get([
- 'DOMTextScanner',
- 'TextSourceRange',
- 'TextSourceElement',
- 'DocumentUtil'
- ]);
-
- try {
- await testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement});
- await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner});
- } finally {
- window.close();
- }
-}
-
-async function testDocumentTextScanningFunctions(dom, {DocumentUtil, 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,
- 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));
-
- startOffset = parseInt(startOffset, 10);
- endOffset = parseInt(endOffset, 10);
- sentenceScanExtent = parseInt(sentenceScanExtent, 10);
- terminateAtNewlines = (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(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 = 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,
- sentenceScanExtent,
- terminateAtNewlines,
- terminatorMap,
- forwardQuoteMap,
- backwardQuoteMap
- ).text;
- assert.strictEqual(sentenceActual, sentence);
-
- // Clean
- source.cleanup();
- }
-}
-
-async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) {
- 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' ?
- new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) :
- new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength)
- );
-
- assert.strictEqual(node, expectedResultNode);
- assert.strictEqual(offset, expectedResultOffset);
- assert.strictEqual(content, expectedResultContent);
- }
-}
-
-
-async function main() {
- await testDocument1();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-hotkey-util.js b/test/test-hotkey-util.js
deleted file mode 100644
index 34f1eb26..00000000
--- a/test/test-hotkey-util.js
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-
-function clone(value) {
- return JSON.parse(JSON.stringify(value));
-}
-
-function createHotkeyUtil() {
- const vm = new VM();
- vm.execute(['js/input/hotkey-util.js']);
- const [HotkeyUtil] = vm.get(['HotkeyUtil']);
- return new HotkeyUtil();
-}
-
-
-function testCommandConversions() {
- const data = [
- {os: 'win', command: 'Alt+F', expectedCommand: 'Alt+F', expectedInput: {key: 'KeyF', modifiers: ['alt']}},
- {os: 'win', command: 'F1', expectedCommand: 'F1', expectedInput: {key: 'F1', modifiers: []}},
-
- {os: 'win', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
- {os: 'win', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
- {os: 'win', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
-
- {os: 'mac', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
- {os: 'mac', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'MacCtrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
- {os: 'mac', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
-
- {os: 'linux', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
- {os: 'linux', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
- {os: 'linux', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}
- ];
-
- const hotkeyUtil = createHotkeyUtil();
- for (const {command, os, expectedInput, expectedCommand} of data) {
- hotkeyUtil.os = os;
- const input = clone(hotkeyUtil.convertCommandToInput(command));
- assert.deepStrictEqual(input, expectedInput);
- const command2 = hotkeyUtil.convertInputToCommand(input.key, input.modifiers);
- assert.deepStrictEqual(command2, expectedCommand);
- }
-}
-
-function testDisplayNames() {
- const data = [
- {os: 'win', key: null, modifiers: [], expected: ''},
- {os: 'win', key: 'KeyF', modifiers: [], expected: 'F'},
- {os: 'win', key: 'F1', modifiers: [], expected: 'F1'},
- {os: 'win', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
- {os: 'win', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
- {os: 'win', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
- {os: 'win', key: null, modifiers: ['alt'], expected: 'Alt'},
- {os: 'win', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
- {os: 'win', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
- {os: 'win', key: null, modifiers: ['shift'], expected: 'Shift'},
- {os: 'win', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
- {os: 'win', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
- {os: 'win', key: null, modifiers: ['meta'], expected: 'Windows'},
- {os: 'win', key: 'KeyF', modifiers: ['meta'], expected: 'Windows + F'},
- {os: 'win', key: 'F1', modifiers: ['meta'], expected: 'Windows + F1'},
- {os: 'win', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
- {os: 'win', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
- {os: 'win', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
-
- {os: 'mac', key: null, modifiers: [], expected: ''},
- {os: 'mac', key: 'KeyF', modifiers: [], expected: 'F'},
- {os: 'mac', key: 'F1', modifiers: [], expected: 'F1'},
- {os: 'mac', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
- {os: 'mac', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
- {os: 'mac', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
- {os: 'mac', key: null, modifiers: ['alt'], expected: 'Opt'},
- {os: 'mac', key: 'KeyF', modifiers: ['alt'], expected: 'Opt + F'},
- {os: 'mac', key: 'F1', modifiers: ['alt'], expected: 'Opt + F1'},
- {os: 'mac', key: null, modifiers: ['shift'], expected: 'Shift'},
- {os: 'mac', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
- {os: 'mac', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
- {os: 'mac', key: null, modifiers: ['meta'], expected: 'Cmd'},
- {os: 'mac', key: 'KeyF', modifiers: ['meta'], expected: 'Cmd + F'},
- {os: 'mac', key: 'F1', modifiers: ['meta'], expected: 'Cmd + F1'},
- {os: 'mac', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
- {os: 'mac', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
- {os: 'mac', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
-
- {os: 'linux', key: null, modifiers: [], expected: ''},
- {os: 'linux', key: 'KeyF', modifiers: [], expected: 'F'},
- {os: 'linux', key: 'F1', modifiers: [], expected: 'F1'},
- {os: 'linux', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
- {os: 'linux', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
- {os: 'linux', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
- {os: 'linux', key: null, modifiers: ['alt'], expected: 'Alt'},
- {os: 'linux', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
- {os: 'linux', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
- {os: 'linux', key: null, modifiers: ['shift'], expected: 'Shift'},
- {os: 'linux', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
- {os: 'linux', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
- {os: 'linux', key: null, modifiers: ['meta'], expected: 'Super'},
- {os: 'linux', key: 'KeyF', modifiers: ['meta'], expected: 'Super + F'},
- {os: 'linux', key: 'F1', modifiers: ['meta'], expected: 'Super + F1'},
- {os: 'linux', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
- {os: 'linux', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
- {os: 'linux', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
-
- {os: 'unknown', key: null, modifiers: [], expected: ''},
- {os: 'unknown', key: 'KeyF', modifiers: [], expected: 'F'},
- {os: 'unknown', key: 'F1', modifiers: [], expected: 'F1'},
- {os: 'unknown', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
- {os: 'unknown', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
- {os: 'unknown', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
- {os: 'unknown', key: null, modifiers: ['alt'], expected: 'Alt'},
- {os: 'unknown', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
- {os: 'unknown', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
- {os: 'unknown', key: null, modifiers: ['shift'], expected: 'Shift'},
- {os: 'unknown', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
- {os: 'unknown', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
- {os: 'unknown', key: null, modifiers: ['meta'], expected: 'Meta'},
- {os: 'unknown', key: 'KeyF', modifiers: ['meta'], expected: 'Meta + F'},
- {os: 'unknown', key: 'F1', modifiers: ['meta'], expected: 'Meta + F1'},
- {os: 'unknown', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
- {os: 'unknown', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
- {os: 'unknown', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}
- ];
-
- const hotkeyUtil = createHotkeyUtil();
- for (const {os, key, modifiers, expected} of data) {
- hotkeyUtil.os = os;
- const displayName = hotkeyUtil.getInputDisplayValue(key, modifiers);
- assert.deepStrictEqual(displayName, expected);
- }
-}
-
-function testSortModifiers() {
- const data = [
- {modifiers: [], expected: []},
- {modifiers: ['shift', 'alt', 'ctrl', 'mouse4', 'meta', 'mouse1', 'mouse0'], expected: ['meta', 'ctrl', 'alt', 'shift', 'mouse0', 'mouse1', 'mouse4']}
- ];
-
- const hotkeyUtil = createHotkeyUtil();
- for (const {modifiers, expected} of data) {
- const modifiers2 = hotkeyUtil.sortModifiers(modifiers);
- assert.strictEqual(modifiers2, modifiers);
- assert.deepStrictEqual(modifiers2, expected);
- }
-}
-
-
-function main() {
- testCommandConversions();
- testDisplayNames();
- testSortModifiers();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-japanese-util.js b/test/test-japanese-util.js
deleted file mode 100644
index 4395a11e..00000000
--- a/test/test-japanese-util.js
+++ /dev/null
@@ -1,881 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-const vm = new VM();
-vm.execute([
- 'lib/wanakana.min.js',
- 'js/language/sandbox/japanese-util.js',
- 'js/general/text-source-map.js'
-]);
-const [JapaneseUtil, TextSourceMap, wanakana] = vm.get(['JapaneseUtil', 'TextSourceMap', 'wanakana']);
-const jp = new JapaneseUtil(wanakana);
-
-
-function testIsCodePointKanji() {
- const data = [
- ['力方', true],
- ['\u53f1\u{20b9f}', true],
- ['かたカタ々kata、。?,.?', false],
- ['逸逸', true]
- ];
-
- for (const [characters, expected] of data) {
- for (const character of characters) {
- const codePoint = character.codePointAt(0);
- const actual = jp.isCodePointKanji(codePoint);
- assert.strictEqual(actual, expected, `isCodePointKanji failed for ${character} (\\u{${codePoint.toString(16)}})`);
- }
- }
-}
-
-function testIsCodePointKana() {
- const data = [
- ['かたカタ', true],
- ['力方々kata、。?,.?', false],
- ['\u53f1\u{20b9f}', false]
- ];
-
- for (const [characters, expected] of data) {
- for (const character of characters) {
- const codePoint = character.codePointAt(0);
- const actual = jp.isCodePointKana(codePoint);
- assert.strictEqual(actual, expected, `isCodePointKana failed for ${character} (\\u{${codePoint.toString(16)}})`);
- }
- }
-}
-
-function testIsCodePointJapanese() {
- const data = [
- ['かたカタ力方々、。?', true],
- ['\u53f1\u{20b9f}', true],
- ['kata,.?', false],
- ['逸逸', true]
- ];
-
- for (const [characters, expected] of data) {
- for (const character of characters) {
- const codePoint = character.codePointAt(0);
- const actual = jp.isCodePointJapanese(codePoint);
- assert.strictEqual(actual, expected, `isCodePointJapanese failed for ${character} (\\u{${codePoint.toString(16)}})`);
- }
- }
-}
-
-function testIsStringEntirelyKana() {
- const data = [
- ['かたかな', true],
- ['カタカナ', true],
- ['ひらがな', true],
- ['ヒラガナ', true],
- ['カタカナひらがな', true],
- ['かたカタ力方々、。?', false],
- ['\u53f1\u{20b9f}', false],
- ['kata,.?', false],
- ['かたカタ力方々、。?invalid', false],
- ['\u53f1\u{20b9f}invalid', false],
- ['kata,.?かた', false]
- ];
-
- for (const [string, expected] of data) {
- assert.strictEqual(jp.isStringEntirelyKana(string), expected);
- }
-}
-
-function testIsStringPartiallyJapanese() {
- const data = [
- ['かたかな', true],
- ['カタカナ', true],
- ['ひらがな', true],
- ['ヒラガナ', true],
- ['カタカナひらがな', true],
- ['かたカタ力方々、。?', true],
- ['\u53f1\u{20b9f}', true],
- ['kata,.?', false],
- ['かたカタ力方々、。?invalid', true],
- ['\u53f1\u{20b9f}invalid', true],
- ['kata,.?かた', true],
- ['逸逸', true]
- ];
-
- for (const [string, expected] of data) {
- assert.strictEqual(jp.isStringPartiallyJapanese(string), expected);
- }
-}
-
-function testConvertKatakanaToHiragana() {
- const data = [
- ['かたかな', 'かたかな'],
- ['ひらがな', 'ひらがな'],
- ['カタカナ', 'かたかな'],
- ['ヒラガナ', 'ひらがな'],
- ['カタカナかたかな', 'かたかなかたかな'],
- ['ヒラガナひらがな', 'ひらがなひらがな'],
- ['chikaraちからチカラ力', 'chikaraちからちから力'],
- ['katakana', 'katakana'],
- ['hiragana', 'hiragana'],
- ['カーナー', 'かあなあ'],
- ['カーナー', 'かーなー', true]
- ];
-
- for (const [string, expected, keepProlongedSoundMarks=false] of data) {
- assert.strictEqual(jp.convertKatakanaToHiragana(string, keepProlongedSoundMarks), expected);
- }
-}
-
-function testConvertHiraganaToKatakana() {
- const data = [
- ['かたかな', 'カタカナ'],
- ['ひらがな', 'ヒラガナ'],
- ['カタカナ', 'カタカナ'],
- ['ヒラガナ', 'ヒラガナ'],
- ['カタカナかたかな', 'カタカナカタカナ'],
- ['ヒラガナひらがな', 'ヒラガナヒラガナ'],
- ['chikaraちからチカラ力', 'chikaraチカラチカラ力'],
- ['katakana', 'katakana'],
- ['hiragana', 'hiragana']
- ];
-
- for (const [string, expected] of data) {
- assert.strictEqual(jp.convertHiraganaToKatakana(string), expected);
- }
-}
-
-function testConvertToRomaji() {
- const data = [
- ['かたかな', 'katakana'],
- ['ひらがな', 'hiragana'],
- ['カタカナ', 'katakana'],
- ['ヒラガナ', 'hiragana'],
- ['カタカナかたかな', 'katakanakatakana'],
- ['ヒラガナひらがな', 'hiraganahiragana'],
- ['chikaraちからチカラ力', 'chikarachikarachikara力'],
- ['katakana', 'katakana'],
- ['hiragana', 'hiragana']
- ];
-
- for (const [string, expected] of data) {
- assert.strictEqual(jp.convertToRomaji(string), expected);
- }
-}
-
-function testConvertNumericToFullWidth() {
- const data = [
- ['0123456789', '0123456789'],
- ['abcdefghij', 'abcdefghij'],
- ['カタカナ', 'カタカナ'],
- ['ひらがな', 'ひらがな']
- ];
-
- for (const [string, expected] of data) {
- assert.strictEqual(jp.convertNumericToFullWidth(string), expected);
- }
-}
-
-function testConvertHalfWidthKanaToFullWidth() {
- const data = [
- ['0123456789', '0123456789'],
- ['abcdefghij', 'abcdefghij'],
- ['カタカナ', 'カタカナ'],
- ['ひらがな', 'ひらがな'],
- ['カキ', 'カキ', [1, 1]],
- ['ガキ', 'ガキ', [2, 1]],
- ['ニホン', 'ニホン', [1, 1, 1]],
- ['ニッポン', 'ニッポン', [1, 1, 2, 1]]
- ];
-
- for (const [string, expected, expectedSourceMapping] of data) {
- const sourceMap = new TextSourceMap(string);
- const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null);
- const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap);
- assert.strictEqual(actual1, expected);
- assert.strictEqual(actual2, expected);
- if (typeof expectedSourceMapping !== 'undefined') {
- assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping)));
- }
- }
-}
-
-function testConvertAlphabeticToKana() {
- const data = [
- ['0123456789', '0123456789'],
- ['abcdefghij', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]],
- ['ABCDEFGHIJ', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], // wanakana.toHiragana converts text to lower case
- ['カタカナ', 'カタカナ'],
- ['ひらがな', 'ひらがな'],
- ['chikara', 'ちから', [3, 2, 2]],
- ['CHIKARA', 'ちから', [3, 2, 2]]
- ];
-
- for (const [string, expected, expectedSourceMapping] of data) {
- const sourceMap = new TextSourceMap(string);
- const actual1 = jp.convertAlphabeticToKana(string, null);
- const actual2 = jp.convertAlphabeticToKana(string, sourceMap);
- assert.strictEqual(actual1, expected);
- assert.strictEqual(actual2, expected);
- if (typeof expectedSourceMapping !== 'undefined') {
- assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping)));
- }
- }
-}
-
-function testDistributeFurigana() {
- const data = [
- [
- ['有り難う', 'ありがとう'],
- [
- {text: '有', reading: 'あ'},
- {text: 'り', reading: ''},
- {text: '難', reading: 'がと'},
- {text: 'う', reading: ''}
- ]
- ],
- [
- ['方々', 'かたがた'],
- [
- {text: '方々', reading: 'かたがた'}
- ]
- ],
- [
- ['お祝い', 'おいわい'],
- [
- {text: 'お', reading: ''},
- {text: '祝', reading: 'いわ'},
- {text: 'い', reading: ''}
- ]
- ],
- [
- ['美味しい', 'おいしい'],
- [
- {text: '美味', reading: 'おい'},
- {text: 'しい', reading: ''}
- ]
- ],
- [
- ['食べ物', 'たべもの'],
- [
- {text: '食', reading: 'た'},
- {text: 'べ', reading: ''},
- {text: '物', reading: 'もの'}
- ]
- ],
- [
- ['試し切り', 'ためしぎり'],
- [
- {text: '試', reading: 'ため'},
- {text: 'し', reading: ''},
- {text: '切', reading: 'ぎ'},
- {text: 'り', reading: ''}
- ]
- ],
- // Ambiguous
- [
- ['飼い犬', 'かいいぬ'],
- [
- {text: '飼い犬', reading: 'かいいぬ'}
- ]
- ],
- [
- ['長い間', 'ながいあいだ'],
- [
- {text: '長い間', reading: 'ながいあいだ'}
- ]
- ],
- // Same/empty reading
- [
- ['飼い犬', ''],
- [
- {text: '飼い犬', reading: ''}
- ]
- ],
- [
- ['かいいぬ', 'かいいぬ'],
- [
- {text: 'かいいぬ', reading: ''}
- ]
- ],
- [
- ['かいぬ', 'かいぬ'],
- [
- {text: 'かいぬ', reading: ''}
- ]
- ],
- // Misc
- [
- ['月', 'か'],
- [
- {text: '月', reading: 'か'}
- ]
- ],
- [
- ['月', 'カ'],
- [
- {text: '月', reading: 'カ'}
- ]
- ],
- // Mismatched kana readings
- [
- ['有り難う', 'アリガトウ'],
- [
- {text: '有', reading: 'ア'},
- {text: 'り', reading: 'リ'},
- {text: '難', reading: 'ガト'},
- {text: 'う', reading: 'ウ'}
- ]
- ],
- [
- ['ありがとう', 'アリガトウ'],
- [
- {text: 'ありがとう', reading: 'アリガトウ'}
- ]
- ],
- // Mismatched kana readings (real examples)
- [
- ['カ月', 'かげつ'],
- [
- {text: 'カ', reading: 'か'},
- {text: '月', reading: 'げつ'}
- ]
- ],
- [
- ['序ノ口', 'じょのくち'],
- [
- {text: '序', reading: 'じょ'},
- {text: 'ノ', reading: 'の'},
- {text: '口', reading: 'くち'}
- ]
- ],
- [
- ['スズメの涙', 'すずめのなみだ'],
- [
- {text: 'スズメ', reading: 'すずめ'},
- {text: 'の', reading: ''},
- {text: '涙', reading: 'なみだ'}
- ]
- ],
- [
- ['二カ所', 'にかしょ'],
- [
- {text: '二', reading: 'に'},
- {text: 'カ', reading: 'か'},
- {text: '所', reading: 'しょ'}
- ]
- ],
- [
- ['八ツ橋', 'やつはし'],
- [
- {text: '八', reading: 'や'},
- {text: 'ツ', reading: 'つ'},
- {text: '橋', reading: 'はし'}
- ]
- ],
- [
- ['八ツ橋', 'やつはし'],
- [
- {text: '八', reading: 'や'},
- {text: 'ツ', reading: 'つ'},
- {text: '橋', reading: 'はし'}
- ]
- ],
- [
- ['一カ月', 'いっかげつ'],
- [
- {text: '一', reading: 'いっ'},
- {text: 'カ', reading: 'か'},
- {text: '月', reading: 'げつ'}
- ]
- ],
- [
- ['一カ所', 'いっかしょ'],
- [
- {text: '一', reading: 'いっ'},
- {text: 'カ', reading: 'か'},
- {text: '所', reading: 'しょ'}
- ]
- ],
- [
- ['カ所', 'かしょ'],
- [
- {text: 'カ', reading: 'か'},
- {text: '所', reading: 'しょ'}
- ]
- ],
- [
- ['数カ月', 'すうかげつ'],
- [
- {text: '数', reading: 'すう'},
- {text: 'カ', reading: 'か'},
- {text: '月', reading: 'げつ'}
- ]
- ],
- [
- ['くノ一', 'くのいち'],
- [
- {text: 'く', reading: ''},
- {text: 'ノ', reading: 'の'},
- {text: '一', reading: 'いち'}
- ]
- ],
- [
- ['くノ一', 'くのいち'],
- [
- {text: 'く', reading: ''},
- {text: 'ノ', reading: 'の'},
- {text: '一', reading: 'いち'}
- ]
- ],
- [
- ['数カ国', 'すうかこく'],
- [
- {text: '数', reading: 'すう'},
- {text: 'カ', reading: 'か'},
- {text: '国', reading: 'こく'}
- ]
- ],
- [
- ['数カ所', 'すうかしょ'],
- [
- {text: '数', reading: 'すう'},
- {text: 'カ', reading: 'か'},
- {text: '所', reading: 'しょ'}
- ]
- ],
- [
- ['壇ノ浦の戦い', 'だんのうらのたたかい'],
- [
- {text: '壇', reading: 'だん'},
- {text: 'ノ', reading: 'の'},
- {text: '浦', reading: 'うら'},
- {text: 'の', reading: ''},
- {text: '戦', reading: 'たたか'},
- {text: 'い', reading: ''}
- ]
- ],
- [
- ['壇ノ浦の戦', 'だんのうらのたたかい'],
- [
- {text: '壇', reading: 'だん'},
- {text: 'ノ', reading: 'の'},
- {text: '浦', reading: 'うら'},
- {text: 'の', reading: ''},
- {text: '戦', reading: 'たたかい'}
- ]
- ],
- [
- ['序ノ口格', 'じょのくちかく'],
- [
- {text: '序', reading: 'じょ'},
- {text: 'ノ', reading: 'の'},
- {text: '口格', reading: 'くちかく'}
- ]
- ],
- [
- ['二カ国語', 'にかこくご'],
- [
- {text: '二', reading: 'に'},
- {text: 'カ', reading: 'か'},
- {text: '国語', reading: 'こくご'}
- ]
- ],
- [
- ['カ国', 'かこく'],
- [
- {text: 'カ', reading: 'か'},
- {text: '国', reading: 'こく'}
- ]
- ],
- [
- ['カ国語', 'かこくご'],
- [
- {text: 'カ', reading: 'か'},
- {text: '国語', reading: 'こくご'}
- ]
- ],
- [
- ['壇ノ浦の合戦', 'だんのうらのかっせん'],
- [
- {text: '壇', reading: 'だん'},
- {text: 'ノ', reading: 'の'},
- {text: '浦', reading: 'うら'},
- {text: 'の', reading: ''},
- {text: '合戦', reading: 'かっせん'}
- ]
- ],
- [
- ['一タ偏', 'いちたへん'],
- [
- {text: '一', reading: 'いち'},
- {text: 'タ', reading: 'た'},
- {text: '偏', reading: 'へん'}
- ]
- ],
- [
- ['ル又', 'るまた'],
- [
- {text: 'ル', reading: 'る'},
- {text: '又', reading: 'また'}
- ]
- ],
- [
- ['ノ木偏', 'のぎへん'],
- [
- {text: 'ノ', reading: 'の'},
- {text: '木偏', reading: 'ぎへん'}
- ]
- ],
- [
- ['一ノ貝', 'いちのかい'],
- [
- {text: '一', reading: 'いち'},
- {text: 'ノ', reading: 'の'},
- {text: '貝', reading: 'かい'}
- ]
- ],
- [
- ['虎ノ門事件', 'とらのもんじけん'],
- [
- {text: '虎', reading: 'とら'},
- {text: 'ノ', reading: 'の'},
- {text: '門事件', reading: 'もんじけん'}
- ]
- ],
- [
- ['教育ニ関スル勅語', 'きょういくにかんするちょくご'],
- [
- {text: '教育', reading: 'きょういく'},
- {text: 'ニ', reading: 'に'},
- {text: '関', reading: 'かん'},
- {text: 'スル', reading: 'する'},
- {text: '勅語', reading: 'ちょくご'}
- ]
- ],
- [
- ['二カ年', 'にかねん'],
- [
- {text: '二', reading: 'に'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['三カ年', 'さんかねん'],
- [
- {text: '三', reading: 'さん'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['四カ年', 'よんかねん'],
- [
- {text: '四', reading: 'よん'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['五カ年', 'ごかねん'],
- [
- {text: '五', reading: 'ご'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['六カ年', 'ろっかねん'],
- [
- {text: '六', reading: 'ろっ'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['七カ年', 'ななかねん'],
- [
- {text: '七', reading: 'なな'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['八カ年', 'はちかねん'],
- [
- {text: '八', reading: 'はち'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['九カ年', 'きゅうかねん'],
- [
- {text: '九', reading: 'きゅう'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['十カ年', 'じゅうかねん'],
- [
- {text: '十', reading: 'じゅう'},
- {text: 'カ', reading: 'か'},
- {text: '年', reading: 'ねん'}
- ]
- ],
- [
- ['鏡ノ間', 'かがみのま'],
- [
- {text: '鏡', reading: 'かがみ'},
- {text: 'ノ', reading: 'の'},
- {text: '間', reading: 'ま'}
- ]
- ],
- [
- ['鏡ノ間', 'かがみのま'],
- [
- {text: '鏡', reading: 'かがみ'},
- {text: 'ノ', reading: 'の'},
- {text: '間', reading: 'ま'}
- ]
- ],
- [
- ['ページ違反', 'ぺーじいはん'],
- [
- {text: 'ペ', reading: 'ぺ'},
- {text: 'ー', reading: ''},
- {text: 'ジ', reading: 'じ'},
- {text: '違反', reading: 'いはん'}
- ]
- ],
- // Mismatched kana
- [
- ['サボる', 'サボル'],
- [
- {text: 'サボ', reading: ''},
- {text: 'る', reading: 'ル'}
- ]
- ],
- // Reading starts with term, but has remainder characters
- [
- ['シック', 'シック・ビルしょうこうぐん'],
- [
- {text: 'シック', reading: 'シック・ビルしょうこうぐん'}
- ]
- ],
- // Kanji distribution tests
- [
- ['逸らす', 'そらす'],
- [
- {text: '逸', reading: 'そ'},
- {text: 'らす', reading: ''}
- ]
- ],
- [
- ['逸らす', 'そらす'],
- [
- {text: '逸', reading: 'そ'},
- {text: 'らす', reading: ''}
- ]
- ]
- ];
-
- for (const [[term, reading], expected] of data) {
- const actual = jp.distributeFurigana(term, reading);
- vm.assert.deepStrictEqual(actual, expected);
- }
-}
-
-function testDistributeFuriganaInflected() {
- const data = [
- [
- ['美味しい', 'おいしい', '美味しかた'],
- [
- {text: '美味', reading: 'おい'},
- {text: 'しかた', reading: ''}
- ]
- ],
- [
- ['食べる', 'たべる', '食べた'],
- [
- {text: '食', reading: 'た'},
- {text: 'べた', reading: ''}
- ]
- ],
- [
- ['迄に', 'までに', 'までに'],
- [
- {text: 'までに', reading: ''}
- ]
- ],
- [
- ['行う', 'おこなう', 'おこなわなかった'],
- [
- {text: 'おこなわなかった', reading: ''}
- ]
- ],
- [
- ['いい', 'いい', 'イイ'],
- [
- {text: 'イイ', reading: ''}
- ]
- ],
- [
- ['否か', 'いなか', '否カ'],
- [
- {text: '否', reading: 'いな'},
- {text: 'カ', reading: 'か'}
- ]
- ]
- ];
-
- for (const [[term, reading, source], expected] of data) {
- const actual = jp.distributeFuriganaInflected(term, reading, source);
- vm.assert.deepStrictEqual(actual, expected);
- }
-}
-
-function testCollapseEmphaticSequences() {
- const data = [
- [['かこい', false], ['かこい', [1, 1, 1]]],
- [['かこい', true], ['かこい', [1, 1, 1]]],
- [['かっこい', false], ['かっこい', [1, 1, 1, 1]]],
- [['かっこい', true], ['かこい', [2, 1, 1]]],
- [['かっっこい', false], ['かっこい', [1, 2, 1, 1]]],
- [['かっっこい', true], ['かこい', [3, 1, 1]]],
- [['かっっっこい', false], ['かっこい', [1, 3, 1, 1]]],
- [['かっっっこい', true], ['かこい', [4, 1, 1]]],
-
- [['こい', false], ['こい', [1, 1]]],
- [['こい', true], ['こい', [1, 1]]],
- [['っこい', false], ['っこい', [1, 1, 1]]],
- [['っこい', true], ['こい', [2, 1]]],
- [['っっこい', false], ['っこい', [2, 1, 1]]],
- [['っっこい', true], ['こい', [3, 1]]],
- [['っっっこい', false], ['っこい', [3, 1, 1]]],
- [['っっっこい', true], ['こい', [4, 1]]],
-
- [['すごい', false], ['すごい', [1, 1, 1]]],
- [['すごい', true], ['すごい', [1, 1, 1]]],
- [['すごーい', false], ['すごーい', [1, 1, 1, 1]]],
- [['すごーい', true], ['すごい', [1, 2, 1]]],
- [['すごーーい', false], ['すごーい', [1, 1, 2, 1]]],
- [['すごーーい', true], ['すごい', [1, 3, 1]]],
- [['すっごーい', false], ['すっごーい', [1, 1, 1, 1, 1]]],
- [['すっごーい', true], ['すごい', [2, 2, 1]]],
- [['すっっごーーい', false], ['すっごーい', [1, 2, 1, 2, 1]]],
- [['すっっごーーい', true], ['すごい', [3, 3, 1]]],
-
- [['', false], ['', []]],
- [['', true], ['', []]],
- [['っ', false], ['っ', [1]]],
- [['っ', true], ['', [1]]],
- [['っっ', false], ['っ', [2]]],
- [['っっ', true], ['', [2]]],
- [['っっっ', false], ['っ', [3]]],
- [['っっっ', true], ['', [3]]]
- ];
-
- for (const [[text, fullCollapse], [expected, expectedSourceMapping]] of data) {
- const sourceMap = new TextSourceMap(text);
- const actual1 = jp.collapseEmphaticSequences(text, fullCollapse, null);
- const actual2 = jp.collapseEmphaticSequences(text, fullCollapse, sourceMap);
- assert.strictEqual(actual1, expected);
- assert.strictEqual(actual2, expected);
- if (typeof expectedSourceMapping !== 'undefined') {
- assert.ok(sourceMap.equals(new TextSourceMap(text, expectedSourceMapping)));
- }
- }
-}
-
-function testIsMoraPitchHigh() {
- const data = [
- [[0, 0], false],
- [[1, 0], true],
- [[2, 0], true],
- [[3, 0], true],
-
- [[0, 1], true],
- [[1, 1], false],
- [[2, 1], false],
- [[3, 1], false],
-
- [[0, 2], false],
- [[1, 2], true],
- [[2, 2], false],
- [[3, 2], false],
-
- [[0, 3], false],
- [[1, 3], true],
- [[2, 3], true],
- [[3, 3], false],
-
- [[0, 4], false],
- [[1, 4], true],
- [[2, 4], true],
- [[3, 4], true]
- ];
-
- for (const [[moraIndex, pitchAccentDownstepPosition], expected] of data) {
- const actual = jp.isMoraPitchHigh(moraIndex, pitchAccentDownstepPosition);
- assert.strictEqual(actual, expected);
- }
-}
-
-function testGetKanaMorae() {
- const data = [
- ['かこ', ['か', 'こ']],
- ['かっこ', ['か', 'っ', 'こ']],
- ['カコ', ['カ', 'コ']],
- ['カッコ', ['カ', 'ッ', 'コ']],
- ['コート', ['コ', 'ー', 'ト']],
- ['ちゃんと', ['ちゃ', 'ん', 'と']],
- ['とうきょう', ['と', 'う', 'きょ', 'う']],
- ['ぎゅう', ['ぎゅ', 'う']],
- ['ディスコ', ['ディ', 'ス', 'コ']]
- ];
-
- for (const [text, expected] of data) {
- const actual = jp.getKanaMorae(text);
- vm.assert.deepStrictEqual(actual, expected);
- }
-}
-
-
-function main() {
- testIsCodePointKanji();
- testIsCodePointKana();
- testIsCodePointJapanese();
- testIsStringEntirelyKana();
- testIsStringPartiallyJapanese();
- testConvertKatakanaToHiragana();
- testConvertHiraganaToKatakana();
- testConvertToRomaji();
- testConvertNumericToFullWidth();
- testConvertHalfWidthKanaToFullWidth();
- testConvertAlphabeticToKana();
- testDistributeFurigana();
- testDistributeFuriganaInflected();
- testCollapseEmphaticSequences();
- testIsMoraPitchHigh();
- testGetKanaMorae();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-json-schema.js b/test/test-json-schema.js
deleted file mode 100644
index 35ecc5e9..00000000
--- a/test/test-json-schema.js
+++ /dev/null
@@ -1,1011 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-const vm = new VM();
-vm.execute([
- 'js/core.js',
- 'js/general/cache-map.js',
- 'js/data/json-schema.js'
-]);
-const JsonSchema = vm.get('JsonSchema');
-
-
-function schemaValidate(schema, value) {
- return new JsonSchema(schema).isValid(value);
-}
-
-function getValidValueOrDefault(schema, value) {
- return new JsonSchema(schema).getValidValueOrDefault(value);
-}
-
-function createProxy(schema, value) {
- return new JsonSchema(schema).createProxy(value);
-}
-
-function clone(value) {
- return JSON.parse(JSON.stringify(value));
-}
-
-
-function testValidate1() {
- const schema = {
- allOf: [
- {
- type: 'number'
- },
- {
- anyOf: [
- {minimum: 10, maximum: 100},
- {minimum: -100, maximum: -10}
- ]
- },
- {
- oneOf: [
- {multipleOf: 3},
- {multipleOf: 5}
- ]
- },
- {
- not: [
- {multipleOf: 20}
- ]
- }
- ]
- };
-
- const jsValidate = (value) => {
- return (
- typeof value === 'number' &&
- (
- (value >= 10 && value <= 100) ||
- (value >= -100 && value <= -10)
- ) &&
- (
- (
- (value % 3) === 0 ||
- (value % 5) === 0
- ) &&
- (value % 15) !== 0
- ) &&
- (value % 20) !== 0
- );
- };
-
- for (let i = -111; i <= 111; i++) {
- const actual = schemaValidate(schema, i);
- const expected = jsValidate(i);
- assert.strictEqual(actual, expected);
- }
-}
-
-function testValidate2() {
- const data = [
- // String tests
- {
- schema: {
- type: 'string'
- },
- inputs: [
- {expected: false, value: null},
- {expected: false, value: void 0},
- {expected: false, value: 0},
- {expected: false, value: {}},
- {expected: false, value: []},
- {expected: true, value: ''}
- ]
- },
- {
- schema: {
- type: 'string',
- minLength: 2
- },
- inputs: [
- {expected: false, value: ''},
- {expected: false, value: '1'},
- {expected: true, value: '12'},
- {expected: true, value: '123'}
- ]
- },
- {
- schema: {
- type: 'string',
- maxLength: 2
- },
- inputs: [
- {expected: true, value: ''},
- {expected: true, value: '1'},
- {expected: true, value: '12'},
- {expected: false, value: '123'}
- ]
- },
- {
- schema: {
- type: 'string',
- pattern: 'test'
- },
- inputs: [
- {expected: false, value: ''},
- {expected: true, value: 'test'},
- {expected: false, value: 'TEST'},
- {expected: true, value: 'ABCtestDEF'},
- {expected: false, value: 'ABCTESTDEF'}
- ]
- },
- {
- schema: {
- type: 'string',
- pattern: '^test$'
- },
- inputs: [
- {expected: false, value: ''},
- {expected: true, value: 'test'},
- {expected: false, value: 'TEST'},
- {expected: false, value: 'ABCtestDEF'},
- {expected: false, value: 'ABCTESTDEF'}
- ]
- },
- {
- schema: {
- type: 'string',
- pattern: '^test$',
- patternFlags: 'i'
- },
- inputs: [
- {expected: false, value: ''},
- {expected: true, value: 'test'},
- {expected: true, value: 'TEST'},
- {expected: false, value: 'ABCtestDEF'},
- {expected: false, value: 'ABCTESTDEF'}
- ]
- },
- {
- schema: {
- type: 'string',
- pattern: '*'
- },
- inputs: [
- {expected: false, value: ''}
- ]
- },
- {
- schema: {
- type: 'string',
- pattern: '.',
- patternFlags: '?'
- },
- inputs: [
- {expected: false, value: ''}
- ]
- },
-
- // Const tests
- {
- schema: {
- const: 32
- },
- inputs: [
- {expected: true, value: 32},
- {expected: false, value: 0},
- {expected: false, value: '32'},
- {expected: false, value: null},
- {expected: false, value: {a: 'b'}},
- {expected: false, value: [1, 2, 3]}
- ]
- },
- {
- schema: {
- const: '32'
- },
- inputs: [
- {expected: false, value: 32},
- {expected: false, value: 0},
- {expected: true, value: '32'},
- {expected: false, value: null},
- {expected: false, value: {a: 'b'}},
- {expected: false, value: [1, 2, 3]}
- ]
- },
- {
- schema: {
- const: null
- },
- inputs: [
- {expected: false, value: 32},
- {expected: false, value: 0},
- {expected: false, value: '32'},
- {expected: true, value: null},
- {expected: false, value: {a: 'b'}},
- {expected: false, value: [1, 2, 3]}
- ]
- },
- {
- schema: {
- const: {a: 'b'}
- },
- inputs: [
- {expected: false, value: 32},
- {expected: false, value: 0},
- {expected: false, value: '32'},
- {expected: false, value: null},
- {expected: false, value: {a: 'b'}},
- {expected: false, value: [1, 2, 3]}
- ]
- },
- {
- schema: {
- const: [1, 2, 3]
- },
- inputs: [
- {expected: false, value: 32},
- {expected: false, value: 0},
- {expected: false, value: '32'},
- {expected: false, value: null},
- {expected: false, value: {a: 'b'}},
- {expected: false, value: [1, 2, 3]}
- ]
- },
-
- // Array contains tests
- {
- schema: {
- type: 'array',
- contains: {const: 32}
- },
- inputs: [
- {expected: false, value: []},
- {expected: true, value: [32]},
- {expected: true, value: [1, 32]},
- {expected: true, value: [1, 32, 1]},
- {expected: false, value: [33]},
- {expected: false, value: [1, 33]},
- {expected: false, value: [1, 33, 1]}
- ]
- },
-
- // Number limits tests
- {
- schema: {
- type: 'number',
- minimum: 0
- },
- inputs: [
- {expected: false, value: -1},
- {expected: true, value: 0},
- {expected: true, value: 1}
- ]
- },
- {
- schema: {
- type: 'number',
- exclusiveMinimum: 0
- },
- inputs: [
- {expected: false, value: -1},
- {expected: false, value: 0},
- {expected: true, value: 1}
- ]
- },
- {
- schema: {
- type: 'number',
- maximum: 0
- },
- inputs: [
- {expected: true, value: -1},
- {expected: true, value: 0},
- {expected: false, value: 1}
- ]
- },
- {
- schema: {
- type: 'number',
- exclusiveMaximum: 0
- },
- inputs: [
- {expected: true, value: -1},
- {expected: false, value: 0},
- {expected: false, value: 1}
- ]
- },
-
- // Integer limits tests
- {
- schema: {
- type: 'integer',
- minimum: 0
- },
- inputs: [
- {expected: false, value: -1},
- {expected: true, value: 0},
- {expected: true, value: 1}
- ]
- },
- {
- schema: {
- type: 'integer',
- exclusiveMinimum: 0
- },
- inputs: [
- {expected: false, value: -1},
- {expected: false, value: 0},
- {expected: true, value: 1}
- ]
- },
- {
- schema: {
- type: 'integer',
- maximum: 0
- },
- inputs: [
- {expected: true, value: -1},
- {expected: true, value: 0},
- {expected: false, value: 1}
- ]
- },
- {
- schema: {
- type: 'integer',
- exclusiveMaximum: 0
- },
- inputs: [
- {expected: true, value: -1},
- {expected: false, value: 0},
- {expected: false, value: 1}
- ]
- },
- {
- schema: {
- type: 'integer',
- multipleOf: 2
- },
- inputs: [
- {expected: true, value: -2},
- {expected: false, value: -1},
- {expected: true, value: 0},
- {expected: false, value: 1},
- {expected: true, value: 2}
- ]
- },
-
- // Numeric type tests
- {
- schema: {
- type: 'number'
- },
- inputs: [
- {expected: true, value: 0},
- {expected: true, value: 0.5},
- {expected: true, value: 1},
- {expected: false, value: '0'},
- {expected: false, value: null},
- {expected: false, value: []},
- {expected: false, value: {}}
- ]
- },
- {
- schema: {
- type: 'integer'
- },
- inputs: [
- {expected: true, value: 0},
- {expected: false, value: 0.5},
- {expected: true, value: 1},
- {expected: false, value: '0'},
- {expected: false, value: null},
- {expected: false, value: []},
- {expected: false, value: {}}
- ]
- },
-
- // Reference tests
- {
- schema: {
- definitions: {
- example: {
- type: 'number'
- }
- },
- $ref: '#/definitions/example'
- },
- inputs: [
- {expected: true, value: 0},
- {expected: true, value: 0.5},
- {expected: true, value: 1},
- {expected: false, value: '0'},
- {expected: false, value: null},
- {expected: false, value: []},
- {expected: false, value: {}}
- ]
- },
- {
- schema: {
- definitions: {
- example: {
- type: 'integer'
- }
- },
- $ref: '#/definitions/example'
- },
- inputs: [
- {expected: true, value: 0},
- {expected: false, value: 0.5},
- {expected: true, value: 1},
- {expected: false, value: '0'},
- {expected: false, value: null},
- {expected: false, value: []},
- {expected: false, value: {}}
- ]
- },
- {
- schema: {
- definitions: {
- example: {
- type: 'object',
- additionalProperties: false,
- properties: {
- test: {
- $ref: '#/definitions/example'
- }
- }
- }
- },
- $ref: '#/definitions/example'
- },
- inputs: [
- {expected: false, value: 0},
- {expected: false, value: 0.5},
- {expected: false, value: 1},
- {expected: false, value: '0'},
- {expected: false, value: null},
- {expected: false, value: []},
- {expected: true, value: {}},
- {expected: false, value: {test: 0}},
- {expected: false, value: {test: 0.5}},
- {expected: false, value: {test: 1}},
- {expected: false, value: {test: '0'}},
- {expected: false, value: {test: null}},
- {expected: false, value: {test: []}},
- {expected: true, value: {test: {}}},
- {expected: true, value: {test: {test: {}}}},
- {expected: true, value: {test: {test: {test: {}}}}}
- ]
- }
- ];
-
- for (const {schema, inputs} of data) {
- for (const {expected, value} of inputs) {
- const actual = schemaValidate(schema, value);
- assert.strictEqual(actual, expected);
- }
- }
-}
-
-
-function testGetValidValueOrDefault1() {
- const data = [
- // Test value defaulting on objects with additionalProperties=false
- {
- schema: {
- type: 'object',
- required: ['test'],
- properties: {
- test: {
- type: 'string',
- default: 'default'
- }
- },
- additionalProperties: false
- },
- inputs: [
- [
- void 0,
- {test: 'default'}
- ],
- [
- null,
- {test: 'default'}
- ],
- [
- 0,
- {test: 'default'}
- ],
- [
- '',
- {test: 'default'}
- ],
- [
- [],
- {test: 'default'}
- ],
- [
- {},
- {test: 'default'}
- ],
- [
- {test: 'value'},
- {test: 'value'}
- ],
- [
- {test2: 'value2'},
- {test: 'default'}
- ],
- [
- {test: 'value', test2: 'value2'},
- {test: 'value'}
- ]
- ]
- },
-
- // Test value defaulting on objects with additionalProperties=true
- {
- schema: {
- type: 'object',
- required: ['test'],
- properties: {
- test: {
- type: 'string',
- default: 'default'
- }
- },
- additionalProperties: true
- },
- inputs: [
- [
- {},
- {test: 'default'}
- ],
- [
- {test: 'value'},
- {test: 'value'}
- ],
- [
- {test2: 'value2'},
- {test: 'default', test2: 'value2'}
- ],
- [
- {test: 'value', test2: 'value2'},
- {test: 'value', test2: 'value2'}
- ]
- ]
- },
-
- // Test value defaulting on objects with additionalProperties={schema}
- {
- schema: {
- type: 'object',
- required: ['test'],
- properties: {
- test: {
- type: 'string',
- default: 'default'
- }
- },
- additionalProperties: {
- type: 'number',
- default: 10
- }
- },
- inputs: [
- [
- {},
- {test: 'default'}
- ],
- [
- {test: 'value'},
- {test: 'value'}
- ],
- [
- {test2: 'value2'},
- {test: 'default', test2: 10}
- ],
- [
- {test: 'value', test2: 'value2'},
- {test: 'value', test2: 10}
- ],
- [
- {test2: 2},
- {test: 'default', test2: 2}
- ],
- [
- {test: 'value', test2: 2},
- {test: 'value', test2: 2}
- ],
- [
- {test: 'value', test2: 2, test3: null},
- {test: 'value', test2: 2, test3: 10}
- ],
- [
- {test: 'value', test2: 2, test3: void 0},
- {test: 'value', test2: 2, test3: 10}
- ]
- ]
- },
-
- // Test value defaulting where hasOwnProperty is false
- {
- schema: {
- type: 'object',
- required: ['test'],
- properties: {
- test: {
- type: 'string',
- default: 'default'
- }
- }
- },
- inputs: [
- [
- {},
- {test: 'default'}
- ],
- [
- {test: 'value'},
- {test: 'value'}
- ],
- [
- Object.create({test: 'value'}),
- {test: 'default'}
- ]
- ]
- },
- {
- schema: {
- type: 'object',
- required: ['toString'],
- properties: {
- toString: {
- type: 'string',
- default: 'default'
- }
- }
- },
- inputs: [
- [
- {},
- {toString: 'default'}
- ],
- [
- {toString: 'value'},
- {toString: 'value'}
- ],
- [
- Object.create({toString: 'value'}),
- {toString: 'default'}
- ]
- ]
- },
-
- // Test enum
- {
- schema: {
- type: 'object',
- required: ['test'],
- properties: {
- test: {
- type: 'string',
- default: 'value1',
- enum: ['value1', 'value2', 'value3']
- }
- }
- },
- inputs: [
- [
- {test: 'value1'},
- {test: 'value1'}
- ],
- [
- {test: 'value2'},
- {test: 'value2'}
- ],
- [
- {test: 'value3'},
- {test: 'value3'}
- ],
- [
- {test: 'value4'},
- {test: 'value1'}
- ]
- ]
- },
-
- // Test valid vs invalid default
- {
- schema: {
- type: 'object',
- required: ['test'],
- properties: {
- test: {
- type: 'integer',
- default: 2,
- minimum: 1
- }
- }
- },
- inputs: [
- [
- {test: -1},
- {test: 2}
- ]
- ]
- },
- {
- schema: {
- type: 'object',
- required: ['test'],
- properties: {
- test: {
- type: 'integer',
- default: 1,
- minimum: 2
- }
- }
- },
- inputs: [
- [
- {test: -1},
- {test: -1}
- ]
- ]
- },
-
- // Test references
- {
- schema: {
- definitions: {
- example: {
- type: 'number',
- default: 0
- }
- },
- $ref: '#/definitions/example'
- },
- inputs: [
- [
- 1,
- 1
- ],
- [
- null,
- 0
- ],
- [
- 'test',
- 0
- ],
- [
- {test: 'value'},
- 0
- ]
- ]
- },
- {
- schema: {
- definitions: {
- example: {
- type: 'object',
- additionalProperties: false,
- properties: {
- test: {
- $ref: '#/definitions/example'
- }
- }
- }
- },
- $ref: '#/definitions/example'
- },
- inputs: [
- [
- 1,
- {}
- ],
- [
- null,
- {}
- ],
- [
- 'test',
- {}
- ],
- [
- {},
- {}
- ],
- [
- {test: {}},
- {test: {}}
- ],
- [
- {test: 'value'},
- {test: {}}
- ],
- [
- {test: {test: {}}},
- {test: {test: {}}}
- ]
- ]
- }
- ];
-
- for (const {schema, inputs} of data) {
- for (const [value, expected] of inputs) {
- const actual = getValidValueOrDefault(schema, value);
- vm.assert.deepStrictEqual(actual, expected);
- }
- }
-}
-
-
-function testProxy1() {
- const data = [
- // Object tests
- {
- schema: {
- type: 'object',
- required: ['test'],
- additionalProperties: false,
- properties: {
- test: {
- type: 'string',
- default: 'default'
- }
- }
- },
- tests: [
- {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
- {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
- {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
- {error: true, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
- {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
- ]
- },
- {
- schema: {
- type: 'object',
- required: ['test'],
- additionalProperties: true,
- properties: {
- test: {
- type: 'string',
- default: 'default'
- }
- }
- },
- tests: [
- {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
- {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
- {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
- {error: false, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
- {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
- ]
- },
- {
- schema: {
- type: 'object',
- required: ['test1'],
- additionalProperties: false,
- properties: {
- test1: {
- type: 'object',
- required: ['test2'],
- additionalProperties: false,
- properties: {
- test2: {
- type: 'object',
- required: ['test3'],
- additionalProperties: false,
- properties: {
- test3: {
- type: 'string',
- default: 'default'
- }
- }
- }
- }
- }
- }
- },
- tests: [
- {error: false, action: (value) => { value.test1.test2.test3 = 'string'; }},
- {error: true, action: (value) => { value.test1.test2.test3 = null; }},
- {error: true, action: (value) => { delete value.test1.test2.test3; }},
- {error: true, action: (value) => { value.test1.test2 = null; }},
- {error: true, action: (value) => { value.test1 = null; }},
- {error: true, action: (value) => { value.test4 = 'string'; }},
- {error: false, action: (value) => { delete value.test4; }}
- ]
- },
-
- // Array tests
- {
- schema: {
- type: 'array',
- items: {
- type: 'string',
- default: 'default'
- }
- },
- tests: [
- {error: false, value: ['default'], action: (value) => { value[0] = 'string'; }},
- {error: true, value: ['default'], action: (value) => { value[0] = null; }},
- {error: false, value: ['default'], action: (value) => { delete value[0]; }},
- {error: false, value: ['default'], action: (value) => { value[1] = 'string'; }},
- {error: false, value: ['default'], action: (value) => {
- value[1] = 'string';
- if (value.length !== 2) { throw new Error(`Invalid length; expected=2; actual=${value.length}`); }
- if (typeof value.push !== 'function') { throw new Error(`Invalid push; expected=function; actual=${typeof value.push}`); }
- }}
- ]
- },
-
- // Reference tests
- {
- schema: {
- definitions: {
- example: {
- type: 'object',
- additionalProperties: false,
- properties: {
- test: {
- $ref: '#/definitions/example'
- }
- }
- }
- },
- $ref: '#/definitions/example'
- },
- tests: [
- {error: false, value: {}, action: (value) => { value.test = {}; }},
- {error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }},
- {error: false, value: {}, action: (value) => { value.test = {test: {}}; }},
- {error: true, value: {}, action: (value) => { value.test = null; }},
- {error: true, value: {}, action: (value) => { value.test = 'string'; }},
- {error: true, value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }},
- {error: true, value: {}, action: (value) => { value.test = {test: 'string'}; }}
- ]
- }
- ];
-
- for (const {schema, tests} of data) {
- for (let {error, value, action} of tests) {
- if (typeof value === 'undefined') { value = getValidValueOrDefault(schema, void 0); }
- value = clone(value);
- assert.ok(schemaValidate(schema, value));
- const valueProxy = createProxy(schema, value);
- if (error) {
- assert.throws(() => action(valueProxy));
- } else {
- assert.doesNotThrow(() => action(valueProxy));
- }
- }
- }
-}
-
-
-function main() {
- testValidate1();
- testValidate2();
- testGetValidValueOrDefault1();
- testProxy1();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-manifest.js b/test/test-manifest.js
deleted file mode 100644
index b52152de..00000000
--- a/test/test-manifest.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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 {testMain} = require('../dev/util');
-const {ManifestUtil} = require('../dev/manifest-util');
-
-
-function loadManifestString() {
- const manifestPath = path.join(__dirname, '..', 'ext', 'manifest.json');
- return fs.readFileSync(manifestPath, {encoding: 'utf8'});
-}
-
-function validateManifest() {
- const manifestUtil = new ManifestUtil();
- const manifest1 = loadManifestString();
- const manifest2 = ManifestUtil.createManifestString(manifestUtil.getManifest());
- assert.strictEqual(manifest1, manifest2, 'Manifest data does not match.');
-}
-
-
-function main() {
- validateManifest();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-object-property-accessor.js b/test/test-object-property-accessor.js
deleted file mode 100644
index d225e893..00000000
--- a/test/test-object-property-accessor.js
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-const vm = new VM({});
-vm.execute('js/general/object-property-accessor.js');
-const ObjectPropertyAccessor = vm.get('ObjectPropertyAccessor');
-
-
-function createTestObject() {
- return {
- 0: null,
- value1: {
- value2: {},
- value3: [],
- value4: null
- },
- value5: [
- {},
- [],
- null
- ]
- };
-}
-
-
-function testGet1() {
- const data = [
- [[], (object) => object],
- [['0'], (object) => object['0']],
- [['value1'], (object) => object.value1],
- [['value1', 'value2'], (object) => object.value1.value2],
- [['value1', 'value3'], (object) => object.value1.value3],
- [['value1', 'value4'], (object) => object.value1.value4],
- [['value5'], (object) => object.value5],
- [['value5', 0], (object) => object.value5[0]],
- [['value5', 1], (object) => object.value5[1]],
- [['value5', 2], (object) => object.value5[2]]
- ];
-
- for (const [pathArray, getExpected] of data) {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
- const expected = getExpected(object);
-
- assert.strictEqual(accessor.get(pathArray), expected);
- }
-}
-
-function testGet2() {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
- const data = [
- [[0], 'Invalid path: [0]'],
- [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
- [['invalid'], 'Invalid path: invalid'],
- [['value1', 'invalid'], 'Invalid path: value1.invalid'],
- [['value1', 'value2', 'invalid'], 'Invalid path: value1.value2.invalid'],
- [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
- [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
- [['value1', 'value3', 0], 'Invalid path: value1.value3[0]'],
- [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
- [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
- [['value5', 'length'], 'Invalid path: value5.length'],
- [['value5', 0, 'invalid'], 'Invalid path: value5[0].invalid'],
- [['value5', 0, 0], 'Invalid path: value5[0][0]'],
- [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
- [['value5', 1, 0], 'Invalid path: value5[1][0]'],
- [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
- [['value5', 2, 0], 'Invalid path: value5[2][0]'],
- [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
- [['value5', 2.5], 'Invalid index']
- ];
-
- for (const [pathArray, message] of data) {
- assert.throws(() => accessor.get(pathArray), {message});
- }
-}
-
-
-function testSet1() {
- const testValue = {};
- const data = [
- ['0'],
- ['value1', 'value2'],
- ['value1', 'value3'],
- ['value1', 'value4'],
- ['value1'],
- ['value5', 0],
- ['value5', 1],
- ['value5', 2],
- ['value5']
- ];
-
- for (const pathArray of data) {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
- accessor.set(pathArray, testValue);
- assert.strictEqual(accessor.get(pathArray), testValue);
- }
-}
-
-function testSet2() {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
- const testValue = {};
- const data = [
- [[], 'Invalid path'],
- [[0], 'Invalid path: [0]'],
- [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
- [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
- [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
- [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
- [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
- [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
- [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
- [['value5', 2, 0], 'Invalid path: value5[2][0]'],
- [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
- [['value5', 2.5], 'Invalid index']
- ];
-
- for (const [pathArray, message] of data) {
- assert.throws(() => accessor.set(pathArray, testValue), {message});
- }
-}
-
-
-function testDelete1() {
- const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property);
-
- const data = [
- [['0'], (object) => !hasOwn(object, '0')],
- [['value1', 'value2'], (object) => !hasOwn(object.value1, 'value2')],
- [['value1', 'value3'], (object) => !hasOwn(object.value1, 'value3')],
- [['value1', 'value4'], (object) => !hasOwn(object.value1, 'value4')],
- [['value1'], (object) => !hasOwn(object, 'value1')],
- [['value5'], (object) => !hasOwn(object, 'value5')]
- ];
-
- for (const [pathArray, validate] of data) {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
- accessor.delete(pathArray);
- assert.ok(validate(object));
- }
-}
-
-function testDelete2() {
- const data = [
- [[], 'Invalid path'],
- [[0], 'Invalid path: [0]'],
- [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
- [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
- [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
- [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
- [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
- [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
- [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
- [['value5', 2, 0], 'Invalid path: value5[2][0]'],
- [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
- [['value5', 2.5], 'Invalid index'],
- [['value5', 0], 'Invalid type'],
- [['value5', 1], 'Invalid type'],
- [['value5', 2], 'Invalid type']
- ];
-
- for (const [pathArray, message] of data) {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
- assert.throws(() => accessor.delete(pathArray), {message});
- }
-}
-
-
-function testSwap1() {
- const data = [
- [['0'], true],
- [['value1', 'value2'], true],
- [['value1', 'value3'], true],
- [['value1', 'value4'], true],
- [['value1'], false],
- [['value5', 0], true],
- [['value5', 1], true],
- [['value5', 2], true],
- [['value5'], false]
- ];
-
- for (const [pathArray1, compareValues1] of data) {
- for (const [pathArray2, compareValues2] of data) {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
- const value1a = accessor.get(pathArray1);
- const value2a = accessor.get(pathArray2);
-
- accessor.swap(pathArray1, pathArray2);
-
- if (!compareValues1 || !compareValues2) { continue; }
-
- const value1b = accessor.get(pathArray1);
- const value2b = accessor.get(pathArray2);
-
- assert.deepStrictEqual(value1a, value2b);
- assert.deepStrictEqual(value2a, value1b);
- }
- }
-}
-
-function testSwap2() {
- const data = [
- [[], [], false, 'Invalid path 1'],
- [['0'], [], false, 'Invalid path 2'],
- [[], ['0'], false, 'Invalid path 1'],
- [[0], ['0'], false, 'Invalid path 1: [0]'],
- [['0'], [0], false, 'Invalid path 2: [0]']
- ];
-
- for (const [pathArray1, pathArray2, checkRevert, message] of data) {
- const object = createTestObject();
- const accessor = new ObjectPropertyAccessor(object);
-
- let value1a;
- let value2a;
- if (checkRevert) {
- try {
- value1a = accessor.get(pathArray1);
- value2a = accessor.get(pathArray2);
- } catch (e) {
- // NOP
- }
- }
-
- assert.throws(() => accessor.swap(pathArray1, pathArray2), {message});
-
- if (!checkRevert) { continue; }
-
- const value1b = accessor.get(pathArray1);
- const value2b = accessor.get(pathArray2);
-
- assert.deepStrictEqual(value1a, value1b);
- assert.deepStrictEqual(value2a, value2b);
- }
-}
-
-
-function testGetPathString1() {
- const data = [
- [[], ''],
- [[0], '[0]'],
- [['escape\\'], '["escape\\\\"]'],
- [['\'quote\''], '["\'quote\'"]'],
- [['"quote"'], '["\\"quote\\""]'],
- [['part1', 'part2'], 'part1.part2'],
- [['part1', 'part2', 3], 'part1.part2[3]'],
- [['part1', 'part2', '3'], 'part1.part2["3"]'],
- [['part1', 'part2', '3part'], 'part1.part2["3part"]'],
- [['part1', 'part2', '3part', 'part4'], 'part1.part2["3part"].part4'],
- [['part1', 'part2', '3part', '4part'], 'part1.part2["3part"]["4part"]']
- ];
-
- for (const [pathArray, expected] of data) {
- assert.strictEqual(ObjectPropertyAccessor.getPathString(pathArray), expected);
- }
-}
-
-function testGetPathString2() {
- const data = [
- [[1.5], 'Invalid index'],
- [[null], 'Invalid type: object']
- ];
-
- for (const [pathArray, message] of data) {
- assert.throws(() => ObjectPropertyAccessor.getPathString(pathArray), {message});
- }
-}
-
-
-function testGetPathArray1() {
- const data = [
- ['', []],
- ['[0]', [0]],
- ['["escape\\\\"]', ['escape\\']],
- ['["\'quote\'"]', ['\'quote\'']],
- ['["\\"quote\\""]', ['"quote"']],
- ['part1.part2', ['part1', 'part2']],
- ['part1.part2[3]', ['part1', 'part2', 3]],
- ['part1.part2["3"]', ['part1', 'part2', '3']],
- ['part1.part2[\'3\']', ['part1', 'part2', '3']],
- ['part1.part2["3part"]', ['part1', 'part2', '3part']],
- ['part1.part2[\'3part\']', ['part1', 'part2', '3part']],
- ['part1.part2["3part"].part4', ['part1', 'part2', '3part', 'part4']],
- ['part1.part2[\'3part\'].part4', ['part1', 'part2', '3part', 'part4']],
- ['part1.part2["3part"]["4part"]', ['part1', 'part2', '3part', '4part']],
- ['part1.part2[\'3part\'][\'4part\']', ['part1', 'part2', '3part', '4part']]
- ];
-
- for (const [pathString, expected] of data) {
- vm.assert.deepStrictEqual(ObjectPropertyAccessor.getPathArray(pathString), expected);
- }
-}
-
-function testGetPathArray2() {
- const data = [
- ['?', 'Unexpected character: ?'],
- ['.', 'Unexpected character: .'],
- ['0', 'Unexpected character: 0'],
- ['part1.[0]', 'Unexpected character: ['],
- ['part1?', 'Unexpected character: ?'],
- ['[part1]', 'Unexpected character: p'],
- ['[0a]', 'Unexpected character: a'],
- ['["part1"x]', 'Unexpected character: x'],
- ['[\'part1\'x]', 'Unexpected character: x'],
- ['["part1"]x', 'Unexpected character: x'],
- ['[\'part1\']x', 'Unexpected character: x'],
- ['part1..part2', 'Unexpected character: .'],
-
- ['[', 'Path not terminated correctly'],
- ['part1.', 'Path not terminated correctly'],
- ['part1[', 'Path not terminated correctly'],
- ['part1["', 'Path not terminated correctly'],
- ['part1[\'', 'Path not terminated correctly'],
- ['part1[""', 'Path not terminated correctly'],
- ['part1[\'\'', 'Path not terminated correctly'],
- ['part1[0', 'Path not terminated correctly'],
- ['part1[0].', 'Path not terminated correctly']
- ];
-
- for (const [pathString, message] of data) {
- assert.throws(() => ObjectPropertyAccessor.getPathArray(pathString), {message});
- }
-}
-
-
-function testHasProperty() {
- const data = [
- [{}, 'invalid', false],
- [{}, 0, false],
- [{valid: 0}, 'valid', true],
- [{null: 0}, null, false],
- [[], 'invalid', false],
- [[], 0, false],
- [[0], 0, true],
- [[0], null, false],
- ['string', 0, false],
- ['string', 'length', false],
- ['string', null, false]
- ];
-
- for (const [object, property, expected] of data) {
- assert.strictEqual(ObjectPropertyAccessor.hasProperty(object, property), expected);
- }
-}
-
-function testIsValidPropertyType() {
- const data = [
- [{}, 'invalid', true],
- [{}, 0, false],
- [{valid: 0}, 'valid', true],
- [{null: 0}, null, false],
- [[], 'invalid', false],
- [[], 0, true],
- [[0], 0, true],
- [[0], null, false],
- ['string', 0, false],
- ['string', 'length', false],
- ['string', null, false]
- ];
-
- for (const [object, property, expected] of data) {
- assert.strictEqual(ObjectPropertyAccessor.isValidPropertyType(object, property), expected);
- }
-}
-
-
-function main() {
- testGet1();
- testGet2();
- testSet1();
- testSet2();
- testDelete1();
- testDelete2();
- testSwap1();
- testSwap2();
- testGetPathString1();
- testGetPathString2();
- testGetPathArray1();
- testGetPathArray2();
- testHasProperty();
- testIsValidPropertyType();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-profile-conditions-util.js b/test/test-profile-conditions-util.js
deleted file mode 100644
index d5187425..00000000
--- a/test/test-profile-conditions-util.js
+++ /dev/null
@@ -1,1099 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-
-const vm = new VM({});
-vm.execute([
- 'js/core.js',
- 'js/general/cache-map.js',
- 'js/data/json-schema.js',
- 'js/background/profile-conditions-util.js'
-]);
-const [ProfileConditionsUtil] = vm.get(['ProfileConditionsUtil']);
-
-
-function testNormalizeContext() {
- const data = [
- // Empty
- {
- context: {},
- expected: {flags: []}
- },
-
- // Domain normalization
- {
- context: {url: ''},
- expected: {url: '', flags: []}
- },
- {
- context: {url: 'http://example.com/'},
- expected: {url: 'http://example.com/', domain: 'example.com', flags: []}
- },
- {
- context: {url: 'http://example.com:1234/'},
- expected: {url: 'http://example.com:1234/', domain: 'example.com', flags: []}
- },
- {
- context: {url: 'http://user@example.com:1234/'},
- expected: {url: 'http://user@example.com:1234/', domain: 'example.com', flags: []}
- }
- ];
-
- for (const {context, expected} of data) {
- const profileConditionsUtil = new ProfileConditionsUtil();
- const actual = profileConditionsUtil.normalizeContext(context);
- vm.assert.deepStrictEqual(actual, expected);
- }
-}
-
-function testSchemas() {
- const data = [
- // Empty
- {
- conditionGroups: [],
- expectedSchema: {},
- inputs: [
- {expected: true, context: {url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {conditions: []}
- ],
- expectedSchema: {},
- inputs: [
- {expected: true, context: {url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {conditions: []},
- {conditions: []}
- ],
- expectedSchema: {},
- inputs: [
- {expected: true, context: {url: 'http://example.com/'}}
- ]
- },
-
- // popupLevel tests
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'equal',
- value: '0'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- depth: {const: 0}
- },
- required: ['depth']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: false, context: {depth: 1, url: 'http://example.com/'}},
- {expected: false, context: {depth: -1, url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'notEqual',
- value: '0'
- }
- ]
- }
- ],
- expectedSchema: {
- not: [
- {
- properties: {
- depth: {const: 0}
- },
- required: ['depth']
- }
- ]
- },
- inputs: [
- {expected: false, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 1, url: 'http://example.com/'}},
- {expected: true, context: {depth: -1, url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'lessThan',
- value: '0'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- depth: {
- type: 'number',
- exclusiveMaximum: 0
- }
- },
- required: ['depth']
- },
- inputs: [
- {expected: false, context: {depth: 0, url: 'http://example.com/'}},
- {expected: false, context: {depth: 1, url: 'http://example.com/'}},
- {expected: true, context: {depth: -1, url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'greaterThan',
- value: '0'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- depth: {
- type: 'number',
- exclusiveMinimum: 0
- }
- },
- required: ['depth']
- },
- inputs: [
- {expected: false, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 1, url: 'http://example.com/'}},
- {expected: false, context: {depth: -1, url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'lessThanOrEqual',
- value: '0'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- depth: {
- type: 'number',
- maximum: 0
- }
- },
- required: ['depth']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: false, context: {depth: 1, url: 'http://example.com/'}},
- {expected: true, context: {depth: -1, url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'greaterThanOrEqual',
- value: '0'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- depth: {
- type: 'number',
- minimum: 0
- }
- },
- required: ['depth']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 1, url: 'http://example.com/'}},
- {expected: false, context: {depth: -1, url: 'http://example.com/'}}
- ]
- },
-
- // url tests
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'url',
- operator: 'matchDomain',
- value: 'example.com'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- domain: {
- oneOf: [
- {const: 'example.com'}
- ]
- }
- },
- required: ['domain']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: false, context: {depth: 0, url: 'http://example1.com/'}},
- {expected: false, context: {depth: 0, url: 'http://example2.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}},
- {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'url',
- operator: 'matchDomain',
- value: 'example.com, example1.com, example2.com'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- domain: {
- oneOf: [
- {const: 'example.com'},
- {const: 'example1.com'},
- {const: 'example2.com'}
- ]
- }
- },
- required: ['domain']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example1.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example2.com/'}},
- {expected: false, context: {depth: 0, url: 'http://example3.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}},
- {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'url',
- operator: 'matchRegExp',
- value: '^http://example\\d?\\.com/[\\w\\W]*$'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- url: {
- type: 'string',
- pattern: '^http://example\\d?\\.com/[\\w\\W]*$',
- patternFlags: 'i'
- }
- },
- required: ['url']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example1.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example2.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example3.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example.com/example'}},
- {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}},
- {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}},
- {expected: false, context: {depth: 0, url: 'http://example-1.com/'}}
- ]
- },
-
- // modifierKeys tests
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'are',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- modifierKeys: {
- type: 'array',
- maxItems: 0,
- minItems: 0
- }
- },
- required: ['modifierKeys']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'are',
- value: 'Alt, Shift'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- modifierKeys: {
- type: 'array',
- maxItems: 2,
- minItems: 2,
- allOf: [
- {contains: {const: 'Alt'}},
- {contains: {const: 'Shift'}}
- ]
- }
- },
- required: ['modifierKeys']
- },
- inputs: [
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'areNot',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- not: [
- {
- properties: {
- modifierKeys: {
- type: 'array',
- maxItems: 0,
- minItems: 0
- }
- },
- required: ['modifierKeys']
- }
- ]
- },
- inputs: [
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'areNot',
- value: 'Alt, Shift'
- }
- ]
- }
- ],
- expectedSchema: {
- not: [
- {
- properties: {
- modifierKeys: {
- type: 'array',
- maxItems: 2,
- minItems: 2,
- allOf: [
- {contains: {const: 'Alt'}},
- {contains: {const: 'Shift'}}
- ]
- }
- },
- required: ['modifierKeys']
- }
- ]
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'include',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- modifierKeys: {
- type: 'array',
- minItems: 0
- }
- },
- required: ['modifierKeys']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'include',
- value: 'Alt, Shift'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- modifierKeys: {
- type: 'array',
- minItems: 2,
- allOf: [
- {contains: {const: 'Alt'}},
- {contains: {const: 'Shift'}}
- ]
- }
- },
- required: ['modifierKeys']
- },
- inputs: [
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'notInclude',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- modifierKeys: {
- type: 'array'
- }
- },
- required: ['modifierKeys']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'modifierKeys',
- operator: 'notInclude',
- value: 'Alt, Shift'
- }
- ]
- }
- ],
- expectedSchema: {
- properties: {
- modifierKeys: {
- type: 'array',
- not: [
- {contains: {const: 'Alt'}},
- {contains: {const: 'Shift'}}
- ]
- }
- },
- required: ['modifierKeys']
- },
- inputs: [
- {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}},
- {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}}
- ]
- },
-
- // flags tests
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'are',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array',
- maxItems: 0,
- minItems: 0
- }
- }
- },
- inputs: [
- {expected: true, context: {}},
- {expected: true, context: {flags: []}},
- {expected: false, context: {flags: ['test1']}},
- {expected: false, context: {flags: ['test1', 'test2']}},
- {expected: false, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'are',
- value: 'test1, test2'
- }
- ]
- }
- ],
- expectedSchema: {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array',
- maxItems: 2,
- minItems: 2,
- allOf: [
- {contains: {const: 'test1'}},
- {contains: {const: 'test2'}}
- ]
- }
- }
- },
- inputs: [
- {expected: false, context: {}},
- {expected: false, context: {flags: []}},
- {expected: false, context: {flags: ['test1']}},
- {expected: true, context: {flags: ['test1', 'test2']}},
- {expected: false, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'areNot',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- not: [
- {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array',
- maxItems: 0,
- minItems: 0
- }
- }
- }
- ]
- },
- inputs: [
- {expected: false, context: {}},
- {expected: false, context: {flags: []}},
- {expected: true, context: {flags: ['test1']}},
- {expected: true, context: {flags: ['test1', 'test2']}},
- {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'areNot',
- value: 'test1, test2'
- }
- ]
- }
- ],
- expectedSchema: {
- not: [
- {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array',
- maxItems: 2,
- minItems: 2,
- allOf: [
- {contains: {const: 'test1'}},
- {contains: {const: 'test2'}}
- ]
- }
- }
- }
- ]
- },
- inputs: [
- {expected: true, context: {}},
- {expected: true, context: {flags: []}},
- {expected: true, context: {flags: ['test1']}},
- {expected: false, context: {flags: ['test1', 'test2']}},
- {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'include',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array',
- minItems: 0
- }
- }
- },
- inputs: [
- {expected: true, context: {}},
- {expected: true, context: {flags: []}},
- {expected: true, context: {flags: ['test1']}},
- {expected: true, context: {flags: ['test1', 'test2']}},
- {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'include',
- value: 'test1, test2'
- }
- ]
- }
- ],
- expectedSchema: {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array',
- minItems: 2,
- allOf: [
- {contains: {const: 'test1'}},
- {contains: {const: 'test2'}}
- ]
- }
- }
- },
- inputs: [
- {expected: false, context: {}},
- {expected: false, context: {flags: []}},
- {expected: false, context: {flags: ['test1']}},
- {expected: true, context: {flags: ['test1', 'test2']}},
- {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'notInclude',
- value: ''
- }
- ]
- }
- ],
- expectedSchema: {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array'
- }
- }
- },
- inputs: [
- {expected: true, context: {}},
- {expected: true, context: {flags: []}},
- {expected: true, context: {flags: ['test1']}},
- {expected: true, context: {flags: ['test1', 'test2']}},
- {expected: true, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'flags',
- operator: 'notInclude',
- value: 'test1, test2'
- }
- ]
- }
- ],
- expectedSchema: {
- required: ['flags'],
- properties: {
- flags: {
- type: 'array',
- not: [
- {contains: {const: 'test1'}},
- {contains: {const: 'test2'}}
- ]
- }
- }
- },
- inputs: [
- {expected: true, context: {}},
- {expected: true, context: {flags: []}},
- {expected: false, context: {flags: ['test1']}},
- {expected: false, context: {flags: ['test1', 'test2']}},
- {expected: false, context: {flags: ['test1', 'test2', 'test3']}}
- ]
- },
-
- // Multiple conditions tests
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'greaterThan',
- value: '0'
- },
- {
- type: 'popupLevel',
- operator: 'lessThan',
- value: '3'
- }
- ]
- }
- ],
- expectedSchema: {
- allOf: [
- {
- properties: {
- depth: {
- type: 'number',
- exclusiveMinimum: 0
- }
- },
- required: ['depth']
- },
- {
- properties: {
- depth: {
- type: 'number',
- exclusiveMaximum: 3
- }
- },
- required: ['depth']
- }
- ]
- },
- inputs: [
- {expected: false, context: {depth: -2, url: 'http://example.com/'}},
- {expected: false, context: {depth: -1, url: 'http://example.com/'}},
- {expected: false, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 1, url: 'http://example.com/'}},
- {expected: true, context: {depth: 2, url: 'http://example.com/'}},
- {expected: false, context: {depth: 3, url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'greaterThan',
- value: '0'
- },
- {
- type: 'popupLevel',
- operator: 'lessThan',
- value: '3'
- }
- ]
- },
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'equal',
- value: '0'
- }
- ]
- }
- ],
- expectedSchema: {
- anyOf: [
- {
- allOf: [
- {
- properties: {
- depth: {
- type: 'number',
- exclusiveMinimum: 0
- }
- },
- required: ['depth']
- },
- {
- properties: {
- depth: {
- type: 'number',
- exclusiveMaximum: 3
- }
- },
- required: ['depth']
- }
- ]
- },
- {
- properties: {
- depth: {const: 0}
- },
- required: ['depth']
- }
- ]
- },
- inputs: [
- {expected: false, context: {depth: -2, url: 'http://example.com/'}},
- {expected: false, context: {depth: -1, url: 'http://example.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 1, url: 'http://example.com/'}},
- {expected: true, context: {depth: 2, url: 'http://example.com/'}},
- {expected: false, context: {depth: 3, url: 'http://example.com/'}}
- ]
- },
- {
- conditionGroups: [
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'greaterThan',
- value: '0'
- },
- {
- type: 'popupLevel',
- operator: 'lessThan',
- value: '3'
- }
- ]
- },
- {
- conditions: [
- {
- type: 'popupLevel',
- operator: 'lessThanOrEqual',
- value: '0'
- },
- {
- type: 'popupLevel',
- operator: 'greaterThanOrEqual',
- value: '-1'
- }
- ]
- }
- ],
- expectedSchema: {
- anyOf: [
- {
- allOf: [
- {
- properties: {
- depth: {
- type: 'number',
- exclusiveMinimum: 0
- }
- },
- required: ['depth']
- },
- {
- properties: {
- depth: {
- type: 'number',
- exclusiveMaximum: 3
- }
- },
- required: ['depth']
- }
- ]
- },
- {
- allOf: [
- {
- properties: {
- depth: {
- type: 'number',
- maximum: 0
- }
- },
- required: ['depth']
- },
- {
- properties: {
- depth: {
- type: 'number',
- minimum: -1
- }
- },
- required: ['depth']
- }
- ]
- }
- ]
- },
- inputs: [
- {expected: false, context: {depth: -2, url: 'http://example.com/'}},
- {expected: true, context: {depth: -1, url: 'http://example.com/'}},
- {expected: true, context: {depth: 0, url: 'http://example.com/'}},
- {expected: true, context: {depth: 1, url: 'http://example.com/'}},
- {expected: true, context: {depth: 2, url: 'http://example.com/'}},
- {expected: false, context: {depth: 3, url: 'http://example.com/'}}
- ]
- }
- ];
-
- for (const {conditionGroups, expectedSchema, inputs} of data) {
- const profileConditionsUtil = new ProfileConditionsUtil();
- const schema = profileConditionsUtil.createSchema(conditionGroups);
- if (typeof expectedSchema !== 'undefined') {
- vm.assert.deepStrictEqual(schema.schema, expectedSchema);
- }
- if (Array.isArray(inputs)) {
- for (const {expected, context} of inputs) {
- const normalizedContext = profileConditionsUtil.normalizeContext(context);
- const actual = schema.isValid(normalizedContext);
- assert.strictEqual(actual, expected);
- }
- }
- }
-}
-
-
-function main() {
- testNormalizeContext();
- testSchemas();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-text-source-map.js b/test/test-text-source-map.js
deleted file mode 100644
index dd8d3bbd..00000000
--- a/test/test-text-source-map.js
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * 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 assert = require('assert');
-const {testMain} = require('../dev/util');
-const {VM} = require('../dev/vm');
-
-const vm = new VM();
-vm.execute(['js/general/text-source-map.js']);
-const TextSourceMap = vm.get('TextSourceMap');
-
-
-function testSource() {
- const data = [
- ['source1'],
- ['source2'],
- ['source3']
- ];
-
- for (const [source] of data) {
- const sourceMap = new TextSourceMap(source);
- assert.strictEqual(source, sourceMap.source);
- }
-}
-
-function testEquals() {
- const data = [
- [['source1', null], ['source1', null], true],
- [['source2', null], ['source2', null], true],
- [['source3', null], ['source3', null], true],
-
- [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', null], true],
- [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', null], true],
- [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', null], true],
-
- [['source1', null], ['source1', [1, 1, 1, 1, 1, 1, 1]], true],
- [['source2', null], ['source2', [1, 1, 1, 1, 1, 1, 1]], true],
- [['source3', null], ['source3', [1, 1, 1, 1, 1, 1, 1]], true],
-
- [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', [1, 1, 1, 1, 1, 1, 1]], true],
- [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', [1, 1, 1, 1, 1, 1, 1]], true],
- [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', [1, 1, 1, 1, 1, 1, 1]], true],
-
- [['source1', [1, 2, 1, 3]], ['source1', [1, 2, 1, 3]], true],
- [['source2', [1, 2, 1, 3]], ['source2', [1, 2, 1, 3]], true],
- [['source3', [1, 2, 1, 3]], ['source3', [1, 2, 1, 3]], true],
-
- [['source1', [1, 3, 1, 2]], ['source1', [1, 2, 1, 3]], false],
- [['source2', [1, 3, 1, 2]], ['source2', [1, 2, 1, 3]], false],
- [['source3', [1, 3, 1, 2]], ['source3', [1, 2, 1, 3]], false],
-
- [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source4', [1, 1, 1, 1, 1, 1, 1]], false],
- [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source5', [1, 1, 1, 1, 1, 1, 1]], false],
- [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source6', [1, 1, 1, 1, 1, 1, 1]], false]
- ];
-
- for (const [[source1, mapping1], [source2, mapping2], expectedEquals] of data) {
- const sourceMap1 = new TextSourceMap(source1, mapping1);
- const sourceMap2 = new TextSourceMap(source2, mapping2);
- assert.ok(sourceMap1.equals(sourceMap1));
- assert.ok(sourceMap2.equals(sourceMap2));
- assert.strictEqual(sourceMap1.equals(sourceMap2), expectedEquals);
- }
-}
-
-function testGetSourceLength() {
- const data = [
- [['source', [1, 1, 1, 1, 1, 1]], 1, 1],
- [['source', [1, 1, 1, 1, 1, 1]], 2, 2],
- [['source', [1, 1, 1, 1, 1, 1]], 3, 3],
- [['source', [1, 1, 1, 1, 1, 1]], 4, 4],
- [['source', [1, 1, 1, 1, 1, 1]], 5, 5],
- [['source', [1, 1, 1, 1, 1, 1]], 6, 6],
-
- [['source', [2, 2, 2]], 1, 2],
- [['source', [2, 2, 2]], 2, 4],
- [['source', [2, 2, 2]], 3, 6],
-
- [['source', [3, 3]], 1, 3],
- [['source', [3, 3]], 2, 6],
-
- [['source', [6, 6]], 1, 6]
- ];
-
- for (const [[source, mapping], finalLength, expectedValue] of data) {
- const sourceMap = new TextSourceMap(source, mapping);
- assert.strictEqual(sourceMap.getSourceLength(finalLength), expectedValue);
- }
-}
-
-function testCombineInsert() {
- const data = [
- // No operations
- [
- ['source', null],
- ['source', [1, 1, 1, 1, 1, 1]],
- []
- ],
-
- // Combine
- [
- ['source', null],
- ['source', [3, 1, 1, 1]],
- [
- ['combine', 0, 2]
- ]
- ],
- [
- ['source', null],
- ['source', [1, 1, 1, 3]],
- [
- ['combine', 3, 2]
- ]
- ],
- [
- ['source', null],
- ['source', [3, 3]],
- [
- ['combine', 0, 2],
- ['combine', 1, 2]
- ]
- ],
- [
- ['source', null],
- ['source', [3, 3]],
- [
- ['combine', 3, 2],
- ['combine', 0, 2]
- ]
- ],
-
- // Insert
- [
- ['source', null],
- ['source', [0, 1, 1, 1, 1, 1, 1]],
- [
- ['insert', 0, 0]
- ]
- ],
- [
- ['source', null],
- ['source', [1, 1, 1, 1, 1, 1, 0]],
- [
- ['insert', 6, 0]
- ]
- ],
- [
- ['source', null],
- ['source', [0, 1, 1, 1, 1, 1, 1, 0]],
- [
- ['insert', 0, 0],
- ['insert', 7, 0]
- ]
- ],
- [
- ['source', null],
- ['source', [0, 1, 1, 1, 1, 1, 1, 0]],
- [
- ['insert', 6, 0],
- ['insert', 0, 0]
- ]
- ],
-
- // Mixed
- [
- ['source', null],
- ['source', [3, 0, 3]],
- [
- ['combine', 0, 2],
- ['insert', 1, 0],
- ['combine', 2, 2]
- ]
- ],
- [
- ['source', null],
- ['source', [3, 0, 3]],
- [
- ['combine', 0, 2],
- ['combine', 1, 2],
- ['insert', 1, 0]
- ]
- ],
- [
- ['source', null],
- ['source', [3, 0, 3]],
- [
- ['insert', 3, 0],
- ['combine', 0, 2],
- ['combine', 2, 2]
- ]
- ]
- ];
-
- for (const [[source, mapping], [expectedSource, expectedMapping], operations] of data) {
- const sourceMap = new TextSourceMap(source, mapping);
- const expectedSourceMap = new TextSourceMap(expectedSource, expectedMapping);
- for (const [operation, ...args] of operations) {
- switch (operation) {
- case 'combine':
- sourceMap.combine(...args);
- break;
- case 'insert':
- sourceMap.insert(...args);
- break;
- }
- }
- assert.ok(sourceMap.equals(expectedSourceMap));
- }
-}
-
-
-function main() {
- testSource();
- testEquals();
- testGetSourceLength();
- testCombineInsert();
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-translator.js b/test/test-translator.js
deleted file mode 100644
index 485eb665..00000000
--- a/test/test-translator.js
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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 {testMain} = require('../dev/util');
-const {TranslatorVM} = require('../dev/translator-vm');
-
-
-function clone(value) {
- return JSON.parse(JSON.stringify(value));
-}
-
-
-async function main() {
- const write = (process.argv[2] === '--write');
-
- const translatorVM = new TranslatorVM();
- const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1');
- await translatorVM.prepare(dictionaryDirectory, 'Test Dictionary 2');
-
- const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json');
- const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'}));
-
- const testResults1FilePath = path.join(__dirname, 'data', 'translator-test-results.json');
- const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'}));
- const actualResults1 = [];
-
- const testResults2FilePath = path.join(__dirname, 'data', 'translator-test-results-note-data1.json');
- const expectedResults2 = JSON.parse(fs.readFileSync(testResults2FilePath, {encoding: 'utf8'}));
- const actualResults2 = [];
-
- for (let i = 0, ii = tests.length; i < ii; ++i) {
- const test = tests[i];
- const expected1 = expectedResults1[i];
- const expected2 = expectedResults2[i];
- switch (test.func) {
- case 'findTerms':
- {
- const {name, mode, text} = test;
- const options = translatorVM.buildOptions(optionsPresets, test.options);
- const {dictionaryEntries, originalTextLength} = clone(await translatorVM.translator.findTerms(mode, text, options));
- const noteDataList = mode !== 'simple' ? clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), mode))) : null;
- actualResults1.push({name, originalTextLength, dictionaryEntries});
- actualResults2.push({name, noteDataList});
- if (!write) {
- assert.deepStrictEqual(originalTextLength, expected1.originalTextLength);
- assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries);
- assert.deepStrictEqual(noteDataList, expected2.noteDataList);
- }
- }
- break;
- case 'findKanji':
- {
- const {name, text} = test;
- const options = translatorVM.buildOptions(optionsPresets, test.options);
- const dictionaryEntries = clone(await translatorVM.translator.findKanji(text, options));
- const noteDataList = clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), null)));
- actualResults1.push({name, dictionaryEntries});
- actualResults2.push({name, noteDataList});
- if (!write) {
- assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries);
- assert.deepStrictEqual(noteDataList, expected2.noteDataList);
- }
- }
- break;
- }
- }
-
- if (write) {
- // Use 2 indent instead of 4 to save a bit of file size
- fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'});
- fs.writeFileSync(testResults2FilePath, JSON.stringify(actualResults2, null, 2), {encoding: 'utf8'});
- }
-}
-
-
-if (require.main === module) { testMain(main); }
diff --git a/test/test-workers.js b/test/test-workers.js
deleted file mode 100644
index b4ec4d7d..00000000
--- a/test/test-workers.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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 {JSDOM} = require('jsdom');
-const {VM} = require('../dev/vm');
-const assert = require('assert');
-
-
-class StubClass {
- prepare() {
- // NOP
- }
-}
-
-
-function loadEslint() {
- return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.eslintrc.json'), {encoding: 'utf8'}));
-}
-
-function filterScriptPaths(scriptPaths) {
- const extDirName = 'ext';
- return scriptPaths.filter((src) => !src.startsWith('/lib/')).map((src) => `${extDirName}${src}`);
-}
-
-function getAllHtmlScriptPaths(fileName) {
- const domSource = fs.readFileSync(fileName, {encoding: 'utf8'});
- const dom = new JSDOM(domSource);
- const {window} = dom;
- const {document} = window;
- try {
- const scripts = document.querySelectorAll('script');
- return [...scripts].map(({src}) => src);
- } finally {
- window.close();
- }
-}
-
-function convertBackgroundScriptsToServiceWorkerScripts(scripts) {
- // Use parse5-based SimpleDOMParser
- scripts.splice(0, 0, '/lib/parse5.js');
- const index = scripts.indexOf('/js/dom/native-simple-dom-parser.js');
- assert.ok(index >= 0);
- scripts[index] = '/js/dom/simple-dom-parser.js';
-}
-
-function getImportedScripts(scriptPath, fields) {
- const importedScripts = [];
-
- const importScripts = (...scripts) => {
- importedScripts.push(...scripts);
- };
-
- const vm = new VM(Object.assign({importScripts}, fields));
- vm.context.self = vm.context;
- vm.execute([scriptPath]);
-
- return importedScripts;
-}
-
-function testServiceWorker() {
- // Verify that sw.js scripts match background.html scripts
- const extDir = path.join(__dirname, '..', 'ext');
- const scripts = getAllHtmlScriptPaths(path.join(extDir, 'background.html'));
- convertBackgroundScriptsToServiceWorkerScripts(scripts);
- const importedScripts = getImportedScripts('sw.js', {});
- assert.deepStrictEqual(scripts, importedScripts);
-
- // Verify that eslint config lists files correctly
- const expectedSwRulesFiles = filterScriptPaths(scripts);
- const eslintConfig = loadEslint();
- const swRules = eslintConfig.overrides.find((item) => (
- typeof item.env === 'object' &&
- item.env !== null &&
- item.env.serviceworker === true
- ));
- assert.ok(typeof swRules !== 'undefined');
- assert.ok(Array.isArray(swRules.files));
- assert.deepStrictEqual(swRules.files, expectedSwRulesFiles);
-}
-
-function testWorkers() {
- testWorker(
- 'js/language/dictionary-worker-main.js',
- {DictionaryWorkerHandler: StubClass}
- );
-}
-
-function testWorker(scriptPath, fields) {
- // Get script paths
- const scripts = getImportedScripts(scriptPath, fields);
-
- // Verify that eslint config lists files correctly
- const expectedRulesFiles = filterScriptPaths(scripts);
- const expectedRulesFilesSet = new Set(expectedRulesFiles);
- const eslintConfig = loadEslint();
- const rules = eslintConfig.overrides.find((item) => (
- typeof item.env === 'object' &&
- item.env !== null &&
- item.env.worker === true
- ));
- assert.ok(typeof rules !== 'undefined');
- assert.ok(Array.isArray(rules.files));
- assert.deepStrictEqual(rules.files.filter((v) => expectedRulesFilesSet.has(v)), expectedRulesFiles);
-}
-
-
-function main() {
- try {
- testServiceWorker();
- testWorkers();
- } catch (e) {
- console.error(e);
- process.exit(-1);
- return;
- }
- process.exit(0);
-}
-
-
-if (require.main === module) { main(); }
diff --git a/test/text-source-map.test.js b/test/text-source-map.test.js
new file mode 100644
index 00000000..aeaba000
--- /dev/null
+++ b/test/text-source-map.test.js
@@ -0,0 +1,237 @@
+/*
+ * 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/>.
+ */
+
+import {expect, test} from 'vitest';
+import {TextSourceMap} from '../ext/js/general/text-source-map.js';
+
+function testSource() {
+ test('Source', () => {
+ const data = [
+ ['source1'],
+ ['source2'],
+ ['source3']
+ ];
+
+ for (const [source] of data) {
+ const sourceMap = new TextSourceMap(source);
+ expect(source).toStrictEqual(sourceMap.source);
+ }
+ });
+}
+
+function testEquals() {
+ test('Equals', () => {
+ const data = [
+ [['source1', null], ['source1', null], true],
+ [['source2', null], ['source2', null], true],
+ [['source3', null], ['source3', null], true],
+
+ [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', null], true],
+ [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', null], true],
+ [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', null], true],
+
+ [['source1', null], ['source1', [1, 1, 1, 1, 1, 1, 1]], true],
+ [['source2', null], ['source2', [1, 1, 1, 1, 1, 1, 1]], true],
+ [['source3', null], ['source3', [1, 1, 1, 1, 1, 1, 1]], true],
+
+ [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', [1, 1, 1, 1, 1, 1, 1]], true],
+ [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', [1, 1, 1, 1, 1, 1, 1]], true],
+ [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', [1, 1, 1, 1, 1, 1, 1]], true],
+
+ [['source1', [1, 2, 1, 3]], ['source1', [1, 2, 1, 3]], true],
+ [['source2', [1, 2, 1, 3]], ['source2', [1, 2, 1, 3]], true],
+ [['source3', [1, 2, 1, 3]], ['source3', [1, 2, 1, 3]], true],
+
+ [['source1', [1, 3, 1, 2]], ['source1', [1, 2, 1, 3]], false],
+ [['source2', [1, 3, 1, 2]], ['source2', [1, 2, 1, 3]], false],
+ [['source3', [1, 3, 1, 2]], ['source3', [1, 2, 1, 3]], false],
+
+ [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source4', [1, 1, 1, 1, 1, 1, 1]], false],
+ [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source5', [1, 1, 1, 1, 1, 1, 1]], false],
+ [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source6', [1, 1, 1, 1, 1, 1, 1]], false]
+ ];
+
+ for (const [[source1, mapping1], [source2, mapping2], expectedEquals] of data) {
+ const sourceMap1 = new TextSourceMap(source1, mapping1);
+ const sourceMap2 = new TextSourceMap(source2, mapping2);
+ expect(sourceMap1.equals(sourceMap1)).toBe(true);
+ expect(sourceMap2.equals(sourceMap2)).toBe(true);
+ expect(sourceMap1.equals(sourceMap2)).toStrictEqual(expectedEquals);
+ }
+ });
+}
+
+function testGetSourceLength() {
+ test('GetSourceLength', () => {
+ const data = [
+ [['source', [1, 1, 1, 1, 1, 1]], 1, 1],
+ [['source', [1, 1, 1, 1, 1, 1]], 2, 2],
+ [['source', [1, 1, 1, 1, 1, 1]], 3, 3],
+ [['source', [1, 1, 1, 1, 1, 1]], 4, 4],
+ [['source', [1, 1, 1, 1, 1, 1]], 5, 5],
+ [['source', [1, 1, 1, 1, 1, 1]], 6, 6],
+
+ [['source', [2, 2, 2]], 1, 2],
+ [['source', [2, 2, 2]], 2, 4],
+ [['source', [2, 2, 2]], 3, 6],
+
+ [['source', [3, 3]], 1, 3],
+ [['source', [3, 3]], 2, 6],
+
+ [['source', [6, 6]], 1, 6]
+ ];
+
+ for (const [[source, mapping], finalLength, expectedValue] of data) {
+ const sourceMap = new TextSourceMap(source, mapping);
+ expect(sourceMap.getSourceLength(finalLength)).toStrictEqual(expectedValue);
+ }
+ });
+}
+
+function testCombineInsert() {
+ test('CombineInsert', () => {
+ const data = [
+ // No operations
+ [
+ ['source', null],
+ ['source', [1, 1, 1, 1, 1, 1]],
+ []
+ ],
+
+ // Combine
+ [
+ ['source', null],
+ ['source', [3, 1, 1, 1]],
+ [
+ ['combine', 0, 2]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [1, 1, 1, 3]],
+ [
+ ['combine', 3, 2]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [3, 3]],
+ [
+ ['combine', 0, 2],
+ ['combine', 1, 2]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [3, 3]],
+ [
+ ['combine', 3, 2],
+ ['combine', 0, 2]
+ ]
+ ],
+
+ // Insert
+ [
+ ['source', null],
+ ['source', [0, 1, 1, 1, 1, 1, 1]],
+ [
+ ['insert', 0, 0]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [1, 1, 1, 1, 1, 1, 0]],
+ [
+ ['insert', 6, 0]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [0, 1, 1, 1, 1, 1, 1, 0]],
+ [
+ ['insert', 0, 0],
+ ['insert', 7, 0]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [0, 1, 1, 1, 1, 1, 1, 0]],
+ [
+ ['insert', 6, 0],
+ ['insert', 0, 0]
+ ]
+ ],
+
+ // Mixed
+ [
+ ['source', null],
+ ['source', [3, 0, 3]],
+ [
+ ['combine', 0, 2],
+ ['insert', 1, 0],
+ ['combine', 2, 2]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [3, 0, 3]],
+ [
+ ['combine', 0, 2],
+ ['combine', 1, 2],
+ ['insert', 1, 0]
+ ]
+ ],
+ [
+ ['source', null],
+ ['source', [3, 0, 3]],
+ [
+ ['insert', 3, 0],
+ ['combine', 0, 2],
+ ['combine', 2, 2]
+ ]
+ ]
+ ];
+
+ for (const [[source, mapping], [expectedSource, expectedMapping], operations] of data) {
+ const sourceMap = new TextSourceMap(source, mapping);
+ const expectedSourceMap = new TextSourceMap(expectedSource, expectedMapping);
+ for (const [operation, ...args] of operations) {
+ switch (operation) {
+ case 'combine':
+ sourceMap.combine(...args);
+ break;
+ case 'insert':
+ sourceMap.insert(...args);
+ break;
+ }
+ }
+ expect(sourceMap.equals(expectedSourceMap)).toBe(true);
+ }
+ });
+}
+
+
+function main() {
+ testSource();
+ testEquals();
+ testGetSourceLength();
+ testCombineInsert();
+}
+
+
+main();
diff --git a/test/translator.test.js b/test/translator.test.js
new file mode 100644
index 00000000..7a827d39
--- /dev/null
+++ b/test/translator.test.js
@@ -0,0 +1,83 @@
+/*
+ * 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/>.
+ */
+
+import {IDBKeyRange, indexedDB} from 'fake-indexeddb';
+import fs from 'fs';
+import {fileURLToPath} from 'node:url';
+import path from 'path';
+import {expect, test, vi} from 'vitest';
+import {TranslatorVM} from '../dev/translator-vm';
+
+vi.stubGlobal('indexedDB', indexedDB);
+vi.stubGlobal('IDBKeyRange', IDBKeyRange);
+
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+
+async function main() {
+ const translatorVM = new TranslatorVM();
+ const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', 'valid-dictionary1');
+ await translatorVM.prepare(dictionaryDirectory, 'Test Dictionary 2');
+
+ const testInputsFilePath = path.join(dirname, 'data', 'translator-test-inputs.json');
+ const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'}));
+
+ const testResults1FilePath = path.join(dirname, 'data', 'translator-test-results.json');
+ const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'}));
+ const actualResults1 = [];
+
+ const testResults2FilePath = path.join(dirname, 'data', 'translator-test-results-note-data1.json');
+ const expectedResults2 = JSON.parse(fs.readFileSync(testResults2FilePath, {encoding: 'utf8'}));
+ const actualResults2 = [];
+
+ for (let i = 0, ii = tests.length; i < ii; ++i) {
+ test(`${i}`, async () => {
+ const t = tests[i];
+ const expected1 = expectedResults1[i];
+ const expected2 = expectedResults2[i];
+ switch (t.func) {
+ case 'findTerms':
+ {
+ const {name, mode, text} = t;
+ const options = translatorVM.buildOptions(optionsPresets, t.options);
+ const {dictionaryEntries, originalTextLength} = structuredClone(await translatorVM.translator.findTerms(mode, text, options));
+ const noteDataList = mode !== 'simple' ? structuredClone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(structuredClone(dictionaryEntry), mode))) : null;
+ actualResults1.push({name, originalTextLength, dictionaryEntries});
+ actualResults2.push({name, noteDataList});
+ expect(originalTextLength).toStrictEqual(expected1.originalTextLength);
+ expect(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries);
+ expect(noteDataList).toEqual(expected2.noteDataList);
+ }
+ break;
+ case 'findKanji':
+ {
+ const {name, text} = t;
+ const options = translatorVM.buildOptions(optionsPresets, t.options);
+ const dictionaryEntries = structuredClone(await translatorVM.translator.findKanji(text, options));
+ const noteDataList = structuredClone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(structuredClone(dictionaryEntry), null)));
+ actualResults1.push({name, dictionaryEntries});
+ actualResults2.push({name, noteDataList});
+ expect(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries);
+ expect(noteDataList).toEqual(expected2.noteDataList);
+ }
+ break;
+ }
+ });
+ }
+}
+
+await main();