From 831c82f7a89d2a7865cd28a9ddf1fc71807f002c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Mon, 13 Jul 2026 22:15:22 +0800 Subject: [PATCH 2/2] test: align updater coverage with Guanghu policy diff --git a/src/hooks/useUpdater.test.ts b/src/hooks/useUpdater.test.ts index ad1a70fe0c54acf8f78aa9eeb5e00d07c5fd585d..cada95a14349a130cf4dd0dde01815bb7f088d4a 100644 --- a/src/hooks/useUpdater.test.ts +++ b/src/hooks/useUpdater.test.ts @@ -1,10 +1,6 @@ -import { renderHook, act } from '@testing-library/react' -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' +import { act, renderHook } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useUpdater } from './useUpdater' -import { - clearRestartRequiredAfterUpdate, - isRestartRequiredAfterUpdate, -} from '../lib/appUpdater' vi.mock('../mock-tauri', () => ({ isTauri: vi.fn(() => false), @@ -16,278 +12,68 @@ vi.mock('../utils/url', () => ({ })) const mockInvoke = vi.fn() - vi.mock('@tauri-apps/api/core', () => ({ invoke: (...args: unknown[]) => mockInvoke(...args), - Channel: class { - onmessage: (response: unknown) => void - - constructor(onmessage?: (response: unknown) => void) { - this.onmessage = onmessage ?? (() => {}) - } - }, })) import { isTauri } from '../mock-tauri' -interface AppUpdateMetadata { - currentVersion: string - version: string - date?: string - body?: string -} - -type DownloadArgs = { - releaseChannel: string - expectedVersion: string - onEvent: { - onmessage: ( - response: - | { event: 'Started'; data: { contentLength?: number } } - | { event: 'Progress'; data: { chunkLength: number } } - | { event: 'Finished' }, - ) => void - } -} - -function makeUpdate(overrides: Partial = {}): AppUpdateMetadata { - return { - currentVersion: '2026.4.15', - version: '2026.4.16', - body: 'Bug fixes and improvements', - ...overrides, - } -} - -function installInvokeHandlers({ - checkResult = null, - downloadImpl, -}: { - checkResult?: AppUpdateMetadata | null | Error - downloadImpl?: (args: DownloadArgs) => Promise -}) { - mockInvoke.mockImplementation((command: string, args?: unknown) => { - if (command === 'check_for_app_update') { - if (checkResult instanceof Error) return Promise.reject(checkResult) - return Promise.resolve(checkResult) - } - - if (command === 'download_and_install_app_update') { - if (downloadImpl) return downloadImpl(args as DownloadArgs) - return Promise.resolve(null) - } - - return Promise.resolve(null) - }) -} - -function renderUpdater(releaseChannel: string, automaticChecksEnabled = true) { - return renderHook(() => useUpdater(releaseChannel, automaticChecksEnabled)) -} - -async function performManualCheck( - releaseChannel: string, - checkResult: AppUpdateMetadata | null | Error, -) { - vi.mocked(isTauri).mockReturnValue(true) - installInvokeHandlers({ checkResult }) - - const hook = renderUpdater(releaseChannel) - let outcome: string | undefined - - await act(async () => { - outcome = await hook.result.current.actions.checkForUpdates() - }) - - return { result: hook.result, outcome } -} - -async function advanceAutoCheck() { - await act(async () => { - await vi.advanceTimersByTimeAsync(3500) - }) +function renderUpdater(automaticChecksEnabled = true) { + return renderHook(() => useUpdater('stable', automaticChecksEnabled)) } describe('useUpdater', () => { beforeEach(() => { vi.useFakeTimers() vi.clearAllMocks() - clearRestartRequiredAfterUpdate() - vi.spyOn(console, 'warn').mockImplementation(() => {}) }) afterEach(() => { vi.useRealTimers() - vi.restoreAllMocks() - }) - - it('starts in idle state', () => { - vi.mocked(isTauri).mockReturnValue(false) - - const { result } = renderUpdater('stable') - - expect(result.current.status).toEqual({ state: 'idle' }) }) - it('does not check for updates when not running in Tauri', async () => { - vi.mocked(isTauri).mockReturnValue(false) - - renderUpdater('stable') - await advanceAutoCheck() - - expect(mockInvoke).not.toHaveBeenCalled() + it('starts in an idle state', () => { + expect(renderUpdater().result.current.status).toEqual({ state: 'idle' }) }) - it('checks for stable updates after the startup delay', async () => { + it('never schedules an updater request in the Guanghu distribution', async () => { vi.mocked(isTauri).mockReturnValue(true) - installInvokeHandlers({ checkResult: null }) - renderUpdater('stable') - await advanceAutoCheck() - - expect(mockInvoke).toHaveBeenCalledWith('check_for_app_update', { - releaseChannel: 'stable', + renderUpdater() + await act(async () => { + await vi.advanceTimersByTimeAsync(3500) }) - }) - - it('does not automatically check for updates when automatic checks are disabled', async () => { - vi.mocked(isTauri).mockReturnValue(true) - installInvokeHandlers({ checkResult: makeUpdate() }) - - renderUpdater('stable', false) - await advanceAutoCheck() expect(mockInvoke).not.toHaveBeenCalled() }) - it('keeps manual update checks available when automatic checks are disabled', async () => { + it('keeps a manual check local and idle', async () => { vi.mocked(isTauri).mockReturnValue(true) - installInvokeHandlers({ checkResult: null }) - - const { result } = renderUpdater('stable', false) + const { result } = renderUpdater(false) let outcome: unknown await act(async () => { outcome = await result.current.actions.checkForUpdates() }) - expect(outcome).toEqual({ kind: 'up-to-date' }) - expect(mockInvoke).toHaveBeenCalledWith('check_for_app_update', { - releaseChannel: 'stable', - }) - }) - - it('transitions to available when an alpha update is found', async () => { - const { result } = await performManualCheck( - 'alpha', - makeUpdate({ version: '2026.4.16-alpha.3' }), - ) - - expect(result.current.status).toEqual({ - state: 'available', - version: '2026.4.16-alpha.3', - displayVersion: 'Alpha 2026.4.16.3', - notes: 'Bug fixes and improvements', - }) - - expect(mockInvoke).toHaveBeenCalledWith('check_for_app_update', { - releaseChannel: 'alpha', - }) - }) - - it('returns up-to-date when no update is available', async () => { - const { result, outcome } = await performManualCheck('stable', null) - expect(outcome).toEqual({ kind: 'up-to-date' }) expect(result.current.status).toEqual({ state: 'idle' }) + expect(mockInvoke).not.toHaveBeenCalled() }) - it('shows checking state while a manual update check is in flight', async () => { - vi.mocked(isTauri).mockReturnValue(true) - let resolveCheck: (value: AppUpdateMetadata | null) => void = () => {} - mockInvoke.mockImplementation((command: string) => { - if (command === 'check_for_app_update') { - return new Promise((resolve) => { - resolveCheck = resolve - }) - } - return Promise.resolve(null) - }) - - const { result } = renderUpdater('stable') + it('does not download or restart from a disabled update channel', async () => { + const { result } = renderUpdater() - let checkPromise: Promise | null = null await act(async () => { - checkPromise = result.current.actions.checkForUpdates() - await Promise.resolve() - }) - - expect(result.current.status).toEqual({ state: 'checking' }) - - await act(async () => { - resolveCheck(null) - expect(await checkPromise).toEqual({ kind: 'up-to-date' }) + await result.current.actions.startDownload() }) expect(result.current.status).toEqual({ state: 'idle' }) + expect(mockInvoke).not.toHaveBeenCalled() }) - it('returns available and sets status when an update exists', async () => { - const { result, outcome } = await performManualCheck( - 'stable', - makeUpdate({ body: undefined }), - ) - - expect(outcome).toEqual({ - kind: 'available', - version: '2026.4.16', - displayVersion: '2026.4.16', - }) - expect(result.current.status).toEqual({ - state: 'available', - version: '2026.4.16', - displayVersion: '2026.4.16', - notes: undefined, - }) - }) - - it('strips stable prerelease suffixes from the display version', async () => { - const { result } = await performManualCheck( - 'stable', - makeUpdate({ version: '2026.4.16-stable.1' }), - ) - - expect(result.current.status).toEqual({ - state: 'available', - version: '2026.4.16-stable.1', - displayVersion: '2026.4.16', - notes: 'Bug fixes and improvements', - }) - }) - - it('returns error when the update check fails', async () => { - const { result, outcome } = await performManualCheck( - 'stable', - new Error('network error'), - ) - - expect(outcome).toEqual({ - kind: 'error', - message: 'Could not check for updates: network error', - }) - expect(console.warn).toHaveBeenCalledWith('[updater] Failed to check for updates') - expect(result.current.status).toEqual({ state: 'error' }) - }) - - it('dismiss resets the banner state', async () => { - vi.mocked(isTauri).mockReturnValue(true) - installInvokeHandlers({ checkResult: makeUpdate() }) - - const { result } = renderUpdater('stable') - - await act(async () => { - await result.current.actions.checkForUpdates() - }) + it('keeps dismiss as a safe idle-state reset', () => { + const { result } = renderUpdater() act(() => { result.current.actions.dismiss() @@ -296,72 +82,13 @@ describe('useUpdater', () => { expect(result.current.status).toEqual({ state: 'idle' }) }) - it('openReleaseNotes opens the release notes page', () => { - vi.mocked(isTauri).mockReturnValue(false) - - const { result } = renderUpdater('stable') + it('opens release notes without enabling application updates', () => { + const { result } = renderUpdater() act(() => { result.current.actions.openReleaseNotes() }) - expect(mockOpenExternalUrl).toHaveBeenCalledWith( - 'https://tolaria.md/releases/' - ) - }) - - it('downloads and installs the available update with progress', async () => { - vi.mocked(isTauri).mockReturnValue(true) - installInvokeHandlers({ - checkResult: makeUpdate(), - downloadImpl: async (args) => { - expect(args.releaseChannel).toBe('stable') - expect(args.expectedVersion).toBe('2026.4.16') - args.onEvent.onmessage({ event: 'Started', data: { contentLength: 1000 } }) - args.onEvent.onmessage({ event: 'Progress', data: { chunkLength: 500 } }) - args.onEvent.onmessage({ event: 'Progress', data: { chunkLength: 500 } }) - args.onEvent.onmessage({ event: 'Finished' }) - }, - }) - - const { result } = renderUpdater('stable') - - await act(async () => { - await result.current.actions.checkForUpdates() - }) - - await act(async () => { - await result.current.actions.startDownload() - }) - - expect(result.current.status).toEqual({ - state: 'ready', - version: '2026.4.16', - displayVersion: '2026.4.16', - }) - expect(isRestartRequiredAfterUpdate()).toBe(true) - }) - - it('transitions to error when download fails', async () => { - vi.mocked(isTauri).mockReturnValue(true) - installInvokeHandlers({ - checkResult: makeUpdate(), - downloadImpl: async () => { - throw new Error('download failed') - }, - }) - - const { result } = renderUpdater('stable') - - await act(async () => { - await result.current.actions.checkForUpdates() - }) - - await act(async () => { - await result.current.actions.startDownload() - }) - - expect(console.warn).toHaveBeenCalledWith('[updater] Download failed') - expect(result.current.status).toEqual({ state: 'error' }) + expect(mockOpenExternalUrl).toHaveBeenCalledWith('https://tolaria.md/releases/') }) }) diff --git a/src/hooks/useUpdater.ts b/src/hooks/useUpdater.ts index c99843b66cc82d282ae96fee4b5b2115c7a6197b..f6bb8796f59995dc6417652f7a6206b89a03c2ea 100644 --- a/src/hooks/useUpdater.ts +++ b/src/hooks/useUpdater.ts @@ -10,6 +10,7 @@ import { formatCalendarVersionForDisplay } from '../utils/calendarVersion' import { openExternalUrl } from '../utils/url' const RELEASE_NOTES_URL = 'https://tolaria.md/releases/' +const GUANGHU_UPDATES_ENABLED = false interface UpdateVersionInfo { version: string @@ -99,12 +100,11 @@ export function useUpdater( ): { status: UpdateStatus; actions: UpdateActions } { // Guanghu is an independent open-source fork. Until Guanghu has its own // signed release feed, never contact or install from Tolaria's updater. - const updatesEnabled = false const [status, setStatus] = useState({ state: 'idle' }) const updateRef = useRef(null) const checkForUpdates = useCallback(async (): Promise => { - if (!updatesEnabled) return { kind: 'up-to-date' } + if (!GUANGHU_UPDATES_ENABLED) return { kind: 'up-to-date' } if (!isTauri()) return { kind: 'up-to-date' } setStatus({ state: 'checking' }) @@ -129,7 +129,7 @@ export function useUpdater( }, [releaseChannel]) useEffect(() => { - if (!updatesEnabled) return + if (!GUANGHU_UPDATES_ENABLED) return if (!automaticChecksEnabled) return if (!isTauri()) return const timer = setTimeout(() => { checkForUpdates() }, 3000) @@ -137,7 +137,7 @@ export function useUpdater( }, [automaticChecksEnabled, checkForUpdates]) const startDownload = useCallback(async () => { - if (!updatesEnabled) return + if (!GUANGHU_UPDATES_ENABLED) return const update = updateRef.current if (!update) return -- 2.50.1 (Apple Git-155)