mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
🐛 fix(update): 修复桌面端更新安装与重启流程
- macOS 下载完成后改为直接走安装更新链路,不再只打开安装目录 - Windows 更新脚本在重启时显式带上目标工作目录,提升安装后拉起稳定性 - 删除失效的 winget 发布工作流,并补充前端与脚本回归测试 Fixes #585
This commit is contained in:
22
.github/workflows/release-winget.yml
vendored
22
.github/workflows/release-winget.yml
vendored
@@ -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 }}
|
||||
@@ -4114,7 +4114,7 @@ function App() {
|
||||
) : null,
|
||||
isLatestUpdateDownloaded ? (
|
||||
<Button key="install-direct" type="primary" icon={<DownloadOutlined />} onClick={handleInstallFromProgress}>
|
||||
{isMacRuntime ? t('app.about.action.open_install_directory') : t('app.about.action.install_update')}
|
||||
{t('app.about.action.install_update')}
|
||||
</Button>
|
||||
) : null,
|
||||
].filter(Boolean)}
|
||||
@@ -5111,7 +5111,7 @@ function App() {
|
||||
] : (updateDownloadProgress.status === 'done' ? [
|
||||
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>,
|
||||
<Button key="install" type="primary" onClick={handleInstallFromProgress}>
|
||||
{isMacRuntime ? t('app.about.action.open_install_directory') : t('app.about.action.install_update')}
|
||||
{t('app.about.action.install_update')}
|
||||
</Button>
|
||||
] : (updateDownloadProgress.status === 'error' ? [
|
||||
<Button key="close" onClick={hideUpdateDownloadProgress}>{t('common.close')}</Button>
|
||||
|
||||
159
frontend/src/hooks/useAppUpdateManager.test.tsx
Normal file
159
frontend/src/hooks/useAppUpdateManager.test.tsx
Normal file
@@ -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<typeof vi.fn>;
|
||||
CheckForUpdatesSilently: ReturnType<typeof vi.fn>;
|
||||
DownloadUpdate: ReturnType<typeof vi.fn>;
|
||||
InstallUpdateAndRestart: ReturnType<typeof vi.fn>;
|
||||
OpenDownloadedUpdateDirectory: ReturnType<typeof vi.fn>;
|
||||
GetAppInfo: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
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<typeof useAppUpdateManager> | null = null;
|
||||
let renderer: ReactTestRenderer | null = null;
|
||||
|
||||
const t = (key: string, params?: Record<string, any>) => {
|
||||
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(<Harness />);
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user