Add Vitest suite covering the smoke-test fixes
Extracts two pure helpers out of Svelte components so they can be
exercised without the reactive runtime, and adds component tests for
ConfirmDialog's Escape-handling behavior.
- apps/tauri/src/lib/grouping.ts (new): `groupTasksByDate` lifted out of
the `groupedPendingTasks` $derived in the app store.
- apps/tauri/src/lib/paths.ts (new): `workspaceNameFromPath` lifted out
of SetupScreen.handleOpen.
- apps/tauri/src/lib/grouping.test.ts: 8 cases — "No Date" placed last
(regression), full bucket ordering, empty input, within-bucket
stable sort, earlier-today stays in Today, multi-task same-day,
No Date preserves insertion order.
- apps/tauri/src/lib/paths.test.ts: 8 cases — POSIX/Windows/mixed
separators, trailing slash regression ("…/Tasks/" → "Tasks"), empty
and root-only fallback, names with spaces.
- apps/tauri/src/lib/components/ConfirmDialog.test.ts: 6 cases —
renders message/detail/custom confirm text, Cancel/Confirm fire the
right callbacks, Escape calls oncancel and does NOT reach an outer
window listener (regression), non-Escape keys are ignored, and the
module-level open-count increments/decrements correctly (including
when two dialogs are mounted at once).
Test harness: Vitest + jsdom + @testing-library/svelte. `npm test`
runs the suite; `resolve.conditions` is set to "browser" under VITEST
so Svelte resolves its client entry and mount() works.
23/23 tests pass. cargo check, cargo test -p onyx-core (162/162),
and npm run build all still green.
This commit is contained in:
parent
8a04895270
commit
67ac43e527
1197
apps/tauri/package-lock.json
generated
1197
apps/tauri/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,16 +7,23 @@
|
|||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
"tauri": "tauri",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/svelte": "^5.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"jsdom": "^29.0.2",
|
||||
"svelte": "^5.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^4.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
|
|
|
|||
105
apps/tauri/src/lib/components/ConfirmDialog.test.ts
Normal file
105
apps/tauri/src/lib/components/ConfirmDialog.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/svelte";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import ConfirmDialog, { isConfirmDialogOpen } from "./ConfirmDialog.svelte";
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("ConfirmDialog", () => {
|
||||
it("renders the message, detail and custom confirm label", () => {
|
||||
render(ConfirmDialog, {
|
||||
message: "Delete task?",
|
||||
detail: "This cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
onconfirm: vi.fn(),
|
||||
oncancel: vi.fn(),
|
||||
});
|
||||
expect(screen.getByText("Delete task?")).toBeInTheDocument();
|
||||
expect(screen.getByText("This cannot be undone.")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Delete" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fires oncancel when Cancel is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const oncancel = vi.fn();
|
||||
render(ConfirmDialog, {
|
||||
message: "Delete?",
|
||||
onconfirm: vi.fn(),
|
||||
oncancel,
|
||||
});
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(oncancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("fires onconfirm when Confirm is clicked and not oncancel", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onconfirm = vi.fn();
|
||||
const oncancel = vi.fn();
|
||||
render(ConfirmDialog, {
|
||||
message: "Delete?",
|
||||
confirmText: "Delete",
|
||||
onconfirm,
|
||||
oncancel,
|
||||
});
|
||||
await user.click(screen.getByRole("button", { name: "Delete" }));
|
||||
expect(onconfirm).toHaveBeenCalledTimes(1);
|
||||
expect(oncancel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("cancels and stops propagation on Escape (regression: used to bubble and pop task detail)", async () => {
|
||||
const oncancel = vi.fn();
|
||||
// An outer bubble-phase listener emulates TasksScreen's svelte:window
|
||||
// Escape handler. If the dialog leaks Escape, this spy fires too.
|
||||
const outer = vi.fn();
|
||||
window.addEventListener("keydown", outer);
|
||||
try {
|
||||
render(ConfirmDialog, {
|
||||
message: "Delete?",
|
||||
onconfirm: vi.fn(),
|
||||
oncancel,
|
||||
});
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
|
||||
expect(oncancel).toHaveBeenCalledTimes(1);
|
||||
expect(outer).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
window.removeEventListener("keydown", outer);
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores non-Escape keydowns", async () => {
|
||||
const oncancel = vi.fn();
|
||||
render(ConfirmDialog, {
|
||||
message: "Delete?",
|
||||
onconfirm: vi.fn(),
|
||||
oncancel,
|
||||
});
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "a" }));
|
||||
window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
|
||||
expect(oncancel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("increments the open-count singleton so parent Escape handlers can defer", () => {
|
||||
expect(isConfirmDialogOpen()).toBe(false);
|
||||
const { unmount } = render(ConfirmDialog, {
|
||||
message: "Delete?",
|
||||
onconfirm: vi.fn(),
|
||||
oncancel: vi.fn(),
|
||||
});
|
||||
expect(isConfirmDialogOpen()).toBe(true);
|
||||
unmount();
|
||||
expect(isConfirmDialogOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it("tracks multiple concurrently-mounted dialogs and releases on unmount", () => {
|
||||
const a = render(ConfirmDialog, { message: "A?", onconfirm: vi.fn(), oncancel: vi.fn() });
|
||||
const b = render(ConfirmDialog, { message: "B?", onconfirm: vi.fn(), oncancel: vi.fn() });
|
||||
expect(isConfirmDialogOpen()).toBe(true);
|
||||
a.unmount();
|
||||
expect(isConfirmDialogOpen()).toBe(true);
|
||||
b.unmount();
|
||||
expect(isConfirmDialogOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
97
apps/tauri/src/lib/grouping.test.ts
Normal file
97
apps/tauri/src/lib/grouping.test.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { groupTasksByDate } from "./grouping";
|
||||
import type { Task } from "./types";
|
||||
|
||||
// 2026-04-17 12:00 local time — "today" in the fixtures below.
|
||||
const NOW = new Date(2026, 3, 17, 12, 0, 0);
|
||||
|
||||
function task(partial: Partial<Task> & { id: string }): Task {
|
||||
return {
|
||||
id: partial.id,
|
||||
title: partial.title ?? partial.id,
|
||||
description: "",
|
||||
status: "backlog",
|
||||
date: partial.date ?? null,
|
||||
has_time: partial.has_time ?? false,
|
||||
version: 1,
|
||||
parent_id: null,
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
describe("groupTasksByDate", () => {
|
||||
it("returns an empty array when there are no pending tasks", () => {
|
||||
expect(groupTasksByDate([], NOW)).toEqual([]);
|
||||
});
|
||||
|
||||
it("puts 'No Date' last — regression: was first, burying urgent tasks", () => {
|
||||
const tasks = [
|
||||
task({ id: "overdue", date: "2026-04-15T00:00:00Z" }),
|
||||
task({ id: "no-date" }),
|
||||
task({ id: "today", date: "2026-04-17T09:00:00Z" }),
|
||||
];
|
||||
const labels = groupTasksByDate(tasks, NOW).map((g) => g.label);
|
||||
expect(labels).toEqual(["Overdue", "Today", "No Date"]);
|
||||
});
|
||||
|
||||
it("orders dated buckets: Overdue, Today, Tomorrow, future…, then No Date", () => {
|
||||
const tasks = [
|
||||
task({ id: "nd1" }),
|
||||
task({ id: "future", date: "2026-04-20T00:00:00Z" }),
|
||||
task({ id: "tomorrow", date: "2026-04-18T00:00:00Z" }),
|
||||
task({ id: "today", date: "2026-04-17T09:00:00Z" }),
|
||||
task({ id: "overdue", date: "2026-04-10T00:00:00Z" }),
|
||||
];
|
||||
const labels = groupTasksByDate(tasks, NOW).map((g) => g.label);
|
||||
expect(labels[0]).toBe("Overdue");
|
||||
expect(labels[1]).toBe("Today");
|
||||
expect(labels[2]).toBe("Tomorrow");
|
||||
// One future day label between tomorrow and No Date
|
||||
expect(labels[labels.length - 1]).toBe("No Date");
|
||||
expect(labels).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("drops empty buckets", () => {
|
||||
const tasks = [task({ id: "t1", date: "2026-04-17T08:00:00Z" })];
|
||||
expect(groupTasksByDate(tasks, NOW).map((g) => g.label)).toEqual(["Today"]);
|
||||
});
|
||||
|
||||
it("sorts tasks within a bucket by due time ascending, stable on ties", () => {
|
||||
const tasks = [
|
||||
task({ id: "b", date: "2026-04-17T15:00:00Z", has_time: true }),
|
||||
task({ id: "a", date: "2026-04-17T09:00:00Z", has_time: true }),
|
||||
task({ id: "c", date: "2026-04-17T15:00:00Z", has_time: true }),
|
||||
];
|
||||
const today = groupTasksByDate(tasks, NOW).find((g) => g.label === "Today")!;
|
||||
expect(today.tasks.map((t) => t.id)).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("places a task with today's date but time before 'now' in the Today bucket (not Overdue)", () => {
|
||||
const tasks = [task({ id: "earlier-today", date: "2026-04-17T08:00:00Z" })];
|
||||
const groups = groupTasksByDate(tasks, NOW);
|
||||
expect(groups.map((g) => g.label)).toEqual(["Today"]);
|
||||
});
|
||||
|
||||
it("preserves No Date order as given by the caller", () => {
|
||||
const tasks = [
|
||||
task({ id: "z" }),
|
||||
task({ id: "a" }),
|
||||
task({ id: "m" }),
|
||||
];
|
||||
const nd = groupTasksByDate(tasks, NOW).find((g) => g.label === "No Date")!;
|
||||
expect(nd.tasks.map((t) => t.id)).toEqual(["z", "a", "m"]);
|
||||
});
|
||||
|
||||
it("groups multiple tasks on the same future day under one label", () => {
|
||||
const tasks = [
|
||||
task({ id: "f1", date: "2026-04-25T09:00:00Z", has_time: true }),
|
||||
task({ id: "f2", date: "2026-04-25T14:00:00Z", has_time: true }),
|
||||
];
|
||||
const groups = groupTasksByDate(tasks, NOW);
|
||||
const future = groups.find((g) => g.date?.getDate() === 25);
|
||||
expect(future).toBeDefined();
|
||||
expect(future!.tasks.map((t) => t.id)).toEqual(["f1", "f2"]);
|
||||
// And it comes before No Date (which is absent here).
|
||||
expect(groups).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
70
apps/tauri/src/lib/grouping.ts
Normal file
70
apps/tauri/src/lib/grouping.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import type { Task } from "./types";
|
||||
|
||||
export type TaskGroup = { label: string; tasks: Task[]; date: Date | null };
|
||||
|
||||
/**
|
||||
* Group pending tasks into date buckets for the "group by date" view.
|
||||
*
|
||||
* Order:
|
||||
* Overdue → Today → Tomorrow → future days (chronological) → No Date
|
||||
*
|
||||
* Within each dated bucket tasks sort by due date+time ascending, with the
|
||||
* original `pendingTasks` index as a stable tiebreaker. "No Date" preserves
|
||||
* the caller-supplied order.
|
||||
*/
|
||||
export function groupTasksByDate(pendingTasks: Task[], now: Date = new Date()): TaskGroup[] {
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrowStart = new Date(todayStart);
|
||||
tomorrowStart.setDate(todayStart.getDate() + 1);
|
||||
|
||||
const overdue: Task[] = [];
|
||||
const today: Task[] = [];
|
||||
const tomorrow: Task[] = [];
|
||||
const futureByDay = new Map<string, { date: Date; tasks: Task[] }>();
|
||||
const noDate: Task[] = [];
|
||||
|
||||
for (const task of pendingTasks) {
|
||||
if (!task.date) {
|
||||
noDate.push(task);
|
||||
} else {
|
||||
const d = new Date(task.date);
|
||||
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
if (dayStart < todayStart) overdue.push(task);
|
||||
else if (dayStart.getTime() === todayStart.getTime()) today.push(task);
|
||||
else if (dayStart.getTime() === tomorrowStart.getTime()) tomorrow.push(task);
|
||||
else {
|
||||
const key = dayStart.toISOString();
|
||||
if (!futureByDay.has(key)) futureByDay.set(key, { date: dayStart, tasks: [] });
|
||||
futureByDay.get(key)!.tasks.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const taskOrderIndex = new Map(pendingTasks.map((t, i) => [t.id, i]));
|
||||
const sortByDue = (a: Task, b: Task) => {
|
||||
const dateDiff = new Date(a.date!).getTime() - new Date(b.date!).getTime();
|
||||
if (dateDiff !== 0) return dateDiff;
|
||||
return (taskOrderIndex.get(a.id) ?? 0) - (taskOrderIndex.get(b.id) ?? 0);
|
||||
};
|
||||
overdue.sort(sortByDue);
|
||||
today.sort(sortByDue);
|
||||
tomorrow.sort(sortByDue);
|
||||
|
||||
const groups: TaskGroup[] = [];
|
||||
if (overdue.length) groups.push({ label: "Overdue", tasks: overdue, date: null });
|
||||
if (today.length) groups.push({ label: "Today", tasks: today, date: todayStart });
|
||||
if (tomorrow.length) groups.push({ label: "Tomorrow", tasks: tomorrow, date: tomorrowStart });
|
||||
|
||||
const currentYear = now.getFullYear();
|
||||
for (const [, { date, tasks }] of [...futureByDay.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
||||
tasks.sort(sortByDue);
|
||||
const opts: Intl.DateTimeFormatOptions = date.getFullYear() !== currentYear
|
||||
? { weekday: "short", month: "short", day: "numeric", year: "numeric" }
|
||||
: { weekday: "short", month: "short", day: "numeric" };
|
||||
groups.push({ label: date.toLocaleDateString(undefined, opts), tasks, date });
|
||||
}
|
||||
|
||||
if (noDate.length) groups.push({ label: "No Date", tasks: noDate, date: null });
|
||||
|
||||
return groups;
|
||||
}
|
||||
38
apps/tauri/src/lib/paths.test.ts
Normal file
38
apps/tauri/src/lib/paths.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { workspaceNameFromPath } from "./paths";
|
||||
|
||||
describe("workspaceNameFromPath", () => {
|
||||
it("returns the last path component of a POSIX path", () => {
|
||||
expect(workspaceNameFromPath("/home/me/Tasks")).toBe("Tasks");
|
||||
});
|
||||
|
||||
it("strips a trailing slash (regression: used to fall back to 'workspace')", () => {
|
||||
expect(workspaceNameFromPath("/home/me/Tasks/")).toBe("Tasks");
|
||||
});
|
||||
|
||||
it("strips multiple trailing slashes", () => {
|
||||
expect(workspaceNameFromPath("/home/me/Tasks///")).toBe("Tasks");
|
||||
});
|
||||
|
||||
it("handles Windows-style backslash paths", () => {
|
||||
expect(workspaceNameFromPath("C:\\Users\\me\\Tasks")).toBe("Tasks");
|
||||
});
|
||||
|
||||
it("strips a trailing backslash on Windows paths", () => {
|
||||
expect(workspaceNameFromPath("C:\\Users\\me\\Tasks\\")).toBe("Tasks");
|
||||
});
|
||||
|
||||
it("handles mixed separators", () => {
|
||||
expect(workspaceNameFromPath("C:\\Users/me\\Tasks")).toBe("Tasks");
|
||||
});
|
||||
|
||||
it("falls back to 'workspace' when the path has no usable tail", () => {
|
||||
expect(workspaceNameFromPath("/")).toBe("workspace");
|
||||
expect(workspaceNameFromPath("\\")).toBe("workspace");
|
||||
expect(workspaceNameFromPath("")).toBe("workspace");
|
||||
});
|
||||
|
||||
it("preserves names with spaces", () => {
|
||||
expect(workspaceNameFromPath("/home/me/My Tasks/")).toBe("My Tasks");
|
||||
});
|
||||
});
|
||||
9
apps/tauri/src/lib/paths.ts
Normal file
9
apps/tauri/src/lib/paths.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Derive a workspace display name from a folder path picked via the file
|
||||
* dialog. Handles both `/` and `\` separators and tolerates trailing
|
||||
* separators (e.g. `"/home/me/Tasks/"` → `"Tasks"`, not `"workspace"`).
|
||||
*/
|
||||
export function workspaceNameFromPath(folder: string): string {
|
||||
const parts = folder.replace(/[\\/]+$/, "").split(/[\\/]/);
|
||||
return parts[parts.length - 1] || "workspace";
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
import { app } from "../stores/app.svelte";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { workspaceNameFromPath } from "../paths";
|
||||
|
||||
let { cancellable = false }: { cancellable?: boolean } = $props();
|
||||
|
||||
|
|
@ -71,11 +72,7 @@
|
|||
const selected = await open({ directory: true, multiple: false });
|
||||
if (!selected) return;
|
||||
const folder = selected as string;
|
||||
// Strip trailing separators before splitting so a path like "/home/me/Tasks/"
|
||||
// yields "Tasks" instead of an empty tail that falls through to "workspace".
|
||||
const parts = folder.replace(/[\\/]+$/, "").split(/[\\/]/);
|
||||
const wsName = parts[parts.length - 1] || "workspace";
|
||||
await app.addWorkspace(wsName, folder);
|
||||
await app.addWorkspace(workspaceNameFromPath(folder), folder);
|
||||
}
|
||||
|
||||
// ── WebDAV handlers ───────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
Screen,
|
||||
SyncResult,
|
||||
} from "../types";
|
||||
import { groupTasksByDate, type TaskGroup } from "../grouping";
|
||||
|
||||
// Listen for file system changes from the backend watcher.
|
||||
listen("fs-changed", () => {
|
||||
|
|
@ -52,65 +53,9 @@ let activeList = $derived(lists.find((l) => l.id === activeListId) ?? null);
|
|||
let pendingTasks = $derived(tasks.filter((t) => t.status === "backlog" && !t.parent_id));
|
||||
let completedTasks = $derived(tasks.filter((t) => t.status === "completed" && !t.parent_id));
|
||||
|
||||
type TaskGroup = { label: string; tasks: Task[]; date: Date | null };
|
||||
|
||||
let groupedPendingTasks = $derived.by((): TaskGroup[] | null => {
|
||||
if (!activeList?.group_by_date) return null;
|
||||
const now = new Date();
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrowStart = new Date(todayStart);
|
||||
tomorrowStart.setDate(todayStart.getDate() + 1);
|
||||
|
||||
const overdue: Task[] = [];
|
||||
const today: Task[] = [];
|
||||
const tomorrow: Task[] = [];
|
||||
const futureByDay = new Map<string, { date: Date; tasks: Task[] }>();
|
||||
const noDate: Task[] = [];
|
||||
|
||||
for (const task of pendingTasks) {
|
||||
if (!task.date) {
|
||||
noDate.push(task);
|
||||
} else {
|
||||
const d = new Date(task.date);
|
||||
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
||||
if (dayStart < todayStart) overdue.push(task);
|
||||
else if (dayStart.getTime() === todayStart.getTime()) today.push(task);
|
||||
else if (dayStart.getTime() === tomorrowStart.getTime()) tomorrow.push(task);
|
||||
else {
|
||||
const key = dayStart.toISOString();
|
||||
if (!futureByDay.has(key)) futureByDay.set(key, { date: dayStart, tasks: [] });
|
||||
futureByDay.get(key)!.tasks.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const taskOrderIndex = new Map(pendingTasks.map((t, i) => [t.id, i]));
|
||||
const sortByDue = (a: Task, b: Task) => {
|
||||
const dateDiff = new Date(a.date!).getTime() - new Date(b.date!).getTime();
|
||||
if (dateDiff !== 0) return dateDiff;
|
||||
return (taskOrderIndex.get(a.id) ?? 0) - (taskOrderIndex.get(b.id) ?? 0);
|
||||
};
|
||||
overdue.sort(sortByDue);
|
||||
today.sort(sortByDue);
|
||||
tomorrow.sort(sortByDue);
|
||||
|
||||
const groups: TaskGroup[] = [];
|
||||
if (overdue.length) groups.push({ label: "Overdue", tasks: overdue, date: null });
|
||||
if (today.length) groups.push({ label: "Today", tasks: today, date: todayStart });
|
||||
if (tomorrow.length) groups.push({ label: "Tomorrow", tasks: tomorrow, date: tomorrowStart });
|
||||
|
||||
const currentYear = now.getFullYear();
|
||||
for (const [, { date, tasks }] of [...futureByDay.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
||||
tasks.sort(sortByDue);
|
||||
const opts: Intl.DateTimeFormatOptions = date.getFullYear() !== currentYear
|
||||
? { weekday: "short", month: "short", day: "numeric", year: "numeric" }
|
||||
: { weekday: "short", month: "short", day: "numeric" };
|
||||
groups.push({ label: date.toLocaleDateString(undefined, opts), tasks, date });
|
||||
}
|
||||
|
||||
if (noDate.length) groups.push({ label: "No Date", tasks: noDate, date: null });
|
||||
|
||||
return groups;
|
||||
return groupTasksByDate(pendingTasks);
|
||||
});
|
||||
|
||||
// Build a map of parent_id -> children for subtask hierarchy
|
||||
|
|
|
|||
1
apps/tauri/src/test/setup.ts
Normal file
1
apps/tauri/src/test/setup.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
import "@testing-library/jest-dom/vitest";
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
/// <reference types="vitest/config" />
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
|
@ -14,4 +15,17 @@ export default defineConfig({
|
|||
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
|
||||
watch: { ignored: ["**/src-tauri/**"] },
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
globals: true,
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
include: ["src/**/*.{test,spec}.{ts,svelte}"],
|
||||
// Resolve Svelte's client (browser) entry under Vitest — without the
|
||||
// browser condition mount() picks up Svelte's SSR export and throws
|
||||
// lifecycle_function_unavailable.
|
||||
server: { deps: { inline: ["@testing-library/svelte"] } },
|
||||
},
|
||||
resolve: {
|
||||
conditions: process.env.VITEST ? ["browser"] : [],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue