mirror of https://github.com/langgenius/dify.git
261 lines
8.3 KiB
TypeScript
261 lines
8.3 KiB
TypeScript
/**
|
|
* 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 }) => (
|
|
<div data-testid="text-generation-app">
|
|
Text Generation
|
|
{isWorkflow && ' (Workflow)'}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
vi.mock('@/app/components/base/chat/chat-with-history', () => ({
|
|
default: ({ installedAppInfo }: { installedAppInfo?: InstalledAppModel }) => (
|
|
<div data-testid="chat-with-history">
|
|
Chat -
|
|
{' '}
|
|
{installedAppInfo?.app.name}
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
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<string, Mock>) => 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(<InstalledApp id="installed-app-1" />)
|
|
|
|
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(<InstalledApp id="installed-app-1" />)
|
|
|
|
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(<InstalledApp id="installed-app-1" />)
|
|
|
|
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(<InstalledApp id="installed-app-1" />)
|
|
|
|
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(<InstalledApp id="installed-app-1" />)
|
|
|
|
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(<InstalledApp id="installed-app-1" />)
|
|
|
|
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(<InstalledApp id="nonexistent" />)
|
|
|
|
expect(screen.getByText(/404/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should show 403 when user has no permission', () => {
|
|
const app = createInstalledApp()
|
|
setupDefaultMocks(app, { userAccess: { data: { result: false } } })
|
|
|
|
render(<InstalledApp id="installed-app-1" />)
|
|
|
|
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(<InstalledApp id="installed-app-1" />)
|
|
|
|
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)
|
|
})
|
|
})
|
|
})
|
|
})
|