// @vitest-environment happy-dom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createApp, nextTick } from 'vue' import AppOnboardingFlow from '../src/components/dashboard/AppOnboardingFlow.vue' const runtimeMocks = vi.hoisted(() => { const app = { app_id: 'com.test.runtime-onboarding', name: 'Runtime onboarding app', owner_org: 'org-runtime-onboarding', existing_app: false, icon_url: null, ios_store_url: null, android_store_url: null, } return { app, createApp: vi.fn(async () => ({ ok: true as const, app, usedAppId: app.app_id, originalAppId: app.app_id, wasRetried: false, })), createDefaultApiKey: vi.fn(), findUsablePlainApiKey: vi.fn(async (): Promise => 'runtime-api-key'), main: { auth: { id: 'user-runtime-onboarding' }, awaitInitialLoad: vi.fn(async () => undefined), isAdmin: false, plans: [], user: { id: 'user-runtime-onboarding', onboarding: {} } as { id: string, onboarding: Record } | null, }, organizationStore: { getOrgByAppId: vi.fn(), setCurrentOrganization: vi.fn(), awaitInitialLoad: vi.fn(async () => undefined), currentOrganization: { gid: 'org-runtime-onboarding', name: 'Runtime organization' }, organizations: [], updateAppOnboarding: vi.fn(), upsertOrganizationApp: vi.fn(), }, query: {} as Record, router: { push: vi.fn(), replace: vi.fn(async () => undefined), }, } }) vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }), })) vi.mock('vue-router', () => ({ useRoute: () => ({ query: runtimeMocks.query }), useRouter: () => runtimeMocks.router, })) vi.mock('vue-sonner', () => ({ toast: { error: vi.fn(), info: vi.fn(), success: vi.fn() } })) vi.mock('~/services/apikeys', async (importOriginal) => { const actual = await importOriginal() return { ...actual, createDefaultApiKey: runtimeMocks.createDefaultApiKey, findUsablePlainApiKey: runtimeMocks.findUsablePlainApiKey, } }) vi.mock('~/services/onboardingTracking', () => ({ sendOnboardingEvent: vi.fn() })) vi.mock('~/services/console', () => ({ getLocalConfig: () => ({ apiHost: 'https://api.capgo.app' }), isLocal: () => false, useConsole: () => { const query = { eq: () => query, maybeSingle: async () => ({ data: null, error: null }), select: () => query, single: async () => ({ data: runtimeMocks.app, error: null }), } return { auth: { getClaims: async () => ({ data: { claims: { sub: runtimeMocks.main.auth.id } } }) }, from: () => query, rpc: async () => ({ data: null, error: null }), } }, })) vi.mock('~/stores/dashboardApps', () => ({ useDashboardAppsStore: () => ({ upsertApp: vi.fn() }) })) vi.mock('~/stores/dialogv2', () => ({ useDialogV2Store: () => ({ lastButtonRole: null, onDialogDismiss: vi.fn(async () => undefined), openDialog: vi.fn(), }), })) vi.mock('~/stores/main', () => ({ useMainStore: () => runtimeMocks.main })) vi.mock('~/stores/organization', () => ({ useOrganizationStore: () => runtimeMocks.organizationStore })) vi.mock('~/utils/onboardingAppCreateHelpers', async (importOriginal) => { const actual = await importOriginal() return { ...actual, createOnboardingAppWithFallbackIds: runtimeMocks.createApp } }) vi.mock('~/utils/onboardingProgressPersistence', () => ({ createOnboardingProgressPersistence: () => ({ abort: vi.fn(), isAborted: () => false, isBlocked: () => false, persist: async () => 'persisted', }), shouldInitializeOnboardingProgressTracking: () => true, })) interface MountedFlow { app: ReturnType container: HTMLDivElement } const mountedFlows: MountedFlow[] = [] async function mountFlow(query: Record = {}, props: { setupAppId?: string } = {}) { runtimeMocks.query = query const container = document.createElement('div') document.body.appendChild(container) const app = createApp(AppOnboardingFlow, { onboarding: false, ...props }) app.config.warnHandler = () => undefined app.mount(container) const mounted = { app, container } mountedFlows.push(mounted) return mounted } function element(container: Element, selector: string) { const value = container.querySelector(selector) if (!value) throw new Error(`Missing onboarding element: ${selector}`) return value } async function click(container: Element, selector: string) { element(container, selector).click() await nextTick() } async function reachIconStep(container: Element) { await vi.waitFor(() => expect(container.querySelector('[data-test="app-onboarding-name"]')).not.toBeNull()) const nameInput = element(container, '[data-test="app-onboarding-name"]') nameInput.value = 'Runtime onboarding app' nameInput.dispatchEvent(new Event('input', { bubbles: true })) await nextTick() await click(container, '[data-test="app-onboarding-continue"]') await vi.waitFor(() => expect(container.textContent).toContain('app-onboarding-icon-step-title')) } beforeEach(() => { runtimeMocks.query = {} runtimeMocks.createApp.mockClear() runtimeMocks.router.push.mockClear() runtimeMocks.router.replace.mockClear() runtimeMocks.createDefaultApiKey.mockReset() runtimeMocks.createDefaultApiKey.mockResolvedValue({ data: { key: 'runtime-created-api-key' }, error: null }) runtimeMocks.findUsablePlainApiKey.mockReset() runtimeMocks.findUsablePlainApiKey.mockResolvedValue('runtime-api-key') runtimeMocks.main.awaitInitialLoad.mockClear() runtimeMocks.main.user = { id: runtimeMocks.main.auth.id, onboarding: {} } runtimeMocks.organizationStore.awaitInitialLoad.mockClear() runtimeMocks.organizationStore.updateAppOnboarding.mockClear() Object.defineProperty(window, 'matchMedia', { configurable: true, value: vi.fn(() => ({ addEventListener: vi.fn(), addListener: vi.fn(), matches: false, removeEventListener: vi.fn(), removeListener: vi.fn(), })), }) }) afterEach(() => { for (const mounted of mountedFlows.splice(0)) { mounted.app.unmount() mounted.container.remove() } }) describe('app onboarding API key runtime loading', () => { it('reuses a key when a fresh existing-org wizard is opened', async () => { const { container } = await mountFlow() await vi.waitFor(() => expect(container.querySelector('[data-test="app-onboarding-name"]')).not.toBeNull()) await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1)) expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledWith( expect.anything(), runtimeMocks.main.auth.id, runtimeMocks.organizationStore.currentOrganization.gid, undefined, ) expect(runtimeMocks.createDefaultApiKey).not.toHaveBeenCalled() }) it('uses the authenticated user ID while the public profile is still loading', async () => { runtimeMocks.main.user = null await mountFlow() await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1)) expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledWith( expect.anything(), runtimeMocks.main.auth.id, runtimeMocks.organizationStore.currentOrganization.gid, undefined, ) }) it('retries a settled failed load when setup continues on Getting started', async () => { const loadError = new Error('transient API-key failure') const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined) runtimeMocks.findUsablePlainApiKey .mockRejectedValueOnce(loadError) .mockResolvedValueOnce('runtime-retried-api-key') try { const { container } = await mountFlow() await vi.waitFor(() => expect(consoleError).toHaveBeenCalledWith('Cannot ensure API key', loadError)) await reachIconStep(container) await click(container, '[data-test="app-onboarding-continue"]') await vi.waitFor(() => expect(runtimeMocks.createApp).toHaveBeenCalledTimes(1)) await vi.waitFor(() => expect(runtimeMocks.router.replace).toHaveBeenCalledWith({ path: `/app/${runtimeMocks.app.app_id}/getting-started`, state: { capgoOnboardingSetupHandoff: expect.objectContaining({ appId: runtimeMocks.app.app_id, flow: 'existing_org', previousStep: 'app_icon', }), }, })) expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1) // Getting started mounts the setup flow for the created app. await mountFlow({}, { setupAppId: runtimeMocks.app.app_id }) await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(2)) expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenLastCalledWith( expect.anything(), runtimeMocks.main.auth.id, runtimeMocks.organizationStore.currentOrganization.gid, runtimeMocks.app.app_id, ) } finally { consoleError.mockRestore() } }) it('shares an in-flight key load across concurrent onboarding component instances', async () => { let finishLookup: ((key: string | null) => void) | undefined runtimeMocks.findUsablePlainApiKey.mockImplementation(() => new Promise((resolve) => { finishLookup = resolve })) await Promise.all([ mountFlow(), mountFlow(), ]) await vi.waitFor(() => expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalled()) expect(runtimeMocks.findUsablePlainApiKey).toHaveBeenCalledTimes(1) finishLookup?.(null) await vi.waitFor(() => expect(runtimeMocks.createDefaultApiKey).toHaveBeenCalledTimes(1)) }) })