Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Syngnat
2026-08-04 11:34:32 +08:00
3 changed files with 170 additions and 3 deletions

View File

@@ -2456,6 +2456,8 @@ function App() {
close: () => void;
isOpen: () => boolean;
} | null>(null);
// 手动「检查更新」发现新版本时,由 useAppUpdateManager 触发打开更新日志弹窗
const openReleaseNotesOnManualCheckRef = useRef<(() => void) | null>(null);
const {
aboutDisplayVersion,
aboutInfo,
@@ -2489,6 +2491,7 @@ function App() {
runtimeBuildType,
t,
updateCenterBridgeRef,
onManualCheckHasUpdateRef: openReleaseNotesOnManualCheckRef,
});
const [aboutLastCheckedAt, setAboutLastCheckedAt] = useState('');
const [releaseNotesModalOpen, setReleaseNotesModalOpen] = useState(false);
@@ -3637,6 +3640,14 @@ function App() {
updateCenterBridgeRef.current = null;
};
}, [handleOpenSettingsCenterPane]);
useEffect(() => {
openReleaseNotesOnManualCheckRef.current = () => {
setReleaseNotesModalOpen(true);
};
return () => {
openReleaseNotesOnManualCheckRef.current = null;
};
}, []);
useEffect(() => {
if (!isSettingsAboutPaneOpen) {
return;
@@ -5409,7 +5420,7 @@ function App() {
key="check"
icon={<CloudDownloadOutlined />}
loading={isCheckingForUpdates}
onClick={() => checkForUpdates(false)}
onClick={() => checkForUpdates(false, true)}
>
{t('app.about.action.check_updates')}
</Button>,

View File

@@ -669,6 +669,46 @@ describe('useAppUpdateManager', () => {
expect(hook?.lastUpdateInfo?.channel).toBe('dev');
});
it('does not invoke the manual-check bridge when a channel change re-check finds an update', async () => {
const openReleaseNotes = vi.fn();
const openReleaseNotesRef = { current: openReleaseNotes };
backendApp.SetUpdateChannel.mockResolvedValue({ success: true, data: { channel: 'dev' } });
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
hasUpdate: true,
channel: 'dev',
currentVersion: '0.8.1',
latestVersion: 'dev-a1b2c3d',
},
});
const Harness = () => {
hook = useAppUpdateManager({
runtimeBuildType: 'release',
t,
onManualCheckHasUpdateRef: openReleaseNotesRef,
});
return null;
};
act(() => {
renderer = create(<Harness />);
});
await act(async () => {
await hook?.changeUpdateChannel('dev');
});
expect(backendApp.SetUpdateChannel).toHaveBeenCalledWith('dev');
expect(backendApp.CheckForUpdates).toHaveBeenCalledTimes(1);
// 通道切换后的自动复查即便发现更新,也不应打开更新日志弹窗(#818 触发边界修正)
expect(openReleaseNotes).not.toHaveBeenCalled();
expect(hook?.lastUpdateInfo?.hasUpdate).toBe(true);
expect(hook?.lastUpdateInfo?.latestVersion).toBe('dev-a1b2c3d');
});
it('keeps release metadata from the backend update response', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
@@ -770,6 +810,114 @@ describe('useAppUpdateManager', () => {
expect(hook?.lastUpdateInfo?.latestVersion).toBe('0.8.2');
});
it('invokes the manual-check bridge when a manual check finds an update', async () => {
const openReleaseNotes = vi.fn();
const openReleaseNotesRef = { current: openReleaseNotes };
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
hasUpdate: true,
channel: 'latest',
currentVersion: '0.8.1',
latestVersion: '0.8.2',
releaseNotesUrl: 'https://github.com/Syngnat/GoNavi/releases/tag/v0.8.2',
},
});
const Harness = () => {
hook = useAppUpdateManager({
runtimeBuildType: 'release',
t,
onManualCheckHasUpdateRef: openReleaseNotesRef,
});
return null;
};
act(() => {
renderer = create(<Harness />);
});
await act(async () => {
await hook?.checkForUpdates(false, true);
});
expect(openReleaseNotes).toHaveBeenCalledTimes(1);
expect(hook?.lastUpdateInfo?.hasUpdate).toBe(true);
expect(hook?.lastUpdateInfo?.latestVersion).toBe('0.8.2');
});
it('does not invoke the manual-check bridge when a manual check finds no update', async () => {
const openReleaseNotes = vi.fn();
const openReleaseNotesRef = { current: openReleaseNotes };
backendApp.CheckForUpdates.mockResolvedValue({
success: true,
data: {
hasUpdate: false,
channel: 'latest',
currentVersion: '0.8.1',
latestVersion: '0.8.1',
},
});
const Harness = () => {
hook = useAppUpdateManager({
runtimeBuildType: 'release',
t,
onManualCheckHasUpdateRef: openReleaseNotesRef,
});
return null;
};
act(() => {
renderer = create(<Harness />);
});
await act(async () => {
await hook?.checkForUpdates(false, true);
});
expect(openReleaseNotes).not.toHaveBeenCalled();
expect(hook?.lastUpdateInfo?.hasUpdate).toBe(false);
expect(messageApi.success).toHaveBeenCalled();
});
it('does not invoke the manual-check bridge on silent update discovery', async () => {
const openReleaseNotes = vi.fn();
const openReleaseNotesRef = { current: openReleaseNotes };
backendApp.CheckForUpdatesSilently.mockResolvedValue({
success: true,
data: {
hasUpdate: true,
channel: 'latest',
currentVersion: '0.8.1',
latestVersion: '0.8.2',
},
});
const Harness = () => {
hook = useAppUpdateManager({
runtimeBuildType: 'release',
t,
onManualCheckHasUpdateRef: openReleaseNotesRef,
});
return null;
};
act(() => {
renderer = create(<Harness />);
});
await act(async () => {
await hook?.checkForUpdates(true);
});
expect(openReleaseNotes).not.toHaveBeenCalled();
expect(hook?.lastUpdateInfo?.hasUpdate).toBe(true);
});
it('opens the downloaded update directory when a package is already downloaded', async () => {
backendApp.CheckForUpdates.mockResolvedValue({
success: true,

View File

@@ -63,6 +63,8 @@ type UseAppUpdateManagerOptions = {
runtimeBuildType: string;
t: Translator;
updateCenterBridgeRef?: MutableRefObject<UpdateCenterBridge | null>;
/** 手动「检查更新」发现新版本时,触发打开更新日志弹窗的桥接回调 */
onManualCheckHasUpdateRef?: MutableRefObject<(() => void) | null>;
};
type AboutInfo = {
@@ -183,6 +185,7 @@ export const useAppUpdateManager = ({
runtimeBuildType,
t,
updateCenterBridgeRef,
onManualCheckHasUpdateRef,
}: UseAppUpdateManagerOptions) => {
const autoCheckForUpdates = useStore((state) => state.autoCheckForUpdates);
const autoCheckForUpdatesIntervalMinutes = useStore(
@@ -470,7 +473,7 @@ export const useAppUpdateManager = ({
void message.success(res?.message || t('app.about.message.install_directory_opened_manual_replace'));
}, [t]);
const checkForUpdates = useCallback(async (silent: boolean) => {
const checkForUpdates = useCallback(async (silent: boolean, openReleaseNotes = false) => {
if (updateCheckInFlightRef.current) return;
updateCheckInFlightRef.current = true;
setIsCheckingForUpdates(true);
@@ -565,6 +568,11 @@ export const useAppUpdateManager = ({
if (!silent) {
void message.info(t('app.about.message.new_version_found', { version: info.latestVersion }));
setAboutUpdateStatus(statusText);
// 仅当显式请求打开更新日志时(如用户点击「检查更新」按钮),才触发弹窗;
// 通道切换后的自动复查等场景不传 openReleaseNotes避免越界打开弹窗#818
if (openReleaseNotes) {
onManualCheckHasUpdateRef?.current?.();
}
}
if (silent && aboutOpen) {
setAboutUpdateStatus(statusText);
@@ -598,7 +606,7 @@ export const useAppUpdateManager = ({
} else {
setLastUpdateInfo(info);
}
}, [formatAboutUpdateStatus, isUpdateCenterOpen, openUpdateCenter, t]);
}, [formatAboutUpdateStatus, isUpdateCenterOpen, onManualCheckHasUpdateRef, openUpdateCenter, t]);
const loadAboutInfo = useCallback(async () => {
setAboutLoading(true);