feat(journal): add per-user mood journal module (task 86)
Ship journal entries with mood tracking, insights, and Recharts charts. Adds v1 API endpoints per ADR 0005 and migration 0020_journal_entries.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("journal happy path", async ({ page }) => {
|
||||
const suffix = Date.now().toString();
|
||||
const title = `E2E Journal ${suffix}`;
|
||||
|
||||
await page.goto("/journal");
|
||||
await expect(page.getByRole("heading", { name: "Journal" })).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "New entry" }).click();
|
||||
await expect(page.getByRole("heading", { name: "New entry" })).toBeVisible();
|
||||
await page.getByLabel("Title (optional)").fill(title);
|
||||
await page.getByRole("button", { name: "Happy" }).click();
|
||||
await page.getByRole("button", { name: "Save entry" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/journal\/[0-9a-f-]+$/);
|
||||
await expect(page.getByRole("heading", { name: "Edit entry" })).toBeVisible();
|
||||
|
||||
await page.goto("/journal");
|
||||
await expect(page.getByRole("link", { name: new RegExp(title) })).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "Mood tracker" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Mood tracker" })).toBeVisible();
|
||||
await expect(page.getByText("Mood over time")).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
computeDayStreak,
|
||||
entryMoodScore,
|
||||
pillsCorrelation,
|
||||
topMoodId,
|
||||
} from "../../src/modules/journal/server/analytics";
|
||||
import { toDayKey } from "../../src/modules/journal/day-key";
|
||||
|
||||
describe("journal analytics", () => {
|
||||
it("scores multi-select moods as an average", () => {
|
||||
const score = entryMoodScore({ moods: ["happy", "stressed"] });
|
||||
assert.equal(score, 3);
|
||||
});
|
||||
|
||||
it("computes consecutive day streak", () => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const streak = computeDayStreak([toDayKey(today), toDayKey(yesterday)]);
|
||||
assert.equal(streak, 2);
|
||||
});
|
||||
|
||||
it("finds the most common mood id", () => {
|
||||
const top = topMoodId([
|
||||
{ moods: ["happy"], recordedAt: "", stress: null, pillsTaken: null, id: "1" },
|
||||
{ moods: ["happy", "calm"], recordedAt: "", stress: null, pillsTaken: null, id: "2" },
|
||||
{ moods: ["sad"], recordedAt: "", stress: null, pillsTaken: null, id: "3" },
|
||||
]);
|
||||
assert.equal(top, "happy");
|
||||
});
|
||||
|
||||
it("compares mood averages with and without pills", () => {
|
||||
const result = pillsCorrelation([
|
||||
{
|
||||
id: "1",
|
||||
recordedAt: "2026-01-01T10:00:00.000Z",
|
||||
moods: ["happy"],
|
||||
stress: null,
|
||||
pillsTaken: true,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
recordedAt: "2026-01-02T10:00:00.000Z",
|
||||
moods: ["sad"],
|
||||
stress: null,
|
||||
pillsTaken: false,
|
||||
},
|
||||
]);
|
||||
assert.equal(result.withPillsCount, 1);
|
||||
assert.equal(result.withoutPillsCount, 1);
|
||||
assert.equal(result.avgMoodWithPills, 5);
|
||||
assert.equal(result.avgMoodWithoutPills, 1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user