From 46821eeb7fc9e00645aeae1c7fce3e6e7b637ca0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 18 Dec 2023 22:47:29 -0500 Subject: Test fixtures (#371) * Move and rename document-test.js file * Only load HTML file content once * Move testDoc construction * Add createTranslatorTest * Add utilities * Update translator tests * Rename * Refactor anki note builder tests * Refactor * Use internal expect * Updates * Remove actual results * Remove concurrent --- test/anki-note-builder.test.js | 130 +++++++++++++------------------------- test/document-test.js | 58 ----------------- test/document-util.test.js | 10 +-- test/dom-text-scanner.test.js | 7 +- test/fixtures/dom-test.js | 58 +++++++++++++++++ test/fixtures/translator-test.js | 125 ++++++++++++++++++++++++++++++++++++ test/jsconfig.json | 1 + test/translator.test.js | 71 +++++++++------------ test/utilities/anki.js | 44 +++++++++++++ test/utilities/translator.js | 81 ++++++++++++++++++++++++ types/test/anki-note-builder.d.ts | 41 ++++++++++++ 11 files changed, 432 insertions(+), 194 deletions(-) delete mode 100644 test/document-test.js create mode 100644 test/fixtures/dom-test.js create mode 100644 test/fixtures/translator-test.js create mode 100644 test/utilities/anki.js create mode 100644 test/utilities/translator.js create mode 100644 types/test/anki-note-builder.d.ts diff --git a/test/anki-note-builder.test.js b/test/anki-note-builder.test.js index 42ee2290..cc136957 100644 --- a/test/anki-note-builder.test.js +++ b/test/anki-note-builder.test.js @@ -18,55 +18,19 @@ // @vitest-environment jsdom -import 'fake-indexeddb/auto'; -import fs from 'fs'; +import {readFileSync} 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 {describe, vi} from 'vitest'; import {AnkiNoteBuilder} from '../ext/js/data/anki-note-builder.js'; import {JapaneseUtil} from '../ext/js/language/sandbox/japanese-util.js'; +import {createTranslatorTest} from './fixtures/translator-test.js'; +import {createFindOptions} from './utilities/translator.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); -/** - * @param {string} url2 - * @returns {Promise} - */ -async function fetch(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.stubGlobal('fetch', fetch); vi.mock('../ext/js/templates/template-renderer-proxy.js', async () => await import('../test/mocks/template-renderer-proxy.js')); -/** - * @returns {Promise} - */ -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; -} - /** * @param {'terms'|'kanji'} type * @returns {string[]} @@ -205,50 +169,44 @@ async function getRenderResults(dictionaryEntries, type, mode, template, expect) } -/** */ -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; - /** @type {import('translation').FindTermsOptions} */ - 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; - /** @type {import('translation').FindKanjiOptions} */ - const options = vm.buildOptions(optionsPresets, t.options); - const dictionaryEntries = structuredClone(await vm.translator.findKanji(text, options)); - const results = structuredClone(await getRenderResults(dictionaryEntries, 'kanji', 'split', template, expect)); - actualResults1.push({name, results}); - expect(results).toStrictEqual(expected1.results); - } - break; - } - }); - } +const testInputsFilePath = path.join(dirname, 'data/translator-test-inputs.json'); +/** @type {import('test/anki-note-builder').TranslatorTestInputs} */ +const {optionsPresets, tests} = JSON.parse(readFileSync(testInputsFilePath, {encoding: 'utf8'})); + +const testResults1FilePath = path.join(dirname, 'data/anki-note-builder-test-results.json'); +const expectedResults1 = JSON.parse(readFileSync(testResults1FilePath, {encoding: 'utf8'})); + +const template = readFileSync(path.join(dirname, '../ext/data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'}); + +const dictionaryName = 'Test Dictionary 2'; +const test = await createTranslatorTest(void 0, path.join(dirname, 'data/dictionaries/valid-dictionary1'), dictionaryName); + +describe('AnkiNoteBuilder', () => { + const testData = tests.map((data, i) => ({data, expected1: expectedResults1[i]})); + describe.each(testData)('Test %#: $data.name', ({data, expected1}) => { + test('Test', async ({expect, translator}) => { + switch (data.func) { + case 'findTerms': + { + const {mode, text} = data; + /** @type {import('translation').FindTermsOptions} */ + const options = createFindOptions(dictionaryName, optionsPresets, data.options); + const {dictionaryEntries} = await translator.findTerms(mode, text, options); + const results = mode !== 'simple' ? await getRenderResults(dictionaryEntries, 'terms', mode, template, expect) : null; + expect(results).toStrictEqual(expected1.results); + } + break; + case 'findKanji': + { + const {text} = data; + /** @type {import('translation').FindKanjiOptions} */ + const options = createFindOptions(dictionaryName, optionsPresets, data.options); + const dictionaryEntries = await translator.findKanji(text, options); + const results = await getRenderResults(dictionaryEntries, 'kanji', 'split', template, expect); + expect(results).toStrictEqual(expected1.results); + } + break; + } + }); }); -} -await main(); +}); diff --git a/test/document-test.js b/test/document-test.js deleted file mode 100644 index 9d763816..00000000 --- a/test/document-test.js +++ /dev/null @@ -1,58 +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 . - */ - -import fs from 'fs'; -import {test} from 'vitest'; -import {builtinEnvironments} from 'vitest/environments'; - -/** - * @param {import('jsdom').DOMWindow} window - */ -function prepareWindow(window) { - const {document} = 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; -} - -/** - * @param {string} [htmlFilePath] - * @returns {import('vitest').TestAPI<{window: import('jsdom').DOMWindow}>} - */ -export function domTest(htmlFilePath) { - return test.extend({ - // eslint-disable-next-line no-empty-pattern - window: async ({}, use) => { - const html = typeof htmlFilePath === 'string' ? fs.readFileSync(htmlFilePath, {encoding: 'utf8'}) : ''; - const env = builtinEnvironments.jsdom; - const {teardown} = await env.setup(global, {jsdom: {html}}); - const window = /** @type {import('jsdom').DOMWindow} */ (/** @type {unknown} */ (global.window)); - prepareWindow(window); - try { - await use(window); - } finally { - teardown(global); - } - } - }); -} diff --git a/test/document-util.test.js b/test/document-util.test.js index 10857df9..51872422 100644 --- a/test/document-util.test.js +++ b/test/document-util.test.js @@ -23,7 +23,7 @@ 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'; -import {domTest} from './document-test.js'; +import {createDomTest} from './fixtures/dom-test.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -109,9 +109,10 @@ function findImposterElement(document) { return document.querySelector('div[style*="2147483646"]>*'); } +const test = createDomTest(path.join(dirname, 'data/html/test-document1.html')); + describe('DocumentUtil', () => { - const testDoc = domTest(path.join(dirname, 'data/html/test-document1.html')); - testDoc('Text scanning functions', ({window}) => { + test('Text scanning functions', ({window}) => { const {document} = window; for (const testElement of /** @type {NodeListOf} */ (document.querySelectorAll('.test[data-test-type=scan]'))) { // Get test parameters @@ -228,8 +229,7 @@ describe('DocumentUtil', () => { }); describe('DOMTextScanner', () => { - const testDoc = domTest(path.join(dirname, 'data/html/test-document1.html')); - testDoc('Seek functions', async ({window}) => { + test('Seek functions', async ({window}) => { const {document} = window; for (const testElement of /** @type {NodeListOf} */ (document.querySelectorAll('.test[data-test-type=text-source-range-seek]'))) { // Get test parameters diff --git a/test/dom-text-scanner.test.js b/test/dom-text-scanner.test.js index 76e95a09..d62e334d 100644 --- a/test/dom-text-scanner.test.js +++ b/test/dom-text-scanner.test.js @@ -20,7 +20,7 @@ import {fileURLToPath} from 'node:url'; import path from 'path'; import {describe, expect} from 'vitest'; import {DOMTextScanner} from '../ext/js/dom/dom-text-scanner.js'; -import {domTest} from './document-test.js'; +import {createDomTest} from './fixtures/dom-test.js'; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -101,9 +101,10 @@ function createAbsoluteGetComputedStyle(window) { } +const test = createDomTest(path.join(dirname, 'data/html/test-dom-text-scanner.html')); + describe('DOMTextScanner', () => { - const testDoc = domTest(path.join(dirname, 'data/html/test-dom-text-scanner.html')); - testDoc('Seek tests', ({window}) => { + test('Seek tests', ({window}) => { const {document} = window; window.getComputedStyle = createAbsoluteGetComputedStyle(window); diff --git a/test/fixtures/dom-test.js b/test/fixtures/dom-test.js new file mode 100644 index 00000000..8cfe80a9 --- /dev/null +++ b/test/fixtures/dom-test.js @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import fs from 'fs'; +import {test} from 'vitest'; +import {builtinEnvironments} from 'vitest/environments'; + +/** + * @param {import('jsdom').DOMWindow} window + */ +function prepareWindow(window) { + const {document} = 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; +} + +/** + * @param {string} [htmlFilePath] + * @returns {import('vitest').TestAPI<{window: import('jsdom').DOMWindow}>} + */ +export function createDomTest(htmlFilePath) { + const html = typeof htmlFilePath === 'string' ? fs.readFileSync(htmlFilePath, {encoding: 'utf8'}) : ''; + return test.extend({ + // eslint-disable-next-line no-empty-pattern + window: async ({}, use) => { + const env = builtinEnvironments.jsdom; + const {teardown} = await env.setup(global, {jsdom: {html}}); + const window = /** @type {import('jsdom').DOMWindow} */ (/** @type {unknown} */ (global.window)); + prepareWindow(window); + try { + await use(window); + } finally { + teardown(global); + } + } + }); +} diff --git a/test/fixtures/translator-test.js b/test/fixtures/translator-test.js new file mode 100644 index 00000000..b17c37d9 --- /dev/null +++ b/test/fixtures/translator-test.js @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * Copyright (C) 2020-2022 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import {IDBKeyRange, indexedDB} from 'fake-indexeddb'; +import {readFileSync} from 'fs'; +import {fileURLToPath, pathToFileURL} from 'node:url'; +import {dirname, join, resolve} from 'path'; +import {expect, vi} from 'vitest'; +import {createDictionaryArchive} from '../../dev/util.js'; +import {AnkiNoteDataCreator} from '../../ext/js/data/sandbox/anki-note-data-creator.js'; +import {DictionaryDatabase} from '../../ext/js/language/dictionary-database.js'; +import {DictionaryImporter} from '../../ext/js/language/dictionary-importer.js'; +import {JapaneseUtil} from '../../ext/js/language/sandbox/japanese-util.js'; +import {Translator} from '../../ext/js/language/translator.js'; +import {DictionaryImporterMediaLoader} from '../mocks/dictionary-importer-media-loader.js'; +import {createDomTest} from './dom-test.js'; + +const extDir = join(dirname(fileURLToPath(import.meta.url)), '../../ext'); +const deinflectionReasonsPath = join(extDir, 'data/deinflect.json'); + +/** @type {import('dev/vm').PseudoChrome} */ +const chrome = { + runtime: { + getURL: (path) => { + return pathToFileURL(join(extDir, path.replace(/^\//, ''))).href; + } + } +}; + +/** + * @param {string} url + * @returns {Promise} + */ +async function fetch(url) { + let filePath; + try { + filePath = fileURLToPath(url); + } catch (e) { + filePath = resolve(extDir, url.replace(/^[/\\]/, '')); + } + await Promise.resolve(); + const content = readFileSync(filePath, {encoding: null}); + return { + ok: true, + status: 200, + statusText: 'OK', + text: async () => content.toString('utf8'), + json: async () => JSON.parse(content.toString('utf8')) + }; +} + +vi.stubGlobal('indexedDB', indexedDB); +vi.stubGlobal('IDBKeyRange', IDBKeyRange); +vi.stubGlobal('fetch', fetch); +vi.stubGlobal('chrome', chrome); + +/** + * @param {string} dictionaryDirectory + * @param {string} dictionaryName + * @returns {Promise<{translator: Translator, ankiNoteDataCreator: AnkiNoteDataCreator}>} + */ +async function createTranslatorContext(dictionaryDirectory, dictionaryName) { + // Dictionary + const testDictionary = createDictionaryArchive(dictionaryDirectory, dictionaryName); + const testDictionaryContent = await testDictionary.generateAsync({type: 'arraybuffer'}); + + // Setup database + const dictionaryImporterMediaLoader = new DictionaryImporterMediaLoader(); + const dictionaryImporter = new DictionaryImporter(dictionaryImporterMediaLoader); + const dictionaryDatabase = new DictionaryDatabase(); + await dictionaryDatabase.prepare(); + + const {errors} = await dictionaryImporter.importDictionary( + dictionaryDatabase, + testDictionaryContent, + {prefixWildcardsSupported: true} + ); + + expect(errors.length).toEqual(0); + + // Setup translator + const japaneseUtil = new JapaneseUtil(null); + const translator = new Translator({japaneseUtil, database: dictionaryDatabase}); + const deinflectionReasons = JSON.parse(readFileSync(deinflectionReasonsPath, {encoding: 'utf8'})); + translator.prepare(deinflectionReasons); + + // Assign properties + const ankiNoteDataCreator = new AnkiNoteDataCreator(japaneseUtil); + return {translator, ankiNoteDataCreator}; +} + +/** + * @param {string|undefined} htmlFilePath + * @param {string} dictionaryDirectory + * @param {string} dictionaryName + * @returns {Promise>} + */ +export async function createTranslatorTest(htmlFilePath, dictionaryDirectory, dictionaryName) { + const test = createDomTest(htmlFilePath); + const {translator, ankiNoteDataCreator} = await createTranslatorContext(dictionaryDirectory, dictionaryName); + /** @type {import('vitest').TestAPI<{window: import('jsdom').DOMWindow, translator: Translator, ankiNoteDataCreator: AnkiNoteDataCreator}>} */ + const result = test.extend({ + window: async ({window}, use) => { await use(window); }, + // eslint-disable-next-line no-empty-pattern + translator: async ({}, use) => { await use(translator); }, + // eslint-disable-next-line no-empty-pattern + ankiNoteDataCreator: async ({}, use) => { await use(ankiNoteDataCreator); } + }); + return result; +} diff --git a/test/jsconfig.json b/test/jsconfig.json index c587abe6..9ab0c332 100644 --- a/test/jsconfig.json +++ b/test/jsconfig.json @@ -31,6 +31,7 @@ "../ext/**/*.js", "../types/ext/**/*.ts", "../types/dev/**/*.ts", + "../types/test/**/*.ts", "../types/other/globals.d.ts" ], "exclude": [ diff --git a/test/translator.test.js b/test/translator.test.js index 3db560a7..59887d7e 100644 --- a/test/translator.test.js +++ b/test/translator.test.js @@ -16,50 +16,41 @@ * along with this program. If not, see . */ -import {IDBKeyRange, indexedDB} from 'fake-indexeddb'; -import fs from 'fs'; +import {readFileSync} 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); +import {describe} from 'vitest'; +import {createTranslatorTest} from './fixtures/translator-test.js'; +import {createTestAnkiNoteData} from './utilities/anki.js'; +import {createFindOptions} from './utilities/translator.js'; 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'); +/** @type {import('test/anki-note-builder').TranslatorTestInputs} */ +const {optionsPresets, tests} = JSON.parse(readFileSync(testInputsFilePath, {encoding: 'utf8'})); - 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(readFileSync(testResults1FilePath, {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(readFileSync(testResults2FilePath, {encoding: 'utf8'})); - const testResults2FilePath = path.join(dirname, 'data', 'translator-test-results-note-data1.json'); - const expectedResults2 = JSON.parse(fs.readFileSync(testResults2FilePath, {encoding: 'utf8'})); - const actualResults2 = []; +const dictionaryName = 'Test Dictionary 2'; +const test = await createTranslatorTest(void 0, path.join(dirname, 'data/dictionaries/valid-dictionary1'), dictionaryName); - 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) { +describe('Translator', () => { + const testData = tests.map((data, i) => ({data, expected1: expectedResults1[i], expected2: expectedResults2[i]})); + describe.each(testData)('Test %#: $data.name', ({data, expected1, expected2}) => { + test('Test', async ({translator, ankiNoteDataCreator, expect}) => { + switch (data.func) { case 'findTerms': { - const {name, mode, text} = t; + const {mode, text} = data; /** @type {import('translation').FindTermsOptions} */ - 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}); + const options = createFindOptions(dictionaryName, optionsPresets, data.options); + const {dictionaryEntries, originalTextLength} = await translator.findTerms(mode, text, options); + const noteDataList = mode !== 'simple' ? dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, mode)) : null; expect(originalTextLength).toStrictEqual(expected1.originalTextLength); expect(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries); expect(noteDataList).toEqual(expected2.noteDataList); @@ -67,20 +58,16 @@ async function main() { break; case 'findKanji': { - const {name, text} = t; + const {text} = data; /** @type {import('translation').FindKanjiOptions} */ - 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), 'split'))); - actualResults1.push({name, dictionaryEntries}); - actualResults2.push({name, noteDataList}); + const options = createFindOptions(dictionaryName, optionsPresets, data.options); + const dictionaryEntries = await translator.findKanji(text, options); + const noteDataList = dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, 'split')); expect(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries); expect(noteDataList).toEqual(expected2.noteDataList); } break; } }); - } -} - -await main(); + }); +}); diff --git a/test/utilities/anki.js b/test/utilities/anki.js new file mode 100644 index 00000000..0a651c30 --- /dev/null +++ b/test/utilities/anki.js @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 Yomitan 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 . + */ + +/** + * @param {import('../../ext/js/data/sandbox/anki-note-data-creator.js').AnkiNoteDataCreator} ankiNoteDataCreator + * @param {import('dictionary').DictionaryEntry} dictionaryEntry + * @param {import('settings').ResultOutputMode} mode + * @returns {import('anki-templates').NoteData} + * @throws {Error} + */ +export function createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, mode) { + const marker = '{marker}'; + /** @type {import('anki-templates-internal').CreateDetails} */ + const data = { + dictionaryEntry, + resultOutputMode: mode, + mode: 'test', + glossaryLayoutMode: 'default', + compactTags: false, + context: { + url: 'url:', + sentence: {text: '', offset: 0}, + documentTitle: 'title', + query: 'query', + fullQuery: 'fullQuery' + }, + media: {} + }; + return ankiNoteDataCreator.create(marker, data); +} diff --git a/test/utilities/translator.js b/test/utilities/translator.js new file mode 100644 index 00000000..9073b206 --- /dev/null +++ b/test/utilities/translator.js @@ -0,0 +1,81 @@ +/* + * 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 . + */ + + +/** + * @template {import('translation').FindTermsOptions|import('translation').FindKanjiOptions} T + * @param {string} dictionaryName + * @param {import('dev/vm').OptionsPresetObject} optionsPresets + * @param {string|import('dev/vm').OptionsPresetObject|(string|import('dev/vm').OptionsPresetObject)[]} optionsArray + * @returns {T} + * @throws {Error} + */ +export function createFindOptions(dictionaryName, optionsPresets, optionsArray) { + /** @type {import('core').UnknownObject} */ + const options = {}; + if (!Array.isArray(optionsArray)) { optionsArray = [optionsArray]; } + for (const entry of optionsArray) { + switch (typeof entry) { + case 'string': + if (!Object.prototype.hasOwnProperty.call(optionsPresets, entry)) { + throw new Error('Invalid options preset'); + } + Object.assign(options, structuredClone(optionsPresets[entry])); + break; + case 'object': + Object.assign(options, structuredClone(entry)); + break; + default: + throw new Error('Invalid options type'); + } + } + + // Construct regex + if (Array.isArray(options.textReplacements)) { + options.textReplacements = options.textReplacements.map((value) => { + if (Array.isArray(value)) { + value = value.map(({pattern, flags, replacement}) => ({pattern: new RegExp(pattern, flags), replacement})); + } + return value; + }); + } + + // Update structure + const placeholder = '${title}'; + if (options.mainDictionary === placeholder) { + options.mainDictionary = dictionaryName; + } + let {enabledDictionaryMap} = options; + if (Array.isArray(enabledDictionaryMap)) { + for (const entry of enabledDictionaryMap) { + if (entry[0] === placeholder) { + entry[0] = dictionaryName; + } + } + enabledDictionaryMap = new Map(enabledDictionaryMap); + options.enabledDictionaryMap = enabledDictionaryMap; + } + const {excludeDictionaryDefinitions} = options; + options.excludeDictionaryDefinitions = ( + Array.isArray(excludeDictionaryDefinitions) ? + new Set(excludeDictionaryDefinitions) : + null + ); + + return /** @type {T} */ (options); +} diff --git a/types/test/anki-note-builder.d.ts b/types/test/anki-note-builder.d.ts new file mode 100644 index 00000000..0ccb25e9 --- /dev/null +++ b/types/test/anki-note-builder.d.ts @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import type {OptionsPresetObject} from 'dev/vm'; +import type {FindTermsMode} from 'translator'; + +export type TranslatorTestInputs = { + optionsPresets: OptionsPresetObject; + tests: TestInput[]; +}; + +export type TestInput = TestInputFindKanji | TestInputFindTerm; + +export type TestInputFindKanji = { + func: 'findKanji'; + name: string; + text: string; + options: string; +}; + +export type TestInputFindTerm = { + func: 'findTerms'; + name: string; + mode: FindTermsMode; + text: string; + options: string; +}; -- cgit v1.2.3