import { readFile } from "node:fs/promises"; import { test } from "node:test"; import assert from "node:assert/strict"; const SNAKE_CASE_RE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/; const PHYSICAL_HITZONE_KEYS = ["cut", "blunt", "ammo"]; const ELEMENTAL_HITZONE_KEYS = ["fire", "water", "thunder", "ice", "dragon"]; function valueAt(object, path) { return path.split(".").reduce((value, key) => value?.[key], object); } function assertNonEmptyString(object, path) { const value = valueAt(object, path); assert.equal(typeof value, "string", `${path} must be a string`); assert.ok(value.trim(), `${path} must not be empty`); } function assertStringArray(object, path, minLength = 1) { const value = valueAt(object, path); assert.ok(Array.isArray(value), `${path} must be an array`); assert.ok(value.length >= minLength, `${path} must contain at least ${minLength} item(s)`); value.forEach((item, index) => { assert.equal(typeof item, "string", `${path}[${index}] must be a string`); assert.ok(item.trim(), `${path}[${index}] must not be empty`); }); } function validateSiteContent(site) { [ "brand.name", "brand.homeAriaLabel", "navigation.home", "navigation.toolboxes", "navigation.games", "navigation.mobileGames", "sidebar.badge", "sidebar.note", "topbar.dashboard", "topbar.toolbox", "topbar.games", "gamesPage.eyebrow", "gamesPage.title", "gamesPage.description", "gamesPage.emptyTitle", "gamesPage.emptyText", "home.hero.eyebrow", "home.hero.title", "home.hero.description", "home.hero.primaryAction", "home.hero.secondaryAction", "home.stats.toolboxSingular", "home.stats.toolboxPlural", "home.stats.toolSingular", "home.stats.toolPlural", "home.origin.eyebrow", "home.origin.title", "home.origin.visualAlt", "home.origin.dialogueAriaLabel", "home.origin.caption", "home.legal.copyright", "home.legal.disclaimer", "toolboxes.eyebrow", "toolboxes.title", "toolboxes.newButton", "toolboxes.importAll", "toolboxes.exportAll", "toolboxes.importOne", "toolboxes.summary", "toolboxes.storageHelp.title", "toolboxes.storageHelp.text", "toolboxes.emptyTitle", "toolboxes.emptyText" ].forEach((path) => assertNonEmptyString(site, path)); assertStringArray(site, "toolboxes.storageHelp.items", 3); const lines = valueAt(site, "home.origin.lines"); assert.ok(Array.isArray(lines), "home.origin.lines must be an array"); assert.ok(lines.length >= 2, "home.origin.lines must contain at least 2 items"); lines.forEach((line, index) => { assert.ok(["user", "app"].includes(line?.speaker), `home.origin.lines[${index}].speaker must be "user" or "app"`); assert.equal(typeof line?.text, "string", `home.origin.lines[${index}].text must be a string`); assert.ok(line.text.trim(), `home.origin.lines[${index}].text must not be empty`); }); } function assertSnakeCase(value, path) { assert.equal(typeof value, "string", `${path} must be a string`); assert.match(value, SNAKE_CASE_RE, `${path} must be snake_case`); } function validateConditionValues(items, path) { assert.ok(Array.isArray(items), `${path} must be an array`); items.forEach((item, index) => { assertSnakeCase(item?.condition, `${path}[${index}].condition`); assert.ok(Array.isArray(item?.values), `${path}[${index}].values must be an array`); item.values.forEach((value, valueIndex) => assertSnakeCase(value, `${path}[${index}].values[${valueIndex}]`)); }); } function validateMonsterData(monsters) { assert.ok(Array.isArray(monsters), "monsters must be an array"); const ids = new Set(); const names = new Set(); monsters.forEach((monster, monsterIndex) => { const path = `monsters[${monsterIndex}]`; assert.equal(typeof monster.id, "number", `${path}.id must be a number`); assert.ok(Number.isInteger(monster.id), `${path}.id must be an integer`); assert.ok(!ids.has(monster.id), `${path}.id must be unique`); ids.add(monster.id); assertSnakeCase(monster.name, `${path}.name`); assert.ok(!names.has(monster.name), `${path}.name must be unique`); names.add(monster.name); assertSnakeCase(monster.type, `${path}.type`); validateConditionValues(monster.ailments, `${path}.ailments`); validateConditionValues(monster.weaknesses, `${path}.weaknesses`); validateConditionValues(monster.weakpoints, `${path}.weakpoints`); assert.equal(monster.damage, undefined, `${path}.damage must not be used`); assert.ok(Array.isArray(monster.hitzones), `${path}.hitzones must be an array`); assert.ok(monster.hitzones.length > 0, `${path}.hitzones must not be empty`); monster.hitzones.forEach((hitzone, hitzoneIndex) => { const hitzonePath = `${path}.hitzones[${hitzoneIndex}]`; assertSnakeCase(hitzone.name, `${hitzonePath}.name`); assert.equal(typeof hitzone.physical, "object", `${hitzonePath}.physical must be an object`); assert.equal(typeof hitzone.elemental, "object", `${hitzonePath}.elemental must be an object`); PHYSICAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.physical[key], "number", `${hitzonePath}.physical.${key} must be a number`)); ELEMENTAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.elemental[key], "number", `${hitzonePath}.elemental.${key} must be a number`)); }); }); } function validateEndemicData(endemicPayload) { ["endemicLife", "aquaticLife"].forEach((collection) => { assert.ok(Array.isArray(endemicPayload[collection]), `${collection} must be an array`); endemicPayload[collection].forEach((item, itemIndex) => { const path = `${collection}[${itemIndex}]`; assertSnakeCase(item.name, `${path}.name`); if (item.description) assertSnakeCase(item.description, `${path}.description`); validateConditionValues(item.locations, `${path}.locations`); }); }); } function validateTranslationKeys(translations, path) { Object.keys(translations).forEach((key) => { assert.ok(!/\s/.test(key), `${path}.${key} must not contain spaces`); }); } function validateDiablo4Affixes(payload) { assert.ok(Array.isArray(payload.categories), "diablo4.categories must be an array"); assert.ok(payload.categories.length > 0, "diablo4.categories must not be empty"); assert.ok(Array.isArray(payload.affixes), "diablo4.affixes must be an array"); assert.ok(payload.affixes.length > 0, "diablo4.affixes must not be empty"); const ids = new Set(); const categoryIds = payload.categories.map((category, index) => { assertSnakeCase(category.id, `diablo4.categories[${index}].id`); assertNonEmptyString({ category }, "category.label"); assertSnakeCase(category.icon, `diablo4.categories[${index}].icon`); assertSnakeCase(category.tone, `diablo4.categories[${index}].tone`); return category.id; }); payload.affixes.forEach((affix, index) => { assertSnakeCase(affix.id, `diablo4.affixes[${index}].id`); assert.ok(!ids.has(affix.id), `diablo4.affixes[${index}].id must be unique`); ids.add(affix.id); assertNonEmptyString({ affix }, "affix.label"); assert.ok(Array.isArray(affix.categories), `diablo4.affixes[${index}].categories must be an array`); affix.categories.forEach((category, categoryIndex) => { assertSnakeCase(category, `diablo4.affixes[${index}].categories[${categoryIndex}]`); assert.ok(categoryIds.includes(category), `diablo4.affixes[${index}].categories[${categoryIndex}] must exist in diablo4.categories`); }); }); } test("vite entrypoint loads the react application", async () => { const html = await readFile("website/index.html", "utf8"); assert.match(html, /
<\/div>/); assert.match(html, /\/favicon\.ico/); assert.match(html, /\/src\/main\.jsx/); }); test("react application defines the expected local toolbox primitives", async () => { const source = await readFile("website/src/main.jsx", "utf8"); const styles = await readFile("website/src/styles/main.scss", "utf8"); const styleNebula = await readFile("website/src/styles/_nebula.scss", "utf8"); const styleTokens = await readFile("website/src/styles/_tokens.scss", "utf8"); const styleBase = await readFile("website/src/styles/_base.scss", "utf8"); const styleHome = await readFile("website/src/styles/_home.scss", "utf8"); const styleToolboxes = await readFile("website/src/styles/_toolboxes.scss", "utf8"); const styleIcons = await readFile("website/src/styles/_icons.scss", "utf8"); const styleMhwilds = await readFile("website/src/styles/_mhwilds.scss", "utf8"); const styleResponsive = await readFile("website/src/styles/_responsive.scss", "utf8"); const iconComponent = await readFile("website/src/components/Icon.jsx", "utf8"); const gamesPage = await readFile("website/src/features/games/GamesPage.jsx", "utf8"); const gameRoute = await readFile("website/src/features/games/GameRoute.jsx", "utf8"); const diablo4Page = await readFile("website/src/features/games/diablo4/Diablo4Page.jsx", "utf8"); const diablo4Overview = await readFile("website/src/features/games/diablo4/Diablo4Overview.jsx", "utf8"); const diablo4Listing = await readFile("website/src/features/games/diablo4/Diablo4Listing.jsx", "utf8"); const diablo4Filters = await readFile("website/src/features/games/diablo4/Diablo4Filters.jsx", "utf8"); const diablo4AffixCard = await readFile("website/src/features/games/diablo4/Diablo4AffixCard.jsx", "utf8"); const diablo4Utils = await readFile("website/src/features/games/diablo4/utils.js", "utf8"); const mhwildsPage = await readFile("website/src/features/games/mhwilds/MhwildsPage.jsx", "utf8"); const mhwildsListing = await readFile("website/src/features/games/mhwilds/MhwildsListing.jsx", "utf8"); const mhwildsFilters = await readFile("website/src/features/games/mhwilds/MhwildsFilters.jsx", "utf8"); const monsterCard = await readFile("website/src/features/games/mhwilds/cards/MonsterCard.jsx", "utf8"); const endemicCard = await readFile("website/src/features/games/mhwilds/cards/EndemicCard.jsx", "utf8"); const damageTable = await readFile("website/src/features/games/mhwilds/cards/DamageTable.jsx", "utf8"); const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8"); const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8"); const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8"); const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8"); const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8"); const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8"); assert.match(source, /sokkog:toolboxes/); assert.match(source, /sokkog:game-toolbox-links/); assert.match(source, /styles\/main\.scss/); assert.match(styles, /@use "tokens"/); assert.match(styles, /@use "nebula"/); assert.match(styles, /@use "toolboxes"/); assert.match(styles, /@use "mhwilds"/); assert.match(styles, /@use "diablo4"/); assert.match(styleTokens, /--gradient-brand/); assert.match(styleBase, /\.card-icon-button,\n\.drawer-action-button/); assert.match(styleNebula, /@function star-field/); assert.match(styleNebula, /body::after/); assert.match(styleBase, /\.import-icon-button/); assert.match(styleHome, /\.dialogue-line i/); assert.match(styleHome, /font-style: italic/); assert.match(styleToolboxes, /\.module-icon-abacus/); assert.match(styleToolboxes, /\.toolbox-icon-picker/); assert.match(styleToolboxes, /\.toolbox-game-icon/); assert.match(styleToolboxes, /\.toolbox-actions-row/); assert.match(styleIcons, /ui-icon-drag/); assert.match(styleIcons, /ui-icon-dropdown/); assert.match(styleIcons, /ui-icon-flip/); assert.match(styleIcons, /ui-icon-add/); assert.match(styleMhwilds, /\.mhwilds-card/); assert.match(styleMhwilds, /\.mhwilds-flip-indicator/); assert.match(styleResponsive, /@media \(max-width: 760px\)/); assert.match(source, /features\/toolboxes\/modules\/index\.jsx/); assert.match(source, /features\/games\/GamesPage\.jsx/); assert.match(source, /features\/games\/GameRoute\.jsx/); assert.match(iconComponent, /export function Icon/); assert.match(gamesPage, /export function GamesPage/); assert.match(gamesPage, /siteContent\.gamesPage/); assert.match(gamesPage, /game-card-cover-link/); assert.match(gamesPage, /game-card-placeholder/); assert.match(gameRoute, /export function GameRoute/); assert.match(gameRoute, /MhwildsPage/); assert.match(gameRoute, /Diablo4Page/); assert.match(diablo4Page, /export function Diablo4Page/); assert.match(diablo4Overview, /export function Diablo4Overview/); assert.match(diablo4Listing, /export function Diablo4Listing/); assert.match(diablo4Filters, /export function Diablo4Filters/); assert.match(diablo4AffixCard, /export function Diablo4AffixCard/); assert.match(diablo4AffixCard, /categoryMap/); assert.match(diablo4Utils, /getFilteredDiablo4Affixes/); assert.match(diablo4Utils, /getCategoryIconStyle/); assert.match(mhwildsPage, /export function MhwildsPage/); assert.match(mhwildsListing, /export function MhwildsListing/); assert.match(mhwildsListing, /endemic_life/); assert.match(mhwildsFilters, /export function MhwildsFilters/); assert.match(monsterCard, /export function MonsterCard/); assert.match(monsterCard, /ui-icon-flip/); assert.match(endemicCard, /export function EndemicCard/); assert.match(damageTable, /rowIndex/); assert.match(moduleRegistry, /export const TOOLBOX_MODULES/); assert.match(moduleRegistry, /export function AddToolControls/); assert.match(moduleRegistry, /export function ToolboxModules/); assert.match(moduleRegistry, /MODULE_COMPONENTS/); assert.match(moduleRegistry, /notepad:/); assert.match(moduleRegistry, /checklist:/); assert.match(moduleRegistry, /screenshots:/); assert.match(moduleRegistry, /links:/); assert.match(moduleRegistry, /counters:/); assert.match(moduleRegistry, /editable: false/); assert.match(moduleRegistry, /module-edit-button/); assert.match(notepadModule, /export function NotepadModule/); assert.match(checklistModule, /export function ChecklistModule/); assert.match(screenshotsModule, /export function ScreenshotsModule/); assert.match(linksModule, /export function LinksModule/); assert.match(countersModule, /export function CountersModule/); assert.match(checklistModule, /editing &&/); assert.match(screenshotsModule, /editing &&/); assert.match(linksModule, /editing &&/); assert.match(countersModule, /editing &&/); assert.match(countersModule, /counter-actions/); assert.match(linksModule, /normalizeUrl/); assert.match(linksModule, /copyText/); assert.match(source, /DEFAULT_SITE_CONTENT/); assert.match(source, /\/data\/site\.json/); assert.match(source, /\/data\/diablo4\/affixes_types\.json/); assert.match(source, /filterOptionKeys/); assert.match(source, /APP_STORAGE_LIMIT_BYTES/); assert.match(source, /DEFAULT_TOOLBOX_ICON/); assert.match(source, /TOOLBOX_ICON_FILES/); assert.match(source, /function normalizeToolboxIcon/); assert.match(source, /function ToolboxIconPicker/); assert.match(source, /function ToolboxGameIcon/); assert.match(source, /function StorageQuota/); assert.match(source, /function writeStorageValue/); assert.match(source, /role="progressbar"/); assert.match(source, /function createGlobalExportPayload/); assert.match(source, /async function importAllToolboxes/); assert.match(source, /import-icon-button/); assert.match(source, /function LinkToolboxModal/); assert.doesNotMatch(source, /description: _description/); assert.match(source, /function CreateToolboxModal/); assert.match(source, /const getToolboxGameId/); assert.match(source, /dragon\.png/); assert.match(source, /value\.split\("\\n"\)/); assert.match(source, /
addModule/); assert.match(source, /qtyTarget/); assert.match(source, /qtyCurrent/); assert.match(source, /normalizeLinksData/); assert.match(source, /normalizeCountersData/); assert.match(checklistModule, /ChecklistItem/); assert.match(screenshotsModule, /clipboardData/); assert.match(source, /async function addScreenshotFiles/); assert.match(source, /function ScreenshotViewer/); assert.match(source, /function openImageInNewTab/); assert.match(source, / { const site = JSON.parse(await readFile("website/public/data/site.json", "utf8")); validateSiteContent(site); }); test("mhwilds data and assets are available", async () => { const site = JSON.parse(await readFile("website/public/data/site.json", "utf8")); const games = JSON.parse(await readFile("website/public/data/games.json", "utf8")); const monsters = JSON.parse(await readFile("website/public/data/mhwilds/monsters.json", "utf8")); const endemicLife = JSON.parse(await readFile("website/public/data/mhwilds/endemic_life.json", "utf8")); const translations = JSON.parse(await readFile("website/public/data/mhwilds/i18n/fr.json", "utf8")); const enTranslations = JSON.parse(await readFile("website/public/data/mhwilds/i18n/en.json", "utf8")); const listingSource = await readFile("website/src/features/games/mhwilds/MhwildsListing.jsx", "utf8"); assert.equal(site.brand.name, "Sokko G"); assert.equal(site.home.hero.title, "Sokko G"); assert.equal(site.gamesPage.title, "Jeux disponibles"); assert.match(site.toolboxes.summary, /espaces modulaires/); assert.ok(site.toolboxes.storageHelp.items.length >= 3); assert.equal(games.games[0].id, "mhwilds"); assert.equal(games.games[0].eyebrow, "Guide de jeu"); assert.match(games.games[0].images.cardCover, /games\/mhwilds\/card-cover\.png/); assert.match(games.games[0].images.heroBg, /games\/mhwilds\/hero-bg\.png/); assert.ok(monsters.monsters.length > 0); assert.deepEqual(monsters.elements, ["fire", "water", "thunder", "ice", "dragon"]); validateMonsterData(monsters.monsters); assert.ok(Array.isArray(endemicLife.locations), "endemicLife.locations must be an array"); assert.ok(endemicLife.locations.length > 0, "endemicLife.locations must not be empty"); assert.ok(endemicLife.endemicLife.length > 0); validateEndemicData(endemicLife); validateTranslationKeys(translations, "fr"); validateTranslationKeys(enTranslations, "en"); assert.equal(translations.monsters, "monstres"); assert.equal(translations.dark_hornet, "frelon clair-obscur"); assert.equal(translations["dark hornet"], undefined); assert.match(listingSource, /function MhwildsListing/); const monsterCardSource = await readFile("website/src/features/games/mhwilds/cards/MonsterCard.jsx", "utf8"); assert.match(monsterCardSource, /monster-card/); assert.match(monsterCardSource, /monster\.hitzones/); await readFile("website/public/static/img/mhwilds/chatacabra.png"); await readFile("website/public/static/img/games/mhwilds/card-cover.png"); await readFile("website/public/static/img/games/mhwilds/hero-bg.png"); }); test("diablo4 data and route are available", async () => { const games = JSON.parse(await readFile("website/public/data/games.json", "utf8")); const affixes = JSON.parse(await readFile("website/public/data/diablo4/affixes_types.json", "utf8")); const routeSource = await readFile("website/src/features/games/GameRoute.jsx", "utf8"); const pageSource = await readFile("website/src/features/games/diablo4/Diablo4Page.jsx", "utf8"); const listingSource = await readFile("website/src/features/games/diablo4/Diablo4Listing.jsx", "utf8"); const diablo4Game = games.games.find((game) => game.id === "diablo4"); assert.ok(diablo4Game, "games.json must declare diablo4"); assert.match(diablo4Game.images.cardCover, /games\/diablo4\/card-cover\.png/); assert.match(diablo4Game.images.heroBg, /games\/diablo4\/hero-bg\.png/); validateDiablo4Affixes(affixes); assert.match(routeSource, /game\.id === "diablo4"/); assert.match(pageSource, /category === "affixes"/); assert.match(listingSource, /diablo4\.filterOptions\.affixes/); await readFile("website/public/static/img/games/diablo4/card-cover.png"); await readFile("website/public/static/img/games/diablo4/hero-bg.png"); await readFile("website/public/static/img/diablo4/axe.svg"); await readFile("website/public/static/img/diablo4/cube.svg"); await readFile("website/public/static/img/diablo4/fire.svg"); await readFile("website/public/static/img/diablo4/shield.svg"); await readFile("website/public/static/img/diablo4/wing.svg"); }); test("server and vite support local env configuration", async () => { const server = await readFile("server.mjs", "utf8"); const viteConfig = await readFile("vite.config.js", "utf8"); const envExample = await readFile(".env.example", "utf8"); assert.match(server, /function loadEnvFile/); assert.match(server, /process\.env\.PORT/); assert.match(viteConfig, /function loadEnvFile/); assert.match(viteConfig, /process\.env\.PORT/); assert.match(envExample, /PORT=5173/); }); test("legacy svg icons are available for reuse", async () => { const manifest = JSON.parse(await readFile("website/public/data/toolbox-icons.json", "utf8")); assert.ok(manifest.icons.length >= 4); await readFile("website/public/static/icons/home.svg", "utf8"); await readFile("website/public/static/icons/add.svg", "utf8"); await readFile("website/public/static/icons/checklist.svg", "utf8"); await readFile("website/public/static/icons/close.svg", "utf8"); await readFile("website/public/static/icons/columns.svg", "utf8"); await readFile("website/public/static/icons/copy.svg", "utf8"); await readFile("website/public/static/icons/abacus.svg", "utf8"); await readFile("website/public/static/icons/export.svg", "utf8"); await readFile("website/public/static/icons/notepad.svg", "utf8"); await readFile("website/public/static/icons/picture.svg", "utf8"); await readFile("website/public/static/icons/rows.svg", "utf8"); await readFile("website/public/static/icons/rubber.svg", "utf8"); await readFile("website/public/static/icons/trashcan.svg", "utf8"); await readFile("website/public/static/icons/zoom.svg", "utf8"); await readFile("website/public/favicon.ico"); await readFile("website/public/static/img/dragon.png"); await readFile("website/public/static/img/toolbox-icons/toolbox.png"); await readFile("website/public/static/img/toolbox-icons/sword.png"); await readFile("website/public/static/img/toolbox-icons/puzzle.png"); });