mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-03 18:33:14 +08:00
feat: 增加更新页
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppUpdateState } from '../../src/shared/app-update'
|
||||
import { AppUpdatePrompt } from '../../src/renderer/src/features/app-update/AppUpdatePrompt'
|
||||
import { AboutPage } from '../../src/renderer/src/features/settings/pages/AboutPage'
|
||||
|
||||
let updateListener: ((state: AppUpdateState) => void) | undefined
|
||||
const api = {
|
||||
checkAppUpdate: vi.fn(),
|
||||
downloadAppUpdate: vi.fn(),
|
||||
getAppUpdateState: vi.fn(),
|
||||
installAppUpdate: vi.fn(),
|
||||
openAppUpdateDownloadPage: vi.fn(),
|
||||
onAppUpdateState: vi.fn((listener: (state: AppUpdateState) => void) => {
|
||||
updateListener = listener
|
||||
return vi.fn()
|
||||
}),
|
||||
revealAppLog: vi.fn()
|
||||
}
|
||||
|
||||
describe('app update UI', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
updateListener = undefined
|
||||
Object.defineProperty(window, 'api', { configurable: true, value: api })
|
||||
})
|
||||
|
||||
it('prompts once for a startup update and starts the download after navigation', async () => {
|
||||
const user = userEvent.setup()
|
||||
const state: AppUpdateState = {
|
||||
status: 'available',
|
||||
currentVersion: '1.9.0',
|
||||
delivery: 'automatic',
|
||||
version: '2.0.0',
|
||||
source: 'startup',
|
||||
isSimulation: true
|
||||
}
|
||||
api.getAppUpdateState.mockResolvedValue(state)
|
||||
api.downloadAppUpdate.mockResolvedValue({ success: true, state })
|
||||
const onDownloadStart = vi.fn()
|
||||
|
||||
render(<AppUpdatePrompt onDownloadStart={onDownloadStart} onNotice={vi.fn()} />)
|
||||
|
||||
await screen.findByRole('alertdialog', { name: '发现新版本 v2.0.0' })
|
||||
await user.click(screen.getByRole('button', { name: '立即下载' }))
|
||||
|
||||
expect(onDownloadStart).toHaveBeenCalledOnce()
|
||||
expect(api.downloadAppUpdate).toHaveBeenCalledOnce()
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByRole('alertdialog', { name: '发现新版本 v2.0.0' })
|
||||
).not.toBeInTheDocument()
|
||||
)
|
||||
})
|
||||
|
||||
it('renders live transfer details and keeps simulated install inside the app', async () => {
|
||||
const user = userEvent.setup()
|
||||
api.getAppUpdateState.mockResolvedValue({
|
||||
status: 'downloading',
|
||||
currentVersion: '1.9.0',
|
||||
delivery: 'automatic',
|
||||
version: '2.0.0',
|
||||
percent: 76,
|
||||
transferred: 45.6 * 1024 * 1024,
|
||||
total: 60 * 1024 * 1024,
|
||||
bytesPerSecond: 3.8 * 1024 * 1024,
|
||||
isSimulation: true
|
||||
})
|
||||
api.installAppUpdate.mockResolvedValue({
|
||||
success: true,
|
||||
simulated: true,
|
||||
message: '开发模拟模式:更新安装动作已模拟,未实际退出应用。'
|
||||
})
|
||||
const onNotice = vi.fn()
|
||||
render(<AboutPage onNotice={onNotice} />)
|
||||
|
||||
await screen.findByText('正在下载 v2.0.0')
|
||||
expect(screen.getByText('76%')).toBeVisible()
|
||||
expect(screen.getByText('45.6 MB / 60.0 MB')).toBeVisible()
|
||||
expect(screen.getByText('3.8 MB/s')).toBeVisible()
|
||||
|
||||
act(() => {
|
||||
updateListener?.({
|
||||
status: 'downloaded',
|
||||
currentVersion: '1.9.0',
|
||||
delivery: 'automatic',
|
||||
version: '2.0.0',
|
||||
percent: 100,
|
||||
transferred: 60 * 1024 * 1024,
|
||||
total: 60 * 1024 * 1024,
|
||||
isSimulation: true
|
||||
})
|
||||
})
|
||||
await user.click(screen.getByRole('button', { name: '立即重启更新' }))
|
||||
|
||||
expect(api.installAppUpdate).toHaveBeenCalledOnce()
|
||||
expect(onNotice).toHaveBeenCalledWith('开发模拟模式:更新安装动作已模拟,未实际退出应用。')
|
||||
})
|
||||
|
||||
it('opens the latest release page instead of downloading in unsigned macOS mode', async () => {
|
||||
const user = userEvent.setup()
|
||||
const state: AppUpdateState = {
|
||||
status: 'available',
|
||||
currentVersion: '2.2.2',
|
||||
delivery: 'release-page',
|
||||
version: '2.2.3',
|
||||
source: 'startup'
|
||||
}
|
||||
api.getAppUpdateState.mockResolvedValue(state)
|
||||
api.openAppUpdateDownloadPage.mockResolvedValue({ success: true })
|
||||
const onDownloadStart = vi.fn()
|
||||
|
||||
render(<AppUpdatePrompt onDownloadStart={onDownloadStart} onNotice={vi.fn()} />)
|
||||
await screen.findByRole('alertdialog', { name: '发现新版本 v2.2.3' })
|
||||
await user.click(screen.getByRole('button', { name: '前往下载' }))
|
||||
|
||||
expect(api.openAppUpdateDownloadPage).toHaveBeenCalledOnce()
|
||||
expect(api.downloadAppUpdate).not.toHaveBeenCalled()
|
||||
expect(onDownloadStart).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows a release-page action in About without progress UI', async () => {
|
||||
const user = userEvent.setup()
|
||||
api.getAppUpdateState.mockResolvedValue({
|
||||
status: 'available',
|
||||
currentVersion: '2.2.2',
|
||||
delivery: 'release-page',
|
||||
version: '2.2.3'
|
||||
})
|
||||
api.openAppUpdateDownloadPage.mockResolvedValue({ success: true })
|
||||
render(<AboutPage onNotice={vi.fn()} />)
|
||||
|
||||
await screen.findByText('发现新版本 v2.2.3')
|
||||
expect(screen.getByText('当前版本:v2.2.2')).toBeVisible()
|
||||
expect(screen.getByText('最新版本:v2.2.3')).toBeVisible()
|
||||
expect(screen.getByRole('button', { name: '前往下载' })).toBeVisible()
|
||||
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: '前往下载' }))
|
||||
expect(api.openAppUpdateDownloadPage).toHaveBeenCalledOnce()
|
||||
expect(api.downloadAppUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['up-to-date', '已是最新版本', '检查更新'],
|
||||
['error', '检查更新失败', '重试']
|
||||
] as const)('renders the %s manual-check result', async (status, message, action) => {
|
||||
api.getAppUpdateState.mockResolvedValue({
|
||||
status,
|
||||
currentVersion: '2.2.2',
|
||||
delivery: 'release-page',
|
||||
message
|
||||
})
|
||||
render(<AboutPage onNotice={vi.fn()} />)
|
||||
|
||||
await screen.findByText(message)
|
||||
expect(screen.getByRole('button', { name: action })).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -64,7 +64,7 @@ describe('FirstUseWelcome', () => {
|
||||
expect(onOpenAISettings).toHaveBeenCalledOnce()
|
||||
expect(screen.getByRole('link', { name: '查看完整使用教程' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://github.com/Wxw-Gu/WechatExplorer/blob/main/docs/user-guide/getting-started.md'
|
||||
'https://github.com/Wxw-Gu/TraceMemo/blob/main/docs/user-guide/getting-started.md'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,9 +20,16 @@ describe('basic settings pages', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
Object.defineProperty(window, 'api', { configurable: true, value: api })
|
||||
api.getAppUpdateState.mockResolvedValue({ status: 'idle', currentVersion: '2.2.2' })
|
||||
api.getAppUpdateState.mockResolvedValue({
|
||||
status: 'idle',
|
||||
currentVersion: '2.2.2',
|
||||
delivery: 'automatic'
|
||||
})
|
||||
api.onAppUpdateState.mockReturnValue(vi.fn())
|
||||
api.checkAppUpdate.mockResolvedValue(undefined)
|
||||
api.checkAppUpdate.mockResolvedValue({
|
||||
success: true,
|
||||
state: { status: 'up-to-date', currentVersion: '2.2.2' }
|
||||
})
|
||||
api.revealAppLog.mockResolvedValue(undefined)
|
||||
api.getSettings.mockResolvedValue({ settings: { debugEnabled: false } })
|
||||
api.setSettings.mockResolvedValue({ settings: { debugEnabled: true } })
|
||||
|
||||
@@ -340,6 +340,62 @@ test('SETTINGS-02 basic settings controls fit a narrow viewport and keep their s
|
||||
}
|
||||
})
|
||||
|
||||
test('UPDATE-01 simulated startup update navigates to live progress and never exits on install', async () => {
|
||||
const fixture = await launchTestApp({ updateSimulation: true })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.0.0' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await dialog.getByRole('button', { name: '立即下载' }).click()
|
||||
|
||||
await expect(fixture.page.getByRole('heading', { name: '关于' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('button', { name: '关于' })).toHaveClass(/active/)
|
||||
await expect(fixture.page.getByText('正在下载 v2.0.0')).toBeVisible()
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const value = await fixture.page
|
||||
.getByRole('progressbar', { name: /更新下载进度/ })
|
||||
.getAttribute('aria-valuenow')
|
||||
return Number(value)
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
await expect(fixture.page.getByText(/MB \/ 60\.0 MB/)).toBeVisible()
|
||||
await expect(fixture.page.getByText(/MB\/s/)).toBeVisible()
|
||||
|
||||
await expect(fixture.page.getByText('v2.0.0 已准备完成')).toBeVisible({ timeout: 5_000 })
|
||||
await fixture.page.getByRole('button', { name: '立即重启更新' }).click()
|
||||
await expect(
|
||||
fixture.page.getByText('开发模拟模式:更新安装动作已模拟,未实际退出应用。')
|
||||
).toBeVisible()
|
||||
await expect(fixture.page.getByRole('heading', { name: '关于' })).toBeVisible()
|
||||
expect(pageErrors).toEqual([])
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('UPDATE-02 unsigned macOS update opens the latest release without downloading', async () => {
|
||||
const fixture = await launchTestApp({ unsignedMacUpdate: true })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.2.3' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.getByText('有新版本可用,前往 GitHub 下载最新版本。')).toBeVisible()
|
||||
await dialog.getByRole('button', { name: '前往下载' }).click()
|
||||
|
||||
const openedUrl = await fixture.page.evaluate(() =>
|
||||
window.electron.ipcRenderer.invoke('app-update:getOpenedDownloadUrl')
|
||||
)
|
||||
expect(openedUrl).toBe('https://github.com/Wxw-Gu/TraceMemo/releases/latest')
|
||||
await expect(fixture.page.getByRole('progressbar')).toHaveCount(0)
|
||||
expect(pageErrors).toEqual([])
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('SETTINGS-03 database key actions keep destructive confirmation keyboard-safe', async () => {
|
||||
const fixture = await launchTestApp()
|
||||
const pageErrors: Error[] = []
|
||||
|
||||
@@ -15,6 +15,8 @@ const VALID_KEY = 'a'.repeat(64)
|
||||
const imageData = `data:image/png;base64,${fs.readFileSync(path.join(root, 'resources/icon.png')).toString('base64')}`
|
||||
const voiceData = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='
|
||||
const configuredNow = Number(process.env.WXE_E2E_NOW_MS)
|
||||
const updateSimulation = process.env.WXE_E2E_UPDATE_SIMULATION === '1'
|
||||
const unsignedMacUpdate = process.env.WXE_E2E_UNSIGNED_MAC_UPDATE === '1'
|
||||
const fixtureNowMs =
|
||||
Number.isFinite(configuredNow) && configuredNow > 0 ? configuredNow : Date.now()
|
||||
|
||||
@@ -801,7 +803,83 @@ handle('accounts:discover', (inputPath) =>
|
||||
]
|
||||
}
|
||||
)
|
||||
handle('app-update:getState', () => ({ status: 'idle', currentVersion: '2.2.0' }))
|
||||
let appUpdateState = updateSimulation
|
||||
? {
|
||||
status: 'available',
|
||||
currentVersion: '1.9.0',
|
||||
delivery: 'automatic',
|
||||
version: '2.0.0',
|
||||
source: 'startup',
|
||||
isSimulation: true
|
||||
}
|
||||
: unsignedMacUpdate
|
||||
? {
|
||||
status: 'available',
|
||||
currentVersion: '2.2.2',
|
||||
delivery: 'release-page',
|
||||
version: '2.2.3',
|
||||
source: 'startup'
|
||||
}
|
||||
: { status: 'idle', currentVersion: '2.2.0', delivery: 'automatic' }
|
||||
let openedUpdateDownloadUrl = ''
|
||||
handle('app-update:getState', () => appUpdateState)
|
||||
handle('app-update:openDownloadPage', () => {
|
||||
openedUpdateDownloadUrl = 'https://github.com/Wxw-Gu/TraceMemo/releases/latest'
|
||||
return { success: true }
|
||||
})
|
||||
handle('app-update:getOpenedDownloadUrl', () => openedUpdateDownloadUrl)
|
||||
handle('app-update:download', async () => {
|
||||
if (!updateSimulation) return { success: true, state: appUpdateState }
|
||||
const total = 60 * 1024 * 1024
|
||||
const steps = [0, 5, 12, 21, 33, 46, 58, 69, 78, 86, 93, 97, 100]
|
||||
let previousTransferred = 0
|
||||
appUpdateState = {
|
||||
...appUpdateState,
|
||||
status: 'downloading',
|
||||
percent: 0,
|
||||
transferred: 0,
|
||||
total,
|
||||
bytesPerSecond: 0
|
||||
}
|
||||
for (const percent of steps.slice(1)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150))
|
||||
const transferred = Math.round((total * percent) / 100)
|
||||
const bytesPerSecond = Math.round((transferred - previousTransferred) / 0.15)
|
||||
previousTransferred = transferred
|
||||
appUpdateState = {
|
||||
...appUpdateState,
|
||||
status: 'downloading',
|
||||
percent,
|
||||
transferred,
|
||||
total,
|
||||
bytesPerSecond
|
||||
}
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed()) window.webContents.send('app-update:state', appUpdateState)
|
||||
}
|
||||
}
|
||||
appUpdateState = {
|
||||
...appUpdateState,
|
||||
status: 'downloaded',
|
||||
percent: 100,
|
||||
transferred: total,
|
||||
total,
|
||||
bytesPerSecond: undefined
|
||||
}
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
if (!window.isDestroyed()) window.webContents.send('app-update:state', appUpdateState)
|
||||
}
|
||||
return { success: true, state: appUpdateState }
|
||||
})
|
||||
handle('app-update:install', async () =>
|
||||
updateSimulation
|
||||
? {
|
||||
success: true,
|
||||
simulated: true,
|
||||
message: '开发模拟模式:更新安装动作已模拟,未实际退出应用。'
|
||||
}
|
||||
: { success: true }
|
||||
)
|
||||
|
||||
for (const channel of [
|
||||
'export:start',
|
||||
@@ -817,8 +895,6 @@ for (const channel of [
|
||||
'image:selectDecoder',
|
||||
'image:openDecoderDownload',
|
||||
'app-update:check',
|
||||
'app-update:download',
|
||||
'app-update:install',
|
||||
'agent-hub:clearLogs',
|
||||
'agent-hub:startLogin',
|
||||
'agent-hub:cancelLogin',
|
||||
|
||||
@@ -21,6 +21,8 @@ export async function launchTestApp(
|
||||
largeContacts?: number
|
||||
corruptCache?: boolean
|
||||
aiFailure?: string
|
||||
updateSimulation?: boolean
|
||||
unsignedMacUpdate?: boolean
|
||||
now?: number
|
||||
appearanceTheme?: 'light' | 'dark'
|
||||
stableUserData?: string
|
||||
@@ -49,6 +51,8 @@ export async function launchTestApp(
|
||||
WXE_E2E_LARGE_CONTACTS: String(options.largeContacts || 0),
|
||||
WXE_E2E_CORRUPT_CACHE: options.corruptCache ? '1' : '0',
|
||||
WXE_E2E_AI_FAILURE: options.aiFailure || '',
|
||||
WXE_E2E_UPDATE_SIMULATION: options.updateSimulation ? '1' : '0',
|
||||
WXE_E2E_UNSIGNED_MAC_UPDATE: options.unsignedMacUpdate ? '1' : '0',
|
||||
WXE_E2E_NOW_MS: options.now ? String(options.now) : '',
|
||||
WXE_E2E_APPEARANCE_THEME: options.appearanceTheme || 'light'
|
||||
}
|
||||
|
||||
@@ -497,6 +497,72 @@ test('SETTINGS-02 basic settings controls visual @visual', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('UPDATE-01 startup prompt and downloaded state visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow, updateSimulation: true })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.0.0' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('update-available-dialog.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
|
||||
await dialog.getByRole('button', { name: '立即下载' }).click()
|
||||
await expect(fixture.page.getByText('v2.0.0 已准备完成')).toBeVisible({ timeout: 5_000 })
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
expect(pageErrors).toEqual([])
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('settings-update-downloaded.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('UPDATE-02 unsigned macOS release prompt visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow, unsignedMacUpdate: true })
|
||||
const pageErrors: Error[] = []
|
||||
fixture.page.on('pageerror', (error) => pageErrors.push(error))
|
||||
try {
|
||||
await fixture.setWindowContentSize(visualViewport)
|
||||
const dialog = fixture.page.getByRole('alertdialog', { name: '发现新版本 v2.2.3' })
|
||||
await expect(dialog).toBeVisible()
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('update-release-page-dialog.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
|
||||
await dialog.getByRole('button', { name: '取消' }).click()
|
||||
await fixture.page
|
||||
.getByRole('navigation', { name: '一级导航' })
|
||||
.getByRole('button', { name: '设置' })
|
||||
.click()
|
||||
await fixture.page.getByRole('button', { name: '关于' }).click()
|
||||
await expect(fixture.page.getByRole('button', { name: '前往下载' })).toBeVisible()
|
||||
await expect(fixture.page.getByRole('progressbar')).toHaveCount(0)
|
||||
expect(
|
||||
await fixture.page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
||||
).toBe(true)
|
||||
expect(pageErrors).toEqual([])
|
||||
await clearScreenshotFocus(fixture.page)
|
||||
await expect(fixture.page).toHaveScreenshot('settings-update-release-page.png', {
|
||||
animations: 'disabled',
|
||||
caret: 'hide'
|
||||
})
|
||||
} finally {
|
||||
await fixture.close()
|
||||
}
|
||||
})
|
||||
|
||||
test('SETTINGS-03 database key controls visual @visual', async () => {
|
||||
const fixture = await launchTestApp({ now: visualNow })
|
||||
const pageErrors: Error[] = []
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { APP_UPDATE_RELEASES_URL } from '../../src/shared/app-update'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const listeners = new Map<string, Set<(...args: unknown[]) => void>>()
|
||||
return {
|
||||
listeners,
|
||||
updater: {
|
||||
autoDownload: true,
|
||||
autoInstallOnAppQuit: false,
|
||||
checkForUpdates: vi.fn(),
|
||||
downloadUpdate: vi.fn(),
|
||||
quitAndInstall: vi.fn(),
|
||||
on: vi.fn((event: string, listener: (...args: unknown[]) => void) => {
|
||||
const eventListeners = listeners.get(event) || new Set()
|
||||
eventListeners.add(listener)
|
||||
listeners.set(event, eventListeners)
|
||||
})
|
||||
},
|
||||
shell: {
|
||||
openExternal: vi.fn()
|
||||
},
|
||||
emit(event: string, ...args: unknown[]): void {
|
||||
for (const listener of listeners.get(event) || []) listener(...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getVersion: () => '1.9.0',
|
||||
isPackaged: false
|
||||
},
|
||||
BrowserWindow: {
|
||||
getAllWindows: () => []
|
||||
},
|
||||
shell: mocks.shell
|
||||
}))
|
||||
|
||||
vi.mock('electron-updater', () => ({ autoUpdater: mocks.updater }))
|
||||
vi.mock('../../src/main/runtime-mode', () => ({ isPackagedRuntime: () => false }))
|
||||
|
||||
import { AppUpdateService } from '../../src/main/services/app-update-service'
|
||||
|
||||
describe('AppUpdateService', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
mocks.listeners.clear()
|
||||
})
|
||||
|
||||
it('keeps update-not-available as the final state when checkForUpdates returns metadata', async () => {
|
||||
const service = new AppUpdateService({ packagedRuntime: () => true, platform: 'win32' })
|
||||
mocks.updater.checkForUpdates.mockImplementationOnce(async () => {
|
||||
mocks.emit('checking-for-update')
|
||||
mocks.emit('update-not-available', { version: '1.9.0' })
|
||||
return { updateInfo: { version: '1.9.0' } }
|
||||
})
|
||||
|
||||
const result = await service.check('manual')
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.state).toMatchObject({
|
||||
status: 'up-to-date',
|
||||
currentVersion: '1.9.0',
|
||||
delivery: 'automatic',
|
||||
source: 'manual'
|
||||
})
|
||||
})
|
||||
|
||||
it('simulates a realistic 2.0.0 download without invoking the real updater', async () => {
|
||||
vi.useFakeTimers()
|
||||
const states: ReturnType<AppUpdateService['getState']>[] = []
|
||||
const service = new AppUpdateService({
|
||||
simulationEnabled: true,
|
||||
simulationDurationMs: 1_000,
|
||||
currentVersion: () => '1.9.0'
|
||||
})
|
||||
service.handleState((state) => states.push(state))
|
||||
|
||||
const checkPromise = service.check('startup')
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
const checkResult = await checkPromise
|
||||
expect(checkResult.state).toMatchObject({
|
||||
status: 'available',
|
||||
version: '2.0.0',
|
||||
source: 'startup',
|
||||
isSimulation: true
|
||||
})
|
||||
|
||||
const downloadPromise = service.download()
|
||||
await vi.advanceTimersByTimeAsync(1_100)
|
||||
const downloadResult = await downloadPromise
|
||||
|
||||
expect(downloadResult.state).toMatchObject({
|
||||
status: 'downloaded',
|
||||
version: '2.0.0',
|
||||
percent: 100,
|
||||
transferred: 60 * 1024 * 1024,
|
||||
total: 60 * 1024 * 1024
|
||||
})
|
||||
expect(
|
||||
states.filter((state) => state.status === 'downloading').map((state) => state.percent)
|
||||
).toEqual([0, 5, 12, 21, 33, 46, 58, 69, 78, 86, 93, 97, 100])
|
||||
expect(states.some((state) => (state.bytesPerSecond || 0) > 0)).toBe(true)
|
||||
expect(mocks.updater.downloadUpdate).not.toHaveBeenCalled()
|
||||
|
||||
expect(service.install()).toEqual({
|
||||
success: true,
|
||||
simulated: true,
|
||||
message: '开发模拟模式:更新安装动作已模拟,未实际退出应用。'
|
||||
})
|
||||
expect(mocks.updater.quitAndInstall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses release-page delivery on unsigned macOS without downloading or installing', async () => {
|
||||
const service = new AppUpdateService({
|
||||
platform: 'darwin',
|
||||
packagedRuntime: () => true,
|
||||
currentVersion: () => '2.2.2',
|
||||
macAutoUpdateEnabled: false
|
||||
})
|
||||
mocks.updater.checkForUpdates.mockImplementationOnce(async () => {
|
||||
mocks.emit('checking-for-update')
|
||||
mocks.emit('update-available', { version: '2.2.3' })
|
||||
return { updateInfo: { version: '2.2.3' } }
|
||||
})
|
||||
|
||||
const result = await service.check('manual')
|
||||
expect(result.state).toMatchObject({
|
||||
status: 'available',
|
||||
delivery: 'release-page',
|
||||
version: '2.2.3'
|
||||
})
|
||||
|
||||
const download = await service.download()
|
||||
expect(download.success).toBe(false)
|
||||
expect(download.state.status).toBe('available')
|
||||
expect(mocks.updater.downloadUpdate).not.toHaveBeenCalled()
|
||||
expect(service.install()).toMatchObject({ success: false })
|
||||
expect(mocks.updater.quitAndInstall).not.toHaveBeenCalled()
|
||||
|
||||
mocks.shell.openExternal.mockResolvedValue(undefined)
|
||||
await expect(service.openDownloadPage()).resolves.toEqual({ success: true })
|
||||
expect(mocks.shell.openExternal).toHaveBeenCalledWith(APP_UPDATE_RELEASES_URL)
|
||||
})
|
||||
|
||||
it('keeps automatic download and quitAndInstall on Windows', async () => {
|
||||
const service = new AppUpdateService({
|
||||
platform: 'win32',
|
||||
packagedRuntime: () => true,
|
||||
currentVersion: () => '2.2.2'
|
||||
})
|
||||
mocks.updater.checkForUpdates.mockImplementationOnce(async () => {
|
||||
mocks.emit('checking-for-update')
|
||||
mocks.emit('update-available', { version: '2.2.3' })
|
||||
return { updateInfo: { version: '2.2.3' } }
|
||||
})
|
||||
mocks.updater.downloadUpdate.mockImplementationOnce(async () => {
|
||||
mocks.emit('download-progress', {
|
||||
percent: 50,
|
||||
transferred: 30,
|
||||
total: 60,
|
||||
bytesPerSecond: 10
|
||||
})
|
||||
mocks.emit('update-downloaded', { version: '2.2.3' })
|
||||
})
|
||||
|
||||
await service.check('manual')
|
||||
const download = await service.download()
|
||||
expect(download.state).toMatchObject({
|
||||
status: 'downloaded',
|
||||
delivery: 'automatic',
|
||||
version: '2.2.3',
|
||||
percent: 100
|
||||
})
|
||||
expect(mocks.updater.downloadUpdate).toHaveBeenCalledOnce()
|
||||
|
||||
expect(service.install()).toEqual({ success: true })
|
||||
expect(mocks.updater.quitAndInstall).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user