test: add edge case tests
This commit is contained in:
@@ -68,4 +68,44 @@ body
|
|||||||
if (result.isOk()) return;
|
if (result.isOk()) return;
|
||||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_TASK_FILE);
|
expect(result.error.variant).toBe(AppErrorVariant.INVALID_TASK_FILE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles raw content with fields in different order", () => {
|
||||||
|
const raw = `---
|
||||||
|
status: done
|
||||||
|
id: "should-be-ignored"
|
||||||
|
title: Reordered
|
||||||
|
priority: low
|
||||||
|
tags: [x, y]
|
||||||
|
assignee: ""
|
||||||
|
created: 2099-01-01T00:00:00.000Z
|
||||||
|
updated: 2026-01-02T00:00:00.000Z
|
||||||
|
---
|
||||||
|
body text
|
||||||
|
`;
|
||||||
|
const result = applyRawTaskContent(raw, previous);
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isErr()) return;
|
||||||
|
expect(result.value.title).toBe("Reordered");
|
||||||
|
expect(result.value.id).toBe(previous.id);
|
||||||
|
expect(result.value.created).toBe(previous.created);
|
||||||
|
expect(result.value.status).toBe(TaskStatus.DONE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles raw content with empty description body", () => {
|
||||||
|
const raw = `---
|
||||||
|
id: "x"
|
||||||
|
title: No body
|
||||||
|
status: todo
|
||||||
|
priority: medium
|
||||||
|
tags: []
|
||||||
|
assignee: ""
|
||||||
|
created: 2026-01-01T00:00:00.000Z
|
||||||
|
updated: 2026-01-01T00:00:00.000Z
|
||||||
|
---
|
||||||
|
`;
|
||||||
|
const result = applyRawTaskContent(raw, previous);
|
||||||
|
expect(result.isOk()).toBe(true);
|
||||||
|
if (result.isErr()) return;
|
||||||
|
expect(result.value.description).toBe("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,4 +80,11 @@ describe("buildDashboardView", () => {
|
|||||||
const view = buildDashboardView(tasks, { selectedId: "missing" });
|
const view = buildDashboardView(tasks, { selectedId: "missing" });
|
||||||
expect(view.selected).toBeUndefined();
|
expect(view.selected).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles empty tasks array", () => {
|
||||||
|
const view = buildDashboardView([]);
|
||||||
|
expect(view.visibleTasks).toEqual([]);
|
||||||
|
expect(view.groups).toEqual([]);
|
||||||
|
expect(view.selected).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -98,4 +98,9 @@ describe("filterTasks", () => {
|
|||||||
});
|
});
|
||||||
expect(result.map((t) => t.id)).toEqual(["4"]);
|
expect(result.map((t) => t.id)).toEqual(["4"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns empty array for empty input", () => {
|
||||||
|
expect(filterTasks([], {})).toEqual([]);
|
||||||
|
expect(filterTasks([], { statuses: ["todo"] })).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -93,4 +93,30 @@ describe("groupTasks", () => {
|
|||||||
expect(groupTasks([], GroupBy.STATUS)).toEqual([]);
|
expect(groupTasks([], GroupBy.STATUS)).toEqual([]);
|
||||||
expect(groupTasks([], GroupBy.NONE)).toEqual([]);
|
expect(groupTasks([], GroupBy.NONE)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("silently drops tasks with unknown status from groups", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
task({ id: "a", title: "A", status: "__garbage__" as TaskStatus }),
|
||||||
|
task({ id: "b", title: "B", status: TaskStatus.TODO }),
|
||||||
|
];
|
||||||
|
const groups = groupTasks(tasks, GroupBy.STATUS);
|
||||||
|
const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id));
|
||||||
|
expect(allGrouped).toEqual(["b"]);
|
||||||
|
expect(allGrouped).not.toContain("a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("silently drops tasks with unknown priority from groups", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
task({
|
||||||
|
id: "a",
|
||||||
|
title: "A",
|
||||||
|
priority: "__garbage__" as TaskPriority,
|
||||||
|
}),
|
||||||
|
task({ id: "b", title: "B", priority: TaskPriority.LOW }),
|
||||||
|
];
|
||||||
|
const groups = groupTasks(tasks, GroupBy.PRIORITY);
|
||||||
|
const allGrouped = groups.flatMap((g) => g.tasks.map((t) => t.id));
|
||||||
|
expect(allGrouped).toEqual(["b"]);
|
||||||
|
expect(allGrouped).not.toContain("a");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,4 +61,28 @@ describe("sortTasks", () => {
|
|||||||
sortTasks(tasks, TaskSortDirection.DESC);
|
sortTasks(tasks, TaskSortDirection.DESC);
|
||||||
expect(tasks.map((t) => t.id)).toEqual(copy.map((t) => t.id));
|
expect(tasks.map((t) => t.id)).toEqual(copy.map((t) => t.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns empty array for empty input", () => {
|
||||||
|
expect(sortTasks([])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves input order when created dates are identical (stable sort)", () => {
|
||||||
|
const sameDate = "2026-01-01T00:00:00.000Z";
|
||||||
|
const tasks: Task[] = [
|
||||||
|
task({ id: "a", title: "A", created: sameDate }),
|
||||||
|
task({ id: "b", title: "B", created: sameDate }),
|
||||||
|
task({ id: "c", title: "C", created: sameDate }),
|
||||||
|
];
|
||||||
|
const result = sortTasks(tasks, TaskSortDirection.ASC);
|
||||||
|
expect(result.map((t) => t.id)).toEqual(["a", "b", "c"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles invalid date strings without crashing", () => {
|
||||||
|
const tasks: Task[] = [
|
||||||
|
task({ id: "a", title: "A", created: "invalid-date" }),
|
||||||
|
task({ id: "b", title: "B", created: "2026-01-01T00:00:00.000Z" }),
|
||||||
|
];
|
||||||
|
const result = sortTasks(tasks, TaskSortDirection.ASC);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,4 +117,31 @@ describe("buildKanbanBoard", () => {
|
|||||||
expect(col.tasks).toEqual([]);
|
expect(col.tasks).toEqual([]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves input order for tasks with same updated timestamp (stable sort)", () => {
|
||||||
|
const sameUpdated = "2026-01-01T00:00:00.000Z";
|
||||||
|
const tasks: Task[] = [
|
||||||
|
task({
|
||||||
|
id: "a",
|
||||||
|
title: "A",
|
||||||
|
status: TaskStatus.TODO,
|
||||||
|
updated: sameUpdated,
|
||||||
|
}),
|
||||||
|
task({
|
||||||
|
id: "b",
|
||||||
|
title: "B",
|
||||||
|
status: TaskStatus.TODO,
|
||||||
|
updated: sameUpdated,
|
||||||
|
}),
|
||||||
|
task({
|
||||||
|
id: "c",
|
||||||
|
title: "C",
|
||||||
|
status: TaskStatus.TODO,
|
||||||
|
updated: sameUpdated,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const board = buildKanbanBoard(tasks);
|
||||||
|
const todo = board.columns.find((col) => col.status === TaskStatus.TODO);
|
||||||
|
expect(todo?.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -195,4 +195,52 @@ describe("FsTaskRepository", () => {
|
|||||||
expect(tasks[0].id).not.toBe(first.id);
|
expect(tasks[0].id).not.toBe(first.id);
|
||||||
expect(tasks[0].description).toBe("second");
|
expect(tasks[0].description).toBe("second");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates task with Cyrillic title and slug", async () => {
|
||||||
|
const task = await repo.create(folder, {
|
||||||
|
...sampleTask,
|
||||||
|
title: "Тестовая задача",
|
||||||
|
});
|
||||||
|
expect(task.title).toBe("Тестовая задача");
|
||||||
|
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
expect(files).toEqual([`тестовая-задача${TASK_FILE_EXTENSION}`]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates task with title exactly at MAX_TASK_TITLE_LENGTH", async () => {
|
||||||
|
const title = "a".repeat(80);
|
||||||
|
const task = await repo.create(folder, { ...sampleTask, title });
|
||||||
|
expect(task.title).toBe(title);
|
||||||
|
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
expect(files).toEqual([`${title}${TASK_FILE_EXTENSION}`]);
|
||||||
|
|
||||||
|
const found = await repo.getById(folder, task.id);
|
||||||
|
expect(found?.title).toBe(title);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates task with tags containing special characters", async () => {
|
||||||
|
const task = await repo.create(folder, {
|
||||||
|
...sampleTask,
|
||||||
|
tags: ["c#", "c++", "tag name", "tag:value"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
expect(files).toHaveLength(1);
|
||||||
|
|
||||||
|
const content = await fs.readFile(
|
||||||
|
`${folder}/fix-login-bug${TASK_FILE_EXTENSION}`,
|
||||||
|
);
|
||||||
|
expect(content).toContain("c#");
|
||||||
|
expect(content).toContain("tag:value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getFilePath produces consistent slug regardless of special characters", async () => {
|
||||||
|
const task = await repo.create(folder, {
|
||||||
|
...sampleTask,
|
||||||
|
title: "Fix___bug...",
|
||||||
|
});
|
||||||
|
const files = await fs.readdir(folder);
|
||||||
|
expect(files).toEqual([`fix-bug${TASK_FILE_EXTENSION}`]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user