joplin-plugin-gtd-calendar/src/tests/Gtd/matrixMoveHandler.test.ts

135 lines
5.5 KiB
TypeScript

import { validateAndMoveMatrixCard } from "../../Gtd/matrixMoveHandler";
import { DataAdapter, MutationAdapter, RawFolder, RawNote, RawTag } from "../../Gtd/types";
const folders: RawFolder[] = [
{ id: "board", parent_id: "", title: "Board" },
{ id: "child", parent_id: "board", title: "Child" },
{ id: "outside", parent_id: "", title: "Outside" },
];
function card(overrides: Partial<RawNote> = {}): RawNote {
return {
id: "card", title: "Card", parent_id: "board", is_todo: 0,
todo_due: 0, todo_completed: 0, updated_time: 0, body: "```gtd\n```",
...overrides,
};
}
function setup(current: RawNote | null = card(), attached: RawTag[] = []) {
const writes: string[] = [];
const read: DataAdapter = {
getFolders: async () => folders,
getNotesInFolder: async () => [],
getNoteTagTitles: async () => [],
};
const mutation: MutationAdapter = {
getNote: async () => current,
getNoteTags: async () => attached.slice(),
getAllTags: async () => [
{ id: "urgent", title: "urgent" },
{ id: "important", title: "important" },
],
createTag: async (title) => ({ id: title, title }),
attachTag: async (tagId) => { writes.push(`attach:${tagId}`); },
detachTag: async (tagId) => { writes.push(`detach:${tagId}`); },
setTodoCompleted: async () => { throw new Error("completion write forbidden"); },
};
return { read, mutation, writes };
}
function intent(overrides: Record<string, unknown> = {}) {
return {
type: "moveMatrixCard", view: "matrix", hostNoteId: "host", cardId: "card",
destination: "topLeft",
rawConfig: "mode: eisenhower\neditable: yes\nnotes: all\ntodos: all",
viewInstanceId: "view-1", ...overrides,
};
}
const host = { id: "host", parentId: "board" };
describe("validateAndMoveMatrixCard", () => {
test("accepts a constrained editable Eisenhower move", async () => {
const { read, mutation, writes } = setup();
await expect(validateAndMoveMatrixCard(intent(), host, read, mutation)).resolves.toEqual({
status: "success", viewInstanceId: "view-1", changed: true,
});
expect(writes).toEqual(["attach:urgent", "attach:important"]);
});
test.each([
{}, intent({ view: "kanban" }), intent({ destination: "done" }),
intent({ cardId: "" }), intent({ rawConfig: { editable: "yes" } }),
intent({ patch: { tags: ["urgent"] } }),
])("rejects malformed or expanded input without writes", async (request) => {
const { read, mutation, writes } = setup();
expect((await validateAndMoveMatrixCard(request, host, read, mutation)).status).toBe("error");
expect(writes).toEqual([]);
});
test.each([
["", "error"],
["mode: eisenhower\neditable: no", "error"],
["mode: skeleton\neditable: yes", "error"],
["mode: eisenhower\neditable: yes\nurgent-tag: same\nimportant-tag: SAME", "error"],
["editable: [yes", "stale"],
] as const)("rejects invalid or non-editable config", async (rawConfig, status) => {
const { read, mutation, writes } = setup();
expect((await validateAndMoveMatrixCard(intent({ rawConfig }), host, read, mutation)).status).toBe(status);
expect(writes).toEqual([]);
});
test("requires the selected host identity", async () => {
for (const selected of [null, { id: "other", parentId: "board" }]) {
const { read, mutation, writes } = setup();
expect((await validateAndMoveMatrixCard(intent(), selected, read, mutation)).status).toBe("stale");
expect(writes).toEqual([]);
}
});
test.each([
[card({ id: "host" }), [], "dashboard"],
[card({ parent_id: "outside" }), [], "scope"],
[card({ body: "plain" }), [], "gtd block"],
[card({ is_todo: 1, todo_completed: 99 }), [], "complete"],
[card(), [{ id: "done", title: "DONE" }], "complete"],
] as Array<[RawNote, RawTag[], string]>)("rejects stale item state", async (current, tags, message) => {
const { read, mutation, writes } = setup(current, tags);
const moveResult = await validateAndMoveMatrixCard(intent(), host, read, mutation);
expect(moveResult).toMatchObject({ status: "stale" });
expect((moveResult as any).message).toContain(message);
expect(writes).toEqual([]);
});
test("resolves notebook and children scope from fresh folders", async () => {
const { read, mutation, writes } = setup(card({ parent_id: "child" }));
const moveResult = await validateAndMoveMatrixCard(intent({
rawConfig: "mode: eisenhower\neditable: yes\nnotebook: Board\nscope: children",
}), host, read, mutation);
expect(moveResult.status).toBe("success");
expect(writes).toEqual(["attach:urgent", "attach:important"]);
});
test.each([
[card({ is_todo: 0 }), "mode: eisenhower\neditable: yes\nnotes: none\ntodos: all"],
[card({ is_todo: 1, body: "plain" }), "mode: eisenhower\neditable: yes\nnotes: all\ntodos: gtd-only"],
[card({ is_todo: 1 }), "mode: eisenhower\neditable: yes\nnotes: all\ntodos: none"],
])("rechecks filters and opt-in", async (current, rawConfig) => {
const { read, mutation, writes } = setup(current);
expect((await validateAndMoveMatrixCard(intent({ rawConfig }), host, read, mutation)).status).toBe("stale");
expect(writes).toEqual([]);
});
test("returns a safe error and logs API diagnostics", async () => {
const { read, mutation, writes } = setup();
const diagnostics: unknown[] = [];
read.getFolders = async () => { throw new Error("private detail"); };
expect(await validateAndMoveMatrixCard(intent(), host, read, mutation, (e) => diagnostics.push(e))).toEqual({
status: "error", viewInstanceId: "view-1",
message: "Could not apply the move. Refresh and try again.",
});
expect(diagnostics).toHaveLength(1);
expect(writes).toEqual([]);
});
});