chore(web): add some tests (#29772)
This commit is contained in:
738
web/app/components/explore/installed-app/index.spec.tsx
Normal file
738
web/app/components/explore/installed-app/index.spec.tsx
Normal file
@@ -0,0 +1,738 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
// Mock external dependencies BEFORE imports
|
||||
jest.mock('use-context-selector', () => ({
|
||||
useContext: jest.fn(),
|
||||
createContext: jest.fn(() => ({})),
|
||||
}))
|
||||
jest.mock('@/context/web-app-context', () => ({
|
||||
useWebAppStore: jest.fn(),
|
||||
}))
|
||||
jest.mock('@/service/access-control', () => ({
|
||||
useGetUserCanAccessApp: jest.fn(),
|
||||
}))
|
||||
jest.mock('@/service/use-explore', () => ({
|
||||
useGetInstalledAppAccessModeByAppId: jest.fn(),
|
||||
useGetInstalledAppParams: jest.fn(),
|
||||
useGetInstalledAppMeta: jest.fn(),
|
||||
}))
|
||||
|
||||
import { useContext } from 'use-context-selector'
|
||||
import InstalledApp from './index'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore'
|
||||
import type { InstalledApp as InstalledAppType } from '@/models/explore'
|
||||
|
||||
/**
|
||||
* Mock child components for unit testing
|
||||
*
|
||||
* RATIONALE FOR MOCKING:
|
||||
* - TextGenerationApp: 648 lines, complex batch processing, task management, file uploads
|
||||
* - ChatWithHistory: 576-line custom hook, complex conversation/history management, 30+ context values
|
||||
*
|
||||
* These components are too complex to test as real components. Using real components would:
|
||||
* 1. Require mocking dozens of their dependencies (services, contexts, hooks)
|
||||
* 2. Make tests fragile and coupled to child component implementation details
|
||||
* 3. Violate the principle of testing one component in isolation
|
||||
*
|
||||
* For a container component like InstalledApp, its responsibility is to:
|
||||
* - Correctly route to the appropriate child component based on app mode
|
||||
* - Pass the correct props to child components
|
||||
* - Handle loading/error states before rendering children
|
||||
*
|
||||
* The internal logic of ChatWithHistory and TextGenerationApp should be tested
|
||||
* in their own dedicated test files.
|
||||
*/
|
||||
jest.mock('@/app/components/share/text-generation', () => ({
|
||||
__esModule: true,
|
||||
default: ({ isInstalledApp, installedAppInfo, isWorkflow }: {
|
||||
isInstalledApp?: boolean
|
||||
installedAppInfo?: InstalledAppType
|
||||
isWorkflow?: boolean
|
||||
}) => (
|
||||
<div data-testid="text-generation-app">
|
||||
Text Generation App
|
||||
{isWorkflow && ' (Workflow)'}
|
||||
{isInstalledApp && ` - ${installedAppInfo?.id}`}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/chat/chat-with-history', () => ({
|
||||
__esModule: true,
|
||||
default: ({ installedAppInfo, className }: {
|
||||
installedAppInfo?: InstalledAppType
|
||||
className?: string
|
||||
}) => (
|
||||
<div data-testid="chat-with-history" className={className}>
|
||||
Chat With History - {installedAppInfo?.id}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('InstalledApp', () => {
|
||||
const mockUpdateAppInfo = jest.fn()
|
||||
const mockUpdateWebAppAccessMode = jest.fn()
|
||||
const mockUpdateAppParams = jest.fn()
|
||||
const mockUpdateWebAppMeta = jest.fn()
|
||||
const mockUpdateUserCanAccessApp = jest.fn()
|
||||
|
||||
const mockInstalledApp = {
|
||||
id: 'installed-app-123',
|
||||
app: {
|
||||
id: 'app-123',
|
||||
name: 'Test App',
|
||||
mode: AppModeEnum.CHAT,
|
||||
icon_type: 'emoji' as const,
|
||||
icon: '🚀',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: '',
|
||||
description: 'Test description',
|
||||
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: {},
|
||||
}
|
||||
|
||||
const mockAppMeta = {
|
||||
tool_icons: {},
|
||||
}
|
||||
|
||||
const mockWebAppAccessMode = {
|
||||
accessMode: AccessMode.PUBLIC,
|
||||
}
|
||||
|
||||
const mockUserCanAccessApp = {
|
||||
result: true,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Mock useContext
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [mockInstalledApp],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
// Mock useWebAppStore
|
||||
;(useWebAppStore as unknown as jest.Mock).mockImplementation((
|
||||
selector: (state: {
|
||||
updateAppInfo: jest.Mock
|
||||
updateWebAppAccessMode: jest.Mock
|
||||
updateAppParams: jest.Mock
|
||||
updateWebAppMeta: jest.Mock
|
||||
updateUserCanAccessApp: jest.Mock
|
||||
}) => unknown,
|
||||
) => {
|
||||
const state = {
|
||||
updateAppInfo: mockUpdateAppInfo,
|
||||
updateWebAppAccessMode: mockUpdateWebAppAccessMode,
|
||||
updateAppParams: mockUpdateAppParams,
|
||||
updateWebAppMeta: mockUpdateWebAppMeta,
|
||||
updateUserCanAccessApp: mockUpdateUserCanAccessApp,
|
||||
}
|
||||
return selector(state)
|
||||
})
|
||||
|
||||
// Mock service hooks with default success states
|
||||
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: mockWebAppAccessMode,
|
||||
error: null,
|
||||
})
|
||||
|
||||
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: mockAppParams,
|
||||
error: null,
|
||||
})
|
||||
|
||||
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: mockAppMeta,
|
||||
error: null,
|
||||
})
|
||||
|
||||
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
|
||||
data: mockUserCanAccessApp,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading state when fetching app params', () => {
|
||||
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
|
||||
isFetching: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { container } = render(<InstalledApp id="installed-app-123" />)
|
||||
const svg = container.querySelector('svg.spin-animation')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading state when fetching app meta', () => {
|
||||
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
|
||||
isFetching: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { container } = render(<InstalledApp id="installed-app-123" />)
|
||||
const svg = container.querySelector('svg.spin-animation')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading state when fetching web app access mode', () => {
|
||||
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
|
||||
isFetching: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { container } = render(<InstalledApp id="installed-app-123" />)
|
||||
const svg = container.querySelector('svg.spin-animation')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading state when fetching installed apps', () => {
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [mockInstalledApp],
|
||||
isFetchingInstalledApps: true,
|
||||
})
|
||||
|
||||
const { container } = render(<InstalledApp id="installed-app-123" />)
|
||||
const svg = container.querySelector('svg.spin-animation')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render app not found (404) when installedApp does not exist', () => {
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="nonexistent-app" />)
|
||||
expect(screen.getByText(/404/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error States', () => {
|
||||
it('should render error when app params fails to load', () => {
|
||||
const error = new Error('Failed to load app params')
|
||||
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: null,
|
||||
error,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByText(/Failed to load app params/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render error when app meta fails to load', () => {
|
||||
const error = new Error('Failed to load app meta')
|
||||
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: null,
|
||||
error,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByText(/Failed to load app meta/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render error when web app access mode fails to load', () => {
|
||||
const error = new Error('Failed to load access mode')
|
||||
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: null,
|
||||
error,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByText(/Failed to load access mode/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render error when user access check fails', () => {
|
||||
const error = new Error('Failed to check user access')
|
||||
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
|
||||
data: null,
|
||||
error,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByText(/Failed to check user access/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render no permission (403) when user cannot access app', () => {
|
||||
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
|
||||
data: { result: false },
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByText(/403/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/no permission/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('App Mode Rendering', () => {
|
||||
it('should render ChatWithHistory for CHAT mode', () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ChatWithHistory for ADVANCED_CHAT mode', () => {
|
||||
const advancedChatApp = {
|
||||
...mockInstalledApp,
|
||||
app: {
|
||||
...mockInstalledApp.app,
|
||||
mode: AppModeEnum.ADVANCED_CHAT,
|
||||
},
|
||||
}
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [advancedChatApp],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ChatWithHistory for AGENT_CHAT mode', () => {
|
||||
const agentChatApp = {
|
||||
...mockInstalledApp,
|
||||
app: {
|
||||
...mockInstalledApp.app,
|
||||
mode: AppModeEnum.AGENT_CHAT,
|
||||
},
|
||||
}
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [agentChatApp],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render TextGenerationApp for COMPLETION mode', () => {
|
||||
const completionApp = {
|
||||
...mockInstalledApp,
|
||||
app: {
|
||||
...mockInstalledApp.app,
|
||||
mode: AppModeEnum.COMPLETION,
|
||||
},
|
||||
}
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [completionApp],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByTestId('text-generation-app')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Text Generation App/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/Workflow/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render TextGenerationApp with workflow flag for WORKFLOW mode', () => {
|
||||
const workflowApp = {
|
||||
...mockInstalledApp,
|
||||
app: {
|
||||
...mockInstalledApp.app,
|
||||
mode: AppModeEnum.WORKFLOW,
|
||||
},
|
||||
}
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [workflowApp],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByTestId('text-generation-app')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Workflow/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should use id prop to find installed app', () => {
|
||||
const app1 = { ...mockInstalledApp, id: 'app-1' }
|
||||
const app2 = { ...mockInstalledApp, id: 'app-2' }
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [app1, app2],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="app-2" />)
|
||||
expect(screen.getByText(/app-2/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle id that does not match any installed app', () => {
|
||||
render(<InstalledApp id="nonexistent-id" />)
|
||||
expect(screen.getByText(/404/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Effects', () => {
|
||||
it('should update app info when installedApp is available', async () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppInfo).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
app_id: 'installed-app-123',
|
||||
site: expect.objectContaining({
|
||||
title: 'Test App',
|
||||
icon_type: 'emoji',
|
||||
icon: '🚀',
|
||||
icon_background: '#FFFFFF',
|
||||
icon_url: '',
|
||||
prompt_public: false,
|
||||
copyright: '',
|
||||
show_workflow_steps: true,
|
||||
use_icon_as_answer_icon: false,
|
||||
}),
|
||||
plan: 'basic',
|
||||
custom_config: null,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update app info to null when installedApp is not found', async () => {
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="nonexistent-app" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppInfo).toHaveBeenCalledWith(null)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update app params when data is available', async () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppParams).toHaveBeenCalledWith(mockAppParams)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update app meta when data is available', async () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateWebAppMeta).toHaveBeenCalledWith(mockAppMeta)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update web app access mode when data is available', async () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateWebAppAccessMode).toHaveBeenCalledWith(AccessMode.PUBLIC)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update user can access app when data is available', async () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateUserCanAccessApp).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update user can access app to false when result is false', async () => {
|
||||
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
|
||||
data: { result: false },
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateUserCanAccessApp).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should update user can access app to false when data is null', async () => {
|
||||
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateUserCanAccessApp).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not update app params when data is null', async () => {
|
||||
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppInfo).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
expect(mockUpdateAppParams).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not update app meta when data is null', async () => {
|
||||
;(useGetInstalledAppMeta as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppInfo).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
expect(mockUpdateWebAppMeta).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not update access mode when data is null', async () => {
|
||||
;(useGetInstalledAppAccessModeByAppId as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAppInfo).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
expect(mockUpdateWebAppAccessMode).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty installedApps array', () => {
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByText(/404/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiple installed apps and find the correct one', () => {
|
||||
const otherApp = {
|
||||
...mockInstalledApp,
|
||||
id: 'other-app-id',
|
||||
app: {
|
||||
...mockInstalledApp.app,
|
||||
name: 'Other App',
|
||||
},
|
||||
}
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [otherApp, mockInstalledApp],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
// Should find and render the correct app
|
||||
expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
|
||||
expect(screen.getByText(/installed-app-123/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply correct CSS classes to container', () => {
|
||||
const { container } = render(<InstalledApp id="installed-app-123" />)
|
||||
const mainDiv = container.firstChild as HTMLElement
|
||||
expect(mainDiv).toHaveClass('h-full', 'bg-background-default', 'py-2', 'pl-0', 'pr-2', 'sm:p-2')
|
||||
})
|
||||
|
||||
it('should apply correct CSS classes to ChatWithHistory', () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
const chatComponent = screen.getByTestId('chat-with-history')
|
||||
expect(chatComponent).toHaveClass('overflow-hidden', 'rounded-2xl', 'shadow-md')
|
||||
})
|
||||
|
||||
it('should handle rapid id prop changes', async () => {
|
||||
const app1 = { ...mockInstalledApp, id: 'app-1' }
|
||||
const app2 = { ...mockInstalledApp, id: 'app-2' }
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [app1, app2],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
const { rerender } = render(<InstalledApp id="app-1" />)
|
||||
expect(screen.getByText(/app-1/)).toBeInTheDocument()
|
||||
|
||||
rerender(<InstalledApp id="app-2" />)
|
||||
expect(screen.getByText(/app-2/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call service hooks with correct appId', () => {
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
expect(useGetInstalledAppAccessModeByAppId).toHaveBeenCalledWith('installed-app-123')
|
||||
expect(useGetInstalledAppParams).toHaveBeenCalledWith('installed-app-123')
|
||||
expect(useGetInstalledAppMeta).toHaveBeenCalledWith('installed-app-123')
|
||||
expect(useGetUserCanAccessApp).toHaveBeenCalledWith({
|
||||
appId: 'app-123',
|
||||
isInstalledApp: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call service hooks with null when installedApp is not found', () => {
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="nonexistent-app" />)
|
||||
|
||||
expect(useGetInstalledAppAccessModeByAppId).toHaveBeenCalledWith(null)
|
||||
expect(useGetInstalledAppParams).toHaveBeenCalledWith(null)
|
||||
expect(useGetInstalledAppMeta).toHaveBeenCalledWith(null)
|
||||
expect(useGetUserCanAccessApp).toHaveBeenCalledWith({
|
||||
appId: undefined,
|
||||
isInstalledApp: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// React.memo wraps the component with a special $$typeof symbol
|
||||
const componentType = (InstalledApp as React.MemoExoticComponent<typeof InstalledApp>).$$typeof
|
||||
expect(componentType).toBeDefined()
|
||||
})
|
||||
|
||||
it('should re-render when props change', () => {
|
||||
const { rerender } = render(<InstalledApp id="installed-app-123" />)
|
||||
expect(screen.getByText(/installed-app-123/)).toBeInTheDocument()
|
||||
|
||||
// Change to a different app
|
||||
const differentApp = {
|
||||
...mockInstalledApp,
|
||||
id: 'different-app-456',
|
||||
app: {
|
||||
...mockInstalledApp.app,
|
||||
name: 'Different App',
|
||||
},
|
||||
}
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [differentApp],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
|
||||
rerender(<InstalledApp id="different-app-456" />)
|
||||
expect(screen.getByText(/different-app-456/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain component stability across re-renders with same props', () => {
|
||||
const { rerender } = render(<InstalledApp id="installed-app-123" />)
|
||||
const initialCallCount = mockUpdateAppInfo.mock.calls.length
|
||||
|
||||
// Rerender with same props - useEffect may still run due to dependencies
|
||||
rerender(<InstalledApp id="installed-app-123" />)
|
||||
|
||||
// Component should render successfully
|
||||
expect(screen.getByTestId('chat-with-history')).toBeInTheDocument()
|
||||
|
||||
// Mock calls might increase due to useEffect, but component should be stable
|
||||
expect(mockUpdateAppInfo.mock.calls.length).toBeGreaterThanOrEqual(initialCallCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Render Priority', () => {
|
||||
it('should show error before loading state', () => {
|
||||
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
|
||||
isFetching: true,
|
||||
data: null,
|
||||
error: new Error('Some error'),
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
// Error should take precedence over loading
|
||||
expect(screen.getByText(/Some error/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show error before permission check', () => {
|
||||
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
|
||||
isFetching: false,
|
||||
data: null,
|
||||
error: new Error('Params error'),
|
||||
})
|
||||
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
|
||||
data: { result: false },
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="installed-app-123" />)
|
||||
// Error should take precedence over permission
|
||||
expect(screen.getByText(/Params error/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/403/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show permission error before 404', () => {
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
;(useGetUserCanAccessApp as jest.Mock).mockReturnValue({
|
||||
data: { result: false },
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(<InstalledApp id="nonexistent-app" />)
|
||||
// Permission should take precedence over 404
|
||||
expect(screen.getByText(/403/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/404/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading before 404', () => {
|
||||
;(useContext as jest.Mock).mockReturnValue({
|
||||
installedApps: [],
|
||||
isFetchingInstalledApps: false,
|
||||
})
|
||||
;(useGetInstalledAppParams as jest.Mock).mockReturnValue({
|
||||
isFetching: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const { container } = render(<InstalledApp id="nonexistent-app" />)
|
||||
// Loading should take precedence over 404
|
||||
const svg = container.querySelector('svg.spin-animation')
|
||||
expect(svg).toBeInTheDocument()
|
||||
expect(screen.queryByText(/404/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user