/** * The grouped preset picker, and the MegaMek camo directory it sits next to. * * Two things here are easy to break silently. The picker marks a tile chosen * by comparing its label to the palette's name, so two presets sharing a name * light up together and neither is wrong enough to notice. And the camo files * are generated from a MegaMek release by a script nobody runs on a normal * day, so a half-finished run, a rename upstream or a stray delete leaves an * index naming files that are not there — which fails as a broken thumbnail in * a dialog, long after the commit that caused it. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { access, mkdtemp, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import { CURATED } from "./megamek-camo.data.mjs"; const run = promisify(execFile); const web = fileURLToPath(new URL("../", import.meta.url)); const CAMO_DIR = join(web, "public/megamek-camo"); // palette.ts is TypeScript importing TypeScript, so it is bundled with the // site's own bundler rather than run through a TS runner. Same shape as // camo-record.test.mjs; the output goes to a temp directory. const dir = await mkdtemp(join(tmpdir(), "camo-presets-")); const bundle = join(dir, "fixture.mjs"); await run( join(web, "node_modules/.bin/rolldown"), [ join(web, "scripts/camo-presets.fixture.ts"), "--format", "esm", "--platform", "node", "--file", bundle, ], { cwd: web }, ); const { PALETTE_GROUPS, PRESETS } = await import(bundle); test("every preset is in a group the picker draws a chip for", () => { for (const preset of PRESETS) { assert.ok( preset.group, `${preset.name} has no group, so no chip shows it and it is unreachable`, ); assert.ok( PALETTE_GROUPS.includes(preset.group), `${preset.name} is in "${preset.group}", which is not in PALETTE_GROUPS`, ); } for (const group of PALETTE_GROUPS) { assert.ok( PRESETS.some((p) => p.group === group), `the "${group}" chip would open an empty grid`, ); } }); test("preset names are unique", () => { const seen = new Set(); for (const preset of PRESETS) { assert.ok( !seen.has(preset.name), `two presets are called "${preset.name}"; the picker marks the chosen ` + "tile by name, so both would look chosen", ); seen.add(preset.name); } }); test("every preset is four colours of whole 0-255 channels", () => { for (const preset of PRESETS) { assert.equal( preset.colors.length, 4, `${preset.name} has ${preset.colors.length} colours; the tile draws four bands`, ); for (const color of preset.colors) { assert.equal( color.length, 3, `${preset.name} has a colour that is not RGB`, ); for (const channel of color) { assert.ok( Number.isInteger(channel) && channel >= 0 && channel <= 255, `${preset.name} has channel ${channel}, which the record encoder rejects`, ); } } } }); test("flags are Pride palettes and nothing else is", () => { for (const preset of PRESETS) { assert.equal( preset.flag === true, preset.group === "Pride", `${preset.name} disagrees with itself: flag is ${preset.flag} and the ` + `group is "${preset.group}". randomCamo excludes flags, so a faction ` + "marked as one would silently drop out of the roll", ); } }); test("the roll still excludes flags", async () => { const random = await readFile(join(web, "src/camo/random.ts"), "utf8"); assert.match( random, /PRESETS\.filter\(\(p\) => !p\.flag\)/, "a flag is something a person picks on purpose, not something the page " + "hands them on load", ); }); test("the camo index names files that are on disk", async () => { const index = JSON.parse( await readFile(join(CAMO_DIR, "index.json"), "utf8"), ); assert.ok(index.release, "the index does not say which release it came from"); for (const category of index.categories) { assert.ok(category.camos.length > 0, `"${category.name}" is empty`); for (const camo of category.camos) { await assert.doesNotReject( access(join(CAMO_DIR, camo.file)), `${camo.file} is in the index and not in public/megamek-camo/`, ); } } }); test("the index carries every curated entry", async () => { const index = JSON.parse( await readFile(join(CAMO_DIR, "index.json"), "utf8"), ); for (const group of CURATED) { const category = index.categories.find((c) => c.name === group.name); assert.ok(category, `"${group.name}" is curated and not in the index`); for (const entry of group.entries) { assert.ok( category.camos.some( (c) => c.name === entry.name && c.src === entry.src, ), `${entry.name} is curated and not in the index; re-run ` + "`MM_HOME=... npm run megamek-camo`", ); } } }); test("every faction the script derives a palette for has one", () => { for (const group of CURATED.filter((g) => g.palettes)) { for (const entry of group.entries) { const preset = PRESETS.find((p) => p.name === entry.name); assert.ok( preset, `${entry.name} has camo art but no palette; the script prints the ` + "block, and pasting it into palette.ts is a manual step", ); assert.equal( preset.group, group.name, `${entry.name} is filed under "${preset.group}" and its art under ` + `"${group.name}"`, ); } } });