import { describe, it, expect, vi, beforeEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' // vi.hoisted runs before vi.mock hoisting const { mockPush, mockWindowOpen } = vi.hoisted(() => ({ mockPush: vi.fn(), mockWindowOpen: vi.fn(), })) // Mock vue-router vi.mock('vue-router', () => ({ useRouter: () => ({ push: mockPush }), })) vi.mock('@/router', () => ({ default: { push: mockPush }, })) vi.stubGlobal('open', mockWindowOpen) import { useAppLauncherStore } from '../appLauncher' describe('useAppLauncherStore', () => { beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() // Default to HTTP to avoid proxy rewriting Object.defineProperty(window, 'location', { value: { origin: 'http://192.168.1.228', protocol: 'http:', hostname: '192.168.1.228' }, writable: true, configurable: true, }) }) it('starts closed with empty state', () => { const store = useAppLauncherStore() expect(store.isOpen).toBe(false) expect(store.url).toBe('') expect(store.title).toBe('') }) it('routes known port apps to full-page session', () => { const store = useAppLauncherStore() // Port 8083 maps to /app/filebrowser/ — should route to session store.open({ url: 'http://192.168.1.228:8083', title: 'FileBrowser' }) // Legacy overlay should NOT open — routed to session view instead expect(store.isOpen).toBe(false) expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'filebrowser' } }) expect(mockWindowOpen).not.toHaveBeenCalled() }) it('routes BTCPay (port 23000) to full-page session', () => { const store = useAppLauncherStore() store.open({ url: 'http://192.168.1.228:23000', title: 'BTCPay' }) expect(store.isOpen).toBe(false) expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'btcpay' } }) }) it('routes Home Assistant (port 8123) to full-page session', () => { const store = useAppLauncherStore() store.open({ url: 'http://192.168.1.228:8123', title: 'Home Assistant' }) expect(store.isOpen).toBe(false) expect(mockPush).toHaveBeenCalled() }) it('routes Grafana (port 3000) to full-page session', () => { const store = useAppLauncherStore() store.open({ url: 'http://192.168.1.228:3000', title: 'Grafana' }) expect(store.isOpen).toBe(false) expect(mockPush).toHaveBeenCalled() }) it('opens in new tab when openInNewTab flag is set for unknown URL', () => { const store = useAppLauncherStore() // Use an unresolvable URL so it doesn't route to session store.open({ url: 'http://192.168.1.228:9999', title: 'Unknown', openInNewTab: true }) expect(store.isOpen).toBe(false) expect(mockWindowOpen).toHaveBeenCalledWith( 'http://192.168.1.228:9999', '_blank', 'noopener,noreferrer', ) }) it('routes HTTPS same-host apps via session view', () => { Object.defineProperty(window, 'location', { value: { origin: 'https://192.168.1.228', protocol: 'https:', hostname: '192.168.1.228' }, writable: true, configurable: true, }) const store = useAppLauncherStore() store.open({ url: 'http://192.168.1.228:8083', title: 'FileBrowser' }) // Known port — routes to full-page session expect(store.isOpen).toBe(false) expect(mockPush).toHaveBeenCalledWith({ name: 'app-session', params: { appId: 'filebrowser' } }) }) it('opens unknown URL in iframe overlay on HTTP', () => { const store = useAppLauncherStore() // Unresolvable URL — falls through to iframe overlay store.open({ url: 'http://192.168.1.228:9999', title: 'Custom App' }) expect(store.isOpen).toBe(true) expect(store.url).toBe('http://192.168.1.228:9999') expect(store.title).toBe('Custom App') expect(mockWindowOpen).not.toHaveBeenCalled() }) it('opens unknown different-host URL in iframe overlay', () => { Object.defineProperty(window, 'location', { value: { origin: 'https://192.168.1.228', protocol: 'https:', hostname: '192.168.1.228' }, writable: true, configurable: true, }) const store = useAppLauncherStore() store.open({ url: 'http://192.168.1.100:9999', title: 'Remote App' }) // Different host, unknown port — opens in iframe overlay (no proxy rewrite) expect(store.isOpen).toBe(true) expect(store.url).toBe('http://192.168.1.100:9999') }) it('close resets state', () => { const store = useAppLauncherStore() // Use unknown URL to trigger iframe overlay store.open({ url: 'http://192.168.1.228:9999', title: 'Custom' }) store.close() expect(store.isOpen).toBe(false) expect(store.url).toBe('') expect(store.title).toBe('') }) it('close restores focus to previous element', async () => { vi.useFakeTimers() const store = useAppLauncherStore() const mockButton = { focus: vi.fn() } as unknown as HTMLElement Object.defineProperty(document, 'activeElement', { value: mockButton, configurable: true }) store.open({ url: 'http://192.168.1.228:9999', title: 'Custom' }) store.close() expect(store.isOpen).toBe(false) expect(store.url).toBe('') // requestAnimationFrame fires the focus restore callback vi.runAllTimers() vi.useRealTimers() }) })