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."); + }); +});