104 lines
3.9 KiB
TypeScript
104 lines
3.9 KiB
TypeScript
// @vitest-environment node
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import sharp from "sharp";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { artManifest } from "../src/map/art/manifest";
|
|
|
|
const webRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const script = join(webRoot, "scripts/map-art.mjs");
|
|
sharp.cache(false);
|
|
/** Reads through a buffer so sharp never holds the temp files open (Windows). */
|
|
const image = (path: string) => sharp(readFileSync(path));
|
|
const SIZES = { body: [256, 512], badge: [256, 512], ship: [128, 256] } as const;
|
|
|
|
const run = (outDir: string) =>
|
|
execFileSync(process.execPath, [script, outDir], { cwd: webRoot, stdio: "pipe" });
|
|
|
|
describe("map art optimization script (CR-003)", () => {
|
|
let first: string;
|
|
let second: string;
|
|
beforeAll(() => {
|
|
first = mkdtempSync(join(tmpdir(), "map-art-a-"));
|
|
second = mkdtempSync(join(tmpdir(), "map-art-b-"));
|
|
run(first);
|
|
run(second);
|
|
}, 120_000);
|
|
afterAll(() => {
|
|
rmSync(first, { recursive: true, force: true });
|
|
rmSync(second, { recursive: true, force: true });
|
|
});
|
|
|
|
const present = Object.entries(artManifest.assets).filter(([, a]) => a.file !== null);
|
|
|
|
it("writes every expected file at the right size", async () => {
|
|
for (const [id, asset] of present) {
|
|
if (asset.kind === "background") {
|
|
const meta = await image(join(first, `${id}.webp`)).metadata();
|
|
expect(meta.format).toBe("webp");
|
|
expect(Math.max(meta.width!, meta.height!)).toBeLessThanOrEqual(2048);
|
|
continue;
|
|
}
|
|
const [one, two] = SIZES[asset.kind];
|
|
for (const [suffix, longSide] of [
|
|
["1x", one],
|
|
["2x", two],
|
|
] as const) {
|
|
const meta = await image(join(first, `${id}@${suffix}.png`)).metadata();
|
|
expect(Math.max(meta.width!, meta.height!), `${id}@${suffix}`).toBe(longSide);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("produces identical files when run twice", () => {
|
|
const files = readdirSync(first).sort();
|
|
expect(readdirSync(second).sort()).toEqual(files);
|
|
for (const file of files) {
|
|
expect(readFileSync(join(second, file)).equals(readFileSync(join(first, file))), file).toBe(
|
|
true,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("matches the committed output pixel for pixel", async () => {
|
|
// Pixels, not bytes: PNG compression may differ between platforms (CI runs Linux).
|
|
const committed = join(webRoot, "public/assets/map-gen");
|
|
for (const file of readdirSync(first).filter((f) => f.endsWith(".png"))) {
|
|
const [fresh, stored] = await Promise.all([
|
|
image(join(first, file)).raw().toBuffer(),
|
|
image(join(committed, file)).raw().toBuffer(),
|
|
]);
|
|
expect(stored.equals(fresh), file).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("keeps the background under 1 MB", () => {
|
|
expect(statSync(join(first, "bg.webp")).size).toBeLessThan(1024 * 1024);
|
|
});
|
|
|
|
it("has source sprites with transparent backgrounds", async () => {
|
|
for (const [id, asset] of present) {
|
|
if (asset.kind === "background") continue;
|
|
const { data, info } = await image(join(webRoot, artManifest.sourceDir, asset.file!))
|
|
.ensureAlpha()
|
|
.raw()
|
|
.toBuffer({ resolveWithObject: true });
|
|
const alpha = (x: number, y: number) => data[(y * info.width + x) * info.channels + 3];
|
|
const corners = [
|
|
alpha(0, 0),
|
|
alpha(info.width - 1, 0),
|
|
alpha(0, info.height - 1),
|
|
alpha(info.width - 1, info.height - 1),
|
|
];
|
|
expect(
|
|
// Alpha up to 2/255 is invisible (ship-lighter.png has a stray alpha-1 corner pixel).
|
|
corners.every((a) => a! <= 2),
|
|
`${asset.file} (${id}): background isn't transparent`,
|
|
).toBe(true);
|
|
}
|
|
});
|
|
});
|