// @vitest-environment jsdom import * as React from "react" import { act, cleanup, fireEvent, render, screen, waitFor, } from "@testing-library/react" import { afterEach, describe, expect, it, vi } from "vitest" import { MessagePanel } from "./message-panel" import type { DevtoolMessage, MessageRepository } from "./types" const message: DevtoolMessage = { catalog: "messages", comments: ["Navigation title"], editable: true, id: "navigation.home", missing: true, obsolete: false, origins: [{ file: "src/navigation.tsx", line: 12 }], packageName: "@workspace/ui", source: "Home", sourceLocale: "en", translation: "", } afterEach(() => { cleanup() vi.restoreAllMocks() Reflect.deleteProperty(window.navigator, "clipboard") }) describe("MessagePanel", () => { it("loads, filters, and saves messages through the repository", async () => { const updateMessage = vi.fn(async (input) => ({ ...message, missing: false, translation: input.translation, })) const repository: MessageRepository = { getMessages: vi.fn(async () => [message]), updateMessage, } render() expect(await screen.findByText("navigation.home")).toBeTruthy() expect(screen.getByRole("region", { name: "@workspace/ui" })).toBeTruthy() const textarea = screen.getByLabelText( "navigation.home translation for zh-Hans" ) expect(screen.queryByRole("button", { name: "Save" })).toBeNull() fireEvent.change(textarea, { target: { value: "首页" } }) fireEvent.click(screen.getByRole("button", { name: "Save" })) await waitFor(() => { expect(updateMessage).toHaveBeenCalledWith({ catalog: "messages", id: "navigation.home", locale: "zh-Hans", translation: "首页", }) }) expect(await screen.findByText("Saved")).toBeTruthy() const missingOnly = screen.getByRole("checkbox", { name: "Missing only", }) const checkboxIcon = missingOnly.nextElementSibling expect(checkboxIcon?.querySelector("svg")).toBeTruthy() fireEvent.click(missingOnly) expect(missingOnly).toHaveProperty("checked", true) fireEvent.change(screen.getByPlaceholderText("Search messages"), { target: { value: "does not exist" }, }) expect(screen.getByText("No messages match your search.")).toBeTruthy() }) it("keeps the current content visible during a background refresh", async () => { let resolveRefresh: | ((messages: readonly DevtoolMessage[]) => void) | undefined const repository: MessageRepository = { getMessages: vi .fn() .mockResolvedValueOnce([message]) .mockImplementationOnce( () => new Promise((resolve) => { resolveRefresh = resolve }) ), updateMessage: vi.fn(), } const view = render( ) expect(await screen.findByText("navigation.home")).toBeTruthy() fireEvent.change(screen.getByPlaceholderText("Search messages"), { target: { value: "home" }, }) view.rerender( ) expect(screen.queryByText("Loading zh-Hans…")).toBeNull() expect(screen.getByText("navigation.home")).toBeTruthy() expect(screen.getByPlaceholderText("Search messages")).toHaveProperty( "value", "home" ) await act(async () => { resolveRefresh?.([]) }) expect( await screen.findByText( "No messages are available yet. Extract the project to populate this catalog." ) ).toBeTruthy() }) it("collapses a package without discarding an unsaved translation", async () => { const repository: MessageRepository = { getMessages: vi.fn(async () => [message]), updateMessage: vi.fn(), } render() const textarea = await screen.findByLabelText( "navigation.home translation for zh-Hans" ) fireEvent.change(textarea, { target: { value: "未保存的首页" } }) const collapseButton = screen.getByRole("button", { name: "Collapse @workspace/ui", }) const packageContent = screen .getByRole("region", { name: "@workspace/ui" }) .querySelector(".i18n-message-package__messages") fireEvent.click(collapseButton) expect(collapseButton.getAttribute("aria-expanded")).toBe("false") expect(textarea).toHaveProperty("value", "未保存的首页") expect(packageContent).toHaveProperty("hidden", true) fireEvent.click( screen.getByRole("button", { name: "Expand @workspace/ui", }) ) expect(textarea).toHaveProperty("value", "未保存的首页") expect(packageContent).toHaveProperty("hidden", false) }) it("shows the application package even when it has no messages", async () => { const repository: MessageRepository = { getMessages: vi.fn(async () => [message]), getPackages: vi.fn(async () => [ { kind: "application" as const, name: "web", }, ]), updateMessage: vi.fn(), } render() expect(await screen.findByRole("region", { name: "web" })).toBeTruthy() expect( screen .getAllByRole("region") .map((region) => region.getAttribute("aria-label")) .filter((label) => label !== "Message editor") ).toEqual(["web", "@workspace/ui"]) expect( screen.getByText("No messages were found in this package.") ).toBeTruthy() expect(screen.getByRole("region", { name: "web" }).textContent).toContain( "0 messages" ) }) it("builds an ID tree and preserves visited editor drafts", async () => { const activeMessage: DevtoolMessage = { ...message, id: "blocks.appearance.active", source: "Active", } const darkMessage: DevtoolMessage = { ...message, id: "blocks.theme.dark", source: "Dark", } const repository: MessageRepository = { getMessages: vi.fn(async () => [activeMessage, darkMessage]), updateMessage: vi.fn(), } render() const activeEditor = await screen.findByLabelText( "blocks.appearance.active translation for zh-Hans" ) expect(screen.getByTitle("blocks")).toBeTruthy() expect(screen.getByTitle("blocks.appearance")).toBeTruthy() expect(screen.getByTitle("blocks.theme")).toBeTruthy() expect(screen.queryByTitle("blocks.appearance.active")).toBeNull() expect(screen.queryByTitle("blocks.theme.dark")).toBeNull() fireEvent.change(activeEditor, { target: { value: "使用中" } }) fireEvent.click(screen.getByTitle("blocks.theme")) expect( await screen.findByLabelText("blocks.theme.dark translation for zh-Hans") ).toBeTruthy() fireEvent.click(screen.getByTitle("blocks.appearance")) expect( await screen.findByLabelText( "blocks.appearance.active translation for zh-Hans" ) ).toHaveProperty("value", "使用中") }) it("uses the final ID segment as right-pane content instead of a tree leaf", async () => { const parentMessage: DevtoolMessage = { ...message, id: "a.b", source: "B", } const childMessage: DevtoolMessage = { ...message, id: "a.b.c", source: "C", } const repository: MessageRepository = { getMessages: vi.fn(async () => [parentMessage, childMessage]), updateMessage: vi.fn(), } render() expect( await screen.findByLabelText("a.b translation for zh-Hans") ).toBeTruthy() expect(screen.getByTitle("a")).toBeTruthy() expect(screen.getByTitle("a.b")).toBeTruthy() expect(screen.queryByTitle("a.b.c")).toBeNull() expect(screen.queryByLabelText("a.b.c translation for zh-Hans")).toBeNull() fireEvent.click(screen.getByTitle("a.b")) expect( await screen.findByLabelText("a.b.c translation for zh-Hans") ).toBeTruthy() }) it("resets an unsaved translation to the repository value", async () => { const updateMessage = vi.fn() const repository: MessageRepository = { getMessages: vi.fn(async () => [message]), updateMessage, } render() const textarea = await screen.findByLabelText( "navigation.home translation for zh-Hans" ) expect(screen.getByText("src/navigation.tsx:12")).toBeTruthy() fireEvent.change(textarea, { target: { value: "未保存的首页" } }) fireEvent.click(screen.getByRole("button", { name: "Reset" })) expect(textarea).toHaveProperty("value", "") expect(screen.queryByRole("button", { name: "Save" })).toBeNull() expect(updateMessage).not.toHaveBeenCalled() }) it("keeps application source messages read-only", async () => { const updateMessage = vi.fn() const repository: MessageRepository = { getMessages: vi.fn(async () => [ { ...message, editable: false, missing: false, packageName: "test-app", translation: "Home", }, ]), getPackages: vi.fn(async () => [ { kind: "application" as const, name: "test-app" }, ]), updateMessage, } render() const textarea = await screen.findByLabelText( "navigation.home translation for en" ) expect(textarea).toHaveProperty("readOnly", true) fireEvent.change(textarea, { target: { value: "Customized home" } }) expect(textarea).toHaveProperty("value", "Home") expect(screen.queryByRole("button", { name: "Save" })).toBeNull() expect(updateMessage).not.toHaveBeenCalled() }) it("copies an explicitly selected message value", async () => { const writeText = vi.fn(async () => undefined) Object.defineProperty(window.navigator, "clipboard", { configurable: true, value: { writeText }, }) const repository: MessageRepository = { getMessages: vi.fn(async () => [message]), updateMessage: vi.fn(), } render() await screen.findByText("navigation.home") fireEvent.click( screen.getByRole("button", { name: "Choose content to copy", }) ) expect( screen.getByRole("menuitem", { name: "Copy current translation" }) ).toHaveProperty("disabled", true) expect( screen.getByRole("menuitem", { name: /Copy source location/, }) ).toBeTruthy() fireEvent.click( screen.getByRole("menuitem", { name: "Copy message ID", }) ) await waitFor(() => { expect(writeText).toHaveBeenCalledWith("navigation.home") }) expect(screen.getByRole("button", { name: "Copied" })).toBeTruthy() expect(screen.queryByRole("menu")).toBeNull() expect(screen.getByRole("status").textContent).toContain("Copied") }) it("distinguishes an empty catalog from a fully translated catalog", async () => { const translatedMessage = { ...message, missing: false, translation: "首页", } const repository: MessageRepository = { getMessages: vi.fn(async () => [translatedMessage]), updateMessage: vi.fn(), } render() expect(await screen.findByText("navigation.home")).toBeTruthy() fireEvent.click( screen.getByRole("checkbox", { name: "Missing only", }) ) expect(screen.getByText("All messages have translations.")).toBeTruthy() }) })