diff options
Diffstat (limited to 'test')
34 files changed, 5993 insertions, 6453 deletions
diff --git a/test/anki-note-builder.test.js b/test/anki-note-builder.test.js new file mode 100644 index 00000000..90bb3cbe --- /dev/null +++ b/test/anki-note-builder.test.js @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2021-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import 'fake-indexeddb/auto'; +import fs from 'fs'; +import {fileURLToPath} from 'node:url'; +import path from 'path'; +import url from 'url'; +import {describe, test, vi} from 'vitest'; +import {TranslatorVM} from '../dev/translator-vm.js'; +import {AnkiNoteBuilder} from '../ext/js/data/anki-note-builder.js'; +import {JapaneseUtil} from '../ext/js/language/sandbox/japanese-util.js'; + +vi.stubGlobal('fetch', async (url2) => { + const extDir = path.join(__dirname, '..', 'ext'); + let filePath; + try { + filePath = url.fileURLToPath(url2); + } catch (e) { + filePath = path.resolve(extDir, url2.replace(/^[/\\]/, '')); + } + await Promise.resolve(); + const content = fs.readFileSync(filePath, {encoding: null}); + return { + ok: true, + status: 200, + statusText: 'OK', + text: async () => Promise.resolve(content.toString('utf8')), + json: async () => Promise.resolve(JSON.parse(content.toString('utf8'))) + }; +}); +vi.mock('../ext/js/templates/template-renderer-proxy.js'); + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function createVM() { + const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', 'valid-dictionary1'); + const vm = new TranslatorVM(); + + await vm.prepare(dictionaryDirectory, 'Test Dictionary 2'); + + return vm; +} + +function getFieldMarkers(type) { + switch (type) { + case 'terms': + return [ + 'audio', + 'clipboard-image', + 'clipboard-text', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'conjugation', + 'dictionary', + 'document-title', + 'expression', + 'frequencies', + 'furigana', + 'furigana-plain', + 'glossary', + 'glossary-brief', + 'glossary-no-dictionary', + 'part-of-speech', + 'pitch-accents', + 'pitch-accent-graphs', + 'pitch-accent-positions', + 'reading', + 'screenshot', + 'search-query', + 'selection-text', + 'sentence', + 'sentence-furigana', + 'tags', + 'url' + ]; + case 'kanji': + return [ + 'character', + 'clipboard-image', + 'clipboard-text', + 'cloze-body', + 'cloze-prefix', + 'cloze-suffix', + 'dictionary', + 'document-title', + 'glossary', + 'kunyomi', + 'onyomi', + 'screenshot', + 'search-query', + 'selection-text', + 'sentence', + 'sentence-furigana', + 'stroke-count', + 'tags', + 'url' + ]; + default: + return []; + } +} + +async function getRenderResults(dictionaryEntries, type, mode, template, expect) { + const markers = getFieldMarkers(type); + const fields = []; + for (const marker of markers) { + fields.push([marker, `{${marker}}`]); + } + + const japaneseUtil = new JapaneseUtil(null); + const clozePrefix = 'cloze-prefix'; + const clozeSuffix = 'cloze-suffix'; + const results = []; + for (const dictionaryEntry of dictionaryEntries) { + let source = ''; + switch (dictionaryEntry.type) { + case 'kanji': + source = dictionaryEntry.character; + break; + case 'term': + if (dictionaryEntry.headwords.length > 0 && dictionaryEntry.headwords[0].sources.length > 0) { + source = dictionaryEntry.headwords[0].sources[0].originalText; + } + break; + } + const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); + const context = { + url: 'url:', + sentence: { + text: `${clozePrefix}${source}${clozeSuffix}`, + offset: clozePrefix.length + }, + documentTitle: 'title', + query: 'query', + fullQuery: 'fullQuery' + }; + const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote({ + dictionaryEntry, + mode: null, + context, + template, + deckName: 'deckName', + modelName: 'modelName', + fields, + tags: ['yomichan'], + checkForDuplicates: true, + duplicateScope: 'collection', + duplicateScopeCheckAllModels: false, + resultOutputMode: mode, + glossaryLayoutMode: 'default', + compactTags: false + }); + for (const error of errors) { + console.error(error); + } + expect(errors.length).toStrictEqual(0); + results.push(noteFields); + } + + return results; +} + + +async function main() { + const vm = await createVM(); + + const testInputsFilePath = path.join(dirname, 'data', 'translator-test-inputs.json'); + const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'})); + + const testResults1FilePath = path.join(dirname, 'data', 'anki-note-builder-test-results.json'); + const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'})); + const actualResults1 = []; + + const template = fs.readFileSync(path.join(dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'}); + + describe.concurrent('AnkiNoteBuilder', () => { + for (let i = 0, ii = tests.length; i < ii; ++i) { + const t = tests[i]; + test(`${t.name}`, async ({expect}) => { + const expected1 = expectedResults1[i]; + switch (t.func) { + case 'findTerms': + { + const {name, mode, text} = t; + const options = vm.buildOptions(optionsPresets, t.options); + const {dictionaryEntries} = structuredClone(await vm.translator.findTerms(mode, text, options)); + const results = mode !== 'simple' ? structuredClone(await getRenderResults(dictionaryEntries, 'terms', mode, template, expect)) : null; + actualResults1.push({name, results}); + expect(results).toStrictEqual(expected1.results); + } + break; + case 'findKanji': + { + const {name, text} = t; + const options = vm.buildOptions(optionsPresets, t.options); + const dictionaryEntries = structuredClone(await vm.translator.findKanji(text, options)); + const results = structuredClone(await getRenderResults(dictionaryEntries, 'kanji', null, template, expect)); + actualResults1.push({name, results}); + expect(results).toStrictEqual(expected1.results); + } + break; + } + }); + } + }); +} +await main(); diff --git a/test/cache-map.test.js b/test/cache-map.test.js new file mode 100644 index 00000000..9d10a719 --- /dev/null +++ b/test/cache-map.test.js @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {expect, test} from 'vitest'; +import {CacheMap} from '../ext/js/general/cache-map.js'; + +function testConstructor() { + test('constructor', () => { + const data = [ + [false, () => new CacheMap(0)], + [false, () => new CacheMap(1)], + [false, () => new CacheMap(Number.MAX_VALUE)], + [true, () => new CacheMap(-1)], + [true, () => new CacheMap(1.5)], + [true, () => new CacheMap(Number.NaN)], + [true, () => new CacheMap(Number.POSITIVE_INFINITY)], + [true, () => new CacheMap('a')] + ]; + + for (const [throws, create] of data) { + if (throws) { + expect(create).toThrowError(); + } else { + expect(create).not.toThrowError(); + } + } + }); +} + +function testApi() { + test('api', () => { + const data = [ + { + maxSize: 1, + expectedSize: 0, + calls: [] + }, + { + maxSize: 10, + expectedSize: 1, + calls: [ + {func: 'get', args: ['a1-b-c'], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: false}, + {func: 'set', args: ['a1-b-c', 32], returnValue: void 0}, + {func: 'get', args: ['a1-b-c'], returnValue: 32}, + {func: 'has', args: ['a1-b-c'], returnValue: true} + ] + }, + { + maxSize: 10, + expectedSize: 2, + calls: [ + {func: 'set', args: ['a1-b-c', 32], returnValue: void 0}, + {func: 'get', args: ['a1-b-c'], returnValue: 32}, + {func: 'set', args: ['a1-b-c', 64], returnValue: void 0}, + {func: 'get', args: ['a1-b-c'], returnValue: 64}, + {func: 'set', args: ['a2-b-c', 96], returnValue: void 0}, + {func: 'get', args: ['a2-b-c'], returnValue: 96} + ] + }, + { + maxSize: 2, + expectedSize: 2, + calls: [ + {func: 'has', args: ['a1-b-c'], returnValue: false}, + {func: 'has', args: ['a2-b-c'], returnValue: false}, + {func: 'has', args: ['a3-b-c'], returnValue: false}, + {func: 'set', args: ['a1-b-c', 1], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: true}, + {func: 'has', args: ['a2-b-c'], returnValue: false}, + {func: 'has', args: ['a3-b-c'], returnValue: false}, + {func: 'set', args: ['a2-b-c', 2], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: true}, + {func: 'has', args: ['a2-b-c'], returnValue: true}, + {func: 'has', args: ['a3-b-c'], returnValue: false}, + {func: 'set', args: ['a3-b-c', 3], returnValue: void 0}, + {func: 'has', args: ['a1-b-c'], returnValue: false}, + {func: 'has', args: ['a2-b-c'], returnValue: true}, + {func: 'has', args: ['a3-b-c'], returnValue: true} + ] + } + ]; + + for (const {maxSize, expectedSize, calls} of data) { + const cache = new CacheMap(maxSize); + expect(cache.maxSize).toStrictEqual(maxSize); + for (const call of calls) { + const {func, args} = call; + let returnValue; + switch (func) { + case 'get': returnValue = cache.get(...args); break; + case 'set': returnValue = cache.set(...args); break; + case 'has': returnValue = cache.has(...args); break; + case 'clear': returnValue = cache.clear(...args); break; + } + if (Object.prototype.hasOwnProperty.call(call, 'returnValue')) { + const {returnValue: expectedReturnValue} = call; + expect(returnValue).toStrictEqual(expectedReturnValue); + } + } + expect(cache.size).toStrictEqual(expectedSize); + } + }); +} + + +function main() { + testConstructor(); + testApi(); +} + + +main(); diff --git a/test/core.test.js b/test/core.test.js new file mode 100644 index 00000000..203460f4 --- /dev/null +++ b/test/core.test.js @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {describe, expect, test} from 'vitest'; +import {DynamicProperty, deepEqual} from '../ext/js/core.js'; + +function testDynamicProperty() { + test('DynamicProperty', () => { + const data = [ + { + initialValue: 0, + operations: [ + { + operation: null, + expectedDefaultValue: 0, + expectedValue: 0, + expectedOverrideCount: 0, + expeectedEventOccurred: false + }, + { + operation: 'set.defaultValue', + args: [1], + expectedDefaultValue: 1, + expectedValue: 1, + expectedOverrideCount: 0, + expeectedEventOccurred: true + }, + { + operation: 'set.defaultValue', + args: [1], + expectedDefaultValue: 1, + expectedValue: 1, + expectedOverrideCount: 0, + expeectedEventOccurred: false + }, + { + operation: 'set.defaultValue', + args: [0], + expectedDefaultValue: 0, + expectedValue: 0, + expectedOverrideCount: 0, + expeectedEventOccurred: true + }, + { + operation: 'setOverride', + args: [8], + expectedDefaultValue: 0, + expectedValue: 8, + expectedOverrideCount: 1, + expeectedEventOccurred: true + }, + { + operation: 'setOverride', + args: [16], + expectedDefaultValue: 0, + expectedValue: 8, + expectedOverrideCount: 2, + expeectedEventOccurred: false + }, + { + operation: 'setOverride', + args: [32, 1], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 3, + expeectedEventOccurred: true + }, + { + operation: 'setOverride', + args: [64, -1], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 4, + expeectedEventOccurred: false + }, + { + operation: 'clearOverride', + args: [-4], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 3, + expeectedEventOccurred: false + }, + { + operation: 'clearOverride', + args: [-3], + expectedDefaultValue: 0, + expectedValue: 32, + expectedOverrideCount: 2, + expeectedEventOccurred: false + }, + { + operation: 'clearOverride', + args: [-2], + expectedDefaultValue: 0, + expectedValue: 64, + expectedOverrideCount: 1, + expeectedEventOccurred: true + }, + { + operation: 'clearOverride', + args: [-1], + expectedDefaultValue: 0, + expectedValue: 0, + expectedOverrideCount: 0, + expeectedEventOccurred: true + } + ] + } + ]; + + for (const {initialValue, operations} of data) { + const property = new DynamicProperty(initialValue); + const overrideTokens = []; + let eventOccurred = false; + const onChange = () => { eventOccurred = true; }; + property.on('change', onChange); + for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) { + eventOccurred = false; + switch (operation) { + case 'set.defaultValue': property.defaultValue = args[0]; break; + case 'setOverride': overrideTokens.push(property.setOverride(...args)); break; + case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break; + } + expect(eventOccurred).toStrictEqual(expeectedEventOccurred); + expect(property.defaultValue).toStrictEqual(expectedDefaultValue); + expect(property.value).toStrictEqual(expectedValue); + expect(property.overrideCount).toStrictEqual(expectedOverrideCount); + } + property.off('change', onChange); + } + }); +} + +function testDeepEqual() { + describe('deepEqual', () => { + const data = [ + // Simple tests + { + value1: 0, + value2: 0, + expected: true + }, + { + value1: null, + value2: null, + expected: true + }, + { + value1: 'test', + value2: 'test', + expected: true + }, + { + value1: true, + value2: true, + expected: true + }, + { + value1: 0, + value2: 1, + expected: false + }, + { + value1: null, + value2: false, + expected: false + }, + { + value1: 'test1', + value2: 'test2', + expected: false + }, + { + value1: true, + value2: false, + expected: false + }, + + // Simple object tests + { + value1: {}, + value2: {}, + expected: true + }, + { + value1: {}, + value2: [], + expected: false + }, + { + value1: [], + value2: [], + expected: true + }, + { + value1: {}, + value2: null, + expected: false + }, + + // Complex object tests + { + value1: [1], + value2: [], + expected: false + }, + { + value1: [1], + value2: [1], + expected: true + }, + { + value1: [1], + value2: [2], + expected: false + }, + + { + value1: {}, + value2: {test: 1}, + expected: false + }, + { + value1: {test: 1}, + value2: {test: 1}, + expected: true + }, + { + value1: {test: 1}, + value2: {test: {test2: false}}, + expected: false + }, + { + value1: {test: {test2: true}}, + value2: {test: {test2: false}}, + expected: false + }, + { + value1: {test: {test2: [true]}}, + value2: {test: {test2: [true]}}, + expected: true + }, + + // Recursive + { + value1: (() => { const x = {}; x.x = x; return x; })(), + value2: (() => { const x = {}; x.x = x; return x; })(), + expected: false + } + ]; + + let index = 0; + for (const {value1, value2, expected} of data) { + test(`${index}`, () => { + const actual1 = deepEqual(value1, value2); + expect(actual1).toStrictEqual(expected); + + const actual2 = deepEqual(value2, value1); + expect(actual2).toStrictEqual(expected); + }); + ++index; + } + }); +} + + +function main() { + testDynamicProperty(); + testDeepEqual(); +} + +main(); diff --git a/test/test-css-json.js b/test/css-json.test.js index ddeee6bd..0aaf7d10 100644 --- a/test/test-css-json.js +++ b/test/css-json.test.js @@ -16,22 +16,18 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -const fs = require('fs'); -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {formatRulesJson, generateRules} = require('../dev/css-to-json-util'); -const {getTargets} = require('../dev/generate-css-json'); - +import fs from 'fs'; +import {expect, test} from 'vitest'; +import {formatRulesJson, generateRules, getTargets} from '../dev/generate-css-json'; function main() { - for (const {cssFile, overridesCssFile, outputPath} of getTargets()) { - const actual = fs.readFileSync(outputPath, {encoding: 'utf8'}); - const expected = formatRulesJson(generateRules(cssFile, overridesCssFile)); - assert.deepStrictEqual(actual, expected); - } + test('css-json', () => { + for (const {cssFile, overridesCssFile, outputPath} of getTargets()) { + const actual = fs.readFileSync(outputPath, {encoding: 'utf8'}); + const expected = formatRulesJson(generateRules(cssFile, overridesCssFile)); + expect(actual).toStrictEqual(expected); + } + }); } - -if (require.main === module) { - testMain(main, process.argv.slice(2)); -} +main(); diff --git a/test/database.test.js b/test/database.test.js new file mode 100644 index 00000000..b53d0e65 --- /dev/null +++ b/test/database.test.js @@ -0,0 +1,853 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {IDBFactory, IDBKeyRange} from 'fake-indexeddb'; +import path from 'path'; +import {beforeEach, describe, expect, test, vi} from 'vitest'; +import {createDictionaryArchive} from '../dev/util.js'; +import {DictionaryDatabase} from '../ext/js/language/dictionary-database.js'; +import {DictionaryImporterMediaLoader} from '../ext/js/language/dictionary-importer-media-loader.js'; +import {DictionaryImporter} from '../ext/js/language/dictionary-importer.js'; + +vi.stubGlobal('IDBKeyRange', IDBKeyRange); + +vi.mock('../ext/js/language/dictionary-importer-media-loader.js'); + +function createTestDictionaryArchive(dictionary, dictionaryName) { + const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary); + return createDictionaryArchive(dictionaryDirectory, dictionaryName); +} + + +function createDictionaryImporter(onProgress) { + const dictionaryImporterMediaLoader = new DictionaryImporterMediaLoader(); + return new DictionaryImporter(dictionaryImporterMediaLoader, (...args) => { + const {stepIndex, stepCount, index, count} = args[0]; + expect(stepIndex < stepCount).toBe(true); + expect(index <= count).toBe(true); + if (typeof onProgress === 'function') { + onProgress(...args); + } + }); +} + + +function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) { + return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0); +} + +function countDictionaryDatabaseEntriesWithReading(dictionaryDatabaseEntries, reading) { + return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0); +} + +function countMetasWithMode(metas, mode) { + return metas.reduce((i, v) => (i + (v.mode === mode ? 1 : 0)), 0); +} + +function countKanjiWithCharacter(kanji, character) { + return kanji.reduce((i, v) => (i + (v.character === character ? 1 : 0)), 0); +} + + + +async function testDatabase1() { + test('Database1', async () => { // Load dictionary data + const testDictionary = createTestDictionaryArchive('valid-dictionary1'); + const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); + const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); + + const title = testDictionaryIndex.title; + const titles = new Map([ + [title, {priority: 0, allowSecondarySearches: false}] + ]); + + // Setup iteration data + const iterations = [ + { + cleanup: async () => { + // Test purge + await dictionaryDatabase.purge(); + await testDatabaseEmpty1(dictionaryDatabase); + } + }, + { + cleanup: async () => { + // Test deleteDictionary + let progressEvent = false; + await dictionaryDatabase.deleteDictionary( + title, + 1000, + () => { + progressEvent = true; + } + ); + expect(progressEvent).toBe(true); + + await testDatabaseEmpty1(dictionaryDatabase); + } + }, + { + cleanup: async () => {} + } + ]; + + // Setup database + const dictionaryDatabase = new DictionaryDatabase(); + await dictionaryDatabase.prepare(); + + for (const {cleanup} of iterations) { + const expectedSummary = { + title, + revision: 'test', + sequenced: true, + version: 3, + importDate: 0, + prefixWildcardsSupported: true, + counts: { + kanji: {total: 2}, + kanjiMeta: {total: 6, freq: 6}, + media: {total: 4}, + tagMeta: {total: 15}, + termMeta: {total: 38, freq: 31, pitch: 7}, + terms: {total: 21} + } + }; + + // Import data + let progressEvent = false; + const dictionaryImporter = createDictionaryImporter(() => { progressEvent = true; }); + const {result, errors} = await dictionaryImporter.importDictionary( + dictionaryDatabase, + testDictionarySource, + {prefixWildcardsSupported: true} + ); + expectedSummary.importDate = result.importDate; + expect(errors).toStrictEqual([]); + expect(result).toStrictEqual(expectedSummary); + expect(progressEvent).toBe(true); + + // Get info summary + const info = await dictionaryDatabase.getDictionaryInfo(); + expect(info).toStrictEqual([expectedSummary]); + + // Get counts + const counts = await dictionaryDatabase.getDictionaryCounts( + info.map((v) => v.title), + true + ); + expect(counts).toStrictEqual({ + counts: [{kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}], + total: {kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4} + }); + + // Test find* functions + await testFindTermsBulkTest1(dictionaryDatabase, titles); + await testTindTermsExactBulk1(dictionaryDatabase, titles); + await testFindTermsBySequenceBulk1(dictionaryDatabase, title); + await testFindTermMetaBulk1(dictionaryDatabase, titles); + await testFindKanjiBulk1(dictionaryDatabase, titles); + await testFindKanjiMetaBulk1(dictionaryDatabase, titles); + await testFindTagForTitle1(dictionaryDatabase, title); + + // Cleanup + await cleanup(); + } + + await dictionaryDatabase.close(); + }); +} + +async function testDatabaseEmpty1(database) { + test('DatabaseEmpty1', async () => { + const info = await database.getDictionaryInfo(); + expect(info).toStrictEqual([]); + + const counts = await database.getDictionaryCounts([], true); + expect(counts).toStrictEqual({ + counts: [], + total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0, media: 0} + }); + }); +} + +async function testFindTermsBulkTest1(database, titles) { + test('FindTermsBulkTest1', async () => { + const data = [ + { + inputs: [ + { + matchType: null, + termList: ['打', '打つ', '打ち込む'] + }, + { + matchType: null, + termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ'] + }, + { + matchType: 'prefix', + termList: ['打'] + } + ], + expectedResults: { + total: 10, + terms: [ + ['打', 2], + ['打つ', 4], + ['打ち込む', 4] + ], + readings: [ + ['だ', 1], + ['ダース', 1], + ['うつ', 2], + ['ぶつ', 2], + ['うちこむ', 2], + ['ぶちこむ', 2] + ] + } + }, + { + inputs: [ + { + matchType: null, + termList: ['込む'] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + matchType: 'suffix', + termList: ['込む'] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打ち込む', 4] + ], + readings: [ + ['うちこむ', 2], + ['ぶちこむ', 2] + ] + } + }, + { + inputs: [ + { + matchType: null, + termList: [] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {termList, matchType} of inputs) { + const results = await database.findTermsBulk(termList, titles, matchType); + expect(results.length).toStrictEqual(expectedResults.total); + for (const [term, count] of expectedResults.terms) { + expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count); + } + for (const [reading, count] of expectedResults.readings) { + expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count); + } + } + } + }); +} + +async function testTindTermsExactBulk1(database, titles) { + test('TindTermsExactBulk1', async () => { + const data = [ + { + inputs: [ + { + termList: [ + {term: '打', reading: 'だ'}, + {term: '打つ', reading: 'うつ'}, + {term: '打ち込む', reading: 'うちこむ'} + ] + } + ], + expectedResults: { + total: 5, + terms: [ + ['打', 1], + ['打つ', 2], + ['打ち込む', 2] + ], + readings: [ + ['だ', 1], + ['うつ', 2], + ['うちこむ', 2] + ] + } + }, + { + inputs: [ + { + termList: [ + {term: '打', reading: 'だ?'}, + {term: '打つ', reading: 'うつ?'}, + {term: '打ち込む', reading: 'うちこむ?'} + ] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + termList: [ + {term: '打つ', reading: 'うつ'}, + {term: '打つ', reading: 'ぶつ'} + ] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打つ', 4] + ], + readings: [ + ['うつ', 2], + ['ぶつ', 2] + ] + } + }, + { + inputs: [ + { + termList: [ + {term: '打つ', reading: 'うちこむ'} + ] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + termList: [] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {termList} of inputs) { + const results = await database.findTermsExactBulk(termList, titles); + expect(results.length).toStrictEqual(expectedResults.total); + for (const [term, count] of expectedResults.terms) { + expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count); + } + for (const [reading, count] of expectedResults.readings) { + expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count); + } + } + } + }); +} + +async function testFindTermsBySequenceBulk1(database, mainDictionary) { + test('FindTermsBySequenceBulk1', async () => { + const data = [ + { + inputs: [ + { + sequenceList: [1, 2, 3, 4, 5] + } + ], + expectedResults: { + total: 11, + terms: [ + ['打', 2], + ['打つ', 4], + ['打ち込む', 4], + ['画像', 1] + ], + readings: [ + ['だ', 1], + ['ダース', 1], + ['うつ', 2], + ['ぶつ', 2], + ['うちこむ', 2], + ['ぶちこむ', 2], + ['がぞう', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [1] + } + ], + expectedResults: { + total: 1, + terms: [ + ['打', 1] + ], + readings: [ + ['だ', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [2] + } + ], + expectedResults: { + total: 1, + terms: [ + ['打', 1] + ], + readings: [ + ['ダース', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [3] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打つ', 4] + ], + readings: [ + ['うつ', 2], + ['ぶつ', 2] + ] + } + }, + { + inputs: [ + { + sequenceList: [4] + } + ], + expectedResults: { + total: 4, + terms: [ + ['打ち込む', 4] + ], + readings: [ + ['うちこむ', 2], + ['ぶちこむ', 2] + ] + } + }, + { + inputs: [ + { + sequenceList: [5] + } + ], + expectedResults: { + total: 1, + terms: [ + ['画像', 1] + ], + readings: [ + ['がぞう', 1] + ] + } + }, + { + inputs: [ + { + sequenceList: [-1] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + }, + { + inputs: [ + { + sequenceList: [] + } + ], + expectedResults: { + total: 0, + terms: [], + readings: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {sequenceList} of inputs) { + const results = await database.findTermsBySequenceBulk(sequenceList.map((query) => ({query, dictionary: mainDictionary}))); + expect(results.length).toStrictEqual(expectedResults.total); + for (const [term, count] of expectedResults.terms) { + expect(countDictionaryDatabaseEntriesWithTerm(results, term)).toStrictEqual(count); + } + for (const [reading, count] of expectedResults.readings) { + expect(countDictionaryDatabaseEntriesWithReading(results, reading)).toStrictEqual(count); + } + } + } + }); +} + +async function testFindTermMetaBulk1(database, titles) { + test('FindTermMetaBulk1', async () => { + const data = [ + { + inputs: [ + { + termList: ['打'] + } + ], + expectedResults: { + total: 11, + modes: [ + ['freq', 11] + ] + } + }, + { + inputs: [ + { + termList: ['打つ'] + } + ], + expectedResults: { + total: 10, + modes: [ + ['freq', 10] + ] + } + }, + { + inputs: [ + { + termList: ['打ち込む'] + } + ], + expectedResults: { + total: 12, + modes: [ + ['freq', 10], + ['pitch', 2] + ] + } + }, + { + inputs: [ + { + termList: ['?'] + } + ], + expectedResults: { + total: 0, + modes: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {termList} of inputs) { + const results = await database.findTermMetaBulk(termList, titles); + expect(results.length).toStrictEqual(expectedResults.total); + for (const [mode, count] of expectedResults.modes) { + expect(countMetasWithMode(results, mode)).toStrictEqual(count); + } + } + } + }); +} + +async function testFindKanjiBulk1(database, titles) { + test('FindKanjiBulk1', async () => { + const data = [ + { + inputs: [ + { + kanjiList: ['打'] + } + ], + expectedResults: { + total: 1, + kanji: [ + ['打', 1] + ] + } + }, + { + inputs: [ + { + kanjiList: ['込'] + } + ], + expectedResults: { + total: 1, + kanji: [ + ['込', 1] + ] + } + }, + { + inputs: [ + { + kanjiList: ['?'] + } + ], + expectedResults: { + total: 0, + kanji: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {kanjiList} of inputs) { + const results = await database.findKanjiBulk(kanjiList, titles); + expect(results.length).toStrictEqual(expectedResults.total); + for (const [kanji, count] of expectedResults.kanji) { + expect(countKanjiWithCharacter(results, kanji)).toStrictEqual(count); + } + } + } + }); +} + +async function testFindKanjiMetaBulk1(database, titles) { + test('FindKanjiMetaBulk1', async () => { + const data = [ + { + inputs: [ + { + kanjiList: ['打'] + } + ], + expectedResults: { + total: 3, + modes: [ + ['freq', 3] + ] + } + }, + { + inputs: [ + { + kanjiList: ['込'] + } + ], + expectedResults: { + total: 3, + modes: [ + ['freq', 3] + ] + } + }, + { + inputs: [ + { + kanjiList: ['?'] + } + ], + expectedResults: { + total: 0, + modes: [] + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {kanjiList} of inputs) { + const results = await database.findKanjiMetaBulk(kanjiList, titles); + expect(results.length).toStrictEqual(expectedResults.total); + for (const [mode, count] of expectedResults.modes) { + expect(countMetasWithMode(results, mode)).toStrictEqual(count); + } + } + } + }); +} + +async function testFindTagForTitle1(database, title) { + test('FindTagForTitle1', async () => { + const data = [ + { + inputs: [ + { + name: 'E1' + } + ], + expectedResults: { + value: {category: 'default', dictionary: title, name: 'E1', notes: 'example tag 1', order: 0, score: 0} + } + }, + { + inputs: [ + { + name: 'K1' + } + ], + expectedResults: { + value: {category: 'default', dictionary: title, name: 'K1', notes: 'example kanji tag 1', order: 0, score: 0} + } + }, + { + inputs: [ + { + name: 'kstat1' + } + ], + expectedResults: { + value: {category: 'class', dictionary: title, name: 'kstat1', notes: 'kanji stat 1', order: 0, score: 0} + } + }, + { + inputs: [ + { + name: 'invalid' + } + ], + expectedResults: { + value: null + } + } + ]; + + for (const {inputs, expectedResults} of data) { + for (const {name} of inputs) { + const result = await database.findTagForTitle(name, title); + expect(result).toStrictEqual(expectedResults.value); + } + } + }); +} + + +async function testDatabase2() { + test('Database2', async () => { // Load dictionary data + const testDictionary = createTestDictionaryArchive('valid-dictionary1'); + const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); + const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); + + const title = testDictionaryIndex.title; + const titles = new Map([ + [title, {priority: 0, allowSecondarySearches: false}] + ]); + + // Setup database + const dictionaryDatabase = new DictionaryDatabase(); + + // Database not open + await expect(dictionaryDatabase.deleteDictionary(title, 1000)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findTermsBulk(['?'], titles, null)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findTermsExactBulk([{term: '?', reading: '?'}], titles)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findTermsBySequenceBulk([{query: 1, dictionary: title}])).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findTermMetaBulk(['?'], titles)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findTermMetaBulk(['?'], titles)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findKanjiBulk(['?'], titles)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findKanjiMetaBulk(['?'], titles)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.findTagForTitle('tag', title)).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.getDictionaryInfo()).rejects.toThrow('Database not open'); + await expect(dictionaryDatabase.getDictionaryCounts(titles, true)).rejects.toThrow('Database not open'); + await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Database is not ready'); + + await dictionaryDatabase.prepare(); + + // already prepared + await expect(dictionaryDatabase.prepare()).rejects.toThrow('Database already open'); + + await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {}); + + // dictionary already imported + await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Dictionary is already imported'); + + await dictionaryDatabase.close(); + }); +} + + +async function testDatabase3() { + const invalidDictionaries = [ + 'invalid-dictionary1', + 'invalid-dictionary2', + 'invalid-dictionary3', + 'invalid-dictionary4', + 'invalid-dictionary5', + 'invalid-dictionary6' + ]; + + + describe('Database3', () => { + for (const invalidDictionary of invalidDictionaries) { + test(`${invalidDictionary}`, async () => { + // Setup database + const dictionaryDatabase = new DictionaryDatabase(); + await dictionaryDatabase.prepare(); + + const testDictionary = createTestDictionaryArchive(invalidDictionary); + const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); + + await expect(createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})).rejects.toThrow('Dictionary has invalid data'); + await dictionaryDatabase.close(); + }); + } + }); +} + + +async function main() { + beforeEach(async () => { + globalThis.indexedDB = new IDBFactory(); + }); + await testDatabase1(); + await testDatabase2(); + await testDatabase3(); +} + +await main(); diff --git a/test/test-deinflector.js b/test/deinflector.test.js index a20cfc95..edb85833 100644 --- a/test/test-deinflector.js +++ b/test/deinflector.test.js @@ -16,14 +16,12 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); +import fs from 'fs'; +import path from 'path'; +import {describe, expect, test} from 'vitest'; +import {Deinflector} from '../ext/js/language/deinflector.js'; - -function hasTermReasons(Deinflector, deinflector, source, expectedTerm, expectedRule, expectedReasons) { +function hasTermReasons(deinflector, source, expectedTerm, expectedRule, expectedReasons) { for (const {term, reasons, rules} of deinflector.deinflect(source, source)) { if (term !== expectedTerm) { continue; } if (typeof expectedRule !== 'undefined') { @@ -917,30 +915,27 @@ function testDeinflections() { } ]; - const vm = new VM(); - vm.execute(['js/language/deinflector.js']); - const [Deinflector] = vm.get(['Deinflector']); - const deinflectionReasons = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/deinflect.json'))); const deinflector = new Deinflector(deinflectionReasons); - for (const {valid, tests} of data) { - for (const {source, term, rule, reasons} of tests) { - const {has, reasons: actualReasons} = hasTermReasons(Deinflector, deinflector, source, term, rule, reasons); - let message = `${source} ${valid ? 'does not have' : 'has'} term candidate ${JSON.stringify(term)}`; - if (typeof rule !== 'undefined') { - message += ` with rule ${JSON.stringify(rule)}`; - } - if (typeof reasons !== 'undefined') { - message += (typeof rule !== 'undefined' ? ' and' : ' with'); - message += ` reasons ${JSON.stringify(reasons)}`; - } - if (actualReasons !== null) { - message += ` (actual reasons: ${JSON.stringify(actualReasons)})`; + describe('deinflections', () => { + for (const {valid, tests} of data) { + for (const {source, term, rule, reasons} of tests) { + const {has} = hasTermReasons(deinflector, source, term, rule, reasons); + let message = `${source} ${valid ? 'has' : 'does not have'} term candidate ${JSON.stringify(term)}`; + if (typeof rule !== 'undefined') { + message += ` with rule ${JSON.stringify(rule)}`; + } + if (typeof reasons !== 'undefined') { + message += (typeof rule !== 'undefined' ? ' and' : ' with'); + message += ` reasons ${JSON.stringify(reasons)}`; + } + test(`${message}`, () => { + expect(has).toStrictEqual(valid); + }); } - assert.strictEqual(has, valid, message); } - } + }); } @@ -949,4 +944,4 @@ function main() { } -if (require.main === module) { testMain(main); } +main(); diff --git a/test/test-dictionary.js b/test/dictionary.test.js index d4390e19..8f160bc1 100644 --- a/test/test-dictionary.js +++ b/test/dictionary.test.js @@ -16,13 +16,16 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -const path = require('path'); -const {createDictionaryArchive, testMain} = require('../dev/util'); -const dictionaryValidate = require('../dev/dictionary-validate'); +import {fileURLToPath} from 'node:url'; +import path from 'path'; +import {expect, test} from 'vitest'; +import * as dictionaryValidate from '../dev/dictionary-validate.js'; +import {createDictionaryArchive} from '../dev/util.js'; +const dirname = path.dirname(fileURLToPath(import.meta.url)); function createTestDictionaryArchive(dictionary, dictionaryName) { - const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary); + const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', dictionary); return createDictionaryArchive(dictionaryDirectory, dictionaryName); } @@ -41,26 +44,16 @@ async function main() { const schemas = dictionaryValidate.getSchemas(); for (const {name, valid} of dictionaries) { - const archive = createTestDictionaryArchive(name); + test(`${name} is ${valid ? 'valid' : 'invalid'}`, async () => { + const archive = createTestDictionaryArchive(name); - let error = null; - try { - await dictionaryValidate.validateDictionary(null, archive, schemas); - } catch (e) { - error = e; - } - - if (valid) { - if (error !== null) { - throw error; - } - } else { - if (error === null) { - throw new Error(`Expected dictionary ${name} to be invalid`); + if (valid) { + await expect(dictionaryValidate.validateDictionary(null, archive, schemas)).resolves.not.toThrow(); + } else { + await expect(dictionaryValidate.validateDictionary(null, archive, schemas)).rejects.toThrow(); } - } + }); } } - -if (require.main === module) { testMain(main); } +await main(); diff --git a/test/document-util.test.js b/test/document-util.test.js new file mode 100644 index 00000000..f2552f78 --- /dev/null +++ b/test/document-util.test.js @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import fs from 'fs'; +import {JSDOM} from 'jsdom'; +import {fileURLToPath} from 'node:url'; +import path from 'path'; +import {expect, test} from 'vitest'; +import {DocumentUtil} from '../ext/js/dom/document-util.js'; +import {DOMTextScanner} from '../ext/js/dom/dom-text-scanner.js'; +import {TextSourceElement} from '../ext/js/dom/text-source-element.js'; +import {TextSourceRange} from '../ext/js/dom/text-source-range.js'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +// DOMRect class definition +class DOMRect { + constructor(x, y, width, height) { + this._x = x; + this._y = y; + this._width = width; + this._height = height; + } + + get x() { return this._x; } + get y() { return this._y; } + get width() { return this._width; } + get height() { return this._height; } + get left() { return this._x + Math.min(0, this._width); } + get right() { return this._x + Math.max(0, this._width); } + get top() { return this._y + Math.min(0, this._height); } + get bottom() { return this._y + Math.max(0, this._height); } +} + + +function createJSDOM(fileName) { + const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); + const dom = new JSDOM(domSource); + const document = dom.window.document; + const window = dom.window; + + // Define innerText setter as an alias for textContent setter + Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', { + set(value) { this.textContent = value; } + }); + + // Placeholder for feature detection + document.caretRangeFromPoint = () => null; + + return dom; +} + +function querySelectorChildOrSelf(element, selector) { + return selector ? element.querySelector(selector) : element; +} + +function getChildTextNodeOrSelf(dom, node) { + if (node === null) { return null; } + const Node = dom.window.Node; + const childNode = node.firstChild; + return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node); +} + +function getPrototypeOfOrNull(value) { + try { + return Object.getPrototypeOf(value); + } catch (e) { + return null; + } +} + +function findImposterElement(document) { + // Finds the imposter element based on it's z-index style + return document.querySelector('div[style*="2147483646"]>*'); +} + + +async function testDocument1() { + const dom = createJSDOM(path.join(dirname, 'data', 'html', 'test-document1.html')); + const window = dom.window; + + try { + await testDocumentTextScanningFunctions(dom); + await testTextSourceRangeSeekFunctions(dom); + } finally { + window.close(); + } +} + +async function testDocumentTextScanningFunctions(dom) { + const document = dom.window.document; + + test('DocumentTextScanningFunctions', () => { + for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) { + // Get test parameters + let { + elementFromPointSelector, + caretRangeFromPointSelector, + startNodeSelector, + startOffset, + endNodeSelector, + endOffset, + resultType, + sentenceScanExtent, + sentence, + hasImposter, + terminateAtNewlines + } = testElement.dataset; + + const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector); + const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector); + const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector)); + const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector)); + + startOffset = parseInt(startOffset, 10); + endOffset = parseInt(endOffset, 10); + sentenceScanExtent = parseInt(sentenceScanExtent, 10); + terminateAtNewlines = (terminateAtNewlines !== 'false'); + + expect(elementFromPointValue).not.toStrictEqual(null); + expect(caretRangeFromPointValue).not.toStrictEqual(null); + expect(startNode).not.toStrictEqual(null); + expect(endNode).not.toStrictEqual(null); + + // Setup functions + document.elementFromPoint = () => elementFromPointValue; + + document.caretRangeFromPoint = (x, y) => { + const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document)); + expect(!!imposter).toStrictEqual(hasImposter === 'true'); + + const range = document.createRange(); + range.setStart(imposter ? imposter : startNode, startOffset); + range.setEnd(imposter ? imposter : startNode, endOffset); + + // Override getClientRects to return a rect guaranteed to contain (x, y) + range.getClientRects = () => [new DOMRect(x - 1, y - 1, 2, 2)]; + return range; + }; + + // Test docRangeFromPoint + const source = DocumentUtil.getRangeFromPoint(0, 0, { + deepContentScan: false, + normalizeCssZoom: true + }); + switch (resultType) { + case 'TextSourceRange': + expect(getPrototypeOfOrNull(source)).toStrictEqual(TextSourceRange.prototype); + break; + case 'TextSourceElement': + expect(getPrototypeOfOrNull(source)).toStrictEqual(TextSourceElement.prototype); + break; + case 'null': + expect(source).toStrictEqual(null); + break; + default: + expect.unreachable(); + break; + } + if (source === null) { continue; } + + // Sentence info + const terminatorString = '…。..??!!'; + const terminatorMap = new Map(); + for (const char of terminatorString) { + terminatorMap.set(char, [false, true]); + } + const quoteArray = [['「', '」'], ['『', '』'], ['\'', '\''], ['"', '"']]; + const forwardQuoteMap = new Map(); + const backwardQuoteMap = new Map(); + for (const [char1, char2] of quoteArray) { + forwardQuoteMap.set(char1, [char2, false]); + backwardQuoteMap.set(char2, [char1, false]); + } + + // Test docSentenceExtract + const sentenceActual = DocumentUtil.extractSentence( + source, + false, + sentenceScanExtent, + terminateAtNewlines, + terminatorMap, + forwardQuoteMap, + backwardQuoteMap + ).text; + expect(sentenceActual).toStrictEqual(sentence); + + // Clean + source.cleanup(); + } + }); +} + +async function testTextSourceRangeSeekFunctions(dom) { + const document = dom.window.document; + + test('TextSourceRangeSeekFunctions', async () => { + for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) { + // Get test parameters + let { + seekNodeSelector, + seekNodeIsText, + seekOffset, + seekLength, + seekDirection, + expectedResultNodeSelector, + expectedResultNodeIsText, + expectedResultOffset, + expectedResultContent + } = testElement.dataset; + + seekOffset = parseInt(seekOffset, 10); + seekLength = parseInt(seekLength, 10); + expectedResultOffset = parseInt(expectedResultOffset, 10); + + let seekNode = testElement.querySelector(seekNodeSelector); + if (seekNodeIsText === 'true') { + seekNode = seekNode.firstChild; + } + + let expectedResultNode = testElement.querySelector(expectedResultNodeSelector); + if (expectedResultNodeIsText === 'true') { + expectedResultNode = expectedResultNode.firstChild; + } + + const {node, offset, content} = ( + seekDirection === 'forward' ? + new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) : + new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength) + ); + + expect(node).toStrictEqual(expectedResultNode); + expect(offset).toStrictEqual(expectedResultOffset); + expect(content).toStrictEqual(expectedResultContent); + } + }); +} + + +async function main() { + await testDocument1(); +} + +await main(); diff --git a/test/test-dom-text-scanner.js b/test/dom-text-scanner.test.js index 37017b01..d1b31276 100644 --- a/test/test-dom-text-scanner.js +++ b/test/dom-text-scanner.test.js @@ -16,13 +16,11 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const {JSDOM} = require('jsdom'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - +import fs from 'fs'; +import {JSDOM} from 'jsdom'; +import path from 'path'; +import {expect, test} from 'vitest'; +import {DOMTextScanner} from '../ext/js/dom/dom-text-scanner.js'; function createJSDOM(fileName) { const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); @@ -87,74 +85,77 @@ function createAbsoluteGetComputedStyle(window) { } -async function testDomTextScanner(dom, {DOMTextScanner}) { +async function testDomTextScanner(dom) { const document = dom.window.document; - for (const testElement of document.querySelectorAll('y-test')) { - let testData = JSON.parse(testElement.dataset.testData); - if (!Array.isArray(testData)) { - testData = [testData]; - } - for (const testDataItem of testData) { - let { - node, - offset, - length, - forcePreserveWhitespace, - generateLayoutContent, - reversible, - expected: { - node: expectedNode, - offset: expectedOffset, - content: expectedContent, - remainder: expectedRemainder - } - } = testDataItem; - - node = querySelectorTextNode(testElement, node); - expectedNode = querySelectorTextNode(testElement, expectedNode); - // Standard test - { - const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); - scanner.seek(length); - - const {node: actualNode1, offset: actualOffset1, content: actualContent1, remainder: actualRemainder1} = scanner; - assert.strictEqual(actualContent1, expectedContent); - assert.strictEqual(actualOffset1, expectedOffset); - assert.strictEqual(actualNode1, expectedNode); - assert.strictEqual(actualRemainder1, expectedRemainder || 0); + test('DomTextScanner', () => { + for (const testElement of document.querySelectorAll('y-test')) { + let testData = JSON.parse(testElement.dataset.testData); + if (!Array.isArray(testData)) { + testData = [testData]; } + for (const testDataItem of testData) { + let { + node, + offset, + length, + forcePreserveWhitespace, + generateLayoutContent, + reversible, + expected: { + node: expectedNode, + offset: expectedOffset, + content: expectedContent, + remainder: expectedRemainder + } + } = testDataItem; + + node = querySelectorTextNode(testElement, node); + expectedNode = querySelectorTextNode(testElement, expectedNode); + + // Standard test + { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length); + + const {node: actualNode1, offset: actualOffset1, content: actualContent1, remainder: actualRemainder1} = scanner; + expect(actualContent1).toStrictEqual(expectedContent); + expect(actualOffset1).toStrictEqual(expectedOffset); + expect(actualNode1).toStrictEqual(expectedNode); + expect(actualRemainder1).toStrictEqual(expectedRemainder || 0); + } - // Substring tests - for (let i = 1; i <= length; ++i) { - const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); - scanner.seek(length - i); + // Substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(node, offset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(length - i); - const {content: actualContent} = scanner; - assert.strictEqual(actualContent, expectedContent.substring(0, expectedContent.length - i)); - } + const {content: actualContent} = scanner; + expect(actualContent).toStrictEqual(expectedContent.substring(0, expectedContent.length - i)); + } - if (reversible === false) { continue; } + if (reversible === false) { continue; } - // Reversed test - { - const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); - scanner.seek(-length); + // Reversed test + { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-length); - const {content: actualContent} = scanner; - assert.strictEqual(actualContent, expectedContent); - } + const {content: actualContent} = scanner; + expect(actualContent).toStrictEqual(expectedContent); + } - // Reversed substring tests - for (let i = 1; i <= length; ++i) { - const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); - scanner.seek(-(length - i)); + // Reversed substring tests + for (let i = 1; i <= length; ++i) { + const scanner = new DOMTextScanner(expectedNode, expectedOffset, forcePreserveWhitespace, generateLayoutContent); + scanner.seek(-(length - i)); - const {content: actualContent} = scanner; - assert.strictEqual(actualContent, expectedContent.substring(i)); + const {content: actualContent} = scanner; + expect(actualContent).toStrictEqual(expectedContent.substring(i)); + } } } - } + }); } @@ -162,17 +163,8 @@ async function testDocument1() { const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-dom-text-scanner.html')); const window = dom.window; try { - const {document, Node, Range} = window; - window.getComputedStyle = createAbsoluteGetComputedStyle(window); - const vm = new VM({document, window, Range, Node}); - vm.execute([ - 'js/data/sandbox/string-util.js', - 'js/dom/dom-text-scanner.js' - ]); - const DOMTextScanner = vm.get('DOMTextScanner'); - await testDomTextScanner(dom, {DOMTextScanner}); } finally { window.close(); @@ -184,5 +176,4 @@ async function main() { await testDocument1(); } - -if (require.main === module) { testMain(main); } +await main(); diff --git a/test/hotkey-util.test.js b/test/hotkey-util.test.js new file mode 100644 index 00000000..8666b98b --- /dev/null +++ b/test/hotkey-util.test.js @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {expect, test} from 'vitest'; +import {HotkeyUtil} from '../ext/js/input/hotkey-util.js'; + +function testCommandConversions() { + test('CommandConversions', () => { + const data = [ + {os: 'win', command: 'Alt+F', expectedCommand: 'Alt+F', expectedInput: {key: 'KeyF', modifiers: ['alt']}}, + {os: 'win', command: 'F1', expectedCommand: 'F1', expectedInput: {key: 'F1', modifiers: []}}, + + {os: 'win', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'win', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'win', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, + + {os: 'mac', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, + {os: 'mac', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'MacCtrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'mac', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, + + {os: 'linux', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'linux', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, + {os: 'linux', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}} + ]; + + const hotkeyUtil = new HotkeyUtil(); + for (const {command, os, expectedInput, expectedCommand} of data) { + hotkeyUtil.os = os; + const input = structuredClone(hotkeyUtil.convertCommandToInput(command)); + expect(input).toStrictEqual(expectedInput); + const command2 = hotkeyUtil.convertInputToCommand(input.key, input.modifiers); + expect(command2).toStrictEqual(expectedCommand); + } + }); +} + +function testDisplayNames() { + test('DisplayNames', () => { + const data = [ + {os: 'win', key: null, modifiers: [], expected: ''}, + {os: 'win', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'win', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'win', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'win', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'win', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'win', key: null, modifiers: ['alt'], expected: 'Alt'}, + {os: 'win', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, + {os: 'win', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, + {os: 'win', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'win', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'win', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'win', key: null, modifiers: ['meta'], expected: 'Windows'}, + {os: 'win', key: 'KeyF', modifiers: ['meta'], expected: 'Windows + F'}, + {os: 'win', key: 'F1', modifiers: ['meta'], expected: 'Windows + F1'}, + {os: 'win', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'win', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'win', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, + + {os: 'mac', key: null, modifiers: [], expected: ''}, + {os: 'mac', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'mac', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'mac', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'mac', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'mac', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'mac', key: null, modifiers: ['alt'], expected: 'Opt'}, + {os: 'mac', key: 'KeyF', modifiers: ['alt'], expected: 'Opt + F'}, + {os: 'mac', key: 'F1', modifiers: ['alt'], expected: 'Opt + F1'}, + {os: 'mac', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'mac', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'mac', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'mac', key: null, modifiers: ['meta'], expected: 'Cmd'}, + {os: 'mac', key: 'KeyF', modifiers: ['meta'], expected: 'Cmd + F'}, + {os: 'mac', key: 'F1', modifiers: ['meta'], expected: 'Cmd + F1'}, + {os: 'mac', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'mac', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'mac', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, + + {os: 'linux', key: null, modifiers: [], expected: ''}, + {os: 'linux', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'linux', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'linux', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'linux', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'linux', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'linux', key: null, modifiers: ['alt'], expected: 'Alt'}, + {os: 'linux', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, + {os: 'linux', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, + {os: 'linux', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'linux', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'linux', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'linux', key: null, modifiers: ['meta'], expected: 'Super'}, + {os: 'linux', key: 'KeyF', modifiers: ['meta'], expected: 'Super + F'}, + {os: 'linux', key: 'F1', modifiers: ['meta'], expected: 'Super + F1'}, + {os: 'linux', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'linux', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'linux', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, + + {os: 'unknown', key: null, modifiers: [], expected: ''}, + {os: 'unknown', key: 'KeyF', modifiers: [], expected: 'F'}, + {os: 'unknown', key: 'F1', modifiers: [], expected: 'F1'}, + {os: 'unknown', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, + {os: 'unknown', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, + {os: 'unknown', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, + {os: 'unknown', key: null, modifiers: ['alt'], expected: 'Alt'}, + {os: 'unknown', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, + {os: 'unknown', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, + {os: 'unknown', key: null, modifiers: ['shift'], expected: 'Shift'}, + {os: 'unknown', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, + {os: 'unknown', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, + {os: 'unknown', key: null, modifiers: ['meta'], expected: 'Meta'}, + {os: 'unknown', key: 'KeyF', modifiers: ['meta'], expected: 'Meta + F'}, + {os: 'unknown', key: 'F1', modifiers: ['meta'], expected: 'Meta + F1'}, + {os: 'unknown', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, + {os: 'unknown', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, + {os: 'unknown', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'} + ]; + + const hotkeyUtil = new HotkeyUtil(); + for (const {os, key, modifiers, expected} of data) { + hotkeyUtil.os = os; + const displayName = hotkeyUtil.getInputDisplayValue(key, modifiers); + expect(displayName).toStrictEqual(expected); + } + }); +} + +function testSortModifiers() { + test('SortModifiers', () => { + const data = [ + {modifiers: [], expected: []}, + {modifiers: ['shift', 'alt', 'ctrl', 'mouse4', 'meta', 'mouse1', 'mouse0'], expected: ['meta', 'ctrl', 'alt', 'shift', 'mouse0', 'mouse1', 'mouse4']} + ]; + + const hotkeyUtil = new HotkeyUtil(); + for (const {modifiers, expected} of data) { + const modifiers2 = hotkeyUtil.sortModifiers(modifiers); + expect(modifiers2).toStrictEqual(modifiers); + expect(modifiers2).toStrictEqual(expected); + } + }); +} + + +function main() { + testCommandConversions(); + testDisplayNames(); + testSortModifiers(); +} + +main(); diff --git a/test/japanese-util.test.js b/test/japanese-util.test.js new file mode 100644 index 00000000..47da4ccb --- /dev/null +++ b/test/japanese-util.test.js @@ -0,0 +1,905 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {expect, test} from 'vitest'; +import {TextSourceMap} from '../ext/js/general/text-source-map.js'; +import {JapaneseUtil} from '../ext/js/language/sandbox/japanese-util.js'; +import * as wanakana from '../ext/lib/wanakana.js'; + +const jp = new JapaneseUtil(wanakana); + +function testIsCodePointKanji() { + test('isCodePointKanji', () => { + const data = [ + ['力方', true], + ['\u53f1\u{20b9f}', true], + ['かたカタ々kata、。?,.?', false], + ['逸逸', true] + ]; + + for (const [characters, expected] of data) { + for (const character of characters) { + const codePoint = character.codePointAt(0); + const actual = jp.isCodePointKanji(codePoint); + expect(actual).toStrictEqual(expected); // `isCodePointKanji failed for ${character} (\\u{${codePoint.toString(16)}})` + } + } + }); +} + +function testIsCodePointKana() { + test('isCodePointKana', () => { + const data = [ + ['かたカタ', true], + ['力方々kata、。?,.?', false], + ['\u53f1\u{20b9f}', false] + ]; + + for (const [characters, expected] of data) { + for (const character of characters) { + const codePoint = character.codePointAt(0); + const actual = jp.isCodePointKana(codePoint); + expect(actual).toStrictEqual(expected); // `isCodePointKana failed for ${character} (\\u{${codePoint.toString(16)}})` + } + } + }); +} + +function testIsCodePointJapanese() { + test('isCodePointJapanese', () => { + const data = [ + ['かたカタ力方々、。?', true], + ['\u53f1\u{20b9f}', true], + ['kata,.?', false], + ['逸逸', true] + ]; + + for (const [characters, expected] of data) { + for (const character of characters) { + const codePoint = character.codePointAt(0); + const actual = jp.isCodePointJapanese(codePoint); + expect(actual).toStrictEqual(expected); // `isCodePointJapanese failed for ${character} (\\u{${codePoint.toString(16)}})` + } + } + }); +} + +function testIsStringEntirelyKana() { + test('isStringEntirelyKana', () => { + const data = [ + ['かたかな', true], + ['カタカナ', true], + ['ひらがな', true], + ['ヒラガナ', true], + ['カタカナひらがな', true], + ['かたカタ力方々、。?', false], + ['\u53f1\u{20b9f}', false], + ['kata,.?', false], + ['かたカタ力方々、。?invalid', false], + ['\u53f1\u{20b9f}invalid', false], + ['kata,.?かた', false] + ]; + + for (const [string, expected] of data) { + expect(jp.isStringEntirelyKana(string)).toStrictEqual(expected); + } + }); +} + +function testIsStringPartiallyJapanese() { + test('isStringPartiallyJapanese', () => { + const data = [ + ['かたかな', true], + ['カタカナ', true], + ['ひらがな', true], + ['ヒラガナ', true], + ['カタカナひらがな', true], + ['かたカタ力方々、。?', true], + ['\u53f1\u{20b9f}', true], + ['kata,.?', false], + ['かたカタ力方々、。?invalid', true], + ['\u53f1\u{20b9f}invalid', true], + ['kata,.?かた', true], + ['逸逸', true] + ]; + + for (const [string, expected] of data) { + expect(jp.isStringPartiallyJapanese(string)).toStrictEqual(expected); + } + }); +} + +function testConvertKatakanaToHiragana() { + test('convertKatakanaToHiragana', () => { + const data = [ + ['かたかな', 'かたかな'], + ['ひらがな', 'ひらがな'], + ['カタカナ', 'かたかな'], + ['ヒラガナ', 'ひらがな'], + ['カタカナかたかな', 'かたかなかたかな'], + ['ヒラガナひらがな', 'ひらがなひらがな'], + ['chikaraちからチカラ力', 'chikaraちからちから力'], + ['katakana', 'katakana'], + ['hiragana', 'hiragana'], + ['カーナー', 'かあなあ'], + ['カーナー', 'かーなー', true] + ]; + + for (const [string, expected, keepProlongedSoundMarks=false] of data) { + expect(jp.convertKatakanaToHiragana(string, keepProlongedSoundMarks)).toStrictEqual(expected); + } + }); +} + +function testConvertHiraganaToKatakana() { + test('ConvertHiraganaToKatakana', () => { + const data = [ + ['かたかな', 'カタカナ'], + ['ひらがな', 'ヒラガナ'], + ['カタカナ', 'カタカナ'], + ['ヒラガナ', 'ヒラガナ'], + ['カタカナかたかな', 'カタカナカタカナ'], + ['ヒラガナひらがな', 'ヒラガナヒラガナ'], + ['chikaraちからチカラ力', 'chikaraチカラチカラ力'], + ['katakana', 'katakana'], + ['hiragana', 'hiragana'] + ]; + + for (const [string, expected] of data) { + expect(jp.convertHiraganaToKatakana(string)).toStrictEqual(expected); + } + }); +} + +function testConvertToRomaji() { + test('ConvertToRomaji', () => { + const data = [ + ['かたかな', 'katakana'], + ['ひらがな', 'hiragana'], + ['カタカナ', 'katakana'], + ['ヒラガナ', 'hiragana'], + ['カタカナかたかな', 'katakanakatakana'], + ['ヒラガナひらがな', 'hiraganahiragana'], + ['chikaraちからチカラ力', 'chikarachikarachikara力'], + ['katakana', 'katakana'], + ['hiragana', 'hiragana'] + ]; + + for (const [string, expected] of data) { + expect(jp.convertToRomaji(string)).toStrictEqual(expected); + } + }); +} + +function testConvertNumericToFullWidth() { + test('ConvertNumericToFullWidth', () => { + const data = [ + ['0123456789', '0123456789'], + ['abcdefghij', 'abcdefghij'], + ['カタカナ', 'カタカナ'], + ['ひらがな', 'ひらがな'] + ]; + + for (const [string, expected] of data) { + expect(jp.convertNumericToFullWidth(string)).toStrictEqual(expected); + } + }); +} + +function testConvertHalfWidthKanaToFullWidth() { + test('ConvertHalfWidthKanaToFullWidth', () => { + const data = [ + ['0123456789', '0123456789'], + ['abcdefghij', 'abcdefghij'], + ['カタカナ', 'カタカナ'], + ['ひらがな', 'ひらがな'], + ['カキ', 'カキ', [1, 1]], + ['ガキ', 'ガキ', [2, 1]], + ['ニホン', 'ニホン', [1, 1, 1]], + ['ニッポン', 'ニッポン', [1, 1, 2, 1]] + ]; + + for (const [string, expected, expectedSourceMapping] of data) { + const sourceMap = new TextSourceMap(string); + const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null); + const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap); + expect(actual1).toStrictEqual(expected); + expect(actual2).toStrictEqual(expected); + if (typeof expectedSourceMapping !== 'undefined') { + expect(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))).toBe(true); + } + } + }); +} + +function testConvertAlphabeticToKana() { + test('ConvertAlphabeticToKana', () => { + const data = [ + ['0123456789', '0123456789'], + ['abcdefghij', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], + ['ABCDEFGHIJ', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], // wanakana.toHiragana converts text to lower case + ['カタカナ', 'カタカナ'], + ['ひらがな', 'ひらがな'], + ['chikara', 'ちから', [3, 2, 2]], + ['CHIKARA', 'ちから', [3, 2, 2]] + ]; + + for (const [string, expected, expectedSourceMapping] of data) { + const sourceMap = new TextSourceMap(string); + const actual1 = jp.convertAlphabeticToKana(string, null); + const actual2 = jp.convertAlphabeticToKana(string, sourceMap); + expect(actual1).toStrictEqual(expected); + expect(actual2).toStrictEqual(expected); + if (typeof expectedSourceMapping !== 'undefined') { + expect(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))).toBe(true); + } + } + }); +} + +function testDistributeFurigana() { + test('DistributeFurigana', () => { + const data = [ + [ + ['有り難う', 'ありがとう'], + [ + {text: '有', reading: 'あ'}, + {text: 'り', reading: ''}, + {text: '難', reading: 'がと'}, + {text: 'う', reading: ''} + ] + ], + [ + ['方々', 'かたがた'], + [ + {text: '方々', reading: 'かたがた'} + ] + ], + [ + ['お祝い', 'おいわい'], + [ + {text: 'お', reading: ''}, + {text: '祝', reading: 'いわ'}, + {text: 'い', reading: ''} + ] + ], + [ + ['美味しい', 'おいしい'], + [ + {text: '美味', reading: 'おい'}, + {text: 'しい', reading: ''} + ] + ], + [ + ['食べ物', 'たべもの'], + [ + {text: '食', reading: 'た'}, + {text: 'べ', reading: ''}, + {text: '物', reading: 'もの'} + ] + ], + [ + ['試し切り', 'ためしぎり'], + [ + {text: '試', reading: 'ため'}, + {text: 'し', reading: ''}, + {text: '切', reading: 'ぎ'}, + {text: 'り', reading: ''} + ] + ], + // Ambiguous + [ + ['飼い犬', 'かいいぬ'], + [ + {text: '飼い犬', reading: 'かいいぬ'} + ] + ], + [ + ['長い間', 'ながいあいだ'], + [ + {text: '長い間', reading: 'ながいあいだ'} + ] + ], + // Same/empty reading + [ + ['飼い犬', ''], + [ + {text: '飼い犬', reading: ''} + ] + ], + [ + ['かいいぬ', 'かいいぬ'], + [ + {text: 'かいいぬ', reading: ''} + ] + ], + [ + ['かいぬ', 'かいぬ'], + [ + {text: 'かいぬ', reading: ''} + ] + ], + // Misc + [ + ['月', 'か'], + [ + {text: '月', reading: 'か'} + ] + ], + [ + ['月', 'カ'], + [ + {text: '月', reading: 'カ'} + ] + ], + // Mismatched kana readings + [ + ['有り難う', 'アリガトウ'], + [ + {text: '有', reading: 'ア'}, + {text: 'り', reading: 'リ'}, + {text: '難', reading: 'ガト'}, + {text: 'う', reading: 'ウ'} + ] + ], + [ + ['ありがとう', 'アリガトウ'], + [ + {text: 'ありがとう', reading: 'アリガトウ'} + ] + ], + // Mismatched kana readings (real examples) + [ + ['カ月', 'かげつ'], + [ + {text: 'カ', reading: 'か'}, + {text: '月', reading: 'げつ'} + ] + ], + [ + ['序ノ口', 'じょのくち'], + [ + {text: '序', reading: 'じょ'}, + {text: 'ノ', reading: 'の'}, + {text: '口', reading: 'くち'} + ] + ], + [ + ['スズメの涙', 'すずめのなみだ'], + [ + {text: 'スズメ', reading: 'すずめ'}, + {text: 'の', reading: ''}, + {text: '涙', reading: 'なみだ'} + ] + ], + [ + ['二カ所', 'にかしょ'], + [ + {text: '二', reading: 'に'}, + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['八ツ橋', 'やつはし'], + [ + {text: '八', reading: 'や'}, + {text: 'ツ', reading: 'つ'}, + {text: '橋', reading: 'はし'} + ] + ], + [ + ['八ツ橋', 'やつはし'], + [ + {text: '八', reading: 'や'}, + {text: 'ツ', reading: 'つ'}, + {text: '橋', reading: 'はし'} + ] + ], + [ + ['一カ月', 'いっかげつ'], + [ + {text: '一', reading: 'いっ'}, + {text: 'カ', reading: 'か'}, + {text: '月', reading: 'げつ'} + ] + ], + [ + ['一カ所', 'いっかしょ'], + [ + {text: '一', reading: 'いっ'}, + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['カ所', 'かしょ'], + [ + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['数カ月', 'すうかげつ'], + [ + {text: '数', reading: 'すう'}, + {text: 'カ', reading: 'か'}, + {text: '月', reading: 'げつ'} + ] + ], + [ + ['くノ一', 'くのいち'], + [ + {text: 'く', reading: ''}, + {text: 'ノ', reading: 'の'}, + {text: '一', reading: 'いち'} + ] + ], + [ + ['くノ一', 'くのいち'], + [ + {text: 'く', reading: ''}, + {text: 'ノ', reading: 'の'}, + {text: '一', reading: 'いち'} + ] + ], + [ + ['数カ国', 'すうかこく'], + [ + {text: '数', reading: 'すう'}, + {text: 'カ', reading: 'か'}, + {text: '国', reading: 'こく'} + ] + ], + [ + ['数カ所', 'すうかしょ'], + [ + {text: '数', reading: 'すう'}, + {text: 'カ', reading: 'か'}, + {text: '所', reading: 'しょ'} + ] + ], + [ + ['壇ノ浦の戦い', 'だんのうらのたたかい'], + [ + {text: '壇', reading: 'だん'}, + {text: 'ノ', reading: 'の'}, + {text: '浦', reading: 'うら'}, + {text: 'の', reading: ''}, + {text: '戦', reading: 'たたか'}, + {text: 'い', reading: ''} + ] + ], + [ + ['壇ノ浦の戦', 'だんのうらのたたかい'], + [ + {text: '壇', reading: 'だん'}, + {text: 'ノ', reading: 'の'}, + {text: '浦', reading: 'うら'}, + {text: 'の', reading: ''}, + {text: '戦', reading: 'たたかい'} + ] + ], + [ + ['序ノ口格', 'じょのくちかく'], + [ + {text: '序', reading: 'じょ'}, + {text: 'ノ', reading: 'の'}, + {text: '口格', reading: 'くちかく'} + ] + ], + [ + ['二カ国語', 'にかこくご'], + [ + {text: '二', reading: 'に'}, + {text: 'カ', reading: 'か'}, + {text: '国語', reading: 'こくご'} + ] + ], + [ + ['カ国', 'かこく'], + [ + {text: 'カ', reading: 'か'}, + {text: '国', reading: 'こく'} + ] + ], + [ + ['カ国語', 'かこくご'], + [ + {text: 'カ', reading: 'か'}, + {text: '国語', reading: 'こくご'} + ] + ], + [ + ['壇ノ浦の合戦', 'だんのうらのかっせん'], + [ + {text: '壇', reading: 'だん'}, + {text: 'ノ', reading: 'の'}, + {text: '浦', reading: 'うら'}, + {text: 'の', reading: ''}, + {text: '合戦', reading: 'かっせん'} + ] + ], + [ + ['一タ偏', 'いちたへん'], + [ + {text: '一', reading: 'いち'}, + {text: 'タ', reading: 'た'}, + {text: '偏', reading: 'へん'} + ] + ], + [ + ['ル又', 'るまた'], + [ + {text: 'ル', reading: 'る'}, + {text: '又', reading: 'また'} + ] + ], + [ + ['ノ木偏', 'のぎへん'], + [ + {text: 'ノ', reading: 'の'}, + {text: '木偏', reading: 'ぎへん'} + ] + ], + [ + ['一ノ貝', 'いちのかい'], + [ + {text: '一', reading: 'いち'}, + {text: 'ノ', reading: 'の'}, + {text: '貝', reading: 'かい'} + ] + ], + [ + ['虎ノ門事件', 'とらのもんじけん'], + [ + {text: '虎', reading: 'とら'}, + {text: 'ノ', reading: 'の'}, + {text: '門事件', reading: 'もんじけん'} + ] + ], + [ + ['教育ニ関スル勅語', 'きょういくにかんするちょくご'], + [ + {text: '教育', reading: 'きょういく'}, + {text: 'ニ', reading: 'に'}, + {text: '関', reading: 'かん'}, + {text: 'スル', reading: 'する'}, + {text: '勅語', reading: 'ちょくご'} + ] + ], + [ + ['二カ年', 'にかねん'], + [ + {text: '二', reading: 'に'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['三カ年', 'さんかねん'], + [ + {text: '三', reading: 'さん'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['四カ年', 'よんかねん'], + [ + {text: '四', reading: 'よん'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['五カ年', 'ごかねん'], + [ + {text: '五', reading: 'ご'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['六カ年', 'ろっかねん'], + [ + {text: '六', reading: 'ろっ'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['七カ年', 'ななかねん'], + [ + {text: '七', reading: 'なな'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['八カ年', 'はちかねん'], + [ + {text: '八', reading: 'はち'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['九カ年', 'きゅうかねん'], + [ + {text: '九', reading: 'きゅう'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['十カ年', 'じゅうかねん'], + [ + {text: '十', reading: 'じゅう'}, + {text: 'カ', reading: 'か'}, + {text: '年', reading: 'ねん'} + ] + ], + [ + ['鏡ノ間', 'かがみのま'], + [ + {text: '鏡', reading: 'かがみ'}, + {text: 'ノ', reading: 'の'}, + {text: '間', reading: 'ま'} + ] + ], + [ + ['鏡ノ間', 'かがみのま'], + [ + {text: '鏡', reading: 'かがみ'}, + {text: 'ノ', reading: 'の'}, + {text: '間', reading: 'ま'} + ] + ], + [ + ['ページ違反', 'ぺーじいはん'], + [ + {text: 'ペ', reading: 'ぺ'}, + {text: 'ー', reading: ''}, + {text: 'ジ', reading: 'じ'}, + {text: '違反', reading: 'いはん'} + ] + ], + // Mismatched kana + [ + ['サボる', 'サボル'], + [ + {text: 'サボ', reading: ''}, + {text: 'る', reading: 'ル'} + ] + ], + // Reading starts with term, but has remainder characters + [ + ['シック', 'シック・ビルしょうこうぐん'], + [ + {text: 'シック', reading: 'シック・ビルしょうこうぐん'} + ] + ], + // Kanji distribution tests + [ + ['逸らす', 'そらす'], + [ + {text: '逸', reading: 'そ'}, + {text: 'らす', reading: ''} + ] + ], + [ + ['逸らす', 'そらす'], + [ + {text: '逸', reading: 'そ'}, + {text: 'らす', reading: ''} + ] + ] + ]; + + for (const [[term, reading], expected] of data) { + const actual = jp.distributeFurigana(term, reading); + expect(actual).toStrictEqual(expected); + } + }); +} + +function testDistributeFuriganaInflected() { + test('DistributeFuriganaInflected', () => { + const data = [ + [ + ['美味しい', 'おいしい', '美味しかた'], + [ + {text: '美味', reading: 'おい'}, + {text: 'しかた', reading: ''} + ] + ], + [ + ['食べる', 'たべる', '食べた'], + [ + {text: '食', reading: 'た'}, + {text: 'べた', reading: ''} + ] + ], + [ + ['迄に', 'までに', 'までに'], + [ + {text: 'までに', reading: ''} + ] + ], + [ + ['行う', 'おこなう', 'おこなわなかった'], + [ + {text: 'おこなわなかった', reading: ''} + ] + ], + [ + ['いい', 'いい', 'イイ'], + [ + {text: 'イイ', reading: ''} + ] + ], + [ + ['否か', 'いなか', '否カ'], + [ + {text: '否', reading: 'いな'}, + {text: 'カ', reading: 'か'} + ] + ] + ]; + + for (const [[term, reading, source], expected] of data) { + const actual = jp.distributeFuriganaInflected(term, reading, source); + expect(actual).toStrictEqual(expected); + } + }); +} + +function testCollapseEmphaticSequences() { + test('CollapseEmphaticSequences', () => { + const data = [ + [['かこい', false], ['かこい', [1, 1, 1]]], + [['かこい', true], ['かこい', [1, 1, 1]]], + [['かっこい', false], ['かっこい', [1, 1, 1, 1]]], + [['かっこい', true], ['かこい', [2, 1, 1]]], + [['かっっこい', false], ['かっこい', [1, 2, 1, 1]]], + [['かっっこい', true], ['かこい', [3, 1, 1]]], + [['かっっっこい', false], ['かっこい', [1, 3, 1, 1]]], + [['かっっっこい', true], ['かこい', [4, 1, 1]]], + + [['こい', false], ['こい', [1, 1]]], + [['こい', true], ['こい', [1, 1]]], + [['っこい', false], ['っこい', [1, 1, 1]]], + [['っこい', true], ['こい', [2, 1]]], + [['っっこい', false], ['っこい', [2, 1, 1]]], + [['っっこい', true], ['こい', [3, 1]]], + [['っっっこい', false], ['っこい', [3, 1, 1]]], + [['っっっこい', true], ['こい', [4, 1]]], + + [['すごい', false], ['すごい', [1, 1, 1]]], + [['すごい', true], ['すごい', [1, 1, 1]]], + [['すごーい', false], ['すごーい', [1, 1, 1, 1]]], + [['すごーい', true], ['すごい', [1, 2, 1]]], + [['すごーーい', false], ['すごーい', [1, 1, 2, 1]]], + [['すごーーい', true], ['すごい', [1, 3, 1]]], + [['すっごーい', false], ['すっごーい', [1, 1, 1, 1, 1]]], + [['すっごーい', true], ['すごい', [2, 2, 1]]], + [['すっっごーーい', false], ['すっごーい', [1, 2, 1, 2, 1]]], + [['すっっごーーい', true], ['すごい', [3, 3, 1]]], + + [['', false], ['', []]], + [['', true], ['', []]], + [['っ', false], ['っ', [1]]], + [['っ', true], ['', [1]]], + [['っっ', false], ['っ', [2]]], + [['っっ', true], ['', [2]]], + [['っっっ', false], ['っ', [3]]], + [['っっっ', true], ['', [3]]] + ]; + + for (const [[text, fullCollapse], [expected, expectedSourceMapping]] of data) { + const sourceMap = new TextSourceMap(text); + const actual1 = jp.collapseEmphaticSequences(text, fullCollapse, null); + const actual2 = jp.collapseEmphaticSequences(text, fullCollapse, sourceMap); + expect(actual1).toStrictEqual(expected); + expect(actual2).toStrictEqual(expected); + if (typeof expectedSourceMapping !== 'undefined') { + expect(sourceMap.equals(new TextSourceMap(text, expectedSourceMapping))).toBe(true); + } + } + }); +} + +function testIsMoraPitchHigh() { + test('IsMoraPitchHigh', () => { + const data = [ + [[0, 0], false], + [[1, 0], true], + [[2, 0], true], + [[3, 0], true], + + [[0, 1], true], + [[1, 1], false], + [[2, 1], false], + [[3, 1], false], + + [[0, 2], false], + [[1, 2], true], + [[2, 2], false], + [[3, 2], false], + + [[0, 3], false], + [[1, 3], true], + [[2, 3], true], + [[3, 3], false], + + [[0, 4], false], + [[1, 4], true], + [[2, 4], true], + [[3, 4], true] + ]; + + for (const [[moraIndex, pitchAccentDownstepPosition], expected] of data) { + const actual = jp.isMoraPitchHigh(moraIndex, pitchAccentDownstepPosition); + expect(actual).toStrictEqual(expected); + } + }); +} + +function testGetKanaMorae() { + test('GetKanaMorae', () => { + const data = [ + ['かこ', ['か', 'こ']], + ['かっこ', ['か', 'っ', 'こ']], + ['カコ', ['カ', 'コ']], + ['カッコ', ['カ', 'ッ', 'コ']], + ['コート', ['コ', 'ー', 'ト']], + ['ちゃんと', ['ちゃ', 'ん', 'と']], + ['とうきょう', ['と', 'う', 'きょ', 'う']], + ['ぎゅう', ['ぎゅ', 'う']], + ['ディスコ', ['ディ', 'ス', 'コ']] + ]; + + for (const [text, expected] of data) { + const actual = jp.getKanaMorae(text); + expect(actual).toStrictEqual(expected); + } + }); +} + + +function main() { + testIsCodePointKanji(); + testIsCodePointKana(); + testIsCodePointJapanese(); + testIsStringEntirelyKana(); + testIsStringPartiallyJapanese(); + testConvertKatakanaToHiragana(); + testConvertHiraganaToKatakana(); + testConvertToRomaji(); + testConvertNumericToFullWidth(); + testConvertHalfWidthKanaToFullWidth(); + testConvertAlphabeticToKana(); + testDistributeFurigana(); + testDistributeFuriganaInflected(); + testCollapseEmphaticSequences(); + testIsMoraPitchHigh(); + testGetKanaMorae(); +} + +main(); diff --git a/test/test-jsdom.js b/test/jsdom.test.js index 5078c240..c53f374e 100644 --- a/test/test-jsdom.js +++ b/test/jsdom.test.js @@ -16,8 +16,8 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -const assert = require('assert'); -const {testMain} = require('../dev/util'); +import {JSDOM} from 'jsdom'; +import {expect, test} from 'vitest'; /** * This function tests the following bug: @@ -25,17 +25,18 @@ const {testMain} = require('../dev/util'); * - https://github.com/dperini/nwsapi/issues/48 */ function testJSDOMSelectorBug() { + test('JSDOMSelectorBug', () => { // nwsapi is used by JSDOM - const {JSDOM} = require('jsdom'); - const dom = new JSDOM(); - const {document} = dom.window; - const div = document.createElement('div'); - div.innerHTML = '<div class="b"><div class="c"></div></div>'; - const c = div.querySelector('.c'); - assert.doesNotThrow(() => { c.matches('.a:nth-last-of-type(1) .b .c'); }); + const dom = new JSDOM(); + const {document} = dom.window; + const div = document.createElement('div'); + div.innerHTML = '<div class="b"><div class="c"></div></div>'; + const c = div.querySelector('.c'); + expect(() => c.matches('.a:nth-last-of-type(1) .b .c')).not.toThrow(); + }); } -function testJSDOM() { +export function testJSDOM() { testJSDOMSelectorBug(); } @@ -43,8 +44,4 @@ function main() { testJSDOM(); } -module.exports = { - testJSDOM -}; - -if (require.main === module) { testMain(main); } +main(); diff --git a/test/json-schema.test.js b/test/json-schema.test.js new file mode 100644 index 00000000..5370e8da --- /dev/null +++ b/test/json-schema.test.js @@ -0,0 +1,1009 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {expect, test} from 'vitest'; +import {JsonSchema} from '../ext/js/data/json-schema.js'; + +function schemaValidate(schema, value) { + return new JsonSchema(schema).isValid(value); +} + +function getValidValueOrDefault(schema, value) { + return new JsonSchema(schema).getValidValueOrDefault(value); +} + +function createProxy(schema, value) { + return new JsonSchema(schema).createProxy(value); +} + +function clone(value) { + return JSON.parse(JSON.stringify(value)); +} + + +function testValidate1() { + test('Validate1', () => { + const schema = { + allOf: [ + { + type: 'number' + }, + { + anyOf: [ + {minimum: 10, maximum: 100}, + {minimum: -100, maximum: -10} + ] + }, + { + oneOf: [ + {multipleOf: 3}, + {multipleOf: 5} + ] + }, + { + not: [ + {multipleOf: 20} + ] + } + ] + }; + + const jsValidate = (value) => { + return ( + typeof value === 'number' && + ( + (value >= 10 && value <= 100) || + (value >= -100 && value <= -10) + ) && + ( + ( + (value % 3) === 0 || + (value % 5) === 0 + ) && + (value % 15) !== 0 + ) && + (value % 20) !== 0 + ); + }; + + for (let i = -111; i <= 111; i++) { + const actual = schemaValidate(schema, i); + const expected = jsValidate(i); + expect(actual).toStrictEqual(expected); + } + }); +} + +function testValidate2() { + test('Validate2', () => { + const data = [ + // String tests + { + schema: { + type: 'string' + }, + inputs: [ + {expected: false, value: null}, + {expected: false, value: void 0}, + {expected: false, value: 0}, + {expected: false, value: {}}, + {expected: false, value: []}, + {expected: true, value: ''} + ] + }, + { + schema: { + type: 'string', + minLength: 2 + }, + inputs: [ + {expected: false, value: ''}, + {expected: false, value: '1'}, + {expected: true, value: '12'}, + {expected: true, value: '123'} + ] + }, + { + schema: { + type: 'string', + maxLength: 2 + }, + inputs: [ + {expected: true, value: ''}, + {expected: true, value: '1'}, + {expected: true, value: '12'}, + {expected: false, value: '123'} + ] + }, + { + schema: { + type: 'string', + pattern: 'test' + }, + inputs: [ + {expected: false, value: ''}, + {expected: true, value: 'test'}, + {expected: false, value: 'TEST'}, + {expected: true, value: 'ABCtestDEF'}, + {expected: false, value: 'ABCTESTDEF'} + ] + }, + { + schema: { + type: 'string', + pattern: '^test$' + }, + inputs: [ + {expected: false, value: ''}, + {expected: true, value: 'test'}, + {expected: false, value: 'TEST'}, + {expected: false, value: 'ABCtestDEF'}, + {expected: false, value: 'ABCTESTDEF'} + ] + }, + { + schema: { + type: 'string', + pattern: '^test$', + patternFlags: 'i' + }, + inputs: [ + {expected: false, value: ''}, + {expected: true, value: 'test'}, + {expected: true, value: 'TEST'}, + {expected: false, value: 'ABCtestDEF'}, + {expected: false, value: 'ABCTESTDEF'} + ] + }, + { + schema: { + type: 'string', + pattern: '*' + }, + inputs: [ + {expected: false, value: ''} + ] + }, + { + schema: { + type: 'string', + pattern: '.', + patternFlags: '?' + }, + inputs: [ + {expected: false, value: ''} + ] + }, + + // Const tests + { + schema: { + const: 32 + }, + inputs: [ + {expected: true, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: '32' + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: true, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: null + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: true, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: {a: 'b'} + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + { + schema: { + const: [1, 2, 3] + }, + inputs: [ + {expected: false, value: 32}, + {expected: false, value: 0}, + {expected: false, value: '32'}, + {expected: false, value: null}, + {expected: false, value: {a: 'b'}}, + {expected: false, value: [1, 2, 3]} + ] + }, + + // Array contains tests + { + schema: { + type: 'array', + contains: {const: 32} + }, + inputs: [ + {expected: false, value: []}, + {expected: true, value: [32]}, + {expected: true, value: [1, 32]}, + {expected: true, value: [1, 32, 1]}, + {expected: false, value: [33]}, + {expected: false, value: [1, 33]}, + {expected: false, value: [1, 33, 1]} + ] + }, + + // Number limits tests + { + schema: { + type: 'number', + minimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: true, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'number', + exclusiveMinimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: false, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'number', + maximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: true, value: 0}, + {expected: false, value: 1} + ] + }, + { + schema: { + type: 'number', + exclusiveMaximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: false, value: 0}, + {expected: false, value: 1} + ] + }, + + // Integer limits tests + { + schema: { + type: 'integer', + minimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: true, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'integer', + exclusiveMinimum: 0 + }, + inputs: [ + {expected: false, value: -1}, + {expected: false, value: 0}, + {expected: true, value: 1} + ] + }, + { + schema: { + type: 'integer', + maximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: true, value: 0}, + {expected: false, value: 1} + ] + }, + { + schema: { + type: 'integer', + exclusiveMaximum: 0 + }, + inputs: [ + {expected: true, value: -1}, + {expected: false, value: 0}, + {expected: false, value: 1} + ] + }, + { + schema: { + type: 'integer', + multipleOf: 2 + }, + inputs: [ + {expected: true, value: -2}, + {expected: false, value: -1}, + {expected: true, value: 0}, + {expected: false, value: 1}, + {expected: true, value: 2} + ] + }, + + // Numeric type tests + { + schema: { + type: 'number' + }, + inputs: [ + {expected: true, value: 0}, + {expected: true, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + { + schema: { + type: 'integer' + }, + inputs: [ + {expected: true, value: 0}, + {expected: false, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + + // Reference tests + { + schema: { + definitions: { + example: { + type: 'number' + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + {expected: true, value: 0}, + {expected: true, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + { + schema: { + definitions: { + example: { + type: 'integer' + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + {expected: true, value: 0}, + {expected: false, value: 0.5}, + {expected: true, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: false, value: {}} + ] + }, + { + schema: { + definitions: { + example: { + type: 'object', + additionalProperties: false, + properties: { + test: { + $ref: '#/definitions/example' + } + } + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + {expected: false, value: 0}, + {expected: false, value: 0.5}, + {expected: false, value: 1}, + {expected: false, value: '0'}, + {expected: false, value: null}, + {expected: false, value: []}, + {expected: true, value: {}}, + {expected: false, value: {test: 0}}, + {expected: false, value: {test: 0.5}}, + {expected: false, value: {test: 1}}, + {expected: false, value: {test: '0'}}, + {expected: false, value: {test: null}}, + {expected: false, value: {test: []}}, + {expected: true, value: {test: {}}}, + {expected: true, value: {test: {test: {}}}}, + {expected: true, value: {test: {test: {test: {}}}}} + ] + } + ]; + + for (const {schema, inputs} of data) { + for (const {expected, value} of inputs) { + const actual = schemaValidate(schema, value); + expect(actual).toStrictEqual(expected); + } + } + }); +} + + +function testGetValidValueOrDefault1() { + test('GetValidValueOrDefault1', () => { + const data = [ + // Test value defaulting on objects with additionalProperties=false + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + }, + additionalProperties: false + }, + inputs: [ + [ + void 0, + {test: 'default'} + ], + [ + null, + {test: 'default'} + ], + [ + 0, + {test: 'default'} + ], + [ + '', + {test: 'default'} + ], + [ + [], + {test: 'default'} + ], + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + {test2: 'value2'}, + {test: 'default'} + ], + [ + {test: 'value', test2: 'value2'}, + {test: 'value'} + ] + ] + }, + + // Test value defaulting on objects with additionalProperties=true + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + }, + additionalProperties: true + }, + inputs: [ + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + {test2: 'value2'}, + {test: 'default', test2: 'value2'} + ], + [ + {test: 'value', test2: 'value2'}, + {test: 'value', test2: 'value2'} + ] + ] + }, + + // Test value defaulting on objects with additionalProperties={schema} + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + }, + additionalProperties: { + type: 'number', + default: 10 + } + }, + inputs: [ + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + {test2: 'value2'}, + {test: 'default', test2: 10} + ], + [ + {test: 'value', test2: 'value2'}, + {test: 'value', test2: 10} + ], + [ + {test2: 2}, + {test: 'default', test2: 2} + ], + [ + {test: 'value', test2: 2}, + {test: 'value', test2: 2} + ], + [ + {test: 'value', test2: 2, test3: null}, + {test: 'value', test2: 2, test3: 10} + ], + [ + {test: 'value', test2: 2, test3: void 0}, + {test: 'value', test2: 2, test3: 10} + ] + ] + }, + + // Test value defaulting where hasOwnProperty is false + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'default' + } + } + }, + inputs: [ + [ + {}, + {test: 'default'} + ], + [ + {test: 'value'}, + {test: 'value'} + ], + [ + Object.create({test: 'value'}), + {test: 'default'} + ] + ] + }, + { + schema: { + type: 'object', + required: ['toString'], + properties: { + toString: { + type: 'string', + default: 'default' + } + } + }, + inputs: [ + [ + {}, + {toString: 'default'} + ], + [ + {toString: 'value'}, + {toString: 'value'} + ], + [ + Object.create({toString: 'value'}), + {toString: 'default'} + ] + ] + }, + + // Test enum + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'string', + default: 'value1', + enum: ['value1', 'value2', 'value3'] + } + } + }, + inputs: [ + [ + {test: 'value1'}, + {test: 'value1'} + ], + [ + {test: 'value2'}, + {test: 'value2'} + ], + [ + {test: 'value3'}, + {test: 'value3'} + ], + [ + {test: 'value4'}, + {test: 'value1'} + ] + ] + }, + + // Test valid vs invalid default + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'integer', + default: 2, + minimum: 1 + } + } + }, + inputs: [ + [ + {test: -1}, + {test: 2} + ] + ] + }, + { + schema: { + type: 'object', + required: ['test'], + properties: { + test: { + type: 'integer', + default: 1, + minimum: 2 + } + } + }, + inputs: [ + [ + {test: -1}, + {test: -1} + ] + ] + }, + + // Test references + { + schema: { + definitions: { + example: { + type: 'number', + default: 0 + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + [ + 1, + 1 + ], + [ + null, + 0 + ], + [ + 'test', + 0 + ], + [ + {test: 'value'}, + 0 + ] + ] + }, + { + schema: { + definitions: { + example: { + type: 'object', + additionalProperties: false, + properties: { + test: { + $ref: '#/definitions/example' + } + } + } + }, + $ref: '#/definitions/example' + }, + inputs: [ + [ + 1, + {} + ], + [ + null, + {} + ], + [ + 'test', + {} + ], + [ + {}, + {} + ], + [ + {test: {}}, + {test: {}} + ], + [ + {test: 'value'}, + {test: {}} + ], + [ + {test: {test: {}}}, + {test: {test: {}}} + ] + ] + } + ]; + + for (const {schema, inputs} of data) { + for (const [value, expected] of inputs) { + const actual = getValidValueOrDefault(schema, value); + expect(actual).toStrictEqual(expected); + } + } + }); +} + + +function testProxy1() { + test('Proxy1', () => { + const data = [ + // Object tests + { + schema: { + type: 'object', + required: ['test'], + additionalProperties: false, + properties: { + test: { + type: 'string', + default: 'default' + } + } + }, + tests: [ + {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }}, + {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }}, + {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }}, + {error: true, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }}, + {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }} + ] + }, + { + schema: { + type: 'object', + required: ['test'], + additionalProperties: true, + properties: { + test: { + type: 'string', + default: 'default' + } + } + }, + tests: [ + {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }}, + {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }}, + {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }}, + {error: false, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }}, + {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }} + ] + }, + { + schema: { + type: 'object', + required: ['test1'], + additionalProperties: false, + properties: { + test1: { + type: 'object', + required: ['test2'], + additionalProperties: false, + properties: { + test2: { + type: 'object', + required: ['test3'], + additionalProperties: false, + properties: { + test3: { + type: 'string', + default: 'default' + } + } + } + } + } + } + }, + tests: [ + {error: false, action: (value) => { value.test1.test2.test3 = 'string'; }}, + {error: true, action: (value) => { value.test1.test2.test3 = null; }}, + {error: true, action: (value) => { delete value.test1.test2.test3; }}, + {error: true, action: (value) => { value.test1.test2 = null; }}, + {error: true, action: (value) => { value.test1 = null; }}, + {error: true, action: (value) => { value.test4 = 'string'; }}, + {error: false, action: (value) => { delete value.test4; }} + ] + }, + + // Array tests + { + schema: { + type: 'array', + items: { + type: 'string', + default: 'default' + } + }, + tests: [ + {error: false, value: ['default'], action: (value) => { value[0] = 'string'; }}, + {error: true, value: ['default'], action: (value) => { value[0] = null; }}, + {error: false, value: ['default'], action: (value) => { delete value[0]; }}, + {error: false, value: ['default'], action: (value) => { value[1] = 'string'; }}, + {error: false, value: ['default'], action: (value) => { + value[1] = 'string'; + if (value.length !== 2) { throw new Error(`Invalid length; expected=2; actual=${value.length}`); } + if (typeof value.push !== 'function') { throw new Error(`Invalid push; expected=function; actual=${typeof value.push}`); } + }} + ] + }, + + // Reference tests + { + schema: { + definitions: { + example: { + type: 'object', + additionalProperties: false, + properties: { + test: { + $ref: '#/definitions/example' + } + } + } + }, + $ref: '#/definitions/example' + }, + tests: [ + {error: false, value: {}, action: (value) => { value.test = {}; }}, + {error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }}, + {error: false, value: {}, action: (value) => { value.test = {test: {}}; }}, + {error: true, value: {}, action: (value) => { value.test = null; }}, + {error: true, value: {}, action: (value) => { value.test = 'string'; }}, + {error: true, value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }}, + {error: true, value: {}, action: (value) => { value.test = {test: 'string'}; }} + ] + } + ]; + + for (const {schema, tests} of data) { + for (let {error, value, action} of tests) { + if (typeof value === 'undefined') { value = getValidValueOrDefault(schema, void 0); } + value = clone(value); + expect(schemaValidate(schema, value)).toBe(true); + const valueProxy = createProxy(schema, value); + if (error) { + expect(() => action(valueProxy)).toThrow(); + } else { + expect(() => action(valueProxy)).not.toThrow(); + } + } + } + }); +} + + +function main() { + testValidate1(); + testValidate2(); + testGetValidValueOrDefault1(); + testProxy1(); +} + + +main(); diff --git a/test/object-property-accessor.test.js b/test/object-property-accessor.test.js new file mode 100644 index 00000000..a8730093 --- /dev/null +++ b/test/object-property-accessor.test.js @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {expect, test} from 'vitest'; +import {ObjectPropertyAccessor} from '../ext/js/general/object-property-accessor.js'; + +function createTestObject() { + return { + 0: null, + value1: { + value2: {}, + value3: [], + value4: null + }, + value5: [ + {}, + [], + null + ] + }; +} + + +function testGet1() { + test('Get1', () => { + const data = [ + [[], (object) => object], + [['0'], (object) => object['0']], + [['value1'], (object) => object.value1], + [['value1', 'value2'], (object) => object.value1.value2], + [['value1', 'value3'], (object) => object.value1.value3], + [['value1', 'value4'], (object) => object.value1.value4], + [['value5'], (object) => object.value5], + [['value5', 0], (object) => object.value5[0]], + [['value5', 1], (object) => object.value5[1]], + [['value5', 2], (object) => object.value5[2]] + ]; + + for (const [pathArray, getExpected] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + const expected = getExpected(object); + + expect(accessor.get(pathArray)).toStrictEqual(expected); + } + }); +} + +function testGet2() { + test('Get2', () => { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + const data = [ + [[0], 'Invalid path: [0]'], + [['0', 'invalid'], 'Invalid path: ["0"].invalid'], + [['invalid'], 'Invalid path: invalid'], + [['value1', 'invalid'], 'Invalid path: value1.invalid'], + [['value1', 'value2', 'invalid'], 'Invalid path: value1.value2.invalid'], + [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], + [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], + [['value1', 'value3', 0], 'Invalid path: value1.value3[0]'], + [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], + [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], + [['value5', 'length'], 'Invalid path: value5.length'], + [['value5', 0, 'invalid'], 'Invalid path: value5[0].invalid'], + [['value5', 0, 0], 'Invalid path: value5[0][0]'], + [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], + [['value5', 1, 0], 'Invalid path: value5[1][0]'], + [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], + [['value5', 2, 0], 'Invalid path: value5[2][0]'], + [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], + [['value5', 2.5], 'Invalid index'] + ]; + + for (const [pathArray, message] of data) { + expect(() => accessor.get(pathArray)).toThrow(message); + } + }); +} + + +function testSet1() { + test('Set1', () => { + const testValue = {}; + const data = [ + ['0'], + ['value1', 'value2'], + ['value1', 'value3'], + ['value1', 'value4'], + ['value1'], + ['value5', 0], + ['value5', 1], + ['value5', 2], + ['value5'] + ]; + + for (const pathArray of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + accessor.set(pathArray, testValue); + expect(accessor.get(pathArray)).toStrictEqual(testValue); + } + }); +} + +function testSet2() { + test('Set2', () => { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + const testValue = {}; + const data = [ + [[], 'Invalid path'], + [[0], 'Invalid path: [0]'], + [['0', 'invalid'], 'Invalid path: ["0"].invalid'], + [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], + [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], + [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], + [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], + [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], + [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], + [['value5', 2, 0], 'Invalid path: value5[2][0]'], + [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], + [['value5', 2.5], 'Invalid index'] + ]; + + for (const [pathArray, message] of data) { + expect(() => accessor.set(pathArray, testValue)).toThrow(message); + } + }); +} + + +function testDelete1() { + test('Delete1', () => { + const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property); + + const data = [ + [['0'], (object) => !hasOwn(object, '0')], + [['value1', 'value2'], (object) => !hasOwn(object.value1, 'value2')], + [['value1', 'value3'], (object) => !hasOwn(object.value1, 'value3')], + [['value1', 'value4'], (object) => !hasOwn(object.value1, 'value4')], + [['value1'], (object) => !hasOwn(object, 'value1')], + [['value5'], (object) => !hasOwn(object, 'value5')] + ]; + + for (const [pathArray, validate] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + accessor.delete(pathArray); + expect(validate(object)).toBe(true); + } + }); +} + +function testDelete2() { + test('Delete2', () => { + const data = [ + [[], 'Invalid path'], + [[0], 'Invalid path: [0]'], + [['0', 'invalid'], 'Invalid path: ["0"].invalid'], + [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], + [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], + [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], + [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], + [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], + [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], + [['value5', 2, 0], 'Invalid path: value5[2][0]'], + [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], + [['value5', 2.5], 'Invalid index'], + [['value5', 0], 'Invalid type'], + [['value5', 1], 'Invalid type'], + [['value5', 2], 'Invalid type'] + ]; + + for (const [pathArray, message] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + expect(() => accessor.delete(pathArray)).toThrow(message); + } + }); +} + + +function testSwap1() { + test('Swap1', () => { + const data = [ + [['0'], true], + [['value1', 'value2'], true], + [['value1', 'value3'], true], + [['value1', 'value4'], true], + [['value1'], false], + [['value5', 0], true], + [['value5', 1], true], + [['value5', 2], true], + [['value5'], false] + ]; + + for (const [pathArray1, compareValues1] of data) { + for (const [pathArray2, compareValues2] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + const value1a = accessor.get(pathArray1); + const value2a = accessor.get(pathArray2); + + accessor.swap(pathArray1, pathArray2); + + if (!compareValues1 || !compareValues2) { continue; } + + const value1b = accessor.get(pathArray1); + const value2b = accessor.get(pathArray2); + + expect(value1a).toStrictEqual(value2b); + expect(value2a).toStrictEqual(value1b); + } + } + }); +} + +function testSwap2() { + test('Swap2', () => { + const data = [ + [[], [], false, 'Invalid path 1'], + [['0'], [], false, 'Invalid path 2'], + [[], ['0'], false, 'Invalid path 1'], + [[0], ['0'], false, 'Invalid path 1: [0]'], + [['0'], [0], false, 'Invalid path 2: [0]'] + ]; + + for (const [pathArray1, pathArray2, checkRevert, message] of data) { + const object = createTestObject(); + const accessor = new ObjectPropertyAccessor(object); + + let value1a; + let value2a; + if (checkRevert) { + try { + value1a = accessor.get(pathArray1); + value2a = accessor.get(pathArray2); + } catch (e) { + // NOP + } + } + + expect(() => accessor.swap(pathArray1, pathArray2)).toThrow(message); + + if (!checkRevert) { continue; } + + const value1b = accessor.get(pathArray1); + const value2b = accessor.get(pathArray2); + + expect(value1a).toStrictEqual(value1b); + expect(value2a).toStrictEqual(value2b); + } + }); +} + + +function testGetPathString1() { + test('GetPathString1', () => { + const data = [ + [[], ''], + [[0], '[0]'], + [['escape\\'], '["escape\\\\"]'], + [['\'quote\''], '["\'quote\'"]'], + [['"quote"'], '["\\"quote\\""]'], + [['part1', 'part2'], 'part1.part2'], + [['part1', 'part2', 3], 'part1.part2[3]'], + [['part1', 'part2', '3'], 'part1.part2["3"]'], + [['part1', 'part2', '3part'], 'part1.part2["3part"]'], + [['part1', 'part2', '3part', 'part4'], 'part1.part2["3part"].part4'], + [['part1', 'part2', '3part', '4part'], 'part1.part2["3part"]["4part"]'] + ]; + + for (const [pathArray, expected] of data) { + expect(ObjectPropertyAccessor.getPathString(pathArray)).toStrictEqual(expected); + } + }); +} + +function testGetPathString2() { + test('GetPathString2', () => { + const data = [ + [[1.5], 'Invalid index'], + [[null], 'Invalid type: object'] + ]; + + for (const [pathArray, message] of data) { + expect(() => ObjectPropertyAccessor.getPathString(pathArray)).toThrow(message); + } + }); +} + + +function testGetPathArray1() { + test('GetPathArray1', () => { + const data = [ + ['', []], + ['[0]', [0]], + ['["escape\\\\"]', ['escape\\']], + ['["\'quote\'"]', ['\'quote\'']], + ['["\\"quote\\""]', ['"quote"']], + ['part1.part2', ['part1', 'part2']], + ['part1.part2[3]', ['part1', 'part2', 3]], + ['part1.part2["3"]', ['part1', 'part2', '3']], + ['part1.part2[\'3\']', ['part1', 'part2', '3']], + ['part1.part2["3part"]', ['part1', 'part2', '3part']], + ['part1.part2[\'3part\']', ['part1', 'part2', '3part']], + ['part1.part2["3part"].part4', ['part1', 'part2', '3part', 'part4']], + ['part1.part2[\'3part\'].part4', ['part1', 'part2', '3part', 'part4']], + ['part1.part2["3part"]["4part"]', ['part1', 'part2', '3part', '4part']], + ['part1.part2[\'3part\'][\'4part\']', ['part1', 'part2', '3part', '4part']] + ]; + + for (const [pathString, expected] of data) { + expect(ObjectPropertyAccessor.getPathArray(pathString)).toStrictEqual(expected); + } + }); +} + +function testGetPathArray2() { + test('GetPathArray2', () => { + const data = [ + ['?', 'Unexpected character: ?'], + ['.', 'Unexpected character: .'], + ['0', 'Unexpected character: 0'], + ['part1.[0]', 'Unexpected character: ['], + ['part1?', 'Unexpected character: ?'], + ['[part1]', 'Unexpected character: p'], + ['[0a]', 'Unexpected character: a'], + ['["part1"x]', 'Unexpected character: x'], + ['[\'part1\'x]', 'Unexpected character: x'], + ['["part1"]x', 'Unexpected character: x'], + ['[\'part1\']x', 'Unexpected character: x'], + ['part1..part2', 'Unexpected character: .'], + + ['[', 'Path not terminated correctly'], + ['part1.', 'Path not terminated correctly'], + ['part1[', 'Path not terminated correctly'], + ['part1["', 'Path not terminated correctly'], + ['part1[\'', 'Path not terminated correctly'], + ['part1[""', 'Path not terminated correctly'], + ['part1[\'\'', 'Path not terminated correctly'], + ['part1[0', 'Path not terminated correctly'], + ['part1[0].', 'Path not terminated correctly'] + ]; + + for (const [pathString, message] of data) { + expect(() => ObjectPropertyAccessor.getPathArray(pathString)).toThrow(message); + } + }); +} + + +function testHasProperty() { + test('HasProperty', () => { + const data = [ + [{}, 'invalid', false], + [{}, 0, false], + [{valid: 0}, 'valid', true], + [{null: 0}, null, false], + [[], 'invalid', false], + [[], 0, false], + [[0], 0, true], + [[0], null, false], + ['string', 0, false], + ['string', 'length', false], + ['string', null, false] + ]; + + for (const [object, property, expected] of data) { + expect(ObjectPropertyAccessor.hasProperty(object, property)).toStrictEqual(expected); + } + }); +} + +function testIsValidPropertyType() { + test('IsValidPropertyType', () => { + const data = [ + [{}, 'invalid', true], + [{}, 0, false], + [{valid: 0}, 'valid', true], + [{null: 0}, null, false], + [[], 'invalid', false], + [[], 0, true], + [[0], 0, true], + [[0], null, false], + ['string', 0, false], + ['string', 'length', false], + ['string', null, false] + ]; + + for (const [object, property, expected] of data) { + expect(ObjectPropertyAccessor.isValidPropertyType(object, property)).toStrictEqual(expected); + } + }); +} + + +function main() { + testGet1(); + testGet2(); + testSet1(); + testSet2(); + testDelete1(); + testDelete2(); + testSwap1(); + testSwap2(); + testGetPathString1(); + testGetPathString2(); + testGetPathArray1(); + testGetPathArray2(); + testHasProperty(); + testIsValidPropertyType(); +} + + +main(); diff --git a/test/test-options-util.js b/test/options-util.test.js index d94028c0..9f49eb28 100644 --- a/test/test-options-util.js +++ b/test/options-util.test.js @@ -16,53 +16,33 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -const fs = require('fs'); -const url = require('url'); -const path = require('path'); -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - - -function createVM(extDir) { - const chrome = { - runtime: { - getURL(path2) { - return url.pathToFileURL(path.join(extDir, path2.replace(/^\//, ''))).href; - } - } +import fs from 'fs'; +import url, {fileURLToPath} from 'node:url'; +import path from 'path'; +import {expect, test, vi} from 'vitest'; +import {OptionsUtil} from '../ext/js/data/options-util.js'; +import {TemplatePatcher} from '../ext/js/templates/template-patcher.js'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); +vi.stubGlobal('fetch', async function fetch(url2) { + const filePath = url.fileURLToPath(url2); + await Promise.resolve(); + const content = fs.readFileSync(filePath, {encoding: null}); + return { + ok: true, + status: 200, + statusText: 'OK', + text: async () => Promise.resolve(content.toString('utf8')), + json: async () => Promise.resolve(JSON.parse(content.toString('utf8'))) }; - - async function fetch(url2) { - const filePath = url.fileURLToPath(url2); - await Promise.resolve(); - const content = fs.readFileSync(filePath, {encoding: null}); - return { - ok: true, - status: 200, - statusText: 'OK', - text: async () => Promise.resolve(content.toString('utf8')), - json: async () => Promise.resolve(JSON.parse(content.toString('utf8'))) - }; +}); +vi.stubGlobal('chrome', { + runtime: { + getURL: (path2) => { + return url.pathToFileURL(path.join(dirname, '..', 'ext', path2.replace(/^\//, ''))).href; + } } - - const vm = new VM({chrome, fetch}); - vm.execute([ - 'js/core.js', - 'js/general/cache-map.js', - 'js/data/json-schema.js', - 'js/templates/template-patcher.js', - 'js/data/options-util.js' - ]); - - return vm; -} - - -function clone(value) { - return JSON.parse(JSON.stringify(value)); -} - +}); function createProfileOptionsTestData1() { return { @@ -632,131 +612,130 @@ function createOptionsUpdatedTestData1() { } -async function testUpdate(extDir) { - const vm = createVM(extDir); - const [OptionsUtil] = vm.get(['OptionsUtil']); - const optionsUtil = new OptionsUtil(); - await optionsUtil.prepare(); +async function testUpdate() { + test('Update', async () => { + const optionsUtil = new OptionsUtil(); + await optionsUtil.prepare(); - const options = createOptionsTestData1(); - const optionsUpdated = clone(await optionsUtil.update(options)); - const optionsExpected = createOptionsUpdatedTestData1(); - assert.deepStrictEqual(optionsUpdated, optionsExpected); + const options = createOptionsTestData1(); + const optionsUpdated = structuredClone(await optionsUtil.update(options)); + const optionsExpected = createOptionsUpdatedTestData1(); + expect(optionsUpdated).toStrictEqual(optionsExpected); + }); } -async function testDefault(extDir) { - const data = [ - (options) => options, - (options) => { - delete options.profiles[0].options.audio.autoPlay; - }, - (options) => { - options.profiles[0].options.audio.autoPlay = void 0; - } - ]; +async function testDefault() { + test('Default', async () => { + const data = [ + (options) => options, + (options) => { + delete options.profiles[0].options.audio.autoPlay; + }, + (options) => { + options.profiles[0].options.audio.autoPlay = void 0; + } + ]; - const vm = createVM(extDir); - const [OptionsUtil] = vm.get(['OptionsUtil']); - const optionsUtil = new OptionsUtil(); - await optionsUtil.prepare(); + const optionsUtil = new OptionsUtil(); + await optionsUtil.prepare(); - for (const modify of data) { - const options = optionsUtil.getDefault(); + for (const modify of data) { + const options = optionsUtil.getDefault(); - const optionsModified = clone(options); - modify(optionsModified); + const optionsModified = structuredClone(options); + modify(optionsModified); - const optionsUpdated = await optionsUtil.update(clone(optionsModified)); - assert.deepStrictEqual(clone(optionsUpdated), clone(options)); - } + const optionsUpdated = await optionsUtil.update(structuredClone(optionsModified)); + expect(structuredClone(optionsUpdated)).toStrictEqual(structuredClone(options)); + } + }); } -async function testFieldTemplatesUpdate(extDir) { - const vm = createVM(extDir); - const [OptionsUtil, TemplatePatcher] = vm.get(['OptionsUtil', 'TemplatePatcher']); - const optionsUtil = new OptionsUtil(); - await optionsUtil.prepare(); +async function testFieldTemplatesUpdate() { + test('FieldTemplatesUpdate', async () => { + const optionsUtil = new OptionsUtil(); + await optionsUtil.prepare(); - const templatePatcher = new TemplatePatcher(); - const loadDataFile = (fileName) => { - const content = fs.readFileSync(path.join(extDir, fileName), {encoding: 'utf8'}); - return templatePatcher.parsePatch(content).addition; - }; - const updates = [ - {version: 2, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v2.handlebars')}, - {version: 4, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v4.handlebars')}, - {version: 6, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')}, - {version: 8, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')}, - {version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.handlebars')}, - {version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')}, - {version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')}, - {version: 21, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v21.handlebars')} - ]; - const getUpdateAdditions = (startVersion, targetVersion) => { - let value = ''; - for (const {version, changes} of updates) { - if (version <= startVersion || version > targetVersion || changes.length === 0) { continue; } - if (value.length > 0) { value += '\n'; } - value += changes; - } - return value; - }; + const templatePatcher = new TemplatePatcher(); + const loadDataFile = (fileName) => { + const content = fs.readFileSync(path.join(dirname, '..', 'ext', fileName), {encoding: 'utf8'}); + return templatePatcher.parsePatch(content).addition; + }; + const updates = [ + {version: 2, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v2.handlebars')}, + {version: 4, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v4.handlebars')}, + {version: 6, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v6.handlebars')}, + {version: 8, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v8.handlebars')}, + {version: 10, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v10.handlebars')}, + {version: 12, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v12.handlebars')}, + {version: 13, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v13.handlebars')}, + {version: 21, changes: loadDataFile('data/templates/anki-field-templates-upgrade-v21.handlebars')} + ]; + const getUpdateAdditions = (startVersion, targetVersion) => { + let value = ''; + for (const {version, changes} of updates) { + if (version <= startVersion || version > targetVersion || changes.length === 0) { continue; } + if (value.length > 0) { value += '\n'; } + value += changes; + } + return value; + }; - const data = [ + const data = [ // Standard format - { - oldVersion: 0, - newVersion: 12, - old: ` + { + oldVersion: 0, + newVersion: 12, + old: ` {{#*inline "character"}} {{~definition.character~}} {{/inline}} {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "character"}} {{~definition.character~}} {{/inline}} <<<UPDATE-ADDITIONS>>> {{~> (lookup . "marker") ~}}`.trimStart() - }, - // Non-standard marker format - { - oldVersion: 0, - newVersion: 12, - old: ` + }, + // Non-standard marker format + { + oldVersion: 0, + newVersion: 12, + old: ` {{#*inline "character"}} {{~definition.character~}} {{/inline}} {{~> (lookup . "marker2") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "character"}} {{~definition.character~}} {{/inline}} {{~> (lookup . "marker2") ~}} <<<UPDATE-ADDITIONS>>>`.trimStart() - }, - // Empty test - { - oldVersion: 0, - newVersion: 12, - old: ` + }, + // Empty test + { + oldVersion: 0, + newVersion: 12, + old: ` {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` <<<UPDATE-ADDITIONS>>> {{~> (lookup . "marker") ~}}`.trimStart() - }, - // Definition tags update - { - oldVersion: 0, - newVersion: 12, - old: ` + }, + // Definition tags update + { + oldVersion: 0, + newVersion: 12, + old: ` {{#*inline "glossary-single"}} {{~#unless brief~}} {{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}} @@ -779,7 +758,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}} `.trimStart(), - expected: ` + expected: ` {{#*inline "glossary-single"}} {{~#unless brief~}} {{~#scope~}} @@ -824,12 +803,12 @@ async function testFieldTemplatesUpdate(extDir) { <<<UPDATE-ADDITIONS>>> {{~> (lookup . "marker") ~}} `.trimStart() - }, - // glossary and glossary-brief update - { - oldVersion: 7, - newVersion: 12, - old: ` + }, + // glossary and glossary-brief update + { + oldVersion: 7, + newVersion: 12, + old: ` {{#*inline "glossary-single"}} {{~#unless brief~}} {{~#scope~}} @@ -895,7 +874,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "glossary-single"}} {{~#unless brief~}} {{~#scope~}} @@ -964,12 +943,12 @@ async function testFieldTemplatesUpdate(extDir) { <<<UPDATE-ADDITIONS>>> {{~> (lookup . "marker") ~}}`.trimStart() - }, - // formatGlossary update - { - oldVersion: 12, - newVersion: 13, - old: ` + }, + // formatGlossary update + { + oldVersion: 12, + newVersion: 13, + old: ` {{#*inline "example"}} {{~#if (op "<=" glossary.length 1)~}} {{#each glossary}}{{#multiLine}}{{.}}{{/multiLine}}{{/each}} @@ -982,7 +961,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "example"}} {{~#if (op "<=" glossary.length 1)~}} {{#each glossary}}{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}{{/each}} @@ -995,12 +974,12 @@ async function testFieldTemplatesUpdate(extDir) { <<<UPDATE-ADDITIONS>>> {{~> (lookup . "marker") ~}}`.trimStart() - }, - // hasMedia/getMedia update - { - oldVersion: 12, - newVersion: 13, - old: ` + }, + // hasMedia/getMedia update + { + oldVersion: 12, + newVersion: 13, + old: ` {{#*inline "audio"}} {{~#if definition.audioFileName~}} [sound:{{definition.audioFileName}}] @@ -1023,7 +1002,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "audio"}} {{~#if (hasMedia "audio")~}} [sound:{{#getMedia "audio"}}{{/getMedia}}] @@ -1048,12 +1027,12 @@ async function testFieldTemplatesUpdate(extDir) { <<<UPDATE-ADDITIONS>>> {{~> (lookup . "marker") ~}}`.trimStart() - }, - // hasMedia/getMedia update - { - oldVersion: 12, - newVersion: 13, - old: ` + }, + // hasMedia/getMedia update + { + oldVersion: 12, + newVersion: 13, + old: ` {{! Pitch Accents }} {{#*inline "pitch-accent-item-downstep-notation"}} {{~#scope~}} @@ -1166,7 +1145,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{! Pitch Accents }} {{#*inline "pitch-accent-item"}} {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} @@ -1216,12 +1195,12 @@ async function testFieldTemplatesUpdate(extDir) { <<<UPDATE-ADDITIONS>>> {{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: furigana and furiganaPlain - { - oldVersion: 20, - newVersion: 21, - old: ` + }, + // block helper update: furigana and furiganaPlain + { + oldVersion: 20, + newVersion: 21, + old: ` {{#*inline "furigana"}} {{~#if merge~}} {{~#each definition.expressions~}} @@ -1263,7 +1242,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "furigana"}} {{~#if merge~}} {{~#each definition.expressions~}} @@ -1304,12 +1283,12 @@ async function testFieldTemplatesUpdate(extDir) { {{/inline}} {{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: formatGlossary - { - oldVersion: 20, - newVersion: 21, - old: ` + }, + // block helper update: formatGlossary + { + oldVersion: 20, + newVersion: 21, + old: ` {{#*inline "glossary-single"}} {{~#unless brief~}} {{~#scope~}} @@ -1344,7 +1323,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "glossary-single"}} {{~#unless brief~}} {{~#scope~}} @@ -1378,12 +1357,12 @@ async function testFieldTemplatesUpdate(extDir) { {{/inline}} {{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: set and get - { - oldVersion: 20, - newVersion: 21, - old: ` + }, + // block helper update: set and get + { + oldVersion: 20, + newVersion: 21, + old: ` {{#*inline "pitch-accent-item-disambiguation"}} {{~#scope~}} {{~#set "exclusive" (spread exclusiveExpressions exclusiveReadings)}}{{/set~}} @@ -1432,7 +1411,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "pitch-accent-item-disambiguation"}} {{~#scope~}} {{~set "exclusive" (spread exclusiveExpressions exclusiveReadings)~}} @@ -1480,12 +1459,12 @@ async function testFieldTemplatesUpdate(extDir) { {{/inline}} {{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: hasMedia and getMedia - { - oldVersion: 20, - newVersion: 21, - old: ` + }, + // block helper update: hasMedia and getMedia + { + oldVersion: 20, + newVersion: 21, + old: ` {{#*inline "audio"}} {{~#if (hasMedia "audio")~}} [sound:{{#getMedia "audio"}}{{/getMedia}}] @@ -1524,7 +1503,7 @@ async function testFieldTemplatesUpdate(extDir) { {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "audio"}} {{~#if (hasMedia "audio")~}} [sound:{{getMedia "audio"}}] @@ -1562,48 +1541,47 @@ async function testFieldTemplatesUpdate(extDir) { {{/inline}} {{~> (lookup . "marker") ~}}`.trimStart() - }, - // block helper update: pronunciation - { - oldVersion: 20, - newVersion: 21, - old: ` + }, + // block helper update: pronunciation + { + oldVersion: 20, + newVersion: 21, + old: ` {{#*inline "pitch-accent-item"}} {{~#pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}}{{~/pronunciation~}} {{/inline}} {{~> (lookup . "marker") ~}}`.trimStart(), - expected: ` + expected: ` {{#*inline "pitch-accent-item"}} {{~pronunciation format=format reading=reading downstepPosition=position nasalPositions=nasalPositions devoicePositions=devoicePositions~}} {{/inline}} {{~> (lookup . "marker") ~}}`.trimStart() - } - ]; + } + ]; - const updatesPattern = /<<<UPDATE-ADDITIONS>>>/g; - for (const {old, expected, oldVersion, newVersion} of data) { - const options = createOptionsTestData1(); - options.profiles[0].options.anki.fieldTemplates = old; - options.version = oldVersion; + const updatesPattern = /<<<UPDATE-ADDITIONS>>>/g; + for (const {old, expected, oldVersion, newVersion} of data) { + const options = createOptionsTestData1(); + options.profiles[0].options.anki.fieldTemplates = old; + options.version = oldVersion; - const expected2 = expected.replace(updatesPattern, getUpdateAdditions(oldVersion, newVersion)); + const expected2 = expected.replace(updatesPattern, getUpdateAdditions(oldVersion, newVersion)); - const optionsUpdated = clone(await optionsUtil.update(options, newVersion)); - const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates; - assert.deepStrictEqual(fieldTemplatesActual, expected2); - } + const optionsUpdated = structuredClone(await optionsUtil.update(options, newVersion)); + const fieldTemplatesActual = optionsUpdated.profiles[0].options.anki.fieldTemplates; + expect(fieldTemplatesActual).toStrictEqual(expected2); + } + }); } async function main() { - const extDir = path.join(__dirname, '..', 'ext'); - await testUpdate(extDir); - await testDefault(extDir); - await testFieldTemplatesUpdate(extDir); + await testUpdate(); + await testDefault(); + await testFieldTemplatesUpdate(); } - -if (require.main === module) { testMain(main); } +await main(); diff --git a/test/profile-conditions-util.test.js b/test/profile-conditions-util.test.js new file mode 100644 index 00000000..ca8b00ef --- /dev/null +++ b/test/profile-conditions-util.test.js @@ -0,0 +1,1090 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {expect, test} from 'vitest'; +import {ProfileConditionsUtil} from '../ext/js/background/profile-conditions-util.js'; + +function testNormalizeContext() { + test('NormalizeContext', () => { + const data = [ + // Empty + { + context: {}, + expected: {flags: []} + }, + + // Domain normalization + { + context: {url: ''}, + expected: {url: '', flags: []} + }, + { + context: {url: 'http://example.com/'}, + expected: {url: 'http://example.com/', domain: 'example.com', flags: []} + }, + { + context: {url: 'http://example.com:1234/'}, + expected: {url: 'http://example.com:1234/', domain: 'example.com', flags: []} + }, + { + context: {url: 'http://user@example.com:1234/'}, + expected: {url: 'http://user@example.com:1234/', domain: 'example.com', flags: []} + } + ]; + + for (const {context, expected} of data) { + const profileConditionsUtil = new ProfileConditionsUtil(); + const actual = profileConditionsUtil.normalizeContext(context); + expect(actual).toStrictEqual(expected); + } + }); +} + +function testSchemas() { + test('Schemas', () => { + const data = [ + // Empty + { + conditionGroups: [], + expectedSchema: {}, + inputs: [ + {expected: true, context: {url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + {conditions: []} + ], + expectedSchema: {}, + inputs: [ + {expected: true, context: {url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + {conditions: []}, + {conditions: []} + ], + expectedSchema: {}, + inputs: [ + {expected: true, context: {url: 'http://example.com/'}} + ] + }, + + // popupLevel tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'equal', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: {const: 0} + }, + required: ['depth'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 1, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'notEqual', + value: '0' + } + ] + } + ], + expectedSchema: { + not: [ + { + properties: { + depth: {const: 0} + }, + required: ['depth'] + } + ] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'lessThan', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'lessThanOrEqual', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + maximum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThanOrEqual', + value: '0' + } + ] + } + ], + expectedSchema: { + properties: { + depth: { + type: 'number', + minimum: 0 + } + }, + required: ['depth'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}} + ] + }, + + // url tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'url', + operator: 'matchDomain', + value: 'example.com' + } + ] + } + ], + expectedSchema: { + properties: { + domain: { + oneOf: [ + {const: 'example.com'} + ] + } + }, + required: ['domain'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example1.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example2.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, + {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'url', + operator: 'matchDomain', + value: 'example.com, example1.com, example2.com' + } + ] + } + ], + expectedSchema: { + properties: { + domain: { + oneOf: [ + {const: 'example.com'}, + {const: 'example1.com'}, + {const: 'example2.com'} + ] + } + }, + required: ['domain'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example3.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, + {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'url', + operator: 'matchRegExp', + value: '^http://example\\d?\\.com/[\\w\\W]*$' + } + ] + } + ], + expectedSchema: { + properties: { + url: { + type: 'string', + pattern: '^http://example\\d?\\.com/[\\w\\W]*$', + patternFlags: 'i' + } + }, + required: ['url'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example3.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com/example'}}, + {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}}, + {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}}, + {expected: false, context: {depth: 0, url: 'http://example-1.com/'}} + ] + }, + + // modifierKeys tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'are', + value: '' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + maxItems: 0, + minItems: 0 + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'are', + value: 'Alt, Shift' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'Alt'}}, + {contains: {const: 'Shift'}} + ] + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'areNot', + value: '' + } + ] + } + ], + expectedSchema: { + not: [ + { + properties: { + modifierKeys: { + type: 'array', + maxItems: 0, + minItems: 0 + } + }, + required: ['modifierKeys'] + } + ] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'areNot', + value: 'Alt, Shift' + } + ] + } + ], + expectedSchema: { + not: [ + { + properties: { + modifierKeys: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'Alt'}}, + {contains: {const: 'Shift'}} + ] + } + }, + required: ['modifierKeys'] + } + ] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'include', + value: '' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + minItems: 0 + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'include', + value: 'Alt, Shift' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + minItems: 2, + allOf: [ + {contains: {const: 'Alt'}}, + {contains: {const: 'Shift'}} + ] + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'notInclude', + value: '' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array' + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'modifierKeys', + operator: 'notInclude', + value: 'Alt, Shift' + } + ] + } + ], + expectedSchema: { + properties: { + modifierKeys: { + type: 'array', + not: [ + {contains: {const: 'Alt'}}, + {contains: {const: 'Shift'}} + ] + } + }, + required: ['modifierKeys'] + }, + inputs: [ + {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, + {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} + ] + }, + + // flags tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'are', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 0, + minItems: 0 + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: false, context: {flags: ['test1', 'test2']}}, + {expected: false, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'are', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: false, context: {}}, + {expected: false, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: false, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'areNot', + value: '' + } + ] + } + ], + expectedSchema: { + not: [ + { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 0, + minItems: 0 + } + } + } + ] + }, + inputs: [ + {expected: false, context: {}}, + {expected: false, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'areNot', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + not: [ + { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + } + ] + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: false, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'include', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + minItems: 0 + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'include', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + minItems: 2, + allOf: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: false, context: {}}, + {expected: false, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'notInclude', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array' + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'notInclude', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + not: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: false, context: {flags: ['test1', 'test2']}}, + {expected: false, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + + // Multiple conditions tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: '3' + } + ] + } + ], + expectedSchema: { + allOf: [ + { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 3 + } + }, + required: ['depth'] + } + ] + }, + inputs: [ + {expected: false, context: {depth: -2, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}}, + {expected: false, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 2, url: 'http://example.com/'}}, + {expected: false, context: {depth: 3, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: '3' + } + ] + }, + { + conditions: [ + { + type: 'popupLevel', + operator: 'equal', + value: '0' + } + ] + } + ], + expectedSchema: { + anyOf: [ + { + allOf: [ + { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 3 + } + }, + required: ['depth'] + } + ] + }, + { + properties: { + depth: {const: 0} + }, + required: ['depth'] + } + ] + }, + inputs: [ + {expected: false, context: {depth: -2, url: 'http://example.com/'}}, + {expected: false, context: {depth: -1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 2, url: 'http://example.com/'}}, + {expected: false, context: {depth: 3, url: 'http://example.com/'}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'popupLevel', + operator: 'greaterThan', + value: '0' + }, + { + type: 'popupLevel', + operator: 'lessThan', + value: '3' + } + ] + }, + { + conditions: [ + { + type: 'popupLevel', + operator: 'lessThanOrEqual', + value: '0' + }, + { + type: 'popupLevel', + operator: 'greaterThanOrEqual', + value: '-1' + } + ] + } + ], + expectedSchema: { + anyOf: [ + { + allOf: [ + { + properties: { + depth: { + type: 'number', + exclusiveMinimum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + exclusiveMaximum: 3 + } + }, + required: ['depth'] + } + ] + }, + { + allOf: [ + { + properties: { + depth: { + type: 'number', + maximum: 0 + } + }, + required: ['depth'] + }, + { + properties: { + depth: { + type: 'number', + minimum: -1 + } + }, + required: ['depth'] + } + ] + } + ] + }, + inputs: [ + {expected: false, context: {depth: -2, url: 'http://example.com/'}}, + {expected: true, context: {depth: -1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 0, url: 'http://example.com/'}}, + {expected: true, context: {depth: 1, url: 'http://example.com/'}}, + {expected: true, context: {depth: 2, url: 'http://example.com/'}}, + {expected: false, context: {depth: 3, url: 'http://example.com/'}} + ] + } + ]; + + for (const {conditionGroups, expectedSchema, inputs} of data) { + const profileConditionsUtil = new ProfileConditionsUtil(); + const schema = profileConditionsUtil.createSchema(conditionGroups); + if (typeof expectedSchema !== 'undefined') { + expect(schema.schema).toStrictEqual(expectedSchema); + } + if (Array.isArray(inputs)) { + for (const {expected, context} of inputs) { + const normalizedContext = profileConditionsUtil.normalizeContext(context); + const actual = schema.isValid(normalizedContext); + expect(actual).toStrictEqual(expected); + } + } + } + }); +} + + +function main() { + testNormalizeContext(); + testSchemas(); +} + +main(); diff --git a/test/test-all.js b/test/test-all.js deleted file mode 100644 index d187879a..00000000 --- a/test/test-all.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const fs = require('fs'); -const path = require('path'); -const {spawnSync} = require('child_process'); -const {getArgs} = require('../dev/util'); - - -function main() { - const args = getArgs(process.argv.slice(2), new Map([ - ['skip', []], - [null, []] - ])); - const directories = args.get(null); - const skip = new Set([__filename, ...args.get('skip')].map((value) => path.resolve(value))); - - const node = process.execPath; - const fileNamePattern = /\.js$/i; - - let first = true; - for (const directory of directories) { - const fileNames = fs.readdirSync(directory); - for (const fileName of fileNames) { - if (!fileNamePattern.test(fileName)) { continue; } - - const fullFileName = path.resolve(path.join(directory, fileName)); - if (skip.has(fullFileName)) { continue; } - - const stats = fs.lstatSync(fullFileName); - if (!stats.isFile()) { continue; } - - process.stdout.write(`${first ? '' : '\n'}Running ${fileName}...\n`); - first = false; - - const {error, status} = spawnSync(node, [fileName], {cwd: directory, stdio: 'inherit'}); - - if (status !== null && status !== 0) { - process.exit(status); - return; - } - if (error) { - throw error; - } - } - } - - process.exit(0); -} - - -if (require.main === module) { main(); } diff --git a/test/test-anki-note-builder.js b/test/test-anki-note-builder.js deleted file mode 100644 index ba937302..00000000 --- a/test/test-anki-note-builder.js +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2021-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const {JSDOM} = require('jsdom'); -const {testMain} = require('../dev/util'); -const {TranslatorVM} = require('../dev/translator-vm'); - - -function clone(value) { - return JSON.parse(JSON.stringify(value)); -} - -async function createVM() { - const dom = new JSDOM(); - const {Node, NodeFilter, document} = dom.window; - - const vm = new TranslatorVM({ - Node, - NodeFilter, - document, - location: new URL('https://yomichan.test/') - }); - - const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1'); - await vm.prepare(dictionaryDirectory, 'Test Dictionary 2'); - - vm.execute([ - 'js/data/anki-note-builder.js', - 'js/data/anki-util.js', - 'js/dom/sandbox/css-style-applier.js', - 'js/display/sandbox/pronunciation-generator.js', - 'js/display/sandbox/structured-content-generator.js', - 'js/templates/sandbox/anki-template-renderer.js', - 'js/templates/sandbox/anki-template-renderer-content-manager.js', - 'js/templates/sandbox/template-renderer.js', - 'js/templates/sandbox/template-renderer-media-provider.js', - 'lib/handlebars.min.js' - ]); - - const [ - JapaneseUtil, - AnkiNoteBuilder, - AnkiTemplateRenderer - ] = vm.get([ - 'JapaneseUtil', - 'AnkiNoteBuilder', - 'AnkiTemplateRenderer' - ]); - - class TemplateRendererProxy { - constructor() { - this._preparePromise = null; - this._ankiTemplateRenderer = new AnkiTemplateRenderer(); - } - - async render(template, data, type) { - await this._prepare(); - return await this._ankiTemplateRenderer.templateRenderer.render(template, data, type); - } - - async renderMulti(items) { - await this._prepare(); - return await this._serializeMulti(this._ankiTemplateRenderer.templateRenderer.renderMulti(items)); - } - - _prepare() { - if (this._preparePromise === null) { - this._preparePromise = this._prepareInternal(); - } - return this._preparePromise; - } - - async _prepareInternal() { - await this._ankiTemplateRenderer.prepare(); - } - - _serializeError(error) { - try { - if (typeof error === 'object' && error !== null) { - const result = { - name: error.name, - message: error.message, - stack: error.stack - }; - if (Object.prototype.hasOwnProperty.call(error, 'data')) { - result.data = error.data; - } - return result; - } - } catch (e) { - // NOP - } - return { - value: error, - hasValue: true - }; - } - - _serializeMulti(array) { - for (let i = 0, ii = array.length; i < ii; ++i) { - const value = array[i]; - const {error} = value; - if (typeof error !== 'undefined') { - value.error = this._serializeError(error); - } - } - return array; - } - } - vm.set({TemplateRendererProxy}); - - return {vm, AnkiNoteBuilder, JapaneseUtil}; -} - -function getFieldMarkers(type) { - switch (type) { - case 'terms': - return [ - 'audio', - 'clipboard-image', - 'clipboard-text', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'conjugation', - 'dictionary', - 'document-title', - 'expression', - 'frequencies', - 'furigana', - 'furigana-plain', - 'glossary', - 'glossary-brief', - 'glossary-no-dictionary', - 'part-of-speech', - 'pitch-accents', - 'pitch-accent-graphs', - 'pitch-accent-positions', - 'reading', - 'screenshot', - 'search-query', - 'selection-text', - 'sentence', - 'sentence-furigana', - 'tags', - 'url' - ]; - case 'kanji': - return [ - 'character', - 'clipboard-image', - 'clipboard-text', - 'cloze-body', - 'cloze-prefix', - 'cloze-suffix', - 'dictionary', - 'document-title', - 'glossary', - 'kunyomi', - 'onyomi', - 'screenshot', - 'search-query', - 'selection-text', - 'sentence', - 'sentence-furigana', - 'stroke-count', - 'tags', - 'url' - ]; - default: - return []; - } -} - -async function getRenderResults(dictionaryEntries, type, mode, template, AnkiNoteBuilder, JapaneseUtil, write) { - const markers = getFieldMarkers(type); - const fields = []; - for (const marker of markers) { - fields.push([marker, `{${marker}}`]); - } - - const japaneseUtil = new JapaneseUtil(null); - const clozePrefix = 'cloze-prefix'; - const clozeSuffix = 'cloze-suffix'; - const results = []; - for (const dictionaryEntry of dictionaryEntries) { - let source = ''; - switch (dictionaryEntry.type) { - case 'kanji': - source = dictionaryEntry.character; - break; - case 'term': - if (dictionaryEntry.headwords.length > 0 && dictionaryEntry.headwords[0].sources.length > 0) { - source = dictionaryEntry.headwords[0].sources[0].originalText; - } - break; - } - const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); - const context = { - url: 'url:', - sentence: { - text: `${clozePrefix}${source}${clozeSuffix}`, - offset: clozePrefix.length - }, - documentTitle: 'title', - query: 'query', - fullQuery: 'fullQuery' - }; - const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote({ - dictionaryEntry, - mode: null, - context, - template, - deckName: 'deckName', - modelName: 'modelName', - fields, - tags: ['yomichan'], - checkForDuplicates: true, - duplicateScope: 'collection', - duplicateScopeCheckAllModels: false, - resultOutputMode: mode, - glossaryLayoutMode: 'default', - compactTags: false - }); - if (!write) { - for (const error of errors) { - console.error(error); - } - assert.strictEqual(errors.length, 0); - } - results.push(noteFields); - } - - return results; -} - - -async function main() { - const write = (process.argv[2] === '--write'); - - const {vm, AnkiNoteBuilder, JapaneseUtil} = await createVM(); - - const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json'); - const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'})); - - const testResults1FilePath = path.join(__dirname, 'data', 'anki-note-builder-test-results.json'); - const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'})); - const actualResults1 = []; - - const template = fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'}); - - for (let i = 0, ii = tests.length; i < ii; ++i) { - const test = tests[i]; - const expected1 = expectedResults1[i]; - switch (test.func) { - case 'findTerms': - { - const {name, mode, text} = test; - const options = vm.buildOptions(optionsPresets, test.options); - const {dictionaryEntries} = clone(await vm.translator.findTerms(mode, text, options)); - const results = mode !== 'simple' ? clone(await getRenderResults(dictionaryEntries, 'terms', mode, template, AnkiNoteBuilder, JapaneseUtil, write)) : null; - actualResults1.push({name, results}); - if (!write) { - assert.deepStrictEqual(results, expected1.results); - } - } - break; - case 'findKanji': - { - const {name, text} = test; - const options = vm.buildOptions(optionsPresets, test.options); - const dictionaryEntries = clone(await vm.translator.findKanji(text, options)); - const results = clone(await getRenderResults(dictionaryEntries, 'kanji', null, template, AnkiNoteBuilder, JapaneseUtil, write)); - actualResults1.push({name, results}); - if (!write) { - assert.deepStrictEqual(results, expected1.results); - } - } - break; - } - } - - if (write) { - // Use 2 indent instead of 4 to save a bit of file size - fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'}); - } -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-build-libs.js b/test/test-build-libs.js deleted file mode 100644 index 496f43f8..00000000 --- a/test/test-build-libs.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const fs = require('fs'); -const assert = require('assert'); -const {getBuildTargets} = require('../dev/build-libs'); - -async function main() { - try { - for (const {path: path2, build} of getBuildTargets()) { - let expectedContent = await build(); - if (typeof expectedContent !== 'string') { - // Buffer - expectedContent = expectedContent.toString('utf8'); - } - const actualContent = fs.readFileSync(path2, {encoding: 'utf8'}); - assert.strictEqual(actualContent, expectedContent); - } - } catch (e) { - console.error(e); - process.exit(-1); - return; - } - process.exit(0); -} - -if (require.main === module) { main(); } diff --git a/test/test-cache-map.js b/test/test-cache-map.js deleted file mode 100644 index 863c6ace..00000000 --- a/test/test-cache-map.js +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - -const vm = new VM({console}); -vm.execute([ - 'js/general/cache-map.js' -]); -const CacheMap = vm.get('CacheMap'); - - -function testConstructor() { - const data = [ - [false, () => new CacheMap(0)], - [false, () => new CacheMap(1)], - [false, () => new CacheMap(Number.MAX_VALUE)], - [true, () => new CacheMap(-1)], - [true, () => new CacheMap(1.5)], - [true, () => new CacheMap(Number.NaN)], - [true, () => new CacheMap(Number.POSITIVE_INFINITY)], - [true, () => new CacheMap('a')] - ]; - - for (const [throws, create] of data) { - if (throws) { - assert.throws(create); - } else { - assert.doesNotThrow(create); - } - } -} - -function testApi() { - const data = [ - { - maxSize: 1, - expectedSize: 0, - calls: [] - }, - { - maxSize: 10, - expectedSize: 1, - calls: [ - {func: 'get', args: ['a1-b-c'], returnValue: void 0}, - {func: 'has', args: ['a1-b-c'], returnValue: false}, - {func: 'set', args: ['a1-b-c', 32], returnValue: void 0}, - {func: 'get', args: ['a1-b-c'], returnValue: 32}, - {func: 'has', args: ['a1-b-c'], returnValue: true} - ] - }, - { - maxSize: 10, - expectedSize: 2, - calls: [ - {func: 'set', args: ['a1-b-c', 32], returnValue: void 0}, - {func: 'get', args: ['a1-b-c'], returnValue: 32}, - {func: 'set', args: ['a1-b-c', 64], returnValue: void 0}, - {func: 'get', args: ['a1-b-c'], returnValue: 64}, - {func: 'set', args: ['a2-b-c', 96], returnValue: void 0}, - {func: 'get', args: ['a2-b-c'], returnValue: 96} - ] - }, - { - maxSize: 2, - expectedSize: 2, - calls: [ - {func: 'has', args: ['a1-b-c'], returnValue: false}, - {func: 'has', args: ['a2-b-c'], returnValue: false}, - {func: 'has', args: ['a3-b-c'], returnValue: false}, - {func: 'set', args: ['a1-b-c', 1], returnValue: void 0}, - {func: 'has', args: ['a1-b-c'], returnValue: true}, - {func: 'has', args: ['a2-b-c'], returnValue: false}, - {func: 'has', args: ['a3-b-c'], returnValue: false}, - {func: 'set', args: ['a2-b-c', 2], returnValue: void 0}, - {func: 'has', args: ['a1-b-c'], returnValue: true}, - {func: 'has', args: ['a2-b-c'], returnValue: true}, - {func: 'has', args: ['a3-b-c'], returnValue: false}, - {func: 'set', args: ['a3-b-c', 3], returnValue: void 0}, - {func: 'has', args: ['a1-b-c'], returnValue: false}, - {func: 'has', args: ['a2-b-c'], returnValue: true}, - {func: 'has', args: ['a3-b-c'], returnValue: true} - ] - } - ]; - - for (const {maxSize, expectedSize, calls} of data) { - const cache = new CacheMap(maxSize); - assert.strictEqual(cache.maxSize, maxSize); - for (const call of calls) { - const {func, args} = call; - let returnValue; - switch (func) { - case 'get': returnValue = cache.get(...args); break; - case 'set': returnValue = cache.set(...args); break; - case 'has': returnValue = cache.has(...args); break; - case 'clear': returnValue = cache.clear(...args); break; - } - if (Object.prototype.hasOwnProperty.call(call, 'returnValue')) { - const {returnValue: expectedReturnValue} = call; - assert.deepStrictEqual(returnValue, expectedReturnValue); - } - } - assert.strictEqual(cache.size, expectedSize); - } -} - - -function main() { - testConstructor(); - testApi(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-core.js b/test/test-core.js deleted file mode 100644 index b42f8cf2..00000000 --- a/test/test-core.js +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - -const vm = new VM(); -vm.execute([ - 'js/core.js' -]); -const [DynamicProperty, deepEqual] = vm.get(['DynamicProperty', 'deepEqual']); - - -function testDynamicProperty() { - const data = [ - { - initialValue: 0, - operations: [ - { - operation: null, - expectedDefaultValue: 0, - expectedValue: 0, - expectedOverrideCount: 0, - expeectedEventOccurred: false - }, - { - operation: 'set.defaultValue', - args: [1], - expectedDefaultValue: 1, - expectedValue: 1, - expectedOverrideCount: 0, - expeectedEventOccurred: true - }, - { - operation: 'set.defaultValue', - args: [1], - expectedDefaultValue: 1, - expectedValue: 1, - expectedOverrideCount: 0, - expeectedEventOccurred: false - }, - { - operation: 'set.defaultValue', - args: [0], - expectedDefaultValue: 0, - expectedValue: 0, - expectedOverrideCount: 0, - expeectedEventOccurred: true - }, - { - operation: 'setOverride', - args: [8], - expectedDefaultValue: 0, - expectedValue: 8, - expectedOverrideCount: 1, - expeectedEventOccurred: true - }, - { - operation: 'setOverride', - args: [16], - expectedDefaultValue: 0, - expectedValue: 8, - expectedOverrideCount: 2, - expeectedEventOccurred: false - }, - { - operation: 'setOverride', - args: [32, 1], - expectedDefaultValue: 0, - expectedValue: 32, - expectedOverrideCount: 3, - expeectedEventOccurred: true - }, - { - operation: 'setOverride', - args: [64, -1], - expectedDefaultValue: 0, - expectedValue: 32, - expectedOverrideCount: 4, - expeectedEventOccurred: false - }, - { - operation: 'clearOverride', - args: [-4], - expectedDefaultValue: 0, - expectedValue: 32, - expectedOverrideCount: 3, - expeectedEventOccurred: false - }, - { - operation: 'clearOverride', - args: [-3], - expectedDefaultValue: 0, - expectedValue: 32, - expectedOverrideCount: 2, - expeectedEventOccurred: false - }, - { - operation: 'clearOverride', - args: [-2], - expectedDefaultValue: 0, - expectedValue: 64, - expectedOverrideCount: 1, - expeectedEventOccurred: true - }, - { - operation: 'clearOverride', - args: [-1], - expectedDefaultValue: 0, - expectedValue: 0, - expectedOverrideCount: 0, - expeectedEventOccurred: true - } - ] - } - ]; - - for (const {initialValue, operations} of data) { - const property = new DynamicProperty(initialValue); - const overrideTokens = []; - let eventOccurred = false; - const onChange = () => { eventOccurred = true; }; - property.on('change', onChange); - for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) { - eventOccurred = false; - switch (operation) { - case 'set.defaultValue': property.defaultValue = args[0]; break; - case 'setOverride': overrideTokens.push(property.setOverride(...args)); break; - case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break; - } - assert.strictEqual(eventOccurred, expeectedEventOccurred); - assert.strictEqual(property.defaultValue, expectedDefaultValue); - assert.strictEqual(property.value, expectedValue); - assert.strictEqual(property.overrideCount, expectedOverrideCount); - } - property.off('change', onChange); - } -} - -function testDeepEqual() { - const data = [ - // Simple tests - { - value1: 0, - value2: 0, - expected: true - }, - { - value1: null, - value2: null, - expected: true - }, - { - value1: 'test', - value2: 'test', - expected: true - }, - { - value1: true, - value2: true, - expected: true - }, - { - value1: 0, - value2: 1, - expected: false - }, - { - value1: null, - value2: false, - expected: false - }, - { - value1: 'test1', - value2: 'test2', - expected: false - }, - { - value1: true, - value2: false, - expected: false - }, - - // Simple object tests - { - value1: {}, - value2: {}, - expected: true - }, - { - value1: {}, - value2: [], - expected: false - }, - { - value1: [], - value2: [], - expected: true - }, - { - value1: {}, - value2: null, - expected: false - }, - - // Complex object tests - { - value1: [1], - value2: [], - expected: false - }, - { - value1: [1], - value2: [1], - expected: true - }, - { - value1: [1], - value2: [2], - expected: false - }, - - { - value1: {}, - value2: {test: 1}, - expected: false - }, - { - value1: {test: 1}, - value2: {test: 1}, - expected: true - }, - { - value1: {test: 1}, - value2: {test: {test2: false}}, - expected: false - }, - { - value1: {test: {test2: true}}, - value2: {test: {test2: false}}, - expected: false - }, - { - value1: {test: {test2: [true]}}, - value2: {test: {test2: [true]}}, - expected: true - }, - - // Recursive - { - value1: (() => { const x = {}; x.x = x; return x; })(), - value2: (() => { const x = {}; x.x = x; return x; })(), - expected: false - } - ]; - - let index = 0; - for (const {value1, value2, expected} of data) { - const actual1 = deepEqual(value1, value2); - assert.strictEqual(actual1, expected, `Failed for test ${index}`); - - const actual2 = deepEqual(value2, value1); - assert.strictEqual(actual2, expected, `Failed for test ${index}`); - - ++index; - } -} - - -function main() { - testDynamicProperty(); - testDeepEqual(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-database.js b/test/test-database.js deleted file mode 100644 index c4cd504a..00000000 --- a/test/test-database.js +++ /dev/null @@ -1,887 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const path = require('path'); -const assert = require('assert'); -const {createDictionaryArchive, testMain} = require('../dev/util'); -const {DatabaseVM, DatabaseVMDictionaryImporterMediaLoader} = require('../dev/database-vm'); - - -const vm = new DatabaseVM(); -vm.execute([ - 'js/core.js', - 'js/general/cache-map.js', - 'js/data/json-schema.js', - 'js/media/media-util.js', - 'js/language/dictionary-importer.js', - 'js/data/database.js', - 'js/language/dictionary-database.js' -]); -const DictionaryImporter = vm.get('DictionaryImporter'); -const DictionaryDatabase = vm.get('DictionaryDatabase'); - - -function createTestDictionaryArchive(dictionary, dictionaryName) { - const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary); - return createDictionaryArchive(dictionaryDirectory, dictionaryName); -} - - -function createDictionaryImporter(onProgress) { - const dictionaryImporterMediaLoader = new DatabaseVMDictionaryImporterMediaLoader(); - return new DictionaryImporter(dictionaryImporterMediaLoader, (...args) => { - const {stepIndex, stepCount, index, count} = args[0]; - assert.ok(stepIndex < stepCount); - assert.ok(index <= count); - if (typeof onProgress === 'function') { - onProgress(...args); - } - }); -} - - -function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) { - return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0); -} - -function countDictionaryDatabaseEntriesWithReading(dictionaryDatabaseEntries, reading) { - return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0); -} - -function countMetasWithMode(metas, mode) { - return metas.reduce((i, v) => (i + (v.mode === mode ? 1 : 0)), 0); -} - -function countKanjiWithCharacter(kanji, character) { - return kanji.reduce((i, v) => (i + (v.character === character ? 1 : 0)), 0); -} - - -function clearDatabase(timeout) { - return new Promise((resolve, reject) => { - let timer = setTimeout(() => { - timer = null; - reject(new Error(`clearDatabase failed to resolve after ${timeout}ms`)); - }, timeout); - - (async () => { - const indexedDB = vm.indexedDB; - for (const {name} of await indexedDB.databases()) { - await new Promise((resolve2, reject2) => { - const request = indexedDB.deleteDatabase(name); - request.onerror = (e) => reject2(e); - request.onsuccess = () => resolve2(); - }); - } - if (timer !== null) { - clearTimeout(timer); - } - resolve(); - })(); - }); -} - - -async function testDatabase1() { - // Load dictionary data - const testDictionary = createTestDictionaryArchive('valid-dictionary1'); - const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); - const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); - - const title = testDictionaryIndex.title; - const titles = new Map([ - [title, {priority: 0, allowSecondarySearches: false}] - ]); - - // Setup iteration data - const iterations = [ - { - cleanup: async () => { - // Test purge - await dictionaryDatabase.purge(); - await testDatabaseEmpty1(dictionaryDatabase); - } - }, - { - cleanup: async () => { - // Test deleteDictionary - let progressEvent = false; - await dictionaryDatabase.deleteDictionary( - title, - 1000, - () => { - progressEvent = true; - } - ); - assert.ok(progressEvent); - - await testDatabaseEmpty1(dictionaryDatabase); - } - }, - { - cleanup: async () => {} - } - ]; - - // Setup database - const dictionaryDatabase = new DictionaryDatabase(); - await dictionaryDatabase.prepare(); - - for (const {cleanup} of iterations) { - const expectedSummary = { - title, - revision: 'test', - sequenced: true, - version: 3, - importDate: 0, - prefixWildcardsSupported: true, - counts: { - kanji: {total: 2}, - kanjiMeta: {total: 6, freq: 6}, - media: {total: 4}, - tagMeta: {total: 15}, - termMeta: {total: 38, freq: 31, pitch: 7}, - terms: {total: 21} - } - }; - - // Import data - let progressEvent = false; - const dictionaryImporter = createDictionaryImporter(() => { progressEvent = true; }); - const {result, errors} = await dictionaryImporter.importDictionary( - dictionaryDatabase, - testDictionarySource, - {prefixWildcardsSupported: true} - ); - expectedSummary.importDate = result.importDate; - vm.assert.deepStrictEqual(errors, []); - vm.assert.deepStrictEqual(result, expectedSummary); - assert.ok(progressEvent); - - // Get info summary - const info = await dictionaryDatabase.getDictionaryInfo(); - vm.assert.deepStrictEqual(info, [expectedSummary]); - - // Get counts - const counts = await dictionaryDatabase.getDictionaryCounts( - info.map((v) => v.title), - true - ); - vm.assert.deepStrictEqual(counts, { - counts: [{kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}], - total: {kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4} - }); - - // Test find* functions - await testFindTermsBulkTest1(dictionaryDatabase, titles); - await testTindTermsExactBulk1(dictionaryDatabase, titles); - await testFindTermsBySequenceBulk1(dictionaryDatabase, title); - await testFindTermMetaBulk1(dictionaryDatabase, titles); - await testFindKanjiBulk1(dictionaryDatabase, titles); - await testFindKanjiMetaBulk1(dictionaryDatabase, titles); - await testFindTagForTitle1(dictionaryDatabase, title); - - // Cleanup - await cleanup(); - } - - await dictionaryDatabase.close(); -} - -async function testDatabaseEmpty1(database) { - const info = await database.getDictionaryInfo(); - vm.assert.deepStrictEqual(info, []); - - const counts = await database.getDictionaryCounts([], true); - vm.assert.deepStrictEqual(counts, { - counts: [], - total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0, media: 0} - }); -} - -async function testFindTermsBulkTest1(database, titles) { - const data = [ - { - inputs: [ - { - matchType: null, - termList: ['打', '打つ', '打ち込む'] - }, - { - matchType: null, - termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ'] - }, - { - matchType: 'prefix', - termList: ['打'] - } - ], - expectedResults: { - total: 10, - terms: [ - ['打', 2], - ['打つ', 4], - ['打ち込む', 4] - ], - readings: [ - ['だ', 1], - ['ダース', 1], - ['うつ', 2], - ['ぶつ', 2], - ['うちこむ', 2], - ['ぶちこむ', 2] - ] - } - }, - { - inputs: [ - { - matchType: null, - termList: ['込む'] - } - ], - expectedResults: { - total: 0, - terms: [], - readings: [] - } - }, - { - inputs: [ - { - matchType: 'suffix', - termList: ['込む'] - } - ], - expectedResults: { - total: 4, - terms: [ - ['打ち込む', 4] - ], - readings: [ - ['うちこむ', 2], - ['ぶちこむ', 2] - ] - } - }, - { - inputs: [ - { - matchType: null, - termList: [] - } - ], - expectedResults: { - total: 0, - terms: [], - readings: [] - } - } - ]; - - for (const {inputs, expectedResults} of data) { - for (const {termList, matchType} of inputs) { - const results = await database.findTermsBulk(termList, titles, matchType); - assert.strictEqual(results.length, expectedResults.total); - for (const [term, count] of expectedResults.terms) { - assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count); - } - for (const [reading, count] of expectedResults.readings) { - assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count); - } - } - } -} - -async function testTindTermsExactBulk1(database, titles) { - const data = [ - { - inputs: [ - { - termList: [ - {term: '打', reading: 'だ'}, - {term: '打つ', reading: 'うつ'}, - {term: '打ち込む', reading: 'うちこむ'} - ] - } - ], - expectedResults: { - total: 5, - terms: [ - ['打', 1], - ['打つ', 2], - ['打ち込む', 2] - ], - readings: [ - ['だ', 1], - ['うつ', 2], - ['うちこむ', 2] - ] - } - }, - { - inputs: [ - { - termList: [ - {term: '打', reading: 'だ?'}, - {term: '打つ', reading: 'うつ?'}, - {term: '打ち込む', reading: 'うちこむ?'} - ] - } - ], - expectedResults: { - total: 0, - terms: [], - readings: [] - } - }, - { - inputs: [ - { - termList: [ - {term: '打つ', reading: 'うつ'}, - {term: '打つ', reading: 'ぶつ'} - ] - } - ], - expectedResults: { - total: 4, - terms: [ - ['打つ', 4] - ], - readings: [ - ['うつ', 2], - ['ぶつ', 2] - ] - } - }, - { - inputs: [ - { - termList: [ - {term: '打つ', reading: 'うちこむ'} - ] - } - ], - expectedResults: { - total: 0, - terms: [], - readings: [] - } - }, - { - inputs: [ - { - termList: [] - } - ], - expectedResults: { - total: 0, - terms: [], - readings: [] - } - } - ]; - - for (const {inputs, expectedResults} of data) { - for (const {termList} of inputs) { - const results = await database.findTermsExactBulk(termList, titles); - assert.strictEqual(results.length, expectedResults.total); - for (const [term, count] of expectedResults.terms) { - assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count); - } - for (const [reading, count] of expectedResults.readings) { - assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count); - } - } - } -} - -async function testFindTermsBySequenceBulk1(database, mainDictionary) { - const data = [ - { - inputs: [ - { - sequenceList: [1, 2, 3, 4, 5] - } - ], - expectedResults: { - total: 11, - terms: [ - ['打', 2], - ['打つ', 4], - ['打ち込む', 4], - ['画像', 1] - ], - readings: [ - ['だ', 1], - ['ダース', 1], - ['うつ', 2], - ['ぶつ', 2], - ['うちこむ', 2], - ['ぶちこむ', 2], - ['がぞう', 1] - ] - } - }, - { - inputs: [ - { - sequenceList: [1] - } - ], - expectedResults: { - total: 1, - terms: [ - ['打', 1] - ], - readings: [ - ['だ', 1] - ] - } - }, - { - inputs: [ - { - sequenceList: [2] - } - ], - expectedResults: { - total: 1, - terms: [ - ['打', 1] - ], - readings: [ - ['ダース', 1] - ] - } - }, - { - inputs: [ - { - sequenceList: [3] - } - ], - expectedResults: { - total: 4, - terms: [ - ['打つ', 4] - ], - readings: [ - ['うつ', 2], - ['ぶつ', 2] - ] - } - }, - { - inputs: [ - { - sequenceList: [4] - } - ], - expectedResults: { - total: 4, - terms: [ - ['打ち込む', 4] - ], - readings: [ - ['うちこむ', 2], - ['ぶちこむ', 2] - ] - } - }, - { - inputs: [ - { - sequenceList: [5] - } - ], - expectedResults: { - total: 1, - terms: [ - ['画像', 1] - ], - readings: [ - ['がぞう', 1] - ] - } - }, - { - inputs: [ - { - sequenceList: [-1] - } - ], - expectedResults: { - total: 0, - terms: [], - readings: [] - } - }, - { - inputs: [ - { - sequenceList: [] - } - ], - expectedResults: { - total: 0, - terms: [], - readings: [] - } - } - ]; - - for (const {inputs, expectedResults} of data) { - for (const {sequenceList} of inputs) { - const results = await database.findTermsBySequenceBulk(sequenceList.map((query) => ({query, dictionary: mainDictionary}))); - assert.strictEqual(results.length, expectedResults.total); - for (const [term, count] of expectedResults.terms) { - assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count); - } - for (const [reading, count] of expectedResults.readings) { - assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count); - } - } - } -} - -async function testFindTermMetaBulk1(database, titles) { - const data = [ - { - inputs: [ - { - termList: ['打'] - } - ], - expectedResults: { - total: 11, - modes: [ - ['freq', 11] - ] - } - }, - { - inputs: [ - { - termList: ['打つ'] - } - ], - expectedResults: { - total: 10, - modes: [ - ['freq', 10] - ] - } - }, - { - inputs: [ - { - termList: ['打ち込む'] - } - ], - expectedResults: { - total: 12, - modes: [ - ['freq', 10], - ['pitch', 2] - ] - } - }, - { - inputs: [ - { - termList: ['?'] - } - ], - expectedResults: { - total: 0, - modes: [] - } - } - ]; - - for (const {inputs, expectedResults} of data) { - for (const {termList} of inputs) { - const results = await database.findTermMetaBulk(termList, titles); - assert.strictEqual(results.length, expectedResults.total); - for (const [mode, count] of expectedResults.modes) { - assert.strictEqual(countMetasWithMode(results, mode), count); - } - } - } -} - -async function testFindKanjiBulk1(database, titles) { - const data = [ - { - inputs: [ - { - kanjiList: ['打'] - } - ], - expectedResults: { - total: 1, - kanji: [ - ['打', 1] - ] - } - }, - { - inputs: [ - { - kanjiList: ['込'] - } - ], - expectedResults: { - total: 1, - kanji: [ - ['込', 1] - ] - } - }, - { - inputs: [ - { - kanjiList: ['?'] - } - ], - expectedResults: { - total: 0, - kanji: [] - } - } - ]; - - for (const {inputs, expectedResults} of data) { - for (const {kanjiList} of inputs) { - const results = await database.findKanjiBulk(kanjiList, titles); - assert.strictEqual(results.length, expectedResults.total); - for (const [kanji, count] of expectedResults.kanji) { - assert.strictEqual(countKanjiWithCharacter(results, kanji), count); - } - } - } -} - -async function testFindKanjiMetaBulk1(database, titles) { - const data = [ - { - inputs: [ - { - kanjiList: ['打'] - } - ], - expectedResults: { - total: 3, - modes: [ - ['freq', 3] - ] - } - }, - { - inputs: [ - { - kanjiList: ['込'] - } - ], - expectedResults: { - total: 3, - modes: [ - ['freq', 3] - ] - } - }, - { - inputs: [ - { - kanjiList: ['?'] - } - ], - expectedResults: { - total: 0, - modes: [] - } - } - ]; - - for (const {inputs, expectedResults} of data) { - for (const {kanjiList} of inputs) { - const results = await database.findKanjiMetaBulk(kanjiList, titles); - assert.strictEqual(results.length, expectedResults.total); - for (const [mode, count] of expectedResults.modes) { - assert.strictEqual(countMetasWithMode(results, mode), count); - } - } - } -} - -async function testFindTagForTitle1(database, title) { - const data = [ - { - inputs: [ - { - name: 'E1' - } - ], - expectedResults: { - value: {category: 'default', dictionary: title, name: 'E1', notes: 'example tag 1', order: 0, score: 0} - } - }, - { - inputs: [ - { - name: 'K1' - } - ], - expectedResults: { - value: {category: 'default', dictionary: title, name: 'K1', notes: 'example kanji tag 1', order: 0, score: 0} - } - }, - { - inputs: [ - { - name: 'kstat1' - } - ], - expectedResults: { - value: {category: 'class', dictionary: title, name: 'kstat1', notes: 'kanji stat 1', order: 0, score: 0} - } - }, - { - inputs: [ - { - name: 'invalid' - } - ], - expectedResults: { - value: null - } - } - ]; - - for (const {inputs, expectedResults} of data) { - for (const {name} of inputs) { - const result = await database.findTagForTitle(name, title); - vm.assert.deepStrictEqual(result, expectedResults.value); - } - } -} - - -async function testDatabase2() { - // Load dictionary data - const testDictionary = createTestDictionaryArchive('valid-dictionary1'); - const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); - const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string')); - - const title = testDictionaryIndex.title; - const titles = new Map([ - [title, {priority: 0, allowSecondarySearches: false}] - ]); - - // Setup database - const dictionaryDatabase = new DictionaryDatabase(); - - // Error: not prepared - await assert.rejects(async () => await dictionaryDatabase.deleteDictionary(title, 1000)); - await assert.rejects(async () => await dictionaryDatabase.findTermsBulk(['?'], titles, null)); - await assert.rejects(async () => await dictionaryDatabase.findTermsExactBulk([{term: '?', reading: '?'}], titles)); - await assert.rejects(async () => await dictionaryDatabase.findTermsBySequenceBulk([{query: 1, dictionary: title}])); - await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles)); - await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles)); - await assert.rejects(async () => await dictionaryDatabase.findKanjiBulk(['?'], titles)); - await assert.rejects(async () => await dictionaryDatabase.findKanjiMetaBulk(['?'], titles)); - await assert.rejects(async () => await dictionaryDatabase.findTagForTitle('tag', title)); - await assert.rejects(async () => await dictionaryDatabase.getDictionaryInfo()); - await assert.rejects(async () => await dictionaryDatabase.getDictionaryCounts(titles, true)); - await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})); - - await dictionaryDatabase.prepare(); - - // Error: already prepared - await assert.rejects(async () => await dictionaryDatabase.prepare()); - - await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {}); - - // Error: dictionary already imported - await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {})); - - await dictionaryDatabase.close(); -} - - -async function testDatabase3() { - const invalidDictionaries = [ - 'invalid-dictionary1', - 'invalid-dictionary2', - 'invalid-dictionary3', - 'invalid-dictionary4', - 'invalid-dictionary5', - 'invalid-dictionary6' - ]; - - // Setup database - const dictionaryDatabase = new DictionaryDatabase(); - await dictionaryDatabase.prepare(); - - for (const invalidDictionary of invalidDictionaries) { - const testDictionary = createTestDictionaryArchive(invalidDictionary); - const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'}); - - let error = null; - try { - await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, {}); - } catch (e) { - error = e; - } - - if (error === null) { - assert.ok(false, `Expected an error while importing ${invalidDictionary}`); - } else { - const prefix = 'Dictionary has invalid data'; - const message = error.message; - assert.ok(typeof message, 'string'); - assert.ok(message.startsWith(prefix), `Expected error message to start with '${prefix}': ${message}`); - } - } - - await dictionaryDatabase.close(); -} - - -async function main() { - const clearTimeout = 5000; - try { - await testDatabase1(); - await clearDatabase(clearTimeout); - - await testDatabase2(); - await clearDatabase(clearTimeout); - - await testDatabase3(); - await clearDatabase(clearTimeout); - } catch (e) { - console.log(e); - process.exit(-1); - throw e; - } -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-document-util.js b/test/test-document-util.js deleted file mode 100644 index a2458b61..00000000 --- a/test/test-document-util.js +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const {JSDOM} = require('jsdom'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - - -// DOMRect class definition -class DOMRect { - constructor(x, y, width, height) { - this._x = x; - this._y = y; - this._width = width; - this._height = height; - } - - get x() { return this._x; } - get y() { return this._y; } - get width() { return this._width; } - get height() { return this._height; } - get left() { return this._x + Math.min(0, this._width); } - get right() { return this._x + Math.max(0, this._width); } - get top() { return this._y + Math.min(0, this._height); } - get bottom() { return this._y + Math.max(0, this._height); } -} - - -function createJSDOM(fileName) { - const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); - const dom = new JSDOM(domSource); - const document = dom.window.document; - const window = dom.window; - - // Define innerText setter as an alias for textContent setter - Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', { - set(value) { this.textContent = value; } - }); - - // Placeholder for feature detection - document.caretRangeFromPoint = () => null; - - return dom; -} - -function querySelectorChildOrSelf(element, selector) { - return selector ? element.querySelector(selector) : element; -} - -function getChildTextNodeOrSelf(dom, node) { - if (node === null) { return null; } - const Node = dom.window.Node; - const childNode = node.firstChild; - return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node); -} - -function getPrototypeOfOrNull(value) { - try { - return Object.getPrototypeOf(value); - } catch (e) { - return null; - } -} - -function findImposterElement(document) { - // Finds the imposter element based on it's z-index style - return document.querySelector('div[style*="2147483646"]>*'); -} - - -async function testDocument1() { - const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-document1.html')); - const window = dom.window; - const document = window.document; - const Node = window.Node; - const Range = window.Range; - - const vm = new VM({document, window, Range, Node}); - vm.execute([ - 'js/data/sandbox/string-util.js', - 'js/dom/dom-text-scanner.js', - 'js/dom/text-source-range.js', - 'js/dom/text-source-element.js', - 'js/dom/document-util.js' - ]); - const [DOMTextScanner, TextSourceRange, TextSourceElement, DocumentUtil] = vm.get([ - 'DOMTextScanner', - 'TextSourceRange', - 'TextSourceElement', - 'DocumentUtil' - ]); - - try { - await testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}); - await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}); - } finally { - window.close(); - } -} - -async function testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}) { - const document = dom.window.document; - - for (const testElement of document.querySelectorAll('.test[data-test-type=scan]')) { - // Get test parameters - let { - elementFromPointSelector, - caretRangeFromPointSelector, - startNodeSelector, - startOffset, - endNodeSelector, - endOffset, - resultType, - sentenceScanExtent, - sentence, - hasImposter, - terminateAtNewlines - } = testElement.dataset; - - const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector); - const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector); - const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector)); - const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector)); - - startOffset = parseInt(startOffset, 10); - endOffset = parseInt(endOffset, 10); - sentenceScanExtent = parseInt(sentenceScanExtent, 10); - terminateAtNewlines = (terminateAtNewlines !== 'false'); - - assert.notStrictEqual(elementFromPointValue, null); - assert.notStrictEqual(caretRangeFromPointValue, null); - assert.notStrictEqual(startNode, null); - assert.notStrictEqual(endNode, null); - - // Setup functions - document.elementFromPoint = () => elementFromPointValue; - - document.caretRangeFromPoint = (x, y) => { - const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document)); - assert.strictEqual(!!imposter, hasImposter === 'true'); - - const range = document.createRange(); - range.setStart(imposter ? imposter : startNode, startOffset); - range.setEnd(imposter ? imposter : startNode, endOffset); - - // Override getClientRects to return a rect guaranteed to contain (x, y) - range.getClientRects = () => [new DOMRect(x - 1, y - 1, 2, 2)]; - return range; - }; - - // Test docRangeFromPoint - const source = DocumentUtil.getRangeFromPoint(0, 0, { - deepContentScan: false, - normalizeCssZoom: true - }); - switch (resultType) { - case 'TextSourceRange': - assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype); - break; - case 'TextSourceElement': - assert.strictEqual(getPrototypeOfOrNull(source), TextSourceElement.prototype); - break; - case 'null': - assert.strictEqual(source, null); - break; - default: - assert.ok(false); - break; - } - if (source === null) { continue; } - - // Sentence info - const terminatorString = '…。..??!!'; - const terminatorMap = new Map(); - for (const char of terminatorString) { - terminatorMap.set(char, [false, true]); - } - const quoteArray = [['「', '」'], ['『', '』'], ['\'', '\''], ['"', '"']]; - const forwardQuoteMap = new Map(); - const backwardQuoteMap = new Map(); - for (const [char1, char2] of quoteArray) { - forwardQuoteMap.set(char1, [char2, false]); - backwardQuoteMap.set(char2, [char1, false]); - } - - // Test docSentenceExtract - const sentenceActual = DocumentUtil.extractSentence( - source, - false, - sentenceScanExtent, - terminateAtNewlines, - terminatorMap, - forwardQuoteMap, - backwardQuoteMap - ).text; - assert.strictEqual(sentenceActual, sentence); - - // Clean - source.cleanup(); - } -} - -async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) { - const document = dom.window.document; - - for (const testElement of document.querySelectorAll('.test[data-test-type=text-source-range-seek]')) { - // Get test parameters - let { - seekNodeSelector, - seekNodeIsText, - seekOffset, - seekLength, - seekDirection, - expectedResultNodeSelector, - expectedResultNodeIsText, - expectedResultOffset, - expectedResultContent - } = testElement.dataset; - - seekOffset = parseInt(seekOffset, 10); - seekLength = parseInt(seekLength, 10); - expectedResultOffset = parseInt(expectedResultOffset, 10); - - let seekNode = testElement.querySelector(seekNodeSelector); - if (seekNodeIsText === 'true') { - seekNode = seekNode.firstChild; - } - - let expectedResultNode = testElement.querySelector(expectedResultNodeSelector); - if (expectedResultNodeIsText === 'true') { - expectedResultNode = expectedResultNode.firstChild; - } - - const {node, offset, content} = ( - seekDirection === 'forward' ? - new DOMTextScanner(seekNode, seekOffset, true, false).seek(seekLength) : - new DOMTextScanner(seekNode, seekOffset, true, false).seek(-seekLength) - ); - - assert.strictEqual(node, expectedResultNode); - assert.strictEqual(offset, expectedResultOffset); - assert.strictEqual(content, expectedResultContent); - } -} - - -async function main() { - await testDocument1(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-hotkey-util.js b/test/test-hotkey-util.js deleted file mode 100644 index 34f1eb26..00000000 --- a/test/test-hotkey-util.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - - -function clone(value) { - return JSON.parse(JSON.stringify(value)); -} - -function createHotkeyUtil() { - const vm = new VM(); - vm.execute(['js/input/hotkey-util.js']); - const [HotkeyUtil] = vm.get(['HotkeyUtil']); - return new HotkeyUtil(); -} - - -function testCommandConversions() { - const data = [ - {os: 'win', command: 'Alt+F', expectedCommand: 'Alt+F', expectedInput: {key: 'KeyF', modifiers: ['alt']}}, - {os: 'win', command: 'F1', expectedCommand: 'F1', expectedInput: {key: 'F1', modifiers: []}}, - - {os: 'win', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, - {os: 'win', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, - {os: 'win', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, - - {os: 'mac', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, - {os: 'mac', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'MacCtrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, - {os: 'mac', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}, - - {os: 'linux', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, - {os: 'linux', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}}, - {os: 'linux', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}} - ]; - - const hotkeyUtil = createHotkeyUtil(); - for (const {command, os, expectedInput, expectedCommand} of data) { - hotkeyUtil.os = os; - const input = clone(hotkeyUtil.convertCommandToInput(command)); - assert.deepStrictEqual(input, expectedInput); - const command2 = hotkeyUtil.convertInputToCommand(input.key, input.modifiers); - assert.deepStrictEqual(command2, expectedCommand); - } -} - -function testDisplayNames() { - const data = [ - {os: 'win', key: null, modifiers: [], expected: ''}, - {os: 'win', key: 'KeyF', modifiers: [], expected: 'F'}, - {os: 'win', key: 'F1', modifiers: [], expected: 'F1'}, - {os: 'win', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, - {os: 'win', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, - {os: 'win', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, - {os: 'win', key: null, modifiers: ['alt'], expected: 'Alt'}, - {os: 'win', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, - {os: 'win', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, - {os: 'win', key: null, modifiers: ['shift'], expected: 'Shift'}, - {os: 'win', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, - {os: 'win', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, - {os: 'win', key: null, modifiers: ['meta'], expected: 'Windows'}, - {os: 'win', key: 'KeyF', modifiers: ['meta'], expected: 'Windows + F'}, - {os: 'win', key: 'F1', modifiers: ['meta'], expected: 'Windows + F1'}, - {os: 'win', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, - {os: 'win', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, - {os: 'win', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, - - {os: 'mac', key: null, modifiers: [], expected: ''}, - {os: 'mac', key: 'KeyF', modifiers: [], expected: 'F'}, - {os: 'mac', key: 'F1', modifiers: [], expected: 'F1'}, - {os: 'mac', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, - {os: 'mac', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, - {os: 'mac', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, - {os: 'mac', key: null, modifiers: ['alt'], expected: 'Opt'}, - {os: 'mac', key: 'KeyF', modifiers: ['alt'], expected: 'Opt + F'}, - {os: 'mac', key: 'F1', modifiers: ['alt'], expected: 'Opt + F1'}, - {os: 'mac', key: null, modifiers: ['shift'], expected: 'Shift'}, - {os: 'mac', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, - {os: 'mac', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, - {os: 'mac', key: null, modifiers: ['meta'], expected: 'Cmd'}, - {os: 'mac', key: 'KeyF', modifiers: ['meta'], expected: 'Cmd + F'}, - {os: 'mac', key: 'F1', modifiers: ['meta'], expected: 'Cmd + F1'}, - {os: 'mac', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, - {os: 'mac', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, - {os: 'mac', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, - - {os: 'linux', key: null, modifiers: [], expected: ''}, - {os: 'linux', key: 'KeyF', modifiers: [], expected: 'F'}, - {os: 'linux', key: 'F1', modifiers: [], expected: 'F1'}, - {os: 'linux', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, - {os: 'linux', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, - {os: 'linux', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, - {os: 'linux', key: null, modifiers: ['alt'], expected: 'Alt'}, - {os: 'linux', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, - {os: 'linux', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, - {os: 'linux', key: null, modifiers: ['shift'], expected: 'Shift'}, - {os: 'linux', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, - {os: 'linux', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, - {os: 'linux', key: null, modifiers: ['meta'], expected: 'Super'}, - {os: 'linux', key: 'KeyF', modifiers: ['meta'], expected: 'Super + F'}, - {os: 'linux', key: 'F1', modifiers: ['meta'], expected: 'Super + F1'}, - {os: 'linux', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, - {os: 'linux', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, - {os: 'linux', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}, - - {os: 'unknown', key: null, modifiers: [], expected: ''}, - {os: 'unknown', key: 'KeyF', modifiers: [], expected: 'F'}, - {os: 'unknown', key: 'F1', modifiers: [], expected: 'F1'}, - {os: 'unknown', key: null, modifiers: ['ctrl'], expected: 'Ctrl'}, - {os: 'unknown', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'}, - {os: 'unknown', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'}, - {os: 'unknown', key: null, modifiers: ['alt'], expected: 'Alt'}, - {os: 'unknown', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'}, - {os: 'unknown', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'}, - {os: 'unknown', key: null, modifiers: ['shift'], expected: 'Shift'}, - {os: 'unknown', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'}, - {os: 'unknown', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'}, - {os: 'unknown', key: null, modifiers: ['meta'], expected: 'Meta'}, - {os: 'unknown', key: 'KeyF', modifiers: ['meta'], expected: 'Meta + F'}, - {os: 'unknown', key: 'F1', modifiers: ['meta'], expected: 'Meta + F1'}, - {os: 'unknown', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'}, - {os: 'unknown', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'}, - {os: 'unknown', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'} - ]; - - const hotkeyUtil = createHotkeyUtil(); - for (const {os, key, modifiers, expected} of data) { - hotkeyUtil.os = os; - const displayName = hotkeyUtil.getInputDisplayValue(key, modifiers); - assert.deepStrictEqual(displayName, expected); - } -} - -function testSortModifiers() { - const data = [ - {modifiers: [], expected: []}, - {modifiers: ['shift', 'alt', 'ctrl', 'mouse4', 'meta', 'mouse1', 'mouse0'], expected: ['meta', 'ctrl', 'alt', 'shift', 'mouse0', 'mouse1', 'mouse4']} - ]; - - const hotkeyUtil = createHotkeyUtil(); - for (const {modifiers, expected} of data) { - const modifiers2 = hotkeyUtil.sortModifiers(modifiers); - assert.strictEqual(modifiers2, modifiers); - assert.deepStrictEqual(modifiers2, expected); - } -} - - -function main() { - testCommandConversions(); - testDisplayNames(); - testSortModifiers(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-japanese-util.js b/test/test-japanese-util.js deleted file mode 100644 index 4395a11e..00000000 --- a/test/test-japanese-util.js +++ /dev/null @@ -1,881 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - -const vm = new VM(); -vm.execute([ - 'lib/wanakana.min.js', - 'js/language/sandbox/japanese-util.js', - 'js/general/text-source-map.js' -]); -const [JapaneseUtil, TextSourceMap, wanakana] = vm.get(['JapaneseUtil', 'TextSourceMap', 'wanakana']); -const jp = new JapaneseUtil(wanakana); - - -function testIsCodePointKanji() { - const data = [ - ['力方', true], - ['\u53f1\u{20b9f}', true], - ['かたカタ々kata、。?,.?', false], - ['逸逸', true] - ]; - - for (const [characters, expected] of data) { - for (const character of characters) { - const codePoint = character.codePointAt(0); - const actual = jp.isCodePointKanji(codePoint); - assert.strictEqual(actual, expected, `isCodePointKanji failed for ${character} (\\u{${codePoint.toString(16)}})`); - } - } -} - -function testIsCodePointKana() { - const data = [ - ['かたカタ', true], - ['力方々kata、。?,.?', false], - ['\u53f1\u{20b9f}', false] - ]; - - for (const [characters, expected] of data) { - for (const character of characters) { - const codePoint = character.codePointAt(0); - const actual = jp.isCodePointKana(codePoint); - assert.strictEqual(actual, expected, `isCodePointKana failed for ${character} (\\u{${codePoint.toString(16)}})`); - } - } -} - -function testIsCodePointJapanese() { - const data = [ - ['かたカタ力方々、。?', true], - ['\u53f1\u{20b9f}', true], - ['kata,.?', false], - ['逸逸', true] - ]; - - for (const [characters, expected] of data) { - for (const character of characters) { - const codePoint = character.codePointAt(0); - const actual = jp.isCodePointJapanese(codePoint); - assert.strictEqual(actual, expected, `isCodePointJapanese failed for ${character} (\\u{${codePoint.toString(16)}})`); - } - } -} - -function testIsStringEntirelyKana() { - const data = [ - ['かたかな', true], - ['カタカナ', true], - ['ひらがな', true], - ['ヒラガナ', true], - ['カタカナひらがな', true], - ['かたカタ力方々、。?', false], - ['\u53f1\u{20b9f}', false], - ['kata,.?', false], - ['かたカタ力方々、。?invalid', false], - ['\u53f1\u{20b9f}invalid', false], - ['kata,.?かた', false] - ]; - - for (const [string, expected] of data) { - assert.strictEqual(jp.isStringEntirelyKana(string), expected); - } -} - -function testIsStringPartiallyJapanese() { - const data = [ - ['かたかな', true], - ['カタカナ', true], - ['ひらがな', true], - ['ヒラガナ', true], - ['カタカナひらがな', true], - ['かたカタ力方々、。?', true], - ['\u53f1\u{20b9f}', true], - ['kata,.?', false], - ['かたカタ力方々、。?invalid', true], - ['\u53f1\u{20b9f}invalid', true], - ['kata,.?かた', true], - ['逸逸', true] - ]; - - for (const [string, expected] of data) { - assert.strictEqual(jp.isStringPartiallyJapanese(string), expected); - } -} - -function testConvertKatakanaToHiragana() { - const data = [ - ['かたかな', 'かたかな'], - ['ひらがな', 'ひらがな'], - ['カタカナ', 'かたかな'], - ['ヒラガナ', 'ひらがな'], - ['カタカナかたかな', 'かたかなかたかな'], - ['ヒラガナひらがな', 'ひらがなひらがな'], - ['chikaraちからチカラ力', 'chikaraちからちから力'], - ['katakana', 'katakana'], - ['hiragana', 'hiragana'], - ['カーナー', 'かあなあ'], - ['カーナー', 'かーなー', true] - ]; - - for (const [string, expected, keepProlongedSoundMarks=false] of data) { - assert.strictEqual(jp.convertKatakanaToHiragana(string, keepProlongedSoundMarks), expected); - } -} - -function testConvertHiraganaToKatakana() { - const data = [ - ['かたかな', 'カタカナ'], - ['ひらがな', 'ヒラガナ'], - ['カタカナ', 'カタカナ'], - ['ヒラガナ', 'ヒラガナ'], - ['カタカナかたかな', 'カタカナカタカナ'], - ['ヒラガナひらがな', 'ヒラガナヒラガナ'], - ['chikaraちからチカラ力', 'chikaraチカラチカラ力'], - ['katakana', 'katakana'], - ['hiragana', 'hiragana'] - ]; - - for (const [string, expected] of data) { - assert.strictEqual(jp.convertHiraganaToKatakana(string), expected); - } -} - -function testConvertToRomaji() { - const data = [ - ['かたかな', 'katakana'], - ['ひらがな', 'hiragana'], - ['カタカナ', 'katakana'], - ['ヒラガナ', 'hiragana'], - ['カタカナかたかな', 'katakanakatakana'], - ['ヒラガナひらがな', 'hiraganahiragana'], - ['chikaraちからチカラ力', 'chikarachikarachikara力'], - ['katakana', 'katakana'], - ['hiragana', 'hiragana'] - ]; - - for (const [string, expected] of data) { - assert.strictEqual(jp.convertToRomaji(string), expected); - } -} - -function testConvertNumericToFullWidth() { - const data = [ - ['0123456789', '0123456789'], - ['abcdefghij', 'abcdefghij'], - ['カタカナ', 'カタカナ'], - ['ひらがな', 'ひらがな'] - ]; - - for (const [string, expected] of data) { - assert.strictEqual(jp.convertNumericToFullWidth(string), expected); - } -} - -function testConvertHalfWidthKanaToFullWidth() { - const data = [ - ['0123456789', '0123456789'], - ['abcdefghij', 'abcdefghij'], - ['カタカナ', 'カタカナ'], - ['ひらがな', 'ひらがな'], - ['カキ', 'カキ', [1, 1]], - ['ガキ', 'ガキ', [2, 1]], - ['ニホン', 'ニホン', [1, 1, 1]], - ['ニッポン', 'ニッポン', [1, 1, 2, 1]] - ]; - - for (const [string, expected, expectedSourceMapping] of data) { - const sourceMap = new TextSourceMap(string); - const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null); - const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap); - assert.strictEqual(actual1, expected); - assert.strictEqual(actual2, expected); - if (typeof expectedSourceMapping !== 'undefined') { - assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))); - } - } -} - -function testConvertAlphabeticToKana() { - const data = [ - ['0123456789', '0123456789'], - ['abcdefghij', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], - ['ABCDEFGHIJ', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], // wanakana.toHiragana converts text to lower case - ['カタカナ', 'カタカナ'], - ['ひらがな', 'ひらがな'], - ['chikara', 'ちから', [3, 2, 2]], - ['CHIKARA', 'ちから', [3, 2, 2]] - ]; - - for (const [string, expected, expectedSourceMapping] of data) { - const sourceMap = new TextSourceMap(string); - const actual1 = jp.convertAlphabeticToKana(string, null); - const actual2 = jp.convertAlphabeticToKana(string, sourceMap); - assert.strictEqual(actual1, expected); - assert.strictEqual(actual2, expected); - if (typeof expectedSourceMapping !== 'undefined') { - assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))); - } - } -} - -function testDistributeFurigana() { - const data = [ - [ - ['有り難う', 'ありがとう'], - [ - {text: '有', reading: 'あ'}, - {text: 'り', reading: ''}, - {text: '難', reading: 'がと'}, - {text: 'う', reading: ''} - ] - ], - [ - ['方々', 'かたがた'], - [ - {text: '方々', reading: 'かたがた'} - ] - ], - [ - ['お祝い', 'おいわい'], - [ - {text: 'お', reading: ''}, - {text: '祝', reading: 'いわ'}, - {text: 'い', reading: ''} - ] - ], - [ - ['美味しい', 'おいしい'], - [ - {text: '美味', reading: 'おい'}, - {text: 'しい', reading: ''} - ] - ], - [ - ['食べ物', 'たべもの'], - [ - {text: '食', reading: 'た'}, - {text: 'べ', reading: ''}, - {text: '物', reading: 'もの'} - ] - ], - [ - ['試し切り', 'ためしぎり'], - [ - {text: '試', reading: 'ため'}, - {text: 'し', reading: ''}, - {text: '切', reading: 'ぎ'}, - {text: 'り', reading: ''} - ] - ], - // Ambiguous - [ - ['飼い犬', 'かいいぬ'], - [ - {text: '飼い犬', reading: 'かいいぬ'} - ] - ], - [ - ['長い間', 'ながいあいだ'], - [ - {text: '長い間', reading: 'ながいあいだ'} - ] - ], - // Same/empty reading - [ - ['飼い犬', ''], - [ - {text: '飼い犬', reading: ''} - ] - ], - [ - ['かいいぬ', 'かいいぬ'], - [ - {text: 'かいいぬ', reading: ''} - ] - ], - [ - ['かいぬ', 'かいぬ'], - [ - {text: 'かいぬ', reading: ''} - ] - ], - // Misc - [ - ['月', 'か'], - [ - {text: '月', reading: 'か'} - ] - ], - [ - ['月', 'カ'], - [ - {text: '月', reading: 'カ'} - ] - ], - // Mismatched kana readings - [ - ['有り難う', 'アリガトウ'], - [ - {text: '有', reading: 'ア'}, - {text: 'り', reading: 'リ'}, - {text: '難', reading: 'ガト'}, - {text: 'う', reading: 'ウ'} - ] - ], - [ - ['ありがとう', 'アリガトウ'], - [ - {text: 'ありがとう', reading: 'アリガトウ'} - ] - ], - // Mismatched kana readings (real examples) - [ - ['カ月', 'かげつ'], - [ - {text: 'カ', reading: 'か'}, - {text: '月', reading: 'げつ'} - ] - ], - [ - ['序ノ口', 'じょのくち'], - [ - {text: '序', reading: 'じょ'}, - {text: 'ノ', reading: 'の'}, - {text: '口', reading: 'くち'} - ] - ], - [ - ['スズメの涙', 'すずめのなみだ'], - [ - {text: 'スズメ', reading: 'すずめ'}, - {text: 'の', reading: ''}, - {text: '涙', reading: 'なみだ'} - ] - ], - [ - ['二カ所', 'にかしょ'], - [ - {text: '二', reading: 'に'}, - {text: 'カ', reading: 'か'}, - {text: '所', reading: 'しょ'} - ] - ], - [ - ['八ツ橋', 'やつはし'], - [ - {text: '八', reading: 'や'}, - {text: 'ツ', reading: 'つ'}, - {text: '橋', reading: 'はし'} - ] - ], - [ - ['八ツ橋', 'やつはし'], - [ - {text: '八', reading: 'や'}, - {text: 'ツ', reading: 'つ'}, - {text: '橋', reading: 'はし'} - ] - ], - [ - ['一カ月', 'いっかげつ'], - [ - {text: '一', reading: 'いっ'}, - {text: 'カ', reading: 'か'}, - {text: '月', reading: 'げつ'} - ] - ], - [ - ['一カ所', 'いっかしょ'], - [ - {text: '一', reading: 'いっ'}, - {text: 'カ', reading: 'か'}, - {text: '所', reading: 'しょ'} - ] - ], - [ - ['カ所', 'かしょ'], - [ - {text: 'カ', reading: 'か'}, - {text: '所', reading: 'しょ'} - ] - ], - [ - ['数カ月', 'すうかげつ'], - [ - {text: '数', reading: 'すう'}, - {text: 'カ', reading: 'か'}, - {text: '月', reading: 'げつ'} - ] - ], - [ - ['くノ一', 'くのいち'], - [ - {text: 'く', reading: ''}, - {text: 'ノ', reading: 'の'}, - {text: '一', reading: 'いち'} - ] - ], - [ - ['くノ一', 'くのいち'], - [ - {text: 'く', reading: ''}, - {text: 'ノ', reading: 'の'}, - {text: '一', reading: 'いち'} - ] - ], - [ - ['数カ国', 'すうかこく'], - [ - {text: '数', reading: 'すう'}, - {text: 'カ', reading: 'か'}, - {text: '国', reading: 'こく'} - ] - ], - [ - ['数カ所', 'すうかしょ'], - [ - {text: '数', reading: 'すう'}, - {text: 'カ', reading: 'か'}, - {text: '所', reading: 'しょ'} - ] - ], - [ - ['壇ノ浦の戦い', 'だんのうらのたたかい'], - [ - {text: '壇', reading: 'だん'}, - {text: 'ノ', reading: 'の'}, - {text: '浦', reading: 'うら'}, - {text: 'の', reading: ''}, - {text: '戦', reading: 'たたか'}, - {text: 'い', reading: ''} - ] - ], - [ - ['壇ノ浦の戦', 'だんのうらのたたかい'], - [ - {text: '壇', reading: 'だん'}, - {text: 'ノ', reading: 'の'}, - {text: '浦', reading: 'うら'}, - {text: 'の', reading: ''}, - {text: '戦', reading: 'たたかい'} - ] - ], - [ - ['序ノ口格', 'じょのくちかく'], - [ - {text: '序', reading: 'じょ'}, - {text: 'ノ', reading: 'の'}, - {text: '口格', reading: 'くちかく'} - ] - ], - [ - ['二カ国語', 'にかこくご'], - [ - {text: '二', reading: 'に'}, - {text: 'カ', reading: 'か'}, - {text: '国語', reading: 'こくご'} - ] - ], - [ - ['カ国', 'かこく'], - [ - {text: 'カ', reading: 'か'}, - {text: '国', reading: 'こく'} - ] - ], - [ - ['カ国語', 'かこくご'], - [ - {text: 'カ', reading: 'か'}, - {text: '国語', reading: 'こくご'} - ] - ], - [ - ['壇ノ浦の合戦', 'だんのうらのかっせん'], - [ - {text: '壇', reading: 'だん'}, - {text: 'ノ', reading: 'の'}, - {text: '浦', reading: 'うら'}, - {text: 'の', reading: ''}, - {text: '合戦', reading: 'かっせん'} - ] - ], - [ - ['一タ偏', 'いちたへん'], - [ - {text: '一', reading: 'いち'}, - {text: 'タ', reading: 'た'}, - {text: '偏', reading: 'へん'} - ] - ], - [ - ['ル又', 'るまた'], - [ - {text: 'ル', reading: 'る'}, - {text: '又', reading: 'また'} - ] - ], - [ - ['ノ木偏', 'のぎへん'], - [ - {text: 'ノ', reading: 'の'}, - {text: '木偏', reading: 'ぎへん'} - ] - ], - [ - ['一ノ貝', 'いちのかい'], - [ - {text: '一', reading: 'いち'}, - {text: 'ノ', reading: 'の'}, - {text: '貝', reading: 'かい'} - ] - ], - [ - ['虎ノ門事件', 'とらのもんじけん'], - [ - {text: '虎', reading: 'とら'}, - {text: 'ノ', reading: 'の'}, - {text: '門事件', reading: 'もんじけん'} - ] - ], - [ - ['教育ニ関スル勅語', 'きょういくにかんするちょくご'], - [ - {text: '教育', reading: 'きょういく'}, - {text: 'ニ', reading: 'に'}, - {text: '関', reading: 'かん'}, - {text: 'スル', reading: 'する'}, - {text: '勅語', reading: 'ちょくご'} - ] - ], - [ - ['二カ年', 'にかねん'], - [ - {text: '二', reading: 'に'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['三カ年', 'さんかねん'], - [ - {text: '三', reading: 'さん'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['四カ年', 'よんかねん'], - [ - {text: '四', reading: 'よん'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['五カ年', 'ごかねん'], - [ - {text: '五', reading: 'ご'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['六カ年', 'ろっかねん'], - [ - {text: '六', reading: 'ろっ'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['七カ年', 'ななかねん'], - [ - {text: '七', reading: 'なな'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['八カ年', 'はちかねん'], - [ - {text: '八', reading: 'はち'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['九カ年', 'きゅうかねん'], - [ - {text: '九', reading: 'きゅう'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['十カ年', 'じゅうかねん'], - [ - {text: '十', reading: 'じゅう'}, - {text: 'カ', reading: 'か'}, - {text: '年', reading: 'ねん'} - ] - ], - [ - ['鏡ノ間', 'かがみのま'], - [ - {text: '鏡', reading: 'かがみ'}, - {text: 'ノ', reading: 'の'}, - {text: '間', reading: 'ま'} - ] - ], - [ - ['鏡ノ間', 'かがみのま'], - [ - {text: '鏡', reading: 'かがみ'}, - {text: 'ノ', reading: 'の'}, - {text: '間', reading: 'ま'} - ] - ], - [ - ['ページ違反', 'ぺーじいはん'], - [ - {text: 'ペ', reading: 'ぺ'}, - {text: 'ー', reading: ''}, - {text: 'ジ', reading: 'じ'}, - {text: '違反', reading: 'いはん'} - ] - ], - // Mismatched kana - [ - ['サボる', 'サボル'], - [ - {text: 'サボ', reading: ''}, - {text: 'る', reading: 'ル'} - ] - ], - // Reading starts with term, but has remainder characters - [ - ['シック', 'シック・ビルしょうこうぐん'], - [ - {text: 'シック', reading: 'シック・ビルしょうこうぐん'} - ] - ], - // Kanji distribution tests - [ - ['逸らす', 'そらす'], - [ - {text: '逸', reading: 'そ'}, - {text: 'らす', reading: ''} - ] - ], - [ - ['逸らす', 'そらす'], - [ - {text: '逸', reading: 'そ'}, - {text: 'らす', reading: ''} - ] - ] - ]; - - for (const [[term, reading], expected] of data) { - const actual = jp.distributeFurigana(term, reading); - vm.assert.deepStrictEqual(actual, expected); - } -} - -function testDistributeFuriganaInflected() { - const data = [ - [ - ['美味しい', 'おいしい', '美味しかた'], - [ - {text: '美味', reading: 'おい'}, - {text: 'しかた', reading: ''} - ] - ], - [ - ['食べる', 'たべる', '食べた'], - [ - {text: '食', reading: 'た'}, - {text: 'べた', reading: ''} - ] - ], - [ - ['迄に', 'までに', 'までに'], - [ - {text: 'までに', reading: ''} - ] - ], - [ - ['行う', 'おこなう', 'おこなわなかった'], - [ - {text: 'おこなわなかった', reading: ''} - ] - ], - [ - ['いい', 'いい', 'イイ'], - [ - {text: 'イイ', reading: ''} - ] - ], - [ - ['否か', 'いなか', '否カ'], - [ - {text: '否', reading: 'いな'}, - {text: 'カ', reading: 'か'} - ] - ] - ]; - - for (const [[term, reading, source], expected] of data) { - const actual = jp.distributeFuriganaInflected(term, reading, source); - vm.assert.deepStrictEqual(actual, expected); - } -} - -function testCollapseEmphaticSequences() { - const data = [ - [['かこい', false], ['かこい', [1, 1, 1]]], - [['かこい', true], ['かこい', [1, 1, 1]]], - [['かっこい', false], ['かっこい', [1, 1, 1, 1]]], - [['かっこい', true], ['かこい', [2, 1, 1]]], - [['かっっこい', false], ['かっこい', [1, 2, 1, 1]]], - [['かっっこい', true], ['かこい', [3, 1, 1]]], - [['かっっっこい', false], ['かっこい', [1, 3, 1, 1]]], - [['かっっっこい', true], ['かこい', [4, 1, 1]]], - - [['こい', false], ['こい', [1, 1]]], - [['こい', true], ['こい', [1, 1]]], - [['っこい', false], ['っこい', [1, 1, 1]]], - [['っこい', true], ['こい', [2, 1]]], - [['っっこい', false], ['っこい', [2, 1, 1]]], - [['っっこい', true], ['こい', [3, 1]]], - [['っっっこい', false], ['っこい', [3, 1, 1]]], - [['っっっこい', true], ['こい', [4, 1]]], - - [['すごい', false], ['すごい', [1, 1, 1]]], - [['すごい', true], ['すごい', [1, 1, 1]]], - [['すごーい', false], ['すごーい', [1, 1, 1, 1]]], - [['すごーい', true], ['すごい', [1, 2, 1]]], - [['すごーーい', false], ['すごーい', [1, 1, 2, 1]]], - [['すごーーい', true], ['すごい', [1, 3, 1]]], - [['すっごーい', false], ['すっごーい', [1, 1, 1, 1, 1]]], - [['すっごーい', true], ['すごい', [2, 2, 1]]], - [['すっっごーーい', false], ['すっごーい', [1, 2, 1, 2, 1]]], - [['すっっごーーい', true], ['すごい', [3, 3, 1]]], - - [['', false], ['', []]], - [['', true], ['', []]], - [['っ', false], ['っ', [1]]], - [['っ', true], ['', [1]]], - [['っっ', false], ['っ', [2]]], - [['っっ', true], ['', [2]]], - [['っっっ', false], ['っ', [3]]], - [['っっっ', true], ['', [3]]] - ]; - - for (const [[text, fullCollapse], [expected, expectedSourceMapping]] of data) { - const sourceMap = new TextSourceMap(text); - const actual1 = jp.collapseEmphaticSequences(text, fullCollapse, null); - const actual2 = jp.collapseEmphaticSequences(text, fullCollapse, sourceMap); - assert.strictEqual(actual1, expected); - assert.strictEqual(actual2, expected); - if (typeof expectedSourceMapping !== 'undefined') { - assert.ok(sourceMap.equals(new TextSourceMap(text, expectedSourceMapping))); - } - } -} - -function testIsMoraPitchHigh() { - const data = [ - [[0, 0], false], - [[1, 0], true], - [[2, 0], true], - [[3, 0], true], - - [[0, 1], true], - [[1, 1], false], - [[2, 1], false], - [[3, 1], false], - - [[0, 2], false], - [[1, 2], true], - [[2, 2], false], - [[3, 2], false], - - [[0, 3], false], - [[1, 3], true], - [[2, 3], true], - [[3, 3], false], - - [[0, 4], false], - [[1, 4], true], - [[2, 4], true], - [[3, 4], true] - ]; - - for (const [[moraIndex, pitchAccentDownstepPosition], expected] of data) { - const actual = jp.isMoraPitchHigh(moraIndex, pitchAccentDownstepPosition); - assert.strictEqual(actual, expected); - } -} - -function testGetKanaMorae() { - const data = [ - ['かこ', ['か', 'こ']], - ['かっこ', ['か', 'っ', 'こ']], - ['カコ', ['カ', 'コ']], - ['カッコ', ['カ', 'ッ', 'コ']], - ['コート', ['コ', 'ー', 'ト']], - ['ちゃんと', ['ちゃ', 'ん', 'と']], - ['とうきょう', ['と', 'う', 'きょ', 'う']], - ['ぎゅう', ['ぎゅ', 'う']], - ['ディスコ', ['ディ', 'ス', 'コ']] - ]; - - for (const [text, expected] of data) { - const actual = jp.getKanaMorae(text); - vm.assert.deepStrictEqual(actual, expected); - } -} - - -function main() { - testIsCodePointKanji(); - testIsCodePointKana(); - testIsCodePointJapanese(); - testIsStringEntirelyKana(); - testIsStringPartiallyJapanese(); - testConvertKatakanaToHiragana(); - testConvertHiraganaToKatakana(); - testConvertToRomaji(); - testConvertNumericToFullWidth(); - testConvertHalfWidthKanaToFullWidth(); - testConvertAlphabeticToKana(); - testDistributeFurigana(); - testDistributeFuriganaInflected(); - testCollapseEmphaticSequences(); - testIsMoraPitchHigh(); - testGetKanaMorae(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-json-schema.js b/test/test-json-schema.js deleted file mode 100644 index 35ecc5e9..00000000 --- a/test/test-json-schema.js +++ /dev/null @@ -1,1011 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - -const vm = new VM(); -vm.execute([ - 'js/core.js', - 'js/general/cache-map.js', - 'js/data/json-schema.js' -]); -const JsonSchema = vm.get('JsonSchema'); - - -function schemaValidate(schema, value) { - return new JsonSchema(schema).isValid(value); -} - -function getValidValueOrDefault(schema, value) { - return new JsonSchema(schema).getValidValueOrDefault(value); -} - -function createProxy(schema, value) { - return new JsonSchema(schema).createProxy(value); -} - -function clone(value) { - return JSON.parse(JSON.stringify(value)); -} - - -function testValidate1() { - const schema = { - allOf: [ - { - type: 'number' - }, - { - anyOf: [ - {minimum: 10, maximum: 100}, - {minimum: -100, maximum: -10} - ] - }, - { - oneOf: [ - {multipleOf: 3}, - {multipleOf: 5} - ] - }, - { - not: [ - {multipleOf: 20} - ] - } - ] - }; - - const jsValidate = (value) => { - return ( - typeof value === 'number' && - ( - (value >= 10 && value <= 100) || - (value >= -100 && value <= -10) - ) && - ( - ( - (value % 3) === 0 || - (value % 5) === 0 - ) && - (value % 15) !== 0 - ) && - (value % 20) !== 0 - ); - }; - - for (let i = -111; i <= 111; i++) { - const actual = schemaValidate(schema, i); - const expected = jsValidate(i); - assert.strictEqual(actual, expected); - } -} - -function testValidate2() { - const data = [ - // String tests - { - schema: { - type: 'string' - }, - inputs: [ - {expected: false, value: null}, - {expected: false, value: void 0}, - {expected: false, value: 0}, - {expected: false, value: {}}, - {expected: false, value: []}, - {expected: true, value: ''} - ] - }, - { - schema: { - type: 'string', - minLength: 2 - }, - inputs: [ - {expected: false, value: ''}, - {expected: false, value: '1'}, - {expected: true, value: '12'}, - {expected: true, value: '123'} - ] - }, - { - schema: { - type: 'string', - maxLength: 2 - }, - inputs: [ - {expected: true, value: ''}, - {expected: true, value: '1'}, - {expected: true, value: '12'}, - {expected: false, value: '123'} - ] - }, - { - schema: { - type: 'string', - pattern: 'test' - }, - inputs: [ - {expected: false, value: ''}, - {expected: true, value: 'test'}, - {expected: false, value: 'TEST'}, - {expected: true, value: 'ABCtestDEF'}, - {expected: false, value: 'ABCTESTDEF'} - ] - }, - { - schema: { - type: 'string', - pattern: '^test$' - }, - inputs: [ - {expected: false, value: ''}, - {expected: true, value: 'test'}, - {expected: false, value: 'TEST'}, - {expected: false, value: 'ABCtestDEF'}, - {expected: false, value: 'ABCTESTDEF'} - ] - }, - { - schema: { - type: 'string', - pattern: '^test$', - patternFlags: 'i' - }, - inputs: [ - {expected: false, value: ''}, - {expected: true, value: 'test'}, - {expected: true, value: 'TEST'}, - {expected: false, value: 'ABCtestDEF'}, - {expected: false, value: 'ABCTESTDEF'} - ] - }, - { - schema: { - type: 'string', - pattern: '*' - }, - inputs: [ - {expected: false, value: ''} - ] - }, - { - schema: { - type: 'string', - pattern: '.', - patternFlags: '?' - }, - inputs: [ - {expected: false, value: ''} - ] - }, - - // Const tests - { - schema: { - const: 32 - }, - inputs: [ - {expected: true, value: 32}, - {expected: false, value: 0}, - {expected: false, value: '32'}, - {expected: false, value: null}, - {expected: false, value: {a: 'b'}}, - {expected: false, value: [1, 2, 3]} - ] - }, - { - schema: { - const: '32' - }, - inputs: [ - {expected: false, value: 32}, - {expected: false, value: 0}, - {expected: true, value: '32'}, - {expected: false, value: null}, - {expected: false, value: {a: 'b'}}, - {expected: false, value: [1, 2, 3]} - ] - }, - { - schema: { - const: null - }, - inputs: [ - {expected: false, value: 32}, - {expected: false, value: 0}, - {expected: false, value: '32'}, - {expected: true, value: null}, - {expected: false, value: {a: 'b'}}, - {expected: false, value: [1, 2, 3]} - ] - }, - { - schema: { - const: {a: 'b'} - }, - inputs: [ - {expected: false, value: 32}, - {expected: false, value: 0}, - {expected: false, value: '32'}, - {expected: false, value: null}, - {expected: false, value: {a: 'b'}}, - {expected: false, value: [1, 2, 3]} - ] - }, - { - schema: { - const: [1, 2, 3] - }, - inputs: [ - {expected: false, value: 32}, - {expected: false, value: 0}, - {expected: false, value: '32'}, - {expected: false, value: null}, - {expected: false, value: {a: 'b'}}, - {expected: false, value: [1, 2, 3]} - ] - }, - - // Array contains tests - { - schema: { - type: 'array', - contains: {const: 32} - }, - inputs: [ - {expected: false, value: []}, - {expected: true, value: [32]}, - {expected: true, value: [1, 32]}, - {expected: true, value: [1, 32, 1]}, - {expected: false, value: [33]}, - {expected: false, value: [1, 33]}, - {expected: false, value: [1, 33, 1]} - ] - }, - - // Number limits tests - { - schema: { - type: 'number', - minimum: 0 - }, - inputs: [ - {expected: false, value: -1}, - {expected: true, value: 0}, - {expected: true, value: 1} - ] - }, - { - schema: { - type: 'number', - exclusiveMinimum: 0 - }, - inputs: [ - {expected: false, value: -1}, - {expected: false, value: 0}, - {expected: true, value: 1} - ] - }, - { - schema: { - type: 'number', - maximum: 0 - }, - inputs: [ - {expected: true, value: -1}, - {expected: true, value: 0}, - {expected: false, value: 1} - ] - }, - { - schema: { - type: 'number', - exclusiveMaximum: 0 - }, - inputs: [ - {expected: true, value: -1}, - {expected: false, value: 0}, - {expected: false, value: 1} - ] - }, - - // Integer limits tests - { - schema: { - type: 'integer', - minimum: 0 - }, - inputs: [ - {expected: false, value: -1}, - {expected: true, value: 0}, - {expected: true, value: 1} - ] - }, - { - schema: { - type: 'integer', - exclusiveMinimum: 0 - }, - inputs: [ - {expected: false, value: -1}, - {expected: false, value: 0}, - {expected: true, value: 1} - ] - }, - { - schema: { - type: 'integer', - maximum: 0 - }, - inputs: [ - {expected: true, value: -1}, - {expected: true, value: 0}, - {expected: false, value: 1} - ] - }, - { - schema: { - type: 'integer', - exclusiveMaximum: 0 - }, - inputs: [ - {expected: true, value: -1}, - {expected: false, value: 0}, - {expected: false, value: 1} - ] - }, - { - schema: { - type: 'integer', - multipleOf: 2 - }, - inputs: [ - {expected: true, value: -2}, - {expected: false, value: -1}, - {expected: true, value: 0}, - {expected: false, value: 1}, - {expected: true, value: 2} - ] - }, - - // Numeric type tests - { - schema: { - type: 'number' - }, - inputs: [ - {expected: true, value: 0}, - {expected: true, value: 0.5}, - {expected: true, value: 1}, - {expected: false, value: '0'}, - {expected: false, value: null}, - {expected: false, value: []}, - {expected: false, value: {}} - ] - }, - { - schema: { - type: 'integer' - }, - inputs: [ - {expected: true, value: 0}, - {expected: false, value: 0.5}, - {expected: true, value: 1}, - {expected: false, value: '0'}, - {expected: false, value: null}, - {expected: false, value: []}, - {expected: false, value: {}} - ] - }, - - // Reference tests - { - schema: { - definitions: { - example: { - type: 'number' - } - }, - $ref: '#/definitions/example' - }, - inputs: [ - {expected: true, value: 0}, - {expected: true, value: 0.5}, - {expected: true, value: 1}, - {expected: false, value: '0'}, - {expected: false, value: null}, - {expected: false, value: []}, - {expected: false, value: {}} - ] - }, - { - schema: { - definitions: { - example: { - type: 'integer' - } - }, - $ref: '#/definitions/example' - }, - inputs: [ - {expected: true, value: 0}, - {expected: false, value: 0.5}, - {expected: true, value: 1}, - {expected: false, value: '0'}, - {expected: false, value: null}, - {expected: false, value: []}, - {expected: false, value: {}} - ] - }, - { - schema: { - definitions: { - example: { - type: 'object', - additionalProperties: false, - properties: { - test: { - $ref: '#/definitions/example' - } - } - } - }, - $ref: '#/definitions/example' - }, - inputs: [ - {expected: false, value: 0}, - {expected: false, value: 0.5}, - {expected: false, value: 1}, - {expected: false, value: '0'}, - {expected: false, value: null}, - {expected: false, value: []}, - {expected: true, value: {}}, - {expected: false, value: {test: 0}}, - {expected: false, value: {test: 0.5}}, - {expected: false, value: {test: 1}}, - {expected: false, value: {test: '0'}}, - {expected: false, value: {test: null}}, - {expected: false, value: {test: []}}, - {expected: true, value: {test: {}}}, - {expected: true, value: {test: {test: {}}}}, - {expected: true, value: {test: {test: {test: {}}}}} - ] - } - ]; - - for (const {schema, inputs} of data) { - for (const {expected, value} of inputs) { - const actual = schemaValidate(schema, value); - assert.strictEqual(actual, expected); - } - } -} - - -function testGetValidValueOrDefault1() { - const data = [ - // Test value defaulting on objects with additionalProperties=false - { - schema: { - type: 'object', - required: ['test'], - properties: { - test: { - type: 'string', - default: 'default' - } - }, - additionalProperties: false - }, - inputs: [ - [ - void 0, - {test: 'default'} - ], - [ - null, - {test: 'default'} - ], - [ - 0, - {test: 'default'} - ], - [ - '', - {test: 'default'} - ], - [ - [], - {test: 'default'} - ], - [ - {}, - {test: 'default'} - ], - [ - {test: 'value'}, - {test: 'value'} - ], - [ - {test2: 'value2'}, - {test: 'default'} - ], - [ - {test: 'value', test2: 'value2'}, - {test: 'value'} - ] - ] - }, - - // Test value defaulting on objects with additionalProperties=true - { - schema: { - type: 'object', - required: ['test'], - properties: { - test: { - type: 'string', - default: 'default' - } - }, - additionalProperties: true - }, - inputs: [ - [ - {}, - {test: 'default'} - ], - [ - {test: 'value'}, - {test: 'value'} - ], - [ - {test2: 'value2'}, - {test: 'default', test2: 'value2'} - ], - [ - {test: 'value', test2: 'value2'}, - {test: 'value', test2: 'value2'} - ] - ] - }, - - // Test value defaulting on objects with additionalProperties={schema} - { - schema: { - type: 'object', - required: ['test'], - properties: { - test: { - type: 'string', - default: 'default' - } - }, - additionalProperties: { - type: 'number', - default: 10 - } - }, - inputs: [ - [ - {}, - {test: 'default'} - ], - [ - {test: 'value'}, - {test: 'value'} - ], - [ - {test2: 'value2'}, - {test: 'default', test2: 10} - ], - [ - {test: 'value', test2: 'value2'}, - {test: 'value', test2: 10} - ], - [ - {test2: 2}, - {test: 'default', test2: 2} - ], - [ - {test: 'value', test2: 2}, - {test: 'value', test2: 2} - ], - [ - {test: 'value', test2: 2, test3: null}, - {test: 'value', test2: 2, test3: 10} - ], - [ - {test: 'value', test2: 2, test3: void 0}, - {test: 'value', test2: 2, test3: 10} - ] - ] - }, - - // Test value defaulting where hasOwnProperty is false - { - schema: { - type: 'object', - required: ['test'], - properties: { - test: { - type: 'string', - default: 'default' - } - } - }, - inputs: [ - [ - {}, - {test: 'default'} - ], - [ - {test: 'value'}, - {test: 'value'} - ], - [ - Object.create({test: 'value'}), - {test: 'default'} - ] - ] - }, - { - schema: { - type: 'object', - required: ['toString'], - properties: { - toString: { - type: 'string', - default: 'default' - } - } - }, - inputs: [ - [ - {}, - {toString: 'default'} - ], - [ - {toString: 'value'}, - {toString: 'value'} - ], - [ - Object.create({toString: 'value'}), - {toString: 'default'} - ] - ] - }, - - // Test enum - { - schema: { - type: 'object', - required: ['test'], - properties: { - test: { - type: 'string', - default: 'value1', - enum: ['value1', 'value2', 'value3'] - } - } - }, - inputs: [ - [ - {test: 'value1'}, - {test: 'value1'} - ], - [ - {test: 'value2'}, - {test: 'value2'} - ], - [ - {test: 'value3'}, - {test: 'value3'} - ], - [ - {test: 'value4'}, - {test: 'value1'} - ] - ] - }, - - // Test valid vs invalid default - { - schema: { - type: 'object', - required: ['test'], - properties: { - test: { - type: 'integer', - default: 2, - minimum: 1 - } - } - }, - inputs: [ - [ - {test: -1}, - {test: 2} - ] - ] - }, - { - schema: { - type: 'object', - required: ['test'], - properties: { - test: { - type: 'integer', - default: 1, - minimum: 2 - } - } - }, - inputs: [ - [ - {test: -1}, - {test: -1} - ] - ] - }, - - // Test references - { - schema: { - definitions: { - example: { - type: 'number', - default: 0 - } - }, - $ref: '#/definitions/example' - }, - inputs: [ - [ - 1, - 1 - ], - [ - null, - 0 - ], - [ - 'test', - 0 - ], - [ - {test: 'value'}, - 0 - ] - ] - }, - { - schema: { - definitions: { - example: { - type: 'object', - additionalProperties: false, - properties: { - test: { - $ref: '#/definitions/example' - } - } - } - }, - $ref: '#/definitions/example' - }, - inputs: [ - [ - 1, - {} - ], - [ - null, - {} - ], - [ - 'test', - {} - ], - [ - {}, - {} - ], - [ - {test: {}}, - {test: {}} - ], - [ - {test: 'value'}, - {test: {}} - ], - [ - {test: {test: {}}}, - {test: {test: {}}} - ] - ] - } - ]; - - for (const {schema, inputs} of data) { - for (const [value, expected] of inputs) { - const actual = getValidValueOrDefault(schema, value); - vm.assert.deepStrictEqual(actual, expected); - } - } -} - - -function testProxy1() { - const data = [ - // Object tests - { - schema: { - type: 'object', - required: ['test'], - additionalProperties: false, - properties: { - test: { - type: 'string', - default: 'default' - } - } - }, - tests: [ - {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }}, - {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }}, - {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }}, - {error: true, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }}, - {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }} - ] - }, - { - schema: { - type: 'object', - required: ['test'], - additionalProperties: true, - properties: { - test: { - type: 'string', - default: 'default' - } - } - }, - tests: [ - {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }}, - {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }}, - {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }}, - {error: false, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }}, - {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }} - ] - }, - { - schema: { - type: 'object', - required: ['test1'], - additionalProperties: false, - properties: { - test1: { - type: 'object', - required: ['test2'], - additionalProperties: false, - properties: { - test2: { - type: 'object', - required: ['test3'], - additionalProperties: false, - properties: { - test3: { - type: 'string', - default: 'default' - } - } - } - } - } - } - }, - tests: [ - {error: false, action: (value) => { value.test1.test2.test3 = 'string'; }}, - {error: true, action: (value) => { value.test1.test2.test3 = null; }}, - {error: true, action: (value) => { delete value.test1.test2.test3; }}, - {error: true, action: (value) => { value.test1.test2 = null; }}, - {error: true, action: (value) => { value.test1 = null; }}, - {error: true, action: (value) => { value.test4 = 'string'; }}, - {error: false, action: (value) => { delete value.test4; }} - ] - }, - - // Array tests - { - schema: { - type: 'array', - items: { - type: 'string', - default: 'default' - } - }, - tests: [ - {error: false, value: ['default'], action: (value) => { value[0] = 'string'; }}, - {error: true, value: ['default'], action: (value) => { value[0] = null; }}, - {error: false, value: ['default'], action: (value) => { delete value[0]; }}, - {error: false, value: ['default'], action: (value) => { value[1] = 'string'; }}, - {error: false, value: ['default'], action: (value) => { - value[1] = 'string'; - if (value.length !== 2) { throw new Error(`Invalid length; expected=2; actual=${value.length}`); } - if (typeof value.push !== 'function') { throw new Error(`Invalid push; expected=function; actual=${typeof value.push}`); } - }} - ] - }, - - // Reference tests - { - schema: { - definitions: { - example: { - type: 'object', - additionalProperties: false, - properties: { - test: { - $ref: '#/definitions/example' - } - } - } - }, - $ref: '#/definitions/example' - }, - tests: [ - {error: false, value: {}, action: (value) => { value.test = {}; }}, - {error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }}, - {error: false, value: {}, action: (value) => { value.test = {test: {}}; }}, - {error: true, value: {}, action: (value) => { value.test = null; }}, - {error: true, value: {}, action: (value) => { value.test = 'string'; }}, - {error: true, value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }}, - {error: true, value: {}, action: (value) => { value.test = {test: 'string'}; }} - ] - } - ]; - - for (const {schema, tests} of data) { - for (let {error, value, action} of tests) { - if (typeof value === 'undefined') { value = getValidValueOrDefault(schema, void 0); } - value = clone(value); - assert.ok(schemaValidate(schema, value)); - const valueProxy = createProxy(schema, value); - if (error) { - assert.throws(() => action(valueProxy)); - } else { - assert.doesNotThrow(() => action(valueProxy)); - } - } - } -} - - -function main() { - testValidate1(); - testValidate2(); - testGetValidValueOrDefault1(); - testProxy1(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-manifest.js b/test/test-manifest.js deleted file mode 100644 index b52152de..00000000 --- a/test/test-manifest.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {ManifestUtil} = require('../dev/manifest-util'); - - -function loadManifestString() { - const manifestPath = path.join(__dirname, '..', 'ext', 'manifest.json'); - return fs.readFileSync(manifestPath, {encoding: 'utf8'}); -} - -function validateManifest() { - const manifestUtil = new ManifestUtil(); - const manifest1 = loadManifestString(); - const manifest2 = ManifestUtil.createManifestString(manifestUtil.getManifest()); - assert.strictEqual(manifest1, manifest2, 'Manifest data does not match.'); -} - - -function main() { - validateManifest(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-object-property-accessor.js b/test/test-object-property-accessor.js deleted file mode 100644 index d225e893..00000000 --- a/test/test-object-property-accessor.js +++ /dev/null @@ -1,416 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - -const vm = new VM({}); -vm.execute('js/general/object-property-accessor.js'); -const ObjectPropertyAccessor = vm.get('ObjectPropertyAccessor'); - - -function createTestObject() { - return { - 0: null, - value1: { - value2: {}, - value3: [], - value4: null - }, - value5: [ - {}, - [], - null - ] - }; -} - - -function testGet1() { - const data = [ - [[], (object) => object], - [['0'], (object) => object['0']], - [['value1'], (object) => object.value1], - [['value1', 'value2'], (object) => object.value1.value2], - [['value1', 'value3'], (object) => object.value1.value3], - [['value1', 'value4'], (object) => object.value1.value4], - [['value5'], (object) => object.value5], - [['value5', 0], (object) => object.value5[0]], - [['value5', 1], (object) => object.value5[1]], - [['value5', 2], (object) => object.value5[2]] - ]; - - for (const [pathArray, getExpected] of data) { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - const expected = getExpected(object); - - assert.strictEqual(accessor.get(pathArray), expected); - } -} - -function testGet2() { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - - const data = [ - [[0], 'Invalid path: [0]'], - [['0', 'invalid'], 'Invalid path: ["0"].invalid'], - [['invalid'], 'Invalid path: invalid'], - [['value1', 'invalid'], 'Invalid path: value1.invalid'], - [['value1', 'value2', 'invalid'], 'Invalid path: value1.value2.invalid'], - [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], - [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], - [['value1', 'value3', 0], 'Invalid path: value1.value3[0]'], - [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], - [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], - [['value5', 'length'], 'Invalid path: value5.length'], - [['value5', 0, 'invalid'], 'Invalid path: value5[0].invalid'], - [['value5', 0, 0], 'Invalid path: value5[0][0]'], - [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], - [['value5', 1, 0], 'Invalid path: value5[1][0]'], - [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], - [['value5', 2, 0], 'Invalid path: value5[2][0]'], - [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], - [['value5', 2.5], 'Invalid index'] - ]; - - for (const [pathArray, message] of data) { - assert.throws(() => accessor.get(pathArray), {message}); - } -} - - -function testSet1() { - const testValue = {}; - const data = [ - ['0'], - ['value1', 'value2'], - ['value1', 'value3'], - ['value1', 'value4'], - ['value1'], - ['value5', 0], - ['value5', 1], - ['value5', 2], - ['value5'] - ]; - - for (const pathArray of data) { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - - accessor.set(pathArray, testValue); - assert.strictEqual(accessor.get(pathArray), testValue); - } -} - -function testSet2() { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - - const testValue = {}; - const data = [ - [[], 'Invalid path'], - [[0], 'Invalid path: [0]'], - [['0', 'invalid'], 'Invalid path: ["0"].invalid'], - [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], - [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], - [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], - [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], - [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], - [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], - [['value5', 2, 0], 'Invalid path: value5[2][0]'], - [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], - [['value5', 2.5], 'Invalid index'] - ]; - - for (const [pathArray, message] of data) { - assert.throws(() => accessor.set(pathArray, testValue), {message}); - } -} - - -function testDelete1() { - const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property); - - const data = [ - [['0'], (object) => !hasOwn(object, '0')], - [['value1', 'value2'], (object) => !hasOwn(object.value1, 'value2')], - [['value1', 'value3'], (object) => !hasOwn(object.value1, 'value3')], - [['value1', 'value4'], (object) => !hasOwn(object.value1, 'value4')], - [['value1'], (object) => !hasOwn(object, 'value1')], - [['value5'], (object) => !hasOwn(object, 'value5')] - ]; - - for (const [pathArray, validate] of data) { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - - accessor.delete(pathArray); - assert.ok(validate(object)); - } -} - -function testDelete2() { - const data = [ - [[], 'Invalid path'], - [[0], 'Invalid path: [0]'], - [['0', 'invalid'], 'Invalid path: ["0"].invalid'], - [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'], - [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'], - [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'], - [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'], - [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'], - [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'], - [['value5', 2, 0], 'Invalid path: value5[2][0]'], - [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'], - [['value5', 2.5], 'Invalid index'], - [['value5', 0], 'Invalid type'], - [['value5', 1], 'Invalid type'], - [['value5', 2], 'Invalid type'] - ]; - - for (const [pathArray, message] of data) { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - - assert.throws(() => accessor.delete(pathArray), {message}); - } -} - - -function testSwap1() { - const data = [ - [['0'], true], - [['value1', 'value2'], true], - [['value1', 'value3'], true], - [['value1', 'value4'], true], - [['value1'], false], - [['value5', 0], true], - [['value5', 1], true], - [['value5', 2], true], - [['value5'], false] - ]; - - for (const [pathArray1, compareValues1] of data) { - for (const [pathArray2, compareValues2] of data) { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - - const value1a = accessor.get(pathArray1); - const value2a = accessor.get(pathArray2); - - accessor.swap(pathArray1, pathArray2); - - if (!compareValues1 || !compareValues2) { continue; } - - const value1b = accessor.get(pathArray1); - const value2b = accessor.get(pathArray2); - - assert.deepStrictEqual(value1a, value2b); - assert.deepStrictEqual(value2a, value1b); - } - } -} - -function testSwap2() { - const data = [ - [[], [], false, 'Invalid path 1'], - [['0'], [], false, 'Invalid path 2'], - [[], ['0'], false, 'Invalid path 1'], - [[0], ['0'], false, 'Invalid path 1: [0]'], - [['0'], [0], false, 'Invalid path 2: [0]'] - ]; - - for (const [pathArray1, pathArray2, checkRevert, message] of data) { - const object = createTestObject(); - const accessor = new ObjectPropertyAccessor(object); - - let value1a; - let value2a; - if (checkRevert) { - try { - value1a = accessor.get(pathArray1); - value2a = accessor.get(pathArray2); - } catch (e) { - // NOP - } - } - - assert.throws(() => accessor.swap(pathArray1, pathArray2), {message}); - - if (!checkRevert) { continue; } - - const value1b = accessor.get(pathArray1); - const value2b = accessor.get(pathArray2); - - assert.deepStrictEqual(value1a, value1b); - assert.deepStrictEqual(value2a, value2b); - } -} - - -function testGetPathString1() { - const data = [ - [[], ''], - [[0], '[0]'], - [['escape\\'], '["escape\\\\"]'], - [['\'quote\''], '["\'quote\'"]'], - [['"quote"'], '["\\"quote\\""]'], - [['part1', 'part2'], 'part1.part2'], - [['part1', 'part2', 3], 'part1.part2[3]'], - [['part1', 'part2', '3'], 'part1.part2["3"]'], - [['part1', 'part2', '3part'], 'part1.part2["3part"]'], - [['part1', 'part2', '3part', 'part4'], 'part1.part2["3part"].part4'], - [['part1', 'part2', '3part', '4part'], 'part1.part2["3part"]["4part"]'] - ]; - - for (const [pathArray, expected] of data) { - assert.strictEqual(ObjectPropertyAccessor.getPathString(pathArray), expected); - } -} - -function testGetPathString2() { - const data = [ - [[1.5], 'Invalid index'], - [[null], 'Invalid type: object'] - ]; - - for (const [pathArray, message] of data) { - assert.throws(() => ObjectPropertyAccessor.getPathString(pathArray), {message}); - } -} - - -function testGetPathArray1() { - const data = [ - ['', []], - ['[0]', [0]], - ['["escape\\\\"]', ['escape\\']], - ['["\'quote\'"]', ['\'quote\'']], - ['["\\"quote\\""]', ['"quote"']], - ['part1.part2', ['part1', 'part2']], - ['part1.part2[3]', ['part1', 'part2', 3]], - ['part1.part2["3"]', ['part1', 'part2', '3']], - ['part1.part2[\'3\']', ['part1', 'part2', '3']], - ['part1.part2["3part"]', ['part1', 'part2', '3part']], - ['part1.part2[\'3part\']', ['part1', 'part2', '3part']], - ['part1.part2["3part"].part4', ['part1', 'part2', '3part', 'part4']], - ['part1.part2[\'3part\'].part4', ['part1', 'part2', '3part', 'part4']], - ['part1.part2["3part"]["4part"]', ['part1', 'part2', '3part', '4part']], - ['part1.part2[\'3part\'][\'4part\']', ['part1', 'part2', '3part', '4part']] - ]; - - for (const [pathString, expected] of data) { - vm.assert.deepStrictEqual(ObjectPropertyAccessor.getPathArray(pathString), expected); - } -} - -function testGetPathArray2() { - const data = [ - ['?', 'Unexpected character: ?'], - ['.', 'Unexpected character: .'], - ['0', 'Unexpected character: 0'], - ['part1.[0]', 'Unexpected character: ['], - ['part1?', 'Unexpected character: ?'], - ['[part1]', 'Unexpected character: p'], - ['[0a]', 'Unexpected character: a'], - ['["part1"x]', 'Unexpected character: x'], - ['[\'part1\'x]', 'Unexpected character: x'], - ['["part1"]x', 'Unexpected character: x'], - ['[\'part1\']x', 'Unexpected character: x'], - ['part1..part2', 'Unexpected character: .'], - - ['[', 'Path not terminated correctly'], - ['part1.', 'Path not terminated correctly'], - ['part1[', 'Path not terminated correctly'], - ['part1["', 'Path not terminated correctly'], - ['part1[\'', 'Path not terminated correctly'], - ['part1[""', 'Path not terminated correctly'], - ['part1[\'\'', 'Path not terminated correctly'], - ['part1[0', 'Path not terminated correctly'], - ['part1[0].', 'Path not terminated correctly'] - ]; - - for (const [pathString, message] of data) { - assert.throws(() => ObjectPropertyAccessor.getPathArray(pathString), {message}); - } -} - - -function testHasProperty() { - const data = [ - [{}, 'invalid', false], - [{}, 0, false], - [{valid: 0}, 'valid', true], - [{null: 0}, null, false], - [[], 'invalid', false], - [[], 0, false], - [[0], 0, true], - [[0], null, false], - ['string', 0, false], - ['string', 'length', false], - ['string', null, false] - ]; - - for (const [object, property, expected] of data) { - assert.strictEqual(ObjectPropertyAccessor.hasProperty(object, property), expected); - } -} - -function testIsValidPropertyType() { - const data = [ - [{}, 'invalid', true], - [{}, 0, false], - [{valid: 0}, 'valid', true], - [{null: 0}, null, false], - [[], 'invalid', false], - [[], 0, true], - [[0], 0, true], - [[0], null, false], - ['string', 0, false], - ['string', 'length', false], - ['string', null, false] - ]; - - for (const [object, property, expected] of data) { - assert.strictEqual(ObjectPropertyAccessor.isValidPropertyType(object, property), expected); - } -} - - -function main() { - testGet1(); - testGet2(); - testSet1(); - testSet2(); - testDelete1(); - testDelete2(); - testSwap1(); - testSwap2(); - testGetPathString1(); - testGetPathString2(); - testGetPathArray1(); - testGetPathArray2(); - testHasProperty(); - testIsValidPropertyType(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-profile-conditions-util.js b/test/test-profile-conditions-util.js deleted file mode 100644 index d5187425..00000000 --- a/test/test-profile-conditions-util.js +++ /dev/null @@ -1,1099 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - - -const vm = new VM({}); -vm.execute([ - 'js/core.js', - 'js/general/cache-map.js', - 'js/data/json-schema.js', - 'js/background/profile-conditions-util.js' -]); -const [ProfileConditionsUtil] = vm.get(['ProfileConditionsUtil']); - - -function testNormalizeContext() { - const data = [ - // Empty - { - context: {}, - expected: {flags: []} - }, - - // Domain normalization - { - context: {url: ''}, - expected: {url: '', flags: []} - }, - { - context: {url: 'http://example.com/'}, - expected: {url: 'http://example.com/', domain: 'example.com', flags: []} - }, - { - context: {url: 'http://example.com:1234/'}, - expected: {url: 'http://example.com:1234/', domain: 'example.com', flags: []} - }, - { - context: {url: 'http://user@example.com:1234/'}, - expected: {url: 'http://user@example.com:1234/', domain: 'example.com', flags: []} - } - ]; - - for (const {context, expected} of data) { - const profileConditionsUtil = new ProfileConditionsUtil(); - const actual = profileConditionsUtil.normalizeContext(context); - vm.assert.deepStrictEqual(actual, expected); - } -} - -function testSchemas() { - const data = [ - // Empty - { - conditionGroups: [], - expectedSchema: {}, - inputs: [ - {expected: true, context: {url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - {conditions: []} - ], - expectedSchema: {}, - inputs: [ - {expected: true, context: {url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - {conditions: []}, - {conditions: []} - ], - expectedSchema: {}, - inputs: [ - {expected: true, context: {url: 'http://example.com/'}} - ] - }, - - // popupLevel tests - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'equal', - value: '0' - } - ] - } - ], - expectedSchema: { - properties: { - depth: {const: 0} - }, - required: ['depth'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: false, context: {depth: 1, url: 'http://example.com/'}}, - {expected: false, context: {depth: -1, url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'notEqual', - value: '0' - } - ] - } - ], - expectedSchema: { - not: [ - { - properties: { - depth: {const: 0} - }, - required: ['depth'] - } - ] - }, - inputs: [ - {expected: false, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 1, url: 'http://example.com/'}}, - {expected: true, context: {depth: -1, url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'lessThan', - value: '0' - } - ] - } - ], - expectedSchema: { - properties: { - depth: { - type: 'number', - exclusiveMaximum: 0 - } - }, - required: ['depth'] - }, - inputs: [ - {expected: false, context: {depth: 0, url: 'http://example.com/'}}, - {expected: false, context: {depth: 1, url: 'http://example.com/'}}, - {expected: true, context: {depth: -1, url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'greaterThan', - value: '0' - } - ] - } - ], - expectedSchema: { - properties: { - depth: { - type: 'number', - exclusiveMinimum: 0 - } - }, - required: ['depth'] - }, - inputs: [ - {expected: false, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 1, url: 'http://example.com/'}}, - {expected: false, context: {depth: -1, url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'lessThanOrEqual', - value: '0' - } - ] - } - ], - expectedSchema: { - properties: { - depth: { - type: 'number', - maximum: 0 - } - }, - required: ['depth'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: false, context: {depth: 1, url: 'http://example.com/'}}, - {expected: true, context: {depth: -1, url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'greaterThanOrEqual', - value: '0' - } - ] - } - ], - expectedSchema: { - properties: { - depth: { - type: 'number', - minimum: 0 - } - }, - required: ['depth'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 1, url: 'http://example.com/'}}, - {expected: false, context: {depth: -1, url: 'http://example.com/'}} - ] - }, - - // url tests - { - conditionGroups: [ - { - conditions: [ - { - type: 'url', - operator: 'matchDomain', - value: 'example.com' - } - ] - } - ], - expectedSchema: { - properties: { - domain: { - oneOf: [ - {const: 'example.com'} - ] - } - }, - required: ['domain'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: false, context: {depth: 0, url: 'http://example1.com/'}}, - {expected: false, context: {depth: 0, url: 'http://example2.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, - {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'url', - operator: 'matchDomain', - value: 'example.com, example1.com, example2.com' - } - ] - } - ], - expectedSchema: { - properties: { - domain: { - oneOf: [ - {const: 'example.com'}, - {const: 'example1.com'}, - {const: 'example2.com'} - ] - } - }, - required: ['domain'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, - {expected: false, context: {depth: 0, url: 'http://example3.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}}, - {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'url', - operator: 'matchRegExp', - value: '^http://example\\d?\\.com/[\\w\\W]*$' - } - ] - } - ], - expectedSchema: { - properties: { - url: { - type: 'string', - pattern: '^http://example\\d?\\.com/[\\w\\W]*$', - patternFlags: 'i' - } - }, - required: ['url'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example1.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example2.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example3.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example.com/example'}}, - {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}}, - {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}}, - {expected: false, context: {depth: 0, url: 'http://example-1.com/'}} - ] - }, - - // modifierKeys tests - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'are', - value: '' - } - ] - } - ], - expectedSchema: { - properties: { - modifierKeys: { - type: 'array', - maxItems: 0, - minItems: 0 - } - }, - required: ['modifierKeys'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'are', - value: 'Alt, Shift' - } - ] - } - ], - expectedSchema: { - properties: { - modifierKeys: { - type: 'array', - maxItems: 2, - minItems: 2, - allOf: [ - {contains: {const: 'Alt'}}, - {contains: {const: 'Shift'}} - ] - } - }, - required: ['modifierKeys'] - }, - inputs: [ - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'areNot', - value: '' - } - ] - } - ], - expectedSchema: { - not: [ - { - properties: { - modifierKeys: { - type: 'array', - maxItems: 0, - minItems: 0 - } - }, - required: ['modifierKeys'] - } - ] - }, - inputs: [ - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'areNot', - value: 'Alt, Shift' - } - ] - } - ], - expectedSchema: { - not: [ - { - properties: { - modifierKeys: { - type: 'array', - maxItems: 2, - minItems: 2, - allOf: [ - {contains: {const: 'Alt'}}, - {contains: {const: 'Shift'}} - ] - } - }, - required: ['modifierKeys'] - } - ] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'include', - value: '' - } - ] - } - ], - expectedSchema: { - properties: { - modifierKeys: { - type: 'array', - minItems: 0 - } - }, - required: ['modifierKeys'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'include', - value: 'Alt, Shift' - } - ] - } - ], - expectedSchema: { - properties: { - modifierKeys: { - type: 'array', - minItems: 2, - allOf: [ - {contains: {const: 'Alt'}}, - {contains: {const: 'Shift'}} - ] - } - }, - required: ['modifierKeys'] - }, - inputs: [ - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'notInclude', - value: '' - } - ] - } - ], - expectedSchema: { - properties: { - modifierKeys: { - type: 'array' - } - }, - required: ['modifierKeys'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'modifierKeys', - operator: 'notInclude', - value: 'Alt, Shift' - } - ] - } - ], - expectedSchema: { - properties: { - modifierKeys: { - type: 'array', - not: [ - {contains: {const: 'Alt'}}, - {contains: {const: 'Shift'}} - ] - } - }, - required: ['modifierKeys'] - }, - inputs: [ - {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt']}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift']}}, - {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['Alt', 'Shift', 'Ctrl']}} - ] - }, - - // flags tests - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'are', - value: '' - } - ] - } - ], - expectedSchema: { - required: ['flags'], - properties: { - flags: { - type: 'array', - maxItems: 0, - minItems: 0 - } - } - }, - inputs: [ - {expected: true, context: {}}, - {expected: true, context: {flags: []}}, - {expected: false, context: {flags: ['test1']}}, - {expected: false, context: {flags: ['test1', 'test2']}}, - {expected: false, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'are', - value: 'test1, test2' - } - ] - } - ], - expectedSchema: { - required: ['flags'], - properties: { - flags: { - type: 'array', - maxItems: 2, - minItems: 2, - allOf: [ - {contains: {const: 'test1'}}, - {contains: {const: 'test2'}} - ] - } - } - }, - inputs: [ - {expected: false, context: {}}, - {expected: false, context: {flags: []}}, - {expected: false, context: {flags: ['test1']}}, - {expected: true, context: {flags: ['test1', 'test2']}}, - {expected: false, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'areNot', - value: '' - } - ] - } - ], - expectedSchema: { - not: [ - { - required: ['flags'], - properties: { - flags: { - type: 'array', - maxItems: 0, - minItems: 0 - } - } - } - ] - }, - inputs: [ - {expected: false, context: {}}, - {expected: false, context: {flags: []}}, - {expected: true, context: {flags: ['test1']}}, - {expected: true, context: {flags: ['test1', 'test2']}}, - {expected: true, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'areNot', - value: 'test1, test2' - } - ] - } - ], - expectedSchema: { - not: [ - { - required: ['flags'], - properties: { - flags: { - type: 'array', - maxItems: 2, - minItems: 2, - allOf: [ - {contains: {const: 'test1'}}, - {contains: {const: 'test2'}} - ] - } - } - } - ] - }, - inputs: [ - {expected: true, context: {}}, - {expected: true, context: {flags: []}}, - {expected: true, context: {flags: ['test1']}}, - {expected: false, context: {flags: ['test1', 'test2']}}, - {expected: true, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'include', - value: '' - } - ] - } - ], - expectedSchema: { - required: ['flags'], - properties: { - flags: { - type: 'array', - minItems: 0 - } - } - }, - inputs: [ - {expected: true, context: {}}, - {expected: true, context: {flags: []}}, - {expected: true, context: {flags: ['test1']}}, - {expected: true, context: {flags: ['test1', 'test2']}}, - {expected: true, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'include', - value: 'test1, test2' - } - ] - } - ], - expectedSchema: { - required: ['flags'], - properties: { - flags: { - type: 'array', - minItems: 2, - allOf: [ - {contains: {const: 'test1'}}, - {contains: {const: 'test2'}} - ] - } - } - }, - inputs: [ - {expected: false, context: {}}, - {expected: false, context: {flags: []}}, - {expected: false, context: {flags: ['test1']}}, - {expected: true, context: {flags: ['test1', 'test2']}}, - {expected: true, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'notInclude', - value: '' - } - ] - } - ], - expectedSchema: { - required: ['flags'], - properties: { - flags: { - type: 'array' - } - } - }, - inputs: [ - {expected: true, context: {}}, - {expected: true, context: {flags: []}}, - {expected: true, context: {flags: ['test1']}}, - {expected: true, context: {flags: ['test1', 'test2']}}, - {expected: true, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'flags', - operator: 'notInclude', - value: 'test1, test2' - } - ] - } - ], - expectedSchema: { - required: ['flags'], - properties: { - flags: { - type: 'array', - not: [ - {contains: {const: 'test1'}}, - {contains: {const: 'test2'}} - ] - } - } - }, - inputs: [ - {expected: true, context: {}}, - {expected: true, context: {flags: []}}, - {expected: false, context: {flags: ['test1']}}, - {expected: false, context: {flags: ['test1', 'test2']}}, - {expected: false, context: {flags: ['test1', 'test2', 'test3']}} - ] - }, - - // Multiple conditions tests - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'greaterThan', - value: '0' - }, - { - type: 'popupLevel', - operator: 'lessThan', - value: '3' - } - ] - } - ], - expectedSchema: { - allOf: [ - { - properties: { - depth: { - type: 'number', - exclusiveMinimum: 0 - } - }, - required: ['depth'] - }, - { - properties: { - depth: { - type: 'number', - exclusiveMaximum: 3 - } - }, - required: ['depth'] - } - ] - }, - inputs: [ - {expected: false, context: {depth: -2, url: 'http://example.com/'}}, - {expected: false, context: {depth: -1, url: 'http://example.com/'}}, - {expected: false, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 1, url: 'http://example.com/'}}, - {expected: true, context: {depth: 2, url: 'http://example.com/'}}, - {expected: false, context: {depth: 3, url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'greaterThan', - value: '0' - }, - { - type: 'popupLevel', - operator: 'lessThan', - value: '3' - } - ] - }, - { - conditions: [ - { - type: 'popupLevel', - operator: 'equal', - value: '0' - } - ] - } - ], - expectedSchema: { - anyOf: [ - { - allOf: [ - { - properties: { - depth: { - type: 'number', - exclusiveMinimum: 0 - } - }, - required: ['depth'] - }, - { - properties: { - depth: { - type: 'number', - exclusiveMaximum: 3 - } - }, - required: ['depth'] - } - ] - }, - { - properties: { - depth: {const: 0} - }, - required: ['depth'] - } - ] - }, - inputs: [ - {expected: false, context: {depth: -2, url: 'http://example.com/'}}, - {expected: false, context: {depth: -1, url: 'http://example.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 1, url: 'http://example.com/'}}, - {expected: true, context: {depth: 2, url: 'http://example.com/'}}, - {expected: false, context: {depth: 3, url: 'http://example.com/'}} - ] - }, - { - conditionGroups: [ - { - conditions: [ - { - type: 'popupLevel', - operator: 'greaterThan', - value: '0' - }, - { - type: 'popupLevel', - operator: 'lessThan', - value: '3' - } - ] - }, - { - conditions: [ - { - type: 'popupLevel', - operator: 'lessThanOrEqual', - value: '0' - }, - { - type: 'popupLevel', - operator: 'greaterThanOrEqual', - value: '-1' - } - ] - } - ], - expectedSchema: { - anyOf: [ - { - allOf: [ - { - properties: { - depth: { - type: 'number', - exclusiveMinimum: 0 - } - }, - required: ['depth'] - }, - { - properties: { - depth: { - type: 'number', - exclusiveMaximum: 3 - } - }, - required: ['depth'] - } - ] - }, - { - allOf: [ - { - properties: { - depth: { - type: 'number', - maximum: 0 - } - }, - required: ['depth'] - }, - { - properties: { - depth: { - type: 'number', - minimum: -1 - } - }, - required: ['depth'] - } - ] - } - ] - }, - inputs: [ - {expected: false, context: {depth: -2, url: 'http://example.com/'}}, - {expected: true, context: {depth: -1, url: 'http://example.com/'}}, - {expected: true, context: {depth: 0, url: 'http://example.com/'}}, - {expected: true, context: {depth: 1, url: 'http://example.com/'}}, - {expected: true, context: {depth: 2, url: 'http://example.com/'}}, - {expected: false, context: {depth: 3, url: 'http://example.com/'}} - ] - } - ]; - - for (const {conditionGroups, expectedSchema, inputs} of data) { - const profileConditionsUtil = new ProfileConditionsUtil(); - const schema = profileConditionsUtil.createSchema(conditionGroups); - if (typeof expectedSchema !== 'undefined') { - vm.assert.deepStrictEqual(schema.schema, expectedSchema); - } - if (Array.isArray(inputs)) { - for (const {expected, context} of inputs) { - const normalizedContext = profileConditionsUtil.normalizeContext(context); - const actual = schema.isValid(normalizedContext); - assert.strictEqual(actual, expected); - } - } - } -} - - -function main() { - testNormalizeContext(); - testSchemas(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-text-source-map.js b/test/test-text-source-map.js deleted file mode 100644 index dd8d3bbd..00000000 --- a/test/test-text-source-map.js +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {VM} = require('../dev/vm'); - -const vm = new VM(); -vm.execute(['js/general/text-source-map.js']); -const TextSourceMap = vm.get('TextSourceMap'); - - -function testSource() { - const data = [ - ['source1'], - ['source2'], - ['source3'] - ]; - - for (const [source] of data) { - const sourceMap = new TextSourceMap(source); - assert.strictEqual(source, sourceMap.source); - } -} - -function testEquals() { - const data = [ - [['source1', null], ['source1', null], true], - [['source2', null], ['source2', null], true], - [['source3', null], ['source3', null], true], - - [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', null], true], - [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', null], true], - [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', null], true], - - [['source1', null], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], - [['source2', null], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], - [['source3', null], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], - - [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], - [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], - [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], - - [['source1', [1, 2, 1, 3]], ['source1', [1, 2, 1, 3]], true], - [['source2', [1, 2, 1, 3]], ['source2', [1, 2, 1, 3]], true], - [['source3', [1, 2, 1, 3]], ['source3', [1, 2, 1, 3]], true], - - [['source1', [1, 3, 1, 2]], ['source1', [1, 2, 1, 3]], false], - [['source2', [1, 3, 1, 2]], ['source2', [1, 2, 1, 3]], false], - [['source3', [1, 3, 1, 2]], ['source3', [1, 2, 1, 3]], false], - - [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source4', [1, 1, 1, 1, 1, 1, 1]], false], - [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source5', [1, 1, 1, 1, 1, 1, 1]], false], - [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source6', [1, 1, 1, 1, 1, 1, 1]], false] - ]; - - for (const [[source1, mapping1], [source2, mapping2], expectedEquals] of data) { - const sourceMap1 = new TextSourceMap(source1, mapping1); - const sourceMap2 = new TextSourceMap(source2, mapping2); - assert.ok(sourceMap1.equals(sourceMap1)); - assert.ok(sourceMap2.equals(sourceMap2)); - assert.strictEqual(sourceMap1.equals(sourceMap2), expectedEquals); - } -} - -function testGetSourceLength() { - const data = [ - [['source', [1, 1, 1, 1, 1, 1]], 1, 1], - [['source', [1, 1, 1, 1, 1, 1]], 2, 2], - [['source', [1, 1, 1, 1, 1, 1]], 3, 3], - [['source', [1, 1, 1, 1, 1, 1]], 4, 4], - [['source', [1, 1, 1, 1, 1, 1]], 5, 5], - [['source', [1, 1, 1, 1, 1, 1]], 6, 6], - - [['source', [2, 2, 2]], 1, 2], - [['source', [2, 2, 2]], 2, 4], - [['source', [2, 2, 2]], 3, 6], - - [['source', [3, 3]], 1, 3], - [['source', [3, 3]], 2, 6], - - [['source', [6, 6]], 1, 6] - ]; - - for (const [[source, mapping], finalLength, expectedValue] of data) { - const sourceMap = new TextSourceMap(source, mapping); - assert.strictEqual(sourceMap.getSourceLength(finalLength), expectedValue); - } -} - -function testCombineInsert() { - const data = [ - // No operations - [ - ['source', null], - ['source', [1, 1, 1, 1, 1, 1]], - [] - ], - - // Combine - [ - ['source', null], - ['source', [3, 1, 1, 1]], - [ - ['combine', 0, 2] - ] - ], - [ - ['source', null], - ['source', [1, 1, 1, 3]], - [ - ['combine', 3, 2] - ] - ], - [ - ['source', null], - ['source', [3, 3]], - [ - ['combine', 0, 2], - ['combine', 1, 2] - ] - ], - [ - ['source', null], - ['source', [3, 3]], - [ - ['combine', 3, 2], - ['combine', 0, 2] - ] - ], - - // Insert - [ - ['source', null], - ['source', [0, 1, 1, 1, 1, 1, 1]], - [ - ['insert', 0, 0] - ] - ], - [ - ['source', null], - ['source', [1, 1, 1, 1, 1, 1, 0]], - [ - ['insert', 6, 0] - ] - ], - [ - ['source', null], - ['source', [0, 1, 1, 1, 1, 1, 1, 0]], - [ - ['insert', 0, 0], - ['insert', 7, 0] - ] - ], - [ - ['source', null], - ['source', [0, 1, 1, 1, 1, 1, 1, 0]], - [ - ['insert', 6, 0], - ['insert', 0, 0] - ] - ], - - // Mixed - [ - ['source', null], - ['source', [3, 0, 3]], - [ - ['combine', 0, 2], - ['insert', 1, 0], - ['combine', 2, 2] - ] - ], - [ - ['source', null], - ['source', [3, 0, 3]], - [ - ['combine', 0, 2], - ['combine', 1, 2], - ['insert', 1, 0] - ] - ], - [ - ['source', null], - ['source', [3, 0, 3]], - [ - ['insert', 3, 0], - ['combine', 0, 2], - ['combine', 2, 2] - ] - ] - ]; - - for (const [[source, mapping], [expectedSource, expectedMapping], operations] of data) { - const sourceMap = new TextSourceMap(source, mapping); - const expectedSourceMap = new TextSourceMap(expectedSource, expectedMapping); - for (const [operation, ...args] of operations) { - switch (operation) { - case 'combine': - sourceMap.combine(...args); - break; - case 'insert': - sourceMap.insert(...args); - break; - } - } - assert.ok(sourceMap.equals(expectedSourceMap)); - } -} - - -function main() { - testSource(); - testEquals(); - testGetSourceLength(); - testCombineInsert(); -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-translator.js b/test/test-translator.js deleted file mode 100644 index 485eb665..00000000 --- a/test/test-translator.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const fs = require('fs'); -const path = require('path'); -const assert = require('assert'); -const {testMain} = require('../dev/util'); -const {TranslatorVM} = require('../dev/translator-vm'); - - -function clone(value) { - return JSON.parse(JSON.stringify(value)); -} - - -async function main() { - const write = (process.argv[2] === '--write'); - - const translatorVM = new TranslatorVM(); - const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1'); - await translatorVM.prepare(dictionaryDirectory, 'Test Dictionary 2'); - - const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json'); - const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'})); - - const testResults1FilePath = path.join(__dirname, 'data', 'translator-test-results.json'); - const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'})); - const actualResults1 = []; - - const testResults2FilePath = path.join(__dirname, 'data', 'translator-test-results-note-data1.json'); - const expectedResults2 = JSON.parse(fs.readFileSync(testResults2FilePath, {encoding: 'utf8'})); - const actualResults2 = []; - - for (let i = 0, ii = tests.length; i < ii; ++i) { - const test = tests[i]; - const expected1 = expectedResults1[i]; - const expected2 = expectedResults2[i]; - switch (test.func) { - case 'findTerms': - { - const {name, mode, text} = test; - const options = translatorVM.buildOptions(optionsPresets, test.options); - const {dictionaryEntries, originalTextLength} = clone(await translatorVM.translator.findTerms(mode, text, options)); - const noteDataList = mode !== 'simple' ? clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), mode))) : null; - actualResults1.push({name, originalTextLength, dictionaryEntries}); - actualResults2.push({name, noteDataList}); - if (!write) { - assert.deepStrictEqual(originalTextLength, expected1.originalTextLength); - assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries); - assert.deepStrictEqual(noteDataList, expected2.noteDataList); - } - } - break; - case 'findKanji': - { - const {name, text} = test; - const options = translatorVM.buildOptions(optionsPresets, test.options); - const dictionaryEntries = clone(await translatorVM.translator.findKanji(text, options)); - const noteDataList = clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), null))); - actualResults1.push({name, dictionaryEntries}); - actualResults2.push({name, noteDataList}); - if (!write) { - assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries); - assert.deepStrictEqual(noteDataList, expected2.noteDataList); - } - } - break; - } - } - - if (write) { - // Use 2 indent instead of 4 to save a bit of file size - fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'}); - fs.writeFileSync(testResults2FilePath, JSON.stringify(actualResults2, null, 2), {encoding: 'utf8'}); - } -} - - -if (require.main === module) { testMain(main); } diff --git a/test/test-workers.js b/test/test-workers.js deleted file mode 100644 index b4ec4d7d..00000000 --- a/test/test-workers.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan Authors - * Copyright (C) 2020-2022 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -const fs = require('fs'); -const path = require('path'); -const {JSDOM} = require('jsdom'); -const {VM} = require('../dev/vm'); -const assert = require('assert'); - - -class StubClass { - prepare() { - // NOP - } -} - - -function loadEslint() { - return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.eslintrc.json'), {encoding: 'utf8'})); -} - -function filterScriptPaths(scriptPaths) { - const extDirName = 'ext'; - return scriptPaths.filter((src) => !src.startsWith('/lib/')).map((src) => `${extDirName}${src}`); -} - -function getAllHtmlScriptPaths(fileName) { - const domSource = fs.readFileSync(fileName, {encoding: 'utf8'}); - const dom = new JSDOM(domSource); - const {window} = dom; - const {document} = window; - try { - const scripts = document.querySelectorAll('script'); - return [...scripts].map(({src}) => src); - } finally { - window.close(); - } -} - -function convertBackgroundScriptsToServiceWorkerScripts(scripts) { - // Use parse5-based SimpleDOMParser - scripts.splice(0, 0, '/lib/parse5.js'); - const index = scripts.indexOf('/js/dom/native-simple-dom-parser.js'); - assert.ok(index >= 0); - scripts[index] = '/js/dom/simple-dom-parser.js'; -} - -function getImportedScripts(scriptPath, fields) { - const importedScripts = []; - - const importScripts = (...scripts) => { - importedScripts.push(...scripts); - }; - - const vm = new VM(Object.assign({importScripts}, fields)); - vm.context.self = vm.context; - vm.execute([scriptPath]); - - return importedScripts; -} - -function testServiceWorker() { - // Verify that sw.js scripts match background.html scripts - const extDir = path.join(__dirname, '..', 'ext'); - const scripts = getAllHtmlScriptPaths(path.join(extDir, 'background.html')); - convertBackgroundScriptsToServiceWorkerScripts(scripts); - const importedScripts = getImportedScripts('sw.js', {}); - assert.deepStrictEqual(scripts, importedScripts); - - // Verify that eslint config lists files correctly - const expectedSwRulesFiles = filterScriptPaths(scripts); - const eslintConfig = loadEslint(); - const swRules = eslintConfig.overrides.find((item) => ( - typeof item.env === 'object' && - item.env !== null && - item.env.serviceworker === true - )); - assert.ok(typeof swRules !== 'undefined'); - assert.ok(Array.isArray(swRules.files)); - assert.deepStrictEqual(swRules.files, expectedSwRulesFiles); -} - -function testWorkers() { - testWorker( - 'js/language/dictionary-worker-main.js', - {DictionaryWorkerHandler: StubClass} - ); -} - -function testWorker(scriptPath, fields) { - // Get script paths - const scripts = getImportedScripts(scriptPath, fields); - - // Verify that eslint config lists files correctly - const expectedRulesFiles = filterScriptPaths(scripts); - const expectedRulesFilesSet = new Set(expectedRulesFiles); - const eslintConfig = loadEslint(); - const rules = eslintConfig.overrides.find((item) => ( - typeof item.env === 'object' && - item.env !== null && - item.env.worker === true - )); - assert.ok(typeof rules !== 'undefined'); - assert.ok(Array.isArray(rules.files)); - assert.deepStrictEqual(rules.files.filter((v) => expectedRulesFilesSet.has(v)), expectedRulesFiles); -} - - -function main() { - try { - testServiceWorker(); - testWorkers(); - } catch (e) { - console.error(e); - process.exit(-1); - return; - } - process.exit(0); -} - - -if (require.main === module) { main(); } diff --git a/test/text-source-map.test.js b/test/text-source-map.test.js new file mode 100644 index 00000000..aeaba000 --- /dev/null +++ b/test/text-source-map.test.js @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {expect, test} from 'vitest'; +import {TextSourceMap} from '../ext/js/general/text-source-map.js'; + +function testSource() { + test('Source', () => { + const data = [ + ['source1'], + ['source2'], + ['source3'] + ]; + + for (const [source] of data) { + const sourceMap = new TextSourceMap(source); + expect(source).toStrictEqual(sourceMap.source); + } + }); +} + +function testEquals() { + test('Equals', () => { + const data = [ + [['source1', null], ['source1', null], true], + [['source2', null], ['source2', null], true], + [['source3', null], ['source3', null], true], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', null], true], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', null], true], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', null], true], + + [['source1', null], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], + [['source2', null], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], + [['source3', null], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], + + [['source1', [1, 2, 1, 3]], ['source1', [1, 2, 1, 3]], true], + [['source2', [1, 2, 1, 3]], ['source2', [1, 2, 1, 3]], true], + [['source3', [1, 2, 1, 3]], ['source3', [1, 2, 1, 3]], true], + + [['source1', [1, 3, 1, 2]], ['source1', [1, 2, 1, 3]], false], + [['source2', [1, 3, 1, 2]], ['source2', [1, 2, 1, 3]], false], + [['source3', [1, 3, 1, 2]], ['source3', [1, 2, 1, 3]], false], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source4', [1, 1, 1, 1, 1, 1, 1]], false], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source5', [1, 1, 1, 1, 1, 1, 1]], false], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source6', [1, 1, 1, 1, 1, 1, 1]], false] + ]; + + for (const [[source1, mapping1], [source2, mapping2], expectedEquals] of data) { + const sourceMap1 = new TextSourceMap(source1, mapping1); + const sourceMap2 = new TextSourceMap(source2, mapping2); + expect(sourceMap1.equals(sourceMap1)).toBe(true); + expect(sourceMap2.equals(sourceMap2)).toBe(true); + expect(sourceMap1.equals(sourceMap2)).toStrictEqual(expectedEquals); + } + }); +} + +function testGetSourceLength() { + test('GetSourceLength', () => { + const data = [ + [['source', [1, 1, 1, 1, 1, 1]], 1, 1], + [['source', [1, 1, 1, 1, 1, 1]], 2, 2], + [['source', [1, 1, 1, 1, 1, 1]], 3, 3], + [['source', [1, 1, 1, 1, 1, 1]], 4, 4], + [['source', [1, 1, 1, 1, 1, 1]], 5, 5], + [['source', [1, 1, 1, 1, 1, 1]], 6, 6], + + [['source', [2, 2, 2]], 1, 2], + [['source', [2, 2, 2]], 2, 4], + [['source', [2, 2, 2]], 3, 6], + + [['source', [3, 3]], 1, 3], + [['source', [3, 3]], 2, 6], + + [['source', [6, 6]], 1, 6] + ]; + + for (const [[source, mapping], finalLength, expectedValue] of data) { + const sourceMap = new TextSourceMap(source, mapping); + expect(sourceMap.getSourceLength(finalLength)).toStrictEqual(expectedValue); + } + }); +} + +function testCombineInsert() { + test('CombineInsert', () => { + const data = [ + // No operations + [ + ['source', null], + ['source', [1, 1, 1, 1, 1, 1]], + [] + ], + + // Combine + [ + ['source', null], + ['source', [3, 1, 1, 1]], + [ + ['combine', 0, 2] + ] + ], + [ + ['source', null], + ['source', [1, 1, 1, 3]], + [ + ['combine', 3, 2] + ] + ], + [ + ['source', null], + ['source', [3, 3]], + [ + ['combine', 0, 2], + ['combine', 1, 2] + ] + ], + [ + ['source', null], + ['source', [3, 3]], + [ + ['combine', 3, 2], + ['combine', 0, 2] + ] + ], + + // Insert + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1]], + [ + ['insert', 0, 0] + ] + ], + [ + ['source', null], + ['source', [1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 6, 0] + ] + ], + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 0, 0], + ['insert', 7, 0] + ] + ], + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 6, 0], + ['insert', 0, 0] + ] + ], + + // Mixed + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['combine', 0, 2], + ['insert', 1, 0], + ['combine', 2, 2] + ] + ], + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['combine', 0, 2], + ['combine', 1, 2], + ['insert', 1, 0] + ] + ], + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['insert', 3, 0], + ['combine', 0, 2], + ['combine', 2, 2] + ] + ] + ]; + + for (const [[source, mapping], [expectedSource, expectedMapping], operations] of data) { + const sourceMap = new TextSourceMap(source, mapping); + const expectedSourceMap = new TextSourceMap(expectedSource, expectedMapping); + for (const [operation, ...args] of operations) { + switch (operation) { + case 'combine': + sourceMap.combine(...args); + break; + case 'insert': + sourceMap.insert(...args); + break; + } + } + expect(sourceMap.equals(expectedSourceMap)).toBe(true); + } + }); +} + + +function main() { + testSource(); + testEquals(); + testGetSourceLength(); + testCombineInsert(); +} + + +main(); diff --git a/test/translator.test.js b/test/translator.test.js new file mode 100644 index 00000000..7a827d39 --- /dev/null +++ b/test/translator.test.js @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {IDBKeyRange, indexedDB} from 'fake-indexeddb'; +import fs from 'fs'; +import {fileURLToPath} from 'node:url'; +import path from 'path'; +import {expect, test, vi} from 'vitest'; +import {TranslatorVM} from '../dev/translator-vm'; + +vi.stubGlobal('indexedDB', indexedDB); +vi.stubGlobal('IDBKeyRange', IDBKeyRange); + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function main() { + const translatorVM = new TranslatorVM(); + const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', 'valid-dictionary1'); + await translatorVM.prepare(dictionaryDirectory, 'Test Dictionary 2'); + + const testInputsFilePath = path.join(dirname, 'data', 'translator-test-inputs.json'); + const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'})); + + const testResults1FilePath = path.join(dirname, 'data', 'translator-test-results.json'); + const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'})); + const actualResults1 = []; + + const testResults2FilePath = path.join(dirname, 'data', 'translator-test-results-note-data1.json'); + const expectedResults2 = JSON.parse(fs.readFileSync(testResults2FilePath, {encoding: 'utf8'})); + const actualResults2 = []; + + for (let i = 0, ii = tests.length; i < ii; ++i) { + test(`${i}`, async () => { + const t = tests[i]; + const expected1 = expectedResults1[i]; + const expected2 = expectedResults2[i]; + switch (t.func) { + case 'findTerms': + { + const {name, mode, text} = t; + const options = translatorVM.buildOptions(optionsPresets, t.options); + const {dictionaryEntries, originalTextLength} = structuredClone(await translatorVM.translator.findTerms(mode, text, options)); + const noteDataList = mode !== 'simple' ? structuredClone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(structuredClone(dictionaryEntry), mode))) : null; + actualResults1.push({name, originalTextLength, dictionaryEntries}); + actualResults2.push({name, noteDataList}); + expect(originalTextLength).toStrictEqual(expected1.originalTextLength); + expect(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries); + expect(noteDataList).toEqual(expected2.noteDataList); + } + break; + case 'findKanji': + { + const {name, text} = t; + const options = translatorVM.buildOptions(optionsPresets, t.options); + const dictionaryEntries = structuredClone(await translatorVM.translator.findKanji(text, options)); + const noteDataList = structuredClone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(structuredClone(dictionaryEntry), null))); + actualResults1.push({name, dictionaryEntries}); + actualResults2.push({name, noteDataList}); + expect(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries); + expect(noteDataList).toEqual(expected2.noteDataList); + } + break; + } + }); + } +} + +await main(); |