274 lines
14 KiB
JavaScript
274 lines
14 KiB
JavaScript
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)/)
|
||
})
|