Skip to content

Commit

Permalink
Test fixtures (#371)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
toasted-nutbread authored Dec 19, 2023
1 parent 521e87d commit 46821ee
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 138 deletions.
130 changes: 44 additions & 86 deletions test/anki-note-builder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<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[]}
Expand Down Expand Up @@ -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();
});
10 changes: 5 additions & 5 deletions test/document-util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions test/dom-text-scanner.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions test/document-test.js → test/fixtures/dom-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
125 changes: 125 additions & 0 deletions test/fixtures/translator-test.js
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions test/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"../ext/**/*.js",
"../types/ext/**/*.ts",
"../types/dev/**/*.ts",
"../types/test/**/*.ts",
"../types/other/globals.d.ts"
],
"exclude": [
Expand Down
Loading

0 comments on commit 46821ee

Please sign in to comment.