Files
HealthLog/e2e/achievements.spec.ts
T
Marc-André BombeckandClaude Opus 4.7 34c967c14a feat(achievements): dedicated /achievements page with locked/unlocked list
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>
2026-05-09 21:17:04 +02:00

75 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 });
});
});