From ddbff070d57f4f5d07a0eac56523f50fe6ff29b2 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 20 Dec 2025 23:55:10 +0800 Subject: [PATCH] feat(settings): add option to skip Claude Code first-run confirmation Add a new setting to automatically skip Claude Code's onboarding screen by writing hasCompletedOnboarding=true to ~/.claude.json. The setting defaults to enabled for better user experience. - Add set/clear_has_completed_onboarding functions in claude_mcp.rs - Add Tauri commands and frontend API integration - Add toggle in WindowSettings with i18n support (en/zh/ja) - Fix hardcoded Chinese text in tests to use i18n keys --- src-tauri/src/claude_mcp.rs | 49 +++++++++++++ src-tauri/src/commands/plugin.rs | 12 ++++ src-tauri/src/lib.rs | 2 + src-tauri/src/settings.rs | 8 +++ src/components/settings/WindowSettings.tsx | 8 +++ src/hooks/useSettings.ts | 57 ++++++++++++++++ src/hooks/useSettingsForm.ts | 3 + src/i18n/locales/en.json | 4 ++ src/i18n/locales/ja.json | 4 ++ src/i18n/locales/zh.json | 4 ++ src/lib/api/settings.ts | 8 +++ src/lib/schemas/settings.ts | 1 + src/types.ts | 2 + tests/components/ImportExportSection.test.tsx | 2 +- tests/components/SettingsDialog.test.tsx | 6 +- tests/hooks/useSettings.test.tsx | 68 +++++++++++++++++++ tests/integration/SettingsDialog.test.tsx | 10 +-- tests/msw/handlers.ts | 4 ++ 18 files changed, 243 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/claude_mcp.rs b/src-tauri/src/claude_mcp.rs index 0369e91c..d17f75b0 100644 --- a/src-tauri/src/claude_mcp.rs +++ b/src-tauri/src/claude_mcp.rs @@ -105,6 +105,55 @@ pub fn read_mcp_json() -> Result, AppError> { Ok(Some(content)) } +/// 在 ~/.claude.json 根对象写入 hasCompletedOnboarding=true(用于跳过 Claude Code 初次安装确认) +/// 仅增量写入该字段,其他字段保持不变 +pub fn set_has_completed_onboarding() -> Result { + let path = user_config_path(); + let mut root = if path.exists() { + read_json_value(&path)? + } else { + serde_json::json!({}) + }; + + let obj = root + .as_object_mut() + .ok_or_else(|| AppError::Config("~/.claude.json 根必须是对象".into()))?; + + let already = obj + .get("hasCompletedOnboarding") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if already { + return Ok(false); + } + + obj.insert("hasCompletedOnboarding".into(), Value::Bool(true)); + write_json_value(&path, &root)?; + Ok(true) +} + +/// 删除 ~/.claude.json 根对象的 hasCompletedOnboarding 字段(恢复 Claude Code 初次安装确认) +/// 仅增量删除该字段,其他字段保持不变 +pub fn clear_has_completed_onboarding() -> Result { + let path = user_config_path(); + if !path.exists() { + return Ok(false); + } + + let mut root = read_json_value(&path)?; + let obj = root + .as_object_mut() + .ok_or_else(|| AppError::Config("~/.claude.json 根必须是对象".into()))?; + + let existed = obj.remove("hasCompletedOnboarding").is_some(); + if !existed { + return Ok(false); + } + + write_json_value(&path, &root)?; + Ok(true) +} + pub fn upsert_mcp_server(id: &str, spec: Value) -> Result { if id.trim().is_empty() { return Err(AppError::InvalidInput("MCP 服务器 ID 不能为空".into())); diff --git a/src-tauri/src/commands/plugin.rs b/src-tauri/src/commands/plugin.rs index 30fab6dc..31d803ce 100644 --- a/src-tauri/src/commands/plugin.rs +++ b/src-tauri/src/commands/plugin.rs @@ -34,3 +34,15 @@ pub async fn apply_claude_plugin_config(official: bool) -> Result pub async fn is_claude_plugin_applied() -> Result { crate::claude_plugin::is_claude_config_applied().map_err(|e| e.to_string()) } + +/// Claude Code:跳过初次安装确认(写入 ~/.claude.json 的 hasCompletedOnboarding=true) +#[tauri::command] +pub async fn apply_claude_onboarding_skip() -> Result { + crate::claude_mcp::set_has_completed_onboarding().map_err(|e| e.to_string()) +} + +/// Claude Code:恢复初次安装确认(删除 ~/.claude.json 的 hasCompletedOnboarding 字段) +#[tauri::command] +pub async fn clear_claude_onboarding_skip() -> Result { + crate::claude_mcp::clear_has_completed_onboarding().map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ce38de83..3143eb2c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -585,6 +585,8 @@ pub fn run() { commands::read_claude_plugin_config, commands::apply_claude_plugin_config, commands::is_claude_plugin_applied, + commands::apply_claude_onboarding_skip, + commands::clear_claude_onboarding_skip, // Claude MCP management commands::get_claude_mcp_status, commands::read_claude_mcp_config, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index bd239b5e..30f4c5de 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -31,6 +31,9 @@ pub struct AppSettings { /// 是否启用 Claude 插件联动 #[serde(default)] pub enable_claude_plugin_integration: bool, + /// 是否跳过 Claude Code 初次安装确认 + #[serde(default = "default_true")] + pub skip_claude_onboarding: bool, /// 是否开机自启 #[serde(default)] pub launch_on_startup: bool, @@ -65,12 +68,17 @@ fn default_minimize_to_tray_on_close() -> bool { true } +fn default_true() -> bool { + true +} + impl Default for AppSettings { fn default() -> Self { Self { show_in_tray: true, minimize_to_tray_on_close: true, enable_claude_plugin_integration: false, + skip_claude_onboarding: true, launch_on_startup: false, language: None, claude_config_dir: None, diff --git a/src/components/settings/WindowSettings.tsx b/src/components/settings/WindowSettings.tsx index 05121ccf..0acc0557 100644 --- a/src/components/settings/WindowSettings.tsx +++ b/src/components/settings/WindowSettings.tsx @@ -46,6 +46,14 @@ export function WindowSettings({ settings, onChange }: WindowSettingsProps) { onChange({ enableClaudePluginIntegration: value }) } /> + + } + title={t("settings.skipClaudeOnboarding")} + description={t("settings.skipClaudeOnboardingDescription")} + checked={!!settings.skipClaudeOnboarding} + onCheckedChange={(value) => onChange({ skipClaudeOnboarding: value })} + /> ); diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 149d1607..897b8e6f 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -160,6 +160,36 @@ export function useSettings(): UseSettingsResult { } } + // Claude Code 初次安装确认:开=写入 hasCompletedOnboarding=true;关=删除该字段 + // 仅在本次更新包含 skipClaudeOnboarding 时触发,避免其它自动保存误触发 + const nextSkipClaudeOnboarding = updates.skipClaudeOnboarding; + if ( + nextSkipClaudeOnboarding !== undefined && + nextSkipClaudeOnboarding !== (data?.skipClaudeOnboarding ?? false) + ) { + try { + if (nextSkipClaudeOnboarding) { + await settingsApi.applyClaudeOnboardingSkip(); + } else { + await settingsApi.clearClaudeOnboardingSkip(); + } + } catch (error) { + console.warn( + "[useSettings] Failed to sync Claude onboarding skip", + error, + ); + toast.error( + nextSkipClaudeOnboarding + ? t("notifications.skipClaudeOnboardingFailed", { + defaultValue: "跳过 Claude Code 初次安装确认失败", + }) + : t("notifications.clearClaudeOnboardingSkipFailed", { + defaultValue: "恢复 Claude Code 初次安装确认失败", + }), + ); + } + } + // 持久化语言偏好 try { if (typeof window !== "undefined" && updates.language) { @@ -242,6 +272,33 @@ export function useSettings(): UseSettingsResult { } } + // Claude Code 初次安装确认:开=写入 hasCompletedOnboarding=true;关=删除该字段 + const prevSkipClaudeOnboarding = data?.skipClaudeOnboarding ?? false; + const nextSkipClaudeOnboarding = payload.skipClaudeOnboarding ?? false; + if (nextSkipClaudeOnboarding !== prevSkipClaudeOnboarding) { + try { + if (nextSkipClaudeOnboarding) { + await settingsApi.applyClaudeOnboardingSkip(); + } else { + await settingsApi.clearClaudeOnboardingSkip(); + } + } catch (error) { + console.warn( + "[useSettings] Failed to sync Claude onboarding skip", + error, + ); + toast.error( + nextSkipClaudeOnboarding + ? t("notifications.skipClaudeOnboardingFailed", { + defaultValue: "跳过 Claude Code 初次安装确认失败", + }) + : t("notifications.clearClaudeOnboardingSkipFailed", { + defaultValue: "恢复 Claude Code 初次安装确认失败", + }), + ); + } + } + // 只在 Claude 插件集成状态真正改变时调用系统 API if ( payload.enableClaudePluginIntegration !== undefined && diff --git a/src/hooks/useSettingsForm.ts b/src/hooks/useSettingsForm.ts index c823992b..f4f09162 100644 --- a/src/hooks/useSettingsForm.ts +++ b/src/hooks/useSettingsForm.ts @@ -83,6 +83,7 @@ export function useSettingsForm(): UseSettingsFormResult { minimizeToTrayOnClose: data.minimizeToTrayOnClose ?? true, enableClaudePluginIntegration: data.enableClaudePluginIntegration ?? false, + skipClaudeOnboarding: data.skipClaudeOnboarding ?? true, claudeConfigDir: sanitizeDir(data.claudeConfigDir), codexConfigDir: sanitizeDir(data.codexConfigDir), language: normalizedLanguage, @@ -102,6 +103,7 @@ export function useSettingsForm(): UseSettingsFormResult { showInTray: true, minimizeToTrayOnClose: true, enableClaudePluginIntegration: false, + skipClaudeOnboarding: true, language: readPersistedLanguage(), } as SettingsFormState); @@ -136,6 +138,7 @@ export function useSettingsForm(): UseSettingsFormResult { minimizeToTrayOnClose: serverData.minimizeToTrayOnClose ?? true, enableClaudePluginIntegration: serverData.enableClaudePluginIntegration ?? false, + skipClaudeOnboarding: serverData.skipClaudeOnboarding ?? true, claudeConfigDir: sanitizeDir(serverData.claudeConfigDir), codexConfigDir: sanitizeDir(serverData.codexConfigDir), language: normalizedLanguage, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 321f0861..34ca1b52 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -130,6 +130,8 @@ "appliedToClaudePlugin": "Applied to Claude plugin", "removedFromClaudePlugin": "Removed from Claude plugin", "syncClaudePluginFailed": "Sync Claude plugin failed", + "skipClaudeOnboardingFailed": "Failed to skip Claude Code first-run confirmation", + "clearClaudeOnboardingSkipFailed": "Failed to restore Claude Code first-run confirmation", "updateSuccess": "Provider updated successfully", "updateFailed": "Failed to update provider: {{error}}", "deleteSuccess": "Provider deleted", @@ -211,6 +213,8 @@ "minimizeToTrayDescription": "When checked, clicking the close button will hide to system tray, otherwise the app will exit directly.", "enableClaudePluginIntegration": "Apply to Claude Code extension", "enableClaudePluginIntegrationDescription": "When enabled, the VS Code Claude Code extension provider will switch with this app", + "skipClaudeOnboarding": "Skip Claude Code first-run confirmation", + "skipClaudeOnboardingDescription": "When enabled, Claude Code will skip the first-run confirmation", "configDirectoryOverride": "Configuration Directory Override (Advanced)", "configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory to the one in WSL to keep provider data consistent with the main environment.", "appConfigDir": "CC Switch Configuration Directory", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index e12829d4..9ca5e909 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -130,6 +130,8 @@ "appliedToClaudePlugin": "Claude プラグインに適用しました", "removedFromClaudePlugin": "Claude プラグインから削除しました", "syncClaudePluginFailed": "Claude プラグインとの同期に失敗しました", + "skipClaudeOnboardingFailed": "Claude Code の初回確認スキップに失敗しました", + "clearClaudeOnboardingSkipFailed": "Claude Code の初回確認の復元に失敗しました", "updateSuccess": "プロバイダーを更新しました", "updateFailed": "プロバイダーの更新に失敗しました: {{error}}", "deleteSuccess": "プロバイダーを削除しました", @@ -211,6 +213,8 @@ "minimizeToTrayDescription": "チェックすると閉じるボタンでトレイに隠し、オフならアプリを終了します。", "enableClaudePluginIntegration": "Claude Code 拡張に適用", "enableClaudePluginIntegrationDescription": "オンにすると VS Code の Claude Code 拡張のプロバイダーも同期します", + "skipClaudeOnboarding": "Claude Code の初回確認をスキップ", + "skipClaudeOnboardingDescription": "オンにすると Claude Code の初回インストール確認をスキップします", "configDirectoryOverride": "設定ディレクトリの上書き(詳細)", "configDirectoryDescription": "WSL などで Claude Code や Codex を使う場合、ここで設定ディレクトリを WSL 側に合わせるとデータを揃えられます。", "appConfigDir": "CC Switch 設定ディレクトリ", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 7ddabe59..5976a0cf 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -130,6 +130,8 @@ "appliedToClaudePlugin": "已应用到 Claude 插件", "removedFromClaudePlugin": "已从 Claude 插件移除", "syncClaudePluginFailed": "同步 Claude 插件失败", + "skipClaudeOnboardingFailed": "跳过 Claude Code 初次安装确认失败", + "clearClaudeOnboardingSkipFailed": "恢复 Claude Code 初次安装确认失败", "updateSuccess": "供应商更新成功", "updateFailed": "更新供应商失败:{{error}}", "deleteSuccess": "供应商已删除", @@ -211,6 +213,8 @@ "minimizeToTrayDescription": "勾选后点击关闭按钮会隐藏到系统托盘,取消则直接退出应用。", "enableClaudePluginIntegration": "应用到 Claude Code 插件", "enableClaudePluginIntegrationDescription": "开启后 Vscode Claude Code 插件的供应商将随本软件切换", + "skipClaudeOnboarding": "跳过 Claude Code 初次安装确认", + "skipClaudeOnboardingDescription": "开启后跳过 Claude Code 初次安装确认", "configDirectoryOverride": "配置目录覆盖(高级)", "configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定为 WSL 里的配置目录,供应商数据与主环境保持一致。", "appConfigDir": "CC Switch 配置目录", diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 4a8035ef..b6428a0f 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -69,6 +69,14 @@ export const settingsApi = { return await invoke("apply_claude_plugin_config", { official }); }, + async applyClaudeOnboardingSkip(): Promise { + return await invoke("apply_claude_onboarding_skip"); + }, + + async clearClaudeOnboardingSkip(): Promise { + return await invoke("clear_claude_onboarding_skip"); + }, + async saveFileDialog(defaultName: string): Promise { return await invoke("save_file_dialog", { defaultName }); }, diff --git a/src/lib/schemas/settings.ts b/src/lib/schemas/settings.ts index 61e6b9da..91709830 100644 --- a/src/lib/schemas/settings.ts +++ b/src/lib/schemas/settings.ts @@ -12,6 +12,7 @@ export const settingsSchema = z.object({ showInTray: z.boolean(), minimizeToTrayOnClose: z.boolean(), enableClaudePluginIntegration: z.boolean().optional(), + skipClaudeOnboarding: z.boolean().optional(), launchOnStartup: z.boolean().optional(), language: z.enum(["en", "zh", "ja"]).optional(), diff --git a/src/types.ts b/src/types.ts index bdf46fcf..08815981 100644 --- a/src/types.ts +++ b/src/types.ts @@ -106,6 +106,8 @@ export interface Settings { minimizeToTrayOnClose: boolean; // 启用 Claude 插件联动(写入 ~/.claude/config.json 的 primaryApiKey) enableClaudePluginIntegration?: boolean; + // 跳过 Claude Code 初次安装确认(写入 ~/.claude.json 的 hasCompletedOnboarding) + skipClaudeOnboarding?: boolean; // 是否开机自启 launchOnStartup?: boolean; // 首选语言(可选,默认中文) diff --git a/tests/components/ImportExportSection.test.tsx b/tests/components/ImportExportSection.test.tsx index 1121699f..948c3fcd 100644 --- a/tests/components/ImportExportSection.test.tsx +++ b/tests/components/ImportExportSection.test.tsx @@ -63,7 +63,7 @@ describe("ImportExportSection Component", () => { fireEvent.click(importButton); expect(baseProps.onImport).toHaveBeenCalledTimes(1); - fireEvent.click(screen.getByRole("button", { name: "Clear selection" })); + fireEvent.click(screen.getByRole("button", { name: "common.clear" })); expect(baseProps.onClear).toHaveBeenCalledTimes(1); }); diff --git a/tests/components/SettingsDialog.test.tsx b/tests/components/SettingsDialog.test.tsx index 22b33144..2e801584 100644 --- a/tests/components/SettingsDialog.test.tsx +++ b/tests/components/SettingsDialog.test.tsx @@ -305,7 +305,7 @@ describe("SettingsPage Component", () => { }); fireEvent.click(screen.getByText("settings.tabAdvanced")); - fireEvent.click(screen.getByText("数据管理")); + fireEvent.click(screen.getByText("settings.advanced.data.title")); // 有文件时,点击导入按钮执行 importConfig fireEvent.click( @@ -319,7 +319,7 @@ describe("SettingsPage Component", () => { expect(importExportMock.exportConfig).toHaveBeenCalled(); // 清除选择按钮 - fireEvent.click(screen.getByRole("button", { name: "Clear selection" })); + fireEvent.click(screen.getByRole("button", { name: "common.clear" })); expect(importExportMock.clearSelection).toHaveBeenCalled(); }); @@ -412,7 +412,7 @@ describe("SettingsPage Component", () => { render(); fireEvent.click(screen.getByText("settings.tabAdvanced")); - fireEvent.click(screen.getByText("配置文件目录")); + fireEvent.click(screen.getByText("settings.advanced.configDir.title")); fireEvent.click(screen.getByText("browse-directory")); expect(settingsMock.browseDirectory).toHaveBeenCalledWith("claude"); diff --git a/tests/hooks/useSettings.test.tsx b/tests/hooks/useSettings.test.tsx index a3035011..2d5ef42f 100644 --- a/tests/hooks/useSettings.test.tsx +++ b/tests/hooks/useSettings.test.tsx @@ -7,6 +7,8 @@ const mutateAsyncMock = vi.fn(); const useSettingsQueryMock = vi.fn(); const setAppConfigDirOverrideMock = vi.fn(); const applyClaudePluginConfigMock = vi.fn(); +const applyClaudeOnboardingSkipMock = vi.fn(); +const clearClaudeOnboardingSkipMock = vi.fn(); const syncCurrentProvidersLiveMock = vi.fn(); const updateTrayMenuMock = vi.fn(); const toastErrorMock = vi.fn(); @@ -50,6 +52,10 @@ vi.mock("@/lib/api", () => ({ setAppConfigDirOverrideMock(...args), applyClaudePluginConfig: (...args: unknown[]) => applyClaudePluginConfigMock(...args), + applyClaudeOnboardingSkip: (...args: unknown[]) => + applyClaudeOnboardingSkipMock(...args), + clearClaudeOnboardingSkip: (...args: unknown[]) => + clearClaudeOnboardingSkipMock(...args), syncCurrentProvidersLive: (...args: unknown[]) => syncCurrentProvidersLiveMock(...args), }, @@ -63,6 +69,7 @@ const createSettingsFormMock = (overrides: Record = {}) => ({ showInTray: true, minimizeToTrayOnClose: true, enableClaudePluginIntegration: false, + skipClaudeOnboarding: true, claudeConfigDir: "/claude", codexConfigDir: "/codex", language: "zh", @@ -111,6 +118,8 @@ describe("useSettings hook", () => { useSettingsQueryMock.mockReset(); setAppConfigDirOverrideMock.mockReset(); applyClaudePluginConfigMock.mockReset(); + applyClaudeOnboardingSkipMock.mockReset(); + clearClaudeOnboardingSkipMock.mockReset(); syncCurrentProvidersLiveMock.mockReset(); toastErrorMock.mockReset(); toastSuccessMock.mockReset(); @@ -120,6 +129,7 @@ describe("useSettings hook", () => { showInTray: true, minimizeToTrayOnClose: true, enableClaudePluginIntegration: false, + skipClaudeOnboarding: true, claudeConfigDir: "/server/claude", codexConfigDir: "/server/codex", language: "zh", @@ -142,6 +152,64 @@ describe("useSettings hook", () => { mutateAsyncMock.mockResolvedValue(true); setAppConfigDirOverrideMock.mockResolvedValue(true); applyClaudePluginConfigMock.mockResolvedValue(true); + applyClaudeOnboardingSkipMock.mockResolvedValue(true); + clearClaudeOnboardingSkipMock.mockResolvedValue(true); + }); + + it("auto-saves and applies Claude onboarding skip when toggled on", async () => { + serverSettings = { + ...serverSettings, + skipClaudeOnboarding: false, + }; + useSettingsQueryMock.mockReturnValue({ + data: serverSettings, + isLoading: false, + }); + + settingsFormMock = createSettingsFormMock({ + settings: { + ...serverSettings, + language: "zh", + skipClaudeOnboarding: false, + }, + }); + + const { result } = renderHook(() => useSettings()); + + await act(async () => { + await result.current.autoSaveSettings({ skipClaudeOnboarding: true }); + }); + + expect(applyClaudeOnboardingSkipMock).toHaveBeenCalledTimes(1); + expect(toastErrorMock).not.toHaveBeenCalled(); + }); + + it("auto-saves and clears Claude onboarding skip when toggled off", async () => { + serverSettings = { + ...serverSettings, + skipClaudeOnboarding: true, + }; + useSettingsQueryMock.mockReturnValue({ + data: serverSettings, + isLoading: false, + }); + + settingsFormMock = createSettingsFormMock({ + settings: { + ...serverSettings, + language: "zh", + skipClaudeOnboarding: true, + }, + }); + + const { result } = renderHook(() => useSettings()); + + await act(async () => { + await result.current.autoSaveSettings({ skipClaudeOnboarding: false }); + }); + + expect(clearClaudeOnboardingSkipMock).toHaveBeenCalledTimes(1); + expect(toastErrorMock).not.toHaveBeenCalled(); }); it("saves settings and flags restart when app config directory changes", async () => { diff --git a/tests/integration/SettingsDialog.test.tsx b/tests/integration/SettingsDialog.test.tsx index 0ebd3eb0..0772e4a8 100644 --- a/tests/integration/SettingsDialog.test.tsx +++ b/tests/integration/SettingsDialog.test.tsx @@ -150,7 +150,7 @@ describe("SettingsPage integration", () => { expect(screen.getByText("language:zh")).toBeInTheDocument(), ); fireEvent.click(screen.getByText("settings.tabAdvanced")); - fireEvent.click(screen.getByText("配置文件目录")); + fireEvent.click(screen.getByText("settings.advanced.configDir.title")); const appInput = await screen.findByPlaceholderText( "settings.browsePlaceholderApp", ); @@ -166,7 +166,7 @@ describe("SettingsPage integration", () => { ); fireEvent.click(screen.getByText("settings.tabAdvanced")); - fireEvent.click(screen.getByText("数据管理")); + fireEvent.click(screen.getByText("settings.advanced.data.title")); fireEvent.click(screen.getByText("settings.selectConfigFile")); await waitFor(() => expect(screen.getByTestId("selected-file").textContent).toContain( @@ -190,7 +190,7 @@ describe("SettingsPage integration", () => { ); fireEvent.click(screen.getByText("settings.tabAdvanced")); - fireEvent.click(screen.getByText("配置文件目录")); + fireEvent.click(screen.getByText("settings.advanced.configDir.title")); const appInput = await screen.findByPlaceholderText( "settings.browsePlaceholderApp", ); @@ -217,7 +217,7 @@ describe("SettingsPage integration", () => { ); fireEvent.click(screen.getByText("settings.tabAdvanced")); - fireEvent.click(screen.getByText("配置文件目录")); + fireEvent.click(screen.getByText("settings.advanced.configDir.title")); const browseButtons = screen.getAllByTitle("settings.browseDirectory"); const resetButtons = screen.getAllByTitle("settings.resetDefault"); @@ -257,7 +257,7 @@ describe("SettingsPage integration", () => { expect(screen.getByText("language:zh")).toBeInTheDocument(), ); fireEvent.click(screen.getByText("settings.tabAdvanced")); - fireEvent.click(screen.getByText("数据管理")); + fireEvent.click(screen.getByText("settings.advanced.data.title")); server.use( http.post("http://tauri.local/save_file_dialog", () => diff --git a/tests/msw/handlers.ts b/tests/msw/handlers.ts index 3d5461d5..df637d66 100644 --- a/tests/msw/handlers.ts +++ b/tests/msw/handlers.ts @@ -176,6 +176,10 @@ export const handlers = [ }, ), + http.post(`${TAURI_ENDPOINT}/apply_claude_onboarding_skip`, () => success(true)), + + http.post(`${TAURI_ENDPOINT}/clear_claude_onboarding_skip`, () => success(true)), + http.post(`${TAURI_ENDPOINT}/get_config_dir`, async ({ request }) => { const { app } = await withJson<{ app: AppId }>(request); return success(app === "claude" ? "/default/claude" : "/default/codex");