/** * Archy Integration Tests * * Tests archyBridge message handling, useArchy composable, * and ArchyAppsGrid component behavior. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // ═══════════════════════════════════════════════════════════════════ // archyBridge — postMessage handling // ═══════════════════════════════════════════════════════════════════ describe('archyBridge: postMessage protocol', () => { let originalParent: Window let messageHandler: ((event: MessageEvent) => void) | null = null beforeEach(() => { originalParent = window.parent // Mock window.parent to simulate being in an iframe Object.defineProperty(window, 'parent', { value: { postMessage: vi.fn(), }, writable: true, configurable: true, }) // Capture addEventListener calls to grab the message handler const originalAddEventListener = window.addEventListener.bind(window) vi.spyOn(window, 'addEventListener').mockImplementation((type: string, handler: any) => { if (type === 'message') { messageHandler = handler } return originalAddEventListener(type, handler) }) }) afterEach(() => { Object.defineProperty(window, 'parent', { value: originalParent, writable: true, configurable: true, }) vi.restoreAllMocks() messageHandler = null }) it('isInArchy returns true when window.parent !== window', async () => { // Dynamic import to get fresh module state const { archyBridge } = await import('@/services/archyBridge') expect(archyBridge.isInArchy()).toBe(true) }) it('init sends ready message to parent', async () => { const { archyBridge } = await import('@/services/archyBridge') archyBridge.init() expect(window.parent.postMessage).toHaveBeenCalledWith( { type: 'ready' }, '*', ) archyBridge.destroy() }) it('requestContext sends context:request message', async () => { const { archyBridge } = await import('@/services/archyBridge') archyBridge.init() // Fire and forget — just verify the message shape archyBridge.requestContext('apps').catch(() => {}) expect(window.parent.postMessage).toHaveBeenCalledWith( expect.objectContaining({ type: 'context:request', category: 'apps', }), '*', ) archyBridge.destroy() }) it('requestAction sends action:request message', async () => { const { archyBridge } = await import('@/services/archyBridge') archyBridge.init() // Fire and forget archyBridge.requestAction('open-app', { appId: 'mempool' }).catch(() => {}) expect(window.parent.postMessage).toHaveBeenCalledWith( expect.objectContaining({ type: 'action:request', action: 'open-app', params: { appId: 'mempool' }, }), '*', ) archyBridge.destroy() }) it('requestTheme sends theme:request message', async () => { const { archyBridge } = await import('@/services/archyBridge') archyBridge.init() archyBridge.requestTheme() expect(window.parent.postMessage).toHaveBeenCalledWith( { type: 'theme:request' }, '*', ) archyBridge.destroy() }) it('onPermissionsUpdate registers and fires callback', async () => { const { archyBridge } = await import('@/services/archyBridge') const callback = vi.fn() const unsubscribe = archyBridge.onPermissionsUpdate(callback) // Unsubscribe should be a function expect(typeof unsubscribe).toBe('function') unsubscribe() }) it('onThemeUpdate registers and fires callback', async () => { const { archyBridge } = await import('@/services/archyBridge') const callback = vi.fn() const unsubscribe = archyBridge.onThemeUpdate(callback) expect(typeof unsubscribe).toBe('function') unsubscribe() }) it('getPermissions returns empty array initially', async () => { const { archyBridge } = await import('@/services/archyBridge') const perms = archyBridge.getPermissions() expect(Array.isArray(perms)).toBe(true) }) it('getTheme returns null initially', async () => { const { archyBridge } = await import('@/services/archyBridge') const theme = archyBridge.getTheme() // May be null or have been set by a previous test expect(theme === null || typeof theme === 'object').toBe(true) }) }) // ═══════════════════════════════════════════════════════════════════ // useArchy — composable behavior // ═══════════════════════════════════════════════════════════════════ describe('useArchy: composable', () => { it('exports expected API shape', async () => { const { useArchy } = await import('@/composables/useArchy') const archy = useArchy() expect(archy).toHaveProperty('isEmbedded') expect(archy).toHaveProperty('isInitialized') expect(archy).toHaveProperty('permissions') expect(archy).toHaveProperty('accentColor') expect(archy).toHaveProperty('installedApps') expect(archy).toHaveProperty('systemInfo') expect(archy).toHaveProperty('networkInfo') expect(archy).toHaveProperty('walletInfo') expect(archy).toHaveProperty('fileList') expect(archy).toHaveProperty('init') expect(archy).toHaveProperty('destroy') expect(archy).toHaveProperty('refreshContext') expect(archy).toHaveProperty('requestAction') expect(archy).toHaveProperty('buildArchyContext') }) it('buildArchyContext returns empty string when not initialized', async () => { const { useArchy } = await import('@/composables/useArchy') const archy = useArchy() // Not initialized, should return empty const ctx = archy.buildArchyContext() expect(typeof ctx).toBe('string') }) it('requestAction returns failure when not initialized', async () => { const { useArchy } = await import('@/composables/useArchy') const archy = useArchy() const result = await archy.requestAction('open-app', { appId: 'test' }) expect(result).toEqual({ success: false, error: 'Not initialized' }) }) it('isEmbedded and isInitialized are readonly refs', async () => { const { useArchy } = await import('@/composables/useArchy') const archy = useArchy() // Should be refs (have .value) expect(typeof archy.isEmbedded.value).toBe('boolean') expect(typeof archy.isInitialized.value).toBe('boolean') }) }) // ═══════════════════════════════════════════════════════════════════ // useArchy: buildArchyContext output format // ═══════════════════════════════════════════════════════════════════ describe('useArchy: buildArchyContext format', () => { it('context string includes wallet info when available', async () => { // We can't easily set internal state without init, so we test the format expectations const { useArchy } = await import('@/composables/useArchy') const archy = useArchy() // Context should be a string const ctx = archy.buildArchyContext() expect(typeof ctx).toBe('string') }) }) // ═══════════════════════════════════════════════════════════════════ // archy-apps.ts — data file // ═══════════════════════════════════════════════════════════════════ describe('archy-apps: data integrity', () => { it('exports ARCHY_APPS array with all major services', async () => { const { ARCHY_APPS } = await import('@/data/archy-apps') expect(Array.isArray(ARCHY_APPS)).toBe(true) expect(ARCHY_APPS.length).toBeGreaterThanOrEqual(15) // Verify all required services are present const ids = ARCHY_APPS.map((a) => a.id) expect(ids).toContain('bitcoin-core') expect(ids).toContain('lnd') expect(ids).toContain('btcpay-server') expect(ids).toContain('mempool') expect(ids).toContain('nextcloud') expect(ids).toContain('immich') expect(ids).toContain('nostr-rs-relay') expect(ids).toContain('home-assistant') expect(ids).toContain('grafana') expect(ids).toContain('searxng') expect(ids).toContain('ollama') expect(ids).toContain('penpot') expect(ids).toContain('onlyoffice') expect(ids).toContain('fedimint') expect(ids).toContain('meshtastic') }) it('each app has required fields', async () => { const { ARCHY_APPS } = await import('@/data/archy-apps') for (const app of ARCHY_APPS) { expect(app.id).toBeTruthy() expect(app.name).toBeTruthy() expect(app.description).toBeTruthy() expect(app.icon).toBeTruthy() expect(app.category).toBeTruthy() expect(app.deepLink).toBeTruthy() expect(app.deepLink.startsWith('/app/')).toBe(true) } }) it('getArchyApp looks up apps by ID', async () => { const { getArchyApp } = await import('@/data/archy-apps') const btc = getArchyApp('bitcoin-core') expect(btc).toBeDefined() expect(btc!.name).toBe('Bitcoin Core') const missing = getArchyApp('nonexistent-app') expect(missing).toBeUndefined() }) it('app IDs are unique', async () => { const { ARCHY_APPS } = await import('@/data/archy-apps') const ids = ARCHY_APPS.map((a) => a.id) expect(new Set(ids).size).toBe(ids.length) }) it('deep links follow /app/{id} pattern', async () => { const { ARCHY_APPS } = await import('@/data/archy-apps') for (const app of ARCHY_APPS) { expect(app.deepLink).toBe(`/app/${app.id}`) } }) }) // ═══════════════════════════════════════════════════════════════════ // Base-aware API paths // ═══════════════════════════════════════════════════════════════════ describe('Base-aware API paths', () => { it('import.meta.env.BASE_URL is defined', () => { // In Vite test environment, BASE_URL defaults to '/' expect(typeof import.meta.env.BASE_URL).toBe('string') expect(import.meta.env.BASE_URL).toBeTruthy() }) })