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)/) })