From 0f4d36938fd0d844f548aa5a7f7e7842df8dfb41 Mon Sep 17 00:00:00 2001 From: Darius Jahandarie Date: Wed, 8 Nov 2023 03:11:35 +0900 Subject: Switch to vitest for ESM support; other fixes --- test/anki-note-builder.test.js | 224 +++++ test/cache-map.test.js | 128 +++ test/core.test.js | 288 ++++++ test/css-json.test.js | 33 + test/database.test.js | 853 +++++++++++++++++ test/deinflector.test.js | 947 +++++++++++++++++++ test/dictionary.test.js | 59 ++ test/document-util.test.js | 259 ++++++ test/dom-text-scanner.test.js | 179 ++++ test/hotkey-util.test.js | 164 ++++ test/japanese-util.test.js | 905 ++++++++++++++++++ test/jsdom.test.js | 47 + test/json-schema.test.js | 1009 +++++++++++++++++++++ test/object-property-accessor.test.js | 438 +++++++++ test/options-util.test.js | 1587 ++++++++++++++++++++++++++++++++ test/profile-conditions-util.test.js | 1090 ++++++++++++++++++++++ test/test-all.js | 67 -- test/test-anki-note-builder.js | 308 ------- test/test-build-libs.js | 42 - test/test-cache-map.js | 132 --- test/test-core.js | 292 ------ test/test-css-json.js | 37 - test/test-database.js | 887 ------------------ test/test-deinflector.js | 952 ------------------- test/test-dictionary.js | 66 -- test/test-document-util.js | 270 ------ test/test-dom-text-scanner.js | 188 ---- test/test-hotkey-util.js | 173 ---- test/test-japanese-util.js | 881 ------------------ test/test-jsdom.js | 50 - test/test-json-schema.js | 1011 --------------------- test/test-manifest.js | 44 - test/test-object-property-accessor.js | 416 --------- test/test-options-util.js | 1609 --------------------------------- test/test-profile-conditions-util.js | 1099 ---------------------- test/test-text-source-map.js | 235 ----- test/test-translator.js | 94 -- test/test-workers.js | 137 --- test/text-source-map.test.js | 237 +++++ test/translator.test.js | 83 ++ 40 files changed, 8530 insertions(+), 8990 deletions(-) create mode 100644 test/anki-note-builder.test.js create mode 100644 test/cache-map.test.js create mode 100644 test/core.test.js create mode 100644 test/css-json.test.js create mode 100644 test/database.test.js create mode 100644 test/deinflector.test.js create mode 100644 test/dictionary.test.js create mode 100644 test/document-util.test.js create mode 100644 test/dom-text-scanner.test.js create mode 100644 test/hotkey-util.test.js create mode 100644 test/japanese-util.test.js create mode 100644 test/jsdom.test.js create mode 100644 test/json-schema.test.js create mode 100644 test/object-property-accessor.test.js create mode 100644 test/options-util.test.js create mode 100644 test/profile-conditions-util.test.js delete mode 100644 test/test-all.js delete mode 100644 test/test-anki-note-builder.js delete mode 100644 test/test-build-libs.js delete mode 100644 test/test-cache-map.js delete mode 100644 test/test-core.js delete mode 100644 test/test-css-json.js delete mode 100644 test/test-database.js delete mode 100644 test/test-deinflector.js delete mode 100644 test/test-dictionary.js delete mode 100644 test/test-document-util.js delete mode 100644 test/test-dom-text-scanner.js delete mode 100644 test/test-hotkey-util.js delete mode 100644 test/test-japanese-util.js delete mode 100644 test/test-jsdom.js delete mode 100644 test/test-json-schema.js delete mode 100644 test/test-manifest.js delete mode 100644 test/test-object-property-accessor.js delete mode 100644 test/test-options-util.js delete mode 100644 test/test-profile-conditions-util.js delete mode 100644 test/test-text-source-map.js delete mode 100644 test/test-translator.js delete mode 100644 test/test-workers.js create mode 100644 test/text-source-map.test.js create mode 100644 test/translator.test.js (limited to 'test') 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 . + */ + +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 . + */ + +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 . + */ + +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/css-json.test.js b/test/css-json.test.js new file mode 100644 index 00000000..0aaf7d10 --- /dev/null +++ b/test/css-json.test.js @@ -0,0 +1,33 @@ +/* + * 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 . + */ + +import fs from 'fs'; +import {expect, test} from 'vitest'; +import {formatRulesJson, generateRules, getTargets} from '../dev/generate-css-json'; + +function main() { + 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); + } + }); +} + +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 . + */ + +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/deinflector.test.js b/test/deinflector.test.js new file mode 100644 index 00000000..edb85833 --- /dev/null +++ b/test/deinflector.test.js @@ -0,0 +1,947 @@ +/* + * 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 . + */ + +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, source, expectedTerm, expectedRule, expectedReasons) { + for (const {term, reasons, rules} of deinflector.deinflect(source, source)) { + if (term !== expectedTerm) { continue; } + if (typeof expectedRule !== 'undefined') { + const expectedFlags = Deinflector.rulesToRuleFlags([expectedRule]); + if (rules !== 0 && (rules & expectedFlags) !== expectedFlags) { continue; } + } + let okay = true; + if (typeof expectedReasons !== 'undefined') { + if (reasons.length !== expectedReasons.length) { continue; } + for (let i = 0, ii = expectedReasons.length; i < ii; ++i) { + if (expectedReasons[i] !== reasons[i]) { + okay = false; + break; + } + } + } + if (okay) { + return {has: true, reasons, rules}; + } + } + return {has: false, reasons: null, rules: null}; +} + + +function testDeinflections() { + const data = [ + { + valid: true, + tests: [ + // Adjective + {term: '愛しい', source: '愛しい', rule: 'adj-i', reasons: []}, + {term: '愛しい', source: '愛しそう', rule: 'adj-i', reasons: ['-sou']}, + {term: '愛しい', source: '愛しすぎる', rule: 'adj-i', reasons: ['-sugiru']}, + {term: '愛しい', source: '愛しかったら', rule: 'adj-i', reasons: ['-tara']}, + {term: '愛しい', source: '愛しかったり', rule: 'adj-i', reasons: ['-tari']}, + {term: '愛しい', source: '愛しくて', rule: 'adj-i', reasons: ['-te']}, + {term: '愛しい', source: '愛しく', rule: 'adj-i', reasons: ['adv']}, + {term: '愛しい', source: '愛しくない', rule: 'adj-i', reasons: ['negative']}, + {term: '愛しい', source: '愛しさ', rule: 'adj-i', reasons: ['noun']}, + {term: '愛しい', source: '愛しかった', rule: 'adj-i', reasons: ['past']}, + {term: '愛しい', source: '愛しくありません', rule: 'adj-i', reasons: ['polite negative']}, + {term: '愛しい', source: '愛しくありませんでした', rule: 'adj-i', reasons: ['polite past negative']}, + {term: '愛しい', source: '愛しき', rule: 'adj-i', reasons: ['-ki']}, + {term: '愛しい', source: '愛しげ', rule: 'adj-i', reasons: ['-ge']}, + + // Common verbs + {term: '食べる', source: '食べる', rule: 'v1', reasons: []}, + {term: '食べる', source: '食べます', rule: 'v1', reasons: ['polite']}, + {term: '食べる', source: '食べた', rule: 'v1', reasons: ['past']}, + {term: '食べる', source: '食べました', rule: 'v1', reasons: ['polite past']}, + {term: '食べる', source: '食べて', rule: 'v1', reasons: ['-te']}, + {term: '食べる', source: '食べられる', rule: 'v1', reasons: ['potential or passive']}, + {term: '食べる', source: '食べられる', rule: 'v1', reasons: ['potential or passive']}, + {term: '食べる', source: '食べさせる', rule: 'v1', reasons: ['causative']}, + {term: '食べる', source: '食べさせられる', rule: 'v1', reasons: ['causative', 'potential or passive']}, + {term: '食べる', source: '食べろ', rule: 'v1', reasons: ['imperative']}, + {term: '食べる', source: '食べない', rule: 'v1', reasons: ['negative']}, + {term: '食べる', source: '食べません', rule: 'v1', reasons: ['polite negative']}, + {term: '食べる', source: '食べなかった', rule: 'v1', reasons: ['negative', 'past']}, + {term: '食べる', source: '食べませんでした', rule: 'v1', reasons: ['polite past negative']}, + {term: '食べる', source: '食べなくて', rule: 'v1', reasons: ['negative', '-te']}, + {term: '食べる', source: '食べられない', rule: 'v1', reasons: ['potential or passive', 'negative']}, + {term: '食べる', source: '食べられない', rule: 'v1', reasons: ['potential or passive', 'negative']}, + {term: '食べる', source: '食べさせない', rule: 'v1', reasons: ['causative', 'negative']}, + {term: '食べる', source: '食べさせられない', rule: 'v1', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '食べる', source: '食べるな', rule: 'v1', reasons: ['imperative negative']}, + + {term: '食べる', source: '食べれば', rule: 'v1', reasons: ['-ba']}, + {term: '食べる', source: '食べちゃう', rule: 'v1', reasons: ['-chau']}, + {term: '食べる', source: '食べちまう', rule: 'v1', reasons: ['-chimau']}, + {term: '食べる', source: '食べなさい', rule: 'v1', reasons: ['-nasai']}, + {term: '食べる', source: '食べそう', rule: 'v1', reasons: ['-sou']}, + {term: '食べる', source: '食べすぎる', rule: 'v1', reasons: ['-sugiru']}, + {term: '食べる', source: '食べたい', rule: 'v1', reasons: ['-tai']}, + {term: '食べる', source: '食べたら', rule: 'v1', reasons: ['-tara']}, + {term: '食べる', source: '食べたり', rule: 'v1', reasons: ['-tari']}, + {term: '食べる', source: '食べず', rule: 'v1', reasons: ['-zu']}, + {term: '食べる', source: '食べぬ', rule: 'v1', reasons: ['-nu']}, + {term: '食べる', source: '食べ', rule: 'v1', reasons: ['masu stem']}, + {term: '食べる', source: '食べましょう', rule: 'v1', reasons: ['polite volitional']}, + {term: '食べる', source: '食べよう', rule: 'v1', reasons: ['volitional']}, + // ['causative passive'] + {term: '食べる', source: '食べとく', rule: 'v1', reasons: ['-toku']}, + {term: '食べる', source: '食べている', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, + {term: '食べる', source: '食べておる', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, + {term: '食べる', source: '食べてる', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, + {term: '食べる', source: '食べとる', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, + {term: '食べる', source: '食べてしまう', rule: 'v1', reasons: ['-te', '-shimau']}, + + {term: '買う', source: '買う', rule: 'v5', reasons: []}, + {term: '買う', source: '買います', rule: 'v5', reasons: ['polite']}, + {term: '買う', source: '買った', rule: 'v5', reasons: ['past']}, + {term: '買う', source: '買いました', rule: 'v5', reasons: ['polite past']}, + {term: '買う', source: '買って', rule: 'v5', reasons: ['-te']}, + {term: '買う', source: '買える', rule: 'v5', reasons: ['potential']}, + {term: '買う', source: '買われる', rule: 'v5', reasons: ['passive']}, + {term: '買う', source: '買わせる', rule: 'v5', reasons: ['causative']}, + {term: '買う', source: '買わせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '買う', source: '買え', rule: 'v5', reasons: ['imperative']}, + {term: '買う', source: '買わない', rule: 'v5', reasons: ['negative']}, + {term: '買う', source: '買いません', rule: 'v5', reasons: ['polite negative']}, + {term: '買う', source: '買わなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '買う', source: '買いませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '買う', source: '買わなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '買う', source: '買えない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '買う', source: '買われない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '買う', source: '買わせない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '買う', source: '買わせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '買う', source: '買うな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '買う', source: '買えば', rule: 'v5', reasons: ['-ba']}, + {term: '買う', source: '買っちゃう', rule: 'v5', reasons: ['-chau']}, + {term: '買う', source: '買っちまう', rule: 'v5', reasons: ['-chimau']}, + {term: '買う', source: '買いなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '買う', source: '買いそう', rule: 'v5', reasons: ['-sou']}, + {term: '買う', source: '買いすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '買う', source: '買いたい', rule: 'v5', reasons: ['-tai']}, + {term: '買う', source: '買ったら', rule: 'v5', reasons: ['-tara']}, + {term: '買う', source: '買ったり', rule: 'v5', reasons: ['-tari']}, + {term: '買う', source: '買わず', rule: 'v5', reasons: ['-zu']}, + {term: '買う', source: '買わぬ', rule: 'v5', reasons: ['-nu']}, + {term: '買う', source: '買い', rule: 'v5', reasons: ['masu stem']}, + {term: '買う', source: '買いましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '買う', source: '買おう', rule: 'v5', reasons: ['volitional']}, + {term: '買う', source: '買わされる', rule: 'v5', reasons: ['causative passive']}, + {term: '買う', source: '買っとく', rule: 'v5', reasons: ['-toku']}, + {term: '買う', source: '買っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '買う', source: '買っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '買う', source: '買ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '買う', source: '買っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '買う', source: '買ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '行く', source: '行く', rule: 'v5', reasons: []}, + {term: '行く', source: '行きます', rule: 'v5', reasons: ['polite']}, + {term: '行く', source: '行った', rule: 'v5', reasons: ['past']}, + {term: '行く', source: '行きました', rule: 'v5', reasons: ['polite past']}, + {term: '行く', source: '行って', rule: 'v5', reasons: ['-te']}, + {term: '行く', source: '行ける', rule: 'v5', reasons: ['potential']}, + {term: '行く', source: '行かれる', rule: 'v5', reasons: ['passive']}, + {term: '行く', source: '行かせる', rule: 'v5', reasons: ['causative']}, + {term: '行く', source: '行かせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '行く', source: '行け', rule: 'v5', reasons: ['imperative']}, + {term: '行く', source: '行かない', rule: 'v5', reasons: ['negative']}, + {term: '行く', source: '行きません', rule: 'v5', reasons: ['polite negative']}, + {term: '行く', source: '行かなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '行く', source: '行きませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '行く', source: '行かなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '行く', source: '行けない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '行く', source: '行かれない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '行く', source: '行かせない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '行く', source: '行かせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '行く', source: '行くな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '行く', source: '行けば', rule: 'v5', reasons: ['-ba']}, + {term: '行く', source: '行っちゃう', rule: 'v5', reasons: ['-chau']}, + {term: '行く', source: '行っちまう', rule: 'v5', reasons: ['-chimau']}, + {term: '行く', source: '行きなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '行く', source: '行きそう', rule: 'v5', reasons: ['-sou']}, + {term: '行く', source: '行きすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '行く', source: '行きたい', rule: 'v5', reasons: ['-tai']}, + {term: '行く', source: '行いたら', rule: 'v5', reasons: ['-tara']}, + {term: '行く', source: '行いたり', rule: 'v5', reasons: ['-tari']}, + {term: '行く', source: '行かず', rule: 'v5', reasons: ['-zu']}, + {term: '行く', source: '行かぬ', rule: 'v5', reasons: ['-nu']}, + {term: '行く', source: '行き', rule: 'v5', reasons: ['masu stem']}, + {term: '行く', source: '行きましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '行く', source: '行こう', rule: 'v5', reasons: ['volitional']}, + {term: '行く', source: '行かされる', rule: 'v5', reasons: ['causative passive']}, + {term: '行く', source: '行いとく', rule: 'v5', reasons: ['-toku']}, + {term: '行く', source: '行っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '行く', source: '行っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '行く', source: '行ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '行く', source: '行っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '行く', source: '行ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '泳ぐ', source: '泳ぐ', rule: 'v5', reasons: []}, + {term: '泳ぐ', source: '泳ぎます', rule: 'v5', reasons: ['polite']}, + {term: '泳ぐ', source: '泳いだ', rule: 'v5', reasons: ['past']}, + {term: '泳ぐ', source: '泳ぎました', rule: 'v5', reasons: ['polite past']}, + {term: '泳ぐ', source: '泳いで', rule: 'v5', reasons: ['-te']}, + {term: '泳ぐ', source: '泳げる', rule: 'v5', reasons: ['potential']}, + {term: '泳ぐ', source: '泳がれる', rule: 'v5', reasons: ['passive']}, + {term: '泳ぐ', source: '泳がせる', rule: 'v5', reasons: ['causative']}, + {term: '泳ぐ', source: '泳がせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '泳ぐ', source: '泳げ', rule: 'v5', reasons: ['imperative']}, + {term: '泳ぐ', source: '泳がない', rule: 'v5', reasons: ['negative']}, + {term: '泳ぐ', source: '泳ぎません', rule: 'v5', reasons: ['polite negative']}, + {term: '泳ぐ', source: '泳がなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '泳ぐ', source: '泳ぎませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '泳ぐ', source: '泳がなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '泳ぐ', source: '泳げない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '泳ぐ', source: '泳がれない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '泳ぐ', source: '泳がせない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '泳ぐ', source: '泳がせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '泳ぐ', source: '泳ぐな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '泳ぐ', source: '泳げば', rule: 'v5', reasons: ['-ba']}, + {term: '泳ぐ', source: '泳いじゃう', rule: 'v5', reasons: ['-chau']}, + {term: '泳ぐ', source: '泳いじまう', rule: 'v5', reasons: ['-chimau']}, + {term: '泳ぐ', source: '泳ぎなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '泳ぐ', source: '泳ぎそう', rule: 'v5', reasons: ['-sou']}, + {term: '泳ぐ', source: '泳ぎすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '泳ぐ', source: '泳ぎたい', rule: 'v5', reasons: ['-tai']}, + {term: '泳ぐ', source: '泳いだら', rule: 'v5', reasons: ['-tara']}, + {term: '泳ぐ', source: '泳いだり', rule: 'v5', reasons: ['-tari']}, + {term: '泳ぐ', source: '泳がず', rule: 'v5', reasons: ['-zu']}, + {term: '泳ぐ', source: '泳がぬ', rule: 'v5', reasons: ['-nu']}, + {term: '泳ぐ', source: '泳ぎ', rule: 'v5', reasons: ['masu stem']}, + {term: '泳ぐ', source: '泳ぎましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '泳ぐ', source: '泳ごう', rule: 'v5', reasons: ['volitional']}, + {term: '泳ぐ', source: '泳がされる', rule: 'v5', reasons: ['causative passive']}, + {term: '泳ぐ', source: '泳いどく', rule: 'v5', reasons: ['-toku']}, + {term: '泳ぐ', source: '泳いでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '泳ぐ', source: '泳いでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '泳ぐ', source: '泳いでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '泳ぐ', source: '泳いでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '話す', source: '話す', rule: 'v5', reasons: []}, + {term: '話す', source: '話します', rule: 'v5', reasons: ['polite']}, + {term: '話す', source: '話した', rule: 'v5', reasons: ['past']}, + {term: '話す', source: '話しました', rule: 'v5', reasons: ['polite past']}, + {term: '話す', source: '話して', rule: 'v5', reasons: ['-te']}, + {term: '話す', source: '話せる', rule: 'v5', reasons: ['potential']}, + {term: '話す', source: '話される', rule: 'v5', reasons: ['passive']}, + {term: '話す', source: '話させる', rule: 'v5', reasons: ['causative']}, + {term: '話す', source: '話させられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '話す', source: '話せ', rule: 'v5', reasons: ['imperative']}, + {term: '話す', source: '話さない', rule: 'v5', reasons: ['negative']}, + {term: '話す', source: '話しません', rule: 'v5', reasons: ['polite negative']}, + {term: '話す', source: '話さなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '話す', source: '話しませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '話す', source: '話さなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '話す', source: '話せない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '話す', source: '話されない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '話す', source: '話させない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '話す', source: '話させられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '話す', source: '話すな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '話す', source: '話せば', rule: 'v5', reasons: ['-ba']}, + {term: '話す', source: '話しちゃう', rule: 'v5', reasons: ['-chau']}, + {term: '話す', source: '話しちまう', rule: 'v5', reasons: ['-chimau']}, + {term: '話す', source: '話しなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '話す', source: '話しそう', rule: 'v5', reasons: ['-sou']}, + {term: '話す', source: '話しすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '話す', source: '話したい', rule: 'v5', reasons: ['-tai']}, + {term: '話す', source: '話したら', rule: 'v5', reasons: ['-tara']}, + {term: '話す', source: '話したり', rule: 'v5', reasons: ['-tari']}, + {term: '話す', source: '話さず', rule: 'v5', reasons: ['-zu']}, + {term: '話す', source: '話さぬ', rule: 'v5', reasons: ['-nu']}, + {term: '話す', source: '話し', rule: 'v5', reasons: ['masu stem']}, + {term: '話す', source: '話しましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '話す', source: '話そう', rule: 'v5', reasons: ['volitional']}, + // ['causative passive'] + {term: '話す', source: '話しとく', rule: 'v5', reasons: ['-toku']}, + {term: '話す', source: '話している', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '話す', source: '話しておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '話す', source: '話してる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '話す', source: '話しとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '話す', source: '話してしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '待つ', source: '待つ', rule: 'v5', reasons: []}, + {term: '待つ', source: '待ちます', rule: 'v5', reasons: ['polite']}, + {term: '待つ', source: '待った', rule: 'v5', reasons: ['past']}, + {term: '待つ', source: '待ちました', rule: 'v5', reasons: ['polite past']}, + {term: '待つ', source: '待って', rule: 'v5', reasons: ['-te']}, + {term: '待つ', source: '待てる', rule: 'v5', reasons: ['potential']}, + {term: '待つ', source: '待たれる', rule: 'v5', reasons: ['passive']}, + {term: '待つ', source: '待たせる', rule: 'v5', reasons: ['causative']}, + {term: '待つ', source: '待たせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '待つ', source: '待て', rule: 'v5', reasons: ['imperative']}, + {term: '待つ', source: '待たない', rule: 'v5', reasons: ['negative']}, + {term: '待つ', source: '待ちません', rule: 'v5', reasons: ['polite negative']}, + {term: '待つ', source: '待たなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '待つ', source: '待ちませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '待つ', source: '待たなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '待つ', source: '待てない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '待つ', source: '待たれない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '待つ', source: '待たせない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '待つ', source: '待たせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '待つ', source: '待つな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '待つ', source: '待てば', rule: 'v5', reasons: ['-ba']}, + {term: '待つ', source: '待っちゃう', rule: 'v5', reasons: ['-chau']}, + {term: '待つ', source: '待っちまう', rule: 'v5', reasons: ['-chimau']}, + {term: '待つ', source: '待ちなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '待つ', source: '待ちそう', rule: 'v5', reasons: ['-sou']}, + {term: '待つ', source: '待ちすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '待つ', source: '待ちたい', rule: 'v5', reasons: ['-tai']}, + {term: '待つ', source: '待ったら', rule: 'v5', reasons: ['-tara']}, + {term: '待つ', source: '待ったり', rule: 'v5', reasons: ['-tari']}, + {term: '待つ', source: '待たず', rule: 'v5', reasons: ['-zu']}, + {term: '待つ', source: '待たぬ', rule: 'v5', reasons: ['-nu']}, + {term: '待つ', source: '待ち', rule: 'v5', reasons: ['masu stem']}, + {term: '待つ', source: '待ちましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '待つ', source: '待とう', rule: 'v5', reasons: ['volitional']}, + {term: '待つ', source: '待たされる', rule: 'v5', reasons: ['causative passive']}, + {term: '待つ', source: '待っとく', rule: 'v5', reasons: ['-toku']}, + {term: '待つ', source: '待っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '待つ', source: '待っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '待つ', source: '待ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '待つ', source: '待っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '待つ', source: '待ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '死ぬ', source: '死ぬ', rule: 'v5', reasons: []}, + {term: '死ぬ', source: '死にます', rule: 'v5', reasons: ['polite']}, + {term: '死ぬ', source: '死んだ', rule: 'v5', reasons: ['past']}, + {term: '死ぬ', source: '死にました', rule: 'v5', reasons: ['polite past']}, + {term: '死ぬ', source: '死んで', rule: 'v5', reasons: ['-te']}, + {term: '死ぬ', source: '死ねる', rule: 'v5', reasons: ['potential']}, + {term: '死ぬ', source: '死なれる', rule: 'v5', reasons: ['passive']}, + {term: '死ぬ', source: '死なせる', rule: 'v5', reasons: ['causative']}, + {term: '死ぬ', source: '死なせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '死ぬ', source: '死ね', rule: 'v5', reasons: ['imperative']}, + {term: '死ぬ', source: '死なない', rule: 'v5', reasons: ['negative']}, + {term: '死ぬ', source: '死にません', rule: 'v5', reasons: ['polite negative']}, + {term: '死ぬ', source: '死ななかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '死ぬ', source: '死にませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '死ぬ', source: '死ななくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '死ぬ', source: '死ねない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '死ぬ', source: '死なれない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '死ぬ', source: '死なせない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '死ぬ', source: '死なせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '死ぬ', source: '死ぬな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '死ぬ', source: '死ねば', rule: 'v5', reasons: ['-ba']}, + {term: '死ぬ', source: '死んじゃう', rule: 'v5', reasons: ['-chau']}, + {term: '死ぬ', source: '死んじまう', rule: 'v5', reasons: ['-chimau']}, + {term: '死ぬ', source: '死になさい', rule: 'v5', reasons: ['-nasai']}, + {term: '死ぬ', source: '死にそう', rule: 'v5', reasons: ['-sou']}, + {term: '死ぬ', source: '死にすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '死ぬ', source: '死にたい', rule: 'v5', reasons: ['-tai']}, + {term: '死ぬ', source: '死んだら', rule: 'v5', reasons: ['-tara']}, + {term: '死ぬ', source: '死んだり', rule: 'v5', reasons: ['-tari']}, + {term: '死ぬ', source: '死なず', rule: 'v5', reasons: ['-zu']}, + {term: '死ぬ', source: '死なぬ', rule: 'v5', reasons: ['-nu']}, + {term: '死ぬ', source: '死に', rule: 'v5', reasons: ['masu stem']}, + {term: '死ぬ', source: '死にましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '死ぬ', source: '死のう', rule: 'v5', reasons: ['volitional']}, + {term: '死ぬ', source: '死なされる', rule: 'v5', reasons: ['causative passive']}, + {term: '死ぬ', source: '死んどく', rule: 'v5', reasons: ['-toku']}, + {term: '死ぬ', source: '死んでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '死ぬ', source: '死んでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '死ぬ', source: '死んでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '死ぬ', source: '死んでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '遊ぶ', source: '遊ぶ', rule: 'v5', reasons: []}, + {term: '遊ぶ', source: '遊びます', rule: 'v5', reasons: ['polite']}, + {term: '遊ぶ', source: '遊んだ', rule: 'v5', reasons: ['past']}, + {term: '遊ぶ', source: '遊びました', rule: 'v5', reasons: ['polite past']}, + {term: '遊ぶ', source: '遊んで', rule: 'v5', reasons: ['-te']}, + {term: '遊ぶ', source: '遊べる', rule: 'v5', reasons: ['potential']}, + {term: '遊ぶ', source: '遊ばれる', rule: 'v5', reasons: ['passive']}, + {term: '遊ぶ', source: '遊ばせる', rule: 'v5', reasons: ['causative']}, + {term: '遊ぶ', source: '遊ばせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '遊ぶ', source: '遊べ', rule: 'v5', reasons: ['imperative']}, + {term: '遊ぶ', source: '遊ばない', rule: 'v5', reasons: ['negative']}, + {term: '遊ぶ', source: '遊びません', rule: 'v5', reasons: ['polite negative']}, + {term: '遊ぶ', source: '遊ばなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '遊ぶ', source: '遊びませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '遊ぶ', source: '遊ばなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '遊ぶ', source: '遊べない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '遊ぶ', source: '遊ばれない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '遊ぶ', source: '遊ばせない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '遊ぶ', source: '遊ばせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '遊ぶ', source: '遊ぶな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '遊ぶ', source: '遊べば', rule: 'v5', reasons: ['-ba']}, + {term: '遊ぶ', source: '遊んじゃう', rule: 'v5', reasons: ['-chau']}, + {term: '遊ぶ', source: '遊んじまう', rule: 'v5', reasons: ['-chimau']}, + {term: '遊ぶ', source: '遊びなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '遊ぶ', source: '遊びそう', rule: 'v5', reasons: ['-sou']}, + {term: '遊ぶ', source: '遊びすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '遊ぶ', source: '遊びたい', rule: 'v5', reasons: ['-tai']}, + {term: '遊ぶ', source: '遊んだら', rule: 'v5', reasons: ['-tara']}, + {term: '遊ぶ', source: '遊んだり', rule: 'v5', reasons: ['-tari']}, + {term: '遊ぶ', source: '遊ばず', rule: 'v5', reasons: ['-zu']}, + {term: '遊ぶ', source: '遊ばぬ', rule: 'v5', reasons: ['-nu']}, + {term: '遊ぶ', source: '遊び', rule: 'v5', reasons: ['masu stem']}, + {term: '遊ぶ', source: '遊びましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '遊ぶ', source: '遊ぼう', rule: 'v5', reasons: ['volitional']}, + {term: '遊ぶ', source: '遊ばされる', rule: 'v5', reasons: ['causative passive']}, + {term: '遊ぶ', source: '遊んどく', rule: 'v5', reasons: ['-toku']}, + {term: '遊ぶ', source: '遊んでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '遊ぶ', source: '遊んでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '遊ぶ', source: '遊んでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '遊ぶ', source: '遊んでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '飲む', source: '飲む', rule: 'v5', reasons: []}, + {term: '飲む', source: '飲みます', rule: 'v5', reasons: ['polite']}, + {term: '飲む', source: '飲んだ', rule: 'v5', reasons: ['past']}, + {term: '飲む', source: '飲みました', rule: 'v5', reasons: ['polite past']}, + {term: '飲む', source: '飲んで', rule: 'v5', reasons: ['-te']}, + {term: '飲む', source: '飲める', rule: 'v5', reasons: ['potential']}, + {term: '飲む', source: '飲まれる', rule: 'v5', reasons: ['passive']}, + {term: '飲む', source: '飲ませる', rule: 'v5', reasons: ['causative']}, + {term: '飲む', source: '飲ませられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '飲む', source: '飲め', rule: 'v5', reasons: ['imperative']}, + {term: '飲む', source: '飲まない', rule: 'v5', reasons: ['negative']}, + {term: '飲む', source: '飲みません', rule: 'v5', reasons: ['polite negative']}, + {term: '飲む', source: '飲まなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '飲む', source: '飲みませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '飲む', source: '飲まなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '飲む', source: '飲めない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '飲む', source: '飲まれない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '飲む', source: '飲ませない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '飲む', source: '飲ませられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '飲む', source: '飲むな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '飲む', source: '飲めば', rule: 'v5', reasons: ['-ba']}, + {term: '飲む', source: '飲んじゃう', rule: 'v5', reasons: ['-chau']}, + {term: '飲む', source: '飲んじまう', rule: 'v5', reasons: ['-chimau']}, + {term: '飲む', source: '飲みなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '飲む', source: '飲みそう', rule: 'v5', reasons: ['-sou']}, + {term: '飲む', source: '飲みすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '飲む', source: '飲みたい', rule: 'v5', reasons: ['-tai']}, + {term: '飲む', source: '飲んだら', rule: 'v5', reasons: ['-tara']}, + {term: '飲む', source: '飲んだり', rule: 'v5', reasons: ['-tari']}, + {term: '飲む', source: '飲まず', rule: 'v5', reasons: ['-zu']}, + {term: '飲む', source: '飲まぬ', rule: 'v5', reasons: ['-nu']}, + {term: '飲む', source: '飲み', rule: 'v5', reasons: ['masu stem']}, + {term: '飲む', source: '飲みましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '飲む', source: '飲もう', rule: 'v5', reasons: ['volitional']}, + {term: '飲む', source: '飲まされる', rule: 'v5', reasons: ['causative passive']}, + {term: '飲む', source: '飲んどく', rule: 'v5', reasons: ['-toku']}, + {term: '飲む', source: '飲んでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '飲む', source: '飲んでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '飲む', source: '飲んでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '飲む', source: '飲んでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + {term: '作る', source: '作る', rule: 'v5', reasons: []}, + {term: '作る', source: '作ります', rule: 'v5', reasons: ['polite']}, + {term: '作る', source: '作った', rule: 'v5', reasons: ['past']}, + {term: '作る', source: '作りました', rule: 'v5', reasons: ['polite past']}, + {term: '作る', source: '作って', rule: 'v5', reasons: ['-te']}, + {term: '作る', source: '作れる', rule: 'v5', reasons: ['potential']}, + {term: '作る', source: '作られる', rule: 'v5', reasons: ['passive']}, + {term: '作る', source: '作らせる', rule: 'v5', reasons: ['causative']}, + {term: '作る', source: '作らせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, + {term: '作る', source: '作れ', rule: 'v5', reasons: ['imperative']}, + {term: '作る', source: '作らない', rule: 'v5', reasons: ['negative']}, + {term: '作る', source: '作りません', rule: 'v5', reasons: ['polite negative']}, + {term: '作る', source: '作らなかった', rule: 'v5', reasons: ['negative', 'past']}, + {term: '作る', source: '作りませんでした', rule: 'v5', reasons: ['polite past negative']}, + {term: '作る', source: '作らなくて', rule: 'v5', reasons: ['negative', '-te']}, + {term: '作る', source: '作れない', rule: 'v5', reasons: ['potential', 'negative']}, + {term: '作る', source: '作られない', rule: 'v5', reasons: ['passive', 'negative']}, + {term: '作る', source: '作らせない', rule: 'v5', reasons: ['causative', 'negative']}, + {term: '作る', source: '作らせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '作る', source: '作るな', rule: 'v5', reasons: ['imperative negative']}, + + {term: '作る', source: '作れば', rule: 'v5', reasons: ['-ba']}, + {term: '作る', source: '作っちゃう', rule: 'v5', reasons: ['-chau']}, + {term: '作る', source: '作っちまう', rule: 'v5', reasons: ['-chimau']}, + {term: '作る', source: '作りなさい', rule: 'v5', reasons: ['-nasai']}, + {term: '作る', source: '作りそう', rule: 'v5', reasons: ['-sou']}, + {term: '作る', source: '作りすぎる', rule: 'v5', reasons: ['-sugiru']}, + {term: '作る', source: '作りたい', rule: 'v5', reasons: ['-tai']}, + {term: '作る', source: '作ったら', rule: 'v5', reasons: ['-tara']}, + {term: '作る', source: '作ったり', rule: 'v5', reasons: ['-tari']}, + {term: '作る', source: '作らず', rule: 'v5', reasons: ['-zu']}, + {term: '作る', source: '作らぬ', rule: 'v5', reasons: ['-nu']}, + {term: '作る', source: '作り', rule: 'v5', reasons: ['masu stem']}, + {term: '作る', source: '作りましょう', rule: 'v5', reasons: ['polite volitional']}, + {term: '作る', source: '作ろう', rule: 'v5', reasons: ['volitional']}, + {term: '作る', source: '作らされる', rule: 'v5', reasons: ['causative passive']}, + {term: '作る', source: '作っとく', rule: 'v5', reasons: ['-toku']}, + {term: '作る', source: '作っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '作る', source: '作っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '作る', source: '作ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '作る', source: '作っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, + {term: '作る', source: '作ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, + + // Irregular verbs + {term: '為る', source: '為る', rule: 'vs', reasons: []}, + {term: '為る', source: '為ます', rule: 'vs', reasons: ['polite']}, + {term: '為る', source: '為た', rule: 'vs', reasons: ['past']}, + {term: '為る', source: '為ました', rule: 'vs', reasons: ['polite past']}, + {term: '為る', source: '為て', rule: 'vs', reasons: ['-te']}, + {term: '為る', source: '為られる', rule: 'vs', reasons: ['potential or passive']}, + {term: '為る', source: '為れる', rule: 'vs', reasons: ['passive']}, + {term: '為る', source: '為せる', rule: 'vs', reasons: ['causative']}, + {term: '為る', source: '為させる', rule: 'vs', reasons: ['causative']}, + {term: '為る', source: '為せられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, + {term: '為る', source: '為させられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, + {term: '為る', source: '為ろ', rule: 'vs', reasons: ['imperative']}, + {term: '為る', source: '為ない', rule: 'vs', reasons: ['negative']}, + {term: '為る', source: '為ません', rule: 'vs', reasons: ['polite negative']}, + {term: '為る', source: '為なかった', rule: 'vs', reasons: ['negative', 'past']}, + {term: '為る', source: '為ませんでした', rule: 'vs', reasons: ['polite past negative']}, + {term: '為る', source: '為なくて', rule: 'vs', reasons: ['negative', '-te']}, + {term: '為る', source: '為られない', rule: 'vs', reasons: ['potential or passive', 'negative']}, + {term: '為る', source: '為れない', rule: 'vs', reasons: ['passive', 'negative']}, + {term: '為る', source: '為せない', rule: 'vs', reasons: ['causative', 'negative']}, + {term: '為る', source: '為させない', rule: 'vs', reasons: ['causative', 'negative']}, + {term: '為る', source: '為せられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '為る', source: '為させられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '為る', source: '為るな', rule: 'vs', reasons: ['imperative negative']}, + + {term: '為る', source: '為れば', rule: 'vs', reasons: ['-ba']}, + {term: '為る', source: '為ちゃう', rule: 'vs', reasons: ['-chau']}, + {term: '為る', source: '為ちまう', rule: 'vs', reasons: ['-chimau']}, + {term: '為る', source: '為なさい', rule: 'vs', reasons: ['-nasai']}, + {term: '為る', source: '為そう', rule: 'vs', reasons: ['-sou']}, + {term: '為る', source: '為すぎる', rule: 'vs', reasons: ['-sugiru']}, + {term: '為る', source: '為たい', rule: 'vs', reasons: ['-tai']}, + {term: '為る', source: '為たら', rule: 'vs', reasons: ['-tara']}, + {term: '為る', source: '為たり', rule: 'vs', reasons: ['-tari']}, + {term: '為る', source: '為ず', rule: 'vs', reasons: ['-zu']}, + {term: '為る', source: '為ぬ', rule: 'vs', reasons: ['-nu']}, + // ['masu stem'] + {term: '為る', source: '為ましょう', rule: 'vs', reasons: ['polite volitional']}, + {term: '為る', source: '為よう', rule: 'vs', reasons: ['volitional']}, + // ['causative passive'] + {term: '為る', source: '為とく', rule: 'vs', reasons: ['-toku']}, + {term: '為る', source: '為ている', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: '為る', source: '為ておる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: '為る', source: '為てる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: '為る', source: '為とる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: '為る', source: '為てしまう', rule: 'vs', reasons: ['-te', '-shimau']}, + + {term: 'する', source: 'する', rule: 'vs', reasons: []}, + {term: 'する', source: 'します', rule: 'vs', reasons: ['polite']}, + {term: 'する', source: 'した', rule: 'vs', reasons: ['past']}, + {term: 'する', source: 'しました', rule: 'vs', reasons: ['polite past']}, + {term: 'する', source: 'して', rule: 'vs', reasons: ['-te']}, + {term: 'する', source: 'せられる', rule: 'vs', reasons: ['potential or passive']}, + {term: 'する', source: 'される', rule: 'vs', reasons: ['passive']}, + {term: 'する', source: 'させる', rule: 'vs', reasons: ['causative']}, + {term: 'する', source: 'せさせる', rule: 'vs', reasons: ['causative']}, + {term: 'する', source: 'させられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, + {term: 'する', source: 'せさせられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, + {term: 'する', source: 'しろ', rule: 'vs', reasons: ['imperative']}, + {term: 'する', source: 'しない', rule: 'vs', reasons: ['negative']}, + {term: 'する', source: 'しません', rule: 'vs', reasons: ['polite negative']}, + {term: 'する', source: 'しなかった', rule: 'vs', reasons: ['negative', 'past']}, + {term: 'する', source: 'しませんでした', rule: 'vs', reasons: ['polite past negative']}, + {term: 'する', source: 'しなくて', rule: 'vs', reasons: ['negative', '-te']}, + {term: 'する', source: 'せられない', rule: 'vs', reasons: ['potential or passive', 'negative']}, + {term: 'する', source: 'されない', rule: 'vs', reasons: ['passive', 'negative']}, + {term: 'する', source: 'させない', rule: 'vs', reasons: ['causative', 'negative']}, + {term: 'する', source: 'せさせない', rule: 'vs', reasons: ['causative', 'negative']}, + {term: 'する', source: 'させられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, + {term: 'する', source: 'せさせられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, + {term: 'する', source: 'するな', rule: 'vs', reasons: ['imperative negative']}, + + {term: 'する', source: 'すれば', rule: 'vs', reasons: ['-ba']}, + {term: 'する', source: 'しちゃう', rule: 'vs', reasons: ['-chau']}, + {term: 'する', source: 'しちまう', rule: 'vs', reasons: ['-chimau']}, + {term: 'する', source: 'しなさい', rule: 'vs', reasons: ['-nasai']}, + {term: 'する', source: 'しそう', rule: 'vs', reasons: ['-sou']}, + {term: 'する', source: 'しすぎる', rule: 'vs', reasons: ['-sugiru']}, + {term: 'する', source: 'したい', rule: 'vs', reasons: ['-tai']}, + {term: 'する', source: 'したら', rule: 'vs', reasons: ['-tara']}, + {term: 'する', source: 'したり', rule: 'vs', reasons: ['-tari']}, + {term: 'する', source: 'せず', rule: 'vs', reasons: ['-zu']}, + {term: 'する', source: 'せぬ', rule: 'vs', reasons: ['-nu']}, + // ['masu stem'] + {term: 'する', source: 'しましょう', rule: 'vs', reasons: ['polite volitional']}, + {term: 'する', source: 'しよう', rule: 'vs', reasons: ['volitional']}, + // ['causative passive'] + {term: 'する', source: 'しとく', rule: 'vs', reasons: ['-toku']}, + {term: 'する', source: 'している', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: 'する', source: 'しておる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: 'する', source: 'してる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: 'する', source: 'しとる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, + {term: 'する', source: 'してしまう', rule: 'vs', reasons: ['-te', '-shimau']}, + + {term: '来る', source: '来る', rule: 'vk', reasons: []}, + {term: '来る', source: '来ます', rule: 'vk', reasons: ['polite']}, + {term: '来る', source: '来た', rule: 'vk', reasons: ['past']}, + {term: '来る', source: '来ました', rule: 'vk', reasons: ['polite past']}, + {term: '来る', source: '来て', rule: 'vk', reasons: ['-te']}, + {term: '来る', source: '来られる', rule: 'vk', reasons: ['potential or passive']}, + {term: '来る', source: '来られる', rule: 'vk', reasons: ['potential or passive']}, + {term: '来る', source: '来させる', rule: 'vk', reasons: ['causative']}, + {term: '来る', source: '来させられる', rule: 'vk', reasons: ['causative', 'potential or passive']}, + {term: '来る', source: '来い', rule: 'vk', reasons: ['imperative']}, + {term: '来る', source: '来ない', rule: 'vk', reasons: ['negative']}, + {term: '来る', source: '来ません', rule: 'vk', reasons: ['polite negative']}, + {term: '来る', source: '来なかった', rule: 'vk', reasons: ['negative', 'past']}, + {term: '来る', source: '来ませんでした', rule: 'vk', reasons: ['polite past negative']}, + {term: '来る', source: '来なくて', rule: 'vk', reasons: ['negative', '-te']}, + {term: '来る', source: '来られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, + {term: '来る', source: '来られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, + {term: '来る', source: '来させない', rule: 'vk', reasons: ['causative', 'negative']}, + {term: '来る', source: '来させられない', rule: 'vk', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '来る', source: '来るな', rule: 'vk', reasons: ['imperative negative']}, + + {term: '来る', source: '来れば', rule: 'vk', reasons: ['-ba']}, + {term: '来る', source: '来ちゃう', rule: 'vk', reasons: ['-chau']}, + {term: '来る', source: '来ちまう', rule: 'vk', reasons: ['-chimau']}, + {term: '来る', source: '来なさい', rule: 'vk', reasons: ['-nasai']}, + {term: '来る', source: '来そう', rule: 'vk', reasons: ['-sou']}, + {term: '来る', source: '来すぎる', rule: 'vk', reasons: ['-sugiru']}, + {term: '来る', source: '来たい', rule: 'vk', reasons: ['-tai']}, + {term: '来る', source: '来たら', rule: 'vk', reasons: ['-tara']}, + {term: '来る', source: '来たり', rule: 'vk', reasons: ['-tari']}, + {term: '来る', source: '来ず', rule: 'vk', reasons: ['-zu']}, + {term: '来る', source: '来ぬ', rule: 'vk', reasons: ['-nu']}, + {term: '来る', source: '来', rule: 'vk', reasons: ['masu stem']}, + {term: '来る', source: '来ましょう', rule: 'vk', reasons: ['polite volitional']}, + {term: '来る', source: '来よう', rule: 'vk', reasons: ['volitional']}, + // ['causative passive'] + {term: '来る', source: '来とく', rule: 'vk', reasons: ['-toku']}, + {term: '来る', source: '来ている', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '来る', source: '来ておる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '来る', source: '来てる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '来る', source: '来とる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '来る', source: '来てしまう', rule: 'vk', reasons: ['-te', '-shimau']}, + + {term: '來る', source: '來る', rule: 'vk', reasons: []}, + {term: '來る', source: '來ます', rule: 'vk', reasons: ['polite']}, + {term: '來る', source: '來た', rule: 'vk', reasons: ['past']}, + {term: '來る', source: '來ました', rule: 'vk', reasons: ['polite past']}, + {term: '來る', source: '來て', rule: 'vk', reasons: ['-te']}, + {term: '來る', source: '來られる', rule: 'vk', reasons: ['potential or passive']}, + {term: '來る', source: '來られる', rule: 'vk', reasons: ['potential or passive']}, + {term: '來る', source: '來させる', rule: 'vk', reasons: ['causative']}, + {term: '來る', source: '來させられる', rule: 'vk', reasons: ['causative', 'potential or passive']}, + {term: '來る', source: '來い', rule: 'vk', reasons: ['imperative']}, + {term: '來る', source: '來ない', rule: 'vk', reasons: ['negative']}, + {term: '來る', source: '來ません', rule: 'vk', reasons: ['polite negative']}, + {term: '來る', source: '來なかった', rule: 'vk', reasons: ['negative', 'past']}, + {term: '來る', source: '來ませんでした', rule: 'vk', reasons: ['polite past negative']}, + {term: '來る', source: '來なくて', rule: 'vk', reasons: ['negative', '-te']}, + {term: '來る', source: '來られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, + {term: '來る', source: '來られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, + {term: '來る', source: '來させない', rule: 'vk', reasons: ['causative', 'negative']}, + {term: '來る', source: '來させられない', rule: 'vk', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '來る', source: '來るな', rule: 'vk', reasons: ['imperative negative']}, + + {term: '來る', source: '來れば', rule: 'vk', reasons: ['-ba']}, + {term: '來る', source: '來ちゃう', rule: 'vk', reasons: ['-chau']}, + {term: '來る', source: '來ちまう', rule: 'vk', reasons: ['-chimau']}, + {term: '來る', source: '來なさい', rule: 'vk', reasons: ['-nasai']}, + {term: '來る', source: '來そう', rule: 'vk', reasons: ['-sou']}, + {term: '來る', source: '來すぎる', rule: 'vk', reasons: ['-sugiru']}, + {term: '來る', source: '來たい', rule: 'vk', reasons: ['-tai']}, + {term: '來る', source: '來たら', rule: 'vk', reasons: ['-tara']}, + {term: '來る', source: '來たり', rule: 'vk', reasons: ['-tari']}, + {term: '來る', source: '來ず', rule: 'vk', reasons: ['-zu']}, + {term: '來る', source: '來ぬ', rule: 'vk', reasons: ['-nu']}, + {term: '來る', source: '來', rule: 'vk', reasons: ['masu stem']}, + {term: '來る', source: '來ましょう', rule: 'vk', reasons: ['polite volitional']}, + {term: '來る', source: '來よう', rule: 'vk', reasons: ['volitional']}, + // ['causative passive'] + {term: '來る', source: '來とく', rule: 'vk', reasons: ['-toku']}, + {term: '來る', source: '來ている', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '來る', source: '來ておる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '來る', source: '來てる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '來る', source: '來とる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: '來る', source: '來てしまう', rule: 'vk', reasons: ['-te', '-shimau']}, + + {term: 'くる', source: 'くる', rule: 'vk', reasons: []}, + {term: 'くる', source: 'きます', rule: 'vk', reasons: ['polite']}, + {term: 'くる', source: 'きた', rule: 'vk', reasons: ['past']}, + {term: 'くる', source: 'きました', rule: 'vk', reasons: ['polite past']}, + {term: 'くる', source: 'きて', rule: 'vk', reasons: ['-te']}, + {term: 'くる', source: 'こられる', rule: 'vk', reasons: ['potential or passive']}, + {term: 'くる', source: 'こられる', rule: 'vk', reasons: ['potential or passive']}, + {term: 'くる', source: 'こさせる', rule: 'vk', reasons: ['causative']}, + {term: 'くる', source: 'こさせられる', rule: 'vk', reasons: ['causative', 'potential or passive']}, + {term: 'くる', source: 'こい', rule: 'vk', reasons: ['imperative']}, + {term: 'くる', source: 'こない', rule: 'vk', reasons: ['negative']}, + {term: 'くる', source: 'きません', rule: 'vk', reasons: ['polite negative']}, + {term: 'くる', source: 'こなかった', rule: 'vk', reasons: ['negative', 'past']}, + {term: 'くる', source: 'きませんでした', rule: 'vk', reasons: ['polite past negative']}, + {term: 'くる', source: 'こなくて', rule: 'vk', reasons: ['negative', '-te']}, + {term: 'くる', source: 'こられない', rule: 'vk', reasons: ['potential or passive', 'negative']}, + {term: 'くる', source: 'こられない', rule: 'vk', reasons: ['potential or passive', 'negative']}, + {term: 'くる', source: 'こさせない', rule: 'vk', reasons: ['causative', 'negative']}, + {term: 'くる', source: 'こさせられない', rule: 'vk', reasons: ['causative', 'potential or passive', 'negative']}, + {term: 'くる', source: 'くるな', rule: 'vk', reasons: ['imperative negative']}, + + {term: 'くる', source: 'くれば', rule: 'vk', reasons: ['-ba']}, + {term: 'くる', source: 'きちゃう', rule: 'vk', reasons: ['-chau']}, + {term: 'くる', source: 'きちまう', rule: 'vk', reasons: ['-chimau']}, + {term: 'くる', source: 'きなさい', rule: 'vk', reasons: ['-nasai']}, + {term: 'くる', source: 'きそう', rule: 'vk', reasons: ['-sou']}, + {term: 'くる', source: 'きすぎる', rule: 'vk', reasons: ['-sugiru']}, + {term: 'くる', source: 'きたい', rule: 'vk', reasons: ['-tai']}, + {term: 'くる', source: 'きたら', rule: 'vk', reasons: ['-tara']}, + {term: 'くる', source: 'きたり', rule: 'vk', reasons: ['-tari']}, + {term: 'くる', source: 'こず', rule: 'vk', reasons: ['-zu']}, + {term: 'くる', source: 'こぬ', rule: 'vk', reasons: ['-nu']}, + {term: 'くる', source: 'き', rule: 'vk', reasons: ['masu stem']}, + {term: 'くる', source: 'きましょう', rule: 'vk', reasons: ['polite volitional']}, + {term: 'くる', source: 'こよう', rule: 'vk', reasons: ['volitional']}, + // ['causative passive'] + {term: 'くる', source: 'きとく', rule: 'vk', reasons: ['-toku']}, + {term: 'くる', source: 'きている', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: 'くる', source: 'きておる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: 'くる', source: 'きてる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: 'くる', source: 'きとる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, + {term: 'くる', source: 'きてしまう', rule: 'vk', reasons: ['-te', '-shimau']}, + + // Zuru verbs + {term: '論ずる', source: '論ずる', rule: 'vz', reasons: []}, + {term: '論ずる', source: '論じます', rule: 'vz', reasons: ['polite']}, + {term: '論ずる', source: '論じた', rule: 'vz', reasons: ['past']}, + {term: '論ずる', source: '論じました', rule: 'vz', reasons: ['polite past']}, + {term: '論ずる', source: '論じて', rule: 'vz', reasons: ['-te']}, + {term: '論ずる', source: '論ぜられる', rule: 'vz', reasons: ['potential or passive']}, + {term: '論ずる', source: '論ざれる', rule: 'vz', reasons: ['potential or passive']}, + {term: '論ずる', source: '論じされる', rule: 'vz', reasons: ['passive']}, + {term: '論ずる', source: '論ぜされる', rule: 'vz', reasons: ['passive']}, + {term: '論ずる', source: '論じさせる', rule: 'vz', reasons: ['causative']}, + {term: '論ずる', source: '論ぜさせる', rule: 'vz', reasons: ['causative']}, + {term: '論ずる', source: '論じさせられる', rule: 'vz', reasons: ['causative', 'potential or passive']}, + {term: '論ずる', source: '論ぜさせられる', rule: 'vz', reasons: ['causative', 'potential or passive']}, + {term: '論ずる', source: '論じろ', rule: 'vz', reasons: ['imperative']}, + {term: '論ずる', source: '論じない', rule: 'vz', reasons: ['negative']}, + {term: '論ずる', source: '論じません', rule: 'vz', reasons: ['polite negative']}, + {term: '論ずる', source: '論じなかった', rule: 'vz', reasons: ['negative', 'past']}, + {term: '論ずる', source: '論じませんでした', rule: 'vz', reasons: ['polite past negative']}, + {term: '論ずる', source: '論じなくて', rule: 'vz', reasons: ['negative', '-te']}, + {term: '論ずる', source: '論ぜられない', rule: 'vz', reasons: ['potential or passive', 'negative']}, + {term: '論ずる', source: '論じされない', rule: 'vz', reasons: ['passive', 'negative']}, + {term: '論ずる', source: '論ぜされない', rule: 'vz', reasons: ['passive', 'negative']}, + {term: '論ずる', source: '論じさせない', rule: 'vz', reasons: ['causative', 'negative']}, + {term: '論ずる', source: '論ぜさせない', rule: 'vz', reasons: ['causative', 'negative']}, + {term: '論ずる', source: '論じさせられない', rule: 'vz', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '論ずる', source: '論ぜさせられない', rule: 'vz', reasons: ['causative', 'potential or passive', 'negative']}, + {term: '論ずる', source: '論ずるな', rule: 'vz', reasons: ['imperative negative']}, + + {term: '論ずる', source: '論ずれば', rule: 'vz', reasons: ['-ba']}, + {term: '論ずる', source: '論じちゃう', rule: 'vz', reasons: ['-chau']}, + {term: '論ずる', source: '論じちまう', rule: 'vz', reasons: ['-chimau']}, + {term: '論ずる', source: '論じなさい', rule: 'vz', reasons: ['-nasai']}, + {term: '論ずる', source: '論じそう', rule: 'vz', reasons: ['-sou']}, + {term: '論ずる', source: '論じすぎる', rule: 'vz', reasons: ['-sugiru']}, + {term: '論ずる', source: '論じたい', rule: 'vz', reasons: ['-tai']}, + {term: '論ずる', source: '論じたら', rule: 'vz', reasons: ['-tara']}, + {term: '論ずる', source: '論じたり', rule: 'vz', reasons: ['-tari']}, + {term: '論ずる', source: '論ぜず', rule: 'vz', reasons: ['-zu']}, + {term: '論ずる', source: '論ぜぬ', rule: 'vz', reasons: ['-nu']}, + // ['masu stem'] + {term: '論ずる', source: '論じましょう', rule: 'vz', reasons: ['polite volitional']}, + {term: '論ずる', source: '論じよう', rule: 'vz', reasons: ['volitional']}, + // ['causative passive'] + {term: '論ずる', source: '論じとく', rule: 'vz', reasons: ['-toku']}, + {term: '論ずる', source: '論じている', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, + {term: '論ずる', source: '論じておる', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, + {term: '論ずる', source: '論じてる', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, + {term: '論ずる', source: '論じとる', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, + {term: '論ずる', source: '論じてしまう', rule: 'vz', reasons: ['-te', '-shimau']}, + + // Uncommon irregular verbs + {term: 'のたまう', source: 'のたもうて', rule: 'v5', reasons: ['-te']}, + {term: 'のたまう', source: 'のたもうた', rule: 'v5', reasons: ['past']}, + {term: 'のたまう', source: 'のたもうたら', rule: 'v5', reasons: ['-tara']}, + {term: 'のたまう', source: 'のたもうたり', rule: 'v5', reasons: ['-tari']}, + + {term: 'おう', source: 'おうて', rule: 'v5', reasons: ['-te']}, + {term: 'こう', source: 'こうて', rule: 'v5', reasons: ['-te']}, + {term: 'そう', source: 'そうて', rule: 'v5', reasons: ['-te']}, + {term: 'とう', source: 'とうて', rule: 'v5', reasons: ['-te']}, + {term: '請う', source: '請うて', rule: 'v5', reasons: ['-te']}, + {term: '乞う', source: '乞うて', rule: 'v5', reasons: ['-te']}, + {term: '恋う', source: '恋うて', rule: 'v5', reasons: ['-te']}, + {term: '問う', source: '問うて', rule: 'v5', reasons: ['-te']}, + {term: '負う', source: '負うて', rule: 'v5', reasons: ['-te']}, + {term: '沿う', source: '沿うて', rule: 'v5', reasons: ['-te']}, + {term: '添う', source: '添うて', rule: 'v5', reasons: ['-te']}, + {term: '副う', source: '副うて', rule: 'v5', reasons: ['-te']}, + {term: '厭う', source: '厭うて', rule: 'v5', reasons: ['-te']}, + + {term: 'おう', source: 'おうた', rule: 'v5', reasons: ['past']}, + {term: 'こう', source: 'こうた', rule: 'v5', reasons: ['past']}, + {term: 'そう', source: 'そうた', rule: 'v5', reasons: ['past']}, + {term: 'とう', source: 'とうた', rule: 'v5', reasons: ['past']}, + {term: '請う', source: '請うた', rule: 'v5', reasons: ['past']}, + {term: '乞う', source: '乞うた', rule: 'v5', reasons: ['past']}, + {term: '恋う', source: '恋うた', rule: 'v5', reasons: ['past']}, + {term: '問う', source: '問うた', rule: 'v5', reasons: ['past']}, + {term: '負う', source: '負うた', rule: 'v5', reasons: ['past']}, + {term: '沿う', source: '沿うた', rule: 'v5', reasons: ['past']}, + {term: '添う', source: '添うた', rule: 'v5', reasons: ['past']}, + {term: '副う', source: '副うた', rule: 'v5', reasons: ['past']}, + {term: '厭う', source: '厭うた', rule: 'v5', reasons: ['past']}, + + {term: 'おう', source: 'おうたら', rule: 'v5', reasons: ['-tara']}, + {term: 'こう', source: 'こうたら', rule: 'v5', reasons: ['-tara']}, + {term: 'そう', source: 'そうたら', rule: 'v5', reasons: ['-tara']}, + {term: 'とう', source: 'とうたら', rule: 'v5', reasons: ['-tara']}, + {term: '請う', source: '請うたら', rule: 'v5', reasons: ['-tara']}, + {term: '乞う', source: '乞うたら', rule: 'v5', reasons: ['-tara']}, + {term: '恋う', source: '恋うたら', rule: 'v5', reasons: ['-tara']}, + {term: '問う', source: '問うたら', rule: 'v5', reasons: ['-tara']}, + {term: '負う', source: '負うたら', rule: 'v5', reasons: ['-tara']}, + {term: '沿う', source: '沿うたら', rule: 'v5', reasons: ['-tara']}, + {term: '添う', source: '添うたら', rule: 'v5', reasons: ['-tara']}, + {term: '副う', source: '副うたら', rule: 'v5', reasons: ['-tara']}, + {term: '厭う', source: '厭うたら', rule: 'v5', reasons: ['-tara']}, + + {term: 'おう', source: 'おうたり', rule: 'v5', reasons: ['-tari']}, + {term: 'こう', source: 'こうたり', rule: 'v5', reasons: ['-tari']}, + {term: 'そう', source: 'そうたり', rule: 'v5', reasons: ['-tari']}, + {term: 'とう', source: 'とうたり', rule: 'v5', reasons: ['-tari']}, + {term: '請う', source: '請うたり', rule: 'v5', reasons: ['-tari']}, + {term: '乞う', source: '乞うたり', rule: 'v5', reasons: ['-tari']}, + {term: '恋う', source: '恋うたり', rule: 'v5', reasons: ['-tari']}, + {term: '問う', source: '問うたり', rule: 'v5', reasons: ['-tari']}, + {term: '負う', source: '負うたり', rule: 'v5', reasons: ['-tari']}, + {term: '沿う', source: '沿うたり', rule: 'v5', reasons: ['-tari']}, + {term: '添う', source: '添うたり', rule: 'v5', reasons: ['-tari']}, + {term: '副う', source: '副うたり', rule: 'v5', reasons: ['-tari']}, + {term: '厭う', source: '厭うたり', rule: 'v5', reasons: ['-tari']}, + + // Combinations + {term: '抱き抱える', source: '抱き抱えていなければ', rule: 'v1', reasons: ['-te', 'progressive or perfect', 'negative', '-ba']}, + {term: '抱きかかえる', source: '抱きかかえていなければ', rule: 'v1', reasons: ['-te', 'progressive or perfect', 'negative', '-ba']}, + {term: '打ち込む', source: '打ち込んでいませんでした', rule: 'v5', reasons: ['-te', 'progressive or perfect', 'polite past negative']}, + {term: '食べる', source: '食べさせられたくなかった', rule: 'v1', reasons: ['causative', 'potential or passive', '-tai', 'negative', 'past']} + ] + }, + { + valid: false, + tests: [ + {term: 'する', source: 'すます', rule: 'vs'}, + {term: 'する', source: 'すた', rule: 'vs'}, + {term: 'する', source: 'すました', rule: 'vs'}, + {term: 'する', source: 'すて', rule: 'vs'}, + {term: 'する', source: 'すれる', rule: 'vs'}, + {term: 'する', source: 'すせる', rule: 'vs'}, + {term: 'する', source: 'すせられる', rule: 'vs'}, + {term: 'する', source: 'すろ', rule: 'vs'}, + {term: 'する', source: 'すない', rule: 'vs'}, + {term: 'する', source: 'すません', rule: 'vs'}, + {term: 'する', source: 'すなかった', rule: 'vs'}, + {term: 'する', source: 'すませんでした', rule: 'vs'}, + {term: 'する', source: 'すなくて', rule: 'vs'}, + {term: 'する', source: 'すれない', rule: 'vs'}, + {term: 'する', source: 'すせない', rule: 'vs'}, + {term: 'する', source: 'すせられない', rule: 'vs'}, + + {term: 'くる', source: 'くます', rule: 'vk'}, + {term: 'くる', source: 'くた', rule: 'vk'}, + {term: 'くる', source: 'くました', rule: 'vk'}, + {term: 'くる', source: 'くて', rule: 'vk'}, + {term: 'くる', source: 'くられる', rule: 'vk'}, + {term: 'くる', source: 'くられる', rule: 'vk'}, + {term: 'くる', source: 'くさせる', rule: 'vk'}, + {term: 'くる', source: 'くさせられる', rule: 'vk'}, + {term: 'くる', source: 'くい', rule: 'vk'}, + {term: 'くる', source: 'くない', rule: 'vk'}, + {term: 'くる', source: 'くません', rule: 'vk'}, + {term: 'くる', source: 'くなかった', rule: 'vk'}, + {term: 'くる', source: 'くませんでした', rule: 'vk'}, + {term: 'くる', source: 'くなくて', rule: 'vk'}, + {term: 'くる', source: 'くられない', rule: 'vk'}, + {term: 'くる', source: 'くられない', rule: 'vk'}, + {term: 'くる', source: 'くさせない', rule: 'vk'}, + {term: 'くる', source: 'くさせられない', rule: 'vk'}, + + {term: 'かわいい', source: 'かわいげ', rule: 'adj-i', reasons: ['-ge']}, + {term: '可愛い', source: 'かわいげ', rule: 'adj-i', reasons: ['-ge']} + ] + }, + { + valid: true, + tests: [ + // -e + {term: 'すごい', source: 'すげえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'やばい', source: 'やべえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'うるさい', source: 'うるせえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'ひどい', source: 'ひでえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'ない', source: 'ねえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'できる', source: 'できねえ', rule: 'v1', reasons: ['negative', '-e']}, + {term: 'しんじる', source: 'しんじねえ', rule: 'v1', reasons: ['negative', '-e']}, + {term: 'さむい', source: 'さめえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'さむい', source: 'さみい', rule: 'adj-i', reasons: ['-e']}, + {term: 'あつい', source: 'あちぇえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'あつい', source: 'あちい', rule: 'adj-i', reasons: ['-e']}, + {term: 'やすい', source: 'やせえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'たかい', source: 'たけえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'かわいい', source: 'かわええ', rule: 'adj-i', reasons: ['-e']}, + {term: 'つよい', source: 'ついぇえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'こわい', source: 'こうぇえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'みじかい', source: 'みじけえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'ながい', source: 'なげえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'くさい', source: 'くせえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'うまい', source: 'うめえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'でかい', source: 'でけえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'まずい', source: 'まっぜえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'ちっちゃい', source: 'ちっちぇえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'あかい', source: 'あけえ', rule: 'adj-i', reasons: ['-e']}, + {term: 'こわい', source: 'こええ', rule: 'adj-i', reasons: ['-e']}, + {term: 'つよい', source: 'つええ', rule: 'adj-i', reasons: ['-e']} + ] + } + ]; + + const deinflectionReasons = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/deinflect.json'))); + const deinflector = new Deinflector(deinflectionReasons); + + 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); + }); + } + } + }); +} + + +function main() { + testDeinflections(); +} + + +main(); diff --git a/test/dictionary.test.js b/test/dictionary.test.js new file mode 100644 index 00000000..8f160bc1 --- /dev/null +++ b/test/dictionary.test.js @@ -0,0 +1,59 @@ +/* + * 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 . + */ + +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); + return createDictionaryArchive(dictionaryDirectory, dictionaryName); +} + + +async function main() { + const dictionaries = [ + {name: 'valid-dictionary1', valid: true}, + {name: 'invalid-dictionary1', valid: false}, + {name: 'invalid-dictionary2', valid: false}, + {name: 'invalid-dictionary3', valid: false}, + {name: 'invalid-dictionary4', valid: false}, + {name: 'invalid-dictionary5', valid: false}, + {name: 'invalid-dictionary6', valid: false} + ]; + + const schemas = dictionaryValidate.getSchemas(); + + for (const {name, valid} of dictionaries) { + test(`${name} is ${valid ? 'valid' : 'invalid'}`, async () => { + const archive = createTestDictionaryArchive(name); + + if (valid) { + await expect(dictionaryValidate.validateDictionary(null, archive, schemas)).resolves.not.toThrow(); + } else { + await expect(dictionaryValidate.validateDictionary(null, archive, schemas)).rejects.toThrow(); + } + }); + } +} + +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 . + */ + +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/dom-text-scanner.test.js b/test/dom-text-scanner.test.js new file mode 100644 index 00000000..d1b31276 --- /dev/null +++ b/test/dom-text-scanner.test.js @@ -0,0 +1,179 @@ +/* + * 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 . + */ + +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'}); + return new JSDOM(domSource); +} + +function querySelectorTextNode(element, selector) { + let textIndex = -1; + const match = /::text$|::nth-text\((\d+)\)$/.exec(selector); + if (match !== null) { + textIndex = (match[1] ? parseInt(match[1], 10) - 1 : 0); + selector = selector.substring(0, selector.length - match[0].length); + } + const result = element.querySelector(selector); + if (textIndex < 0) { + return result; + } + for (let n = result.firstChild; n !== null; n = n.nextSibling) { + if (n.nodeType === n.constructor.TEXT_NODE) { + if (textIndex === 0) { + return n; + } + --textIndex; + } + } + return null; +} + + +function getComputedFontSizeInPixels(window, getComputedStyle, element) { + for (; element !== null; element = element.parentNode) { + if (element.nodeType === window.Node.ELEMENT_NODE) { + const fontSize = getComputedStyle(element).fontSize; + if (fontSize.endsWith('px')) { + const value = parseFloat(fontSize.substring(0, fontSize.length - 2)); + return value; + } + } + } + const defaultFontSize = 14; + return defaultFontSize; +} + +function createAbsoluteGetComputedStyle(window) { + // Wrapper to convert em units to px units + const getComputedStyleOld = window.getComputedStyle.bind(window); + return (element, ...args) => { + const style = getComputedStyleOld(element, ...args); + return new Proxy(style, { + get: (target, property) => { + let result = target[property]; + if (typeof result === 'string') { + result = result.replace(/([-+]?\d(?:\.\d)?(?:[eE][-+]?\d+)?)em/g, (g0, g1) => { + const fontSize = getComputedFontSizeInPixels(window, getComputedStyleOld, element); + return `${parseFloat(g1) * fontSize}px`; + }); + } + return result; + } + }); + }; +} + + +async function testDomTextScanner(dom) { + const document = dom.window.document; + + 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); + + const {content: actualContent} = scanner; + expect(actualContent).toStrictEqual(expectedContent.substring(0, expectedContent.length - i)); + } + + if (reversible === false) { continue; } + + // Reversed test + { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-length); + + 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)); + + const {content: actualContent} = scanner; + expect(actualContent).toStrictEqual(expectedContent.substring(i)); + } + } + } + }); +} + + +async function testDocument1() { + const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-dom-text-scanner.html')); + const window = dom.window; + try { + window.getComputedStyle = createAbsoluteGetComputedStyle(window); + + await testDomTextScanner(dom, {DOMTextScanner}); + } finally { + window.close(); + } +} + + +async function main() { + await testDocument1(); +} + +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 . + */ + +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 . + */ + +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/jsdom.test.js b/test/jsdom.test.js new file mode 100644 index 00000000..c53f374e --- /dev/null +++ b/test/jsdom.test.js @@ -0,0 +1,47 @@ +/* + * 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 . + */ + +import {JSDOM} from 'jsdom'; +import {expect, test} from 'vitest'; + +/** + * This function tests the following bug: + * - https://github.com/jsdom/jsdom/issues/3211 + * - https://github.com/dperini/nwsapi/issues/48 + */ +function testJSDOMSelectorBug() { + test('JSDOMSelectorBug', () => { + // nwsapi is used by JSDOM + const dom = new JSDOM(); + const {document} = dom.window; + const div = document.createElement('div'); + div.innerHTML = '
'; + const c = div.querySelector('.c'); + expect(() => c.matches('.a:nth-last-of-type(1) .b .c')).not.toThrow(); + }); +} + +export function testJSDOM() { + testJSDOMSelectorBug(); +} + +function main() { + testJSDOM(); +} + +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 . + */ + +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 . + */ + +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/options-util.test.js b/test/options-util.test.js new file mode 100644 index 00000000..9f49eb28 --- /dev/null +++ b/test/options-util.test.js @@ -0,0 +1,1587 @@ +/* + * 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 . + */ + +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'))) + }; +}); +vi.stubGlobal('chrome', { + runtime: { + getURL: (path2) => { + return url.pathToFileURL(path.join(dirname, '..', 'ext', path2.replace(/^\//, ''))).href; + } + } +}); + +function createProfileOptionsTestData1() { + return { + version: 14, + general: { + enable: true, + enableClipboardPopups: false, + resultOutputMode: 'group', + debugInfo: false, + maxResults: 32, + showAdvanced: false, + popupDisplayMode: 'default', + popupWidth: 400, + popupHeight: 250, + popupHorizontalOffset: 0, + popupVerticalOffset: 10, + popupHorizontalOffset2: 10, + popupVerticalOffset2: 0, + popupHorizontalTextPosition: 'below', + popupVerticalTextPosition: 'before', + popupScalingFactor: 1, + popupScaleRelativeToPageZoom: false, + popupScaleRelativeToVisualViewport: true, + showGuide: true, + compactTags: false, + compactGlossaries: false, + mainDictionary: '', + popupTheme: 'default', + popupOuterTheme: 'default', + customPopupCss: '', + customPopupOuterCss: '', + enableWanakana: true, + enableClipboardMonitor: false, + showPitchAccentDownstepNotation: true, + showPitchAccentPositionNotation: true, + showPitchAccentGraph: false, + showIframePopupsInRootFrame: false, + useSecurePopupFrameUrl: true, + usePopupShadowDom: true + }, + audio: { + enabled: true, + sources: ['jpod101', 'text-to-speech', 'custom'], + volume: 100, + autoPlay: false, + customSourceUrl: 'http://localhost/audio.mp3?term={expression}&reading={reading}', + textToSpeechVoice: 'example-voice' + }, + scanning: { + middleMouse: true, + touchInputEnabled: true, + selectText: true, + alphanumeric: true, + autoHideResults: false, + delay: 20, + length: 10, + modifier: 'shift', + deepDomScan: false, + popupNestingMaxDepth: 0, + enablePopupSearch: false, + enableOnPopupExpressions: false, + enableOnSearchPage: true, + enableSearchTags: false, + layoutAwareScan: false + }, + translation: { + convertHalfWidthCharacters: 'false', + convertNumericCharacters: 'false', + convertAlphabeticCharacters: 'false', + convertHiraganaToKatakana: 'false', + convertKatakanaToHiragana: 'variant', + collapseEmphaticSequences: 'false' + }, + dictionaries: { + 'Test Dictionary': { + priority: 0, + enabled: true, + allowSecondarySearches: false + } + }, + parsing: { + enableScanningParser: true, + enableMecabParser: false, + selectedParser: null, + termSpacing: true, + readingMode: 'hiragana' + }, + anki: { + enable: false, + server: 'http://127.0.0.1:8765', + tags: ['yomichan'], + sentenceExt: 200, + screenshot: {format: 'png', quality: 92}, + terms: {deck: '', model: '', fields: {}}, + kanji: {deck: '', model: '', fields: {}}, + duplicateScope: 'collection', + fieldTemplates: null + } + }; +} + +function createOptionsTestData1() { + return { + profiles: [ + { + name: 'Default', + options: createProfileOptionsTestData1(), + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'equal', + value: 1 + }, + { + type: 'popupLevel', + operator: 'notEqual', + value: 0 + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: 3 + }, + { + type: 'popupLevel', + operator: 'greaterThan', + value: 0 + }, + { + type: 'popupLevel', + operator: 'lessThanOrEqual', + value: 2 + }, + { + type: 'popupLevel', + operator: 'greaterThanOrEqual', + value: 1 + } + ] + }, + { + conditions: [ + { + type: 'url', + operator: 'matchDomain', + value: 'example.com' + }, + { + type: 'url', + operator: 'matchRegExp', + value: 'example\\.com' + } + ] + }, + { + conditions: [ + { + type: 'modifierKeys', + operator: 'are', + value: [ + 'ctrl', + 'shift' + ] + }, + { + type: 'modifierKeys', + operator: 'areNot', + value: [ + 'alt', + 'shift' + ] + }, + { + type: 'modifierKeys', + operator: 'include', + value: 'alt' + }, + { + type: 'modifierKeys', + operator: 'notInclude', + value: 'ctrl' + } + ] + } + ] + } + ], + profileCurrent: 0, + version: 2, + global: { + database: { + prefixWildcardsSupported: false + } + } + }; +} + + +function createProfileOptionsUpdatedTestData1() { + return { + general: { + enable: true, + resultOutputMode: 'group', + debugInfo: false, + maxResults: 32, + showAdvanced: false, + popupDisplayMode: 'default', + popupWidth: 400, + popupHeight: 250, + popupHorizontalOffset: 0, + popupVerticalOffset: 10, + popupHorizontalOffset2: 10, + popupVerticalOffset2: 0, + popupHorizontalTextPosition: 'below', + popupVerticalTextPosition: 'before', + popupScalingFactor: 1, + popupScaleRelativeToPageZoom: false, + popupScaleRelativeToVisualViewport: true, + showGuide: true, + compactTags: false, + glossaryLayoutMode: 'default', + mainDictionary: '', + popupTheme: 'light', + popupOuterTheme: 'light', + customPopupCss: '', + customPopupOuterCss: '', + enableWanakana: true, + showPitchAccentDownstepNotation: true, + showPitchAccentPositionNotation: true, + showPitchAccentGraph: false, + showIframePopupsInRootFrame: false, + useSecurePopupFrameUrl: true, + usePopupShadowDom: true, + usePopupWindow: false, + popupCurrentIndicatorMode: 'triangle', + popupActionBarVisibility: 'auto', + popupActionBarLocation: 'top', + frequencyDisplayMode: 'split-tags-grouped', + termDisplayMode: 'ruby', + sortFrequencyDictionary: null, + sortFrequencyDictionaryOrder: 'descending' + }, + audio: { + enabled: true, + sources: [ + { + type: 'jpod101', + url: '', + voice: '' + }, + { + type: 'text-to-speech', + url: '', + voice: 'example-voice' + }, + { + type: 'custom', + url: 'http://localhost/audio.mp3?term={term}&reading={reading}', + voice: '' + } + ], + volume: 100, + autoPlay: false + }, + scanning: { + touchInputEnabled: true, + selectText: true, + alphanumeric: true, + autoHideResults: false, + delay: 20, + length: 10, + deepDomScan: false, + popupNestingMaxDepth: 0, + enablePopupSearch: false, + enableOnPopupExpressions: false, + enableOnSearchPage: true, + enableSearchTags: false, + layoutAwareScan: false, + hideDelay: 0, + pointerEventsEnabled: false, + matchTypePrefix: false, + hidePopupOnCursorExit: false, + hidePopupOnCursorExitDelay: 0, + normalizeCssZoom: true, + preventMiddleMouse: { + onWebPages: false, + onPopupPages: false, + onSearchPages: false, + onSearchQuery: false + }, + inputs: [ + { + include: 'shift', + exclude: 'mouse0', + types: { + mouse: true, + touch: false, + pen: false + }, + options: { + showAdvanced: false, + searchTerms: true, + searchKanji: true, + scanOnTouchMove: true, + scanOnTouchPress: true, + scanOnTouchRelease: false, + scanOnPenMove: true, + scanOnPenHover: true, + scanOnPenReleaseHover: false, + scanOnPenPress: true, + scanOnPenRelease: false, + preventTouchScrolling: true, + preventPenScrolling: true + } + }, + { + include: 'mouse2', + exclude: '', + types: { + mouse: true, + touch: false, + pen: false + }, + options: { + showAdvanced: false, + searchTerms: true, + searchKanji: true, + scanOnTouchMove: true, + scanOnTouchPress: true, + scanOnTouchRelease: false, + scanOnPenMove: true, + scanOnPenHover: true, + scanOnPenReleaseHover: false, + scanOnPenPress: true, + scanOnPenRelease: false, + preventTouchScrolling: true, + preventPenScrolling: true + } + }, + { + include: '', + exclude: '', + types: { + mouse: false, + touch: true, + pen: true + }, + options: { + showAdvanced: false, + searchTerms: true, + searchKanji: true, + scanOnTouchMove: true, + scanOnTouchPress: true, + scanOnTouchRelease: false, + scanOnPenMove: true, + scanOnPenHover: true, + scanOnPenReleaseHover: false, + scanOnPenPress: true, + scanOnPenRelease: false, + preventTouchScrolling: true, + preventPenScrolling: true + } + } + ] + }, + translation: { + convertHalfWidthCharacters: 'false', + convertNumericCharacters: 'false', + convertAlphabeticCharacters: 'false', + convertHiraganaToKatakana: 'false', + convertKatakanaToHiragana: 'variant', + collapseEmphaticSequences: 'false', + textReplacements: { + searchOriginal: true, + groups: [] + } + }, + dictionaries: [ + { + name: 'Test Dictionary', + priority: 0, + enabled: true, + allowSecondarySearches: false, + definitionsCollapsible: 'not-collapsible' + } + ], + parsing: { + enableScanningParser: true, + enableMecabParser: false, + selectedParser: null, + termSpacing: true, + readingMode: 'hiragana' + }, + anki: { + enable: false, + server: 'http://127.0.0.1:8765', + tags: ['yomichan'], + screenshot: {format: 'png', quality: 92}, + terms: {deck: '', model: '', fields: {}}, + kanji: {deck: '', model: '', fields: {}}, + duplicateScope: 'collection', + duplicateScopeCheckAllModels: false, + displayTags: 'never', + checkForDuplicates: true, + fieldTemplates: null, + suspendNewCards: false, + noteGuiMode: 'browse', + apiKey: '', + downloadTimeout: 0 + }, + sentenceParsing: { + scanExtent: 200, + terminationCharacterMode: 'custom', + terminationCharacters: [ + {enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false}, + {enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false}, + {enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false}, + {enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false}, + {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '︒', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '︕', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '︖', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, + {enabled: true, character1: '︙', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true} + ] + }, + inputs: { + hotkeys: [ + {action: 'close', argument: '', key: 'Escape', modifiers: [], scopes: ['popup'], enabled: true}, + {action: 'focusSearchBox', argument: '', key: 'Escape', modifiers: [], scopes: ['search'], enabled: true}, + {action: 'previousEntry', argument: '3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'nextEntry', argument: '3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'lastEntry', argument: '', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'firstEntry', argument: '', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'previousEntry', argument: '1', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'nextEntry', argument: '1', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'historyBackward', argument: '', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'historyForward', argument: '', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteKanji', argument: '', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteTermKanji', argument: '', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'addNoteTermKana', argument: '', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'playAudio', argument: '', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'viewNote', argument: '', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, + {action: 'copyHostSelection', argument: '', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true} + ] + }, + popupWindow: { + width: 400, + height: 250, + left: 0, + top: 0, + useLeft: false, + useTop: false, + windowType: 'popup', + windowState: 'normal' + }, + clipboard: { + enableBackgroundMonitor: false, + enableSearchPageMonitor: false, + autoSearchContent: true, + maximumSearchLength: 1000 + }, + accessibility: { + forceGoogleDocsHtmlRendering: false + } + }; +} + +function createOptionsUpdatedTestData1() { + return { + profiles: [ + { + name: 'Default', + options: createProfileOptionsUpdatedTestData1(), + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'equal', + value: '1' + }, + { + type: 'popupLevel', + operator: 'notEqual', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: '3' + }, + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThanOrEqual', + value: '2' + }, + { + type: 'popupLevel', + operator: 'greaterThanOrEqual', + value: '1' + } + ] + }, + { + conditions: [ + { + type: 'url', + operator: 'matchDomain', + value: 'example.com' + }, + { + type: 'url', + operator: 'matchRegExp', + value: 'example\\.com' + } + ] + }, + { + conditions: [ + { + type: 'modifierKeys', + operator: 'are', + value: 'ctrl, shift' + }, + { + type: 'modifierKeys', + operator: 'areNot', + value: 'alt, shift' + }, + { + type: 'modifierKeys', + operator: 'include', + value: 'alt' + }, + { + type: 'modifierKeys', + operator: 'notInclude', + value: 'ctrl' + } + ] + } + ] + } + ], + profileCurrent: 0, + version: 21, + global: { + database: { + prefixWildcardsSupported: false + } + } + }; +} + + +async function testUpdate() { + test('Update', async () => { + const optionsUtil = new OptionsUtil(); + await optionsUtil.prepare(); + + const options = createOptionsTestData1(); + const optionsUpdated = structuredClone(await optionsUtil.update(options)); + const optionsExpected = createOptionsUpdatedTestData1(); + expect(optionsUpdated).toStrictEqual(optionsExpected); + }); +} + +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 optionsUtil = new OptionsUtil(); + await optionsUtil.prepare(); + + for (const modify of data) { + const options = optionsUtil.getDefault(); + + const optionsModified = structuredClone(options); + modify(optionsModified); + + const optionsUpdated = await optionsUtil.update(structuredClone(optionsModified)); + expect(structuredClone(optionsUpdated)).toStrictEqual(structuredClone(options)); + } + }); +} + +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(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 = [ + // Standard format + { + oldVersion: 0, + newVersion: 12, + old: ` +{{#*inline "character"}} + {{~definition.character~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "character"}} + {{~definition.character~}} +{{/inline}} + +<<>> +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // Non-standard marker format + { + oldVersion: 0, + newVersion: 12, + old: ` +{{#*inline "character"}} + {{~definition.character~}} +{{/inline}} + +{{~> (lookup . "marker2") ~}}`.trimStart(), + + expected: ` +{{#*inline "character"}} + {{~definition.character~}} +{{/inline}} + +{{~> (lookup . "marker2") ~}} +<<>>`.trimStart() + }, + // Empty test + { + oldVersion: 0, + newVersion: 12, + old: ` +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +<<>> +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // Definition tags update + { + oldVersion: 0, + newVersion: 12, + old: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#if definitionTags~}}({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}) {{/if~}} + {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} +{{/inline}} + +{{#*inline "glossary-single2"}} + {{~#unless brief~}} + {{~#if definitionTags~}}({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}) {{/if~}} + {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} +{{/inline}} + +{{#*inline "glossary"}} + {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries~}} + {{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries~}} +{{/inline}} + +{{~> (lookup . "marker") ~}} +`.trimStart(), + + expected: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#scope~}} + {{~#set "any" false}}{{/set~}} + {{~#if definitionTags~}}{{#each definitionTags~}} + {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/each~}} + {{~#if (get "any")}}) {{/if~}} + {{~/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} +{{/inline}} + +{{#*inline "glossary-single2"}} + {{~#unless brief~}} + {{~#scope~}} + {{~#set "any" false}}{{/set~}} + {{~#if definitionTags~}}{{#each definitionTags~}} + {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/each~}} + {{~#if (get "any")}}) {{/if~}} + {{~/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} +{{/inline}} + +{{#*inline "glossary"}} + {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}} + {{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries data=../.~}} +{{/inline}} + +<<>> +{{~> (lookup . "marker") ~}} +`.trimStart() + }, + // glossary and glossary-brief update + { + oldVersion: 7, + newVersion: 12, + old: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#scope~}} + {{~#set "any" false}}{{/set~}} + {{~#if definitionTags~}}{{#each definitionTags~}} + {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/each~}} + {{~#if (get "any")}}) {{/if~}} + {{~/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} + {{~#if glossary.[1]~}} + {{~#if compactGlossaries~}} + {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} +
    {{#each glossary}}
  • {{#multiLine}}{{.}}{{/multiLine}}
  • {{/each}}
+ {{~/if~}} + {{~else~}} + {{~#multiLine}}{{glossary.[0]}}{{/multiLine~}} + {{~/if~}} +{{/inline}} + +{{#*inline "character"}} + {{~definition.character~}} +{{/inline}} + +{{#*inline "glossary"}} +
+ {{~#if modeKanji~}} + {{~#if definition.glossary.[1]~}} +
    {{#each definition.glossary}}
  1. {{.}}
  2. {{/each}}
+ {{~else~}} + {{definition.glossary.[0]}} + {{~/if~}} + {{~else~}} + {{~#if group~}} + {{~#if definition.definitions.[1]~}} +
    {{#each definition.definitions}}
  1. {{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}
  2. {{/each}}
+ {{~else~}} + {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}} + {{~/if~}} + {{~else if merge~}} + {{~#if definition.definitions.[1]~}} +
    {{#each definition.definitions}}
  1. {{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}
  2. {{/each}}
+ {{~else~}} + {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}} + {{~/if~}} + {{~else~}} + {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}} + {{~/if~}} + {{~/if~}} +
+{{/inline}} + +{{#*inline "glossary-brief"}} + {{~> glossary brief=true ~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#scope~}} + {{~#set "any" false}}{{/set~}} + {{~#each definitionTags~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/each~}} + {{~#unless noDictionaryTag~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{dictionary}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/unless~}} + {{~#if (get "any")}}) {{/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} + {{~#if (op "<=" glossary.length 1)~}} + {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} + {{~else if @root.compactGlossaries~}} + {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} +
    {{#each glossary}}
  • {{#multiLine}}{{.}}{{/multiLine}}
  • {{/each}}
+ {{~/if~}} + {{~#set "previousDictionary" dictionary~}}{{~/set~}} +{{/inline}} + +{{#*inline "character"}} + {{~definition.character~}} +{{/inline}} + +{{~#*inline "glossary"~}} +
+ {{~#scope~}} + {{~#if (op "===" definition.type "term")~}} + {{~> glossary-single definition brief=brief noDictionaryTag=noDictionaryTag ~}} + {{~else if (op "||" (op "===" definition.type "termGrouped") (op "===" definition.type "termMerged"))~}} + {{~#if (op ">" definition.definitions.length 1)~}} +
    {{~#each definition.definitions~}}
  1. {{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}
  2. {{~/each~}}
+ {{~else~}} + {{~#each definition.definitions~}}{{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}{{~/each~}} + {{~/if~}} + {{~else if (op "===" definition.type "kanji")~}} + {{~#if (op ">" definition.glossary.length 1)~}} +
    {{#each definition.glossary}}
  1. {{.}}
  2. {{/each}}
+ {{~else~}} + {{~#each definition.glossary~}}{{.}}{{~/each~}} + {{~/if~}} + {{~/if~}} + {{~/scope~}} +
+{{~/inline~}} + +{{#*inline "glossary-no-dictionary"}} + {{~> glossary noDictionaryTag=true ~}} +{{/inline}} + +{{#*inline "glossary-brief"}} + {{~> glossary brief=true ~}} +{{/inline}} + +<<>> +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // formatGlossary update + { + oldVersion: 12, + newVersion: 13, + old: ` +{{#*inline "example"}} + {{~#if (op "<=" glossary.length 1)~}} + {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} + {{~else if @root.compactGlossaries~}} + {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} +
    {{#each glossary}}
  • {{#multiLine}}{{.}}{{/multiLine}}
  • {{/each}}
+ {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "example"}} + {{~#if (op "<=" glossary.length 1)~}} + {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} + {{~else if @root.compactGlossaries~}} + {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} +
    {{#each glossary}}
  • {{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}
  • {{/each}}
+ {{~/if~}} +{{/inline}} + +<<>> +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // hasMedia/getMedia update + { + oldVersion: 12, + newVersion: 13, + old: ` +{{#*inline "audio"}} + {{~#if definition.audioFileName~}} + [sound:{{definition.audioFileName}}] + {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} + +{{/inline}} + +{{#*inline "clipboard-image"}} + {{~#if definition.clipboardImageFileName~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} + {{~#if definition.clipboardText~}}{{definition.clipboardText}}{{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "audio"}} + {{~#if (hasMedia "audio")~}} + [sound:{{#getMedia "audio"}}{{/getMedia}}] + {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} + {{~#if (hasMedia "screenshot")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} + {{~#if (hasMedia "clipboardImage")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} + {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +<<>> +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // hasMedia/getMedia update + { + oldVersion: 12, + newVersion: 13, + old: ` +{{! Pitch Accents }} +{{#*inline "pitch-accent-item-downstep-notation"}} + {{~#scope~}} + + {{~#set "style1a"~}}display:inline-block;position:relative;{{~/set~}} + {{~#set "style1b"~}}padding-right:0.1em;margin-right:0.1em;{{~/set~}} + {{~#set "style2a"~}}display:block;user-select:none;pointer-events:none;position:absolute;top:0.1em;left:0;right:0;height:0;border-top:0.1em solid;{{~/set~}} + {{~#set "style2b"~}}right:-0.1em;height:0.4em;border-right:0.1em solid;{{~/set~}} + {{~#each (getKanaMorae reading)~}} + {{~#set "style1"}}{{#get "style1a"}}{{/get}}{{/set~}} + {{~#set "style2"}}{{/set~}} + {{~#if (isMoraPitchHigh @index ../position)}} + {{~#set "style2"}}{{#get "style2a"}}{{/get}}{{/set~}} + {{~#if (op "!" (isMoraPitchHigh (op "+" @index 1) ../position))~}} + {{~#set "style1" (op "+" (get "style1") (get "style1b"))}}{{/set~}} + {{~#set "style2" (op "+" (get "style2") (get "style2b"))}}{{/set~}} + {{~/if~}} + {{~/if~}} + {{{.}}} + {{~/each~}} + + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-item-graph-position-x"}}{{#op "+" 25 (op "*" index 50)}}{{/op}}{{/inline}} +{{#*inline "pitch-accent-item-graph-position-y"}}{{#op "+" 25 (op "?:" (isMoraPitchHigh index position) 0 50)}}{{/op}}{{/inline}} +{{#*inline "pitch-accent-item-graph-position"}}{{> pitch-accent-item-graph-position-x index=index position=position}} {{> pitch-accent-item-graph-position-y index=index position=position}}{{/inline}} +{{#*inline "pitch-accent-item-graph"}} + {{~#scope~}} + {{~#set "morae" (getKanaMorae reading)}}{{/set~}} + {{~#set "morae-count" (property (get "morae") "length")}}{{/set~}} + + + + + + + pitch-accent-item-graph-position index=@index position=../position~}} + {{~#set "cmd" "L"}}{{/set~}} + {{~/each~}} + "> + pitch-accent-item-graph-position index=(get "morae-count") position=position}}"> + {{#each (get "morae")}} + + {{/each}} + + + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-item-position"~}} + [{{position}}] +{{~/inline}} + +{{#*inline "pitch-accent-item"}} + {{~#if (op "==" format "downstep-notation")~}} + {{~> pitch-accent-item-downstep-notation~}} + {{~else if (op "==" format "graph")~}} + {{~> pitch-accent-item-graph~}} + {{~else if (op "==" format "position")~}} + {{~> pitch-accent-item-position~}} + {{~/if~}} +{{/inline}} + +{{#*inline "pitch-accent-item-disambiguation"}} + {{~#scope~}} + {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} + {{~#if (op ">" (property (get "exclusive") "length") 0)~}} + {{~#set "separator" ""~}}{{/set~}} + ({{#each (get "exclusive")~}} + {{~#get "separator"}}{{/get~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-list"}} + {{~#if (op ">" pitchCount 0)~}} + {{~#if (op ">" pitchCount 1)~}}
    {{~/if~}} + {{~#each pitches~}} + {{~#each pitches~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  1. {{~/if~}} + {{~> pitch-accent-item-disambiguation~}} + {{~> pitch-accent-item format=../../format~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  2. {{~/if~}} + {{~/each~}} + {{~/each~}} + {{~#if (op ">" pitchCount 1)~}}
{{~/if~}} + {{~else~}} + No pitch accent data + {{~/if~}} +{{/inline}} + +{{#*inline "pitch-accents"}} + {{~> pitch-accent-list format='downstep-notation'~}} +{{/inline}} + +{{#*inline "pitch-accent-graphs"}} + {{~> pitch-accent-list format='graph'~}} +{{/inline}} + +{{#*inline "pitch-accent-positions"}} + {{~> pitch-accent-list format='position'~}} +{{/inline}} +{{! End Pitch Accents }} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{! Pitch Accents }} +{{#*inline "pitch-accent-item"}} + {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} +{{/inline}} + +{{#*inline "pitch-accent-item-disambiguation"}} + {{~#scope~}} + {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} + {{~#if (op ">" (property (get "exclusive") "length") 0)~}} + {{~#set "separator" ""~}}{{/set~}} + ({{#each (get "exclusive")~}} + {{~#get "separator"}}{{/get~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "pitch-accent-list"}} + {{~#if (op ">" pitchCount 0)~}} + {{~#if (op ">" pitchCount 1)~}}
    {{~/if~}} + {{~#each pitches~}} + {{~#each pitches~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  1. {{~/if~}} + {{~> pitch-accent-item-disambiguation~}} + {{~> pitch-accent-item format=../../format~}} + {{~#if (op ">" ../../pitchCount 1)~}}
  2. {{~/if~}} + {{~/each~}} + {{~/each~}} + {{~#if (op ">" pitchCount 1)~}}
{{~/if~}} + {{~else~}} + No pitch accent data + {{~/if~}} +{{/inline}} + +{{#*inline "pitch-accents"}} + {{~> pitch-accent-list format='text'~}} +{{/inline}} + +{{#*inline "pitch-accent-graphs"}} + {{~> pitch-accent-list format='graph'~}} +{{/inline}} + +{{#*inline "pitch-accent-positions"}} + {{~> pitch-accent-list format='position'~}} +{{/inline}} +{{! End Pitch Accents }} + +<<>> +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: furigana and furiganaPlain + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "furigana"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~#furigana}}{{{.}}}{{/furigana~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{#furigana}}{{{definition}}}{{/furigana}} + {{~/if~}} +{{/inline}} + +{{#*inline "furigana-plain"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} + {{~/if~}} +{{/inline}} + +{{#*inline "frequencies"}} + {{~#if (op ">" definition.frequencies.length 0)~}} +
    + {{~#each definition.frequencies~}} +
  • + {{~#if (op "!==" ../definition.type "kanji")~}} + {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( + {{~#furigana expression reading~}}{{~/furigana~}} + ) {{/if~}} + {{~/if~}} + {{~dictionary}}: {{frequency~}} +
  • + {{~/each~}} +
+ {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "furigana"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~furigana .~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{furigana definition}} + {{~/if~}} +{{/inline}} + +{{#*inline "furigana-plain"}} + {{~#if merge~}} + {{~#each definition.expressions~}} + {{~furiganaPlain .~}} + {{~#unless @last}}、{{/unless~}} + {{~/each~}} + {{~else~}} + {{furiganaPlain definition}} + {{~/if~}} +{{/inline}} + +{{#*inline "frequencies"}} + {{~#if (op ">" definition.frequencies.length 0)~}} +
    + {{~#each definition.frequencies~}} +
  • + {{~#if (op "!==" ../definition.type "kanji")~}} + {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( + {{~furigana expression reading~}} + ) {{/if~}} + {{~/if~}} + {{~dictionary}}: {{frequency~}} +
  • + {{~/each~}} +
+ {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: formatGlossary + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#scope~}} + {{~#set "any" false}}{{/set~}} + {{~#each definitionTags~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/each~}} + {{~#unless noDictionaryTag~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{dictionary}} + {{~#set "any" true}}{{/set~}} + {{~/if~}} + {{~/unless~}} + {{~#if (get "any")}}) {{/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} + {{~#if (op "<=" glossary.length 1)~}} + {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} + {{~else if @root.compactGlossaries~}} + {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} +
    {{#each glossary}}
  • {{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}
  • {{/each}}
+ {{~/if~}} + {{~#set "previousDictionary" dictionary~}}{{~/set~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "glossary-single"}} + {{~#unless brief~}} + {{~#scope~}} + {{~set "any" false~}} + {{~#each definitionTags~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{name}} + {{~set "any" true~}} + {{~/if~}} + {{~/each~}} + {{~#unless noDictionaryTag~}} + {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} + {{~#if (get "any")}}, {{else}}({{/if~}} + {{dictionary}} + {{~set "any" true~}} + {{~/if~}} + {{~/unless~}} + {{~#if (get "any")}}) {{/if~}} + {{~/scope~}} + {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} + {{~/unless~}} + {{~#if (op "<=" glossary.length 1)~}} + {{#each glossary}}{{formatGlossary ../dictionary .}}{{/each}} + {{~else if @root.compactGlossaries~}} + {{#each glossary}}{{formatGlossary ../dictionary .}}{{#unless @last}} | {{/unless}}{{/each}} + {{~else~}} +
    {{#each glossary}}
  • {{formatGlossary ../dictionary .}}
  • {{/each}}
+ {{~/if~}} + {{~set "previousDictionary" dictionary~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: set and get + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "pitch-accent-item-disambiguation"}} + {{~#scope~}} + {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} + {{~#if (op ">" (property (get "exclusive") "length") 0)~}} + {{~#set "separator" ""~}}{{/set~}} + ({{#each (get "exclusive")~}} + {{~#get "separator"}}{{/get~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "stroke-count"}} + {{~#scope~}} + {{~#set "found" false}}{{/set~}} + {{~#each definition.stats.misc~}} + {{~#if (op "===" name "strokes")~}} + {{~#set "found" true}}{{/set~}} + Stroke count: {{value}} + {{~/if~}} + {{~/each~}} + {{~#if (op "!" (get "found"))~}} + Stroke count: Unknown + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "part-of-speech"}} + {{~#scope~}} + {{~#if (op "!==" definition.type "kanji")~}} + {{~#set "first" true}}{{/set~}} + {{~#each definition.expressions~}} + {{~#each wordClasses~}} + {{~#unless (get (concat "used_" .))~}} + {{~> part-of-speech-pretty . ~}} + {{~#unless (get "first")}}, {{/unless~}} + {{~#set (concat "used_" .) true~}}{{~/set~}} + {{~#set "first" false~}}{{~/set~}} + {{~/unless~}} + {{~/each~}} + {{~/each~}} + {{~#if (get "first")~}}Unknown{{~/if~}} + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "pitch-accent-item-disambiguation"}} + {{~#scope~}} + {{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}} + {{~#if (op ">" (property (get "exclusive") "length") 0)~}} + {{~set "separator" ""~}} + ({{#each (get "exclusive")~}} + {{~get "separator"~}}{{{.}}} + {{~/each}} only) + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "stroke-count"}} + {{~#scope~}} + {{~set "found" false~}} + {{~#each definition.stats.misc~}} + {{~#if (op "===" name "strokes")~}} + {{~set "found" true~}} + Stroke count: {{value}} + {{~/if~}} + {{~/each~}} + {{~#if (op "!" (get "found"))~}} + Stroke count: Unknown + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{#*inline "part-of-speech"}} + {{~#scope~}} + {{~#if (op "!==" definition.type "kanji")~}} + {{~set "first" true~}} + {{~#each definition.expressions~}} + {{~#each wordClasses~}} + {{~#unless (get (concat "used_" .))~}} + {{~> part-of-speech-pretty . ~}} + {{~#unless (get "first")}}, {{/unless~}} + {{~set (concat "used_" .) true~}} + {{~set "first" false~}} + {{~/unless~}} + {{~/each~}} + {{~/each~}} + {{~#if (get "first")~}}Unknown{{~/if~}} + {{~/if~}} + {{~/scope~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // block helper update: hasMedia and getMedia + { + oldVersion: 20, + newVersion: 21, + old: ` +{{#*inline "audio"}} + {{~#if (hasMedia "audio")~}} + [sound:{{#getMedia "audio"}}{{/getMedia}}] + {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} + {{~#if (hasMedia "screenshot")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} + {{~#if (hasMedia "clipboardImage")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} + {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +{{#*inline "selection-text"}} + {{~#if (hasMedia "selectionText")}}{{#getMedia "selectionText"}}{{/getMedia}}{{/if~}} +{{/inline}} + +{{#*inline "sentence-furigana"}} + {{~#if definition.cloze~}} + {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} + {{#getMedia "textFurigana" definition.cloze.sentence escape=false}}{{/getMedia}} + {{~else~}} + {{definition.cloze.sentence}} + {{~/if~}} + {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart(), + + expected: ` +{{#*inline "audio"}} + {{~#if (hasMedia "audio")~}} + [sound:{{getMedia "audio"}}] + {{~/if~}} +{{/inline}} + +{{#*inline "screenshot"}} + {{~#if (hasMedia "screenshot")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-image"}} + {{~#if (hasMedia "clipboardImage")~}} + + {{~/if~}} +{{/inline}} + +{{#*inline "clipboard-text"}} + {{~#if (hasMedia "clipboardText")}}{{getMedia "clipboardText"}}{{/if~}} +{{/inline}} + +{{#*inline "selection-text"}} + {{~#if (hasMedia "selectionText")}}{{getMedia "selectionText"}}{{/if~}} +{{/inline}} + +{{#*inline "sentence-furigana"}} + {{~#if definition.cloze~}} + {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} + {{getMedia "textFurigana" definition.cloze.sentence escape=false}} + {{~else~}} + {{definition.cloze.sentence}} + {{~/if~}} + {{~/if~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + }, + // 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: ` +{{#*inline "pitch-accent-item"}} + {{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}} +{{/inline}} + +{{~> (lookup . "marker") ~}}`.trimStart() + } + ]; + + const updatesPattern = /<<>>/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 optionsUpdated = structuredClone(await optionsUtil.update(options, newVersion)); + const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates; + expect(fieldTemplatesActual).toStrictEqual(expected2); + } + }); +} + + +async function main() { + await testUpdate(); + await testDefault(); + await testFieldTemplatesUpdate(); +} + +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 . + */ + +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 . - */ - -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 . - */ - -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 . - */ - -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 . - */ - -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 . - */ - -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-css-json.js b/test/test-css-json.js deleted file mode 100644 index ddeee6bd..00000000 --- a/test/test-css-json.js +++ /dev/null @@ -1,37 +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 . - */ - -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'); - - -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); - } -} - - -if (require.main === module) { - testMain(main, process.argv.slice(2)); -} 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 . - */ - -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-deinflector.js b/test/test-deinflector.js deleted file mode 100644 index a20cfc95..00000000 --- a/test/test-deinflector.js +++ /dev/null @@ -1,952 +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 . - */ - -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - - -function hasTermReasons(Deinflector, deinflector, source, expectedTerm, expectedRule, expectedReasons) { - for (const {term, reasons, rules} of deinflector.deinflect(source, source)) { - if (term !== expectedTerm) { continue; } - if (typeof expectedRule !== 'undefined') { - const expectedFlags = Deinflector.rulesToRuleFlags([expectedRule]); - if (rules !== 0 && (rules & expectedFlags) !== expectedFlags) { continue; } - } - let okay = true; - if (typeof expectedReasons !== 'undefined') { - if (reasons.length !== expectedReasons.length) { continue; } - for (let i = 0, ii = expectedReasons.length; i < ii; ++i) { - if (expectedReasons[i] !== reasons[i]) { - okay = false; - break; - } - } - } - if (okay) { - return {has: true, reasons, rules}; - } - } - return {has: false, reasons: null, rules: null}; -} - - -function testDeinflections() { - const data = [ - { - valid: true, - tests: [ - // Adjective - {term: '愛しい', source: '愛しい', rule: 'adj-i', reasons: []}, - {term: '愛しい', source: '愛しそう', rule: 'adj-i', reasons: ['-sou']}, - {term: '愛しい', source: '愛しすぎる', rule: 'adj-i', reasons: ['-sugiru']}, - {term: '愛しい', source: '愛しかったら', rule: 'adj-i', reasons: ['-tara']}, - {term: '愛しい', source: '愛しかったり', rule: 'adj-i', reasons: ['-tari']}, - {term: '愛しい', source: '愛しくて', rule: 'adj-i', reasons: ['-te']}, - {term: '愛しい', source: '愛しく', rule: 'adj-i', reasons: ['adv']}, - {term: '愛しい', source: '愛しくない', rule: 'adj-i', reasons: ['negative']}, - {term: '愛しい', source: '愛しさ', rule: 'adj-i', reasons: ['noun']}, - {term: '愛しい', source: '愛しかった', rule: 'adj-i', reasons: ['past']}, - {term: '愛しい', source: '愛しくありません', rule: 'adj-i', reasons: ['polite negative']}, - {term: '愛しい', source: '愛しくありませんでした', rule: 'adj-i', reasons: ['polite past negative']}, - {term: '愛しい', source: '愛しき', rule: 'adj-i', reasons: ['-ki']}, - {term: '愛しい', source: '愛しげ', rule: 'adj-i', reasons: ['-ge']}, - - // Common verbs - {term: '食べる', source: '食べる', rule: 'v1', reasons: []}, - {term: '食べる', source: '食べます', rule: 'v1', reasons: ['polite']}, - {term: '食べる', source: '食べた', rule: 'v1', reasons: ['past']}, - {term: '食べる', source: '食べました', rule: 'v1', reasons: ['polite past']}, - {term: '食べる', source: '食べて', rule: 'v1', reasons: ['-te']}, - {term: '食べる', source: '食べられる', rule: 'v1', reasons: ['potential or passive']}, - {term: '食べる', source: '食べられる', rule: 'v1', reasons: ['potential or passive']}, - {term: '食べる', source: '食べさせる', rule: 'v1', reasons: ['causative']}, - {term: '食べる', source: '食べさせられる', rule: 'v1', reasons: ['causative', 'potential or passive']}, - {term: '食べる', source: '食べろ', rule: 'v1', reasons: ['imperative']}, - {term: '食べる', source: '食べない', rule: 'v1', reasons: ['negative']}, - {term: '食べる', source: '食べません', rule: 'v1', reasons: ['polite negative']}, - {term: '食べる', source: '食べなかった', rule: 'v1', reasons: ['negative', 'past']}, - {term: '食べる', source: '食べませんでした', rule: 'v1', reasons: ['polite past negative']}, - {term: '食べる', source: '食べなくて', rule: 'v1', reasons: ['negative', '-te']}, - {term: '食べる', source: '食べられない', rule: 'v1', reasons: ['potential or passive', 'negative']}, - {term: '食べる', source: '食べられない', rule: 'v1', reasons: ['potential or passive', 'negative']}, - {term: '食べる', source: '食べさせない', rule: 'v1', reasons: ['causative', 'negative']}, - {term: '食べる', source: '食べさせられない', rule: 'v1', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '食べる', source: '食べるな', rule: 'v1', reasons: ['imperative negative']}, - - {term: '食べる', source: '食べれば', rule: 'v1', reasons: ['-ba']}, - {term: '食べる', source: '食べちゃう', rule: 'v1', reasons: ['-chau']}, - {term: '食べる', source: '食べちまう', rule: 'v1', reasons: ['-chimau']}, - {term: '食べる', source: '食べなさい', rule: 'v1', reasons: ['-nasai']}, - {term: '食べる', source: '食べそう', rule: 'v1', reasons: ['-sou']}, - {term: '食べる', source: '食べすぎる', rule: 'v1', reasons: ['-sugiru']}, - {term: '食べる', source: '食べたい', rule: 'v1', reasons: ['-tai']}, - {term: '食べる', source: '食べたら', rule: 'v1', reasons: ['-tara']}, - {term: '食べる', source: '食べたり', rule: 'v1', reasons: ['-tari']}, - {term: '食べる', source: '食べず', rule: 'v1', reasons: ['-zu']}, - {term: '食べる', source: '食べぬ', rule: 'v1', reasons: ['-nu']}, - {term: '食べる', source: '食べ', rule: 'v1', reasons: ['masu stem']}, - {term: '食べる', source: '食べましょう', rule: 'v1', reasons: ['polite volitional']}, - {term: '食べる', source: '食べよう', rule: 'v1', reasons: ['volitional']}, - // ['causative passive'] - {term: '食べる', source: '食べとく', rule: 'v1', reasons: ['-toku']}, - {term: '食べる', source: '食べている', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, - {term: '食べる', source: '食べておる', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, - {term: '食べる', source: '食べてる', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, - {term: '食べる', source: '食べとる', rule: 'v1', reasons: ['-te', 'progressive or perfect']}, - {term: '食べる', source: '食べてしまう', rule: 'v1', reasons: ['-te', '-shimau']}, - - {term: '買う', source: '買う', rule: 'v5', reasons: []}, - {term: '買う', source: '買います', rule: 'v5', reasons: ['polite']}, - {term: '買う', source: '買った', rule: 'v5', reasons: ['past']}, - {term: '買う', source: '買いました', rule: 'v5', reasons: ['polite past']}, - {term: '買う', source: '買って', rule: 'v5', reasons: ['-te']}, - {term: '買う', source: '買える', rule: 'v5', reasons: ['potential']}, - {term: '買う', source: '買われる', rule: 'v5', reasons: ['passive']}, - {term: '買う', source: '買わせる', rule: 'v5', reasons: ['causative']}, - {term: '買う', source: '買わせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '買う', source: '買え', rule: 'v5', reasons: ['imperative']}, - {term: '買う', source: '買わない', rule: 'v5', reasons: ['negative']}, - {term: '買う', source: '買いません', rule: 'v5', reasons: ['polite negative']}, - {term: '買う', source: '買わなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '買う', source: '買いませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '買う', source: '買わなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '買う', source: '買えない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '買う', source: '買われない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '買う', source: '買わせない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '買う', source: '買わせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '買う', source: '買うな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '買う', source: '買えば', rule: 'v5', reasons: ['-ba']}, - {term: '買う', source: '買っちゃう', rule: 'v5', reasons: ['-chau']}, - {term: '買う', source: '買っちまう', rule: 'v5', reasons: ['-chimau']}, - {term: '買う', source: '買いなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '買う', source: '買いそう', rule: 'v5', reasons: ['-sou']}, - {term: '買う', source: '買いすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '買う', source: '買いたい', rule: 'v5', reasons: ['-tai']}, - {term: '買う', source: '買ったら', rule: 'v5', reasons: ['-tara']}, - {term: '買う', source: '買ったり', rule: 'v5', reasons: ['-tari']}, - {term: '買う', source: '買わず', rule: 'v5', reasons: ['-zu']}, - {term: '買う', source: '買わぬ', rule: 'v5', reasons: ['-nu']}, - {term: '買う', source: '買い', rule: 'v5', reasons: ['masu stem']}, - {term: '買う', source: '買いましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '買う', source: '買おう', rule: 'v5', reasons: ['volitional']}, - {term: '買う', source: '買わされる', rule: 'v5', reasons: ['causative passive']}, - {term: '買う', source: '買っとく', rule: 'v5', reasons: ['-toku']}, - {term: '買う', source: '買っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '買う', source: '買っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '買う', source: '買ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '買う', source: '買っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '買う', source: '買ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '行く', source: '行く', rule: 'v5', reasons: []}, - {term: '行く', source: '行きます', rule: 'v5', reasons: ['polite']}, - {term: '行く', source: '行った', rule: 'v5', reasons: ['past']}, - {term: '行く', source: '行きました', rule: 'v5', reasons: ['polite past']}, - {term: '行く', source: '行って', rule: 'v5', reasons: ['-te']}, - {term: '行く', source: '行ける', rule: 'v5', reasons: ['potential']}, - {term: '行く', source: '行かれる', rule: 'v5', reasons: ['passive']}, - {term: '行く', source: '行かせる', rule: 'v5', reasons: ['causative']}, - {term: '行く', source: '行かせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '行く', source: '行け', rule: 'v5', reasons: ['imperative']}, - {term: '行く', source: '行かない', rule: 'v5', reasons: ['negative']}, - {term: '行く', source: '行きません', rule: 'v5', reasons: ['polite negative']}, - {term: '行く', source: '行かなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '行く', source: '行きませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '行く', source: '行かなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '行く', source: '行けない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '行く', source: '行かれない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '行く', source: '行かせない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '行く', source: '行かせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '行く', source: '行くな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '行く', source: '行けば', rule: 'v5', reasons: ['-ba']}, - {term: '行く', source: '行っちゃう', rule: 'v5', reasons: ['-chau']}, - {term: '行く', source: '行っちまう', rule: 'v5', reasons: ['-chimau']}, - {term: '行く', source: '行きなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '行く', source: '行きそう', rule: 'v5', reasons: ['-sou']}, - {term: '行く', source: '行きすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '行く', source: '行きたい', rule: 'v5', reasons: ['-tai']}, - {term: '行く', source: '行いたら', rule: 'v5', reasons: ['-tara']}, - {term: '行く', source: '行いたり', rule: 'v5', reasons: ['-tari']}, - {term: '行く', source: '行かず', rule: 'v5', reasons: ['-zu']}, - {term: '行く', source: '行かぬ', rule: 'v5', reasons: ['-nu']}, - {term: '行く', source: '行き', rule: 'v5', reasons: ['masu stem']}, - {term: '行く', source: '行きましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '行く', source: '行こう', rule: 'v5', reasons: ['volitional']}, - {term: '行く', source: '行かされる', rule: 'v5', reasons: ['causative passive']}, - {term: '行く', source: '行いとく', rule: 'v5', reasons: ['-toku']}, - {term: '行く', source: '行っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '行く', source: '行っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '行く', source: '行ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '行く', source: '行っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '行く', source: '行ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '泳ぐ', source: '泳ぐ', rule: 'v5', reasons: []}, - {term: '泳ぐ', source: '泳ぎます', rule: 'v5', reasons: ['polite']}, - {term: '泳ぐ', source: '泳いだ', rule: 'v5', reasons: ['past']}, - {term: '泳ぐ', source: '泳ぎました', rule: 'v5', reasons: ['polite past']}, - {term: '泳ぐ', source: '泳いで', rule: 'v5', reasons: ['-te']}, - {term: '泳ぐ', source: '泳げる', rule: 'v5', reasons: ['potential']}, - {term: '泳ぐ', source: '泳がれる', rule: 'v5', reasons: ['passive']}, - {term: '泳ぐ', source: '泳がせる', rule: 'v5', reasons: ['causative']}, - {term: '泳ぐ', source: '泳がせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '泳ぐ', source: '泳げ', rule: 'v5', reasons: ['imperative']}, - {term: '泳ぐ', source: '泳がない', rule: 'v5', reasons: ['negative']}, - {term: '泳ぐ', source: '泳ぎません', rule: 'v5', reasons: ['polite negative']}, - {term: '泳ぐ', source: '泳がなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '泳ぐ', source: '泳ぎませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '泳ぐ', source: '泳がなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '泳ぐ', source: '泳げない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '泳ぐ', source: '泳がれない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '泳ぐ', source: '泳がせない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '泳ぐ', source: '泳がせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '泳ぐ', source: '泳ぐな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '泳ぐ', source: '泳げば', rule: 'v5', reasons: ['-ba']}, - {term: '泳ぐ', source: '泳いじゃう', rule: 'v5', reasons: ['-chau']}, - {term: '泳ぐ', source: '泳いじまう', rule: 'v5', reasons: ['-chimau']}, - {term: '泳ぐ', source: '泳ぎなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '泳ぐ', source: '泳ぎそう', rule: 'v5', reasons: ['-sou']}, - {term: '泳ぐ', source: '泳ぎすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '泳ぐ', source: '泳ぎたい', rule: 'v5', reasons: ['-tai']}, - {term: '泳ぐ', source: '泳いだら', rule: 'v5', reasons: ['-tara']}, - {term: '泳ぐ', source: '泳いだり', rule: 'v5', reasons: ['-tari']}, - {term: '泳ぐ', source: '泳がず', rule: 'v5', reasons: ['-zu']}, - {term: '泳ぐ', source: '泳がぬ', rule: 'v5', reasons: ['-nu']}, - {term: '泳ぐ', source: '泳ぎ', rule: 'v5', reasons: ['masu stem']}, - {term: '泳ぐ', source: '泳ぎましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '泳ぐ', source: '泳ごう', rule: 'v5', reasons: ['volitional']}, - {term: '泳ぐ', source: '泳がされる', rule: 'v5', reasons: ['causative passive']}, - {term: '泳ぐ', source: '泳いどく', rule: 'v5', reasons: ['-toku']}, - {term: '泳ぐ', source: '泳いでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '泳ぐ', source: '泳いでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '泳ぐ', source: '泳いでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '泳ぐ', source: '泳いでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '話す', source: '話す', rule: 'v5', reasons: []}, - {term: '話す', source: '話します', rule: 'v5', reasons: ['polite']}, - {term: '話す', source: '話した', rule: 'v5', reasons: ['past']}, - {term: '話す', source: '話しました', rule: 'v5', reasons: ['polite past']}, - {term: '話す', source: '話して', rule: 'v5', reasons: ['-te']}, - {term: '話す', source: '話せる', rule: 'v5', reasons: ['potential']}, - {term: '話す', source: '話される', rule: 'v5', reasons: ['passive']}, - {term: '話す', source: '話させる', rule: 'v5', reasons: ['causative']}, - {term: '話す', source: '話させられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '話す', source: '話せ', rule: 'v5', reasons: ['imperative']}, - {term: '話す', source: '話さない', rule: 'v5', reasons: ['negative']}, - {term: '話す', source: '話しません', rule: 'v5', reasons: ['polite negative']}, - {term: '話す', source: '話さなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '話す', source: '話しませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '話す', source: '話さなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '話す', source: '話せない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '話す', source: '話されない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '話す', source: '話させない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '話す', source: '話させられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '話す', source: '話すな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '話す', source: '話せば', rule: 'v5', reasons: ['-ba']}, - {term: '話す', source: '話しちゃう', rule: 'v5', reasons: ['-chau']}, - {term: '話す', source: '話しちまう', rule: 'v5', reasons: ['-chimau']}, - {term: '話す', source: '話しなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '話す', source: '話しそう', rule: 'v5', reasons: ['-sou']}, - {term: '話す', source: '話しすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '話す', source: '話したい', rule: 'v5', reasons: ['-tai']}, - {term: '話す', source: '話したら', rule: 'v5', reasons: ['-tara']}, - {term: '話す', source: '話したり', rule: 'v5', reasons: ['-tari']}, - {term: '話す', source: '話さず', rule: 'v5', reasons: ['-zu']}, - {term: '話す', source: '話さぬ', rule: 'v5', reasons: ['-nu']}, - {term: '話す', source: '話し', rule: 'v5', reasons: ['masu stem']}, - {term: '話す', source: '話しましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '話す', source: '話そう', rule: 'v5', reasons: ['volitional']}, - // ['causative passive'] - {term: '話す', source: '話しとく', rule: 'v5', reasons: ['-toku']}, - {term: '話す', source: '話している', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '話す', source: '話しておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '話す', source: '話してる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '話す', source: '話しとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '話す', source: '話してしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '待つ', source: '待つ', rule: 'v5', reasons: []}, - {term: '待つ', source: '待ちます', rule: 'v5', reasons: ['polite']}, - {term: '待つ', source: '待った', rule: 'v5', reasons: ['past']}, - {term: '待つ', source: '待ちました', rule: 'v5', reasons: ['polite past']}, - {term: '待つ', source: '待って', rule: 'v5', reasons: ['-te']}, - {term: '待つ', source: '待てる', rule: 'v5', reasons: ['potential']}, - {term: '待つ', source: '待たれる', rule: 'v5', reasons: ['passive']}, - {term: '待つ', source: '待たせる', rule: 'v5', reasons: ['causative']}, - {term: '待つ', source: '待たせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '待つ', source: '待て', rule: 'v5', reasons: ['imperative']}, - {term: '待つ', source: '待たない', rule: 'v5', reasons: ['negative']}, - {term: '待つ', source: '待ちません', rule: 'v5', reasons: ['polite negative']}, - {term: '待つ', source: '待たなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '待つ', source: '待ちませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '待つ', source: '待たなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '待つ', source: '待てない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '待つ', source: '待たれない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '待つ', source: '待たせない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '待つ', source: '待たせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '待つ', source: '待つな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '待つ', source: '待てば', rule: 'v5', reasons: ['-ba']}, - {term: '待つ', source: '待っちゃう', rule: 'v5', reasons: ['-chau']}, - {term: '待つ', source: '待っちまう', rule: 'v5', reasons: ['-chimau']}, - {term: '待つ', source: '待ちなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '待つ', source: '待ちそう', rule: 'v5', reasons: ['-sou']}, - {term: '待つ', source: '待ちすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '待つ', source: '待ちたい', rule: 'v5', reasons: ['-tai']}, - {term: '待つ', source: '待ったら', rule: 'v5', reasons: ['-tara']}, - {term: '待つ', source: '待ったり', rule: 'v5', reasons: ['-tari']}, - {term: '待つ', source: '待たず', rule: 'v5', reasons: ['-zu']}, - {term: '待つ', source: '待たぬ', rule: 'v5', reasons: ['-nu']}, - {term: '待つ', source: '待ち', rule: 'v5', reasons: ['masu stem']}, - {term: '待つ', source: '待ちましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '待つ', source: '待とう', rule: 'v5', reasons: ['volitional']}, - {term: '待つ', source: '待たされる', rule: 'v5', reasons: ['causative passive']}, - {term: '待つ', source: '待っとく', rule: 'v5', reasons: ['-toku']}, - {term: '待つ', source: '待っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '待つ', source: '待っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '待つ', source: '待ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '待つ', source: '待っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '待つ', source: '待ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '死ぬ', source: '死ぬ', rule: 'v5', reasons: []}, - {term: '死ぬ', source: '死にます', rule: 'v5', reasons: ['polite']}, - {term: '死ぬ', source: '死んだ', rule: 'v5', reasons: ['past']}, - {term: '死ぬ', source: '死にました', rule: 'v5', reasons: ['polite past']}, - {term: '死ぬ', source: '死んで', rule: 'v5', reasons: ['-te']}, - {term: '死ぬ', source: '死ねる', rule: 'v5', reasons: ['potential']}, - {term: '死ぬ', source: '死なれる', rule: 'v5', reasons: ['passive']}, - {term: '死ぬ', source: '死なせる', rule: 'v5', reasons: ['causative']}, - {term: '死ぬ', source: '死なせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '死ぬ', source: '死ね', rule: 'v5', reasons: ['imperative']}, - {term: '死ぬ', source: '死なない', rule: 'v5', reasons: ['negative']}, - {term: '死ぬ', source: '死にません', rule: 'v5', reasons: ['polite negative']}, - {term: '死ぬ', source: '死ななかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '死ぬ', source: '死にませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '死ぬ', source: '死ななくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '死ぬ', source: '死ねない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '死ぬ', source: '死なれない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '死ぬ', source: '死なせない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '死ぬ', source: '死なせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '死ぬ', source: '死ぬな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '死ぬ', source: '死ねば', rule: 'v5', reasons: ['-ba']}, - {term: '死ぬ', source: '死んじゃう', rule: 'v5', reasons: ['-chau']}, - {term: '死ぬ', source: '死んじまう', rule: 'v5', reasons: ['-chimau']}, - {term: '死ぬ', source: '死になさい', rule: 'v5', reasons: ['-nasai']}, - {term: '死ぬ', source: '死にそう', rule: 'v5', reasons: ['-sou']}, - {term: '死ぬ', source: '死にすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '死ぬ', source: '死にたい', rule: 'v5', reasons: ['-tai']}, - {term: '死ぬ', source: '死んだら', rule: 'v5', reasons: ['-tara']}, - {term: '死ぬ', source: '死んだり', rule: 'v5', reasons: ['-tari']}, - {term: '死ぬ', source: '死なず', rule: 'v5', reasons: ['-zu']}, - {term: '死ぬ', source: '死なぬ', rule: 'v5', reasons: ['-nu']}, - {term: '死ぬ', source: '死に', rule: 'v5', reasons: ['masu stem']}, - {term: '死ぬ', source: '死にましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '死ぬ', source: '死のう', rule: 'v5', reasons: ['volitional']}, - {term: '死ぬ', source: '死なされる', rule: 'v5', reasons: ['causative passive']}, - {term: '死ぬ', source: '死んどく', rule: 'v5', reasons: ['-toku']}, - {term: '死ぬ', source: '死んでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '死ぬ', source: '死んでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '死ぬ', source: '死んでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '死ぬ', source: '死んでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '遊ぶ', source: '遊ぶ', rule: 'v5', reasons: []}, - {term: '遊ぶ', source: '遊びます', rule: 'v5', reasons: ['polite']}, - {term: '遊ぶ', source: '遊んだ', rule: 'v5', reasons: ['past']}, - {term: '遊ぶ', source: '遊びました', rule: 'v5', reasons: ['polite past']}, - {term: '遊ぶ', source: '遊んで', rule: 'v5', reasons: ['-te']}, - {term: '遊ぶ', source: '遊べる', rule: 'v5', reasons: ['potential']}, - {term: '遊ぶ', source: '遊ばれる', rule: 'v5', reasons: ['passive']}, - {term: '遊ぶ', source: '遊ばせる', rule: 'v5', reasons: ['causative']}, - {term: '遊ぶ', source: '遊ばせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '遊ぶ', source: '遊べ', rule: 'v5', reasons: ['imperative']}, - {term: '遊ぶ', source: '遊ばない', rule: 'v5', reasons: ['negative']}, - {term: '遊ぶ', source: '遊びません', rule: 'v5', reasons: ['polite negative']}, - {term: '遊ぶ', source: '遊ばなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '遊ぶ', source: '遊びませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '遊ぶ', source: '遊ばなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '遊ぶ', source: '遊べない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '遊ぶ', source: '遊ばれない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '遊ぶ', source: '遊ばせない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '遊ぶ', source: '遊ばせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '遊ぶ', source: '遊ぶな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '遊ぶ', source: '遊べば', rule: 'v5', reasons: ['-ba']}, - {term: '遊ぶ', source: '遊んじゃう', rule: 'v5', reasons: ['-chau']}, - {term: '遊ぶ', source: '遊んじまう', rule: 'v5', reasons: ['-chimau']}, - {term: '遊ぶ', source: '遊びなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '遊ぶ', source: '遊びそう', rule: 'v5', reasons: ['-sou']}, - {term: '遊ぶ', source: '遊びすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '遊ぶ', source: '遊びたい', rule: 'v5', reasons: ['-tai']}, - {term: '遊ぶ', source: '遊んだら', rule: 'v5', reasons: ['-tara']}, - {term: '遊ぶ', source: '遊んだり', rule: 'v5', reasons: ['-tari']}, - {term: '遊ぶ', source: '遊ばず', rule: 'v5', reasons: ['-zu']}, - {term: '遊ぶ', source: '遊ばぬ', rule: 'v5', reasons: ['-nu']}, - {term: '遊ぶ', source: '遊び', rule: 'v5', reasons: ['masu stem']}, - {term: '遊ぶ', source: '遊びましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '遊ぶ', source: '遊ぼう', rule: 'v5', reasons: ['volitional']}, - {term: '遊ぶ', source: '遊ばされる', rule: 'v5', reasons: ['causative passive']}, - {term: '遊ぶ', source: '遊んどく', rule: 'v5', reasons: ['-toku']}, - {term: '遊ぶ', source: '遊んでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '遊ぶ', source: '遊んでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '遊ぶ', source: '遊んでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '遊ぶ', source: '遊んでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '飲む', source: '飲む', rule: 'v5', reasons: []}, - {term: '飲む', source: '飲みます', rule: 'v5', reasons: ['polite']}, - {term: '飲む', source: '飲んだ', rule: 'v5', reasons: ['past']}, - {term: '飲む', source: '飲みました', rule: 'v5', reasons: ['polite past']}, - {term: '飲む', source: '飲んで', rule: 'v5', reasons: ['-te']}, - {term: '飲む', source: '飲める', rule: 'v5', reasons: ['potential']}, - {term: '飲む', source: '飲まれる', rule: 'v5', reasons: ['passive']}, - {term: '飲む', source: '飲ませる', rule: 'v5', reasons: ['causative']}, - {term: '飲む', source: '飲ませられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '飲む', source: '飲め', rule: 'v5', reasons: ['imperative']}, - {term: '飲む', source: '飲まない', rule: 'v5', reasons: ['negative']}, - {term: '飲む', source: '飲みません', rule: 'v5', reasons: ['polite negative']}, - {term: '飲む', source: '飲まなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '飲む', source: '飲みませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '飲む', source: '飲まなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '飲む', source: '飲めない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '飲む', source: '飲まれない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '飲む', source: '飲ませない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '飲む', source: '飲ませられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '飲む', source: '飲むな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '飲む', source: '飲めば', rule: 'v5', reasons: ['-ba']}, - {term: '飲む', source: '飲んじゃう', rule: 'v5', reasons: ['-chau']}, - {term: '飲む', source: '飲んじまう', rule: 'v5', reasons: ['-chimau']}, - {term: '飲む', source: '飲みなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '飲む', source: '飲みそう', rule: 'v5', reasons: ['-sou']}, - {term: '飲む', source: '飲みすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '飲む', source: '飲みたい', rule: 'v5', reasons: ['-tai']}, - {term: '飲む', source: '飲んだら', rule: 'v5', reasons: ['-tara']}, - {term: '飲む', source: '飲んだり', rule: 'v5', reasons: ['-tari']}, - {term: '飲む', source: '飲まず', rule: 'v5', reasons: ['-zu']}, - {term: '飲む', source: '飲まぬ', rule: 'v5', reasons: ['-nu']}, - {term: '飲む', source: '飲み', rule: 'v5', reasons: ['masu stem']}, - {term: '飲む', source: '飲みましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '飲む', source: '飲もう', rule: 'v5', reasons: ['volitional']}, - {term: '飲む', source: '飲まされる', rule: 'v5', reasons: ['causative passive']}, - {term: '飲む', source: '飲んどく', rule: 'v5', reasons: ['-toku']}, - {term: '飲む', source: '飲んでいる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '飲む', source: '飲んでおる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '飲む', source: '飲んでる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '飲む', source: '飲んでしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - {term: '作る', source: '作る', rule: 'v5', reasons: []}, - {term: '作る', source: '作ります', rule: 'v5', reasons: ['polite']}, - {term: '作る', source: '作った', rule: 'v5', reasons: ['past']}, - {term: '作る', source: '作りました', rule: 'v5', reasons: ['polite past']}, - {term: '作る', source: '作って', rule: 'v5', reasons: ['-te']}, - {term: '作る', source: '作れる', rule: 'v5', reasons: ['potential']}, - {term: '作る', source: '作られる', rule: 'v5', reasons: ['passive']}, - {term: '作る', source: '作らせる', rule: 'v5', reasons: ['causative']}, - {term: '作る', source: '作らせられる', rule: 'v5', reasons: ['causative', 'potential or passive']}, - {term: '作る', source: '作れ', rule: 'v5', reasons: ['imperative']}, - {term: '作る', source: '作らない', rule: 'v5', reasons: ['negative']}, - {term: '作る', source: '作りません', rule: 'v5', reasons: ['polite negative']}, - {term: '作る', source: '作らなかった', rule: 'v5', reasons: ['negative', 'past']}, - {term: '作る', source: '作りませんでした', rule: 'v5', reasons: ['polite past negative']}, - {term: '作る', source: '作らなくて', rule: 'v5', reasons: ['negative', '-te']}, - {term: '作る', source: '作れない', rule: 'v5', reasons: ['potential', 'negative']}, - {term: '作る', source: '作られない', rule: 'v5', reasons: ['passive', 'negative']}, - {term: '作る', source: '作らせない', rule: 'v5', reasons: ['causative', 'negative']}, - {term: '作る', source: '作らせられない', rule: 'v5', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '作る', source: '作るな', rule: 'v5', reasons: ['imperative negative']}, - - {term: '作る', source: '作れば', rule: 'v5', reasons: ['-ba']}, - {term: '作る', source: '作っちゃう', rule: 'v5', reasons: ['-chau']}, - {term: '作る', source: '作っちまう', rule: 'v5', reasons: ['-chimau']}, - {term: '作る', source: '作りなさい', rule: 'v5', reasons: ['-nasai']}, - {term: '作る', source: '作りそう', rule: 'v5', reasons: ['-sou']}, - {term: '作る', source: '作りすぎる', rule: 'v5', reasons: ['-sugiru']}, - {term: '作る', source: '作りたい', rule: 'v5', reasons: ['-tai']}, - {term: '作る', source: '作ったら', rule: 'v5', reasons: ['-tara']}, - {term: '作る', source: '作ったり', rule: 'v5', reasons: ['-tari']}, - {term: '作る', source: '作らず', rule: 'v5', reasons: ['-zu']}, - {term: '作る', source: '作らぬ', rule: 'v5', reasons: ['-nu']}, - {term: '作る', source: '作り', rule: 'v5', reasons: ['masu stem']}, - {term: '作る', source: '作りましょう', rule: 'v5', reasons: ['polite volitional']}, - {term: '作る', source: '作ろう', rule: 'v5', reasons: ['volitional']}, - {term: '作る', source: '作らされる', rule: 'v5', reasons: ['causative passive']}, - {term: '作る', source: '作っとく', rule: 'v5', reasons: ['-toku']}, - {term: '作る', source: '作っている', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '作る', source: '作っておる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '作る', source: '作ってる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '作る', source: '作っとる', rule: 'v5', reasons: ['-te', 'progressive or perfect']}, - {term: '作る', source: '作ってしまう', rule: 'v5', reasons: ['-te', '-shimau']}, - - // Irregular verbs - {term: '為る', source: '為る', rule: 'vs', reasons: []}, - {term: '為る', source: '為ます', rule: 'vs', reasons: ['polite']}, - {term: '為る', source: '為た', rule: 'vs', reasons: ['past']}, - {term: '為る', source: '為ました', rule: 'vs', reasons: ['polite past']}, - {term: '為る', source: '為て', rule: 'vs', reasons: ['-te']}, - {term: '為る', source: '為られる', rule: 'vs', reasons: ['potential or passive']}, - {term: '為る', source: '為れる', rule: 'vs', reasons: ['passive']}, - {term: '為る', source: '為せる', rule: 'vs', reasons: ['causative']}, - {term: '為る', source: '為させる', rule: 'vs', reasons: ['causative']}, - {term: '為る', source: '為せられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, - {term: '為る', source: '為させられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, - {term: '為る', source: '為ろ', rule: 'vs', reasons: ['imperative']}, - {term: '為る', source: '為ない', rule: 'vs', reasons: ['negative']}, - {term: '為る', source: '為ません', rule: 'vs', reasons: ['polite negative']}, - {term: '為る', source: '為なかった', rule: 'vs', reasons: ['negative', 'past']}, - {term: '為る', source: '為ませんでした', rule: 'vs', reasons: ['polite past negative']}, - {term: '為る', source: '為なくて', rule: 'vs', reasons: ['negative', '-te']}, - {term: '為る', source: '為られない', rule: 'vs', reasons: ['potential or passive', 'negative']}, - {term: '為る', source: '為れない', rule: 'vs', reasons: ['passive', 'negative']}, - {term: '為る', source: '為せない', rule: 'vs', reasons: ['causative', 'negative']}, - {term: '為る', source: '為させない', rule: 'vs', reasons: ['causative', 'negative']}, - {term: '為る', source: '為せられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '為る', source: '為させられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '為る', source: '為るな', rule: 'vs', reasons: ['imperative negative']}, - - {term: '為る', source: '為れば', rule: 'vs', reasons: ['-ba']}, - {term: '為る', source: '為ちゃう', rule: 'vs', reasons: ['-chau']}, - {term: '為る', source: '為ちまう', rule: 'vs', reasons: ['-chimau']}, - {term: '為る', source: '為なさい', rule: 'vs', reasons: ['-nasai']}, - {term: '為る', source: '為そう', rule: 'vs', reasons: ['-sou']}, - {term: '為る', source: '為すぎる', rule: 'vs', reasons: ['-sugiru']}, - {term: '為る', source: '為たい', rule: 'vs', reasons: ['-tai']}, - {term: '為る', source: '為たら', rule: 'vs', reasons: ['-tara']}, - {term: '為る', source: '為たり', rule: 'vs', reasons: ['-tari']}, - {term: '為る', source: '為ず', rule: 'vs', reasons: ['-zu']}, - {term: '為る', source: '為ぬ', rule: 'vs', reasons: ['-nu']}, - // ['masu stem'] - {term: '為る', source: '為ましょう', rule: 'vs', reasons: ['polite volitional']}, - {term: '為る', source: '為よう', rule: 'vs', reasons: ['volitional']}, - // ['causative passive'] - {term: '為る', source: '為とく', rule: 'vs', reasons: ['-toku']}, - {term: '為る', source: '為ている', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: '為る', source: '為ておる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: '為る', source: '為てる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: '為る', source: '為とる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: '為る', source: '為てしまう', rule: 'vs', reasons: ['-te', '-shimau']}, - - {term: 'する', source: 'する', rule: 'vs', reasons: []}, - {term: 'する', source: 'します', rule: 'vs', reasons: ['polite']}, - {term: 'する', source: 'した', rule: 'vs', reasons: ['past']}, - {term: 'する', source: 'しました', rule: 'vs', reasons: ['polite past']}, - {term: 'する', source: 'して', rule: 'vs', reasons: ['-te']}, - {term: 'する', source: 'せられる', rule: 'vs', reasons: ['potential or passive']}, - {term: 'する', source: 'される', rule: 'vs', reasons: ['passive']}, - {term: 'する', source: 'させる', rule: 'vs', reasons: ['causative']}, - {term: 'する', source: 'せさせる', rule: 'vs', reasons: ['causative']}, - {term: 'する', source: 'させられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, - {term: 'する', source: 'せさせられる', rule: 'vs', reasons: ['causative', 'potential or passive']}, - {term: 'する', source: 'しろ', rule: 'vs', reasons: ['imperative']}, - {term: 'する', source: 'しない', rule: 'vs', reasons: ['negative']}, - {term: 'する', source: 'しません', rule: 'vs', reasons: ['polite negative']}, - {term: 'する', source: 'しなかった', rule: 'vs', reasons: ['negative', 'past']}, - {term: 'する', source: 'しませんでした', rule: 'vs', reasons: ['polite past negative']}, - {term: 'する', source: 'しなくて', rule: 'vs', reasons: ['negative', '-te']}, - {term: 'する', source: 'せられない', rule: 'vs', reasons: ['potential or passive', 'negative']}, - {term: 'する', source: 'されない', rule: 'vs', reasons: ['passive', 'negative']}, - {term: 'する', source: 'させない', rule: 'vs', reasons: ['causative', 'negative']}, - {term: 'する', source: 'せさせない', rule: 'vs', reasons: ['causative', 'negative']}, - {term: 'する', source: 'させられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, - {term: 'する', source: 'せさせられない', rule: 'vs', reasons: ['causative', 'potential or passive', 'negative']}, - {term: 'する', source: 'するな', rule: 'vs', reasons: ['imperative negative']}, - - {term: 'する', source: 'すれば', rule: 'vs', reasons: ['-ba']}, - {term: 'する', source: 'しちゃう', rule: 'vs', reasons: ['-chau']}, - {term: 'する', source: 'しちまう', rule: 'vs', reasons: ['-chimau']}, - {term: 'する', source: 'しなさい', rule: 'vs', reasons: ['-nasai']}, - {term: 'する', source: 'しそう', rule: 'vs', reasons: ['-sou']}, - {term: 'する', source: 'しすぎる', rule: 'vs', reasons: ['-sugiru']}, - {term: 'する', source: 'したい', rule: 'vs', reasons: ['-tai']}, - {term: 'する', source: 'したら', rule: 'vs', reasons: ['-tara']}, - {term: 'する', source: 'したり', rule: 'vs', reasons: ['-tari']}, - {term: 'する', source: 'せず', rule: 'vs', reasons: ['-zu']}, - {term: 'する', source: 'せぬ', rule: 'vs', reasons: ['-nu']}, - // ['masu stem'] - {term: 'する', source: 'しましょう', rule: 'vs', reasons: ['polite volitional']}, - {term: 'する', source: 'しよう', rule: 'vs', reasons: ['volitional']}, - // ['causative passive'] - {term: 'する', source: 'しとく', rule: 'vs', reasons: ['-toku']}, - {term: 'する', source: 'している', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: 'する', source: 'しておる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: 'する', source: 'してる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: 'する', source: 'しとる', rule: 'vs', reasons: ['-te', 'progressive or perfect']}, - {term: 'する', source: 'してしまう', rule: 'vs', reasons: ['-te', '-shimau']}, - - {term: '来る', source: '来る', rule: 'vk', reasons: []}, - {term: '来る', source: '来ます', rule: 'vk', reasons: ['polite']}, - {term: '来る', source: '来た', rule: 'vk', reasons: ['past']}, - {term: '来る', source: '来ました', rule: 'vk', reasons: ['polite past']}, - {term: '来る', source: '来て', rule: 'vk', reasons: ['-te']}, - {term: '来る', source: '来られる', rule: 'vk', reasons: ['potential or passive']}, - {term: '来る', source: '来られる', rule: 'vk', reasons: ['potential or passive']}, - {term: '来る', source: '来させる', rule: 'vk', reasons: ['causative']}, - {term: '来る', source: '来させられる', rule: 'vk', reasons: ['causative', 'potential or passive']}, - {term: '来る', source: '来い', rule: 'vk', reasons: ['imperative']}, - {term: '来る', source: '来ない', rule: 'vk', reasons: ['negative']}, - {term: '来る', source: '来ません', rule: 'vk', reasons: ['polite negative']}, - {term: '来る', source: '来なかった', rule: 'vk', reasons: ['negative', 'past']}, - {term: '来る', source: '来ませんでした', rule: 'vk', reasons: ['polite past negative']}, - {term: '来る', source: '来なくて', rule: 'vk', reasons: ['negative', '-te']}, - {term: '来る', source: '来られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, - {term: '来る', source: '来られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, - {term: '来る', source: '来させない', rule: 'vk', reasons: ['causative', 'negative']}, - {term: '来る', source: '来させられない', rule: 'vk', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '来る', source: '来るな', rule: 'vk', reasons: ['imperative negative']}, - - {term: '来る', source: '来れば', rule: 'vk', reasons: ['-ba']}, - {term: '来る', source: '来ちゃう', rule: 'vk', reasons: ['-chau']}, - {term: '来る', source: '来ちまう', rule: 'vk', reasons: ['-chimau']}, - {term: '来る', source: '来なさい', rule: 'vk', reasons: ['-nasai']}, - {term: '来る', source: '来そう', rule: 'vk', reasons: ['-sou']}, - {term: '来る', source: '来すぎる', rule: 'vk', reasons: ['-sugiru']}, - {term: '来る', source: '来たい', rule: 'vk', reasons: ['-tai']}, - {term: '来る', source: '来たら', rule: 'vk', reasons: ['-tara']}, - {term: '来る', source: '来たり', rule: 'vk', reasons: ['-tari']}, - {term: '来る', source: '来ず', rule: 'vk', reasons: ['-zu']}, - {term: '来る', source: '来ぬ', rule: 'vk', reasons: ['-nu']}, - {term: '来る', source: '来', rule: 'vk', reasons: ['masu stem']}, - {term: '来る', source: '来ましょう', rule: 'vk', reasons: ['polite volitional']}, - {term: '来る', source: '来よう', rule: 'vk', reasons: ['volitional']}, - // ['causative passive'] - {term: '来る', source: '来とく', rule: 'vk', reasons: ['-toku']}, - {term: '来る', source: '来ている', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '来る', source: '来ておる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '来る', source: '来てる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '来る', source: '来とる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '来る', source: '来てしまう', rule: 'vk', reasons: ['-te', '-shimau']}, - - {term: '來る', source: '來る', rule: 'vk', reasons: []}, - {term: '來る', source: '來ます', rule: 'vk', reasons: ['polite']}, - {term: '來る', source: '來た', rule: 'vk', reasons: ['past']}, - {term: '來る', source: '來ました', rule: 'vk', reasons: ['polite past']}, - {term: '來る', source: '來て', rule: 'vk', reasons: ['-te']}, - {term: '來る', source: '來られる', rule: 'vk', reasons: ['potential or passive']}, - {term: '來る', source: '來られる', rule: 'vk', reasons: ['potential or passive']}, - {term: '來る', source: '來させる', rule: 'vk', reasons: ['causative']}, - {term: '來る', source: '來させられる', rule: 'vk', reasons: ['causative', 'potential or passive']}, - {term: '來る', source: '來い', rule: 'vk', reasons: ['imperative']}, - {term: '來る', source: '來ない', rule: 'vk', reasons: ['negative']}, - {term: '來る', source: '來ません', rule: 'vk', reasons: ['polite negative']}, - {term: '來る', source: '來なかった', rule: 'vk', reasons: ['negative', 'past']}, - {term: '來る', source: '來ませんでした', rule: 'vk', reasons: ['polite past negative']}, - {term: '來る', source: '來なくて', rule: 'vk', reasons: ['negative', '-te']}, - {term: '來る', source: '來られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, - {term: '來る', source: '來られない', rule: 'vk', reasons: ['potential or passive', 'negative']}, - {term: '來る', source: '來させない', rule: 'vk', reasons: ['causative', 'negative']}, - {term: '來る', source: '來させられない', rule: 'vk', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '來る', source: '來るな', rule: 'vk', reasons: ['imperative negative']}, - - {term: '來る', source: '來れば', rule: 'vk', reasons: ['-ba']}, - {term: '來る', source: '來ちゃう', rule: 'vk', reasons: ['-chau']}, - {term: '來る', source: '來ちまう', rule: 'vk', reasons: ['-chimau']}, - {term: '來る', source: '來なさい', rule: 'vk', reasons: ['-nasai']}, - {term: '來る', source: '來そう', rule: 'vk', reasons: ['-sou']}, - {term: '來る', source: '來すぎる', rule: 'vk', reasons: ['-sugiru']}, - {term: '來る', source: '來たい', rule: 'vk', reasons: ['-tai']}, - {term: '來る', source: '來たら', rule: 'vk', reasons: ['-tara']}, - {term: '來る', source: '來たり', rule: 'vk', reasons: ['-tari']}, - {term: '來る', source: '來ず', rule: 'vk', reasons: ['-zu']}, - {term: '來る', source: '來ぬ', rule: 'vk', reasons: ['-nu']}, - {term: '來る', source: '來', rule: 'vk', reasons: ['masu stem']}, - {term: '來る', source: '來ましょう', rule: 'vk', reasons: ['polite volitional']}, - {term: '來る', source: '來よう', rule: 'vk', reasons: ['volitional']}, - // ['causative passive'] - {term: '來る', source: '來とく', rule: 'vk', reasons: ['-toku']}, - {term: '來る', source: '來ている', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '來る', source: '來ておる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '來る', source: '來てる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '來る', source: '來とる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: '來る', source: '來てしまう', rule: 'vk', reasons: ['-te', '-shimau']}, - - {term: 'くる', source: 'くる', rule: 'vk', reasons: []}, - {term: 'くる', source: 'きます', rule: 'vk', reasons: ['polite']}, - {term: 'くる', source: 'きた', rule: 'vk', reasons: ['past']}, - {term: 'くる', source: 'きました', rule: 'vk', reasons: ['polite past']}, - {term: 'くる', source: 'きて', rule: 'vk', reasons: ['-te']}, - {term: 'くる', source: 'こられる', rule: 'vk', reasons: ['potential or passive']}, - {term: 'くる', source: 'こられる', rule: 'vk', reasons: ['potential or passive']}, - {term: 'くる', source: 'こさせる', rule: 'vk', reasons: ['causative']}, - {term: 'くる', source: 'こさせられる', rule: 'vk', reasons: ['causative', 'potential or passive']}, - {term: 'くる', source: 'こい', rule: 'vk', reasons: ['imperative']}, - {term: 'くる', source: 'こない', rule: 'vk', reasons: ['negative']}, - {term: 'くる', source: 'きません', rule: 'vk', reasons: ['polite negative']}, - {term: 'くる', source: 'こなかった', rule: 'vk', reasons: ['negative', 'past']}, - {term: 'くる', source: 'きませんでした', rule: 'vk', reasons: ['polite past negative']}, - {term: 'くる', source: 'こなくて', rule: 'vk', reasons: ['negative', '-te']}, - {term: 'くる', source: 'こられない', rule: 'vk', reasons: ['potential or passive', 'negative']}, - {term: 'くる', source: 'こられない', rule: 'vk', reasons: ['potential or passive', 'negative']}, - {term: 'くる', source: 'こさせない', rule: 'vk', reasons: ['causative', 'negative']}, - {term: 'くる', source: 'こさせられない', rule: 'vk', reasons: ['causative', 'potential or passive', 'negative']}, - {term: 'くる', source: 'くるな', rule: 'vk', reasons: ['imperative negative']}, - - {term: 'くる', source: 'くれば', rule: 'vk', reasons: ['-ba']}, - {term: 'くる', source: 'きちゃう', rule: 'vk', reasons: ['-chau']}, - {term: 'くる', source: 'きちまう', rule: 'vk', reasons: ['-chimau']}, - {term: 'くる', source: 'きなさい', rule: 'vk', reasons: ['-nasai']}, - {term: 'くる', source: 'きそう', rule: 'vk', reasons: ['-sou']}, - {term: 'くる', source: 'きすぎる', rule: 'vk', reasons: ['-sugiru']}, - {term: 'くる', source: 'きたい', rule: 'vk', reasons: ['-tai']}, - {term: 'くる', source: 'きたら', rule: 'vk', reasons: ['-tara']}, - {term: 'くる', source: 'きたり', rule: 'vk', reasons: ['-tari']}, - {term: 'くる', source: 'こず', rule: 'vk', reasons: ['-zu']}, - {term: 'くる', source: 'こぬ', rule: 'vk', reasons: ['-nu']}, - {term: 'くる', source: 'き', rule: 'vk', reasons: ['masu stem']}, - {term: 'くる', source: 'きましょう', rule: 'vk', reasons: ['polite volitional']}, - {term: 'くる', source: 'こよう', rule: 'vk', reasons: ['volitional']}, - // ['causative passive'] - {term: 'くる', source: 'きとく', rule: 'vk', reasons: ['-toku']}, - {term: 'くる', source: 'きている', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: 'くる', source: 'きておる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: 'くる', source: 'きてる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: 'くる', source: 'きとる', rule: 'vk', reasons: ['-te', 'progressive or perfect']}, - {term: 'くる', source: 'きてしまう', rule: 'vk', reasons: ['-te', '-shimau']}, - - // Zuru verbs - {term: '論ずる', source: '論ずる', rule: 'vz', reasons: []}, - {term: '論ずる', source: '論じます', rule: 'vz', reasons: ['polite']}, - {term: '論ずる', source: '論じた', rule: 'vz', reasons: ['past']}, - {term: '論ずる', source: '論じました', rule: 'vz', reasons: ['polite past']}, - {term: '論ずる', source: '論じて', rule: 'vz', reasons: ['-te']}, - {term: '論ずる', source: '論ぜられる', rule: 'vz', reasons: ['potential or passive']}, - {term: '論ずる', source: '論ざれる', rule: 'vz', reasons: ['potential or passive']}, - {term: '論ずる', source: '論じされる', rule: 'vz', reasons: ['passive']}, - {term: '論ずる', source: '論ぜされる', rule: 'vz', reasons: ['passive']}, - {term: '論ずる', source: '論じさせる', rule: 'vz', reasons: ['causative']}, - {term: '論ずる', source: '論ぜさせる', rule: 'vz', reasons: ['causative']}, - {term: '論ずる', source: '論じさせられる', rule: 'vz', reasons: ['causative', 'potential or passive']}, - {term: '論ずる', source: '論ぜさせられる', rule: 'vz', reasons: ['causative', 'potential or passive']}, - {term: '論ずる', source: '論じろ', rule: 'vz', reasons: ['imperative']}, - {term: '論ずる', source: '論じない', rule: 'vz', reasons: ['negative']}, - {term: '論ずる', source: '論じません', rule: 'vz', reasons: ['polite negative']}, - {term: '論ずる', source: '論じなかった', rule: 'vz', reasons: ['negative', 'past']}, - {term: '論ずる', source: '論じませんでした', rule: 'vz', reasons: ['polite past negative']}, - {term: '論ずる', source: '論じなくて', rule: 'vz', reasons: ['negative', '-te']}, - {term: '論ずる', source: '論ぜられない', rule: 'vz', reasons: ['potential or passive', 'negative']}, - {term: '論ずる', source: '論じされない', rule: 'vz', reasons: ['passive', 'negative']}, - {term: '論ずる', source: '論ぜされない', rule: 'vz', reasons: ['passive', 'negative']}, - {term: '論ずる', source: '論じさせない', rule: 'vz', reasons: ['causative', 'negative']}, - {term: '論ずる', source: '論ぜさせない', rule: 'vz', reasons: ['causative', 'negative']}, - {term: '論ずる', source: '論じさせられない', rule: 'vz', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '論ずる', source: '論ぜさせられない', rule: 'vz', reasons: ['causative', 'potential or passive', 'negative']}, - {term: '論ずる', source: '論ずるな', rule: 'vz', reasons: ['imperative negative']}, - - {term: '論ずる', source: '論ずれば', rule: 'vz', reasons: ['-ba']}, - {term: '論ずる', source: '論じちゃう', rule: 'vz', reasons: ['-chau']}, - {term: '論ずる', source: '論じちまう', rule: 'vz', reasons: ['-chimau']}, - {term: '論ずる', source: '論じなさい', rule: 'vz', reasons: ['-nasai']}, - {term: '論ずる', source: '論じそう', rule: 'vz', reasons: ['-sou']}, - {term: '論ずる', source: '論じすぎる', rule: 'vz', reasons: ['-sugiru']}, - {term: '論ずる', source: '論じたい', rule: 'vz', reasons: ['-tai']}, - {term: '論ずる', source: '論じたら', rule: 'vz', reasons: ['-tara']}, - {term: '論ずる', source: '論じたり', rule: 'vz', reasons: ['-tari']}, - {term: '論ずる', source: '論ぜず', rule: 'vz', reasons: ['-zu']}, - {term: '論ずる', source: '論ぜぬ', rule: 'vz', reasons: ['-nu']}, - // ['masu stem'] - {term: '論ずる', source: '論じましょう', rule: 'vz', reasons: ['polite volitional']}, - {term: '論ずる', source: '論じよう', rule: 'vz', reasons: ['volitional']}, - // ['causative passive'] - {term: '論ずる', source: '論じとく', rule: 'vz', reasons: ['-toku']}, - {term: '論ずる', source: '論じている', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, - {term: '論ずる', source: '論じておる', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, - {term: '論ずる', source: '論じてる', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, - {term: '論ずる', source: '論じとる', rule: 'vz', reasons: ['-te', 'progressive or perfect']}, - {term: '論ずる', source: '論じてしまう', rule: 'vz', reasons: ['-te', '-shimau']}, - - // Uncommon irregular verbs - {term: 'のたまう', source: 'のたもうて', rule: 'v5', reasons: ['-te']}, - {term: 'のたまう', source: 'のたもうた', rule: 'v5', reasons: ['past']}, - {term: 'のたまう', source: 'のたもうたら', rule: 'v5', reasons: ['-tara']}, - {term: 'のたまう', source: 'のたもうたり', rule: 'v5', reasons: ['-tari']}, - - {term: 'おう', source: 'おうて', rule: 'v5', reasons: ['-te']}, - {term: 'こう', source: 'こうて', rule: 'v5', reasons: ['-te']}, - {term: 'そう', source: 'そうて', rule: 'v5', reasons: ['-te']}, - {term: 'とう', source: 'とうて', rule: 'v5', reasons: ['-te']}, - {term: '請う', source: '請うて', rule: 'v5', reasons: ['-te']}, - {term: '乞う', source: '乞うて', rule: 'v5', reasons: ['-te']}, - {term: '恋う', source: '恋うて', rule: 'v5', reasons: ['-te']}, - {term: '問う', source: '問うて', rule: 'v5', reasons: ['-te']}, - {term: '負う', source: '負うて', rule: 'v5', reasons: ['-te']}, - {term: '沿う', source: '沿うて', rule: 'v5', reasons: ['-te']}, - {term: '添う', source: '添うて', rule: 'v5', reasons: ['-te']}, - {term: '副う', source: '副うて', rule: 'v5', reasons: ['-te']}, - {term: '厭う', source: '厭うて', rule: 'v5', reasons: ['-te']}, - - {term: 'おう', source: 'おうた', rule: 'v5', reasons: ['past']}, - {term: 'こう', source: 'こうた', rule: 'v5', reasons: ['past']}, - {term: 'そう', source: 'そうた', rule: 'v5', reasons: ['past']}, - {term: 'とう', source: 'とうた', rule: 'v5', reasons: ['past']}, - {term: '請う', source: '請うた', rule: 'v5', reasons: ['past']}, - {term: '乞う', source: '乞うた', rule: 'v5', reasons: ['past']}, - {term: '恋う', source: '恋うた', rule: 'v5', reasons: ['past']}, - {term: '問う', source: '問うた', rule: 'v5', reasons: ['past']}, - {term: '負う', source: '負うた', rule: 'v5', reasons: ['past']}, - {term: '沿う', source: '沿うた', rule: 'v5', reasons: ['past']}, - {term: '添う', source: '添うた', rule: 'v5', reasons: ['past']}, - {term: '副う', source: '副うた', rule: 'v5', reasons: ['past']}, - {term: '厭う', source: '厭うた', rule: 'v5', reasons: ['past']}, - - {term: 'おう', source: 'おうたら', rule: 'v5', reasons: ['-tara']}, - {term: 'こう', source: 'こうたら', rule: 'v5', reasons: ['-tara']}, - {term: 'そう', source: 'そうたら', rule: 'v5', reasons: ['-tara']}, - {term: 'とう', source: 'とうたら', rule: 'v5', reasons: ['-tara']}, - {term: '請う', source: '請うたら', rule: 'v5', reasons: ['-tara']}, - {term: '乞う', source: '乞うたら', rule: 'v5', reasons: ['-tara']}, - {term: '恋う', source: '恋うたら', rule: 'v5', reasons: ['-tara']}, - {term: '問う', source: '問うたら', rule: 'v5', reasons: ['-tara']}, - {term: '負う', source: '負うたら', rule: 'v5', reasons: ['-tara']}, - {term: '沿う', source: '沿うたら', rule: 'v5', reasons: ['-tara']}, - {term: '添う', source: '添うたら', rule: 'v5', reasons: ['-tara']}, - {term: '副う', source: '副うたら', rule: 'v5', reasons: ['-tara']}, - {term: '厭う', source: '厭うたら', rule: 'v5', reasons: ['-tara']}, - - {term: 'おう', source: 'おうたり', rule: 'v5', reasons: ['-tari']}, - {term: 'こう', source: 'こうたり', rule: 'v5', reasons: ['-tari']}, - {term: 'そう', source: 'そうたり', rule: 'v5', reasons: ['-tari']}, - {term: 'とう', source: 'とうたり', rule: 'v5', reasons: ['-tari']}, - {term: '請う', source: '請うたり', rule: 'v5', reasons: ['-tari']}, - {term: '乞う', source: '乞うたり', rule: 'v5', reasons: ['-tari']}, - {term: '恋う', source: '恋うたり', rule: 'v5', reasons: ['-tari']}, - {term: '問う', source: '問うたり', rule: 'v5', reasons: ['-tari']}, - {term: '負う', source: '負うたり', rule: 'v5', reasons: ['-tari']}, - {term: '沿う', source: '沿うたり', rule: 'v5', reasons: ['-tari']}, - {term: '添う', source: '添うたり', rule: 'v5', reasons: ['-tari']}, - {term: '副う', source: '副うたり', rule: 'v5', reasons: ['-tari']}, - {term: '厭う', source: '厭うたり', rule: 'v5', reasons: ['-tari']}, - - // Combinations - {term: '抱き抱える', source: '抱き抱えていなければ', rule: 'v1', reasons: ['-te', 'progressive or perfect', 'negative', '-ba']}, - {term: '抱きかかえる', source: '抱きかかえていなければ', rule: 'v1', reasons: ['-te', 'progressive or perfect', 'negative', '-ba']}, - {term: '打ち込む', source: '打ち込んでいませんでした', rule: 'v5', reasons: ['-te', 'progressive or perfect', 'polite past negative']}, - {term: '食べる', source: '食べさせられたくなかった', rule: 'v1', reasons: ['causative', 'potential or passive', '-tai', 'negative', 'past']} - ] - }, - { - valid: false, - tests: [ - {term: 'する', source: 'すます', rule: 'vs'}, - {term: 'する', source: 'すた', rule: 'vs'}, - {term: 'する', source: 'すました', rule: 'vs'}, - {term: 'する', source: 'すて', rule: 'vs'}, - {term: 'する', source: 'すれる', rule: 'vs'}, - {term: 'する', source: 'すせる', rule: 'vs'}, - {term: 'する', source: 'すせられる', rule: 'vs'}, - {term: 'する', source: 'すろ', rule: 'vs'}, - {term: 'する', source: 'すない', rule: 'vs'}, - {term: 'する', source: 'すません', rule: 'vs'}, - {term: 'する', source: 'すなかった', rule: 'vs'}, - {term: 'する', source: 'すませんでした', rule: 'vs'}, - {term: 'する', source: 'すなくて', rule: 'vs'}, - {term: 'する', source: 'すれない', rule: 'vs'}, - {term: 'する', source: 'すせない', rule: 'vs'}, - {term: 'する', source: 'すせられない', rule: 'vs'}, - - {term: 'くる', source: 'くます', rule: 'vk'}, - {term: 'くる', source: 'くた', rule: 'vk'}, - {term: 'くる', source: 'くました', rule: 'vk'}, - {term: 'くる', source: 'くて', rule: 'vk'}, - {term: 'くる', source: 'くられる', rule: 'vk'}, - {term: 'くる', source: 'くられる', rule: 'vk'}, - {term: 'くる', source: 'くさせる', rule: 'vk'}, - {term: 'くる', source: 'くさせられる', rule: 'vk'}, - {term: 'くる', source: 'くい', rule: 'vk'}, - {term: 'くる', source: 'くない', rule: 'vk'}, - {term: 'くる', source: 'くません', rule: 'vk'}, - {term: 'くる', source: 'くなかった', rule: 'vk'}, - {term: 'くる', source: 'くませんでした', rule: 'vk'}, - {term: 'くる', source: 'くなくて', rule: 'vk'}, - {term: 'くる', source: 'くられない', rule: 'vk'}, - {term: 'くる', source: 'くられない', rule: 'vk'}, - {term: 'くる', source: 'くさせない', rule: 'vk'}, - {term: 'くる', source: 'くさせられない', rule: 'vk'}, - - {term: 'かわいい', source: 'かわいげ', rule: 'adj-i', reasons: ['-ge']}, - {term: '可愛い', source: 'かわいげ', rule: 'adj-i', reasons: ['-ge']} - ] - }, - { - valid: true, - tests: [ - // -e - {term: 'すごい', source: 'すげえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'やばい', source: 'やべえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'うるさい', source: 'うるせえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'ひどい', source: 'ひでえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'ない', source: 'ねえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'できる', source: 'できねえ', rule: 'v1', reasons: ['negative', '-e']}, - {term: 'しんじる', source: 'しんじねえ', rule: 'v1', reasons: ['negative', '-e']}, - {term: 'さむい', source: 'さめえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'さむい', source: 'さみい', rule: 'adj-i', reasons: ['-e']}, - {term: 'あつい', source: 'あちぇえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'あつい', source: 'あちい', rule: 'adj-i', reasons: ['-e']}, - {term: 'やすい', source: 'やせえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'たかい', source: 'たけえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'かわいい', source: 'かわええ', rule: 'adj-i', reasons: ['-e']}, - {term: 'つよい', source: 'ついぇえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'こわい', source: 'こうぇえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'みじかい', source: 'みじけえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'ながい', source: 'なげえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'くさい', source: 'くせえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'うまい', source: 'うめえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'でかい', source: 'でけえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'まずい', source: 'まっぜえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'ちっちゃい', source: 'ちっちぇえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'あかい', source: 'あけえ', rule: 'adj-i', reasons: ['-e']}, - {term: 'こわい', source: 'こええ', rule: 'adj-i', reasons: ['-e']}, - {term: 'つよい', source: 'つええ', rule: 'adj-i', reasons: ['-e']} - ] - } - ]; - - 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)})`; - } - assert.strictEqual(has, valid, message); - } - } -} - - -function main() { - testDeinflections(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-dictionary.js b/test/test-dictionary.js deleted file mode 100644 index d4390e19..00000000 --- a/test/test-dictionary.js +++ /dev/null @@ -1,66 +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 . - */ - -const path = require('path'); -const {createDictionaryArchive, testMain} = require('../dev/util'); -const dictionaryValidate = require('../dev/dictionary-validate'); - - -function createTestDictionaryArchive(dictionary, dictionaryName) { - const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary); - return createDictionaryArchive(dictionaryDirectory, dictionaryName); -} - - -async function main() { - const dictionaries = [ - {name: 'valid-dictionary1', valid: true}, - {name: 'invalid-dictionary1', valid: false}, - {name: 'invalid-dictionary2', valid: false}, - {name: 'invalid-dictionary3', valid: false}, - {name: 'invalid-dictionary4', valid: false}, - {name: 'invalid-dictionary5', valid: false}, - {name: 'invalid-dictionary6', valid: false} - ]; - - const schemas = dictionaryValidate.getSchemas(); - - for (const {name, valid} of dictionaries) { - 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 (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 . - */ - -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-dom-text-scanner.js b/test/test-dom-text-scanner.js deleted file mode 100644 index 37017b01..00000000 --- a/test/test-dom-text-scanner.js +++ /dev/null @@ -1,188 +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 . - */ - -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'); - - -function createJSDOM(fileName) { - const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); - return new JSDOM(domSource); -} - -function querySelectorTextNode(element, selector) { - let textIndex = -1; - const match = /::text$|::nth-text\((\d+)\)$/.exec(selector); - if (match !== null) { - textIndex = (match[1] ? parseInt(match[1], 10) - 1 : 0); - selector = selector.substring(0, selector.length - match[0].length); - } - const result = element.querySelector(selector); - if (textIndex < 0) { - return result; - } - for (let n = result.firstChild; n !== null; n = n.nextSibling) { - if (n.nodeType === n.constructor.TEXT_NODE) { - if (textIndex === 0) { - return n; - } - --textIndex; - } - } - return null; -} - - -function getComputedFontSizeInPixels(window, getComputedStyle, element) { - for (; element !== null; element = element.parentNode) { - if (element.nodeType === window.Node.ELEMENT_NODE) { - const fontSize = getComputedStyle(element).fontSize; - if (fontSize.endsWith('px')) { - const value = parseFloat(fontSize.substring(0, fontSize.length - 2)); - return value; - } - } - } - const defaultFontSize = 14; - return defaultFontSize; -} - -function createAbsoluteGetComputedStyle(window) { - // Wrapper to convert em units to px units - const getComputedStyleOld = window.getComputedStyle.bind(window); - return (element, ...args) => { - const style = getComputedStyleOld(element, ...args); - return new Proxy(style, { - get: (target, property) => { - let result = target[property]; - if (typeof result === 'string') { - result = result.replace(/([-+]?\d(?:\.\d)?(?:[eE][-+]?\d+)?)em/g, (g0, g1) => { - const fontSize = getComputedFontSizeInPixels(window, getComputedStyleOld, element); - return `${parseFloat(g1) * fontSize}px`; - }); - } - return result; - } - }); - }; -} - - -async function testDomTextScanner(dom, {DOMTextScanner}) { - 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); - } - - // 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)); - } - - if (reversible === false) { continue; } - - // Reversed test - { - const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); - scanner.seek(-length); - - const {content: actualContent} = scanner; - assert.strictEqual(actualContent, expectedContent); - } - - // 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)); - } - } - } -} - - -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(); - } -} - - -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 . - */ - -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 . - */ - -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-jsdom.js b/test/test-jsdom.js deleted file mode 100644 index 5078c240..00000000 --- a/test/test-jsdom.js +++ /dev/null @@ -1,50 +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 . - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); - -/** - * This function tests the following bug: - * - https://github.com/jsdom/jsdom/issues/3211 - * - https://github.com/dperini/nwsapi/issues/48 - */ -function testJSDOMSelectorBug() { - // nwsapi is used by JSDOM - const {JSDOM} = require('jsdom'); - const dom = new JSDOM(); - const {document} = dom.window; - const div = document.createElement('div'); - div.innerHTML = '
'; - const c = div.querySelector('.c'); - assert.doesNotThrow(() => { c.matches('.a:nth-last-of-type(1) .b .c'); }); -} - -function testJSDOM() { - testJSDOMSelectorBug(); -} - -function main() { - testJSDOM(); -} - -module.exports = { - testJSDOM -}; - -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 . - */ - -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 . - */ - -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 . - */ - -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-options-util.js b/test/test-options-util.js deleted file mode 100644 index d94028c0..00000000 --- a/test/test-options-util.js +++ /dev/null @@ -1,1609 +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 . - */ - -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; - } - } - }; - - 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'))) - }; - } - - 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 { - version: 14, - general: { - enable: true, - enableClipboardPopups: false, - resultOutputMode: 'group', - debugInfo: false, - maxResults: 32, - showAdvanced: false, - popupDisplayMode: 'default', - popupWidth: 400, - popupHeight: 250, - popupHorizontalOffset: 0, - popupVerticalOffset: 10, - popupHorizontalOffset2: 10, - popupVerticalOffset2: 0, - popupHorizontalTextPosition: 'below', - popupVerticalTextPosition: 'before', - popupScalingFactor: 1, - popupScaleRelativeToPageZoom: false, - popupScaleRelativeToVisualViewport: true, - showGuide: true, - compactTags: false, - compactGlossaries: false, - mainDictionary: '', - popupTheme: 'default', - popupOuterTheme: 'default', - customPopupCss: '', - customPopupOuterCss: '', - enableWanakana: true, - enableClipboardMonitor: false, - showPitchAccentDownstepNotation: true, - showPitchAccentPositionNotation: true, - showPitchAccentGraph: false, - showIframePopupsInRootFrame: false, - useSecurePopupFrameUrl: true, - usePopupShadowDom: true - }, - audio: { - enabled: true, - sources: ['jpod101', 'text-to-speech', 'custom'], - volume: 100, - autoPlay: false, - customSourceUrl: 'http://localhost/audio.mp3?term={expression}&reading={reading}', - textToSpeechVoice: 'example-voice' - }, - scanning: { - middleMouse: true, - touchInputEnabled: true, - selectText: true, - alphanumeric: true, - autoHideResults: false, - delay: 20, - length: 10, - modifier: 'shift', - deepDomScan: false, - popupNestingMaxDepth: 0, - enablePopupSearch: false, - enableOnPopupExpressions: false, - enableOnSearchPage: true, - enableSearchTags: false, - layoutAwareScan: false - }, - translation: { - convertHalfWidthCharacters: 'false', - convertNumericCharacters: 'false', - convertAlphabeticCharacters: 'false', - convertHiraganaToKatakana: 'false', - convertKatakanaToHiragana: 'variant', - collapseEmphaticSequences: 'false' - }, - dictionaries: { - 'Test Dictionary': { - priority: 0, - enabled: true, - allowSecondarySearches: false - } - }, - parsing: { - enableScanningParser: true, - enableMecabParser: false, - selectedParser: null, - termSpacing: true, - readingMode: 'hiragana' - }, - anki: { - enable: false, - server: 'http://127.0.0.1:8765', - tags: ['yomichan'], - sentenceExt: 200, - screenshot: {format: 'png', quality: 92}, - terms: {deck: '', model: '', fields: {}}, - kanji: {deck: '', model: '', fields: {}}, - duplicateScope: 'collection', - fieldTemplates: null - } - }; -} - -function createOptionsTestData1() { - return { - profiles: [ - { - name: 'Default', - options: createProfileOptionsTestData1(), - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'equal', - value: 1 - }, - { - type: 'popupLevel', - operator: 'notEqual', - value: 0 - }, - { - type: 'popupLevel', - operator: 'lessThan', - value: 3 - }, - { - type: 'popupLevel', - operator: 'greaterThan', - value: 0 - }, - { - type: 'popupLevel', - operator: 'lessThanOrEqual', - value: 2 - }, - { - type: 'popupLevel', - operator: 'greaterThanOrEqual', - value: 1 - } - ] - }, - { - conditions: [ - { - type: 'url', - operator: 'matchDomain', - value: 'example.com' - }, - { - type: 'url', - operator: 'matchRegExp', - value: 'example\\.com' - } - ] - }, - { - conditions: [ - { - type: 'modifierKeys', - operator: 'are', - value: [ - 'ctrl', - 'shift' - ] - }, - { - type: 'modifierKeys', - operator: 'areNot', - value: [ - 'alt', - 'shift' - ] - }, - { - type: 'modifierKeys', - operator: 'include', - value: 'alt' - }, - { - type: 'modifierKeys', - operator: 'notInclude', - value: 'ctrl' - } - ] - } - ] - } - ], - profileCurrent: 0, - version: 2, - global: { - database: { - prefixWildcardsSupported: false - } - } - }; -} - - -function createProfileOptionsUpdatedTestData1() { - return { - general: { - enable: true, - resultOutputMode: 'group', - debugInfo: false, - maxResults: 32, - showAdvanced: false, - popupDisplayMode: 'default', - popupWidth: 400, - popupHeight: 250, - popupHorizontalOffset: 0, - popupVerticalOffset: 10, - popupHorizontalOffset2: 10, - popupVerticalOffset2: 0, - popupHorizontalTextPosition: 'below', - popupVerticalTextPosition: 'before', - popupScalingFactor: 1, - popupScaleRelativeToPageZoom: false, - popupScaleRelativeToVisualViewport: true, - showGuide: true, - compactTags: false, - glossaryLayoutMode: 'default', - mainDictionary: '', - popupTheme: 'light', - popupOuterTheme: 'light', - customPopupCss: '', - customPopupOuterCss: '', - enableWanakana: true, - showPitchAccentDownstepNotation: true, - showPitchAccentPositionNotation: true, - showPitchAccentGraph: false, - showIframePopupsInRootFrame: false, - useSecurePopupFrameUrl: true, - usePopupShadowDom: true, - usePopupWindow: false, - popupCurrentIndicatorMode: 'triangle', - popupActionBarVisibility: 'auto', - popupActionBarLocation: 'top', - frequencyDisplayMode: 'split-tags-grouped', - termDisplayMode: 'ruby', - sortFrequencyDictionary: null, - sortFrequencyDictionaryOrder: 'descending' - }, - audio: { - enabled: true, - sources: [ - { - type: 'jpod101', - url: '', - voice: '' - }, - { - type: 'text-to-speech', - url: '', - voice: 'example-voice' - }, - { - type: 'custom', - url: 'http://localhost/audio.mp3?term={term}&reading={reading}', - voice: '' - } - ], - volume: 100, - autoPlay: false - }, - scanning: { - touchInputEnabled: true, - selectText: true, - alphanumeric: true, - autoHideResults: false, - delay: 20, - length: 10, - deepDomScan: false, - popupNestingMaxDepth: 0, - enablePopupSearch: false, - enableOnPopupExpressions: false, - enableOnSearchPage: true, - enableSearchTags: false, - layoutAwareScan: false, - hideDelay: 0, - pointerEventsEnabled: false, - matchTypePrefix: false, - hidePopupOnCursorExit: false, - hidePopupOnCursorExitDelay: 0, - normalizeCssZoom: true, - preventMiddleMouse: { - onWebPages: false, - onPopupPages: false, - onSearchPages: false, - onSearchQuery: false - }, - inputs: [ - { - include: 'shift', - exclude: 'mouse0', - types: { - mouse: true, - touch: false, - pen: false - }, - options: { - showAdvanced: false, - searchTerms: true, - searchKanji: true, - scanOnTouchMove: true, - scanOnTouchPress: true, - scanOnTouchRelease: false, - scanOnPenMove: true, - scanOnPenHover: true, - scanOnPenReleaseHover: false, - scanOnPenPress: true, - scanOnPenRelease: false, - preventTouchScrolling: true, - preventPenScrolling: true - } - }, - { - include: 'mouse2', - exclude: '', - types: { - mouse: true, - touch: false, - pen: false - }, - options: { - showAdvanced: false, - searchTerms: true, - searchKanji: true, - scanOnTouchMove: true, - scanOnTouchPress: true, - scanOnTouchRelease: false, - scanOnPenMove: true, - scanOnPenHover: true, - scanOnPenReleaseHover: false, - scanOnPenPress: true, - scanOnPenRelease: false, - preventTouchScrolling: true, - preventPenScrolling: true - } - }, - { - include: '', - exclude: '', - types: { - mouse: false, - touch: true, - pen: true - }, - options: { - showAdvanced: false, - searchTerms: true, - searchKanji: true, - scanOnTouchMove: true, - scanOnTouchPress: true, - scanOnTouchRelease: false, - scanOnPenMove: true, - scanOnPenHover: true, - scanOnPenReleaseHover: false, - scanOnPenPress: true, - scanOnPenRelease: false, - preventTouchScrolling: true, - preventPenScrolling: true - } - } - ] - }, - translation: { - convertHalfWidthCharacters: 'false', - convertNumericCharacters: 'false', - convertAlphabeticCharacters: 'false', - convertHiraganaToKatakana: 'false', - convertKatakanaToHiragana: 'variant', - collapseEmphaticSequences: 'false', - textReplacements: { - searchOriginal: true, - groups: [] - } - }, - dictionaries: [ - { - name: 'Test Dictionary', - priority: 0, - enabled: true, - allowSecondarySearches: false, - definitionsCollapsible: 'not-collapsible' - } - ], - parsing: { - enableScanningParser: true, - enableMecabParser: false, - selectedParser: null, - termSpacing: true, - readingMode: 'hiragana' - }, - anki: { - enable: false, - server: 'http://127.0.0.1:8765', - tags: ['yomichan'], - screenshot: {format: 'png', quality: 92}, - terms: {deck: '', model: '', fields: {}}, - kanji: {deck: '', model: '', fields: {}}, - duplicateScope: 'collection', - duplicateScopeCheckAllModels: false, - displayTags: 'never', - checkForDuplicates: true, - fieldTemplates: null, - suspendNewCards: false, - noteGuiMode: 'browse', - apiKey: '', - downloadTimeout: 0 - }, - sentenceParsing: { - scanExtent: 200, - terminationCharacterMode: 'custom', - terminationCharacters: [ - {enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '︒', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '︕', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '︖', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '︙', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true} - ] - }, - inputs: { - hotkeys: [ - {action: 'close', argument: '', key: 'Escape', modifiers: [], scopes: ['popup'], enabled: true}, - {action: 'focusSearchBox', argument: '', key: 'Escape', modifiers: [], scopes: ['search'], enabled: true}, - {action: 'previousEntry', argument: '3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'nextEntry', argument: '3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'lastEntry', argument: '', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'firstEntry', argument: '', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'previousEntry', argument: '1', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'nextEntry', argument: '1', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'historyBackward', argument: '', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'historyForward', argument: '', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteKanji', argument: '', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteTermKanji', argument: '', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteTermKana', argument: '', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'playAudio', argument: '', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'viewNote', argument: '', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'copyHostSelection', argument: '', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true} - ] - }, - popupWindow: { - width: 400, - height: 250, - left: 0, - top: 0, - useLeft: false, - useTop: false, - windowType: 'popup', - windowState: 'normal' - }, - clipboard: { - enableBackgroundMonitor: false, - enableSearchPageMonitor: false, - autoSearchContent: true, - maximumSearchLength: 1000 - }, - accessibility: { - forceGoogleDocsHtmlRendering: false - } - }; -} - -function createOptionsUpdatedTestData1() { - return { - profiles: [ - { - name: 'Default', - options: createProfileOptionsUpdatedTestData1(), - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'equal', - value: '1' - }, - { - type: 'popupLevel', - operator: 'notEqual', - value: '0' - }, - { - type: 'popupLevel', - operator: 'lessThan', - value: '3' - }, - { - type: 'popupLevel', - operator: 'greaterThan', - value: '0' - }, - { - type: 'popupLevel', - operator: 'lessThanOrEqual', - value: '2' - }, - { - type: 'popupLevel', - operator: 'greaterThanOrEqual', - value: '1' - } - ] - }, - { - conditions: [ - { - type: 'url', - operator: 'matchDomain', - value: 'example.com' - }, - { - type: 'url', - operator: 'matchRegExp', - value: 'example\\.com' - } - ] - }, - { - conditions: [ - { - type: 'modifierKeys', - operator: 'are', - value: 'ctrl, shift' - }, - { - type: 'modifierKeys', - operator: 'areNot', - value: 'alt, shift' - }, - { - type: 'modifierKeys', - operator: 'include', - value: 'alt' - }, - { - type: 'modifierKeys', - operator: 'notInclude', - value: 'ctrl' - } - ] - } - ] - } - ], - profileCurrent: 0, - version: 21, - global: { - database: { - prefixWildcardsSupported: false - } - } - }; -} - - -async function testUpdate(extDir) { - const vm = createVM(extDir); - const [OptionsUtil] = vm.get(['OptionsUtil']); - const optionsUtil = new OptionsUtil(); - await optionsUtil.prepare(); - - const options = createOptionsTestData1(); - const optionsUpdated = clone(await optionsUtil.update(options)); - const optionsExpected = createOptionsUpdatedTestData1(); - assert.deepStrictEqual(optionsUpdated, 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; - } - ]; - - const vm = createVM(extDir); - const [OptionsUtil] = vm.get(['OptionsUtil']); - const optionsUtil = new OptionsUtil(); - await optionsUtil.prepare(); - - for (const modify of data) { - const options = optionsUtil.getDefault(); - - const optionsModified = clone(options); - modify(optionsModified); - - const optionsUpdated = await optionsUtil.update(clone(optionsModified)); - assert.deepStrictEqual(clone(optionsUpdated), clone(options)); - } -} - -async function testFieldTemplatesUpdate(extDir) { - const vm = createVM(extDir); - const [OptionsUtil, TemplatePatcher] = vm.get(['OptionsUtil', 'TemplatePatcher']); - 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 data = [ - // Standard format - { - oldVersion: 0, - newVersion: 12, - old: ` -{{#*inline "character"}} - {{~definition.character~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "character"}} - {{~definition.character~}} -{{/inline}} - -<<>> -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // Non-standard marker format - { - oldVersion: 0, - newVersion: 12, - old: ` -{{#*inline "character"}} - {{~definition.character~}} -{{/inline}} - -{{~> (lookup . "marker2") ~}}`.trimStart(), - - expected: ` -{{#*inline "character"}} - {{~definition.character~}} -{{/inline}} - -{{~> (lookup . "marker2") ~}} -<<>>`.trimStart() - }, - // Empty test - { - oldVersion: 0, - newVersion: 12, - old: ` -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -<<>> -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // Definition tags update - { - oldVersion: 0, - newVersion: 12, - old: ` -{{#*inline "glossary-single"}} - {{~#unless brief~}} - {{~#if definitionTags~}}({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}) {{/if~}} - {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} -{{/inline}} - -{{#*inline "glossary-single2"}} - {{~#unless brief~}} - {{~#if definitionTags~}}({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}) {{/if~}} - {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} -{{/inline}} - -{{#*inline "glossary"}} - {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries~}} - {{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries~}} -{{/inline}} - -{{~> (lookup . "marker") ~}} -`.trimStart(), - - expected: ` -{{#*inline "glossary-single"}} - {{~#unless brief~}} - {{~#scope~}} - {{~#set "any" false}}{{/set~}} - {{~#if definitionTags~}}{{#each definitionTags~}} - {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{name}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/each~}} - {{~#if (get "any")}}) {{/if~}} - {{~/if~}} - {{~/scope~}} - {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} -{{/inline}} - -{{#*inline "glossary-single2"}} - {{~#unless brief~}} - {{~#scope~}} - {{~#set "any" false}}{{/set~}} - {{~#if definitionTags~}}{{#each definitionTags~}} - {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{name}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/each~}} - {{~#if (get "any")}}) {{/if~}} - {{~/if~}} - {{~/scope~}} - {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} -{{/inline}} - -{{#*inline "glossary"}} - {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}} - {{~> glossary-single definition brief=brief compactGlossaries=../compactGlossaries data=../.~}} -{{/inline}} - -<<>> -{{~> (lookup . "marker") ~}} -`.trimStart() - }, - // glossary and glossary-brief update - { - oldVersion: 7, - newVersion: 12, - old: ` -{{#*inline "glossary-single"}} - {{~#unless brief~}} - {{~#scope~}} - {{~#set "any" false}}{{/set~}} - {{~#if definitionTags~}}{{#each definitionTags~}} - {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{name}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/each~}} - {{~#if (get "any")}}) {{/if~}} - {{~/if~}} - {{~/scope~}} - {{~#if only~}}({{#each only}}{{{.}}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} - {{~#if glossary.[1]~}} - {{~#if compactGlossaries~}} - {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} - {{~else~}} -
    {{#each glossary}}
  • {{#multiLine}}{{.}}{{/multiLine}}
  • {{/each}}
- {{~/if~}} - {{~else~}} - {{~#multiLine}}{{glossary.[0]}}{{/multiLine~}} - {{~/if~}} -{{/inline}} - -{{#*inline "character"}} - {{~definition.character~}} -{{/inline}} - -{{#*inline "glossary"}} -
- {{~#if modeKanji~}} - {{~#if definition.glossary.[1]~}} -
    {{#each definition.glossary}}
  1. {{.}}
  2. {{/each}}
- {{~else~}} - {{definition.glossary.[0]}} - {{~/if~}} - {{~else~}} - {{~#if group~}} - {{~#if definition.definitions.[1]~}} -
    {{#each definition.definitions}}
  1. {{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}
  2. {{/each}}
- {{~else~}} - {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}} - {{~/if~}} - {{~else if merge~}} - {{~#if definition.definitions.[1]~}} -
    {{#each definition.definitions}}
  1. {{> glossary-single brief=../brief compactGlossaries=../compactGlossaries data=../.}}
  2. {{/each}}
- {{~else~}} - {{~> glossary-single definition.definitions.[0] brief=brief compactGlossaries=compactGlossaries data=.~}} - {{~/if~}} - {{~else~}} - {{~> glossary-single definition brief=brief compactGlossaries=compactGlossaries data=.~}} - {{~/if~}} - {{~/if~}} -
-{{/inline}} - -{{#*inline "glossary-brief"}} - {{~> glossary brief=true ~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "glossary-single"}} - {{~#unless brief~}} - {{~#scope~}} - {{~#set "any" false}}{{/set~}} - {{~#each definitionTags~}} - {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{name}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/each~}} - {{~#unless noDictionaryTag~}} - {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{dictionary}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/unless~}} - {{~#if (get "any")}}) {{/if~}} - {{~/scope~}} - {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} - {{~#if (op "<=" glossary.length 1)~}} - {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} - {{~else if @root.compactGlossaries~}} - {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} - {{~else~}} -
    {{#each glossary}}
  • {{#multiLine}}{{.}}{{/multiLine}}
  • {{/each}}
- {{~/if~}} - {{~#set "previousDictionary" dictionary~}}{{~/set~}} -{{/inline}} - -{{#*inline "character"}} - {{~definition.character~}} -{{/inline}} - -{{~#*inline "glossary"~}} -
- {{~#scope~}} - {{~#if (op "===" definition.type "term")~}} - {{~> glossary-single definition brief=brief noDictionaryTag=noDictionaryTag ~}} - {{~else if (op "||" (op "===" definition.type "termGrouped") (op "===" definition.type "termMerged"))~}} - {{~#if (op ">" definition.definitions.length 1)~}} -
    {{~#each definition.definitions~}}
  1. {{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}
  2. {{~/each~}}
- {{~else~}} - {{~#each definition.definitions~}}{{~> glossary-single . brief=../brief noDictionaryTag=../noDictionaryTag ~}}{{~/each~}} - {{~/if~}} - {{~else if (op "===" definition.type "kanji")~}} - {{~#if (op ">" definition.glossary.length 1)~}} -
    {{#each definition.glossary}}
  1. {{.}}
  2. {{/each}}
- {{~else~}} - {{~#each definition.glossary~}}{{.}}{{~/each~}} - {{~/if~}} - {{~/if~}} - {{~/scope~}} -
-{{~/inline~}} - -{{#*inline "glossary-no-dictionary"}} - {{~> glossary noDictionaryTag=true ~}} -{{/inline}} - -{{#*inline "glossary-brief"}} - {{~> glossary brief=true ~}} -{{/inline}} - -<<>> -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // formatGlossary update - { - oldVersion: 12, - newVersion: 13, - old: ` -{{#*inline "example"}} - {{~#if (op "<=" glossary.length 1)~}} - {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} - {{~else if @root.compactGlossaries~}} - {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{#unless @last}} | {{/unless}}{{/each}} - {{~else~}} -
    {{#each glossary}}
  • {{#multiLine}}{{.}}{{/multiLine}}
  • {{/each}}
- {{~/if~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "example"}} - {{~#if (op "<=" glossary.length 1)~}} - {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} - {{~else if @root.compactGlossaries~}} - {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} - {{~else~}} -
    {{#each glossary}}
  • {{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}
  • {{/each}}
- {{~/if~}} -{{/inline}} - -<<>> -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // hasMedia/getMedia update - { - oldVersion: 12, - newVersion: 13, - old: ` -{{#*inline "audio"}} - {{~#if definition.audioFileName~}} - [sound:{{definition.audioFileName}}] - {{~/if~}} -{{/inline}} - -{{#*inline "screenshot"}} - -{{/inline}} - -{{#*inline "clipboard-image"}} - {{~#if definition.clipboardImageFileName~}} - - {{~/if~}} -{{/inline}} - -{{#*inline "clipboard-text"}} - {{~#if definition.clipboardText~}}{{definition.clipboardText}}{{~/if~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "audio"}} - {{~#if (hasMedia "audio")~}} - [sound:{{#getMedia "audio"}}{{/getMedia}}] - {{~/if~}} -{{/inline}} - -{{#*inline "screenshot"}} - {{~#if (hasMedia "screenshot")~}} - - {{~/if~}} -{{/inline}} - -{{#*inline "clipboard-image"}} - {{~#if (hasMedia "clipboardImage")~}} - - {{~/if~}} -{{/inline}} - -{{#*inline "clipboard-text"}} - {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} -{{/inline}} - -<<>> -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // hasMedia/getMedia update - { - oldVersion: 12, - newVersion: 13, - old: ` -{{! Pitch Accents }} -{{#*inline "pitch-accent-item-downstep-notation"}} - {{~#scope~}} - - {{~#set "style1a"~}}display:inline-block;position:relative;{{~/set~}} - {{~#set "style1b"~}}padding-right:0.1em;margin-right:0.1em;{{~/set~}} - {{~#set "style2a"~}}display:block;user-select:none;pointer-events:none;position:absolute;top:0.1em;left:0;right:0;height:0;border-top:0.1em solid;{{~/set~}} - {{~#set "style2b"~}}right:-0.1em;height:0.4em;border-right:0.1em solid;{{~/set~}} - {{~#each (getKanaMorae reading)~}} - {{~#set "style1"}}{{#get "style1a"}}{{/get}}{{/set~}} - {{~#set "style2"}}{{/set~}} - {{~#if (isMoraPitchHigh @index ../position)}} - {{~#set "style2"}}{{#get "style2a"}}{{/get}}{{/set~}} - {{~#if (op "!" (isMoraPitchHigh (op "+" @index 1) ../position))~}} - {{~#set "style1" (op "+" (get "style1") (get "style1b"))}}{{/set~}} - {{~#set "style2" (op "+" (get "style2") (get "style2b"))}}{{/set~}} - {{~/if~}} - {{~/if~}} - {{{.}}} - {{~/each~}} - - {{~/scope~}} -{{/inline}} - -{{#*inline "pitch-accent-item-graph-position-x"}}{{#op "+" 25 (op "*" index 50)}}{{/op}}{{/inline}} -{{#*inline "pitch-accent-item-graph-position-y"}}{{#op "+" 25 (op "?:" (isMoraPitchHigh index position) 0 50)}}{{/op}}{{/inline}} -{{#*inline "pitch-accent-item-graph-position"}}{{> pitch-accent-item-graph-position-x index=index position=position}} {{> pitch-accent-item-graph-position-y index=index position=position}}{{/inline}} -{{#*inline "pitch-accent-item-graph"}} - {{~#scope~}} - {{~#set "morae" (getKanaMorae reading)}}{{/set~}} - {{~#set "morae-count" (property (get "morae") "length")}}{{/set~}} - - - - - - - pitch-accent-item-graph-position index=@index position=../position~}} - {{~#set "cmd" "L"}}{{/set~}} - {{~/each~}} - "> - pitch-accent-item-graph-position index=(get "morae-count") position=position}}"> - {{#each (get "morae")}} - - {{/each}} - - - {{~/scope~}} -{{/inline}} - -{{#*inline "pitch-accent-item-position"~}} - [{{position}}] -{{~/inline}} - -{{#*inline "pitch-accent-item"}} - {{~#if (op "==" format "downstep-notation")~}} - {{~> pitch-accent-item-downstep-notation~}} - {{~else if (op "==" format "graph")~}} - {{~> pitch-accent-item-graph~}} - {{~else if (op "==" format "position")~}} - {{~> pitch-accent-item-position~}} - {{~/if~}} -{{/inline}} - -{{#*inline "pitch-accent-item-disambiguation"}} - {{~#scope~}} - {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} - {{~#if (op ">" (property (get "exclusive") "length") 0)~}} - {{~#set "separator" ""~}}{{/set~}} - ({{#each (get "exclusive")~}} - {{~#get "separator"}}{{/get~}}{{{.}}} - {{~/each}} only) - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{#*inline "pitch-accent-list"}} - {{~#if (op ">" pitchCount 0)~}} - {{~#if (op ">" pitchCount 1)~}}
    {{~/if~}} - {{~#each pitches~}} - {{~#each pitches~}} - {{~#if (op ">" ../../pitchCount 1)~}}
  1. {{~/if~}} - {{~> pitch-accent-item-disambiguation~}} - {{~> pitch-accent-item format=../../format~}} - {{~#if (op ">" ../../pitchCount 1)~}}
  2. {{~/if~}} - {{~/each~}} - {{~/each~}} - {{~#if (op ">" pitchCount 1)~}}
{{~/if~}} - {{~else~}} - No pitch accent data - {{~/if~}} -{{/inline}} - -{{#*inline "pitch-accents"}} - {{~> pitch-accent-list format='downstep-notation'~}} -{{/inline}} - -{{#*inline "pitch-accent-graphs"}} - {{~> pitch-accent-list format='graph'~}} -{{/inline}} - -{{#*inline "pitch-accent-positions"}} - {{~> pitch-accent-list format='position'~}} -{{/inline}} -{{! End Pitch Accents }} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{! Pitch Accents }} -{{#*inline "pitch-accent-item"}} - {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} -{{/inline}} - -{{#*inline "pitch-accent-item-disambiguation"}} - {{~#scope~}} - {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} - {{~#if (op ">" (property (get "exclusive") "length") 0)~}} - {{~#set "separator" ""~}}{{/set~}} - ({{#each (get "exclusive")~}} - {{~#get "separator"}}{{/get~}}{{{.}}} - {{~/each}} only) - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{#*inline "pitch-accent-list"}} - {{~#if (op ">" pitchCount 0)~}} - {{~#if (op ">" pitchCount 1)~}}
    {{~/if~}} - {{~#each pitches~}} - {{~#each pitches~}} - {{~#if (op ">" ../../pitchCount 1)~}}
  1. {{~/if~}} - {{~> pitch-accent-item-disambiguation~}} - {{~> pitch-accent-item format=../../format~}} - {{~#if (op ">" ../../pitchCount 1)~}}
  2. {{~/if~}} - {{~/each~}} - {{~/each~}} - {{~#if (op ">" pitchCount 1)~}}
{{~/if~}} - {{~else~}} - No pitch accent data - {{~/if~}} -{{/inline}} - -{{#*inline "pitch-accents"}} - {{~> pitch-accent-list format='text'~}} -{{/inline}} - -{{#*inline "pitch-accent-graphs"}} - {{~> pitch-accent-list format='graph'~}} -{{/inline}} - -{{#*inline "pitch-accent-positions"}} - {{~> pitch-accent-list format='position'~}} -{{/inline}} -{{! End Pitch Accents }} - -<<>> -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: furigana and furiganaPlain - { - oldVersion: 20, - newVersion: 21, - old: ` -{{#*inline "furigana"}} - {{~#if merge~}} - {{~#each definition.expressions~}} - {{~#furigana}}{{{.}}}{{/furigana~}} - {{~#unless @last}}、{{/unless~}} - {{~/each~}} - {{~else~}} - {{#furigana}}{{{definition}}}{{/furigana}} - {{~/if~}} -{{/inline}} - -{{#*inline "furigana-plain"}} - {{~#if merge~}} - {{~#each definition.expressions~}} - {{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}} - {{~#unless @last}}、{{/unless~}} - {{~/each~}} - {{~else~}} - {{#furiganaPlain}}{{{definition}}}{{/furiganaPlain}} - {{~/if~}} -{{/inline}} - -{{#*inline "frequencies"}} - {{~#if (op ">" definition.frequencies.length 0)~}} -
    - {{~#each definition.frequencies~}} -
  • - {{~#if (op "!==" ../definition.type "kanji")~}} - {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( - {{~#furigana expression reading~}}{{~/furigana~}} - ) {{/if~}} - {{~/if~}} - {{~dictionary}}: {{frequency~}} -
  • - {{~/each~}} -
- {{~/if~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "furigana"}} - {{~#if merge~}} - {{~#each definition.expressions~}} - {{~furigana .~}} - {{~#unless @last}}、{{/unless~}} - {{~/each~}} - {{~else~}} - {{furigana definition}} - {{~/if~}} -{{/inline}} - -{{#*inline "furigana-plain"}} - {{~#if merge~}} - {{~#each definition.expressions~}} - {{~furiganaPlain .~}} - {{~#unless @last}}、{{/unless~}} - {{~/each~}} - {{~else~}} - {{furiganaPlain definition}} - {{~/if~}} -{{/inline}} - -{{#*inline "frequencies"}} - {{~#if (op ">" definition.frequencies.length 0)~}} -
    - {{~#each definition.frequencies~}} -
  • - {{~#if (op "!==" ../definition.type "kanji")~}} - {{~#if (op "||" (op ">" ../uniqueExpressions.length 1) (op ">" ../uniqueReadings.length 1))~}}( - {{~furigana expression reading~}} - ) {{/if~}} - {{~/if~}} - {{~dictionary}}: {{frequency~}} -
  • - {{~/each~}} -
- {{~/if~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: formatGlossary - { - oldVersion: 20, - newVersion: 21, - old: ` -{{#*inline "glossary-single"}} - {{~#unless brief~}} - {{~#scope~}} - {{~#set "any" false}}{{/set~}} - {{~#each definitionTags~}} - {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{name}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/each~}} - {{~#unless noDictionaryTag~}} - {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{dictionary}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/unless~}} - {{~#if (get "any")}}) {{/if~}} - {{~/scope~}} - {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} - {{~#if (op "<=" glossary.length 1)~}} - {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} - {{~else if @root.compactGlossaries~}} - {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{#unless @last}} | {{/unless}}{{/each}} - {{~else~}} -
    {{#each glossary}}
  • {{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}
  • {{/each}}
- {{~/if~}} - {{~#set "previousDictionary" dictionary~}}{{~/set~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "glossary-single"}} - {{~#unless brief~}} - {{~#scope~}} - {{~set "any" false~}} - {{~#each definitionTags~}} - {{~#if (op "||" (op "!" @root.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{name}} - {{~set "any" true~}} - {{~/if~}} - {{~/each~}} - {{~#unless noDictionaryTag~}} - {{~#if (op "||" (op "!" @root.compactTags) (op "!==" dictionary (get "previousDictionary")))~}} - {{~#if (get "any")}}, {{else}}({{/if~}} - {{dictionary}} - {{~set "any" true~}} - {{~/if~}} - {{~/unless~}} - {{~#if (get "any")}}) {{/if~}} - {{~/scope~}} - {{~#if only~}}({{#each only}}{{.}}{{#unless @last}}, {{/unless}}{{/each}} only) {{/if~}} - {{~/unless~}} - {{~#if (op "<=" glossary.length 1)~}} - {{#each glossary}}{{formatGlossary ../dictionary .}}{{/each}} - {{~else if @root.compactGlossaries~}} - {{#each glossary}}{{formatGlossary ../dictionary .}}{{#unless @last}} | {{/unless}}{{/each}} - {{~else~}} -
    {{#each glossary}}
  • {{formatGlossary ../dictionary .}}
  • {{/each}}
- {{~/if~}} - {{~set "previousDictionary" dictionary~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: set and get - { - oldVersion: 20, - newVersion: 21, - old: ` -{{#*inline "pitch-accent-item-disambiguation"}} - {{~#scope~}} - {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} - {{~#if (op ">" (property (get "exclusive") "length") 0)~}} - {{~#set "separator" ""~}}{{/set~}} - ({{#each (get "exclusive")~}} - {{~#get "separator"}}{{/get~}}{{{.}}} - {{~/each}} only) - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{#*inline "stroke-count"}} - {{~#scope~}} - {{~#set "found" false}}{{/set~}} - {{~#each definition.stats.misc~}} - {{~#if (op "===" name "strokes")~}} - {{~#set "found" true}}{{/set~}} - Stroke count: {{value}} - {{~/if~}} - {{~/each~}} - {{~#if (op "!" (get "found"))~}} - Stroke count: Unknown - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{#*inline "part-of-speech"}} - {{~#scope~}} - {{~#if (op "!==" definition.type "kanji")~}} - {{~#set "first" true}}{{/set~}} - {{~#each definition.expressions~}} - {{~#each wordClasses~}} - {{~#unless (get (concat "used_" .))~}} - {{~> part-of-speech-pretty . ~}} - {{~#unless (get "first")}}, {{/unless~}} - {{~#set (concat "used_" .) true~}}{{~/set~}} - {{~#set "first" false~}}{{~/set~}} - {{~/unless~}} - {{~/each~}} - {{~/each~}} - {{~#if (get "first")~}}Unknown{{~/if~}} - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "pitch-accent-item-disambiguation"}} - {{~#scope~}} - {{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}} - {{~#if (op ">" (property (get "exclusive") "length") 0)~}} - {{~set "separator" ""~}} - ({{#each (get "exclusive")~}} - {{~get "separator"~}}{{{.}}} - {{~/each}} only) - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{#*inline "stroke-count"}} - {{~#scope~}} - {{~set "found" false~}} - {{~#each definition.stats.misc~}} - {{~#if (op "===" name "strokes")~}} - {{~set "found" true~}} - Stroke count: {{value}} - {{~/if~}} - {{~/each~}} - {{~#if (op "!" (get "found"))~}} - Stroke count: Unknown - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{#*inline "part-of-speech"}} - {{~#scope~}} - {{~#if (op "!==" definition.type "kanji")~}} - {{~set "first" true~}} - {{~#each definition.expressions~}} - {{~#each wordClasses~}} - {{~#unless (get (concat "used_" .))~}} - {{~> part-of-speech-pretty . ~}} - {{~#unless (get "first")}}, {{/unless~}} - {{~set (concat "used_" .) true~}} - {{~set "first" false~}} - {{~/unless~}} - {{~/each~}} - {{~/each~}} - {{~#if (get "first")~}}Unknown{{~/if~}} - {{~/if~}} - {{~/scope~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: hasMedia and getMedia - { - oldVersion: 20, - newVersion: 21, - old: ` -{{#*inline "audio"}} - {{~#if (hasMedia "audio")~}} - [sound:{{#getMedia "audio"}}{{/getMedia}}] - {{~/if~}} -{{/inline}} - -{{#*inline "screenshot"}} - {{~#if (hasMedia "screenshot")~}} - - {{~/if~}} -{{/inline}} - -{{#*inline "clipboard-image"}} - {{~#if (hasMedia "clipboardImage")~}} - - {{~/if~}} -{{/inline}} - -{{#*inline "clipboard-text"}} - {{~#if (hasMedia "clipboardText")}}{{#getMedia "clipboardText"}}{{/getMedia}}{{/if~}} -{{/inline}} - -{{#*inline "selection-text"}} - {{~#if (hasMedia "selectionText")}}{{#getMedia "selectionText"}}{{/getMedia}}{{/if~}} -{{/inline}} - -{{#*inline "sentence-furigana"}} - {{~#if definition.cloze~}} - {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} - {{#getMedia "textFurigana" definition.cloze.sentence escape=false}}{{/getMedia}} - {{~else~}} - {{definition.cloze.sentence}} - {{~/if~}} - {{~/if~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart(), - - expected: ` -{{#*inline "audio"}} - {{~#if (hasMedia "audio")~}} - [sound:{{getMedia "audio"}}] - {{~/if~}} -{{/inline}} - -{{#*inline "screenshot"}} - {{~#if (hasMedia "screenshot")~}} - - {{~/if~}} -{{/inline}} - -{{#*inline "clipboard-image"}} - {{~#if (hasMedia "clipboardImage")~}} - - {{~/if~}} -{{/inline}} - -{{#*inline "clipboard-text"}} - {{~#if (hasMedia "clipboardText")}}{{getMedia "clipboardText"}}{{/if~}} -{{/inline}} - -{{#*inline "selection-text"}} - {{~#if (hasMedia "selectionText")}}{{getMedia "selectionText"}}{{/if~}} -{{/inline}} - -{{#*inline "sentence-furigana"}} - {{~#if definition.cloze~}} - {{~#if (hasMedia "textFurigana" definition.cloze.sentence)~}} - {{getMedia "textFurigana" definition.cloze.sentence escape=false}} - {{~else~}} - {{definition.cloze.sentence}} - {{~/if~}} - {{~/if~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart() - }, - // 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: ` -{{#*inline "pitch-accent-item"}} - {{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}} -{{/inline}} - -{{~> (lookup . "marker") ~}}`.trimStart() - } - ]; - - const updatesPattern = /<<>>/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 optionsUpdated = clone(await optionsUtil.update(options, newVersion)); - const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates; - assert.deepStrictEqual(fieldTemplatesActual, expected2); - } -} - - -async function main() { - const extDir = path.join(__dirname, '..', 'ext'); - await testUpdate(extDir); - await testDefault(extDir); - await testFieldTemplatesUpdate(extDir); -} - - -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 . - */ - -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 . - */ - -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 . - */ - -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 . - */ - -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 . + */ + +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 . + */ + +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(); -- cgit v1.2.3