Files
kandev/apps/web/components/settings/notification-sound-section.test.tsx
T
João Salavisa e4c4efec18 feat(i18n): localize Settings > General > Notifications
Settings > General > Notifications held all of its copy as literals, so the page
stayed English under a locale switch. Every string now resolves through t() /
<Trans> at render, and the page is on the i18nGuardFiles allowlist so it cannot
drift back.

Event titles, sound preset names and the permission button label resolve from
maps of catalog keys rather than module-scope t(), so they follow a locale
switch. The Apprise install notice is a single <Trans> message so the link stays
inside the sentence. SCREENS in the pseudo-locale oracle gains a per-screen
allow list for text the frontend must not translate (backend-owned provider
names).
2026-08-02 01:49:44 +02:00

104 lines
3.8 KiB
TypeScript

import { useState } from "react";
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getSoundPreferences, setSoundPreferences } from "@/lib/notifications/sound";
import { NotificationSoundSection } from "./notification-sound-section";
import { SettingsSaveProvider } from "./settings-save-provider";
import { SettingsPageTemplate } from "./settings-page-template";
const DIRTY = "true";
const DIRTY_ATTRIBUTE = "data-settings-dirty";
const SOUND_TOGGLE_NAME = "Enable notification sound";
function NotificationPageHarness({ onProviderSave }: { onProviderSave: () => void }) {
const [soundIsDirty, setSoundIsDirty] = useState(false);
return (
<SettingsPageTemplate
title="Notifications"
isDirty={false}
cardIsDirty={soundIsDirty}
saveStatus="idle"
onSave={onProviderSave}
>
<NotificationSoundSection onDirtyChange={setSoundIsDirty} />
</SettingsPageTemplate>
);
}
beforeEach(() => {
window.localStorage.clear();
setSoundPreferences({ enabled: false, presetId: "plim" });
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe("NotificationSoundSection", () => {
it("persists a dirty sound preference only through the route Save action", async () => {
render(
<SettingsSaveProvider>
<NotificationSoundSection />
</SettingsSaveProvider>,
);
const toggle = screen.getByRole("switch", { name: SOUND_TOGGLE_NAME });
fireEvent.click(toggle);
expect(getSoundPreferences()).toEqual({ enabled: false, presetId: "plim" });
expect(toggle.getAttribute(DIRTY_ATTRIBUTE)).toBe(DIRTY);
expect(screen.getByTestId("notification-sound-group").getAttribute(DIRTY_ATTRIBUTE)).toBe(
DIRTY,
);
fireEvent.click(await screen.findByRole("button", { name: "Save changes" }));
await waitFor(() => expect(getSoundPreferences()).toEqual({ enabled: true, presetId: "plim" }));
await waitFor(() => expect(toggle.getAttribute(DIRTY_ATTRIBUTE)).toBe("false"));
});
it("shows the selected preset under its translated name", async () => {
setSoundPreferences({ enabled: true, presetId: "ding" });
render(
<SettingsSaveProvider>
<NotificationSoundSection />
</SettingsSaveProvider>,
);
// The preset id is the persisted sentinel; only the name is translated.
const trigger = await screen.findByRole("combobox", { name: "Notification sound" });
expect(trigger.textContent).toContain("Ding");
});
it("reports dirtiness so the notifications parent card can highlight", async () => {
const onDirtyChange = vi.fn();
render(
<SettingsSaveProvider>
<NotificationSoundSection onDirtyChange={onDirtyChange} />
</SettingsSaveProvider>,
);
await waitFor(() => expect(onDirtyChange).toHaveBeenLastCalledWith(false));
fireEvent.click(screen.getByRole("switch", { name: SOUND_TOGGLE_NAME }));
await waitFor(() => expect(onDirtyChange).toHaveBeenLastCalledWith(true));
});
it("does not register the provider save for a sound-only parent-card highlight", async () => {
const onProviderSave = vi.fn();
render(
<SettingsSaveProvider>
<NotificationPageHarness onProviderSave={onProviderSave} />
</SettingsSaveProvider>,
);
fireEvent.click(screen.getByRole("switch", { name: SOUND_TOGGLE_NAME }));
const parentCard = screen.getByText("Notification Sound").closest('[data-slot="card"]');
await waitFor(() => expect(parentCard?.getAttribute(DIRTY_ATTRIBUTE)).toBe(DIRTY));
fireEvent.click(screen.getByRole("button", { name: "Save changes" }));
await waitFor(() => expect(getSoundPreferences().enabled).toBe(true));
expect(onProviderSave).not.toHaveBeenCalled();
});
});