import React from "react"; import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { api } from "./api"; import { I18nProvider, useI18n } from "./i18n/I18nProvider"; import { ToastProvider } from "./toast"; function Probe() { const { language, setLanguage, hydrateLanguage, t } = useI18n(); return (
{language} {t("settings")}
); } beforeEach(() => { jest.clearAllMocks(); window.localStorage.clear(); }); test("migrates the legacy Norwegian code and persists Bokmål using nb-NO", async () => { window.localStorage.setItem("uiLanguage", "no"); render(); expect(screen.getByLabelText("language")).toHaveTextContent("nb"); expect(screen.getByLabelText("settings-label")).toHaveTextContent("Innstillinger"); expect(document.documentElement.lang).toBe("nb-NO"); fireEvent.click(screen.getByRole("button", { name: "English" })); expect(screen.getByLabelText("settings-label")).toHaveTextContent("Settings"); await waitFor(() => expect(api.put).toHaveBeenCalledWith("/auth/preferences/language", { language: "en" })); }); test("hydrates an authenticated server preference without writing it back", () => { render(); fireEvent.click(screen.getByRole("button", { name: /Restore server preference/i })); expect(screen.getByLabelText("language")).toHaveTextContent("nb"); expect(window.localStorage.getItem("uiLanguage")).toBe("nb"); expect(api.put).not.toHaveBeenCalled(); }); test.each([ ["Norsk", "nb", "nb-NO", "Språket er endret på denne enheten, men kunne ikke lagres på kontoen din.", "Prøv igjen"], ["English", "en", "en", "The language changed on this device, but your account preference could not be saved.", "Retry"], ])("a failed account language save keeps the local choice and offers a localized retry: %s", async (button, local, remote, message, retry) => { (api.put as jest.Mock).mockRejectedValueOnce({ response: { status: 500, data: "Internal database detail" } }); render(); fireEvent.click(screen.getByRole("button", { name: button })); expect(screen.getByLabelText("language")).toHaveTextContent(local); expect(window.localStorage.getItem("uiLanguage")).toBe(local); expect(document.documentElement.lang).toBe(remote); const alert = await screen.findByRole("alert"); expect(alert).toHaveTextContent(message); expect(alert).not.toHaveTextContent("Internal database detail"); expect(api.put).toHaveBeenCalledTimes(1); expect(api.put).toHaveBeenLastCalledWith("/auth/preferences/language", { language: remote }); fireEvent.click(within(alert).getByRole("button", { name: retry })); await waitFor(() => expect(api.put).toHaveBeenCalledTimes(2)); expect(api.put).toHaveBeenLastCalledWith("/auth/preferences/language", { language: remote }); await waitFor(() => expect(screen.queryByRole("alert")).not.toBeInTheDocument()); expect(screen.getByLabelText("language")).toHaveTextContent(local); }); test("a delayed language-save failure cannot offer a retry for a newer successful choice", async () => { let rejectOld!: (error: Error) => void; (api.put as jest.Mock).mockImplementationOnce(() => new Promise((_, reject) => { rejectOld = reject; })); render(); fireEvent.click(screen.getByRole("button", { name: "Norsk" })); fireEvent.click(screen.getByRole("button", { name: "English" })); await act(async () => { rejectOld(new Error("offline")); }); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect(screen.getByLabelText("language")).toHaveTextContent("en"); expect(api.put).toHaveBeenCalledTimes(2); }); test("a previously delivered language retry cannot reverse a newer choice", async () => { (api.put as jest.Mock).mockRejectedValueOnce(new Error("offline")); render(); fireEvent.click(screen.getByRole("button", { name: "Norsk" })); const alert = await screen.findByRole("alert"); const retry = within(alert).getByRole("button", { name: "Prøv igjen" }); fireEvent.click(screen.getByRole("button", { name: "English" })); fireEvent.click(retry); expect(screen.getByLabelText("language")).toHaveTextContent("en"); expect(window.localStorage.getItem("uiLanguage")).toBe("en"); expect(api.put).toHaveBeenCalledTimes(2); });