- `tech-tree-layout.int.spec.ts`: pure layout (wrapping, manual overrides, category bands, forward-edge warnings, cycles, catalog-stability). - `tech-tree-prereq-edits.int.spec.ts`: add/remove/switch prerequisites with cycle rejection and the pin-on-connect stability contract. - `tech-tree-stability.int.spec.ts`: freeze pins every relocated card on a data change, plus edge visuals (per-path colors, per-source lanes). - `tech-categories.int.spec.ts`: CRUD, RBAC, division gating, FK set-null, position round-trip. - `tech-tree.e2e.spec.ts`: nav entry, render, preview/panel edits, both quick-create paths, drag persistence + Auto-arrange, save positions surviving a reload, connect/disconnect, and multi-column wrapping.
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
addPrerequisite,
|
|
buildPrereqRows,
|
|
dependsOn,
|
|
removePrerequisite,
|
|
setPrerequisiteKind,
|
|
withPrereqs,
|
|
} from "@/components/admin/tech-tree/prereqEdits";
|
|
import type { TreeTech } from "@/components/admin/tech-tree/types";
|
|
|
|
const tech = (over: Partial<TreeTech> & { id: number }): TreeTech => ({
|
|
name: `Tech ${over.id}`,
|
|
type: "upgrade",
|
|
approvalStatus: "in_progress",
|
|
summary: "",
|
|
categoryId: null,
|
|
prereqAnd: [],
|
|
prereqOr: [],
|
|
resourceCosts: [],
|
|
treePosition: null,
|
|
...over,
|
|
});
|
|
|
|
describe("tech tree prerequisite edits (pure)", () => {
|
|
it("adds an AND prerequisite", () => {
|
|
const techs = [tech({ id: 1 }), tech({ id: 2 })];
|
|
const result = addPrerequisite(techs, 2, 1, "and");
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) expect(result.lists).toEqual({ and: [1], or: [] });
|
|
});
|
|
|
|
it("adds an OR prerequisite", () => {
|
|
const techs = [tech({ id: 1 }), tech({ id: 2 })];
|
|
const result = addPrerequisite(techs, 2, 1, "or");
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) expect(result.lists).toEqual({ and: [], or: [1] });
|
|
});
|
|
|
|
it("rejects self references", () => {
|
|
const result = addPrerequisite([tech({ id: 1 })], 1, 1, "and");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error).toMatch(/own prerequisite/i);
|
|
});
|
|
|
|
it("rejects duplicates", () => {
|
|
const techs = [tech({ id: 1 }), tech({ id: 2, prereqAnd: [1] })];
|
|
const result = addPrerequisite(techs, 2, 1, "or");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error).toMatch(/already a prerequisite/i);
|
|
});
|
|
|
|
it("rejects a direct cycle", () => {
|
|
const techs = [tech({ id: 1, prereqAnd: [2] }), tech({ id: 2 })];
|
|
// Adding 1 as a prerequisite of 2 would close 2 -> 1 -> 2.
|
|
const result = addPrerequisite(techs, 2, 1, "and");
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error).toMatch(/circular/i);
|
|
});
|
|
|
|
it("rejects a transitive cycle", () => {
|
|
const techs = [
|
|
tech({ id: 1 }),
|
|
tech({ id: 2, prereqAnd: [1] }),
|
|
tech({ id: 3, prereqAnd: [2] }),
|
|
];
|
|
// 3 depends on 1 transitively; making 3 a prerequisite of 1 closes a loop.
|
|
expect(dependsOn(techs, 3, 1)).toBe(true);
|
|
const result = addPrerequisite(techs, 1, 3, "and");
|
|
expect(result.ok).toBe(false);
|
|
});
|
|
|
|
it("allows a forward chain", () => {
|
|
const techs = [tech({ id: 1 }), tech({ id: 2, prereqAnd: [1] }), tech({ id: 3 })];
|
|
const result = addPrerequisite(techs, 3, 2, "and");
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) expect(result.lists).toEqual({ and: [2], or: [] });
|
|
});
|
|
|
|
it("removes a prerequisite from both groups", () => {
|
|
const techs = [tech({ id: 1 }), tech({ id: 2, prereqAnd: [1], prereqOr: [1, 3] })];
|
|
const result = removePrerequisite(techs, 2, 1);
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) expect(result.lists).toEqual({ and: [], or: [3] });
|
|
});
|
|
|
|
it("moves a prerequisite between AND and OR", () => {
|
|
const techs = [tech({ id: 1 }), tech({ id: 2, prereqAnd: [1] })];
|
|
const toOr = setPrerequisiteKind(techs, 2, 1, "or");
|
|
expect(toOr.ok).toBe(true);
|
|
if (toOr.ok) expect(toOr.lists).toEqual({ and: [], or: [1] });
|
|
|
|
const back = setPrerequisiteKind(techs, 2, 1, "and");
|
|
expect(back.ok).toBe(true);
|
|
if (back.ok) expect(back.lists).toEqual({ and: [1], or: [] });
|
|
});
|
|
|
|
it("builds a single document-shaped prerequisite row", () => {
|
|
expect(buildPrereqRows({ and: [1, 2], or: [3] })).toEqual([
|
|
{ requiresAnd: { technology: [1, 2] }, requiresOr: { technology: [3] } },
|
|
]);
|
|
});
|
|
|
|
it("withPrereqs returns a tech carrying the new lists", () => {
|
|
const updated = withPrereqs(tech({ id: 1 }), { and: [4], or: [5] });
|
|
expect(updated.prereqAnd).toEqual([4]);
|
|
expect(updated.prereqOr).toEqual([5]);
|
|
});
|
|
});
|