mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
133 lines
9.3 KiB
JavaScript
133 lines
9.3 KiB
JavaScript
// Execute the real Updates JSX and handlers with independent state fixtures.
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const vm = require('node:vm')
|
|
const root = path.resolve(__dirname, '../../AppImage')
|
|
const ts = require(path.join(root, 'node_modules/typescript'))
|
|
const source = fs.readFileSync(path.join(root, 'components/virtual-machines.tsx'), 'utf8')
|
|
const tree = ts.createSourceFile('vm.tsx', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
|
let appRender, bulkRender, checkButton, checkHandler
|
|
const declarations = []
|
|
function walk(node) {
|
|
if (ts.isCallExpression(node) && node.expression.getText(tree) === 'appSections.map') appRender = node.arguments[0].getText(tree)
|
|
if (ts.isConditionalExpression(node) && node.condition.getText(tree) === 'bulkConfigured'
|
|
&& node.whenTrue.getText(tree).includes('applyBulkUpdate(')) bulkRender = node.whenTrue.getText(tree)
|
|
if (ts.isVariableDeclaration(node)) {
|
|
if (node.name.getText(tree) === 'checkOsUpdates') checkHandler = node.getText(tree)
|
|
if (['selectedBulkUpdateStates', 'bulkHasPendingUpdates', 'bulkAllTargetsUpToDate', 'bulkHasUnknownTargets'].includes(node.name.getText(tree)))
|
|
declarations.push('const ' + node.getText(tree) + ';')
|
|
}
|
|
if (ts.isJsxElement(node) && node.openingElement.tagName.getText(tree) === 'Button'
|
|
&& node.openingElement.attributes.getText(tree).includes('checkOsUpdates(selectedVM.vmid)')) checkButton = node.getText(tree)
|
|
ts.forEachChild(node, walk)
|
|
}
|
|
walk(tree)
|
|
assert(appRender && bulkRender && checkButton && checkHandler)
|
|
function compile(code, context) {
|
|
const result = ts.transpileModule(code, {compilerOptions: {target: ts.ScriptTarget.ES2020,
|
|
module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX}, reportDiagnostics: true})
|
|
assert.equal(result.diagnostics.length, 0)
|
|
vm.runInNewContext(result.outputText, context)
|
|
return context.exports
|
|
}
|
|
function flatten(node) {
|
|
if (Array.isArray(node)) return node.flatMap(flatten)
|
|
return node && typeof node === 'object' ? [node, ...flatten(node.props?.children)] : []
|
|
}
|
|
function text(node) {
|
|
if (Array.isArray(node)) return node.map(text).join(' ')
|
|
return node && typeof node === 'object' ? text(node.props?.children) : typeof node === 'string' ? node : ''
|
|
}
|
|
for (const locale of ['en', 'sk']) {
|
|
const messages = JSON.parse(fs.readFileSync(path.join(root, `messages/${locale}/common.json`)))
|
|
const t = key => key.split('.').reduce((o, part) => o?.[part], messages) ?? key
|
|
const calls = []
|
|
const context = {exports: {}, t, customCmdEditingApp: null, uc: null, helperExists: false,
|
|
helperUsesWebUpdater: false, selectedVM: {vmid: 210, status: 'stopped'}, openCustomCmdEditor() {},
|
|
openApplyTerminal: (...args) => calls.push(args), getUpdateActionButtonClass: () => 'original-style',
|
|
bulkApplying: false, osCheckingVmid: null, updatesRefreshing: false, controlLoading: false,
|
|
applyOpen: false, osUpdateStatusKnown: true, hasOsUpdates: false,
|
|
dockerEngineHasUpdate: false, dockerEngineUpToDate: true, dockerInventoryRefreshing: false,
|
|
dockerInventoryAvailable: true, dockerUpdateUnits: [], selectedBulkLabels: ['OS', 'Custom app'],
|
|
bulkTargets: ['os', 'app:custom'], applyBulkUpdate: () => calls.push('bulk'), checkOsUpdates() {},
|
|
require: () => ({jsx: (type, props) => ({type, props}), jsxs: (type, props) => ({type, props})})}
|
|
for (const name of ['Button', 'Package', 'Settings2', 'ArrowUpCircle', 'CheckCircle2', 'RefreshCw', 'Pin', 'Loader2']) context[name] = name
|
|
const ui = compile('exports.app = (' + appRender + '); exports.check = () => (' + checkButton + ');', context)
|
|
const original = {id: 'custom', name: 'Custom app', installed_via: 'manual', installed_version: null,
|
|
latest_version: '2.0', update_available: null, update_command: 'approved-command', checked_at: '2026-10-14T10:43:19Z'}
|
|
const before = JSON.stringify(original)
|
|
const rendered = ui.app(original)
|
|
assert.equal(ui.check().props.disabled, true, 'a stopped CT must not start an index refresh from the check button')
|
|
assert.match(text(rendered), locale === 'sk' ? /Nainštalovaná verzia nie je overená/ : /Installed version is unverified/)
|
|
assert.doesNotMatch(text(rendered), /Čaká sa|pending|upstreamAvailable/)
|
|
const action = flatten(rendered).find(n => n.type === 'Button')
|
|
assert.equal(text(action).trim(), locale === 'sk' ? 'Spustiť vlastný príkaz' : 'Run custom command')
|
|
assert.equal(calls.length, 0, 'render/check timestamp must not execute an updater')
|
|
action.props.onClick()
|
|
assert.equal(calls.length, 1)
|
|
assert.equal(calls[0][0], 210)
|
|
assert.equal(calls[0][1], 'app')
|
|
assert.equal(calls[0][2].updateCommand, 'approved-command')
|
|
assert.deepEqual(Array.from(calls[0][2].targetIds), ['app:custom'])
|
|
assert.equal(JSON.stringify(original), before, 'UI must not confirm or edit the stored version')
|
|
calls.length = 0
|
|
const knownVersion = ui.app({...original, installed_version: '1.0'})
|
|
assert(text(knownVersion).includes('1.0'), 'keep the known installed version when comparison is unavailable')
|
|
assert.doesNotMatch(text(knownVersion), /Čaká sa|Version tracking pending/)
|
|
const noDetector = ui.app({...original, installed_via: undefined})
|
|
assert(text(noDetector).includes(t('vmLxc.updates.versionTrackingNotConfigured')))
|
|
assert.equal(text(flatten(noDetector).find(n => n.type === 'Button')).trim(), t('vmLxc.updates.runCustomCommand'))
|
|
assert.equal(text(ui.check()).trim(), locale === 'sk' ? 'Skontrolovať balíky systému' : 'Check system packages')
|
|
for (const installed_via of ['binary', 'file', 'dpkg', 'apk']) {
|
|
const unavailable = ui.app({...original, installed_via, error: 'probe failed'})
|
|
assert.match(text(unavailable), locale === 'sk' ? /Dostupnosť aktualizácie nie je overená/ : /Update availability is unverified/)
|
|
assert.doesNotMatch(text(unavailable), /Čaká sa|next scheduled|upToDateAtLabel/)
|
|
}
|
|
const current = {...original, installed_version: '2.0', update_available: false}
|
|
const pending = {...original, installed_version: '1.0', update_available: true}
|
|
assert(text(ui.app(current)).includes(t('vmLxc.updates.upToDateAtLabel')))
|
|
assert(text(ui.app(pending)).includes(t('vmLxc.updates.installedLabel')))
|
|
assert.equal(text(flatten(ui.app(pending)).find(n => n.type === 'Button')).trim(), t('vmLxc.updates.applyUpdate'))
|
|
assert(!flatten(ui.app({...original, update_command: ''})).some(n => n.type === 'Button'), 'no command means no execution')
|
|
function bulk(app, osKnown = true, extra = {}) {
|
|
return compile(declarations.join('\n') + '\nexports.rendered=(' + bulkRender + ');',
|
|
{...context, exports: {}, registeredApps: [app], osUpdateStatusKnown: osKnown, ...extra}).rendered
|
|
}
|
|
const unknownBulk = bulk(original)
|
|
assert.match(text(unknownBulk), locale === 'sk' ? /nie je overená/ : /is unverified/)
|
|
assert.equal(flatten(unknownBulk).find(n => n.type === 'Button').props.disabled, false, 'preserve explicit manual execution')
|
|
assert.equal(flatten(bulk(current)).find(n => n.type === 'Button').props.disabled, true)
|
|
assert.equal(flatten(bulk(original, false)).find(n => n.type === 'Button').props.disabled, true, 'unknown OS blocks bulk')
|
|
assert.equal(flatten(bulk(original, true, {bulkApplying: true})).find(n => n.type === 'Button').props.disabled, true)
|
|
const mixed = bulk(pending, true, {registeredApps: [pending, {...original, id: 'unknown'}],
|
|
bulkTargets: ['os', 'app:custom', 'app:unknown']})
|
|
assert.match(text(mixed), locale === 'sk' ? /nie je overená/ : /is unverified/)
|
|
assert.equal(flatten(mixed).find(n => n.type === 'Button').props.disabled, false)
|
|
const imageUnavailable = bulk(original, true, {bulkTargets: ['os', 'docker-unit:one'], dockerInventoryAvailable: false})
|
|
assert.equal(flatten(imageUnavailable).find(n => n.type === 'Button').props.disabled, true, 'unavailable image cannot be run')
|
|
assert.doesNotMatch(text(bulk(pending)), locale === 'sk' ? /nie je overená/ : /is unverified/)
|
|
assert.equal(calls.length, 0, 'all status fixtures are side-effect free')
|
|
}
|
|
assert.match(source, /t\("vmLxc\.updates\.startBeforeCheck"\)/,
|
|
'the Updates tab must explain why checking a stopped CT is disabled')
|
|
;(async () => {
|
|
const context = {exports: {}, osCheckRequestRef: {current: null}, applyOpen: false, controlLoading: false,
|
|
selectedVMRef: {current: {vmid: 210}}, vmModalCacheRef: {current: {details: new Map()}},
|
|
setOsCheckingVmid() {}, setOsCheckError() {}, setSelectedVM() {}, setVMDetails() {}, mutate: async () => {},
|
|
t: key => key, requests: []}
|
|
context.refreshCheckLxcUpdates = async vmid => {context.requests.push(['check', vmid]); return {count: 0}}
|
|
context.fetchApi = async (url, options) => {context.requests.push([url, options]); return {}}
|
|
const handler = compile('const ' + checkHandler + '; exports.check = checkOsUpdates;', context)
|
|
await handler.check(210)
|
|
assert.deepEqual(context.requests, [['check', 210], ['/api/vms/210', undefined]])
|
|
context.requests.length = 0
|
|
for (const guard of ['applyOpen', 'controlLoading']) {
|
|
context[guard] = true
|
|
await handler.check(210)
|
|
context[guard] = false
|
|
}
|
|
assert.equal(context.requests.length, 0, 'blocked checks make no API/probe requests')
|
|
console.log('PASS common update status: EN/SK unknown/current/pending/error/custom/no-command/bulk/OS check guards; actual JSX and handlers')
|
|
})().catch(error => {console.error(error); process.exitCode = 1})
|