Files
pda-overseas-uniapp/tests/app-update.test.mjs
T
2026-11-05 14:53:57 +08:00

274 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import { runInNewContext } from 'node:vm'
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
async function createUpdater({ development = true, packageName = 'io.dcloud.HBuilder',
manifest = { appid: '__UNI__936D079', versionName: '0.0.4', versionCode: 4 },
runtime = {}, resourceVersion = '0.0.4', response = { update: false }, acceptUpdate = false,
downloadResult = { statusCode: 200, tempFilePath: '_doc/temp/download/package' },
fileErrorAt, installError } = {}) {
const calls = { requests: [], modals: [], toasts: [], downloads: [], moves: [], installs: [], restarts: 0 }
const source = (await read('../src/utils/app-update.js'))
.replace(/^import .*$/gm, '')
.replaceAll('import.meta.env.VITE_PDA_UPDATE_URL', 'updateUrl')
.replaceAll('import.meta.env.DEV', 'development')
.replace('export async function checkForAppUpdate', 'async function checkForAppUpdate')
const updater = runInNewContext(`${source}\n;({ getRuntimeInfo, checkForAppUpdate })`, {
appManifest: manifest, development, updateUrl: 'https://updates.example.test/pda/overseas/latest',
plus: {
io: {
resolveLocalFileSystemURL(path, success, fail) {
const fileError = { code: 10, message: 'file operation failed' }
if (fileErrorAt === 'resolve') return fail(fileError)
const parent = {}
success({
getParent(success, fail) { fileErrorAt === 'parent' ? fail(fileError) : success(parent) },
moveTo(destination, name, success, fail) {
assert.equal(destination, parent)
calls.moves.push({ path, name })
if (fileErrorAt === 'move') return fail(fileError)
success({ toLocalURL: () => `_doc/temp/download/${name}` })
}
})
}
},
runtime: { appid: 'HBuilder', version: '5.24.0', versionCode: 52400,
getProperty(_appid, callback) { callback({ version: resourceVersion }) },
install(path, options, success, fail) {
calls.installs.push({ path, options })
if (installError) return fail(installError)
if (!/\.(wgt|apk)$/.test(path)) return fail({ code: -1201, message: 'WGT/WGTU文件格式错误' })
success()
},
restart() { calls.restarts += 1 }, ...runtime
}
},
uni: {
getAppBaseInfo: () => ({ packageName }),
request(options) {
calls.requests.push(options)
options.success({ statusCode: 200, data: typeof response === 'function' ? response(options.data) : response })
},
downloadFile(options) {
calls.downloads.push(options)
options.success(downloadResult)
options.complete()
return { onProgressUpdate() {} }
},
showLoading() {}, hideLoading() {},
showModal(options) {
calls.modals.push(options)
options.success?.({ confirm: acceptUpdate && options.confirmText === '立即更新' })
options.complete?.()
},
showToast(options) { calls.toasts.push(options) }
}
})
return { ...updater, calls }
}
const publishedApk = { update: true, appid: '__UNI__936D079', packageType: 'apk',
versionName: '0.0.5', versionCode: 5, minAppVersionCode: 5,
downloadUrl: 'https://updates.example.test/overseas-0.0.5.apk', mandatory: false }
const publishedWgt = { ...publishedApk, packageType: 'wgt', minAppVersionCode: 4,
downloadUrl: 'https://updates.example.test/overseas-0.0.5.wgt', releaseNotes: '更新测试' }
test('App-Plus checks the independent overseas update channel after launch', async () => {
const [app, updater, env] = await Promise.all([
read('../src/App.vue'),
read('../src/utils/app-update.js'),
read('../.env.example')
])
assert.match(app, /#ifdef APP-PLUS[\s\S]*?checkForAppUpdate\(\)/)
assert.match(env, /VITE_PDA_UPDATE_URL=https:\/\//)
assert.match(updater, /plus\.runtime\.getProperty/)
assert.match(updater, /appid:[\s\S]*?platform: 'android'[\s\S]*?appVersion:[\s\S]*?appVersionCode:[\s\S]*?wgtVersion:/)
})
test('overseas online update supports mandatory APK and wgt installation flows', async () => {
const updater = await read('../src/utils/app-update.js')
assert.match(updater, /\['apk', 'wgt'\]\.includes\(packageType\)/)
assert.match(updater, /update\.versionCode > runtime\.appVersionCode/)
assert.match(updater, /compareVersions\(update\.versionName, runtime\.wgtVersion\) > 0/)
assert.match(updater, /更新包必须使用 HTTPS 地址/)
assert.match(updater, /showCancel: !update\.mandatory/)
assert.match(updater, /uni\.downloadFile\([\s\S]*?onProgressUpdate/)
assert.match(updater, /plus\.runtime\.install/)
assert.match(updater, /packageType === 'wgt'[\s\S]*?plus\.runtime\.restart/)
assert.match(updater, /if \(retry\) await runUpdate\(update\)/)
})
test('overseas update manifest rejects a domestic AppID and incompatible WGT base', async () => {
const updater = await read('../src/utils/app-update.js')
const body = updater.match(/function normalizeUpdate\(payload, runtime\) \{[\s\S]*?\n\}/)?.[0]
assert.ok(body)
const normalize = new Function(`${body}; return normalizeUpdate`)()
const runtime = { appid: 'overseas-app', appVersionCode: 3 }
const manifest = { update: true, appid: 'overseas-app', packageType: 'wgt', downloadUrl: 'https://example.com/app.wgt', versionName: '1.0.4', minAppVersionCode: 3 }
assert.equal(normalize(manifest, runtime).minAppVersionCode, 3)
assert.throws(() => normalize({ ...manifest, appid: 'domestic-app' }, runtime), /不匹配/)
assert.throws(() => normalize({ ...manifest, minAppVersionCode: 4 }, runtime), /新版 APK/)
assert.throws(() => normalize({ ...manifest, minAppVersionCode: undefined }, runtime), /新版 APK/)
})
test('overseas profile exposes manual App update checking and the runtime version', async () => {
const profile = await read('../src/pages/profile/profile.vue')
assert.match(profile, /应用版本[\s\S]*?\{\{ appVersion \}\}/)
assert.match(profile, /class="profile-row profile-row-action" @tap="checkUpdate"[\s\S]*检查更新/)
assert.match(profile, /plus\.runtime\.version/)
assert.match(profile, /checkForAppUpdate\(\{ manual: true \}\)/)
assert.match(profile, /#ifndef APP-PLUS[\s\S]*?请在 PDA App 中检查更新/)
})
test('HBuilderX 0.0.4 checks the project version and offers a published 0.0.5 APK', async () => {
const updater = await createUpdater({ response: query => query.appVersionCode < 5 ? publishedApk : { update: false } })
await updater.checkForAppUpdate({ manual: true })
const query = updater.calls.requests[0].data
assert.equal(query.appid, '__UNI__936D079')
assert.equal(query.appVersion, '0.0.4')
assert.equal(query.appVersionCode, 4)
assert.equal(query.wgtVersion, '0.0.4')
assert.equal(updater.calls.modals[0]?.title, '发现更新 0.0.5')
assert.equal(updater.calls.toasts.length, 0)
})
test('standard HBuilder base uses project versions when running production WGT resources', async () => {
const updater = await createUpdater({ development: false })
const info = await updater.getRuntimeInfo()
assert.equal(info.appid, '__UNI__936D079')
assert.equal(info.appVersionCode, 4)
assert.equal(info.appVersion, '0.0.4')
})
test('released APK keeps its native base version after a WGT version increase', async () => {
const updater = await createUpdater({ development: false, packageName: 'com.baiyuelogistics.pda.overseas',
manifest: { appid: '__UNI__936D079', versionName: '0.0.5', versionCode: 5 },
runtime: { appid: '__UNI__936D079', version: '0.0.4', versionCode: 4 }, resourceVersion: '0.0.5' })
const info = await updater.getRuntimeInfo()
assert.equal(info.appVersionCode, 4)
assert.equal(info.appVersion, '0.0.4')
assert.equal(info.wgtVersion, '0.0.5')
})
test('HBuilderX 0.0.4 can detect a compatible published 0.0.5 WGT', async () => {
const updater = await createUpdater({ response: publishedWgt })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.requests[0].data.appVersionCode, 4)
assert.equal(updater.calls.requests[0].data.wgtVersion, '0.0.4')
assert.equal(updater.calls.modals[0]?.title, '发现更新 0.0.5')
assert.equal(updater.calls.modals[0]?.content, '更新测试')
})
test('released APK 0.0.4 shows the published 0.0.5 WGT release notes', async () => {
const updater = await createUpdater({ development: false, packageName: 'com.baiyuelogistics.pda.overseas',
runtime: { appid: '__UNI__936D079', version: '0.0.4', versionCode: 4 }, response: publishedWgt })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.modals[0]?.title, '发现更新 0.0.5')
assert.equal(updater.calls.modals[0]?.content, '更新测试')
})
test('an APK 0.0.4 with WGT 0.0.5 does not offer that WGT again', async () => {
const updater = await createUpdater({ development: false, packageName: 'com.baiyuelogistics.pda.overseas',
runtime: { appid: '__UNI__936D079', version: '0.0.4', versionCode: 4 },
resourceVersion: '0.0.5', response: publishedWgt })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.modals.length, 0)
assert.equal(updater.calls.toasts[0]?.title, '当前已是最新版本')
})
test('a WGT requiring APK 5 is rejected on a released APK 4 after resource updates', async () => {
const updater = await createUpdater({ development: false, packageName: 'com.baiyuelogistics.pda.overseas',
manifest: { appid: '__UNI__936D079', versionName: '0.0.5', versionCode: 5 },
runtime: { appid: '__UNI__936D079', version: '0.0.4', versionCode: 4 },
resourceVersion: '0.0.5', response: { ...publishedWgt, versionName: '0.0.6', minAppVersionCode: 5 } })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.modals.length, 0)
assert.equal(updater.calls.toasts[0]?.title, '当前安装包不支持此热更新,请安装新版 APK')
})
test('an unreadable native APK version is reported instead of claiming the latest version', async () => {
const updater = await createUpdater({ development: false, packageName: 'com.baiyuelogistics.pda.overseas',
runtime: { appid: '__UNI__936D079', version: '0.0.4', versionCode: 0 } })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.requests.length, 0)
assert.equal(updater.calls.toasts[0]?.title, '无法读取当前应用版本,请重新运行 App')
})
test('update service business errors are shown instead of claiming the latest version', async () => {
const updater = await createUpdater({ response: { code: 'S0503', message: 'PDA 发布通道未初始化', data: null } })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.toasts[0]?.title, 'PDA 发布通道未初始化')
assert.equal(updater.calls.modals.length, 0)
})
test('invalid update responses are rejected instead of claiming the latest version', async () => {
const updater = await createUpdater({ response: {} })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.toasts[0]?.title, '更新服务返回了无效的版本信息')
})
test('unpublished versions remain unavailable to the device', async () => {
const updater = await createUpdater({ response: { update: false } })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.modals.length, 0)
assert.equal(updater.calls.toasts[0]?.title, '当前已是最新版本')
})
test('a downloaded extensionless WGT is renamed before install and restarts on success', async () => {
const updater = await createUpdater({ response: publishedWgt, acceptUpdate: true })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.downloads[0]?.url, publishedWgt.downloadUrl)
assert.equal(updater.calls.moves[0]?.path, '_doc/temp/download/package')
assert.match(updater.calls.moves[0]?.name, /^overseas-pda-0\.0\.5-\d+\.wgt$/)
assert.equal(updater.calls.installs[0]?.path, `_doc/temp/download/${updater.calls.moves[0].name}`)
assert.equal(updater.calls.installs[0]?.options.force, false)
assert.equal(updater.calls.modals[1]?.title, '更新完成')
assert.equal(updater.calls.restarts, 1)
})
test('an extensionless APK receives the APK suffix and does not restart as a WGT', async () => {
const updater = await createUpdater({ response: publishedApk, acceptUpdate: true })
await updater.checkForAppUpdate({ manual: true })
assert.match(updater.calls.installs[0]?.path, /\.apk$/)
assert.equal(updater.calls.installs[0]?.options.force, false)
assert.equal(updater.calls.restarts, 0)
})
test('native installation errors remain visible with their actual reason and code', async () => {
const updater = await createUpdater({ response: publishedWgt, acceptUpdate: true,
installError: { code: -1205, message: 'WGT安装包中manifest.json文件的version版本不匹配' } })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.modals[1]?.title, '更新失败')
assert.match(updater.calls.modals[1]?.content, /-1205/)
assert.match(updater.calls.modals[1]?.content, /version版本不匹配/)
assert.equal(updater.calls.restarts, 0)
assert.equal(updater.calls.installs.length, 1)
})
test('unreadable or unrenamable downloads never reach the native installer', async () => {
for (const fileErrorAt of ['resolve', 'parent', 'move']) {
const updater = await createUpdater({ response: publishedWgt, acceptUpdate: true, fileErrorAt })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.installs.length, 0, fileErrorAt)
assert.equal(updater.calls.restarts, 0, fileErrorAt)
assert.equal(updater.calls.modals[1]?.title, '更新失败', fileErrorAt)
assert.match(updater.calls.modals[1]?.content, /更新包文件准备失败/, fileErrorAt)
}
})
test('an unsuccessful HTTP download never prepares or installs an update', async () => {
const updater = await createUpdater({ response: publishedWgt, acceptUpdate: true,
downloadResult: { statusCode: 404, tempFilePath: '_doc/temp/download/package' } })
await updater.checkForAppUpdate({ manual: true })
assert.equal(updater.calls.moves.length, 0)
assert.equal(updater.calls.installs.length, 0)
assert.match(updater.calls.toasts[0]?.title, /下载失败(404)/)
})