diff --git a/apps/web/components/settings/notification-events-table.tsx b/apps/web/components/settings/notification-events-table.tsx index b075d7b999..7697daae19 100644 --- a/apps/web/components/settings/notification-events-table.tsx +++ b/apps/web/components/settings/notification-events-table.tsx @@ -161,9 +161,7 @@ function MobileEventList({ key={provider.id} className="flex min-h-11 items-center gap-3 rounded-md border border-muted px-3 py-2" > - - {provider.name} - + {provider.name} - +
@@ -246,7 +244,7 @@ export function NotificationEventsTable(props: Props) { const { t } = useTranslation(); if (props.tableProviders.length === 0) { return ( -

{t("settings:noProvidersConfiguredYet")}

+

{t("settings:noProvidersConfiguredYet")}

); } diff --git a/apps/web/components/settings/notification-permission-section.tsx b/apps/web/components/settings/notification-permission-section.tsx index b59a28697b..cb215ce05e 100644 --- a/apps/web/components/settings/notification-permission-section.tsx +++ b/apps/web/components/settings/notification-permission-section.tsx @@ -47,8 +47,8 @@ export function DesktopNotificationsSection({
-
{t("settings:desktopNotifications")}
-

+

{t("settings:desktopNotifications")}
+

{t("settings:desktopNotificationsDescription")}

@@ -99,7 +99,7 @@ export function DesktopNotificationsSection({ - + {t("settings:notificationsNotShowingHint")} @@ -107,17 +107,17 @@ export function DesktopNotificationsSection({
{notificationPermission === "denied" && ( -

+

{nativeNotifications.isAvailable() ? t("settings:notificationsBlockedInOs") : t("settings:notificationsBlockedInBrowser")}

)} {notificationPermission === "unsupported" && ( -

{t("settings:notificationsUnsupported")}

+

{t("settings:notificationsUnsupported")}

)} {notificationPermission === "error" && ( -

{t("settings:notificationPermissionCheckFailed")}

+

{t("settings:notificationPermissionCheckFailed")}

)}
); diff --git a/apps/web/components/settings/notification-sound-section.tsx b/apps/web/components/settings/notification-sound-section.tsx index f67c011ca8..cbcb716151 100644 --- a/apps/web/components/settings/notification-sound-section.tsx +++ b/apps/web/components/settings/notification-sound-section.tsx @@ -66,8 +66,8 @@ export function NotificationSoundSection({ >
-
{t("settings:notificationSound")}
-

+

{t("settings:notificationSound")}
+

{t("settings:notificationSoundDescription")}

diff --git a/apps/web/components/settings/notifications-settings.test.tsx b/apps/web/components/settings/notifications-settings.test.tsx index 49b4d039d6..722fe05847 100644 --- a/apps/web/components/settings/notifications-settings.test.tsx +++ b/apps/web/components/settings/notifications-settings.test.tsx @@ -67,4 +67,29 @@ describe("NotificationsSettings", () => { "View installation instructions.", ); }); + + // The page body renders inside a `SettingsCard`, whose `Card` base is + // `text-xs/relaxed`; sibling settings pages keep group headings at `text-sm` + // and descriptions/tables at `text-xs`. This body used to hard-code + // `text-base`/`text-sm` throughout, so the whole page read a size larger than + // its siblings. The `text-2xl` page title above the card is the shared + // `SettingsPageTemplate` heading and is deliberately out of scope. jsdom has + // no Tailwind, so assert on the utility classes rather than computed sizes. + it("keeps the card body within the settings type scale", () => { + const { container } = render( + + + , + ); + + const body = container.querySelector('[data-slot="card-content"]'); + expect(body).not.toBeNull(); + const oversized = [...body!.querySelectorAll("[class]")] + .map((element) => element.getAttribute("class") ?? "") + // Lookahead, not `(?:\s|$)`: Tailwind's line-height modifier makes the + // next character `/` (the Card base is literally `text-xs/relaxed`), so a + // consuming boundary would let `text-xl/relaxed` through. + .filter((className) => /(?:^|\s)text-(?:base|lg|\d?xl)(?=[\s/]|$)/.test(className)); + expect(oversized).toEqual([]); + }); }); diff --git a/apps/web/components/settings/notifications-settings.tsx b/apps/web/components/settings/notifications-settings.tsx index dffaf4dca0..8f4474c7e9 100644 --- a/apps/web/components/settings/notifications-settings.tsx +++ b/apps/web/components/settings/notifications-settings.tsx @@ -215,13 +215,13 @@ function ExternalProvidersSection({ return (
-
{t("settings:externalProviders")}
-

+

{t("settings:externalProviders")}
+

{t("settings:externalProvidersDescription")}

{!appriseAvailable && ( -

+

{/* The link is part of the sentence, so the whole notice is one message and the <2> tag addresses the anchor by child index. */} @@ -239,7 +239,7 @@ function ExternalProvidersSection({

)} {appriseProviders.length === 0 && ( -

{t("settings:noAppriseProviders")}

+

{t("settings:noAppriseProviders")}

)}
-
{t("settings:notificationEvents")}
-

+

{t("settings:notificationEvents")}
+

{t("settings:notificationEventsDescription")}

@@ -469,7 +469,7 @@ function AppriseProviderForm({ data-settings-dirty={formIsDirty} data-settings-dirty-level="container" > -
{t("settings:appriseProvider")}
+
{t("settings:appriseProvider")}
onNameChange(event.target.value)} diff --git a/apps/web/e2e/tests/settings/mobile-notifications-type-scale.spec.ts b/apps/web/e2e/tests/settings/mobile-notifications-type-scale.spec.ts new file mode 100644 index 0000000000..c13d2f6f48 --- /dev/null +++ b/apps/web/e2e/tests/settings/mobile-notifications-type-scale.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from "../../fixtures/test-base"; +import { PROVIDER_NAMES, seedNotificationProviders } from "./notifications-type-scale-helpers"; + +test.describe("Mobile notifications settings type scale", () => { + test("renders the stacked event list on the shared settings scale", async ({ + testPage, + apiClient, + prCapture, + }) => { + await seedNotificationProviders(apiClient); + + // No setViewportSize here on purpose: this spec only runs under the + // `mobile-chrome` project (testMatch /mobile-.*\.spec\.ts/, Pixel 5), and + // `chromium` testIgnores the same pattern. The testPage context sets only + // baseURL, so it inherits the project's device. + await testPage.goto("/settings/general/notifications"); + + const eventList = testPage.getByTestId("notification-events-mobile-list"); + await expect(eventList).toBeVisible(); + + // One row per provider per event section, so the seeded name repeats. Match + // them all rather than .first() — a regression confined to a later section + // would otherwise pass. + const providerRows = eventList.getByText(PROVIDER_NAMES[0], { exact: true }); + const rowCount = await providerRows.count(); + expect(rowCount).toBeGreaterThan(1); + + // Capture before asserting so a run against the pre-fix code still produces + // the "before" image for the PR description. + await prCapture.screenshot("notifications-mobile", { + caption: "Settings → General → Notifications on a phone viewport", + }); + + // The mobile provider rows used to be text-sm while the section heading + // above them inherited the 12px Card base — larger children under a smaller + // heading. Every row sits at 12px now. + const sizes = await providerRows.evaluateAll((elements) => + elements.map((el) => parseFloat(getComputedStyle(el).fontSize)), + ); + expect(sizes).toHaveLength(rowCount); + for (const size of sizes) expect(size).toBeCloseTo(12, 1); + }); +}); diff --git a/apps/web/e2e/tests/settings/notifications-type-scale-helpers.ts b/apps/web/e2e/tests/settings/notifications-type-scale-helpers.ts new file mode 100644 index 0000000000..099b9061ba --- /dev/null +++ b/apps/web/e2e/tests/settings/notifications-type-scale-helpers.ts @@ -0,0 +1,24 @@ +import { expect } from "../../fixtures/test-base"; +import type { ApiClient } from "../../helpers/api-client"; + +const TURN_FINISHED = "session.turn_finished"; +const CLARIFICATION_REQUESTED = "session.clarification_requested"; + +/** Two providers so the desktop events table renders more than one column. */ +export const PROVIDER_NAMES = ["Desktop", "Team channel"]; + +/** + * The settings type scale is shared by the desktop table and the mobile + * stacked list, so both specs seed the same providers. Keeping one helper stops + * the two from drifting into asserting against different fixtures. + */ +export async function seedNotificationProviders(apiClient: ApiClient): Promise { + for (const name of PROVIDER_NAMES) { + const response = await apiClient.rawRequest("POST", "/api/v1/notification-providers", { + name, + type: "local", + events: [TURN_FINISHED, CLARIFICATION_REQUESTED], + }); + expect(response.ok).toBe(true); + } +} diff --git a/apps/web/e2e/tests/settings/notifications-type-scale.spec.ts b/apps/web/e2e/tests/settings/notifications-type-scale.spec.ts new file mode 100644 index 0000000000..efac8009df --- /dev/null +++ b/apps/web/e2e/tests/settings/notifications-type-scale.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from "../../fixtures/test-base"; +import { seedNotificationProviders } from "./notifications-type-scale-helpers"; + +test.describe("Notifications settings type scale", () => { + test("renders the card body on the shared settings scale", async ({ + testPage, + apiClient, + prCapture, + }) => { + await seedNotificationProviders(apiClient); + + await testPage.setViewportSize({ width: 1280, height: 1100 }); + await testPage.goto("/settings/general/notifications"); + + const table = testPage.getByTestId("notification-events-desktop-table"); + await expect(table).toBeVisible(); + const description = testPage.getByTestId("external-providers-description"); + await expect(description).toBeVisible(); + + // Capture before asserting so a run against the pre-fix code still produces + // the "before" image for the PR description. + await prCapture.screenshot("notifications-desktop", { + caption: "Settings → General → Notifications at 1280px", + }); + + // The page body inherits the Card base (text-xs/relaxed = 12px); group + // headings sit one step up at text-sm (14px). Assert the computed sizes so + // the capture is backed by a real measurement, not just an eyeball. + expect( + await description.evaluate((el) => parseFloat(getComputedStyle(el).fontSize)), + ).toBeCloseTo(12, 1); + expect(await table.evaluate((el) => parseFloat(getComputedStyle(el).fontSize))).toBeCloseTo( + 12, + 1, + ); + }); +});
{t("settings:notificationType")}