Merge branch 'feat/v144-settings' into release/v1.44.0

# Conflicts:
#	src/components/settings/privacy-section.tsx
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 20:51:39 +02:00
commit e1b70537cc
129 files changed
+5138 -3169

No files matched your search

+4 -2
View File
@@ -807,12 +807,14 @@ test.describe("Coach page frame", () => {
const popover = page.locator('[data-slot="coach-settings-popover"]');
await expect(popover).toBeVisible();
await expect(popover).toHaveAttribute("role", "dialog");
// The model is chosen in Settings > AI; the overlay shows what is in
// effect and links there.
await expect(
popover.locator('[data-slot="coach-quick-provider"]'),
).toHaveValue("local");
).toHaveAttribute("data-value", "local");
await expect(
popover.locator('[data-slot="coach-quick-model"]'),
).toHaveValue("qwen2.5");
).toHaveAttribute("data-value", "qwen2.5");
await expect(
popover.locator('[data-slot="coach-sources-window"]'),
).toBeVisible();
+2 -18
View File
@@ -270,21 +270,6 @@ async function saveCard(
return (await settled).status();
}
/** Flip a channel card's enable switch and wait for the route to answer. */
async function toggleCard(
page: Page,
switchId: string,
endpoint: string,
): Promise<number> {
const settled = page.waitForResponse(
(res) =>
new URL(res.url()).pathname === endpoint &&
res.request().method() === "PUT",
);
await page.locator(switchId).click();
return (await settled).status();
}
/**
* Configure both channels the way a person would: email on with a recipient,
* ntfy saved and left off. Everything here happens on the settings surface.
@@ -292,11 +277,10 @@ async function toggleCard(
async function configureChannels(page: Page): Promise<void> {
await openChannels(page);
// The enable switch is a field of the form: it saves with the one Save.
await fillField(page, "#email-recipient", EMAIL_RECIPIENT);
await page.locator("#email-toggle").click();
expect(await saveCard(page, "#email", "/api/settings/email")).toBe(200);
expect(await toggleCard(page, "#email-toggle", "/api/settings/email")).toBe(
200,
);
await expect(page.locator("#email-toggle")).toHaveAttribute(
"data-state",
"checked",
+6 -2
View File
@@ -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. */
+47
View File
@@ -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);
});
});
+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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