jan/web-app/src/hooks/__tests__/useAppUpdater.test.ts
2025-09-30 21:48:38 +07:00

410 lines
11 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { useAppUpdater, UpdateState } from '../useAppUpdater'
// Mock dependencies
vi.mock('@/lib/utils', () => ({
isDev: vi.fn(() => false),
}))
vi.mock('@tauri-apps/plugin-updater', () => ({
check: vi.fn(),
}))
vi.mock('@janhq/core', () => ({
events: {
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
},
AppEvent: {
onAppUpdateDownloadUpdate: 'onAppUpdateDownloadUpdate',
onAppUpdateDownloadSuccess: 'onAppUpdateDownloadSuccess',
onAppUpdateDownloadError: 'onAppUpdateDownloadError',
},
}))
vi.mock('@tauri-apps/api/event', () => ({
emit: vi.fn(),
}))
vi.mock('@/types/events', () => ({
SystemEvent: {
KILL_SIDECAR: 'KILL_SIDECAR',
},
}))
// Mock the ServiceHub
const mockStopAllModels = vi.fn()
const mockUpdaterCheck = vi.fn()
const mockUpdaterDownloadAndInstall = vi.fn()
const mockUpdaterDownloadAndInstallWithProgress = vi.fn()
const mockEventsEmit = vi.fn()
vi.mock('@/hooks/useServiceHub', () => ({
getServiceHub: () => ({
models: () => ({
stopAllModels: mockStopAllModels,
}),
updater: () => ({
check: mockUpdaterCheck,
downloadAndInstall: mockUpdaterDownloadAndInstall,
downloadAndInstallWithProgress: mockUpdaterDownloadAndInstallWithProgress,
}),
events: () => ({
emit: mockEventsEmit,
}),
}),
}))
// Mock global window.core
Object.defineProperty(window, 'core', {
value: {
api: {
relaunch: vi.fn(),
},
},
writable: true,
})
// Mock global AUTO_UPDATER_DISABLED
Object.defineProperty(global, 'AUTO_UPDATER_DISABLED', {
value: false,
writable: true,
})
import { isDev } from '@/lib/utils'
import { check } from '@tauri-apps/plugin-updater'
import { events } from '@janhq/core'
import { emit } from '@tauri-apps/api/event'
describe('useAppUpdater', () => {
const mockEvents = events as any
const mockIsDev = isDev as any
const mockEmit = emit as any
const mockRelaunch = window.core?.api?.relaunch as any
beforeEach(() => {
vi.clearAllMocks()
mockIsDev.mockReturnValue(false)
})
afterEach(() => {
vi.resetAllMocks()
})
it('should initialize with default state', () => {
const { result } = renderHook(() => useAppUpdater())
expect(result.current.updateState).toEqual({
isUpdateAvailable: false,
updateInfo: null,
isDownloading: false,
downloadProgress: 0,
downloadedBytes: 0,
totalBytes: 0,
remindMeLater: false,
})
})
it('should set up event listeners for update state sync', () => {
renderHook(() => useAppUpdater())
expect(mockEvents.on).toHaveBeenCalledWith(
'onAppUpdateStateSync',
expect.any(Function)
)
})
it('should clean up event listeners on unmount', () => {
const { unmount } = renderHook(() => useAppUpdater())
unmount()
expect(mockEvents.off).toHaveBeenCalledWith(
'onAppUpdateStateSync',
expect.any(Function)
)
})
it('should handle app update state sync events', () => {
const { result } = renderHook(() => useAppUpdater())
// Get the handler function that was registered
const syncHandler = mockEvents.on.mock.calls[0][1]
act(() => {
syncHandler({ isUpdateAvailable: true, remindMeLater: true })
})
expect(result.current.updateState.isUpdateAvailable).toBe(true)
expect(result.current.updateState.remindMeLater).toBe(true)
})
describe('checkForUpdate', () => {
it('should check for updates and find an available update', async () => {
const mockUpdate = {
version: '1.2.0',
downloadAndInstall: vi.fn(),
}
mockUpdaterCheck.mockResolvedValue(mockUpdate)
const { result } = renderHook(() => useAppUpdater())
let updateResult: any
await act(async () => {
updateResult = await result.current.checkForUpdate()
})
expect(mockUpdaterCheck).toHaveBeenCalled()
expect(result.current.updateState.isUpdateAvailable).toBe(true)
expect(result.current.updateState.updateInfo).toBe(mockUpdate)
expect(result.current.updateState.remindMeLater).toBe(false)
expect(updateResult).toBe(mockUpdate)
})
it('should handle no update available', async () => {
mockUpdaterCheck.mockResolvedValue(null)
const { result } = renderHook(() => useAppUpdater())
let updateResult: any
await act(async () => {
updateResult = await result.current.checkForUpdate()
})
expect(result.current.updateState.isUpdateAvailable).toBe(false)
expect(result.current.updateState.updateInfo).toBe(null)
expect(updateResult).toBe(null)
})
it('should handle errors during update check', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
mockUpdaterCheck.mockRejectedValue(new Error('Network error'))
const { result } = renderHook(() => useAppUpdater())
let updateResult: any
await act(async () => {
updateResult = await result.current.checkForUpdate()
})
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error checking for updates:',
expect.any(Error)
)
expect(result.current.updateState.isUpdateAvailable).toBe(false)
expect(result.current.updateState.updateInfo).toBe(null)
expect(updateResult).toBe(null)
consoleErrorSpy.mockRestore()
})
it('should reset remindMeLater when requested', async () => {
mockUpdaterCheck.mockResolvedValue(null)
const { result } = renderHook(() => useAppUpdater())
// Set remindMeLater to true first
act(() => {
result.current.setRemindMeLater(true)
})
expect(result.current.updateState.remindMeLater).toBe(true)
await act(async () => {
await result.current.checkForUpdate(true)
})
expect(result.current.updateState.remindMeLater).toBe(false)
})
it('should skip update check in dev mode', async () => {
mockIsDev.mockReturnValue(true)
const { result } = renderHook(() => useAppUpdater())
let updateResult: any
await act(async () => {
updateResult = await result.current.checkForUpdate()
})
expect(mockUpdaterCheck).not.toHaveBeenCalled()
expect(result.current.updateState.isUpdateAvailable).toBe(false)
expect(updateResult).toBe(null)
})
})
describe('setRemindMeLater', () => {
it('should set remind me later state', () => {
const { result } = renderHook(() => useAppUpdater())
act(() => {
result.current.setRemindMeLater(true)
})
expect(result.current.updateState.remindMeLater).toBe(true)
act(() => {
result.current.setRemindMeLater(false)
})
expect(result.current.updateState.remindMeLater).toBe(false)
})
it('should sync remind me later state to other instances', () => {
const { result } = renderHook(() => useAppUpdater())
act(() => {
result.current.setRemindMeLater(true)
})
expect(mockEvents.emit).toHaveBeenCalledWith('onAppUpdateStateSync', {
remindMeLater: true,
})
})
})
describe('downloadAndInstallUpdate', () => {
it('should download and install update successfully', async () => {
const mockDownloadAndInstall = vi.fn()
const mockUpdate = {
version: '1.2.0',
downloadAndInstall: mockDownloadAndInstall,
}
// Mock check to return the update
mockUpdaterCheck.mockResolvedValue(mockUpdate)
const { result } = renderHook(() => useAppUpdater())
// Set update info first by calling checkForUpdate
await act(async () => {
await result.current.checkForUpdate()
})
// Mock the download and install process
mockUpdaterDownloadAndInstallWithProgress.mockImplementation(async (progressCallback) => {
// Simulate download events
progressCallback({
event: 'Started',
data: { contentLength: 1000 },
})
progressCallback({
event: 'Progress',
data: { chunkLength: 500 },
})
progressCallback({
event: 'Progress',
data: { chunkLength: 500 },
})
progressCallback({
event: 'Finished',
})
})
await act(async () => {
await result.current.downloadAndInstallUpdate()
})
expect(mockStopAllModels).toHaveBeenCalled()
expect(mockEventsEmit).toHaveBeenCalledWith('KILL_SIDECAR')
expect(mockUpdaterDownloadAndInstallWithProgress).toHaveBeenCalled()
expect(mockRelaunch).toHaveBeenCalled()
})
it('should handle download errors', async () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const mockDownloadAndInstall = vi.fn()
const mockUpdate = {
version: '1.2.0',
downloadAndInstall: mockDownloadAndInstall,
}
// Mock check to return the update
mockUpdaterCheck.mockResolvedValue(mockUpdate)
const { result } = renderHook(() => useAppUpdater())
// Set update info first by calling checkForUpdate
await act(async () => {
await result.current.checkForUpdate()
})
mockUpdaterDownloadAndInstallWithProgress.mockRejectedValue(new Error('Download failed'))
await act(async () => {
await result.current.downloadAndInstallUpdate()
})
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error downloading update:',
expect.any(Error)
)
expect(result.current.updateState.isDownloading).toBe(false)
expect(mockEvents.emit).toHaveBeenCalledWith('onAppUpdateDownloadError', {
message: 'Download failed',
})
consoleErrorSpy.mockRestore()
})
it('should not download if no update info is available', async () => {
const { result } = renderHook(() => useAppUpdater())
await act(async () => {
await result.current.downloadAndInstallUpdate()
})
expect(mockStopAllModels).not.toHaveBeenCalled()
})
it('should emit progress events during download', async () => {
const mockDownloadAndInstall = vi.fn()
const mockUpdate = {
version: '1.2.0',
downloadAndInstall: mockDownloadAndInstall,
}
// Mock check to return the update
mockUpdaterCheck.mockResolvedValue(mockUpdate)
const { result } = renderHook(() => useAppUpdater())
// Set update info first by calling checkForUpdate
await act(async () => {
await result.current.checkForUpdate()
})
mockUpdaterDownloadAndInstallWithProgress.mockImplementation(async (progressCallback) => {
progressCallback({
event: 'Started',
data: { contentLength: 2000 },
})
progressCallback({
event: 'Progress',
data: { chunkLength: 1000 },
})
progressCallback({
event: 'Finished',
})
})
await act(async () => {
await result.current.downloadAndInstallUpdate()
})
expect(mockEvents.emit).toHaveBeenCalledWith('onAppUpdateDownloadUpdate', {
progress: 0,
downloadedBytes: 0,
totalBytes: 2000,
})
expect(mockEvents.emit).toHaveBeenCalledWith('onAppUpdateDownloadUpdate', {
progress: 0.5,
downloadedBytes: 1000,
totalBytes: 2000,
})
expect(mockEvents.emit).toHaveBeenCalledWith('onAppUpdateDownloadSuccess', {})
})
})
})