Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
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});