diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml deleted file mode 100644 index bba6b4b..0000000 --- a/.github/workflows/release-winget.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Publish to WinGet -on: - push: - tags: - - 'v*' - workflow_dispatch: - inputs: - release_tag: - required: true - description: 'Tag of release you want to publish' - type: string - -jobs: - publish: - runs-on: windows-2025-vs2026 - steps: - - uses: vedantmgoyal9/winget-releaser@v2 - with: - identifier: Syngnat.GoNavi - installers-regex: 'GoNavi-windows-(amd64|arm64)\.exe$' - release-tag: ${{ inputs.release_tag || github.ref_name }} - token: ${{ secrets.WINGET_TOKEN }} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 06dc72a..5aa0990 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4114,7 +4114,7 @@ function App() { ) : null, isLatestUpdateDownloaded ? ( ) : null, ].filter(Boolean)} @@ -5111,7 +5111,7 @@ function App() { ] : (updateDownloadProgress.status === 'done' ? [ , ] : (updateDownloadProgress.status === 'error' ? [ diff --git a/frontend/src/hooks/useAppUpdateManager.test.tsx b/frontend/src/hooks/useAppUpdateManager.test.tsx new file mode 100644 index 0000000..6b1296f --- /dev/null +++ b/frontend/src/hooks/useAppUpdateManager.test.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { act, create, type ReactTestRenderer } from 'react-test-renderer'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useAppUpdateManager } from './useAppUpdateManager'; + +const runtimeApi = vi.hoisted(() => ({ + EventsOn: vi.fn(() => vi.fn()), +})); + +const messageApi = vi.hoisted(() => ({ + info: vi.fn(), + success: vi.fn(), + error: vi.fn(), +})); + +vi.mock('../../wailsjs/runtime', () => runtimeApi); + +vi.mock('antd', () => ({ + message: messageApi, +})); + +type BackendAppMock = { + CheckForUpdates: ReturnType; + CheckForUpdatesSilently: ReturnType; + DownloadUpdate: ReturnType; + InstallUpdateAndRestart: ReturnType; + OpenDownloadedUpdateDirectory: ReturnType; + GetAppInfo: ReturnType; +}; + +const createBackendAppMock = (): BackendAppMock => ({ + CheckForUpdates: vi.fn(), + CheckForUpdatesSilently: vi.fn(), + DownloadUpdate: vi.fn(), + InstallUpdateAndRestart: vi.fn(), + OpenDownloadedUpdateDirectory: vi.fn(), + GetAppInfo: vi.fn(async () => ({ success: true, data: { version: '0.8.1', author: 'Syngnat' } })), +}); + +describe('useAppUpdateManager', () => { + let backendApp: BackendAppMock; + let hook: ReturnType | null = null; + let renderer: ReactTestRenderer | null = null; + + const t = (key: string, params?: Record) => { + if (params?.version) return `${key}:${params.version}`; + if (params?.path) return `${key}:${params.path}`; + if (params?.error) return `${key}:${params.error}`; + return key; + }; + + const renderHook = (isMacRuntime: boolean) => { + const Harness = () => { + hook = useAppUpdateManager({ + isMacRuntime, + runtimeBuildType: 'release', + t, + }); + return null; + }; + + act(() => { + renderer = create(); + }); + }; + + beforeEach(() => { + backendApp = createBackendAppMock(); + hook = null; + renderer = null; + runtimeApi.EventsOn.mockClear(); + messageApi.info.mockReset(); + messageApi.success.mockReset(); + messageApi.error.mockReset(); + vi.useFakeTimers(); + vi.stubGlobal('window', { + setTimeout, + clearTimeout, + setInterval, + clearInterval, + go: { + app: { + App: backendApp, + }, + }, + }); + }); + + afterEach(() => { + act(() => { + renderer?.unmount(); + }); + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + it('uses InstallUpdateAndRestart for downloaded macOS updates', async () => { + backendApp.CheckForUpdates.mockResolvedValue({ + success: true, + data: { + hasUpdate: true, + currentVersion: '0.8.1', + latestVersion: '0.8.2', + downloaded: true, + assetSize: 1024, + }, + }); + backendApp.InstallUpdateAndRestart.mockResolvedValue({ success: true }); + backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true }); + + renderHook(true); + + await act(async () => { + await hook?.checkForUpdates(false); + }); + + await act(async () => { + await hook?.handleInstallFromProgress(); + }); + + expect(backendApp.InstallUpdateAndRestart).toHaveBeenCalledTimes(1); + expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled(); + }); + + it('does not auto-open the downloaded macOS package directory after download succeeds', async () => { + backendApp.CheckForUpdates.mockResolvedValue({ + success: true, + data: { + hasUpdate: true, + currentVersion: '0.8.1', + latestVersion: '0.8.2', + downloaded: false, + assetSize: 2048, + }, + }); + backendApp.DownloadUpdate.mockResolvedValue({ + success: true, + data: { + downloadPath: '/Users/test/Desktop/GoNavi-0.8.2-MacOS-Arm64.dmg', + }, + }); + backendApp.OpenDownloadedUpdateDirectory.mockResolvedValue({ success: true }); + + renderHook(true); + + await act(async () => { + await hook?.checkForUpdates(false); + }); + + await act(async () => { + await hook?.downloadUpdate(hook?.lastUpdateInfo!, false); + }); + + expect(backendApp.DownloadUpdate).toHaveBeenCalledTimes(1); + expect(backendApp.OpenDownloadedUpdateDirectory).not.toHaveBeenCalled(); + expect(hook?.lastUpdateInfo?.downloaded).toBe(true); + }); +}); diff --git a/frontend/src/hooks/useAppUpdateManager.ts b/frontend/src/hooks/useAppUpdateManager.ts index d83e280..6f36d33 100644 --- a/frontend/src/hooks/useAppUpdateManager.ts +++ b/frontend/src/hooks/useAppUpdateManager.ts @@ -167,16 +167,6 @@ export const useAppUpdateManager = ({ void message.success({ content: t('app.about.message.download_completed'), duration: 2 }); } setAboutUpdateStatus(formatAboutUpdateStatus({ ...info, downloaded: true })); - if (isMacRuntime && !updateUserDismissedRef.current) { - try { - const openRes = await (window as any).go.app.App.OpenDownloadedUpdateDirectory(); - if (openRes?.success) { - void message.success(openRes?.message || t('app.about.message.install_directory_opened_manual_replace')); - } - } catch (e) { - console.warn('自动打开下载目录失败', e); - } - } } else { setUpdateDownloadProgress((prev) => ({ ...prev, @@ -218,28 +208,6 @@ export const useAppUpdateManager = ({ if (!canInstall) { return; } - if (isMacRuntime) { - const res = await (window as any).go.app.App.OpenDownloadedUpdateDirectory(); - if (!res?.success) { - void message.error(t('app.about.message.open_install_directory_failed_with_error', { error: res?.message || t('common.unknown') })); - updateDownloadedVersionRef.current = null; - updateDownloadMetaRef.current = null; - setUpdateDownloadProgress((prev) => ({ - ...prev, - status: 'idle', - percent: 0, - downloaded: 0, - open: false, - })); - setLastUpdateInfo((prev) => prev ? { ...prev, downloaded: false, downloadPath: undefined } : prev); - setAboutUpdateStatus((prev) => lastUpdateInfo ? formatAboutUpdateStatus({ ...lastUpdateInfo, downloaded: false, downloadPath: undefined }) : prev); - return; - } - updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null; - hideUpdateDownloadProgress(); - void message.success(res?.message || t('app.about.message.install_directory_opened_manual_replace')); - return; - } const res = await (window as any).go.app.App.InstallUpdateAndRestart(); if (!res?.success) { void message.error(t('app.about.message.install_failed_with_error', { error: res?.message || t('common.unknown') })); @@ -247,7 +215,7 @@ export const useAppUpdateManager = ({ } updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null; hideUpdateDownloadProgress(); - }, [formatAboutUpdateStatus, hideUpdateDownloadProgress, isMacRuntime, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]); + }, [hideUpdateDownloadProgress, lastUpdateInfo, updateDownloadProgress.status, updateDownloadProgress.version, t]); const checkForUpdates = useCallback(async (silent: boolean) => { if (updateCheckInFlightRef.current) return; diff --git a/internal/app/methods_update.go b/internal/app/methods_update.go index 7b6e909..73eb22b 100644 --- a/internal/app/methods_update.go +++ b/internal/app/methods_update.go @@ -1076,6 +1076,7 @@ if not exist "%SOURCE%" ( ) for %%I in ("%TARGET%") do set "TARGET_NAME=%%~nxI" +for %%I in ("%TARGET%") do set "TARGET_DIR=%%~dpI" for %%I in ("%SOURCE%") do set "SOURCE_EXT=%%~xI" set "SOURCE_EXE=" @@ -1161,10 +1162,10 @@ exit /b 1 :move_done del /F /Q "%TARGET_OLD%" >> "%LOG_FILE%" 2>&1 -start "" "%TARGET%" >> "%LOG_FILE%" 2>&1 +start "" /D "%TARGET_DIR%" "%TARGET%" >> "%LOG_FILE%" 2>&1 if %ERRORLEVEL% NEQ 0 ( call :log cmd start failed, trying powershell Start-Process - powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%'" >> "%LOG_FILE%" 2>&1 + powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'" >> "%LOG_FILE%" 2>&1 if !ERRORLEVEL! NEQ 0 ( call :log relaunch failed exit /b 1 diff --git a/internal/app/methods_update_windows_script_test.go b/internal/app/methods_update_windows_script_test.go index a8926b0..d3f58c5 100644 --- a/internal/app/methods_update_windows_script_test.go +++ b/internal/app/methods_update_windows_script_test.go @@ -102,7 +102,7 @@ func TestBuildWindowsScriptUsesDelayedErrorlevelInsideBlocks(t *testing.T) { for _, token := range []string{ `if !ERRORLEVEL! NEQ 0 (`, - `powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%'" >> "%LOG_FILE%" 2>&1`, + `powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'" >> "%LOG_FILE%" 2>&1`, `set "TARGET_OLD=%TARGET%.old"`, } { if !strings.Contains(script, token) { @@ -111,6 +111,26 @@ func TestBuildWindowsScriptUsesDelayedErrorlevelInsideBlocks(t *testing.T) { } } +func TestBuildWindowsScriptRelaunchUsesTargetDirectory(t *testing.T) { + script := buildWindowsScript( + `C:\tmp\GoNavi-v0.5.0-windows-amd64.exe`, + `C:\Program Files\GoNavi\GoNavi.exe`, + `C:\Program Files\GoNavi\.gonavi-update-windows-v0.5.0`, + `C:\Program Files\GoNavi\logs\update-install.log`, + 99999, + ) + + for _, token := range []string{ + `for %%I in ("%TARGET%") do set "TARGET_DIR=%%~dpI"`, + `start "" /D "%TARGET_DIR%" "%TARGET%" >> "%LOG_FILE%" 2>&1`, + `Start-Process -FilePath '%TARGET%' -WorkingDirectory '%TARGET_DIR%'`, + } { + if !strings.Contains(script, token) { + t.Fatalf("windows update relaunch missing token: %s\nscript:\n%s", token, script) + } + } +} + func TestBuildWindowsLaunchCommandUsesDirectHiddenCall(t *testing.T) { cmd := buildWindowsLaunchCommand(`C:\tmp\gonavi-update\update.cmd`)