410 lines
11 KiB
TypeScript
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', {})
|
|
})
|
|
})
|
|
})
|