This commit is contained in:
2026-11-05 14:53:57 +08:00
parent 173bbe2d02
commit cc280338f1
5 changed files with 317 additions and 16 deletions
+218
View File
@@ -1,9 +1,82 @@
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'),
@@ -53,3 +126,148 @@ test('overseas profile exposes manual App update checking and the runtime versio
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)/)
})
+31 -2
View File
@@ -1,7 +1,11 @@
import test from 'node:test'
import { runInNewContext } from 'node:vm'
import assert from 'node:assert/strict'
import { readFile, readdir } from 'node:fs/promises'
import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { loadEnv } from 'vite'
const read = path => readFile(new URL(path, import.meta.url), 'utf8')
test('PDA H5 has no container publishing entry', async () => {
const files = await readdir(new URL('../deploy/', import.meta.url))
@@ -28,10 +32,12 @@ test('host nginx serves uploaded files and proxies only the API prefix', async (
test('only H5 production overrides the toolchain asset base', async () => {
const source = (await read('../vite.config.js'))
.replace(/^import .*$/gm, '')
.replaceAll('import.meta.url', 'configUrl')
.replace('export default ', '')
for (const platform of ['h5', 'app', 'app-plus', 'app-harmony', undefined]) {
for (const mode of ['production', 'development']) {
const factory = runInNewContext(source, {
URL, fileURLToPath, configUrl: new URL('../vite.config.js', import.meta.url).href,
defineConfig: value => value,
loadEnv: () => ({}),
uni: () => ({}),
@@ -45,8 +51,10 @@ test('only H5 production overrides the toolchain asset base', async () => {
})
test('H5 proxy uses the shared gateway prefix and preserves API suffixes', async () => {
const source = (await read('../vite.config.js')).replace(/^import .*$/gm, '').replace('export default ', '')
const source = (await read('../vite.config.js')).replace(/^import .*$/gm, '')
.replaceAll('import.meta.url', 'configUrl').replace('export default ', '')
const factory = runInNewContext(source, {
URL, fileURLToPath, configUrl: new URL('../vite.config.js', import.meta.url).href,
defineConfig: value => value, loadEnv: () => ({ API_PROXY_TARGET: 'https://gateway.example.com' }),
uni: () => ({}), process: { env: { UNI_PLATFORM: 'h5' }, cwd: () => '/project' }
})
@@ -57,3 +65,24 @@ test('H5 proxy uses the shared gateway prefix and preserves API suffixes', async
assert.equal(proxy['/gateway-api'].rewrite(`/gateway-api${suffix}`), `/api${suffix}`)
}
})
test('App update configuration loads from the project when HBuilderX starts elsewhere', async t => {
const fixturePrefix = join(tmpdir(), 'overseas-pda-env-')
const fixtureDir = await mkdtemp(fixturePrefix)
assert.ok(resolve(fixtureDir).startsWith(resolve(fixturePrefix)))
t.after(() => rm(fixtureDir, { recursive: true, force: true }))
const endpoint = 'https://updates.example.test/pda/overseas/latest'
await writeFile(join(fixtureDir, '.env.local'), `VITE_PDA_UPDATE_URL=${endpoint}\n`)
const source = (await read('../vite.config.js')).replace(/^import .*$/gm, '')
.replaceAll('import.meta.url', 'configUrl').replace('export default ', '')
const factory = runInNewContext(source, {
URL, fileURLToPath, configUrl: pathToFileURL(join(fixtureDir, 'vite.config.js')).href,
defineConfig: value => value, loadEnv, uni: () => ({}),
process: { env: { UNI_PLATFORM: 'app' }, cwd: () => join(fixtureDir, 'HBuilderX') }
})
for (const mode of ['development', 'production']) {
const config = factory({ mode })
assert.equal(JSON.parse(config.define['import.meta.env.VITE_PDA_UPDATE_URL']), endpoint)
assert.equal(resolve(config.envDir), resolve(fixtureDir))
}
})