feat: sync split translator shortcut display

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
JackCmd233 2026-04-28 01:17:15 +08:00
commit fa00a20b2c
2 changed files with 80 additions and 12 deletions

View file

@ -2,7 +2,8 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { afterEach, describe, expect, it, vi } from "vitest"
const i18nTMock = vi.hoisted(() => vi.fn((key: string, values?: unknown[]) => values ? `${key}:${values.join(",")}` : key))
const i18nTMock = vi.hoisted(() => vi.fn((key: string) => key))
const getExtensionCommandShortcutMock = vi.fn()
const openExtensionShortcutSettingsMock = vi.fn()
const toastErrorMock = vi.fn()
@ -26,6 +27,14 @@ vi.mock("@/components/ui/base-ui/button", () => ({
),
}))
vi.mock("@/components/ui/base-ui/input", () => ({
Input: (props: React.ComponentProps<"input">) => <input {...props} />,
}))
vi.mock("@/utils/extension-command-shortcut", () => ({
getExtensionCommandShortcut: (...args: unknown[]) => getExtensionCommandShortcutMock(...args),
}))
vi.mock("@/utils/navigation", () => ({
openExtensionShortcutSettings: (...args: unknown[]) => openExtensionShortcutSettingsMock(...args),
}))
@ -46,16 +55,35 @@ async function renderSplitTranslatorShortcut() {
}
describe("splitTranslatorShortcut", () => {
it("renders the split translator shortcut settings card", async () => {
it("renders the current split translator shortcut when one is configured", async () => {
getExtensionCommandShortcutMock.mockResolvedValue("Alt+S")
await renderSplitTranslatorShortcut()
expect(screen.getByText("options.translation.splitTranslatorShortcut.title")).toBeInTheDocument()
expect(screen.getByText("options.translation.splitTranslatorShortcut.description:Alt+S")).toBeInTheDocument()
expect(screen.getByText("options.translation.splitTranslatorShortcut.description")).toBeInTheDocument()
expect(await screen.findByDisplayValue("Alt+S")).toBeInTheDocument()
expect(screen.getByRole("button", { name: "options.translation.splitTranslatorShortcut.openSettings" })).toBeInTheDocument()
expect(i18nTMock).toHaveBeenCalledWith("options.translation.splitTranslatorShortcut.description", ["Alt+S"])
})
it("renders an unset label when the browser command has no shortcut", async () => {
getExtensionCommandShortcutMock.mockResolvedValue("")
await renderSplitTranslatorShortcut()
expect(await screen.findByDisplayValue("options.translation.splitTranslatorShortcut.unset")).toBeInTheDocument()
})
it("falls back to the unset label when reading the browser command fails", async () => {
getExtensionCommandShortcutMock.mockRejectedValue(new Error("blocked"))
await renderSplitTranslatorShortcut()
expect(await screen.findByDisplayValue("options.translation.splitTranslatorShortcut.unset")).toBeInTheDocument()
})
it("opens browser shortcut settings when clicked", async () => {
getExtensionCommandShortcutMock.mockResolvedValue("Alt+S")
openExtensionShortcutSettingsMock.mockResolvedValue(undefined)
await renderSplitTranslatorShortcut()
@ -68,6 +96,7 @@ describe("splitTranslatorShortcut", () => {
})
it("shows an error toast when browser shortcut settings cannot be opened", async () => {
getExtensionCommandShortcutMock.mockResolvedValue("Alt+S")
openExtensionShortcutSettingsMock.mockRejectedValue(new Error("blocked"))
await renderSplitTranslatorShortcut()

View file

@ -1,27 +1,66 @@
import { i18n } from "#imports"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/base-ui/button"
import { Input } from "@/components/ui/base-ui/input"
import { SPLIT_TRANSLATOR_COMMAND } from "@/entrypoints/background/split-translator-command"
import { getExtensionCommandShortcut } from "@/utils/extension-command-shortcut"
import { openExtensionShortcutSettings } from "@/utils/navigation"
import { formatPageTranslationShortcut } from "@/utils/page-translation-shortcut"
import { ConfigCard } from "../../components/config-card"
const DEFAULT_SPLIT_TRANSLATOR_SHORTCUT = "Alt+S"
export function SplitTranslatorShortcut() {
const t = i18n.t as (key: string, values?: string[]) => string
const [shortcut, setShortcut] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
void getExtensionCommandShortcut(SPLIT_TRANSLATOR_COMMAND)
.then((nextShortcut) => {
if (!cancelled) {
setShortcut(nextShortcut)
}
})
.catch(() => {
if (!cancelled) {
setShortcut("")
}
})
return () => {
cancelled = true
}
}, [])
const handleOpenShortcutSettings = () => {
void openExtensionShortcutSettings().catch(() => {
toast.error(i18n.t("options.translation.splitTranslatorShortcut.openFailed"))
toast.error(t("options.translation.splitTranslatorShortcut.openFailed"))
})
}
const displayShortcut = shortcut === null
? ""
: shortcut
? formatPageTranslationShortcut(shortcut)
: t("options.translation.splitTranslatorShortcut.unset")
return (
<ConfigCard
id="split-translator-shortcut"
title={i18n.t("options.translation.splitTranslatorShortcut.title")}
description={i18n.t("options.translation.splitTranslatorShortcut.description", [DEFAULT_SPLIT_TRANSLATOR_SHORTCUT])}
title={t("options.translation.splitTranslatorShortcut.title")}
description={t("options.translation.splitTranslatorShortcut.description")}
>
<Button type="button" variant="outline" onClick={handleOpenShortcutSettings}>
{i18n.t("options.translation.splitTranslatorShortcut.openSettings")}
</Button>
<div className="flex max-w-sm flex-col gap-3">
<Input
aria-label={t("options.translation.splitTranslatorShortcut.title")}
readOnly
value={displayShortcut}
/>
<Button type="button" variant="outline" onClick={handleOpenShortcutSettings}>
{t("options.translation.splitTranslatorShortcut.openSettings")}
</Button>
</div>
</ConfigCard>
)
}