merge(reconcile): t2-widgettoggle audit findings

This commit is contained in:
Marc-André Bombeck committed 2026-06-16 03:02:55 +02:00
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 (