mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Pre-v1.4.15 the page silently dropped locked achievements, so the user
only saw what they had already earned. Now every badge renders — locked
ones are grayed out with a "Locked" badge, criterion hint
({current} / {target}), and a progress bar. Unlocked entries keep the
existing primary-gradient card with the "Completed on …" footer.
Achievements are grouped by a new `category` axis (medication, vitals,
security, engagement) — derived in `getAchievementCategory()` so a
metric is always in exactly one bucket. Inside a category, unlocked
items sort first; among locked items the closest-to-unlock one ranks
first so the user always sees their immediate next goal at the top.
The sidebar link to /achievements was already in place under the
user-area group; this commit only adds the new locked/grouped render
plus an e2e smoke spec that walks the desktop sidebar → click →
heading visible path.
i18n: 9 new keys under `achievements.*` (locked, criterionHint,
progressPercent, categories.{medication,vitals,security,engagement},
dashboardCard.{title,viewAll,empty}) — EN + DE. C4's parity audit will
sweep later.
Tests: +9 unit (groupByCategory ordering + page render in EN/DE) +
1 e2e (sidebar → /achievements). 909 / 909 unit pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
|
||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||
|
||
/**
|
||
* v1.4.15 phase-B4 — /achievements navigation smoke.
|
||
*
|
||
* The page itself has unit-test coverage for the locked-vs-unlocked
|
||
* card render; this spec answers the higher-level "can the user reach
|
||
* the page from the global nav?" question. We mock
|
||
* `/api/gamification/achievements` so the assertion does not depend on
|
||
* the seed user actually having any achievement data — the routing +
|
||
* sidebar wiring is what matters here, not the data freshness.
|
||
*/
|
||
test.describe("achievements page is reachable from the sidebar", () => {
|
||
test.use({ storageState: STORAGE_STATE_PATH });
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.route("**/api/gamification/achievements", (route) =>
|
||
route.fulfill({
|
||
status: 200,
|
||
contentType: "application/json",
|
||
body: JSON.stringify({
|
||
data: {
|
||
summary: {
|
||
unlockedCount: 0,
|
||
totalCount: 38,
|
||
earnedPoints: 0,
|
||
totalPoints: 9999,
|
||
completionPercent: 0,
|
||
nextAchievement: null,
|
||
},
|
||
achievements: [
|
||
{
|
||
id: "intake-total-1",
|
||
metric: "totalTakenIntakes",
|
||
category: "medication",
|
||
titleKey: "achievements.badges.intakeTotal1.title",
|
||
descriptionKey: "achievements.badges.intakeTotal1.description",
|
||
icon: "Pill",
|
||
format: "count",
|
||
target: 1,
|
||
current: 0,
|
||
points: 8,
|
||
unlocked: false,
|
||
progressPercent: 0,
|
||
completedAt: null,
|
||
},
|
||
],
|
||
metrics: {},
|
||
},
|
||
error: null,
|
||
}),
|
||
}),
|
||
);
|
||
});
|
||
|
||
test("desktop sidebar exposes the achievements link", async ({ page }) => {
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
// The sidebar nav is hidden on small viewports (`md:flex`); the
|
||
// chromium-desktop project uses 1280×720 so it's visible here. Match
|
||
// by aria-label so we don't accidentally pick up a tooltip.
|
||
const sidebar = page.locator('aside[aria-label="Sidebar"]');
|
||
await expect(sidebar).toBeVisible();
|
||
await sidebar.getByRole("link", { name: "Achievements" }).click();
|
||
await expect(page).toHaveURL(/\/achievements\b/);
|
||
// Page title from the loaded route. The unit test guards the
|
||
// grouped + locked-card render shape; here we only assert the route
|
||
// mounted and rendered the heading.
|
||
await expect(
|
||
page.getByRole("heading", { name: "Achievements" }),
|
||
).toBeVisible({ timeout: 5000 });
|
||
});
|
||
});
|