mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +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", []],
|
[/^no version matched in command output$/, "commandNoVersion", []],
|
||||||
[/^unsupported method: ([\s\S]+)$/, "unsupportedMethod", ["method"]],
|
[/^unsupported method: ([\s\S]+)$/, "unsupportedMethod", ["method"]],
|
||||||
[/^unknown upstream_type: ([\s\S]+)$/, "unknownUpstream", ["type"]],
|
[/^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 rejected the request \(403\)$/, "githubRejected", []],
|
||||||
[/^github error (\d+)$/, "githubError", ["code"]],
|
[/^github error (\d+)$/, "githubError", ["code"]],
|
||||||
[/^no tag \/ release name in response$/, "noRelease", []],
|
[/^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())
|
assert.deepEqual(Object.keys(enErrors).sort(), Object.keys(skErrors).sort())
|
||||||
for (const [key, template] of Object.entries(enErrors)) {
|
for (const [key, template] of Object.entries(enErrors)) {
|
||||||
const params = Object.fromEntries([...template.matchAll(/\{(\w+)\}/g)].map(([, name]) => [name,
|
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)
|
const message = en(`vmLxc.appEditor.errors.${key}`, params)
|
||||||
assert.equal(localize(message, en), message)
|
assert.equal(localize(message, en), message)
|
||||||
assert.equal(localize(message, sk), sk(`vmLxc.appEditor.errors.${key}`, params), key)
|
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('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')
|
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')
|
assert.equal(localize(message, sk), message, 'Unknown detail must remain intact')
|
||||||
}
|
}
|
||||||
for (const empty of [null, undefined, '']) assert.equal(localize(empty, sk), '')
|
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.commandPlaceholder, catalogs.en.vmLxc.appEditor.commandPlaceholder)
|
||||||
assert.equal(catalogs.sk.vmLxc.appEditor.regexPlaceholderVersion, catalogs.en.vmLxc.appEditor.regexPlaceholderVersion)
|
assert.equal(catalogs.sk.vmLxc.appEditor.regexPlaceholderVersion, catalogs.en.vmLxc.appEditor.regexPlaceholderVersion)
|
||||||
async function main() {
|
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
|
let requests = 0
|
||||||
const backend = 'file_path is required and must be an absolute path'
|
const backend = 'file_path is required and must be an absolute path'
|
||||||
const {fetchApi} = load('lib/api-config.ts', {fetch: async () => {
|
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: [],
|
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 = []
|
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 native = {vmid: 101, status: 'not_scanned', candidates: [], stale: false}
|
||||||
let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete', scanIssues
|
let holdScan, scanGate, holdSave, saveGate, scanStatus = 'complete', scanIssues
|
||||||
let dockerStatus = "complete", dockerCached = {vmid:101,status:"not_scanned",candidates:[],stale:false}
|
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 = {}) => {
|
const api = async (url, options = {}) => {
|
||||||
calls.push({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 (url.endsWith('/logo-discovery')) {
|
||||||
if (iconGate) await iconGate
|
if (iconGate) await iconGate
|
||||||
if (iconError) throw iconError
|
if (iconError) throw iconError
|
||||||
@@ -96,6 +99,7 @@ function fixture({nativeCandidate = candidate, dockerCandidate = null, apps = []
|
|||||||
}
|
}
|
||||||
const exports = {}
|
const exports = {}
|
||||||
vm.runInNewContext(compiled.outputText, {exports, console, setTimeout, clearTimeout, AbortController,
|
vm.runInNewContext(compiled.outputText, {exports, console, setTimeout, clearTimeout, AbortController,
|
||||||
|
confirm: prompt => {confirmPrompts.push(prompt); return confirmAccepted},
|
||||||
require: name => {
|
require: name => {
|
||||||
if (name === 'react') return react
|
if (name === 'react') return react
|
||||||
if (name === 'react/jsx-runtime') return {jsx: (type, props) => ({type, props}), jsxs: (type, props) => ({type, props})}
|
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
|
return context.exports
|
||||||
}
|
}
|
||||||
if (name.includes('lxc-apps-cache')) return cache
|
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-color')) return {useIsLightTheme: () => false, categoryChipStyle: () => ({})}
|
||||||
if (name.includes('category-label')) return {getCategoryLabel: value => value}
|
if (name.includes('category-label')) return {getCategoryLabel: value => value}
|
||||||
return new Proxy({}, {get: (_, key) => key})
|
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 settle = async () => {for (let i = 0; i < 6; i++) await Promise.resolve(); render()}
|
||||||
const button = key => {
|
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)
|
assert.ok(match, 'Missing button ' + key)
|
||||||
return match
|
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})},
|
holdIcon: () => {iconGate = new Promise(resolve => {resolveIcon = resolve})},
|
||||||
resolveIcon: () => resolveIcon(), setIconResult: value => {iconResult = value},
|
resolveIcon: () => resolveIcon(), setIconResult: value => {iconResult = value},
|
||||||
setIconError: value => {iconError = value},
|
setIconError: value => {iconError = value},
|
||||||
|
|||||||
Reference in New Issue
Block a user