fix(apps): preserve lifecycle state and wait for usable launch endpoints
This commit is contained in:
@@ -34,6 +34,7 @@ vi.mock('@/api/rpc-client', () => ({
|
||||
vi.stubGlobal('open', mockWindowOpen)
|
||||
|
||||
import { useAppLauncherStore, senderMatchesApp } from '../appLauncher'
|
||||
import { useAppStore } from '../app'
|
||||
|
||||
describe('useAppLauncherStore', () => {
|
||||
beforeEach(() => {
|
||||
@@ -54,6 +55,25 @@ describe('useAppLauncherStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks both browser and embedded launch while HTTP is unready', () => {
|
||||
const app = useAppStore()
|
||||
app.data = { 'package-data': { gitea: { state: 'running', 'ui-ready': false, health: 'healthy', manifest: { id: 'gitea', title: 'Gitea' } } } } as never
|
||||
const launcher = useAppLauncherStore()
|
||||
launcher.openSession('gitea')
|
||||
expect(launcher.panelAppId).toBeNull()
|
||||
launcher.open({ url: 'http://192.0.2.10:3001/', title: 'Gitea', openInNewTab: true })
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
expect(launcher.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('also gates a dynamic app resolved through its runtime URL', () => {
|
||||
useAppStore().data = { 'package-data': { custom: { state: 'running', 'ui-ready': false, manifest: { id: 'custom', title: 'Custom' }, installed: { 'interface-addresses': { main: { 'lan-address': 'http://localhost:18993/' } } } } } } as never
|
||||
const launcher = useAppLauncherStore()
|
||||
launcher.open({ url: 'http://192.0.2.10:18993/', title: 'Custom', openInNewTab: true })
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
expect(launcher.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('starts closed with empty state', () => {
|
||||
const store = useAppLauncherStore()
|
||||
expect(store.isOpen).toBe(false)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { reactive, nextTick } from 'vue'
|
||||
|
||||
const fake = reactive<{ packages: Record<string, unknown> }>({ packages: {} })
|
||||
vi.mock('../sync', () => ({ useSyncStore: () => fake }))
|
||||
vi.mock('../../api/rpc-client', () => ({ rpcClient: {} }))
|
||||
import { useServerStore } from '../server'
|
||||
|
||||
function installing(phase = 'preparing-app') {
|
||||
return { state: 'installing', manifest: { title: 'Git Workshop' }, 'install-progress': { phase, size: 0, downloaded: 0 } }
|
||||
}
|
||||
|
||||
describe('installation state after hard refresh', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
fake.packages = {}
|
||||
})
|
||||
|
||||
it('restores an in-flight install from an already-loaded server snapshot', () => {
|
||||
fake.packages = { 'archipelago-source': installing() }
|
||||
const store = useServerStore()
|
||||
expect(store.isInstalling('archipelago-source')).toBe(true)
|
||||
expect(store.installingApps.get('archipelago-source')).toMatchObject({
|
||||
progress: 20,
|
||||
message: 'Downloading, building and starting app…',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a long download visible and clears it on terminal success', async () => {
|
||||
const store = useServerStore()
|
||||
fake.packages = { 'nginx-proxy-manager': installing() }
|
||||
await nextTick()
|
||||
expect(store.isInstalling('nginx-proxy-manager')).toBe(true)
|
||||
fake.packages = { 'nginx-proxy-manager': installing() }
|
||||
await nextTick()
|
||||
expect(store.installingApps.get('nginx-proxy-manager')?.progress).toBe(20)
|
||||
fake.packages = { 'nginx-proxy-manager': { state: 'running' } }
|
||||
await nextTick()
|
||||
expect(store.isInstalling('nginx-proxy-manager')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user