mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
merge(reconcile): t2-widgettoggle audit findings
This commit is contained in:
commit
36bb4734f5
2 files changed
+74
No files matched your search
@@ -57,6 +57,17 @@ vi.mock("sonner", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// v1.18.0 — mutable module map so the widget-toggle gating tests can flip a
|
||||
// module off without re-mocking. Defaults to `undefined` (no map → fail-open,
|
||||
// every web-renderable row shown), preserving the pre-existing row counts.
|
||||
const authState: {
|
||||
modules: Partial<Record<string, boolean>> | undefined;
|
||||
} = { modules: undefined };
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ user: { modules: authState.modules } }),
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import {
|
||||
DashboardLayoutSection,
|
||||
@@ -71,6 +82,7 @@ function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
|
||||
beforeEach(() => {
|
||||
queryState.layout = DEFAULT_DASHBOARD_LAYOUT;
|
||||
authState.modules = undefined;
|
||||
});
|
||||
|
||||
describe("<DashboardLayoutSection> — tile + chart split", () => {
|
||||
@@ -382,3 +394,47 @@ describe("<DashboardLayoutSection> — iOS-pin-only ids hidden from web (v1.11.2
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.18.0 — a dashboard widget toggle whose owning module is disabled is a
|
||||
* dead control: the snapshot gates the tile/chart out server-side no matter
|
||||
* what the switch says. The Settings list must hide those rows. Core widgets
|
||||
* (no module entry in `WIDGET_MODULE_BY_ID`) always show; the gate fails open
|
||||
* when the module map is absent or the key is unset.
|
||||
*/
|
||||
describe("<DashboardLayoutSection> — disabled-module widget toggles", () => {
|
||||
// The achievements widget label drives its switch aria-label; it is the
|
||||
// canonical disabled-module probe (label resolves to "Achievements").
|
||||
const ACHIEVEMENTS_ARIA = "Achievements — ";
|
||||
// Mood is a module-owned widget too; weight is a core widget with NO module
|
||||
// entry, so it must survive any module-off state.
|
||||
const MOOD_ARIA = "Mood — ";
|
||||
const WEIGHT_ARIA = "Weight — ";
|
||||
|
||||
it("hides a widget toggle whose owning module is disabled", () => {
|
||||
authState.modules = { achievements: false };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
// The achievements row (and its switches) are gone…
|
||||
expect(html).not.toContain(ACHIEVEMENTS_ARIA);
|
||||
// …and exactly one fewer web-renderable row renders.
|
||||
const tileSwitches = html.match(/data-slot="widget-tile-switch"/g) ?? [];
|
||||
expect(tileSwitches).toHaveLength(WEB_RENDERABLE_ROW_COUNT - 1);
|
||||
});
|
||||
|
||||
it("keeps an enabled-module widget and core (no-module) widgets shown", () => {
|
||||
authState.modules = { achievements: false };
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
// Enabled module (mood) still renders…
|
||||
expect(html).toContain(MOOD_ARIA);
|
||||
// …and a core widget with no module entry (weight) always shows.
|
||||
expect(html).toContain(WEIGHT_ARIA);
|
||||
});
|
||||
|
||||
it("shows every row when the module map is absent (fail-open)", () => {
|
||||
authState.modules = undefined;
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const tileSwitches = html.match(/data-slot="widget-tile-switch"/g) ?? [];
|
||||
expect(tileSwitches).toHaveLength(WEB_RENDERABLE_ROW_COUNT);
|
||||
expect(html).toContain(ACHIEVEMENTS_ARIA);
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { WIDGET_MODULE_BY_ID } from "@/lib/dashboard/snapshot";
|
||||
|
||||
/**
|
||||
* v1.4.47 W4 — pure reorder helper shared by the arrow buttons and the
|
||||
@@ -149,6 +151,14 @@ const WIDGET_LABEL_KEYS: Record<DashboardWidgetId, string> = {
|
||||
export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
// v1.18.0 — a widget toggle whose owning module is disabled is a dead
|
||||
// control (the snapshot gates the tile/chart out server-side regardless
|
||||
// of the switch). Read the resolved module map and hide those rows.
|
||||
// Fail-open: a missing map / absent key (stale /me payload, core widget)
|
||||
// reads as enabled, so the row always shows unless the module is
|
||||
// explicitly `false`.
|
||||
const { user } = useAuth();
|
||||
const modules = user?.modules;
|
||||
// v1.4.47 W4 — stable id namespace for the drag-handle `aria-describedby`
|
||||
// tooltip. One hint paragraph is rendered once at the bottom of the list
|
||||
// and referenced by every drag handle in this section.
|
||||
@@ -471,6 +481,14 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
.filter((w): w is typeof w & { id: DashboardWidgetId } =>
|
||||
webWidgetIds.has(w.id),
|
||||
)
|
||||
// v1.18.0 — hide a widget toggle whose owning module is
|
||||
// disabled. Map the widget id → ModuleKey FIRST (undefined =
|
||||
// core widget = always shown), THEN check the module map.
|
||||
// Fail-open: only an explicit `false` hides the row.
|
||||
.filter((w) => {
|
||||
const moduleKey = WIDGET_MODULE_BY_ID[w.id];
|
||||
return !moduleKey || modules?.[moduleKey] !== false;
|
||||
})
|
||||
.sort((a, b) => a.order - b.order);
|
||||
const sortedIds = sortedWidgets.map((w) => w.id);
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user