feat(客户端更新): 登录页/首页更新面板显示更新包下载进度条

- 新增 shared/utils/update-progress.ts:进度事件解析/百分比钳制/文案格式化(纯函数)
- 新增 shared/components/UpdateProgressBar.vue:两页共用,失败态琥珀色并停在断点
  百分比、总大小未知走不确定态
- useVersionUpdate:先挂监听再发桥调用(避免丢首帧),监听 pywebview-update-progress
  (Python 侧 evaluate_js 推送,同 pywebview-download-progress 约定);失败保留上次进度
- 测试:tests/update-progress.test.ts(8 用例)

兼容:老客户端没有该事件源,进度条不出现,其余行为不变(优雅降级)。
注:本仓库 modules-withdraw / modules-image-video 有 3 个既存失败用例(隔离复现),
与本次改动无关;全仓库仅新增测试引用本次新增模块。
This commit is contained in:
2026-09-11 01:59:42 +08:00
parent 47b2758fe8
commit 94a0c287e0
6 changed files with 361 additions and 5 deletions
+120
View File
@@ -0,0 +1,120 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
formatBytes,
formatUpdateProgressText,
normalizeUpdateProgress,
updateProgressPercent,
type UpdateProgress,
} from '../src/shared/utils/update-progress.ts'
function progressOf(partial: Partial<UpdateProgress>): UpdateProgress {
return {
status: 'downloading',
message: '',
downloaded: 0,
total: 0,
percent: -1,
...partial,
}
}
test('更新进度:正常进度帧解析出字节数与百分比', () => {
const progress = normalizeUpdateProgress({
status: 'downloading',
message: '正在下载更新包...',
downloaded: 62914560,
total: 104857600,
percent: 60,
error: '',
})
assert.ok(progress, '合法事件必须解析成功')
assert.equal(progress.status, 'downloading')
assert.equal(progress.downloaded, 62914560)
assert.equal(progress.total, 104857600)
assert.equal(progress.percent, 60)
assert.equal(updateProgressPercent(progress), 60)
})
test('更新进度:状态缺失或非法的事件必须丢弃(不得把进度打回初始态)', () => {
assert.equal(normalizeUpdateProgress(null), null)
assert.equal(normalizeUpdateProgress(undefined), null)
assert.equal(normalizeUpdateProgress({}), null)
assert.equal(normalizeUpdateProgress({ downloaded: 100 }), null)
assert.equal(normalizeUpdateProgress({ status: 'unknown-stage' }), null)
assert.equal(normalizeUpdateProgress('downloading'), null)
})
test('更新进度:缺字段的脏事件按 0 字节/未知总大小兜底,不产生 NaN', () => {
const progress = normalizeUpdateProgress({ status: 'failed', message: '下载失败' })
assert.ok(progress)
assert.equal(progress.downloaded, 0)
assert.equal(progress.total, 0)
assert.equal(progress.percent, -1, '缺百分比按未知处理')
assert.equal(updateProgressPercent(progress), -1, '未知总大小走不确定态')
assert.equal(Number.isNaN(progress.downloaded), false)
})
test('更新进度:百分比越界钳制到 0-100,负值视为不确定态', () => {
assert.equal(updateProgressPercent(progressOf({ percent: 120 })), 100)
assert.equal(updateProgressPercent(progressOf({ percent: -5 })), -1)
assert.equal(updateProgressPercent(progressOf({ percent: 99.6 })), 100, '四舍五入取整')
assert.equal(updateProgressPercent(null), -1)
})
test('更新进度文案:下载中显示百分比与已下载/总大小', () => {
const text = formatUpdateProgressText(progressOf({
downloaded: 62914560,
total: 104857600,
percent: 60,
}))
assert.equal(text, '正在下载更新包 60%60.0 MB/100.0 MB')
})
test('更新进度文案:总大小未知时显示已下载量,刚开始时退回阶段文案', () => {
assert.equal(
formatUpdateProgressText(progressOf({ downloaded: 3145728, percent: -1 })),
'正在下载更新包(已下载 3.0 MB)',
)
assert.equal(
formatUpdateProgressText(progressOf({ message: '正在准备下载更新包...' })),
'正在准备下载更新包...',
)
})
test('更新进度文案:校验中/已就绪/失败直接用 Python 侧说明(失败停在断点处)', () => {
assert.equal(
formatUpdateProgressText(progressOf({
status: 'verifying',
message: '安装包下载完成,正在校验完整性...',
})),
'安装包下载完成,正在校验完整性...',
)
assert.equal(
formatUpdateProgressText(progressOf({
status: 'ready',
message: '安装包已就绪,正在启动更新程序,程序即将退出...',
percent: 100,
})),
'安装包已就绪,正在启动更新程序,程序即将退出...',
)
const failed = progressOf({
status: 'failed',
message: '下载中断(65536/307510 字节),断点已保留,请重试续传',
downloaded: 65536,
total: 307510,
percent: 21.3,
})
assert.equal(formatUpdateProgressText(failed), '下载中断(65536/307510 字节),断点已保留,请重试续传')
assert.equal(updateProgressPercent(failed), 21, '失败时进度条停在断点百分比')
assert.equal(formatUpdateProgressText(null), '')
})
test('更新进度文案:字节数格式化覆盖 KB/MB 与异常输入', () => {
assert.equal(formatBytes(0), '0 MB')
assert.equal(formatBytes(-1), '0 MB')
assert.equal(formatBytes(Number.NaN), '0 MB')
assert.equal(formatBytes(512 * 1024), '512 KB')
assert.equal(formatBytes(3 * 1024 * 1024), '3.0 MB')
})