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/fixtures/dom-test.js | 58 ++++++++++++++++++ test/fixtures/translator-test.js | 125 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 test/fixtures/dom-test.js create mode 100644 test/fixtures/translator-test.js (limited to 'test/fixtures') 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; +} -- cgit v1.2.3