mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 02:12:12 +00:00
test: cover editor cancellation and preserve command diagnostics
This commit is contained in:
@@ -27,7 +27,7 @@ const RULES: [RegExp, string, string[]][] = [
|
||||
[/^no version matched in command output$/, "commandNoVersion", []],
|
||||
[/^unsupported method: ([\s\S]+)$/, "unsupportedMethod", ["method"]],
|
||||
[/^unknown upstream_type: ([\s\S]+)$/, "unknownUpstream", ["type"]],
|
||||
[/^([\s\S]+): not found$/, "githubNotFound", ["repo"]],
|
||||
[/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+): not found$/, "githubNotFound", ["repo"]],
|
||||
[/^github rejected the request \(403\)$/, "githubRejected", []],
|
||||
[/^github error (\d+)$/, "githubError", ["code"]],
|
||||
[/^no tag \/ release name in response$/, "noRelease", []],
|
||||
|
||||
@@ -71,20 +71,46 @@ const enErrors = catalogs.en.vmLxc.appEditor.errors, skErrors = catalogs.sk.vmLx
|
||||
assert.deepEqual(Object.keys(enErrors).sort(), Object.keys(skErrors).sort())
|
||||
for (const [key, template] of Object.entries(enErrors)) {
|
||||
const params = Object.fromEntries([...template.matchAll(/\{(\w+)\}/g)].map(([, name]) => [name,
|
||||
['index','port','max','code'].includes(name) ? '17' : "detail /opt/app --version {literal} <tag>\nnext line"]))
|
||||
['index','port','max','code'].includes(name) ? '17' : name === 'repo' ? 'owner/project' : "detail /opt/app --version {literal} <tag>\nnext line"]))
|
||||
const message = en(`vmLxc.appEditor.errors.${key}`, params)
|
||||
assert.equal(localize(message, en), message)
|
||||
assert.equal(localize(message, sk), sk(`vmLxc.appEditor.errors.${key}`, params), key)
|
||||
}
|
||||
assert.equal(localize('binary_args[4] exceeds 256 chars', sk), 'Argument binary_args[4] môže mať najviac 256 znakov.')
|
||||
assert.equal(localize('tag_regex is not a valid regex: [bad]\nposition 4', sk), 'Regulárny výraz pre tagy (tag_regex) je neplatný: [bad]\nposition 4')
|
||||
for (const message of ['unknown /opt/app --version', '<script>alert(1)</script>', 'file_regex is required extra', 'prefix file_regex is required', 'binary_args[-1] exceeds 4 chars']) {
|
||||
for (const message of ['unknown /opt/app --version', 'myapp: not found', 'sh: myapp: not found', '<script>alert(1)</script>', 'file_regex is required extra', 'prefix file_regex is required', 'binary_args[-1] exceeds 4 chars']) {
|
||||
assert.equal(localize(message, sk), message, 'Unknown detail must remain intact')
|
||||
}
|
||||
for (const empty of [null, undefined, '']) assert.equal(localize(empty, sk), '')
|
||||
assert.equal(catalogs.sk.vmLxc.appEditor.commandPlaceholder, catalogs.en.vmLxc.appEditor.commandPlaceholder)
|
||||
assert.equal(catalogs.sk.vmLxc.appEditor.regexPlaceholderVersion, catalogs.en.vmLxc.appEditor.regexPlaceholderVersion)
|
||||
async function main() {
|
||||
const {fixture, flatten, text} = require('./test_native_app_discovery_ui.cjs')
|
||||
const recorded = {id: 'fiction', name: 'Fixture', installed_via: 'file', file_path: 'relative', ports: [], state: {}}
|
||||
for (const locale of ['en', 'sk']) {
|
||||
const t = translate(locale)
|
||||
const f = fixture({apps: [recorded], translate: t, apiError: 'file_path is required and must be an absolute path'})
|
||||
f.render(); await f.settle()
|
||||
const mode = flatten(f.tree()).find(n => n.type === 'button' && text(n).trim() === t('vmLxc.appEditor.editButton'))
|
||||
assert.ok(mode); mode.props.onClick(); f.render()
|
||||
const action = key => flatten(f.tree()).find(n => n.type === 'button' && text(n).trim() === t('vmLxc.appEditor.' + key))
|
||||
await action('removeButton').props.onClick(); f.render()
|
||||
assert.deepEqual(f.confirmPrompts, [t('vmLxc.appEditor.confirmRemove')])
|
||||
assert.equal(f.calls.filter(c => c.method === 'DELETE').length, 0)
|
||||
assert.equal(f.cachedWrites.length, 0, 'decline leaves cache untouched')
|
||||
await action('editFieldsButton').props.onClick(); f.render()
|
||||
await f.button('saveButton').props.onClick(); f.render()
|
||||
assert.ok(text(f.tree()).includes(t('vmLxc.appEditor.errors.filePath')))
|
||||
assert.equal(flatten(f.tree()).find(n => n.props?.id === 'app-file-path').props.value, 'relative')
|
||||
assert.equal(f.calls.filter(c => c.method === 'PUT').length, 1)
|
||||
assert.equal(f.cachedWrites.length, 0, 'failed save preserves cached record')
|
||||
await f.button('testDetectorButton').props.onClick(); f.render()
|
||||
assert.ok(text(f.tree()).includes(t('vmLxc.appEditor.errors.filePath')))
|
||||
const before = f.calls.filter(c => c.method && c.method !== 'GET').length
|
||||
f.button('cancelButton').props.onClick(); f.render()
|
||||
assert.equal(f.calls.filter(c => c.method && c.method !== 'GET').length, before)
|
||||
assert.ok(text(f.tree()).includes('Fixture'))
|
||||
}
|
||||
let requests = 0
|
||||
const backend = 'file_path is required and must be an absolute path'
|
||||
const {fetchApi} = load('lib/api-config.ts', {fetch: async () => {
|
||||
|
||||
@@ -23,9 +23,10 @@ const candidate = {
|
||||
evidence: [{kind: 'installation_file', value: '/opt/handmade/server'}], default_ports: [],
|
||||
}
|
||||
|
||||
function fixture({nativeCandidate = candidate, dockerCandidate = null, apps = []} = {}) {
|
||||
function fixture({nativeCandidate = candidate, dockerCandidate = null, apps = [], translate,
|
||||
confirmAccepted = false, apiError = null, detectorResult = null} = {}) {
|
||||
let cursor = 0, tree, props, effects = [], states = [], refs = [], effectStates = [], memoStates = []
|
||||
const calls = [], cachedWrites = []
|
||||
const calls = [], cachedWrites = [], confirmPrompts = []
|
||||
let native = {vmid: 101, status: 'not_scanned', candidates: [], stale: false}
|
||||
let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete', scanIssues
|
||||
let dockerStatus = "complete", dockerCached = {vmid:101,status:"not_scanned",candidates:[],stale:false}
|
||||
@@ -56,6 +57,8 @@ function fixture({nativeCandidate = candidate, dockerCandidate = null, apps = []
|
||||
}
|
||||
const api = async (url, options = {}) => {
|
||||
calls.push({url, ...options})
|
||||
if (apiError && options.method && options.method !== 'GET') throw new Error(apiError)
|
||||
if (detectorResult && url.endsWith('/apps/test')) return detectorResult
|
||||
if (url.endsWith('/logo-discovery')) {
|
||||
if (iconGate) await iconGate
|
||||
if (iconError) throw iconError
|
||||
@@ -96,6 +99,7 @@ function fixture({nativeCandidate = candidate, dockerCandidate = null, apps = []
|
||||
}
|
||||
const exports = {}
|
||||
vm.runInNewContext(compiled.outputText, {exports, console, setTimeout, clearTimeout, AbortController,
|
||||
confirm: prompt => {confirmPrompts.push(prompt); return confirmAccepted},
|
||||
require: name => {
|
||||
if (name === 'react') return react
|
||||
if (name === 'react/jsx-runtime') return {jsx: (type, props) => ({type, props}), jsxs: (type, props) => ({type, props})}
|
||||
@@ -108,7 +112,7 @@ function fixture({nativeCandidate = candidate, dockerCandidate = null, apps = []
|
||||
return context.exports
|
||||
}
|
||||
if (name.includes('lxc-apps-cache')) return cache
|
||||
if (name.includes('i18n/provider')) return {useT: () => (key, values) => key + (values ? JSON.stringify(values) : '')}
|
||||
if (name.includes('i18n/provider')) return {useT: () => translate || ((key, values) => key + (values ? JSON.stringify(values) : ''))}
|
||||
if (name.includes('category-color')) return {useIsLightTheme: () => false, categoryChipStyle: () => ({})}
|
||||
if (name.includes('category-label')) return {getCategoryLabel: value => value}
|
||||
return new Proxy({}, {get: (_, key) => key})
|
||||
@@ -143,11 +147,12 @@ function fixture({nativeCandidate = candidate, dockerCandidate = null, apps = []
|
||||
}
|
||||
const settle = async () => {for (let i = 0; i < 6; i++) await Promise.resolve(); render()}
|
||||
const button = key => {
|
||||
const match = flatten(tree).find(n => n.type === 'Button' && text(n).includes('vmLxc.appEditor.' + key))
|
||||
const label = translate ? translate('vmLxc.appEditor.' + key) : 'vmLxc.appEditor.' + key
|
||||
const match = flatten(tree).find(n => n.type === 'Button' && text(n).includes(label))
|
||||
assert.ok(match, 'Missing button ' + key)
|
||||
return match
|
||||
}
|
||||
return {render, renderParent, settle, calls, cachedWrites, button, tree: () => tree, bundleReads: () => bundleReads,
|
||||
return {render, renderParent, settle, calls, cachedWrites, confirmPrompts, button, tree: () => tree, bundleReads: () => bundleReads,
|
||||
holdIcon: () => {iconGate = new Promise(resolve => {resolveIcon = resolve})},
|
||||
resolveIcon: () => resolveIcon(), setIconResult: value => {iconResult = value},
|
||||
setIconError: value => {iconError = value},
|
||||
|
||||
Reference in New Issue
Block a user