This commit is contained in:
TheGiddyLimit
2024-01-01 19:34:49 +00:00
parent 332769043f
commit 8117ebddc5
1748 changed files with 2544409 additions and 1 deletions

184
test/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,184 @@
module.exports = {
"extends": "eslint:recommended",
"env": {
"browser": false,
"es6": true,
"jquery": true,
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
},
"rules": {
"accessor-pairs": "off",
"arrow-spacing": ["error", {"before": true, "after": true}],
"block-spacing": ["error", "always"],
"brace-style": ["error", "1tbs", {"allowSingleLine": true}],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "always-multiline",
}],
"comma-spacing": ["error", {"before": false, "after": true}],
"comma-style": ["error", "last"],
"constructor-super": "error",
"curly": ["error", "multi-line"],
"dot-location": ["error", "property"],
"eqeqeq": ["error", "always", {"null": "ignore"}],
"func-call-spacing": ["error", "never"],
"generator-star-spacing": ["error", {"before": false, "after": true}],
"handle-callback-err": ["error", "^(err|error)$"],
"indent": [
"error",
"tab",
{
"SwitchCase": 1,
},
],
"key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
"keyword-spacing": ["error", {"before": true, "after": true}],
"new-cap": ["error", {"newIsCap": true, "capIsNew": false}],
"new-parens": "error",
"no-array-constructor": "error",
"no-caller": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": ["error", {"checkLoops": false}],
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-extra-parens": ["error", "functions"],
"no-fallthrough": "error",
"no-floating-decimal": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-implied-eval": "error",
"no-inner-declarations": ["error", "functions"],
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": ["error", {"allowLoop": true, "allowSwitch": false}],
"no-lone-blocks": "error",
"no-mixed-operators": ["error", {
"groups": [
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["&&", "||"],
["in", "instanceof"],
],
"allowSamePrecedence": true,
}],
"no-mixed-spaces-and-tabs": "error",
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": ["error", {"max": 1, "maxEOF": 0}],
"no-negated-in-lhs": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-symbol": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-path-concat": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-return-await": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-template-curly-in-string": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef": "off",
"no-undef-init": "error",
"no-unexpected-multiline": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": ["error", {"defaultAssignment": false}],
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unused-expressions": ["error", {
"allowShortCircuit": true,
"allowTernary": true,
"allowTaggedTemplates": true,
}],
"no-unused-vars": "off",
"no-use-before-define": ["error", {"functions": false, "classes": false, "variables": false}],
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-escape": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-whitespace-before-property": "error",
"no-with": "error",
"object-property-newline": ["error", {"allowMultiplePropertiesPerLine": true}],
"one-var": ["error", {"initialized": "never"}],
"operator-linebreak": ["error", "after", {
"overrides": {
"?": "before",
":": "before",
"+": "before",
"-": "before",
"*": "before",
"/": "before",
"||": "before",
"&&": "before",
},
}],
"padded-blocks": ["error", {"blocks": "never", "switches": "never", "classes": "never"}],
"prefer-promise-reject-errors": "error",
"rest-spread-spacing": ["error", "never"],
"semi": ["warn", "always"],
"semi-spacing": ["error", {"before": false, "after": true}],
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", "always"],
"space-in-parens": ["error", "never"],
"space-infix-ops": "error",
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
"spaced-comment": ["error", "always", {
"line": {"markers": ["*package", "!", "/", ",", "="]},
"block": {
"balanced": true,
"markers": ["*package", "!", ",", ":", "::", "flow-include"],
"exceptions": ["*"],
},
}],
"symbol-description": "error",
"template-curly-spacing": ["error", "never"],
"template-tag-spacing": ["error", "never"],
"unicode-bom": ["error", "never"],
"use-isnan": "error",
"valid-typeof": ["error", {"requireStringLiterals": true}],
"wrap-iife": ["error", "any", {"functionPrototypeMethods": true}],
"yield-star-spacing": ["error", "both"],
"yoda": ["error", "never"],
"no-prototype-builtins": "off",
"require-atomic-updates": "off",
"no-console": 0,
"prefer-template": "error",
"quotes": ["error", "double", {"allowTemplateLiterals": true}],
"no-var": "error",
},
};

1
test/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.log

View File

@@ -0,0 +1,31 @@
import "../js/parser.js";
import "../js/utils.js";
import * as ut from "../node/util.js";
async function main () {
console.log(`##### Validating adventure/book contents #####`);
const errors = [];
[
{filename: "adventures.json", prop: "adventure", dir: "adventure"},
{filename: "books.json", prop: "book", dir: "book"},
].flatMap(({filename, prop, dir}) => ut.readJson(`./data/${filename}`)[prop]
.map(({id, contents}) => ({filename: `./data/${dir}/${dir}-${id.toLowerCase()}.json`, contents})))
.forEach(({filename, contents}) => {
const json = ut.readJson(filename);
if (json.data.length === contents.length) return;
errors.push(`Contents length did not match data length in "${filename}"`);
});
if (errors.length) {
errors.forEach(err => console.error(err));
return false;
}
return true;
}
export default main();

View File

@@ -0,0 +1,65 @@
import "../js/parser.js";
import "../js/utils.js";
import * as ut from "../node/util.js";
const getClosestEntryId = stack => {
const ent = [...stack].reverse().find(ent => ent.id);
if (!ent) return null;
return ent.id;
};
async function main () {
console.log(`##### Validating adventure/book map grids #####`);
const errors = [];
const walker = MiscUtil.getWalker({isNoModification: true, keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST});
const IMAGE_TYPES_MAP = new Set(["map", "mapPlayer"]);
[
{filename: "adventures.json", prop: "adventure", dir: "adventure"},
{filename: "books.json", prop: "book", dir: "book"},
]
.flatMap(({filename, prop, dir}) => ut.readJson(`./data/${filename}`)[prop]
.map(({id}) => ({filename: `./data/${dir}/${dir}-${id.toLowerCase()}.json`})))
.forEach(({filename}) => {
const json = ut.readJson(filename);
const errorsFile = [];
const stack = [];
walker.walk(
json,
{
object: (obj) => {
if (obj.type !== "image" || !IMAGE_TYPES_MAP.has(obj.imageType)) return;
if (obj.grid !== undefined) return;
const closestEntryId = getClosestEntryId(stack);
const ptsId = [
obj.id ? `id "${obj.id}"` : "",
obj.mapParent?.id ? `parent id "${obj.mapParent.id}"` : "",
closestEntryId ? `closest entry id "${closestEntryId}"` : "",
]
.filter(Boolean)
.join("; ");
errorsFile.push(`${obj.title ? `"${obj.title}"` : "[Untitled]"}${ptsId ? ` (${ptsId})` : ""}`);
},
},
null,
stack,
);
if (!errorsFile.length) return;
errors.push(`Found maps with no "grid" in "${filename}"\n${errorsFile.map(it => `\t${it}`).join("\n")}`);
});
if (errors.length) {
errors.forEach(err => console.error(err));
return false;
}
return true;
}
export default main();

View File

@@ -0,0 +1,54 @@
import "../js/parser.js";
import "../js/utils.js";
import * as ut from "../node/util.js";
async function main () {
console.log(`##### Validating adventure/book page numbers #####`);
const walker = MiscUtil.getWalker({isNoModification: true});
const errors = [];
[
{filename: "adventures.json", prop: "adventure", dir: "adventure"},
{filename: "books.json", prop: "book", dir: "book"},
].flatMap(({filename, prop, dir}) => ut.readJson(`./data/${filename}`)[prop]
.map(({id}) => `./data/${dir}/${dir}-${id.toLowerCase()}.json`))
.forEach(filename => {
const stack = [];
let pagePrev = -1;
const errorsFile = [];
walker.walk(
ut.readJson(filename),
{
object: (obj) => {
if (obj.page == null || typeof obj.page !== "number") return obj;
if (obj.page < pagePrev) {
const id = obj.id || [...stack].reverse().find(it => it.id)?.id;
const name = obj.name || [...stack].reverse().find(it => it.name)?.name;
errorsFile.push(`Previous page ${pagePrev} > ${obj.page}${id || name ? ` (at or near ${[id ? `id "${id}"` : "", name ? `name "${name}"` : ""].filter(Boolean).join("; ")})` : ""}`);
}
pagePrev = obj.page;
return obj;
},
},
null,
stack,
);
if (!errorsFile.length) return;
errors.push(`Page numbers were not monotonically increasing in "${filename}":\n${errorsFile.map(err => `\t${err}\n`).join("")}\n`);
});
if (errors.length) {
errors.forEach(err => console.error(err));
return false;
}
return true;
}
export default main();

28
test/test-all.js Normal file
View File

@@ -0,0 +1,28 @@
"use strict";
function handleFail () {
console.error("Tests failed!");
process.exit(1);
}
async function main () {
if (!(await (await import("./test-tags.js")).default)) handleFail();
if (!(await (await import("./test-images.js")).default)) handleFail();
if (!(await (await import("./test-image-paths.js")).default)) handleFail();
await (await import("./test-pagenumbers.js")).default; // don't fail on missing page numbers
if (!(await (await import("./test-json.js")).default)) handleFail();
if (!(await (await import("./test-misc.js")).default)) handleFail();
if (!(await (await import("./test-multisource.js")).default)) handleFail();
if (!(await (await import("./test-language-fonts.js")).default)) handleFail();
if (!(await (await import("./test-adventure-book-contents.js")).default)) handleFail();
await (await import("./test-adventure-book-map-grids.js")).default; // don't fail on missing map grids
if (!(await (await import("./test-foundry.js")).default)) handleFail();
process.exit(0);
}
main()
.then(() => console.log("Tests complete."))
.catch(e => {
console.error(e);
throw e;
});

165
test/test-foundry.js Normal file
View File

@@ -0,0 +1,165 @@
import * as fs from "fs";
import * as ut from "../node/util.js";
import "../js/parser.js";
import "../js/utils.js";
import "../js/render.js";
import "../js/render-dice.js";
class TestFoundry {
static async pLoadData (originalFilename, originalPath) {
switch (originalFilename) {
case "races.json": {
ut.patchLoadJson();
const out = await DataUtil.race.loadJSON({isAddBaseRaces: true});
ut.unpatchLoadJson();
return out;
}
default: return ut.readJson(originalPath);
}
}
static testClasses ({errors}) {
const classIndex = ut.readJson("./data/class/index.json");
const classFiles = Object.values(classIndex)
.map(file => ut.readJson(`./data/class/${file}`));
const uidsClass = new Set();
const uidsClassFeature = new Set();
const uidsSubclassFeature = new Set();
classFiles.forEach(data => {
(data.class || []).forEach(cls => {
const uid = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](cls);
uidsClass.add(uid);
});
(data.classFeature || []).forEach(cf => {
const uid = UrlUtil.URL_TO_HASH_BUILDER["classFeature"](cf);
uidsClassFeature.add(uid);
});
(data.subclassFeature || []).forEach(scf => {
const uid = UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](scf);
uidsSubclassFeature.add(uid);
});
});
const foundryData = ut.readJson("./data/class/foundry.json");
(foundryData.class || []).forEach(cls => {
const uid = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](cls);
if (!uidsClass.has(uid)) errors.push(`\tClass "${uid}" not found!`);
});
(foundryData.classFeature || []).forEach(fcf => {
const uid = UrlUtil.URL_TO_HASH_BUILDER["classFeature"](fcf);
if (!uidsClassFeature.has(uid)) errors.push(`\tClass feature "${uid}" not found!`);
});
(foundryData.subclassFeature || []).forEach(fscf => {
const uid = UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](fscf);
if (!uidsSubclassFeature.has(uid)) errors.push(`\tSubclass feature "${uid}" not found!`);
});
}
static async pTestDir ({errors, dir}) {
const FOUNDRY_FILE = "foundry.json";
const dirList = fs.readdirSync(`./data/${dir}`)
.filter(it => !it.startsWith("fluff-") && it !== "sources.json" && it !== "index.json");
if (!dirList.includes(FOUNDRY_FILE)) throw new Error(`No "${FOUNDRY_FILE}" file found in dir "${dir}"!`);
const foundryPath = `./data/${dir}/${FOUNDRY_FILE}`;
const foundryData = ut.readJson(foundryPath);
const originalDatas = await dirList
.filter(it => it !== FOUNDRY_FILE)
.pSerialAwaitMap(it => this.pLoadData(it, `./data/${dir}/${it}`));
await this.pTestFile({errors, foundryData, foundryPath, originalDatas});
}
static async pTestRoot ({errors}) {
const dirList = fs.readdirSync(`./data/`);
const foundryFiles = dirList.filter(it => it.startsWith("foundry-") && it.endsWith(".json"));
for (const foundryFile of foundryFiles) {
const foundryPath = `./data/${foundryFile}`;
const foundryData = ut.readJson(foundryPath);
const originalFile = foundryFile.replace(/^foundry-/i, "");
const originalData = await this.pLoadData(originalFile, `./data/${originalFile}`);
await this.pTestFile({errors, foundryData, foundryPath, originalDatas: [originalData]});
}
}
static testSpecialRaceFeatures ({foundryData, originalDatas, errors}) {
const uidsRaceFeature = new Set();
const HASH_BUILDER = it => UrlUtil.encodeForHash([it.name, it.source, it.raceName, it.raceSource]);
originalDatas.forEach(originalData => {
originalData.race.forEach(race => {
DataUtil.generic.getVersions(race).forEach(ver => this._testSpecialRaceFeatures_addRace({HASH_BUILDER, uidsRaceFeature, race: ver}));
this._testSpecialRaceFeatures_addRace({HASH_BUILDER, uidsRaceFeature, race});
});
});
foundryData.raceFeature.forEach(raceFeature => {
const uid = HASH_BUILDER(raceFeature);
if (!uidsRaceFeature.has(uid)) errors.push(`\tRace feature "${uid}" not found!`);
});
}
static _testSpecialRaceFeatures_addRace ({HASH_BUILDER, uidsRaceFeature, race}) {
(race.entries || []).forEach(ent => {
const uid = HASH_BUILDER({source: race.source, ...ent, raceName: race.name, raceSource: race.source});
uidsRaceFeature.add(uid);
});
}
static async pTestSpecialMagicItemVariants ({foundryData, originalDatas, errors}) {
const variants = await this.pLoadData("magicvariants.json", `./data/magicvariants.json`);
const prop = "magicvariant";
this.doCompareData({prop, foundryData, originalDatas: [variants], errors});
}
static doCompareData ({prop, foundryData, originalDatas, errors}) {
foundryData[prop].forEach(it => {
const match = originalDatas.first(variants => variants[prop].find(og => og.name === it.name && (og?.inherits?.source ?? og.source) === it.source));
if (!match) errors.push(`\t"${prop}" ${it.name} (${it.source}) not found!`);
});
}
static async pTestFile ({foundryPath, foundryData, originalDatas, errors}) {
Object.entries(foundryData)
.forEach(([prop, arr]) => {
if (SPECIAL_PROPS[prop]) return SPECIAL_PROPS[prop]({foundryPath, foundryData, originalDatas, errors});
if (!(arr instanceof Array)) return;
if (originalDatas.every(originalData => !originalData[prop] || !(originalData[prop] instanceof Array))) return console.warn(`\tUntested prop "${prop}" in file ${foundryPath}`);
this.doCompareData({prop, foundryData, originalDatas, errors});
});
}
}
const SPECIAL_PROPS = {
"raceFeature": TestFoundry.testSpecialRaceFeatures.bind(TestFoundry),
"magicvariant": TestFoundry.pTestSpecialMagicItemVariants.bind(TestFoundry),
};
async function main () {
const errors = [];
TestFoundry.testClasses({errors});
await TestFoundry.pTestDir({dir: "spells", errors});
await TestFoundry.pTestRoot({errors});
if (!errors.length) console.log("##### Foundry Tests Passed #####");
else {
console.error("Foundry data errors:");
errors.forEach(err => console.error(err));
}
return !errors.length;
}
export default main();

82
test/test-image-paths.js Normal file
View File

@@ -0,0 +1,82 @@
import "../js/parser.js";
import "../js/utils.js";
import "../js/render.js";
import * as ut from "../node/util.js";
import * as fs from "fs";
const _IS_FIX = false;
async function main () {
console.log(`##### Validating image paths #####`);
const walker = MiscUtil.getWalker({isNoModification: true});
const errors = [];
const doTestFile = (filePath, imgDirs) => {
const errorsFile = [];
const data = ut.readJson(filePath);
walker.walk(
data,
{
object: (obj) => {
if (obj.type !== "image" || obj.href?.type !== "internal") return;
const dir = obj.href.path.split("/")[0];
if (imgDirs.includes(dir)) return;
if (!_IS_FIX) {
errorsFile.push(`Path ${obj.href.path} had incorrect image path beginning "${dir}"`);
return;
}
const pathOut = `${imgDirs[0]}/${obj.href.path.slice(dir.length + 1)}`;
const pathInFull = `img/${obj.href.path}`;
const pathOutFull = `img/${pathOut}`;
fs.mkdirSync(pathOutFull.split("/").slice(0, -1).join("/"), {recursive: true});
fs.copyFileSync(pathInFull, pathOutFull);
console.log(`Copied file: "${pathInFull}" -> "${pathOutFull}"`);
obj.href.path = pathOut;
},
},
);
if (!errorsFile.length) return data;
errors.push(`Cross-page image paths found in "${filePath}":\n${errorsFile.map(err => `\t${err}\n`).join("")}\n`);
return data;
};
[
{index: "bestiary/fluff-index.json", imgDirs: ["bestiary"]},
{index: "spells/fluff-index.json", imgDirs: ["spells"]},
]
.forEach(meta => {
if (meta.index) {
const dir = meta.index.split("/")[0];
Object.values(ut.readJson(`data/${meta.index}`))
.forEach(filename => {
const dataPath = `data/${dir}/${filename}`;
const data = doTestFile(dataPath, meta.imgDirs);
if (_IS_FIX) fs.writeFileSync(dataPath, CleanUtil.getCleanJson(data, {isFast: false}), "utf-8");
});
return;
}
throw new Error("Unimplemented!");
});
if (errors.length) {
errors.forEach(err => console.error(err));
return false;
}
return true;
}
export default main();

169
test/test-images.js Normal file
View File

@@ -0,0 +1,169 @@
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
import * as ut from "../node/util.js";
class _TestTokenImages {
static _IS_CLEAN_MM_EXTRAS = false;
static _PATH_BASE = `./img/bestiary/tokens`;
static _EXT = "webp";
static _IGNORED_PREFIXES = [
".",
"_",
];
static _expected = new Set();
static _expectedDirs = {};
static _existing = new Set();
static _expectedFromHashToken = {};
static _mmTokens = null;
static _isMmToken (filename) {
if (!this._mmTokens) this._mmTokens = fs.readdirSync(`${this._PATH_BASE}/${Parser.sourceJsonToAbv(Parser.SRC_MM)}`).mergeMap(it => ({[it]: true}));
return !!this._mmTokens[filename.split("/").last()];
}
static _readBestiaryJson () {
fs.readdirSync("./data/bestiary")
.filter(file => file.startsWith("bestiary") && file.endsWith(".json"))
.forEach(file => {
ut.readJson(`./data/bestiary/${file}`).monster
.forEach(m => {
const source = Parser.sourceJsonToAbv(m.source);
const implicitTokenPath = `${this._PATH_BASE}/${source}/${Parser.nameToTokenName(m.name)}.${this._EXT}`;
if (m.hasToken) this._expectedFromHashToken[implicitTokenPath] = true;
if (!fs.existsSync(`${this._PATH_BASE}/${source}`)) {
this._expectedDirs[source] = true;
return;
}
this._expected.add(implicitTokenPath);
// add tokens specified as part of variants
if (m.variant) {
m.variant
.filter(it => it.token)
.forEach(entry => this._expected.add(`${this._PATH_BASE}/${Parser.sourceJsonToAbv(entry.token.source)}/${Parser.nameToTokenName(entry.token.name)}.${this._EXT}`));
}
// add tokens specified as alt art
if (m.altArt) {
m.altArt
.forEach(alt => this._expected.add(`${this._PATH_BASE}/${Parser.sourceJsonToAbv(alt.source)}/${Parser.nameToTokenName(alt.name)}.${this._EXT}`));
}
});
});
}
static _readImageDirs () {
fs.readdirSync(this._PATH_BASE)
.filter(file => !(this._IGNORED_PREFIXES.some(it => file.startsWith(it))))
.forEach(dir => {
fs.readdirSync(`${this._PATH_BASE}/${dir}`)
.forEach(file => {
this._existing.add(`${this._PATH_BASE}/${dir.replace("(", "").replace(")", "")}/${file}`);
});
});
}
static _getIsError () {
let isError = false;
const results = [];
this._expected.forEach((img) => {
if (!this._existing.has(img)) results.push(`[ MISSING] ${img}`);
});
this._existing.forEach((img) => {
delete this._expectedFromHashToken[img];
if (!this._expected.has(img)) {
if (this._IS_CLEAN_MM_EXTRAS && this._isMmToken(img)) {
fs.unlinkSync(img);
results.push(`[ !DELETE] ${img}`);
return;
}
results.push(`[ EXTRA] ${img}`);
isError = true;
}
});
Object.keys(this._expectedDirs).forEach(k => results.push(`Directory ${k} doesn't exist!`));
results
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.forEach((img) => console.warn(img));
if (Object.keys(this._expectedFromHashToken).length) console.warn(`Declared in Bestiary data but not found:`);
Object.keys(this._expectedFromHashToken).forEach(img => console.warn(`[MISMATCH] ${img}`));
if (!this._expected.size && !Object.keys(this._expectedFromHashToken).length) console.log("Tokens are as expected.");
return isError;
}
static run () {
console.log(`##### Reconciling the PNG tokens against the bestiary JSON #####`);
this._readBestiaryJson();
this._readImageDirs();
return this._getIsError();
}
}
class _TestAdventureBookImages {
static run () {
const pathsMissing = [];
const walker = MiscUtil.getWalker({isNoModification: true});
const getHandler = (filename, out) => {
const checkHref = (href) => {
if (href?.type !== "internal") return;
if (fs.existsSync(`./img/${href.path}`)) return;
out.push(`${filename} :: ${href.path}`);
};
return (obj) => {
if (obj.type !== "image") return;
checkHref(obj.href);
checkHref(obj.hrefThumbnail);
};
};
[
{filename: "adventures.json", prop: "adventure", dir: "adventure"},
{filename: "books.json", prop: "book", dir: "book"},
].flatMap(({filename, prop, dir}) => ut.readJson(`./data/${filename}`)[prop]
.map(({id}) => `./data/${dir}/${dir}-${id.toLowerCase()}.json`))
.forEach(filename => {
walker.walk(
ut.readJson(filename),
{
object: getHandler(filename, pathsMissing),
},
);
});
if (pathsMissing.length) {
console.log(`Adventure/Book Errors:\n${pathsMissing.map(it => `\t${it}`).join("\n")}`);
return true;
}
console.log(`##### Adventure/Book Image Tests Passed #####`);
return false;
}
}
function main () {
if (!fs.existsSync("./img")) return true;
if (_TestTokenImages.run()) return false;
if (_TestAdventureBookImages.run()) return false;
return true;
}
export default main();

57
test/test-json.js Normal file
View File

@@ -0,0 +1,57 @@
import * as fs from "fs";
import {Um, Uf, JsonTester} from "5etools-utils";
const LOG_TAG = "JSON";
const _IS_FAIL_SLOW = !!process.env.FAIL_SLOW;
const _GENERATED_ALLOWLIST = new Set([
"bookref-quick.json",
"gendata-spell-source-lookup.json",
]);
async function main () {
const jsonTester = new JsonTester({
tagLog: LOG_TAG,
fnGetSchemaId: (filePath) => {
const relativeFilePath = filePath.replace("data/", "");
if (relativeFilePath.startsWith("adventure/")) return "adventure/adventure.json";
if (relativeFilePath.startsWith("book/")) return "book/book.json";
if (relativeFilePath.startsWith("bestiary/bestiary-")) return "bestiary/bestiary.json";
if (relativeFilePath.startsWith("bestiary/fluff-bestiary-")) return "bestiary/fluff-bestiary.json";
if (relativeFilePath.startsWith("class/class-")) return "class/class.json";
if (relativeFilePath.startsWith("spells/spells-")) return "spells/spells.json";
if (relativeFilePath.startsWith("spells/fluff-spells-")) return "spells/fluff-spells.json";
return relativeFilePath;
},
});
const fileList = Uf.listJsonFiles("data")
.filter(filePath => {
if (filePath.includes("data/generated")) return _GENERATED_ALLOWLIST.has(filePath.split("/").at(-1));
return true;
});
const results = await jsonTester.pGetErrorsOnDirsWorkers({
isFailFast: !_IS_FAIL_SLOW,
fileList,
});
const {errors, errorsFull} = results;
if (errors.length) {
if (!process.env.CI) fs.writeFileSync(`test/test-json.error.log`, errorsFull.join("\n\n=====\n\n"));
console.error(`Schema test failed (${errors.length} failure${errors.length === 1 ? "" : "s"}).`);
return false;
}
Um.info(LOG_TAG, `All schema tests passed.`);
return true;
}
export default main();

View File

@@ -0,0 +1,26 @@
import * as fs from "fs";
import * as ut from "../node/util.js";
async function main () {
console.log(`##### Validating language fonts #####`);
const json = ut.readJson("./data/languages.json");
const errors = [];
json.languageScript.forEach(lang => {
if (!lang.fonts?.length) return;
lang.fonts
.filter(path => !fs.existsSync(path))
.forEach(path => errors.push(`languageScript "${lang.name}" font path "${path}" does not exist!`));
});
if (errors.length) {
errors.forEach(err => console.error(`\t${err}`));
return false;
}
return true;
}
export default main();

27
test/test-misc.js Normal file
View File

@@ -0,0 +1,27 @@
import "../js/parser.js";
import "../js/utils.js";
function testCatIds () {
const errors = [];
Object.keys(Parser.CAT_ID_TO_FULL).forEach(catId => {
if (Parser.CAT_ID_TO_PROP[catId] === undefined) errors.push(`Missing property for ID: ${catId} (${Parser.CAT_ID_TO_FULL[catId]})`);
if (UrlUtil.CAT_TO_PAGE[catId] === undefined) errors.push(`Missing page for ID: ${catId} (${Parser.CAT_ID_TO_FULL[catId]})`);
});
return errors;
}
async function main () {
let anyErrors = false;
const errorsCatIds = testCatIds();
if (errorsCatIds.length) {
anyErrors = true;
console.error(`Category ID errors:`);
errorsCatIds.forEach(it => console.error(`\t${it}`));
}
if (!anyErrors) console.log("##### Misc Tests Passed #####");
return !anyErrors; // invert the result as this is what the test runner expects
}
export default main();

49
test/test-multisource.js Normal file
View File

@@ -0,0 +1,49 @@
import * as ut from "../node/util.js";
import "../js/parser.js";
import "../js/utils.js";
const _MULTISOURCE_DIRS = [
"bestiary",
"spells",
];
function main () {
console.log(`##### Testing multisource sources... #####`);
const sourceIncorrect = [];
_MULTISOURCE_DIRS.forEach(dir => {
const indexPath = `./data/${dir}/index.json`;
const indexPathFluff = `./data/${dir}/fluff-index.json`;
const indexes = [
ut.readJson(indexPath),
ut.readJson(indexPathFluff),
];
indexes.forEach(index => {
Object.entries(index)
.forEach(([source, filename]) => {
const json = ut.readJson(`./data/${dir}/${filename}`);
Object.values(json)
.forEach(arr => {
if (!arr || !(arr instanceof Array)) return;
arr.forEach(ent => {
if (!ent.source || source === ent.source) return;
sourceIncorrect.push(`${filename} :: ${ent.name} :: ${ent.source} -> ${source}`);
});
});
});
});
});
if (!sourceIncorrect.length) {
console.log(`##### Multisource source test passed! #####`);
return true;
}
console.error(`##### Multisource source test failed! #####\n${sourceIncorrect.map(it => `\t${it}`).join("\n")}`);
}
export default main();

146
test/test-pagenumbers.js Normal file
View File

@@ -0,0 +1,146 @@
import * as ut from "../node/util.js";
import * as rl from "readline-sync";
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
const BLOCKLIST_FILE_PREFIXES = [
...ut.FILE_PREFIX_BLOCKLIST,
"fluff-",
// specific files
"makebrew-creature.json",
"makecards.json",
"foundry.json",
"characters.json",
];
const BLOCKLIST_KEYS = new Set([
"_meta",
"data",
"itemProperty",
"itemEntry",
"lifeClass",
"lifeBackground",
"lifeTrinket",
"cr",
"monsterfeatures",
"adventure",
"book",
"itemTypeAdditionalEntries",
"legendaryGroup",
"languageScript",
"dragonMundaneItems",
]);
const BLOCKLIST_SOURCES = new Set([
// region Sources which only exist in digital form
Parser.SRC_DC,
Parser.SRC_SLW,
Parser.SRC_SDW,
Parser.SRC_VD,
Parser.SRC_HAT_TG,
Parser.SRC_HAT_LMI,
Parser.SRC_LK,
Parser.SRC_AATM,
Parser.SRC_HFStCM,
// N.b.: other MCV source creatures mysteriously have page numbers on Beyond
Parser.SRC_MCV4EC,
// endregion
// region Sources which are screens, and therefore "pageless"
Parser.SRC_SCREEN,
Parser.SRC_SCREEN_WILDERNESS_KIT,
Parser.SRC_SCREEN_DUNGEON_KIT,
Parser.SRC_SCREEN_SPELLJAMMER,
// endregion
]);
const SUB_KEYS = {};
function run ({isModificationMode = false} = {}) {
console.log(`##### Checking for Missing Page Numbers #####`);
const FILE_MAP = {};
const files = ut.listFiles({dir: `./data`, blocklistFilePrefixes: BLOCKLIST_FILE_PREFIXES});
files
.forEach(file => {
let mods = 0;
const json = ut.readJson(file);
Object.keys(json)
.filter(k => !BLOCKLIST_KEYS.has(k))
.forEach(k => {
const data = json[k];
if (data instanceof Array) {
const noPage = data
.filter(it => !BLOCKLIST_SOURCES.has(SourceUtil.getEntitySource(it)))
.filter(it => !(it.inherits ? it.inherits.page : it.page))
.filter(it => !it._copy?._preserve?.page);
const subKeys = SUB_KEYS[k];
if (subKeys) {
subKeys.forEach(sk => {
data
.filter(it => it[sk] && it[sk] instanceof Array)
.forEach(it => {
const subArr = it[sk];
subArr
.forEach(subIt => subIt.source = subIt.source || it.source);
noPage.push(...subArr
// Skip un-named entries, as these are usually found on the page of their parent
.filter(subIt => subIt.name)
.filter(subIt => !BLOCKLIST_SOURCES.has(subIt.source))
.filter(subIt => !subIt.page));
});
});
}
if (noPage.length && isModificationMode) {
console.log(`${file}:`);
console.log(`\t${noPage.length} missing page number${noPage.length === 1 ? "" : "s"}`);
}
noPage
.forEach(it => {
const ident = `${k.padEnd(20, " ")} ${SourceUtil.getEntitySource(it).padEnd(32, " ")} ${it.name}`;
if (isModificationMode) {
console.log(` ${ident}`);
const page = rl.questionInt(" - Page = ");
if (page) {
it.page = page;
mods++;
}
} else {
const list = (FILE_MAP[file] = FILE_MAP[file] || []);
list.push(ident);
}
});
}
});
if (mods > 0) {
let answer = "";
while (!["y", "n", "quit"].includes(answer)) {
answer = rl.question(`Save file with ${mods} modification${mods === 1 ? "" : "s"}? [y/n/quit]`);
if (answer === "y") {
console.log(`Saving ${file}...`);
fs.writeFileSync(file, CleanUtil.getCleanJson(json), "utf-8");
} else if (answer === "quit") {
process.exit(1);
}
}
}
});
const filesWithMissingPages = Object.keys(FILE_MAP);
if (filesWithMissingPages.length) {
console.warn(`##### Files with Missing Page Numbers #####`);
filesWithMissingPages.forEach(f => {
console.warn(`${f}:`);
FILE_MAP[f].forEach(it => console.warn(`\t${it}`));
});
} else console.log(`Page numbers are as expected.`);
}
run();

1339
test/test-tags.js Normal file

File diff suppressed because it is too large Load Diff