diff --git a/apps/web/components/settings/secrets-delete-dialog.test.tsx b/apps/web/components/settings/secrets-delete-dialog.test.tsx new file mode 100644 index 0000000000..9df6ff1aa3 --- /dev/null +++ b/apps/web/components/settings/secrets-delete-dialog.test.tsx @@ -0,0 +1,61 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { activateLocale } from "@/lib/i18n"; +import type { SecretListItem } from "@/lib/types/http-secrets"; +import { DeleteSecretDialog } from "./secrets-settings"; + +/** + * A secret's name is user data. It reaches the dialog as an interpolated value + * and must never become a catalog key — the pseudo locale is the oracle here: + * the sentence around the name changes, the name itself must not. + */ +const secret: SecretListItem = { + id: "secret-1", + name: "OpenAI Production Key", + has_value: true, + created_at: "", + updated_at: "", +}; + +/** + * The name renders inside a ``, so testing-library's text matcher — which + * joins only an element's direct text-node children — never sees the sentence + * whole. Read the description element's own `textContent` instead. + */ +function descriptionText(): string { + const description = screen.getByText(secret.name).closest("p"); + // Fail on the broken query rather than on an empty string comparison, which + // would point every assertion below at the wrong cause. + if (!description) throw new Error("secret name is not inside the

DialogDescription"); + return description.textContent ?? ""; +} + +afterEach(async () => { + cleanup(); + await activateLocale("en"); +}); + +describe("DeleteSecretDialog", () => { + it("renders the confirmation copy around the secret name", () => { + render( + , + ); + + expect(descriptionText()).toBe( + `This will permanently remove ${secret.name}. This action cannot be undone.`, + ); + }); + + it("keeps the secret name verbatim when the locale changes", async () => { + await activateLocale("pseudo"); + render( + , + ); + + // The surrounding sentence is catalog copy, so pseudo accents it. + expect(descriptionText()).toMatch(/[À-ɏ]/); + expect(descriptionText()).not.toContain("This will permanently remove"); + // The name is user data — byte-identical under any locale. + expect(descriptionText()).toContain(secret.name); + }); +}); diff --git a/apps/web/components/settings/secrets-settings.tsx b/apps/web/components/settings/secrets-settings.tsx index d422607206..b7230d74d0 100644 --- a/apps/web/components/settings/secrets-settings.tsx +++ b/apps/web/components/settings/secrets-settings.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { IconEdit, IconTrash, IconEye, IconEyeOff, IconKey } from "@tabler/icons-react"; import { Button } from "@kandev/ui/button"; import { @@ -64,6 +65,7 @@ function SecretForm({ showSubmit = true, baselineState, }: SecretFormProps) { + const { t } = useTranslation(); const nameIsDirty = Boolean(baselineState) && formState.name.trim() !== baselineState?.name; const valueIsDirty = Boolean(baselineState) && formState.value !== baselineState?.value; return ( @@ -76,14 +78,14 @@ function SecretForm({ onFormChange({ name: e.target.value })} - placeholder="Name (e.g. OpenAI Production Key)" + placeholder={t("settings:nameEGOpenaiProductionKey")} disabled={isBusy} data-settings-dirty={nameIsDirty} />