add vitest and regression tests for arc 1
This commit is contained in:
parent
0074f0e10c
commit
9f538b1543
9 changed files with 1561 additions and 4 deletions
1157
package-lock.json
generated
1157
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -12,6 +12,8 @@
|
||||||
"analyze:requests": "node scripts/request-analysis-worker.mjs",
|
"analyze:requests": "node scripts/request-analysis-worker.mjs",
|
||||||
"validate:content": "node scripts/validate-content-schemas.mjs",
|
"validate:content": "node scripts/validate-content-schemas.mjs",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
|
|
@ -23,6 +25,9 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.12.3",
|
"@types/node": "^24.12.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|
@ -31,9 +36,10 @@
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^8.0.12"
|
"vite": "^8.0.12",
|
||||||
|
"vitest": "^4.1.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
81
src/components/worldshaperShared.test.ts
Normal file
81
src/components/worldshaperShared.test.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import {
|
||||||
|
getMapBackgroundTileId,
|
||||||
|
parseHeightLayers,
|
||||||
|
parseRoomLayers,
|
||||||
|
} from "./worldshaperShared";
|
||||||
|
import type { JsonObject } from "../editorCore";
|
||||||
|
|
||||||
|
describe("worldshaperShared", () => {
|
||||||
|
it("prefers top-level background tile ids and falls back to the legacy nested shape", () => {
|
||||||
|
expect(getMapBackgroundTileId({ backgroundTileId: "grass" })).toBe("grass");
|
||||||
|
expect(getMapBackgroundTileId({ tiles: { backgroundTileId: "water" } as unknown as JsonObject })).toBe("water");
|
||||||
|
expect(getMapBackgroundTileId({})).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("synthesizes and resizes room layers from the current map payload", () => {
|
||||||
|
const result = parseRoomLayers({
|
||||||
|
rows: ["##", "#"],
|
||||||
|
roomLayers: [
|
||||||
|
{
|
||||||
|
layer: 2,
|
||||||
|
name: "Objects",
|
||||||
|
rows: ["A"],
|
||||||
|
instanceIds: ["npc_1", ""],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, 3, 2);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
layer: 0,
|
||||||
|
name: undefined,
|
||||||
|
zIndex: 0,
|
||||||
|
rows: ["##.", "#.."],
|
||||||
|
instanceIds: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
layer: 2,
|
||||||
|
name: "Objects",
|
||||||
|
zIndex: 0,
|
||||||
|
rows: ["A ", " "],
|
||||||
|
instanceIds: ["npc_1"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes, trims, and bounds height patches while dropping duplicate ids", () => {
|
||||||
|
const result = parseHeightLayers({
|
||||||
|
heightLayers: [
|
||||||
|
{
|
||||||
|
id: "ridge",
|
||||||
|
z: 3,
|
||||||
|
x: -1,
|
||||||
|
y: -1,
|
||||||
|
rows: [
|
||||||
|
"...",
|
||||||
|
".9.",
|
||||||
|
"..8",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ridge",
|
||||||
|
z: 9,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rows: ["1"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, 4, 4);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "ridge",
|
||||||
|
name: undefined,
|
||||||
|
z: 3,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rows: ["9", " 8"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
106
src/editorCore.test.ts
Normal file
106
src/editorCore.test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import {
|
||||||
|
getSpriteRows,
|
||||||
|
normalizeImageRecordForSave,
|
||||||
|
normalizeImagesPayloadForSave,
|
||||||
|
normalizeTileRecordForSave,
|
||||||
|
resolveUnifiedColorSymbol,
|
||||||
|
setUnifiedColorEntries,
|
||||||
|
} from "./editorCore";
|
||||||
|
import type { JsonObject, JsonValue } from "./editorCore";
|
||||||
|
|
||||||
|
describe("editorCore", () => {
|
||||||
|
it("normalizes tile records with padded rows and compatibility symbols", () => {
|
||||||
|
const result = normalizeTileRecordForSave({
|
||||||
|
id: "tile_grass",
|
||||||
|
rows: ["A", "BC", "DROP"],
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(expect.objectContaining({
|
||||||
|
id: "tile_grass",
|
||||||
|
symbol: "t",
|
||||||
|
rows: ["A.", "BC"],
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes image records into frame-based storage while preserving tile compatibility", () => {
|
||||||
|
const result = normalizeImageRecordForSave({
|
||||||
|
id: "tile_tree",
|
||||||
|
roles: ["tile", "sprite", "other", "tile"],
|
||||||
|
rows: ["A", "BC"],
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
speed: -10,
|
||||||
|
playback: "invalid",
|
||||||
|
tileSymbol: "$",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.rows).toBeUndefined();
|
||||||
|
expect(result.symbol).toBeUndefined();
|
||||||
|
expect(result.graphicRole).toBeUndefined();
|
||||||
|
expect(result.roles).toEqual(["tile", "sprite"]);
|
||||||
|
expect(result.defaultFrame).toBe("frame_0");
|
||||||
|
expect(result.speed).toBe(0);
|
||||||
|
expect(result.playback).toBe("normal");
|
||||||
|
expect(result.tileSymbol).toBe("$");
|
||||||
|
expect(result.frames).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "frame_0",
|
||||||
|
rows: ["A.", "BC"],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves sprite rows from the default enabled frame", () => {
|
||||||
|
const record: JsonObject = {
|
||||||
|
defaultFrame: "walk_1",
|
||||||
|
frames: [
|
||||||
|
{ id: "idle", rows: ["II"], enabled: true },
|
||||||
|
{ id: "walk_1", rows: ["WW"], enabled: true },
|
||||||
|
{ id: "disabled", rows: ["DD"], enabled: false },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getSpriteRows(record)).toEqual(["WW"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes image payload arrays without changing non-record entries", () => {
|
||||||
|
const payload: JsonValue = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
id: "sprite_hero",
|
||||||
|
roles: ["sprite"],
|
||||||
|
rows: ["X"],
|
||||||
|
},
|
||||||
|
"leave-me-alone",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(normalizeImagesPayloadForSave(payload)).toEqual({
|
||||||
|
schemaVersion: 1,
|
||||||
|
images: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "sprite_hero",
|
||||||
|
roles: ["sprite"],
|
||||||
|
defaultFrame: "frame_0",
|
||||||
|
}),
|
||||||
|
"leave-me-alone",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the unified color lookup from catalog entries", () => {
|
||||||
|
setUnifiedColorEntries([
|
||||||
|
{ key: "A", color: "#123456" },
|
||||||
|
{ key: "!", color: "#FFFFFF" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(resolveUnifiedColorSymbol("A")).toBe("#123456");
|
||||||
|
expect(resolveUnifiedColorSymbol(".")).toBe("#00000000");
|
||||||
|
expect(resolveUnifiedColorSymbol("Z", "#ABCDEF")).toBe("#ABCDEF");
|
||||||
|
});
|
||||||
|
});
|
||||||
1
src/test/setup.ts
Normal file
1
src/test/setup.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
77
src/worldChunking.test.ts
Normal file
77
src/worldChunking.test.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import {
|
||||||
|
DEFAULT_WORLD_CHUNK_SIZE,
|
||||||
|
buildChunkFileName,
|
||||||
|
buildChunkKey,
|
||||||
|
createEmptyChunk,
|
||||||
|
localToWorldCoord,
|
||||||
|
normalizeChunkDimension,
|
||||||
|
resolveWorldChunkAddress,
|
||||||
|
worldToChunkCoord,
|
||||||
|
worldToLocalCoord,
|
||||||
|
} from "./worldChunking";
|
||||||
|
|
||||||
|
describe("worldChunking", () => {
|
||||||
|
it("normalizes chunk dimensions with flooring and fallback behavior", () => {
|
||||||
|
expect(normalizeChunkDimension(12.9)).toBe(12);
|
||||||
|
expect(normalizeChunkDimension(0, 24)).toBe(24);
|
||||||
|
expect(normalizeChunkDimension(-5, 24)).toBe(1);
|
||||||
|
expect(normalizeChunkDimension("bad", 18)).toBe(18);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts world coordinates into chunk and local coordinates across the origin", () => {
|
||||||
|
expect(worldToChunkCoord(0, 32)).toBe(0);
|
||||||
|
expect(worldToChunkCoord(31, 32)).toBe(0);
|
||||||
|
expect(worldToChunkCoord(32, 32)).toBe(1);
|
||||||
|
expect(worldToChunkCoord(-1, 32)).toBe(-1);
|
||||||
|
expect(worldToChunkCoord(-33, 32)).toBe(-2);
|
||||||
|
|
||||||
|
expect(worldToLocalCoord(31, 32)).toBe(31);
|
||||||
|
expect(worldToLocalCoord(32, 32)).toBe(0);
|
||||||
|
expect(worldToLocalCoord(-1, 32)).toBe(31);
|
||||||
|
expect(worldToLocalCoord(-33, 32)).toBe(31);
|
||||||
|
|
||||||
|
expect(localToWorldCoord(-2, 31, 32)).toBe(-33);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves stable chunk addressing metadata", () => {
|
||||||
|
expect(buildChunkKey(-3, 4)).toBe("-3:4");
|
||||||
|
expect(buildChunkFileName(-3, 4)).toBe("-3_4.json");
|
||||||
|
expect(resolveWorldChunkAddress(-33, 64, 32, 16)).toEqual({
|
||||||
|
chunkX: -2,
|
||||||
|
chunkY: 4,
|
||||||
|
localX: 31,
|
||||||
|
localY: 0,
|
||||||
|
chunkKey: "-2:4",
|
||||||
|
fileName: "-2_4.json",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates empty chunks with the current compatibility layer defaults", () => {
|
||||||
|
const chunk = createEmptyChunk("overworld", 2, -1, "grass", 4, 3);
|
||||||
|
|
||||||
|
expect(chunk).toEqual({
|
||||||
|
schemaVersion: 1,
|
||||||
|
worldId: "overworld",
|
||||||
|
chunkX: 2,
|
||||||
|
chunkY: -1,
|
||||||
|
width: 4,
|
||||||
|
height: 3,
|
||||||
|
backgroundTileId: "grass",
|
||||||
|
roomLayers: [
|
||||||
|
{
|
||||||
|
layer: 0,
|
||||||
|
rows: ["....", "....", "...."],
|
||||||
|
instanceIds: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
layer: 1,
|
||||||
|
rows: [" ", " ", " "],
|
||||||
|
instanceIds: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
heightLayers: [],
|
||||||
|
instances: [],
|
||||||
|
});
|
||||||
|
expect(createEmptyChunk("world", 0, 0).width).toBe(DEFAULT_WORLD_CHUNK_SIZE);
|
||||||
|
});
|
||||||
|
});
|
||||||
121
src/worldshaperStudio/graphicsDocumentHelpers.test.ts
Normal file
121
src/worldshaperStudio/graphicsDocumentHelpers.test.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
import {
|
||||||
|
buildImageRecordFromSpriteRecord,
|
||||||
|
buildImageRecordFromTileRecord,
|
||||||
|
buildTileRecordFromImageRecord,
|
||||||
|
getImageRecordFromPayload,
|
||||||
|
normalizeGraphicRoles,
|
||||||
|
normalizeImagesPayloadSnapshot,
|
||||||
|
} from "./graphicsDocumentHelpers";
|
||||||
|
import type { JsonObject } from "../editorCore";
|
||||||
|
|
||||||
|
describe("graphicsDocumentHelpers", () => {
|
||||||
|
it("keeps only supported graphic roles and de-duplicates them", () => {
|
||||||
|
expect(normalizeGraphicRoles(["tile", "sprite", "tile", "other", "", null])).toEqual(["tile", "sprite"]);
|
||||||
|
expect(normalizeGraphicRoles("tile")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds tile-backed image records without inventing new storage rules", () => {
|
||||||
|
const existingRecord: JsonObject = {
|
||||||
|
id: "tile_grass",
|
||||||
|
roles: ["sprite"],
|
||||||
|
tileSymbol: "g",
|
||||||
|
tags: ["existing"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = buildImageRecordFromTileRecord({
|
||||||
|
id: "tile_grass",
|
||||||
|
name: "Grass",
|
||||||
|
symbol: "#",
|
||||||
|
rows: ["AB", "C"],
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
pixelScale: 3,
|
||||||
|
opacity: 2,
|
||||||
|
tags: ["terrain", "terrain"],
|
||||||
|
}, existingRecord);
|
||||||
|
|
||||||
|
expect(result.roles).toEqual(["sprite", "tile"]);
|
||||||
|
expect(result.tileSymbol).toBe("#");
|
||||||
|
expect(result.rows).toBeUndefined();
|
||||||
|
expect(result.defaultFrame).toBe("frame_0");
|
||||||
|
expect(result.frames).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "frame_0",
|
||||||
|
rows: ["AB", "C."],
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
pixelScale: 3,
|
||||||
|
opacity: 1,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(result.tags).toEqual(["terrain"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates sprite roles while preserving compatibility fields from existing images", () => {
|
||||||
|
const existingRecord: JsonObject = {
|
||||||
|
id: "hero",
|
||||||
|
roles: ["tile", "sprite"],
|
||||||
|
tileSymbol: "@",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = buildImageRecordFromSpriteRecord({
|
||||||
|
id: "hero",
|
||||||
|
rows: ["X"],
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
}, "other", existingRecord);
|
||||||
|
|
||||||
|
expect(result.roles).toEqual(["tile"]);
|
||||||
|
expect(result.tileSymbol).toBe("@");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hydrates image rows from the resolved default frame", () => {
|
||||||
|
const payload: JsonObject = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
id: "tile_grass",
|
||||||
|
roles: ["tile"],
|
||||||
|
tileSymbol: "G",
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
defaultFrame: "alt",
|
||||||
|
frames: [
|
||||||
|
{ id: "base", rows: ["AA", "AA"], enabled: true },
|
||||||
|
{ id: "alt", rows: ["BB", "BB"], enabled: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const snapshot = normalizeImagesPayloadSnapshot(payload);
|
||||||
|
const imageRecord = getImageRecordFromPayload(snapshot, "tile_grass");
|
||||||
|
|
||||||
|
expect(imageRecord).toEqual(expect.objectContaining({
|
||||||
|
id: "tile_grass",
|
||||||
|
rows: ["BB", "BB"],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects tile records from image records through the current compatibility adapter", () => {
|
||||||
|
const tileRecord = buildTileRecordFromImageRecord({
|
||||||
|
id: "tile_grass",
|
||||||
|
tileSymbol: "G",
|
||||||
|
name: "Grass",
|
||||||
|
description: "Ground",
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
frames: [
|
||||||
|
{ id: "frame_0", rows: ["AB", "CD"] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tileRecord).toEqual(expect.objectContaining({
|
||||||
|
id: "tile_grass",
|
||||||
|
symbol: "G",
|
||||||
|
rows: ["AB", "CD"],
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"target": "es2023",
|
"target": "es2023",
|
||||||
"lib": ["ES2023", "DOM"],
|
"lib": ["ES2023", "DOM"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client", "vitest/globals"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
|
|
|
||||||
10
vitest.config.ts
Normal file
10
vitest.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
globals: true,
|
||||||
|
setupFiles: "./src/test/setup.ts",
|
||||||
|
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue