Files
simple-react-app-kit/packages/i18n/src/devtool/message-panel.test.tsx
T

393 lines
12 KiB
TypeScript
Raw Normal View History

// @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(<MessagePanel locale="zh-Hans" repository={repository} />)
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:
2026-07-30 15:50:51 +08:00
| ((messages: readonly DevtoolMessage[]) => void)
| undefined
const repository: MessageRepository = {
getMessages: vi
.fn()
.mockResolvedValueOnce([message])
.mockImplementationOnce(
() =>
new Promise<readonly DevtoolMessage[]>((resolve) => {
resolveRefresh = resolve
})
),
updateMessage: vi.fn(),
}
const view = render(
<MessagePanel locale="zh-Hans" refreshToken={0} repository={repository} />
)
expect(await screen.findByText("navigation.home")).toBeTruthy()
fireEvent.change(screen.getByPlaceholderText("Search messages"), {
target: { value: "home" },
})
view.rerender(
<MessagePanel locale="zh-Hans" refreshToken={1} repository={repository} />
)
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(<MessagePanel locale="zh-Hans" repository={repository} />)
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(<MessagePanel locale="zh-Hans" repository={repository} />)
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(<MessagePanel locale="zh-Hans" repository={repository} />)
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(<MessagePanel locale="zh-Hans" repository={repository} />)
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(<MessagePanel locale="zh-Hans" repository={repository} />)
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(<MessagePanel locale="en" repository={repository} />)
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(<MessagePanel locale="zh-Hans" repository={repository} />)
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(<MessagePanel locale="zh-Hans" repository={repository} />)
expect(await screen.findByText("navigation.home")).toBeTruthy()
fireEvent.click(
screen.getByRole("checkbox", {
name: "Missing only",
})
)
expect(screen.getByText("All messages have translations.")).toBeTruthy()
})
})