Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 190 lines 6.5 kB view raw
1/* 2 * Copyright (C) 2023-2025 Yomitan Authors 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18import Ajv from 'ajv'; 19import {readFileSync} from 'fs'; 20import {join, dirname as pathDirname} from 'path'; 21import {createGenerator} from 'ts-json-schema-generator'; 22import {fileURLToPath} from 'url'; 23import {describe, expect, test} from 'vitest'; 24import {parseJson} from '../dev/json.js'; 25import {getAllFiles} from '../dev/util.js'; 26 27const dirname = pathDirname(fileURLToPath(import.meta.url)); 28const rootDir = join(dirname, '..'); 29 30/** 31 * @param {import('test/json').JsconfigType|undefined} jsconfigType 32 * @returns {string} 33 */ 34function getJsconfigPath(jsconfigType) { 35 let path; 36 switch (jsconfigType) { 37 case 'dev': path = '../dev/jsconfig.json'; break; 38 case 'test': path = '../test/jsconfig.json'; break; 39 case 'benches': path = '../benches/jsconfig.json'; break; 40 default: path = '../jsconfig.json'; break; 41 } 42 return join(dirname, path); 43} 44 45/** 46 * @returns {Ajv} 47 */ 48function createAjv() { 49 return new Ajv({ 50 meta: true, 51 strictTuples: false, 52 allowUnionTypes: true, 53 }); 54} 55 56/** 57 * @param {string} path 58 * @param {string} type 59 * @param {import('test/json').JsconfigType|undefined} jsconfigType 60 * @returns {import('ajv').ValidateFunction<unknown>} 61 */ 62function createValidatorFunctionFromTypeScript(path, type, jsconfigType) { 63 /** @type {import('ts-json-schema-generator/dist/src/Config').Config} */ 64 const config = { 65 path, 66 tsconfig: getJsconfigPath(jsconfigType), 67 type, 68 jsDoc: 'none', 69 additionalProperties: false, 70 minify: false, 71 expose: 'none', 72 strictTuples: true, 73 }; 74 const schema = createGenerator(config).createSchema(config.type); 75 const ajv = createAjv(); 76 return ajv.compile(schema); 77} 78 79/** 80 * @param {string} path 81 * @returns {import('ajv').ValidateFunction<unknown>} 82 */ 83function createValidatorFunctionFromSchemaJson(path) { 84 /** @type {import('ajv').Schema} */ 85 const schema = parseJson(readFileSync(path, {encoding: 'utf8'})); 86 const ajv = createAjv(); 87 return ajv.compile(schema); 88} 89 90/** 91 * @param {string} value 92 * @returns {string} 93 */ 94function normalizePathDirectorySeparators(value) { 95 return value.replace(/\\/g, '/'); 96} 97 98 99describe.concurrent('JSON validation', () => { 100 const ignoreDirectories = new Set([ 101 'builds', 102 'dictionaries', 103 'node_modules', 104 'playwright-report', 105 'playwright', 106 'test-results', 107 'dev/lib', 108 'test/playwright', 109 ]); 110 111 const existingJsonFiles = getAllFiles(rootDir, (path, isDirectory) => { 112 const fileNameNormalized = normalizePathDirectorySeparators(path); 113 return ( 114 isDirectory ? 115 !ignoreDirectories.has(fileNameNormalized) : 116 /\.json$/i.test(fileNameNormalized) 117 ); 118 }); 119 /** @type {Set<string>} */ 120 const existingJsonFileSet = new Set(); 121 for (const path of existingJsonFiles) { 122 existingJsonFileSet.add(normalizePathDirectorySeparators(path)); 123 } 124 125 const jsonFileName = 'json.json'; 126 127 /** @type {import('test/json').JsonInfo} */ 128 const jsonFileData = parseJson(readFileSync(join(dirname, `data/${jsonFileName}`), {encoding: 'utf8'})); 129 130 test(`Each item in ${jsonFileName} must have a unique path`, () => { 131 /** @type {Set<string>} */ 132 const set = new Set(); 133 for (const {path} of jsonFileData.files) { 134 set.add(path); 135 } 136 expect(set.size).toBe(jsonFileData.files.length); 137 }); 138 139 /** @type {Map<string, import('test/json').JsonFileInfo>} */ 140 const jsonFileMap = new Map(); 141 for (const item of jsonFileData.files) { 142 jsonFileMap.set(item.path, item); 143 } 144 145 // Validate file existance 146 const requiredFiles = jsonFileData.files.filter((v) => !v.ignore); 147 test.each(requiredFiles)('File must exist in project: $path', ({path}) => { 148 expect(existingJsonFileSet.has(path)).toBe(true); 149 }); 150 151 // Validate new files 152 const existingJsonFiles2 = existingJsonFiles.map((path) => ({path: normalizePathDirectorySeparators(path)})); 153 test.each(existingJsonFiles2)(`File must exist in ${jsonFileName}: $path`, ({path}) => { 154 expect(jsonFileMap.has(path)).toBe(true); 155 }); 156 157 // Validate schemas 1 158 /** @type {import('test/json').JsonFileParseInfo[]} */ 159 const schemaValidationTargets1 = []; 160 for (const info of jsonFileData.files) { 161 if (info.ignore || !existingJsonFileSet.has(info.path)) { continue; } 162 schemaValidationTargets1.push(info); 163 } 164 test.each(schemaValidationTargets1)('Validating file against TypeScript: $path', ({path, typeFile, type, jsconfig}) => { 165 const validate = createValidatorFunctionFromTypeScript(join(rootDir, typeFile), type, jsconfig); 166 const data = parseJson(readFileSync(join(rootDir, path), {encoding: 'utf8'})); 167 const valid = validate(data); 168 const {errors} = validate; 169 expect(errors).toBe(null); 170 expect(valid).toBe(true); 171 }); 172 173 // Validate schemas 2 174 /** @type {{path: string, schema: string}[]} */ 175 const schemaValidationTargets2 = []; 176 for (const info of jsonFileData.files) { 177 if (info.ignore || !existingJsonFileSet.has(info.path)) { continue; } 178 const {schema, path} = info; 179 if (typeof schema !== 'string') { continue; } 180 schemaValidationTargets2.push({schema, path}); 181 } 182 test.each(schemaValidationTargets2)('Validating file against schema: $path', ({path, schema}) => { 183 const validate = createValidatorFunctionFromSchemaJson(join(rootDir, schema)); 184 const data = parseJson(readFileSync(join(rootDir, path), {encoding: 'utf8'})); 185 const valid = validate(data); 186 const {errors} = validate; 187 expect(errors).toBe(null); 188 expect(valid).toBe(true); 189 }); 190});