160 lines
6.2 KiB
TypeScript
160 lines
6.2 KiB
TypeScript
import { moveMatrixItem } from "../../Gtd/matrixMutation";
|
|
import { MatrixDestination, MutationAdapter, RawNote, RawTag } from "../../Gtd/types";
|
|
|
|
function note(isTodo: boolean): RawNote {
|
|
return {
|
|
id: "card", title: "Card", parent_id: "folder", is_todo: isTodo ? 1 : 0,
|
|
todo_due: 123, todo_completed: 0, updated_time: 456, body: "```gtd\n```",
|
|
};
|
|
}
|
|
|
|
function fakeAdapter(current: RawNote | null, attached: RawTag[], all?: RawTag[]) {
|
|
const calls: string[] = [];
|
|
const adapter: MutationAdapter = {
|
|
getNote: async () => current,
|
|
getNoteTags: async () => attached.slice(),
|
|
getAllTags: async () => (all || [
|
|
{ id: "u", title: "urgent" }, { id: "i", title: "important" },
|
|
]).slice(),
|
|
createTag: async (title) => { calls.push(`create:${title}`); return { id: title, title }; },
|
|
attachTag: async (tagId) => { calls.push(`attach:${tagId}`); },
|
|
detachTag: async (tagId) => { calls.push(`detach:${tagId}`); },
|
|
setTodoCompleted: async (_noteId, completedTime) => {
|
|
calls.push(`complete:${completedTime > 0}`);
|
|
},
|
|
};
|
|
return { adapter, calls };
|
|
}
|
|
|
|
const states: Array<{
|
|
destination: MatrixDestination;
|
|
urgent: boolean;
|
|
important: boolean;
|
|
}> = [
|
|
{ destination: "topLeft", urgent: true, important: true },
|
|
{ destination: "topRight", urgent: false, important: true },
|
|
{ destination: "bottomLeft", urgent: true, important: false },
|
|
{ destination: "bottomRight", urgent: false, important: false },
|
|
];
|
|
|
|
const config = {
|
|
mode: "eisenhower" as const,
|
|
urgentTag: "urgent", importantTag: "important",
|
|
inProgressTag: "in-progress", onDeckTag: "on-deck", doneTag: "done",
|
|
};
|
|
|
|
describe("moveMatrixItem", () => {
|
|
for (const isTodo of [false, true]) {
|
|
for (const source of states) {
|
|
for (const target of states) {
|
|
test(`${isTodo ? "to-do" : "note"}: ${source.destination} -> ${target.destination}`, async () => {
|
|
const tags: RawTag[] = [];
|
|
if (source.urgent) tags.push({ id: "u", title: "urgent" });
|
|
if (source.important) tags.push({ id: "i", title: "important" });
|
|
const { adapter, calls } = fakeAdapter(note(isTodo), tags);
|
|
const result = await moveMatrixItem(adapter, "card", target.destination, config);
|
|
const expected: string[] = [];
|
|
if (source.urgent !== target.urgent) expected.push(`${target.urgent ? "attach" : "detach"}:u`);
|
|
if (source.important !== target.important) expected.push(`${target.important ? "attach" : "detach"}:i`);
|
|
expect(calls).toEqual(expected);
|
|
expect(result.changed).toBe(source.destination !== target.destination);
|
|
expect(result.note).toMatchObject({
|
|
is_todo: isTodo ? 1 : 0, todo_due: 123, todo_completed: 0,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
test("uses custom tag names, existing case variants, and deterministic duplicates", async () => {
|
|
const { adapter, calls } = fakeAdapter(note(false), [
|
|
{ id: "z", title: "FIRE" }, { id: "a", title: "fire" },
|
|
{ id: "keep", title: "keep-me" },
|
|
], [{ id: "m", title: "MATTERS" }]);
|
|
await moveMatrixItem(adapter, "card", "topRight", {
|
|
...config, urgentTag: "fire", importantTag: "matters",
|
|
});
|
|
expect(calls).toEqual(["detach:a", "detach:z", "attach:m"]);
|
|
});
|
|
|
|
test("creates only a missing required tag", async () => {
|
|
const { adapter, calls } = fakeAdapter(note(true), [], []);
|
|
await moveMatrixItem(adapter, "card", "bottomLeft", config);
|
|
expect(calls).toEqual(["create:urgent", "attach:urgent"]);
|
|
});
|
|
|
|
test.each(["skeleton", "eisenhower"] as const)(
|
|
"%s Done completes a native to-do and removes its in-progress tag",
|
|
async (mode) => {
|
|
const { adapter, calls } = fakeAdapter(note(true), [
|
|
{ id: "p", title: "in-progress" }, { id: "u", title: "urgent" },
|
|
]);
|
|
await moveMatrixItem(adapter, "card", "done", { ...config, mode });
|
|
expect(calls).toEqual(["complete:true", "detach:p"]);
|
|
}
|
|
);
|
|
|
|
test.each(["skeleton", "eisenhower"] as const)(
|
|
"%s Done applies the configured done tag to an ordinary note",
|
|
async (mode) => {
|
|
const { adapter, calls } = fakeAdapter(note(false), [
|
|
{ id: "p", title: "in-progress" },
|
|
], [{ id: "d", title: "Finished" }]);
|
|
await moveMatrixItem(adapter, "card", "done", {
|
|
...config, mode, doneTag: "finished",
|
|
});
|
|
expect(calls).toEqual(["attach:d", "detach:p"]);
|
|
}
|
|
);
|
|
|
|
test.each([
|
|
["topLeft", "urgent"], ["topRight", "in-progress"],
|
|
["bottomLeft", "on-deck"], ["bottomRight", null],
|
|
] as Array<[MatrixDestination, string | null]>)
|
|
("Skeleton %s normalizes the three workflow tags", async (destination, expected) => {
|
|
const { adapter, calls } = fakeAdapter(note(false), [
|
|
{ id: "u", title: "urgent" }, { id: "i", title: "in-progress" },
|
|
{ id: "o", title: "on-deck" },
|
|
], [
|
|
{ id: "u", title: "urgent" }, { id: "i", title: "in-progress" },
|
|
{ id: "o", title: "on-deck" },
|
|
]);
|
|
await moveMatrixItem(adapter, "card", destination, { ...config, mode: "skeleton" });
|
|
const keptId = expected === "urgent" ? "u" : expected === "in-progress" ? "i" : expected === "on-deck" ? "o" : null;
|
|
expect(calls).toEqual(["u", "i", "o"].filter((id) => id !== keptId).map((id) => `detach:${id}`));
|
|
});
|
|
|
|
test("fresh validation can reject completed or newly filtered cards before writes", async () => {
|
|
for (const current of [note(true), note(false)]) {
|
|
current.todo_completed = current.is_todo ? 100 : 0;
|
|
const { adapter, calls } = fakeAdapter(current, []);
|
|
await expect(moveMatrixItem(adapter, "card", "topLeft", config, () => {
|
|
throw new Error("card is no longer eligible");
|
|
})).rejects.toThrow("no longer eligible");
|
|
expect(calls).toEqual([]);
|
|
}
|
|
});
|
|
|
|
test("surfaces partial failures without compensating writes", async () => {
|
|
const { adapter, calls } = fakeAdapter(note(false), [], [
|
|
{ id: "u", title: "urgent" }, { id: "i", title: "important" },
|
|
]);
|
|
adapter.attachTag = async (tagId) => {
|
|
calls.push(`attach:${tagId}`);
|
|
if (tagId === "i") throw new Error("important attach failed");
|
|
};
|
|
await expect(moveMatrixItem(adapter, "card", "topLeft", config)).rejects.toThrow(
|
|
"important attach failed"
|
|
);
|
|
expect(calls).toEqual(["attach:u", "attach:i"]);
|
|
});
|
|
|
|
test("fails before writes when the card disappeared", async () => {
|
|
const { adapter, calls } = fakeAdapter(null, []);
|
|
await expect(moveMatrixItem(adapter, "card", "topLeft", config)).rejects.toThrow(
|
|
"no longer exists"
|
|
);
|
|
expect(calls).toEqual([]);
|
|
});
|
|
});
|