mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test(dashboard-hero): cover hero-ring-order default + reorder; i18n hint
Add resolveHeroRingOrder unit coverage (default health-score-first, reconcile against selected set, drop/append, dedupe) plus resolver/serializer round-trip; hero SSR tests for the default lead + a persisted custom order + dropped-when-unrendered; Settings picker tests for the pinned health-score anchor row, its locked-on switch, drag/arrow reorder, and a persisted non-leading order. Extend the score-rings picker description across six locales.
This commit is contained in:
1 parent
abe688caba
commit
abb2db37ee
13 files changed
+317
-26
No files matched your search
@@ -386,10 +386,13 @@ describe("<DashboardHero> — ring row (v1.27.7)", () => {
|
||||
);
|
||||
const row = html.match(/data-slot="dashboard-hero-rings"/g) ?? [];
|
||||
expect(row).toHaveLength(1);
|
||||
// Selection order preserved, health ring trailing (3 rings total).
|
||||
// v1.27.27 — default order leads with the health-score ring, then the
|
||||
// selected rings in selection order (3 rings total).
|
||||
const healthIdx = html.indexOf('href="/insights"');
|
||||
const readinessIdx = html.indexOf('data-ring="READINESS"');
|
||||
const complianceIdx = html.indexOf('data-ring="MED_COMPLIANCE"');
|
||||
expect(readinessIdx).toBeGreaterThan(-1);
|
||||
expect(healthIdx).toBeGreaterThan(-1);
|
||||
expect(readinessIdx).toBeGreaterThan(healthIdx);
|
||||
expect(complianceIdx).toBeGreaterThan(readinessIdx);
|
||||
const rings = html.match(/data-slot="score-ring"/g) ?? [];
|
||||
expect(rings).toHaveLength(3);
|
||||
@@ -562,6 +565,90 @@ describe("<DashboardHero> — ring links (v1.27.24)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DashboardHero> — ring order (v1.27.27)", () => {
|
||||
const threeRings = {
|
||||
healthScore: { score: 82, band: "green" as const, delta: 2 },
|
||||
scoreRings: [
|
||||
{ id: "READINESS" as const, score: 71, band: "green" as const },
|
||||
{ id: "SLEEP_SCORE" as const, score: 66, band: "yellow" as const },
|
||||
{
|
||||
id: "MED_COMPLIANCE" as const,
|
||||
score: 33,
|
||||
band: "yellow" as const,
|
||||
doses: { taken: 1, scheduled: 3 },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("default (no persisted order) leads with the health-score ring", () => {
|
||||
const html = render(baseSnapshot(threeRings));
|
||||
// The health-score slide (its /insights link) precedes every score ring.
|
||||
const healthIdx = html.indexOf('href="/insights"');
|
||||
const readinessIdx = html.indexOf('data-ring="READINESS"');
|
||||
const sleepIdx = html.indexOf('data-ring="SLEEP_SCORE"');
|
||||
const medIdx = html.indexOf('data-ring="MED_COMPLIANCE"');
|
||||
expect(healthIdx).toBeGreaterThan(-1);
|
||||
expect(readinessIdx).toBeGreaterThan(healthIdx);
|
||||
expect(sleepIdx).toBeGreaterThan(readinessIdx);
|
||||
expect(medIdx).toBeGreaterThan(sleepIdx);
|
||||
// First carousel dot is current (slide 0 = health score).
|
||||
const firstDot = html.match(
|
||||
/<button[^>]*data-slot="dashboard-hero-ring-dot"[^>]*>/,
|
||||
);
|
||||
expect(firstDot![0]).toContain('aria-current="true"');
|
||||
});
|
||||
|
||||
it("honours a persisted custom order (health score moved off the leading edge)", () => {
|
||||
const html = render(
|
||||
baseSnapshot({
|
||||
...threeRings,
|
||||
layout: {
|
||||
version: 1,
|
||||
widgets: [],
|
||||
heroRingOrder: ["READINESS", "MED_COMPLIANCE", "HEALTH_SCORE"],
|
||||
selectedScoreRings: ["READINESS", "SLEEP_SCORE", "MED_COMPLIANCE"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const readinessIdx = html.indexOf('data-ring="READINESS"');
|
||||
const medIdx = html.indexOf('data-ring="MED_COMPLIANCE"');
|
||||
const healthIdx = html.indexOf('href="/insights"');
|
||||
// READINESS first, MED_COMPLIANCE second, health score last.
|
||||
expect(readinessIdx).toBeGreaterThan(-1);
|
||||
expect(medIdx).toBeGreaterThan(readinessIdx);
|
||||
expect(healthIdx).toBeGreaterThan(medIdx);
|
||||
// A rendered ring the order omits (SLEEP_SCORE) is still drawn (appended).
|
||||
expect(html).toContain('data-ring="SLEEP_SCORE"');
|
||||
// Every slide still renders exactly once.
|
||||
expect(html.match(/data-carousel-slide=""/g)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("drops an ordered id whose ring isn't rendered (no data / disabled)", () => {
|
||||
// The persisted order names RECOVERY_SCORE, but the snapshot resolved
|
||||
// no such ring — it must not leave a gap or a phantom slide.
|
||||
const html = render(
|
||||
baseSnapshot({
|
||||
healthScore: { score: 60, band: "yellow", delta: 0 },
|
||||
scoreRings: [{ id: "READINESS", score: 71, band: "green" }],
|
||||
layout: {
|
||||
version: 1,
|
||||
widgets: [],
|
||||
heroRingOrder: ["RECOVERY_SCORE", "HEALTH_SCORE", "READINESS"],
|
||||
selectedScoreRings: ["READINESS"],
|
||||
},
|
||||
}),
|
||||
);
|
||||
// Only the two rendered rings appear (health + readiness), in the
|
||||
// surviving order: health score then readiness.
|
||||
expect(html.match(/data-carousel-slide=""/g)).toHaveLength(2);
|
||||
expect(html).not.toContain('data-ring="RECOVERY_SCORE"');
|
||||
const healthIdx = html.indexOf('href="/insights"');
|
||||
const readinessIdx = html.indexOf('data-ring="READINESS"');
|
||||
expect(healthIdx).toBeGreaterThan(-1);
|
||||
expect(readinessIdx).toBeGreaterThan(healthIdx);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<DashboardHero> — greeting", () => {
|
||||
it("personalises from the snapshot username with the server greeting hour", () => {
|
||||
const html = render(
|
||||
|
||||
@@ -362,9 +362,7 @@ export function DashboardHero({
|
||||
);
|
||||
const ringSlides: HeroRingSlide[] = heroRingOrder
|
||||
.map((id) =>
|
||||
id === HEALTH_SCORE_RING_ID
|
||||
? healthScoreSlide
|
||||
: scoreRingSlides.get(id),
|
||||
id === HEALTH_SCORE_RING_ID ? healthScoreSlide : scoreRingSlides.get(id),
|
||||
)
|
||||
.filter((slide): slide is HeroRingSlide => slide !== undefined);
|
||||
|
||||
|
||||
@@ -455,8 +455,10 @@ describe("<DashboardLayoutSection> — hero score-ring picker (v1.27.7)", () =>
|
||||
queryState.layout = { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: true };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
expect(html).toContain('data-slot="score-rings-picker"');
|
||||
// v1.27.27 — one switch per score ring (4) plus the always-present
|
||||
// health-score anchor row = 5.
|
||||
const switches = html.match(/data-slot="score-ring-switch"/g) ?? [];
|
||||
expect(switches).toHaveLength(4);
|
||||
expect(switches).toHaveLength(5);
|
||||
// The default selection (MED_COMPLIANCE) reads checked.
|
||||
const compliance = html.match(
|
||||
/<button[^>]*data-slot="score-ring-switch"[^>]*data-ring="MED_COMPLIANCE"[^>]*>/,
|
||||
@@ -465,23 +467,25 @@ describe("<DashboardLayoutSection> — hero score-ring picker (v1.27.7)", () =>
|
||||
expect(compliance![0]).toContain('data-state="checked"');
|
||||
});
|
||||
|
||||
it("hides rings whose owning module is disabled", () => {
|
||||
it("hides rings whose owning module is disabled (anchor stays)", () => {
|
||||
queryState.layout = { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: true };
|
||||
authState.modules = { recovery: false, sleep: false };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const switches = html.match(/data-slot="score-ring-switch"/g) ?? [];
|
||||
// READINESS + RECOVERY_SCORE (recovery) and SLEEP_SCORE (sleep) are
|
||||
// gone; only the adherence ring remains offered.
|
||||
expect(switches).toHaveLength(1);
|
||||
// gone; the adherence ring plus the health-score anchor remain = 2.
|
||||
expect(switches).toHaveLength(2);
|
||||
expect(html).toContain('data-ring="MED_COMPLIANCE"');
|
||||
expect(html).toContain('data-ring="HEALTH_SCORE"');
|
||||
});
|
||||
|
||||
it("hides the derived rings when the insights module is off", () => {
|
||||
it("hides the derived rings when the insights module is off (anchor stays)", () => {
|
||||
queryState.layout = { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: true };
|
||||
authState.modules = { insights: false };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const switches = html.match(/data-slot="score-ring-switch"/g) ?? [];
|
||||
expect(switches).toHaveLength(1);
|
||||
// Adherence ring + health-score anchor.
|
||||
expect(switches).toHaveLength(2);
|
||||
expect(html).toContain('data-ring="MED_COMPLIANCE"');
|
||||
});
|
||||
|
||||
@@ -507,3 +511,89 @@ describe("<DashboardLayoutSection> — hero score-ring picker (v1.27.7)", () =>
|
||||
expect(readiness![0]).not.toContain('disabled=""');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.27.27 — the health-score ring is a first-class, always-present,
|
||||
* reorderable member of the hero ring row. The picker renders it as a
|
||||
* pinned row (no on/off toggle — a disabled, checked switch) that leads by
|
||||
* default and reorders with the same drag-handle + arrow-button pair as the
|
||||
* score rings and the widget list.
|
||||
*/
|
||||
describe("<DashboardLayoutSection> — health-score anchor + reorder (v1.27.27)", () => {
|
||||
it("renders the health-score anchor as a reorderable row, leading by default", () => {
|
||||
queryState.layout = { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: true };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
// A sortable ring row exists for the health-score anchor.
|
||||
expect(html).toContain('data-slot="score-ring-row"');
|
||||
const anchorRow = html.match(
|
||||
/<div[^>]*data-slot="score-ring-row"[^>]*data-ring="HEALTH_SCORE"[^>]*>/,
|
||||
);
|
||||
expect(anchorRow).not.toBeNull();
|
||||
// Default order leads with the anchor: its row precedes the adherence
|
||||
// ring's row.
|
||||
const anchorIdx = html.indexOf('data-ring="HEALTH_SCORE"');
|
||||
const medIdx = html.indexOf(
|
||||
'data-slot="score-ring-row" data-ring="MED_COMPLIANCE"',
|
||||
);
|
||||
expect(anchorIdx).toBeGreaterThan(-1);
|
||||
// The anchor row appears before any selected score-ring row.
|
||||
expect(anchorIdx).toBeLessThan(html.indexOf('data-ring="MED_COMPLIANCE"'));
|
||||
expect(medIdx === -1 || anchorIdx < medIdx).toBe(true);
|
||||
});
|
||||
|
||||
it("the anchor switch is checked and disabled (locked-on, no toggle-off)", () => {
|
||||
queryState.layout = { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: true };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const anchorSwitch = html.match(
|
||||
/<button[^>]*data-slot="score-ring-switch"[^>]*data-ring="HEALTH_SCORE"[^>]*>/,
|
||||
);
|
||||
expect(anchorSwitch).not.toBeNull();
|
||||
expect(anchorSwitch![0]).toContain('data-state="checked"');
|
||||
expect(anchorSwitch![0]).toContain('disabled=""');
|
||||
});
|
||||
|
||||
it("the anchor carries a drag handle + move arrows (reorderable)", () => {
|
||||
queryState.layout = { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: true };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
// The health-score row is a full sortable row: its drag handle exists.
|
||||
const handles = html.match(/data-slot="score-ring-drag-handle"/g) ?? [];
|
||||
// One handle per hero-order row = anchor + default MED_COMPLIANCE = 2.
|
||||
expect(handles.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("honours a persisted order that moves the anchor off the lead", () => {
|
||||
queryState.layout = {
|
||||
...DEFAULT_DASHBOARD_LAYOUT,
|
||||
heroVisible: true,
|
||||
selectedScoreRings: ["READINESS", "MED_COMPLIANCE"],
|
||||
heroRingOrder: ["READINESS", "HEALTH_SCORE", "MED_COMPLIANCE"],
|
||||
};
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const readinessRow = html.indexOf(
|
||||
'data-slot="score-ring-row" data-ring="READINESS"',
|
||||
);
|
||||
const anchorRow = html.indexOf(
|
||||
'data-slot="score-ring-row" data-ring="HEALTH_SCORE"',
|
||||
);
|
||||
const medRow = html.indexOf(
|
||||
'data-slot="score-ring-row" data-ring="MED_COMPLIANCE"',
|
||||
);
|
||||
expect(readinessRow).toBeGreaterThan(-1);
|
||||
expect(anchorRow).toBeGreaterThan(readinessRow);
|
||||
expect(medRow).toBeGreaterThan(anchorRow);
|
||||
});
|
||||
|
||||
it("the ring mutation PUTs both selectedScoreRings and heroRingOrder (source pin)", () => {
|
||||
const src = readFileSync(
|
||||
join(
|
||||
process.cwd(),
|
||||
"src/components/settings/dashboard-layout-section.tsx",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
expect(src).toContain("selectedScoreRings: next.selectedScoreRings");
|
||||
expect(src).toContain("heroRingOrder: next.heroRingOrder");
|
||||
// The order is the single source of truth: selection is derived from it.
|
||||
expect(src).toMatch(/id !== HEALTH_SCORE_RING_ID/);
|
||||
});
|
||||
});
|
||||
@@ -382,8 +382,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
);
|
||||
// Keep an open draft's ring slice in sync so a later Save of the
|
||||
// OTHER layout edits can't revert the instantly-applied choice.
|
||||
if (draft)
|
||||
setDraft({ ...draft, selectedScoreRings, heroRingOrder: next });
|
||||
if (draft) setDraft({ ...draft, selectedScoreRings, heroRingOrder: next });
|
||||
ringMutation.mutate({ selectedScoreRings, heroRingOrder: next });
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user