aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2023-12-18 22:47:29 -0500
committerGitHub <noreply@github.com>2023-12-19 03:47:29 +0000
commit46821eeb7fc9e00645aeae1c7fce3e6e7b637ca0 (patch)
tree7ed0b027946081bbc88bffefc0bfcb2c20d15106
parent521e87d01142063c785054741d3703de37a1636c (diff)
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
-rw-r--r--test/anki-note-builder.test.js130
-rw-r--r--test/document-util.test.js10
-rw-r--r--test/dom-text-scanner.test.js7
-rw-r--r--test/fixtures/dom-test.js (renamed from test/document-test.js)4
-rw-r--r--test/fixtures/translator-test.js125
-rw-r--r--test/jsconfig.json1
-rw-r--r--test/translator.test.js71
-rw-r--r--test/utilities/anki.js44
-rw-r--r--test/utilities/translator.js81
-rw-r--r--types/test/anki-note-builder.d.ts41
10 files changed, 376 insertions, 138 deletions
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,56 +18,20 @@
// @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<import('dev/vm').PseudoFetchResponse>}
- */
-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<TranslatorVM>}
- */
-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-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<HTMLElement>} */ (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<HTMLElement>} */ (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/document-test.js b/test/fixtures/dom-test.js
index 9d763816..8cfe80a9 100644
--- a/test/document-test.js
+++ b/test/fixtures/dom-test.js
@@ -39,11 +39,11 @@ function prepareWindow(window) {
* @param {string} [htmlFilePath]
* @returns {import('vitest').TestAPI<{window: import('jsdom').DOMWindow}>}
*/
-export function domTest(htmlFilePath) {
+export function createDomTest(htmlFilePath) {
+ const html = typeof htmlFilePath === 'string' ? fs.readFileSync(htmlFilePath, {encoding: 'utf8'}) : '<!DOCTYPE html>';
return test.extend({
// eslint-disable-next-line no-empty-pattern
window: async ({}, use) => {
- const html = typeof htmlFilePath === 'string' ? fs.readFileSync(htmlFilePath, {encoding: 'utf8'}) : '<!DOCTYPE html>';
const env = builtinEnvironments.jsdom;
const {teardown} = await env.setup(global, {jsdom: {html}});
const window = /** @type {import('jsdom').DOMWindow} */ (/** @type {unknown} */ (global.window));
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 <https://www.gnu.org/licenses/>.
+ */
+
+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<import('dev/vm').PseudoFetchResponse>}
+ */
+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<import('vitest').TestAPI<{window: import('jsdom').DOMWindow, translator: Translator, ankiNoteDataCreator: AnkiNoteDataCreator}>>}
+ */
+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 <https://www.gnu.org/licenses/>.
*/
-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 <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+
+/**
+ * @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 <https://www.gnu.org/licenses/>.
+ */
+
+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;
+};