Per the rule that only front-end apps with a UI belong in "My Apps" (databases/backends/headless go to Websites), make the manifest's interfaces.main.ui the deciding signal. isWebsitePackage now treats any package that declares a UI as an app even when it isn't in the curated APP_CATEGORY_MAP, and falls through headless LAN-reachable packages to Websites. Additive — service-by-name infra and curated known apps are unchanged, so no currently-correct app moves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
4.6 KiB
TypeScript
109 lines
4.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { ref } from 'vue'
|
|
import { PackageState, type PackageDataEntry } from '@/types/api'
|
|
import { canLaunch, filterEntriesForTab, hasFrontendUi, isServiceContainer, isServicePackage, isWebsitePackage, launchBlockedReason, resolveAppIcon, useCategoriesWithApps } from '../appsConfig'
|
|
|
|
function makePkg(id: string, title: string, category: string): PackageDataEntry {
|
|
return {
|
|
state: PackageState.Running,
|
|
manifest: {
|
|
id,
|
|
title,
|
|
version: '1.0.0',
|
|
description: { short: '', long: '' },
|
|
'release-notes': '',
|
|
license: '',
|
|
'wrapper-repo': '',
|
|
'upstream-repo': '',
|
|
'support-site': '',
|
|
'marketing-site': '',
|
|
'donation-url': null,
|
|
category,
|
|
} as unknown as PackageDataEntry['manifest'],
|
|
'static-files': { license: '', instructions: '', icon: '' },
|
|
}
|
|
}
|
|
|
|
describe('appsConfig service filtering', () => {
|
|
it('treats bitcoin stack UI sidecars as services', () => {
|
|
expect(isServiceContainer('bitcoin-ui')).toBe(true)
|
|
expect(isServiceContainer('lnd-ui')).toBe(true)
|
|
expect(isServiceContainer('electrs-ui')).toBe(true)
|
|
})
|
|
|
|
it('treats container aliases as services even with non-service keys', () => {
|
|
const aliasPkg = makePkg('bitcoin-ui', 'Bitcoin UI', 'money')
|
|
expect(isServicePackage('core-lnd-ui', aliasPkg)).toBe(true)
|
|
})
|
|
|
|
it('removes service-only categories from app category tabs', () => {
|
|
const packages = ref<Record<string, PackageDataEntry>>({
|
|
'core-bitcoin-ui': makePkg('bitcoin-ui', 'Bitcoin UI', 'money'),
|
|
'filebrowser': makePkg('filebrowser', 'File Browser', 'data'),
|
|
})
|
|
|
|
const allCategories = ref([
|
|
{ id: 'all', name: 'All' },
|
|
{ id: 'money', name: 'Money' },
|
|
{ id: 'data', name: 'Data' },
|
|
])
|
|
|
|
const visible = useCategoriesWithApps(packages, allCategories)
|
|
expect(visible.value.map(c => c.id)).toEqual(['all', 'data'])
|
|
})
|
|
|
|
it('filters apps tab by category using manifest-aware service checks', () => {
|
|
const entries: Array<[string, PackageDataEntry]> = [
|
|
['core-bitcoin-ui', makePkg('bitcoin-ui', 'Bitcoin UI', 'money')],
|
|
['filebrowser', makePkg('filebrowser', 'File Browser', 'data')],
|
|
['btcpay-server', makePkg('btcpay-server', 'BTCPay', 'commerce')],
|
|
]
|
|
|
|
const appsAll = filterEntriesForTab(entries, 'apps', 'all')
|
|
expect(appsAll.map(([id]) => id)).toEqual(['filebrowser', 'btcpay-server'])
|
|
|
|
const appsData = filterEntriesForTab(entries, 'apps', 'data')
|
|
expect(appsData.map(([id]) => id)).toEqual(['filebrowser'])
|
|
})
|
|
|
|
it('routes service aliases into services tab and excludes user apps', () => {
|
|
const entries: Array<[string, PackageDataEntry]> = [
|
|
['core-lnd-ui', makePkg('lnd-ui', 'LND UI', 'money')],
|
|
['grafana', makePkg('grafana', 'Grafana', 'data')],
|
|
]
|
|
|
|
const services = filterEntriesForTab(entries, 'services', 'all')
|
|
expect(services.map(([id]) => id)).toEqual(['core-lnd-ui'])
|
|
})
|
|
|
|
it('falls back to packaged app icon when static icon token is not a path', () => {
|
|
const pkg = makePkg('gitea', 'Gitea', 'dev')
|
|
pkg['static-files']!.icon = 'git-branch'
|
|
expect(resolveAppIcon('gitea', pkg)).toBe('/assets/img/app-icons/gitea.svg')
|
|
})
|
|
|
|
it('classifies an unknown app by whether its manifest declares a UI (#45)', () => {
|
|
// Headless: a LAN address but no declared UI → Website.
|
|
const headless = makePkg('some-backend', 'Some Backend', 'other')
|
|
headless.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:9000' } } } as unknown as PackageDataEntry['installed']
|
|
expect(hasFrontendUi(headless)).toBe(false)
|
|
expect(isWebsitePackage('some-backend', headless)).toBe(true)
|
|
|
|
// Front-end app: declares interfaces.main.ui → My Apps even when not in the
|
|
// curated category map.
|
|
const uiApp = makePkg('some-ui-app', 'Some UI App', 'other')
|
|
;(uiApp.manifest as unknown as Record<string, unknown>).interfaces = { main: { ui: 'http://localhost:9001' } }
|
|
uiApp.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:9001' } } } as unknown as PackageDataEntry['installed']
|
|
expect(hasFrontendUi(uiApp)).toBe(true)
|
|
expect(isWebsitePackage('some-ui-app', uiApp)).toBe(false)
|
|
})
|
|
|
|
it('explains that Fedimint waits for Bitcoin sync before Guardian starts', () => {
|
|
const pkg = makePkg('fedimint', 'Fedimint', 'money')
|
|
pkg.state = PackageState.Starting
|
|
pkg.installed = { 'interface-addresses': { main: { 'lan-address': 'http://localhost:8175' } } } as unknown as PackageDataEntry['installed']
|
|
expect(launchBlockedReason('fedimint', pkg)).toContain('Bitcoin')
|
|
expect(canLaunch(pkg)).toBe(true)
|
|
})
|
|
})
|