From 40f967d87e2dc9476ae658fb5947d880ba27b71c Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:14:58 +0000 Subject: [PATCH] test: cover editor cancellation and preserve command diagnostics --- AppImage/lib/lxc-app-editor-errors.ts | 2 +- tests/lxc_updates/test_app_editor_errors.cjs | 30 +++++++++++++++++-- .../test_native_app_discovery_ui.cjs | 15 ++++++---- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/AppImage/lib/lxc-app-editor-errors.ts b/AppImage/lib/lxc-app-editor-errors.ts index 506091e7..f1a2a5eb 100644 --- a/AppImage/lib/lxc-app-editor-errors.ts +++ b/AppImage/lib/lxc-app-editor-errors.ts @@ -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", []], diff --git a/tests/lxc_updates/test_app_editor_errors.cjs b/tests/lxc_updates/test_app_editor_errors.cjs index dd1f5e52..9fe5f52b 100644 --- a/tests/lxc_updates/test_app_editor_errors.cjs +++ b/tests/lxc_updates/test_app_editor_errors.cjs @@ -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} \nnext line"])) + ['index','port','max','code'].includes(name) ? '17' : name === 'repo' ? 'owner/project' : "detail /opt/app --version {literal} \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', '', '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', '', '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 () => { diff --git a/tests/lxc_updates/test_native_app_discovery_ui.cjs b/tests/lxc_updates/test_native_app_discovery_ui.cjs index 3322b3f1..4baffcc2 100644 --- a/tests/lxc_updates/test_native_app_discovery_ui.cjs +++ b/tests/lxc_updates/test_native_app_discovery_ui.cjs @@ -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},