diff --git a/e2e/settings-admin-one-piece.spec.ts b/e2e/settings-admin-one-piece.spec.ts
index 4fc1b69d2..3f10f6540 100644
--- a/e2e/settings-admin-one-piece.spec.ts
+++ b/e2e/settings-admin-one-piece.spec.ts
@@ -58,11 +58,13 @@ const SETTINGS_ROUTES = [
"/settings/api",
"/settings/mcp",
"/settings/advanced",
+ "/settings/preview",
"/settings/about",
] as const;
/**
- * The Layout hub's seven children. They are the routes whose header block
+ * The Arrangement hub's children, plus (v1.44) the module pages under
+ * Modules, which use the same back-link header. They are the routes whose header block
* changed MOST — the shell used to hand-roll a `space-y-6` between the
* back-link and the title, and `PageHeader`'s documented contract is
* `space-y-1.5`. Nothing measured them before, so the step went from 24 px
@@ -73,10 +75,12 @@ const LAYOUT_CHILD_ROUTES = [
"/settings/layout/dashboard",
"/settings/layout/insights",
"/settings/layout/medications",
- "/settings/layout/mood",
"/settings/layout/labs",
"/settings/layout/illness",
"/settings/layout/vorsorge",
+ "/settings/modules/medications",
+ "/settings/modules/mood",
+ "/settings/modules/labs",
] as const;
/** The `space-y-1.5` between a back-link and the title it sits above. */
diff --git a/e2e/settings-mobile-nav.spec.ts b/e2e/settings-mobile-nav.spec.ts
new file mode 100644
index 000000000..e8a115c68
--- /dev/null
+++ b/e2e/settings-mobile-nav.spec.ts
@@ -0,0 +1,47 @@
+import { expect, test } from "./setup/test";
+
+import { STORAGE_STATE_PATH } from "./setup/global-setup";
+
+/**
+ * Settings navigation at 390 px: one button names the current section and
+ * opens a sheet with every section under its group. It replaced a strip of
+ * every section as chips, where most of Settings sat off screen.
+ */
+test.describe("settings navigation on a phone", () => {
+ test.use({ storageState: STORAGE_STATE_PATH });
+
+ test("names the section, opens the grouped list and moves on a tap", async ({
+ page,
+ }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/settings/account", { waitUntil: "domcontentloaded" });
+
+ const trigger = page.locator('[data-slot="settings-mobile-nav-trigger"]');
+ await expect(trigger).toBeVisible({ timeout: 15_000 });
+ await expect(trigger).toContainText("Account");
+
+ // The click can land before hydration; retry until the sheet opens.
+ const list = page.locator('[data-slot="settings-mobile-nav-list"]');
+ await expect(async () => {
+ await trigger.click();
+ await expect(list).toBeVisible({ timeout: 2_000 });
+ }).toPass({ timeout: 15_000 });
+
+ for (const group of ["Account", "Tracking", "Notifications", "System"]) {
+ await expect(
+ list.getByRole("heading", { name: group, exact: true }),
+ ).toBeVisible();
+ }
+
+ await list.getByRole("link", { name: "Security" }).click();
+ await expect(page).toHaveURL(/\/settings\/security$/);
+ await expect(list).toHaveCount(0);
+ await expect(trigger).toContainText("Security");
+
+ // Nothing scrolls sideways.
+ const overflow = await page.evaluate(
+ () => document.documentElement.scrollWidth - window.innerWidth,
+ );
+ expect(overflow).toBeLessThanOrEqual(1);
+ });
+});
diff --git a/e2e/settings-overscroll.spec.ts b/e2e/settings-overscroll.spec.ts
index 59f858dfc..0df30bde2 100644
--- a/e2e/settings-overscroll.spec.ts
+++ b/e2e/settings-overscroll.spec.ts
@@ -33,7 +33,7 @@ const ROUTES = [
// one-scroll-floor assertion below).
"/settings/layout/insights",
"/settings/layout/medications",
- "/settings/layout/mood",
+ "/settings/modules/mood",
"/settings/modules",
"/settings/account",
"/settings/notifications",
diff --git a/e2e/timezone-profile.spec.ts b/e2e/timezone-profile.spec.ts
index f4865aa1d..7f4177b9a 100644
--- a/e2e/timezone-profile.spec.ts
+++ b/e2e/timezone-profile.spec.ts
@@ -6,7 +6,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
* Issue #490 — client-rendered timestamps follow the PROFILE timezone.
*
* `/api/auth/me` is mocked with an `Asia/Manila` profile; the sessions
- * card on /settings/privacy renders `lastActiveAt` through
+ * card on /settings/security renders `lastActiveAt` through
* `useFormatters()`, which reads the localStorage timezone mirror that
* `fetchMe` fills from the mocked payload. A fixed UTC instant must
* therefore paint the MANILA wall clock (14:30), never the Berlin
@@ -75,7 +75,7 @@ test.describe("profile-timezone display (#490)", () => {
}),
);
- await page.goto("/settings/privacy", { waitUntil: "domcontentloaded" });
+ await page.goto("/settings/security", { waitUntil: "domcontentloaded" });
// The sessions card is collapsed by default — expand it first. The
// toggle's onClick can be dropped if Playwright clicks before React has
diff --git a/src/components/settings/__tests__/preview-section.test.tsx b/src/components/settings/__tests__/preview-section.test.tsx
new file mode 100644
index 000000000..7dc3e11ba
--- /dev/null
+++ b/src/components/settings/__tests__/preview-section.test.tsx
@@ -0,0 +1,51 @@
+import { describe, expect, it } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { queryKeys } from "@/lib/query-keys";
+import { PreviewSection } from "../preview-section";
+import { primeLocales } from "@/__tests__/helpers/prime-locales";
+
+primeLocales("de");
+
+function render(topics: boolean | undefined, locale: "en" | "de" = "en") {
+ const client = new QueryClient({
+ defaultOptions: { queries: { retry: false, refetchOnMount: false } },
+ });
+ if (topics !== undefined) {
+ client.setQueryData(queryKeys.previewFeatures(), { topics });
+ }
+ return renderToStaticMarkup(
+
+
+
+
+ ,
+ );
+}
+
+describe("", () => {
+ it("shows the Topics switch off by default, saved on change", () => {
+ const html = render(false);
+ expect(html).toContain('data-slot="settings-preview-topics-switch"');
+ expect(html).toMatch(
+ /data-slot="settings-preview-topics-switch"[^>]*aria-checked="false"|aria-checked="false"[^>]*data-slot="settings-preview-topics-switch"/,
+ );
+ expect(html).toContain("Topics (beta)");
+ // A switch, no Save: one save model per card.
+ expect(html).not.toContain('type="submit"');
+ });
+
+ it("reads on once the person switched it on", () => {
+ expect(render(true)).toMatch(
+ /data-slot="settings-preview-topics-switch"[^>]*aria-checked="true"|aria-checked="true"[^>]*data-slot="settings-preview-topics-switch"/,
+ );
+ });
+
+ it("says the existing pages stay as they are and nothing raw leaks", () => {
+ const html = render(false, "de");
+ expect(html).toContain("Themen (Beta)");
+ expect(html).not.toContain("settings.preview.");
+ });
+});