feat: add extensive test suite

This commit is contained in:
2026-08-30 11:33:53 +07:00 Unverified
parent 061cce00dc
commit 8359c7272e
131 changed files with 8717 additions and 2200 deletions
+101
View File
@@ -0,0 +1,101 @@
import { describe, expect, test } from "bun:test";
import {
midpointOrder,
normalizedOrders,
ORDER_STEP,
reorderFlatItems,
} from "./order";
const items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }];
describe("reorderFlatItems", () => {
test.each([
["a", "d", ["b", "c", "d", "a"]],
["d", "a", ["d", "a", "b", "c"]],
["b", "c", ["a", "c", "b", "d"]],
["c", "b", ["a", "c", "b", "d"]],
])("moves %s over %s", (active, over, expected) => {
const result = reorderFlatItems(items, active, over);
expect(result.map(({ id }) => id)).toEqual(expected);
expect(result).not.toBe(items);
expect(items.map(({ id }) => id)).toEqual(["a", "b", "c", "d"]);
});
test("returns the original array when IDs are equal", () => {
expect(reorderFlatItems(items, "b", "b")).toBe(items);
});
test.each([
["missing", "a"],
["a", "missing"],
["missing", "other"],
])("returns the original array for unknown IDs", (active, over) => {
expect(reorderFlatItems(items, active, over)).toBe(items);
});
test("moves the first matching item when IDs are duplicated", () => {
const duplicate = [
{ id: "a", value: 1 },
{ id: "a", value: 2 },
{ id: "b", value: 3 },
];
expect(
reorderFlatItems(duplicate, "a", "b").map(({ value }) => value),
).toEqual([2, 3, 1]);
});
test("handles an empty array", () => {
const empty: { id: string }[] = [];
expect(reorderFlatItems(empty, "a", "b")).toBe(empty);
});
});
describe("midpointOrder", () => {
test.each([
[undefined, undefined, ORDER_STEP],
[undefined, 30, 20],
[20, undefined, 30],
[10, 30, 20],
[10, 31, 20],
[-20, -10, -15],
[0, 1, null],
[1, 2, null],
[-2, -1, null],
[10, 10, null],
] as const)("returns %p between %p and %p", (prev, next, expected) => {
expect(midpointOrder(prev, next)).toBe(expected);
});
test("treats null like a missing boundary", () => {
expect(midpointOrder(null as unknown as undefined, 20)).toBe(10);
expect(midpointOrder(20, null as unknown as undefined)).toBe(30);
});
});
describe("normalizedOrders", () => {
test("assigns stable ORDER_STEP increments", () => {
expect(normalizedOrders(["c", "a", "b"])).toEqual([
{ id: "c", order: 10 },
{ id: "a", order: 20 },
{ id: "b", order: 30 },
]);
});
test("returns an empty list for no IDs", () => {
expect(normalizedOrders([])).toEqual([]);
});
test("preserves duplicate and empty IDs", () => {
expect(normalizedOrders(["", "x", "x"])).toEqual([
{ id: "", order: 10 },
{ id: "x", order: 20 },
{ id: "x", order: 30 },
]);
});
test("does not mutate the input", () => {
const ids = ["b", "a"];
normalizedOrders(ids);
expect(ids).toEqual(["b", "a"]);
});
});
+33 -1
View File
@@ -24,13 +24,45 @@ export function midpointOrder(prev?: number, next?: number) {
return middle;
}
export function normalizedOrders(ids: string[]) {
export function normalizedOrders(ids: readonly string[]) {
return ids.map((id, index) => ({
id,
order: (index + 1) * ORDER_STEP,
}));
}
export type OrderItem = { id: string; order: number };
export function plannedOrderUpdates(
items: OrderItem[],
ids: readonly string[],
activeId: string,
staleMessage: string,
) {
const currentIds = items.map(({ id }) => id);
const uniqueIds = new Set(ids);
if (
ids.length === 0 ||
uniqueIds.size !== ids.length ||
currentIds.length !== ids.length ||
currentIds.some((id) => !uniqueIds.has(id))
) {
throw new Error(staleMessage);
}
const index = ids.indexOf(activeId);
if (index === -1) throw new Error("Moved item is missing.");
const byId = new Map(items.map((item) => [item.id, item]));
const previous = index > 0 ? byId.get(ids[index - 1]) : undefined;
const next = index < ids.length - 1 ? byId.get(ids[index + 1]) : undefined;
const nextOrder = midpointOrder(previous?.order, next?.order);
return nextOrder == null
? normalizedOrders(ids)
: [{ id: activeId, order: nextOrder }];
}
function arrayMove<T>(arr: T[], oldIndex: number, newIndex: number) {
return arr.toSpliced(oldIndex, 1).toSpliced(newIndex, 0, arr[oldIndex]);
}
@@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test";
import { plannedOrderUpdates } from "./order";
import { reorderService } from "./reorder-service";
const items = [
{ id: "a", order: 10 },
{ id: "b", order: 20 },
{ id: "c", order: 30 },
];
describe("plannedOrderUpdates", () => {
test("uses a midpoint when sufficient order space exists", () => {
expect(plannedOrderUpdates(items, ["b", "a", "c"], "a", "stale")).toEqual([
{ id: "a", order: 25 },
]);
});
test("normalizes every item when adjacent orders have no midpoint", () => {
expect(
plannedOrderUpdates(
[
{ id: "a", order: 1 },
{ id: "b", order: 2 },
{ id: "c", order: 3 },
],
["b", "a", "c"],
"a",
"stale",
),
).toEqual([
{ id: "b", order: 10 },
{ id: "a", order: 20 },
{ id: "c", order: 30 },
]);
});
test.each([
{ ids: [] as string[] },
{ ids: ["a", "a", "c"] },
{ ids: ["a", "b"] },
{ ids: ["a", "b", "stale"] },
{ ids: ["a", "b", "c", "extra"] },
])("rejects malformed, duplicate, or stale IDs: %p", ({ ids }) => {
expect(() => plannedOrderUpdates(items, ids, "a", "refresh")).toThrow(
"refresh",
);
});
test("rejects an active ID absent from an otherwise valid order", () => {
expect(() =>
plannedOrderUpdates(items, ["a", "b", "c"], "missing", "stale"),
).toThrow("Moved item is missing");
});
});
describe("reorderService", () => {
test("requires admin before opening a transaction", async () => {
let transactionCalled = false;
expect(
reorderService("a", ["a"], "stale", {
adminCheck: async () => false,
transaction: async (callback) => {
transactionCalled = true;
return callback({
list: async () => [],
update: async () => {},
});
},
afterReorder: async () => {},
}),
).rejects.toBe("Unauthorized");
expect(transactionCalled).toBe(false);
});
test("applies planned updates atomically before side effects", async () => {
const calls: string[] = [];
await reorderService("a", ["b", "a", "c"], "stale", {
adminCheck: async () => true,
transaction: async (callback) => {
await callback({
list: async () => items,
update: async (id, order) => {
calls.push(`update:${id}:${order}`);
},
});
},
afterReorder: async () => {
calls.push("after");
},
});
expect(calls).toEqual(["update:a:25", "after"]);
});
});
@@ -0,0 +1,35 @@
import { type OrderItem, plannedOrderUpdates } from "./order";
export interface ReorderDependencies {
adminCheck: () => Promise<unknown>;
transaction: (
callback: (repository: {
list: () => Promise<OrderItem[]>;
update: (id: string, order: number) => Promise<void>;
}) => Promise<void>,
) => Promise<void>;
afterReorder: () => Promise<void> | void;
}
export async function reorderService(
activeId: string,
ids: readonly string[],
staleMessage: string,
dependencies: ReorderDependencies,
) {
if (!(await dependencies.adminCheck())) throw "Unauthorized";
await dependencies.transaction(async (repository) => {
const updates = plannedOrderUpdates(
await repository.list(),
ids,
activeId,
staleMessage,
);
for (const update of updates) {
await repository.update(update.id, update.order);
}
});
await dependencies.afterReorder();
}