mirror of https://github.com/langgenius/dify.git
test: add unit tests for marketplace and readme panel components, including context management and rendering logic
This commit is contained in:
parent
1149b2f1f3
commit
7c639280a6
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,893 @@
|
|||
import type { PluginDetail } from '../types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, PluginSource } from '../types'
|
||||
import { BUILTIN_TOOLS_ARRAY } from './constants'
|
||||
import { ReadmeEntrance } from './entrance'
|
||||
import ReadmePanel from './index'
|
||||
import { ReadmeShowType, useReadmePanelStore } from './store'
|
||||
|
||||
// ================================
|
||||
// Mock external dependencies only
|
||||
// ================================
|
||||
|
||||
// Mock usePluginReadme hook
|
||||
const mockUsePluginReadme = vi.fn()
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
usePluginReadme: (params: { plugin_unique_identifier: string, language?: string }) => mockUsePluginReadme(params),
|
||||
}))
|
||||
|
||||
// Mock useLanguage hook
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useLanguage: () => 'en-US',
|
||||
}))
|
||||
|
||||
// Mock DetailHeader component (complex component with many dependencies)
|
||||
vi.mock('../plugin-detail-panel/detail-header', () => ({
|
||||
default: ({ detail, isReadmeView }: { detail: PluginDetail, isReadmeView: boolean }) => (
|
||||
<div data-testid="detail-header" data-is-readme-view={isReadmeView}>
|
||||
{detail.name}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// ================================
|
||||
// Test Data Factories
|
||||
// ================================
|
||||
|
||||
const createMockPluginDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => ({
|
||||
id: 'test-plugin-id',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
name: 'test-plugin',
|
||||
plugin_id: 'test-plugin-id',
|
||||
plugin_unique_identifier: 'test-plugin@1.0.0',
|
||||
declaration: {
|
||||
plugin_unique_identifier: 'test-plugin@1.0.0',
|
||||
version: '1.0.0',
|
||||
author: 'test-author',
|
||||
icon: 'test-icon.png',
|
||||
name: 'test-plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
label: { 'en-US': 'Test Plugin' } as Record<string, string>,
|
||||
description: { 'en-US': 'Test plugin description' } as Record<string, string>,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
resource: null,
|
||||
plugins: null,
|
||||
verified: true,
|
||||
endpoint: { settings: [], endpoints: [] },
|
||||
model: null,
|
||||
tags: [],
|
||||
agent_strategy: null,
|
||||
meta: { version: '1.0.0' },
|
||||
trigger: {
|
||||
events: [],
|
||||
identity: {
|
||||
author: 'test-author',
|
||||
name: 'test-plugin',
|
||||
label: { 'en-US': 'Test Plugin' } as Record<string, string>,
|
||||
description: { 'en-US': 'Test plugin description' } as Record<string, string>,
|
||||
icon: 'test-icon.png',
|
||||
tags: [],
|
||||
},
|
||||
subscription_constructor: {
|
||||
credentials_schema: [],
|
||||
oauth_schema: { client_schema: [], credentials_schema: [] },
|
||||
parameters: [],
|
||||
},
|
||||
subscription_schema: [],
|
||||
},
|
||||
},
|
||||
installation_id: 'install-123',
|
||||
tenant_id: 'tenant-123',
|
||||
endpoints_setups: 0,
|
||||
endpoints_active: 0,
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_unique_identifier: 'test-plugin@1.0.0',
|
||||
source: PluginSource.marketplace,
|
||||
status: 'active' as const,
|
||||
deprecated_reason: '',
|
||||
alternative_plugin_id: '',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Test Utilities
|
||||
// ================================
|
||||
|
||||
const createQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const renderWithQueryClient = (ui: React.ReactElement) => {
|
||||
const queryClient = createQueryClient()
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ui}
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
// ================================
|
||||
// Constants Tests
|
||||
// ================================
|
||||
describe('BUILTIN_TOOLS_ARRAY', () => {
|
||||
it('should contain expected builtin tools', () => {
|
||||
expect(BUILTIN_TOOLS_ARRAY).toContain('code')
|
||||
expect(BUILTIN_TOOLS_ARRAY).toContain('audio')
|
||||
expect(BUILTIN_TOOLS_ARRAY).toContain('time')
|
||||
expect(BUILTIN_TOOLS_ARRAY).toContain('webscraper')
|
||||
})
|
||||
|
||||
it('should have exactly 4 builtin tools', () => {
|
||||
expect(BUILTIN_TOOLS_ARRAY).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Store Tests
|
||||
// ================================
|
||||
describe('useReadmePanelStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state before each test
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
})
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have undefined currentPluginDetail initially', () => {
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('setCurrentPluginDetail', () => {
|
||||
it('should set currentPluginDetail with detail and default showType', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
act(() => {
|
||||
setCurrentPluginDetail(mockDetail)
|
||||
})
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toEqual({
|
||||
detail: mockDetail,
|
||||
showType: ReadmeShowType.drawer,
|
||||
})
|
||||
})
|
||||
|
||||
it('should set currentPluginDetail with custom showType', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
act(() => {
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.modal)
|
||||
})
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toEqual({
|
||||
detail: mockDetail,
|
||||
showType: ReadmeShowType.modal,
|
||||
})
|
||||
})
|
||||
|
||||
it('should clear currentPluginDetail when called without arguments', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
// First set a detail
|
||||
act(() => {
|
||||
setCurrentPluginDetail(mockDetail)
|
||||
})
|
||||
|
||||
// Then clear it
|
||||
act(() => {
|
||||
setCurrentPluginDetail()
|
||||
})
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should clear currentPluginDetail when called with undefined', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
// First set a detail
|
||||
act(() => {
|
||||
setCurrentPluginDetail(mockDetail)
|
||||
})
|
||||
|
||||
// Then clear it with explicit undefined
|
||||
act(() => {
|
||||
setCurrentPluginDetail(undefined)
|
||||
})
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ReadmeShowType enum', () => {
|
||||
it('should have drawer and modal types', () => {
|
||||
expect(ReadmeShowType.drawer).toBe('drawer')
|
||||
expect(ReadmeShowType.modal).toBe('modal')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// ReadmeEntrance Component Tests
|
||||
// ================================
|
||||
describe('ReadmeEntrance', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render the entrance button with full tip text', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.readmeInfo.needHelpCheckReadme')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with short tip text when showShortTip is true', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
render(<ReadmeEntrance pluginDetail={mockDetail} showShortTip />)
|
||||
|
||||
expect(screen.getByText('plugin.readmeInfo.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render divider when showShortTip is false', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={mockDetail} showShortTip={false} />)
|
||||
|
||||
expect(container.querySelector('.bg-divider-regular')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render divider when showShortTip is true', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={mockDetail} showShortTip />)
|
||||
|
||||
expect(container.querySelector('.bg-divider-regular')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply drawer mode padding class', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
const { container } = render(
|
||||
<ReadmeEntrance pluginDetail={mockDetail} showType={ReadmeShowType.drawer} />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.px-4')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
const { container } = render(
|
||||
<ReadmeEntrance pluginDetail={mockDetail} className="custom-class" />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.custom-class')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Conditional Rendering / Edge Cases
|
||||
// ================================
|
||||
describe('Conditional Rendering', () => {
|
||||
it('should return null when pluginDetail is null/undefined', () => {
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={null as unknown as PluginDetail} />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null when plugin_unique_identifier is missing', () => {
|
||||
const mockDetail = createMockPluginDetail({ plugin_unique_identifier: '' })
|
||||
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for builtin tool: code', () => {
|
||||
const mockDetail = createMockPluginDetail({ id: 'code' })
|
||||
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for builtin tool: audio', () => {
|
||||
const mockDetail = createMockPluginDetail({ id: 'audio' })
|
||||
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for builtin tool: time', () => {
|
||||
const mockDetail = createMockPluginDetail({ id: 'time' })
|
||||
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for builtin tool: webscraper', () => {
|
||||
const mockDetail = createMockPluginDetail({ id: 'webscraper' })
|
||||
|
||||
const { container } = render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render for non-builtin plugins', () => {
|
||||
const mockDetail = createMockPluginDetail({ id: 'custom-plugin' })
|
||||
|
||||
render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// User Interactions / Event Handlers
|
||||
// ================================
|
||||
describe('User Interactions', () => {
|
||||
it('should call setCurrentPluginDetail with drawer type when clicked', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toEqual({
|
||||
detail: mockDetail,
|
||||
showType: ReadmeShowType.drawer,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call setCurrentPluginDetail with modal type when clicked', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
render(<ReadmeEntrance pluginDetail={mockDetail} showType={ReadmeShowType.modal} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toEqual({
|
||||
detail: mockDetail,
|
||||
showType: ReadmeShowType.modal,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Prop Variations
|
||||
// ================================
|
||||
describe('Prop Variations', () => {
|
||||
it('should use default showType when not provided', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
render(<ReadmeEntrance pluginDetail={mockDetail} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail?.showType).toBe(ReadmeShowType.drawer)
|
||||
})
|
||||
|
||||
it('should handle modal showType correctly', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
render(<ReadmeEntrance pluginDetail={mockDetail} showType={ReadmeShowType.modal} />)
|
||||
|
||||
// Modal mode should not have px-4 class
|
||||
const container = screen.getByRole('button').parentElement
|
||||
expect(container).not.toHaveClass('px-4')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// ReadmePanel Component Tests
|
||||
// ================================
|
||||
describe('ReadmePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
// Reset mock
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should return null when no plugin detail is set', () => {
|
||||
const { container } = renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render portal content when plugin detail is set', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(screen.getByText('plugin.readmeInfo.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render DetailHeader component', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(screen.getByTestId('detail-header')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('detail-header')).toHaveAttribute('data-is-readme-view', 'true')
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// ActionButton wraps the close icon
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Loading State Tests
|
||||
// ================================
|
||||
describe('Loading State', () => {
|
||||
it('should show loading indicator when isLoading is true', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Loading component should be rendered with role="status"
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Error State Tests
|
||||
// ================================
|
||||
describe('Error State', () => {
|
||||
it('should show error message when error occurs', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: new Error('Failed to fetch'),
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(screen.getByText('plugin.readmeInfo.failedToFetch')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// No Readme Available State Tests
|
||||
// ================================
|
||||
describe('No Readme Available', () => {
|
||||
it('should show no readme message when readme is empty', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: { readme: '' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(screen.getByText('plugin.readmeInfo.noReadmeAvailable')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show no readme message when data is null', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(screen.getByText('plugin.readmeInfo.noReadmeAvailable')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Markdown Content Tests
|
||||
// ================================
|
||||
describe('Markdown Content', () => {
|
||||
it('should render markdown container when readme is available', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: { readme: '# Test Readme Content' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Markdown component container should be rendered
|
||||
// Note: The Markdown component uses dynamic import, so content may load asynchronously
|
||||
const markdownContainer = document.querySelector('.markdown-body')
|
||||
expect(markdownContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show error or no-readme message when readme is available', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: { readme: '# Test Readme Content' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Should not show error or no-readme message
|
||||
expect(screen.queryByText('plugin.readmeInfo.failedToFetch')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.readmeInfo.noReadmeAvailable')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Portal Rendering Tests (Drawer Mode)
|
||||
// ================================
|
||||
describe('Portal Rendering - Drawer Mode', () => {
|
||||
it('should render drawer styled container in drawer mode', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: { readme: '# Test' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Drawer mode has specific max-width
|
||||
const drawerContainer = document.querySelector('.max-w-\\[600px\\]')
|
||||
expect(drawerContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct drawer positioning classes', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Check for drawer-specific classes
|
||||
const backdrop = document.querySelector('.justify-start')
|
||||
expect(backdrop).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Portal Rendering Tests (Modal Mode)
|
||||
// ================================
|
||||
describe('Portal Rendering - Modal Mode', () => {
|
||||
it('should render modal styled container in modal mode', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: { readme: '# Test' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.modal)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Modal mode has different max-width
|
||||
const modalContainer = document.querySelector('.max-w-\\[800px\\]')
|
||||
expect(modalContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct modal positioning classes', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.modal)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Check for modal-specific classes
|
||||
const backdrop = document.querySelector('.items-center.justify-center')
|
||||
expect(backdrop).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// User Interactions / Event Handlers
|
||||
// ================================
|
||||
describe('User Interactions', () => {
|
||||
it('should close panel when close button is clicked', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should close panel when backdrop is clicked', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Click on the backdrop (outer div)
|
||||
const backdrop = document.querySelector('.fixed.inset-0')
|
||||
fireEvent.click(backdrop!)
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should not close panel when content area is clicked', async () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// Click on the content container (should stop propagation)
|
||||
const contentContainer = document.querySelector('.pointer-events-auto')
|
||||
fireEvent.click(contentContainer!)
|
||||
|
||||
await waitFor(() => {
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// API Call Tests
|
||||
// ================================
|
||||
describe('API Calls', () => {
|
||||
it('should call usePluginReadme with correct parameters', () => {
|
||||
const mockDetail = createMockPluginDetail({
|
||||
plugin_unique_identifier: 'custom-plugin@2.0.0',
|
||||
})
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(mockUsePluginReadme).toHaveBeenCalledWith({
|
||||
plugin_unique_identifier: 'custom-plugin@2.0.0',
|
||||
language: 'en-US',
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass undefined language for zh-Hans locale', () => {
|
||||
// Re-mock useLanguage to return zh-Hans
|
||||
vi.doMock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useLanguage: () => 'zh-Hans',
|
||||
}))
|
||||
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
// This test verifies the language handling logic exists in the component
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
// The component should have called the hook
|
||||
expect(mockUsePluginReadme).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty plugin_unique_identifier', () => {
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const mockDetail = createMockPluginDetail({
|
||||
plugin_unique_identifier: '',
|
||||
})
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(mockUsePluginReadme).toHaveBeenCalledWith({
|
||||
plugin_unique_identifier: '',
|
||||
language: 'en-US',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle detail with missing declaration', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
// Simulate missing fields
|
||||
delete (mockDetail as Partial<PluginDetail>).declaration
|
||||
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
// This should not throw
|
||||
expect(() => setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle rapid open/close operations', async () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
// Rapidly toggle the panel
|
||||
act(() => {
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
setCurrentPluginDetail()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.modal)
|
||||
})
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail?.showType).toBe(ReadmeShowType.modal)
|
||||
})
|
||||
|
||||
it('should handle switching between drawer and modal modes', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
// Start with drawer
|
||||
act(() => {
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
})
|
||||
|
||||
let state = useReadmePanelStore.getState()
|
||||
expect(state.currentPluginDetail?.showType).toBe(ReadmeShowType.drawer)
|
||||
|
||||
// Switch to modal
|
||||
act(() => {
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.modal)
|
||||
})
|
||||
|
||||
state = useReadmePanelStore.getState()
|
||||
expect(state.currentPluginDetail?.showType).toBe(ReadmeShowType.modal)
|
||||
})
|
||||
|
||||
it('should handle undefined detail gracefully', () => {
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
|
||||
// Set to undefined explicitly
|
||||
act(() => {
|
||||
setCurrentPluginDetail(undefined, ReadmeShowType.drawer)
|
||||
})
|
||||
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
expect(currentPluginDetail).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Integration Tests
|
||||
// ================================
|
||||
describe('Integration', () => {
|
||||
it('should work correctly when opened from ReadmeEntrance', () => {
|
||||
const mockDetail = createMockPluginDetail()
|
||||
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: { readme: '# Integration Test' },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
// Render both components
|
||||
const { rerender } = renderWithQueryClient(
|
||||
<>
|
||||
<ReadmeEntrance pluginDetail={mockDetail} />
|
||||
<ReadmePanel />
|
||||
</>,
|
||||
)
|
||||
|
||||
// Initially panel should not show content
|
||||
expect(screen.queryByTestId('detail-header')).not.toBeInTheDocument()
|
||||
|
||||
// Click the entrance button
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Re-render to pick up store changes
|
||||
rerender(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<ReadmeEntrance pluginDetail={mockDetail} />
|
||||
<ReadmePanel />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
// Panel should now show content
|
||||
expect(screen.getByTestId('detail-header')).toBeInTheDocument()
|
||||
// Markdown content renders in a container (dynamic import may not render content synchronously)
|
||||
expect(document.querySelector('.markdown-body')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display correct plugin information in header', () => {
|
||||
const mockDetail = createMockPluginDetail({
|
||||
name: 'my-awesome-plugin',
|
||||
})
|
||||
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail(mockDetail, ReadmeShowType.drawer)
|
||||
|
||||
renderWithQueryClient(<ReadmePanel />)
|
||||
|
||||
expect(screen.getByText('my-awesome-plugin')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue