diff options
Diffstat (limited to 'test/json.test.js')
-rw-r--r-- | test/json.test.js | 189 |
1 files changed, 189 insertions, 0 deletions
diff --git a/test/json.test.js b/test/json.test.js new file mode 100644 index 00000000..8cf01491 --- /dev/null +++ b/test/json.test.js @@ -0,0 +1,189 @@ +/* + * 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 Ajv from 'ajv'; +import {readFileSync} from 'fs'; +import {join, dirname as pathDirname} from 'path'; +import {createGenerator} from 'ts-json-schema-generator'; +import {fileURLToPath} from 'url'; +import {describe, expect, test} from 'vitest'; +import {parseJson} from '../dev/json.js'; +import {getAllFiles} from '../dev/util.js'; + +const dirname = pathDirname(fileURLToPath(import.meta.url)); +const rootDir = join(dirname, '..'); + +/** + * @param {import('test/json').JsconfigType|undefined} jsconfigType + * @returns {string} + */ +function getJsconfigPath(jsconfigType) { + let path; + switch (jsconfigType) { + case 'dev': path = '../dev/jsconfig.json'; break; + case 'test': path = '../test/jsconfig.json'; break; + default: path = '../jsconfig.json'; break; + } + return join(dirname, path); +} + +/** + * @returns {Ajv} + */ +function createAjv() { + return new Ajv({ + meta: true, + strictTuples: false, + allowUnionTypes: true + }); +} + +/** + * @param {string} path + * @param {string} type + * @param {import('test/json').JsconfigType|undefined} jsconfigType + * @returns {import('ajv').ValidateFunction<unknown>} + */ +function createValidatorFunctionFromTypeScript(path, type, jsconfigType) { + /** @type {import('ts-json-schema-generator/dist/src/Config').Config} */ + const config = { + path, + tsconfig: getJsconfigPath(jsconfigType), + type, + jsDoc: 'none', + additionalProperties: false, + minify: false, + expose: 'none', + strictTuples: true + }; + const schema = createGenerator(config).createSchema(config.type); + const ajv = createAjv(); + return ajv.compile(schema); +} + +/** + * @param {string} path + * @returns {import('ajv').ValidateFunction<unknown>} + */ +function createValidatorFunctionFromSchemaJson(path) { + /** @type {import('ajv').Schema} */ + const schema = parseJson(readFileSync(path, {encoding: 'utf8'})); + const ajv = createAjv(); + return ajv.compile(schema); +} + +/** + * @param {string} value + * @returns {string} + */ +function normalizePathDirectorySeparators(value) { + return value.replace(/\\/g, '/'); +} + + +describe.concurrent('JSON validation', () => { + const ignoreDirectories = new Set([ + 'builds', + 'dictionaries', + 'node_modules', + 'playwright-report', + 'playwright', + 'test-results', + 'dev/lib', + 'test/playwright' + ]); + + const existingJsonFiles = getAllFiles(rootDir, (path, isDirectory) => { + const fileNameNormalized = normalizePathDirectorySeparators(path); + if (isDirectory) { + return !ignoreDirectories.has(fileNameNormalized); + } else { + return /\.json$/i.test(fileNameNormalized); + } + }); + /** @type {Set<string>} */ + const existingJsonFileSet = new Set(); + for (const path of existingJsonFiles) { + existingJsonFileSet.add(normalizePathDirectorySeparators(path)); + } + + const jsonFileName = 'json.json'; + + /** @type {import('test/json').JsonInfo} */ + const jsonFileData = parseJson(readFileSync(join(dirname, `data/${jsonFileName}`), {encoding: 'utf8'})); + + test(`Each item in ${jsonFileName} must have a unique path`, () => { + /** @type {Set<string>} */ + const set = new Set(); + for (const {path} of jsonFileData.files) { + set.add(path); + } + expect(set.size).toBe(jsonFileData.files.length); + }); + + /** @type {Map<string, import('test/json').JsonFileInfo>} */ + const jsonFileMap = new Map(); + for (const item of jsonFileData.files) { + jsonFileMap.set(item.path, item); + } + + // Validate file existance + const requiredFiles = jsonFileData.files.filter((v) => !v.ignore); + test.each(requiredFiles)('File must exist in project: $path', ({path}) => { + expect(existingJsonFileSet.has(path)).toBe(true); + }); + + // Validate new files + const existingJsonFiles2 = existingJsonFiles.map((path) => ({path: normalizePathDirectorySeparators(path)})); + test.each(existingJsonFiles2)(`File must exist in ${jsonFileName}: $path`, ({path}) => { + expect(jsonFileMap.has(path)).toBe(true); + }); + + // Validate schemas 1 + /** @type {import('test/json').JsonFileParseInfo[]} */ + const schemaValidationTargets1 = []; + for (const info of jsonFileData.files) { + if (info.ignore || !existingJsonFileSet.has(info.path)) { continue; } + schemaValidationTargets1.push(info); + } + test.each(schemaValidationTargets1)('Validating file against TypeScript: $path', ({path, typeFile, type, jsconfig}) => { + const validate = createValidatorFunctionFromTypeScript(join(rootDir, typeFile), type, jsconfig); + const data = parseJson(readFileSync(join(rootDir, path), {encoding: 'utf8'})); + const valid = validate(data); + const {errors} = validate; + expect(errors).toBe(null); + expect(valid).toBe(true); + }); + + // Validate schemas 2 + /** @type {{path: string, schema: string}[]} */ + const schemaValidationTargets2 = []; + for (const info of jsonFileData.files) { + if (info.ignore || !existingJsonFileSet.has(info.path)) { continue; } + const {schema, path} = info; + if (typeof schema !== 'string') { continue; } + schemaValidationTargets2.push({schema, path}); + } + test.each(schemaValidationTargets2)('Validating file against schema: $path', ({path, schema}) => { + const validate = createValidatorFunctionFromSchemaJson(join(rootDir, schema)); + const data = parseJson(readFileSync(join(rootDir, path), {encoding: 'utf8'})); + const valid = validate(data); + const {errors} = validate; + expect(errors).toBe(null); + expect(valid).toBe(true); + }); +}); |