From 67404c2de2dd1881238fbb7636f0527beb00a15b Mon Sep 17 00:00:00 2001 From: Mpho10111 Date: Tue, 11 Aug 2026 21:15:21 +0200 Subject: [PATCH] test: add deterministic task behaviour tests --- .gitignore | 1 + eslint.config.mjs | 7 ++ lib/db.ts | 16 +++++ package.json | 3 +- tests/tasks.test.cjs | 159 +++++++++++++++++++++++++++++++++++++++++++ tsconfig.test.json | 19 ++++++ 6 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 tests/tasks.test.cjs create mode 100644 tsconfig.test.json diff --git a/.gitignore b/.gitignore index 264faa3..e6e4bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # testing /coverage +/.test-build # next.js /.next/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..953187e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,10 +5,17 @@ import nextTs from "eslint-config-next/typescript"; const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, + { + files: ["tests/**/*.cjs"], + rules: { + "@typescript-eslint/no-require-imports": "off", + }, + }, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: ".next/**", + ".test-build/**", "out/**", "build/**", "next-env.d.ts", diff --git a/lib/db.ts b/lib/db.ts index 667c314..514b29a 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -21,6 +21,12 @@ export function getDatabase() { return globalDatabase.plannerDatabase; } + if (globalDatabase.plannerDatabase) { + globalDatabase.plannerDatabase.close(); + delete globalDatabase.plannerDatabase; + delete globalDatabase.plannerDatabasePath; + } + fs.mkdirSync(path.dirname(databasePath), { recursive: true }); const database = new DatabaseSync(databasePath); @@ -34,3 +40,13 @@ export function getDatabase() { return database; } + +export function closeDatabaseForTests() { + const globalDatabase = globalThis as DatabaseGlobal; + + if (globalDatabase.plannerDatabase) { + globalDatabase.plannerDatabase.close(); + delete globalDatabase.plannerDatabase; + delete globalDatabase.plannerDatabasePath; + } +} diff --git a/package.json b/package.json index b1e3670..565e644 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "tsc -p tsconfig.test.json && node --test --test-concurrency=1 --test-isolation=none tests/tasks.test.cjs" }, "dependencies": { "next": "16.3.0", diff --git a/tests/tasks.test.cjs b/tests/tasks.test.cjs new file mode 100644 index 0000000..cf23278 --- /dev/null +++ b/tests/tasks.test.cjs @@ -0,0 +1,159 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); + +const { + archiveTask, + createTask, + getTask, + listTasks, + updateTask, +} = require("../.test-build/lib/tasks.js"); +const { closeDatabaseForTests } = require("../.test-build/lib/db.js"); + +function useThrowawayDatabase(t) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "planner-tests-")); + const databasePath = path.join(directory, "tasks.sqlite"); + + process.env.TASKS_DB_PATH = databasePath; + + t.after(() => { + closeDatabaseForTests(); + delete process.env.TASKS_DB_PATH; + fs.rmSync(directory, { recursive: true, force: true }); + }); + + return databasePath; +} + +test("creating a task persists all four required fields", (t) => { + useThrowawayDatabase(t); + + const created = createTask({ + title: "Finish Lab 1", + description: "Wire the planner to SQLite", + dueDate: "2026-08-20", + topic: "Academic", + }); + + const reloaded = getTask(created.id); + + assert.equal(reloaded.title, "Finish Lab 1"); + assert.equal(reloaded.description, "Wire the planner to SQLite"); + assert.equal(reloaded.dueDate, "2026-08-20"); + assert.equal(reloaded.topic, "Academic"); + assert.equal(reloaded.status, "Todo"); + assert.equal(reloaded.archivedAt, null); +}); + +test("editing a task persists the changed fields and status", (t) => { + useThrowawayDatabase(t); + + const created = createTask({ + title: "Draft notes", + description: "Initial wording", + dueDate: "2026-08-21", + topic: "Academic", + }); + + const edited = updateTask(created.id, { + title: "Submit notes", + description: "Final wording", + dueDate: "2026-08-22", + topic: "Work", + status: "In-Progress", + }); + const reloaded = getTask(created.id); + + assert.equal(edited.title, "Submit notes"); + assert.equal(reloaded.description, "Final wording"); + assert.equal(reloaded.dueDate, "2026-08-22"); + assert.equal(reloaded.topic, "Work"); + assert.equal(reloaded.status, "In-Progress"); +}); + +test("archiving marks a task archived without deleting it", (t) => { + useThrowawayDatabase(t); + + const created = createTask({ + title: "Old task", + description: "Keep for records", + dueDate: "2026-08-23", + topic: "Personal", + }); + + const archived = archiveTask(created.id); + const activeTasks = listTasks(); + const allTasks = listTasks({ includeArchived: true }); + + assert.equal(typeof archived.archivedAt, "string"); + assert.equal(getTask(created.id).id, created.id); + assert.equal(activeTasks.some((task) => task.id === created.id), false); + assert.equal(allTasks.some((task) => task.id === created.id), true); +}); + +test("overdue is derived from due date and is not stored as a status", (t) => { + useThrowawayDatabase(t); + + const overdue = createTask({ + title: "Past task", + description: "This date has passed", + dueDate: "2000-01-01", + topic: "Work", + }); + const future = createTask({ + title: "Future task", + description: "This date has not passed", + dueDate: "2999-01-01", + topic: "Work", + }); + const completed = updateTask(overdue.id, { status: "Complete" }); + + assert.equal(overdue.status, "Todo"); + assert.equal(overdue.isOverdue, true); + assert.equal(future.isOverdue, false); + assert.equal(completed.status, "Complete"); + assert.equal(completed.isOverdue, false); + assert.notEqual(completed.status, "Overdue"); +}); + +test("sorting by topic, status, and due date is deterministic", (t) => { + useThrowawayDatabase(t); + + const work = createTask({ + title: "Work item", + description: "Second topic alphabetically", + dueDate: "2026-09-03", + topic: "Work", + }); + const academic = createTask({ + title: "Academic item", + description: "First topic alphabetically", + dueDate: "2026-09-02", + topic: "Academic", + }); + const personal = createTask({ + title: "Personal item", + description: "Earliest due date", + dueDate: "2026-09-01", + topic: "Personal", + }); + + updateTask(work.id, { status: "Complete" }); + updateTask(academic.id, { status: "In-Progress" }); + + assert.deepEqual( + listTasks({ sort: "topic" }).map((task) => task.topic), + ["Academic", "Personal", "Work"] + ); + assert.deepEqual( + listTasks({ sort: "status" }).map((task) => task.status), + ["Todo", "In-Progress", "Complete"] + ); + assert.deepEqual( + listTasks({ sort: "dueDate" }).map((task) => task.id), + [personal.id, academic.id, work.id] + ); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..73769e4 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": false, + "baseUrl": ".", + "incremental": false, + "module": "CommonJS", + "moduleResolution": "node", + "noEmit": false, + "outDir": ".test-build", + "paths": { + "@/*": ["./*"] + }, + "target": "ES2022", + "tsBuildInfoFile": ".test-build/tsconfig.test.tsbuildinfo" + }, + "include": ["lib/**/*.ts", "types/**/*.ts"], + "exclude": ["node_modules", ".next", ".test-build"] +}