/** * Integration test: Installed App Flow * * Tests the end-to-end user flow of installed apps: sidebar navigation, * mode-based routing (Chat / Completion / Workflow), and lifecycle * operations (pin/unpin, delete). */ import type { Mock } from 'vitest' import type { InstalledApp as InstalledAppModel } from '@/models/explore' import { render, screen, waitFor } from '@testing-library/react' import { useContext } from 'use-context-selector' import InstalledApp from '@/app/components/explore/installed-app' import { useWebAppStore } from '@/context/web-app-context' import { AccessMode } from '@/models/access-control' import { useGetUserCanAccessApp } from '@/service/access-control' import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore' import { AppModeEnum } from '@/types/app' // Mock external dependencies vi.mock('use-context-selector', () => ({ useContext: vi.fn(), createContext: vi.fn(() => ({})), })) vi.mock('@/context/web-app-context', () => ({ useWebAppStore: vi.fn(), })) vi.mock('@/service/access-control', () => ({ useGetUserCanAccessApp: vi.fn(), })) vi.mock('@/service/use-explore', () => ({ useGetInstalledAppAccessModeByAppId: vi.fn(), useGetInstalledAppParams: vi.fn(), useGetInstalledAppMeta: vi.fn(), })) vi.mock('@/app/components/share/text-generation', () => ({ default: ({ isWorkflow }: { isWorkflow?: boolean }) => (
Text Generation {isWorkflow && ' (Workflow)'}
), })) vi.mock('@/app/components/base/chat/chat-with-history', () => ({ default: ({ installedAppInfo }: { installedAppInfo?: InstalledAppModel }) => (
Chat - {' '} {installedAppInfo?.app.name}
), })) describe('Installed App Flow', () => { const mockUpdateAppInfo = vi.fn() const mockUpdateWebAppAccessMode = vi.fn() const mockUpdateAppParams = vi.fn() const mockUpdateWebAppMeta = vi.fn() const mockUpdateUserCanAccessApp = vi.fn() const createInstalledApp = (mode: AppModeEnum = AppModeEnum.CHAT): InstalledAppModel => ({ id: 'installed-app-1', app: { id: 'real-app-id', name: 'Integration Test App', mode, icon_type: 'emoji', icon: '🧪', icon_background: '#FFFFFF', icon_url: '', description: 'Test app for integration', use_icon_as_answer_icon: false, }, uninstallable: true, is_pinned: false, }) const mockAppParams = { user_input_form: [], file_upload: { image: { enabled: false, number_limits: 0, transfer_methods: [] } }, system_parameters: {}, } type MockOverrides = { context?: { installedApps?: InstalledAppModel[], isFetchingInstalledApps?: boolean } accessMode?: { isFetching?: boolean, data?: unknown, error?: unknown } params?: { isFetching?: boolean, data?: unknown, error?: unknown } meta?: { isFetching?: boolean, data?: unknown, error?: unknown } userAccess?: { data?: unknown, error?: unknown } } const setupDefaultMocks = (app?: InstalledAppModel, overrides: MockOverrides = {}) => { ;(useContext as Mock).mockReturnValue({ installedApps: app ? [app] : [], isFetchingInstalledApps: false, ...overrides.context, }) ;(useWebAppStore as unknown as Mock).mockImplementation((selector: (state: Record) => unknown) => { return selector({ updateAppInfo: mockUpdateAppInfo, updateWebAppAccessMode: mockUpdateWebAppAccessMode, updateAppParams: mockUpdateAppParams, updateWebAppMeta: mockUpdateWebAppMeta, updateUserCanAccessApp: mockUpdateUserCanAccessApp, }) }) ;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({ isFetching: false, data: { accessMode: AccessMode.PUBLIC }, error: null, ...overrides.accessMode, }) ;(useGetInstalledAppParams as Mock).mockReturnValue({ isFetching: false, data: mockAppParams, error: null, ...overrides.params, }) ;(useGetInstalledAppMeta as Mock).mockReturnValue({ isFetching: false, data: { tool_icons: {} }, error: null, ...overrides.meta, }) ;(useGetUserCanAccessApp as Mock).mockReturnValue({ data: { result: true }, error: null, ...overrides.userAccess, }) } beforeEach(() => { vi.clearAllMocks() }) describe('Mode-Based Routing', () => { it.each([ [AppModeEnum.CHAT, 'chat-with-history'], [AppModeEnum.ADVANCED_CHAT, 'chat-with-history'], [AppModeEnum.AGENT_CHAT, 'chat-with-history'], ])('should render ChatWithHistory for %s mode', (mode, testId) => { const app = createInstalledApp(mode) setupDefaultMocks(app) render() expect(screen.getByTestId(testId)).toBeInTheDocument() expect(screen.getByText(/Integration Test App/)).toBeInTheDocument() }) it('should render TextGenerationApp for COMPLETION mode', () => { const app = createInstalledApp(AppModeEnum.COMPLETION) setupDefaultMocks(app) render() expect(screen.getByTestId('text-generation-app')).toBeInTheDocument() expect(screen.getByText('Text Generation')).toBeInTheDocument() expect(screen.queryByText(/Workflow/)).not.toBeInTheDocument() }) it('should render TextGenerationApp with workflow flag for WORKFLOW mode', () => { const app = createInstalledApp(AppModeEnum.WORKFLOW) setupDefaultMocks(app) render() expect(screen.getByTestId('text-generation-app')).toBeInTheDocument() expect(screen.getByText(/Workflow/)).toBeInTheDocument() }) }) describe('Data Loading Flow', () => { it('should show loading spinner when params are being fetched', () => { const app = createInstalledApp() setupDefaultMocks(app, { params: { isFetching: true, data: null } }) const { container } = render() expect(container.querySelector('svg.spin-animation')).toBeInTheDocument() expect(screen.queryByTestId('chat-with-history')).not.toBeInTheDocument() }) it('should render content when all data is available', () => { const app = createInstalledApp() setupDefaultMocks(app) render() expect(screen.getByTestId('chat-with-history')).toBeInTheDocument() }) }) describe('Error Handling Flow', () => { it('should show error state when API fails', () => { const app = createInstalledApp() setupDefaultMocks(app, { params: { data: null, error: new Error('Network error') } }) render() expect(screen.getByText(/Network error/)).toBeInTheDocument() }) it('should show 404 when app is not found', () => { setupDefaultMocks(undefined, { accessMode: { data: null }, params: { data: null }, meta: { data: null }, userAccess: { data: null }, }) render() expect(screen.getByText(/404/)).toBeInTheDocument() }) it('should show 403 when user has no permission', () => { const app = createInstalledApp() setupDefaultMocks(app, { userAccess: { data: { result: false } } }) render() expect(screen.getByText(/403/)).toBeInTheDocument() }) }) describe('State Synchronization', () => { it('should update all stores when app data is loaded', async () => { const app = createInstalledApp() setupDefaultMocks(app) render() await waitFor(() => { expect(mockUpdateAppInfo).toHaveBeenCalledWith( expect.objectContaining({ app_id: 'installed-app-1', site: expect.objectContaining({ title: 'Integration Test App', icon: '🧪', }), }), ) expect(mockUpdateAppParams).toHaveBeenCalledWith(mockAppParams) expect(mockUpdateWebAppMeta).toHaveBeenCalledWith({ tool_icons: {} }) expect(mockUpdateWebAppAccessMode).toHaveBeenCalledWith(AccessMode.PUBLIC) expect(mockUpdateUserCanAccessApp).toHaveBeenCalledWith(true) }) }) }) })