mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
fix(dashboard): hide iOS-pin-only widgets from the web settings list
The eight v1.10 additive HealthKit signals (cardio recovery, six-minute walk, stair ascent/descent speed, breathing disturbances, wrist temperature, falls, walking steadiness) are writable dashboard widget ids so the native client's Home-pin PUT validates them, but the web dashboard renders strip tiles via hardcoded per-id blocks and has no render path for them. Left as-is, the web Settings -> Dashboard list would offer eight toggles that do nothing on web. Name them in a new IOS_PIN_ONLY_WIDGET_IDS set (writable-but-not- web-rendered) and filter that set out of the rendered settings list, so the toggles never appear on web while the ids stay accepted by the widgets PUT enum for the native client. Add tests asserting the eight remain writable (catalogue membership) yet are absent from the web list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2994954138
commit
2763a64b83
4 files changed
+147
-13
No files matched your search
@@ -4,9 +4,18 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import {
|
||||
DEFAULT_DASHBOARD_LAYOUT,
|
||||
DASHBOARD_IOS_ONLY_WIDGET_IDS,
|
||||
DASHBOARD_WIDGET_IDS,
|
||||
IOS_PIN_ONLY_WIDGET_IDS,
|
||||
type DashboardLayout,
|
||||
} from "@/lib/dashboard-layout";
|
||||
|
||||
// v1.11.2 HIGH-1 — the web Settings list renders one row per WRITABLE id
|
||||
// (`DASHBOARD_WIDGET_IDS`) MINUS the `IOS_PIN_ONLY_WIDGET_IDS` (writable so
|
||||
// the iOS pin PUT validates, but with no web render path). The default
|
||||
// layout still carries all 24 writable widgets; only this subset renders.
|
||||
const WEB_RENDERABLE_ROW_COUNT =
|
||||
DASHBOARD_WIDGET_IDS.length - IOS_PIN_ONLY_WIDGET_IDS.length;
|
||||
|
||||
// Mutable holder so individual tests can inject a layout (e.g. one that
|
||||
// carries iOS-only ids) into the mocked `useQuery` without re-mocking
|
||||
// the module. Defaults to the 16-tile web default layout.
|
||||
@@ -88,10 +97,10 @@ describe("<DashboardLayoutSection> — tile + chart split", () => {
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const tileSwitches = html.match(/data-slot="widget-tile-switch"/g) ?? [];
|
||||
const chartSwitches = html.match(/data-slot="widget-chart-switch"/g) ?? [];
|
||||
// Default layout has 13 widgets — both switch counts must match
|
||||
// exactly so the split control covers every widget.
|
||||
expect(tileSwitches).toHaveLength(DEFAULT_DASHBOARD_LAYOUT.widgets.length);
|
||||
expect(chartSwitches).toHaveLength(DEFAULT_DASHBOARD_LAYOUT.widgets.length);
|
||||
// One tile + one chart switch per WEB-renderable widget (the
|
||||
// iOS-pin-only ids are filtered out — they have no web render path).
|
||||
expect(tileSwitches).toHaveLength(WEB_RENDERABLE_ROW_COUNT);
|
||||
expect(chartSwitches).toHaveLength(WEB_RENDERABLE_ROW_COUNT);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +117,7 @@ describe("<DashboardLayoutSection> — drag-and-drop reorder", () => {
|
||||
it("paints a drag handle for every widget row", () => {
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const handles = html.match(/data-slot="widget-drag-handle"/g) ?? [];
|
||||
expect(handles).toHaveLength(DEFAULT_DASHBOARD_LAYOUT.widgets.length);
|
||||
expect(handles).toHaveLength(WEB_RENDERABLE_ROW_COUNT);
|
||||
});
|
||||
|
||||
it("each drag handle has an aria-describedby pointing to a shared hint", () => {
|
||||
@@ -117,7 +126,7 @@ describe("<DashboardLayoutSection> — drag-and-drop reorder", () => {
|
||||
// to care about attribute ordering inside the rendered <button>.
|
||||
const handleButtons =
|
||||
html.match(/<button[^>]*data-slot="widget-drag-handle"[^>]*>/g) ?? [];
|
||||
expect(handleButtons).toHaveLength(DEFAULT_DASHBOARD_LAYOUT.widgets.length);
|
||||
expect(handleButtons).toHaveLength(WEB_RENDERABLE_ROW_COUNT);
|
||||
// Every handle declares aria-describedby and they all share the
|
||||
// same id (one hint paragraph below the list — single source of
|
||||
// truth for screen readers).
|
||||
@@ -139,8 +148,8 @@ describe("<DashboardLayoutSection> — drag-and-drop reorder", () => {
|
||||
// strings — counted once per row.
|
||||
const moveUpCount = (html.match(/aria-label="Move up"/g) ?? []).length;
|
||||
const moveDownCount = (html.match(/aria-label="Move down"/g) ?? []).length;
|
||||
expect(moveUpCount).toBe(DEFAULT_DASHBOARD_LAYOUT.widgets.length);
|
||||
expect(moveDownCount).toBe(DEFAULT_DASHBOARD_LAYOUT.widgets.length);
|
||||
expect(moveUpCount).toBe(WEB_RENDERABLE_ROW_COUNT);
|
||||
expect(moveDownCount).toBe(WEB_RENDERABLE_ROW_COUNT);
|
||||
});
|
||||
|
||||
it("hint string localises to German", () => {
|
||||
@@ -229,10 +238,11 @@ describe("<DashboardLayoutSection> — iOS-only id skip (v1.7.0)", () => {
|
||||
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
|
||||
// One row (= one tile switch) per WEB-known widget — the iOS-only
|
||||
// ids are skipped, not rendered as raw-id rows.
|
||||
// One row (= one tile switch) per WEB-renderable widget — the iOS-only
|
||||
// ids (and the iOS-pin-only writable ids) are skipped, not rendered as
|
||||
// raw-id rows.
|
||||
const tileSwitches = html.match(/data-slot="widget-tile-switch"/g) ?? [];
|
||||
expect(tileSwitches).toHaveLength(DEFAULT_DASHBOARD_LAYOUT.widgets.length);
|
||||
expect(tileSwitches).toHaveLength(WEB_RENDERABLE_ROW_COUNT);
|
||||
|
||||
// No iOS-only raw id leaks into the markup as a row label.
|
||||
for (const iosId of DASHBOARD_IOS_ONLY_WIDGET_IDS) {
|
||||
@@ -258,3 +268,43 @@ describe("<DashboardLayoutSection> — iOS-only id skip (v1.7.0)", () => {
|
||||
expect(html).toContain("dashboard-layout");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.11.2 HIGH-1 — the 8 B5 ids are WRITABLE (in DASHBOARD_WIDGET_IDS so the
|
||||
* iOS pin PUT validates them) but have NO web render path, so the web
|
||||
* Settings list must NOT offer a dead toggle for them.
|
||||
*/
|
||||
describe("<DashboardLayoutSection> — iOS-pin-only ids hidden from web (v1.11.2)", () => {
|
||||
it("renders one fewer row per iOS-pin-only id than the writable id count", () => {
|
||||
// Default layout carries all 24 writable widgets incl. the 8 pin-only.
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const tileSwitches = html.match(/data-slot="widget-tile-switch"/g) ?? [];
|
||||
expect(tileSwitches).toHaveLength(WEB_RENDERABLE_ROW_COUNT);
|
||||
// Sanity: WEB_RENDERABLE_ROW_COUNT == writable − pin-only.
|
||||
expect(WEB_RENDERABLE_ROW_COUNT).toBe(
|
||||
DASHBOARD_WIDGET_IDS.length - IOS_PIN_ONLY_WIDGET_IDS.length,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not paint a row whose aria-label matches an iOS-pin-only widget label", () => {
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
// The pin-only ids reuse `measurements.type*` labels that resolve to
|
||||
// distinctive English strings; none should appear as a toggle aria-label.
|
||||
const pinOnlyLabels: Record<(typeof IOS_PIN_ONLY_WIDGET_IDS)[number], string> =
|
||||
{
|
||||
cardioRecovery: "Cardio recovery",
|
||||
sixMinuteWalk: "Six-minute walk distance",
|
||||
stairAscentSpeed: "Stair ascent speed",
|
||||
stairDescentSpeed: "Stair descent speed",
|
||||
breathingDisturbances: "Breathing disturbances",
|
||||
wristTemperature: "Wrist temperature",
|
||||
falls: "Falls",
|
||||
walkingSteadiness: "Walking steadiness",
|
||||
};
|
||||
for (const id of IOS_PIN_ONLY_WIDGET_IDS) {
|
||||
// The widget label drives the switch aria-label; if the row were
|
||||
// rendered the localised label would show up in the markup.
|
||||
expect(html).not.toContain(`${pinOnlyLabels[id]} — `);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
COMPARISON_BASELINES,
|
||||
DEFAULT_DASHBOARD_LAYOUT,
|
||||
DASHBOARD_WIDGET_IDS,
|
||||
IOS_PIN_ONLY_WIDGET_IDS,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import {
|
||||
Select,
|
||||
@@ -422,12 +423,21 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
// v1.7.0 — the stored layout now round-trips the 11 iOS-only
|
||||
// widget ids so the native client can drop its local merge
|
||||
// workarounds. The web Settings list has no tile/chart
|
||||
// surface for them, so skip any id outside the 16 web-known
|
||||
// surface for them, so skip any id outside the web-known
|
||||
// ids rather than render an unlabelled row with dead toggles.
|
||||
// The skipped ids stay untouched in the persisted layout
|
||||
// because the Save mutation PUTs `layout.widgets` whole and
|
||||
// the server retains every catalogue id.
|
||||
const webWidgetIds = new Set<string>(DASHBOARD_WIDGET_IDS);
|
||||
//
|
||||
// v1.11.2 HIGH-1 — the 8 B5 ids are WRITABLE (in
|
||||
// `DASHBOARD_WIDGET_IDS` so the iOS pin PUT validates) but
|
||||
// have no web render path either, so exclude
|
||||
// `IOS_PIN_ONLY_WIDGET_IDS` too: a web toggle for them would
|
||||
// be a silent no-op on the web dashboard.
|
||||
const iosPinOnly = new Set<string>(IOS_PIN_ONLY_WIDGET_IDS);
|
||||
const webWidgetIds = new Set<string>(
|
||||
DASHBOARD_WIDGET_IDS.filter((wid) => !iosPinOnly.has(wid)),
|
||||
);
|
||||
const sortedWidgets = [...layout.widgets]
|
||||
.filter((w): w is typeof w & { id: DashboardWidgetId } =>
|
||||
webWidgetIds.has(w.id),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
DASHBOARD_WIDGET_IDS,
|
||||
DASHBOARD_IOS_ONLY_WIDGET_IDS,
|
||||
DASHBOARD_WIDGET_CATALOGUE_IDS,
|
||||
IOS_PIN_ONLY_WIDGET_IDS,
|
||||
type DashboardLayout,
|
||||
} from "@/lib/dashboard-layout";
|
||||
|
||||
@@ -421,6 +422,50 @@ describe("DASHBOARD_WIDGET_CATALOGUE_IDS — 27-id catalogue", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.11.2 HIGH-1 — the 8 B5 metrics are WRITABLE (members of
|
||||
* `DASHBOARD_WIDGET_IDS`, so the widgets PUT enum — derived from the
|
||||
* catalogue — accepts them, which the iOS Home-pin request requires) but
|
||||
* have NO web render path. `IOS_PIN_ONLY_WIDGET_IDS` names exactly that
|
||||
* set so the web Settings list can filter them out (asserted in
|
||||
* `dashboard-layout-section.test.tsx`).
|
||||
*/
|
||||
describe("IOS_PIN_ONLY_WIDGET_IDS — writable but not web-rendered", () => {
|
||||
it("is the 8 B5 ids verbatim", () => {
|
||||
expect([...IOS_PIN_ONLY_WIDGET_IDS]).toEqual([
|
||||
"cardioRecovery",
|
||||
"sixMinuteWalk",
|
||||
"stairAscentSpeed",
|
||||
"stairDescentSpeed",
|
||||
"breathingDisturbances",
|
||||
"wristTemperature",
|
||||
"falls",
|
||||
"walkingSteadiness",
|
||||
]);
|
||||
});
|
||||
|
||||
it("every pin-only id is WRITABLE (in DASHBOARD_WIDGET_IDS so the PUT enum accepts it)", () => {
|
||||
const writable = new Set<string>(DASHBOARD_WIDGET_IDS);
|
||||
for (const id of IOS_PIN_ONLY_WIDGET_IDS) {
|
||||
expect(writable.has(id)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("every pin-only id is in the catalogue the widgets PUT Zod enum derives from", () => {
|
||||
const catalogue = new Set<string>(DASHBOARD_WIDGET_CATALOGUE_IDS);
|
||||
for (const id of IOS_PIN_ONLY_WIDGET_IDS) {
|
||||
expect(catalogue.has(id)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not overlap the iOS-only (non-writable) catalogue ids", () => {
|
||||
const iosOnly = new Set<string>(DASHBOARD_IOS_ONLY_WIDGET_IDS);
|
||||
for (const id of IOS_PIN_ONLY_WIDGET_IDS) {
|
||||
expect(iosOnly.has(id)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.7.0 W1 — iOS-only ids round-trip through the stored layout so the
|
||||
* native client can drop its local merge workarounds
|
||||
|
||||
@@ -58,6 +58,35 @@ export const DASHBOARD_WIDGET_IDS = [
|
||||
|
||||
export type DashboardWidgetId = (typeof DASHBOARD_WIDGET_IDS)[number];
|
||||
|
||||
/**
|
||||
* v1.11.2 B5 / HIGH-1 — widget ids that are WRITABLE (members of
|
||||
* `DASHBOARD_WIDGET_IDS`, so the iOS pin PUT validates them) but have NO
|
||||
* web render path: `src/app/page.tsx` renders strip tiles via hardcoded
|
||||
* per-id blocks and never iterates an id→component map, so these ids draw
|
||||
* nothing on the web dashboard. They are surfaced and pinned by the native
|
||||
* iOS client only.
|
||||
*
|
||||
* They MUST stay in `DASHBOARD_WIDGET_IDS` (the iOS v0.14 Home-pin request
|
||||
* needs the widgets PUT enum to accept them), but the web Settings →
|
||||
* Dashboard list filters them out (see `dashboard-layout-section.tsx`) so a
|
||||
* web user is never offered a toggle that silently does nothing on web.
|
||||
*
|
||||
* The 11 `DASHBOARD_IOS_ONLY_WIDGET_IDS` below are a separate concern: those
|
||||
* are NOT writable (not in `DASHBOARD_WIDGET_IDS`) so they never reached the
|
||||
* web Settings list in the first place. This set is specifically the
|
||||
* writable-but-not-web-rendered ids that B5 introduced.
|
||||
*/
|
||||
export const IOS_PIN_ONLY_WIDGET_IDS = [
|
||||
"cardioRecovery",
|
||||
"sixMinuteWalk",
|
||||
"stairAscentSpeed",
|
||||
"stairDescentSpeed",
|
||||
"breathingDisturbances",
|
||||
"wristTemperature",
|
||||
"falls",
|
||||
"walkingSteadiness",
|
||||
] as const satisfies readonly DashboardWidgetId[];
|
||||
|
||||
/**
|
||||
* v1.7.0 — iOS-only widget ids the native client materialises in its
|
||||
* own default layout but the server PUT enum does NOT yet accept. These
|
||||
|
||||
Reference in new issue
Block a user