test: add deterministic task behaviour tests
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
/.test-build
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
|||||||
@@ -5,10 +5,17 @@ import nextTs from "eslint-config-next/typescript";
|
|||||||
const eslintConfig = defineConfig([
|
const eslintConfig = defineConfig([
|
||||||
...nextVitals,
|
...nextVitals,
|
||||||
...nextTs,
|
...nextTs,
|
||||||
|
{
|
||||||
|
files: ["tests/**/*.cjs"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-require-imports": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
// Override default ignores of eslint-config-next.
|
// Override default ignores of eslint-config-next.
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
// Default ignores of eslint-config-next:
|
// Default ignores of eslint-config-next:
|
||||||
".next/**",
|
".next/**",
|
||||||
|
".test-build/**",
|
||||||
"out/**",
|
"out/**",
|
||||||
"build/**",
|
"build/**",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|||||||
16
lib/db.ts
16
lib/db.ts
@@ -21,6 +21,12 @@ export function getDatabase() {
|
|||||||
return globalDatabase.plannerDatabase;
|
return globalDatabase.plannerDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (globalDatabase.plannerDatabase) {
|
||||||
|
globalDatabase.plannerDatabase.close();
|
||||||
|
delete globalDatabase.plannerDatabase;
|
||||||
|
delete globalDatabase.plannerDatabasePath;
|
||||||
|
}
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
|
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
|
||||||
|
|
||||||
const database = new DatabaseSync(databasePath);
|
const database = new DatabaseSync(databasePath);
|
||||||
@@ -34,3 +40,13 @@ export function getDatabase() {
|
|||||||
|
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function closeDatabaseForTests() {
|
||||||
|
const globalDatabase = globalThis as DatabaseGlobal;
|
||||||
|
|
||||||
|
if (globalDatabase.plannerDatabase) {
|
||||||
|
globalDatabase.plannerDatabase.close();
|
||||||
|
delete globalDatabase.plannerDatabase;
|
||||||
|
delete globalDatabase.plannerDatabasePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"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": {
|
"dependencies": {
|
||||||
"next": "16.3.0",
|
"next": "16.3.0",
|
||||||
|
|||||||
159
tests/tasks.test.cjs
Normal file
159
tests/tasks.test.cjs
Normal file
@@ -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]
|
||||||
|
);
|
||||||
|
});
|
||||||
19
tsconfig.test.json
Normal file
19
tsconfig.test.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user