mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 01:48:04 +08:00
test(web): add comprehensive unit and integration tests for plugins and tools modules (#32220)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
@ -0,0 +1,50 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import CardMoreInfo from '../card-more-info'
|
||||
|
||||
vi.mock('../base/download-count', () => ({
|
||||
default: ({ downloadCount }: { downloadCount: number }) => (
|
||||
<span data-testid="download-count">{downloadCount}</span>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('CardMoreInfo', () => {
|
||||
it('renders tags with # prefix', () => {
|
||||
render(<CardMoreInfo tags={['search', 'agent']} />)
|
||||
expect(screen.getByText('search')).toBeInTheDocument()
|
||||
expect(screen.getByText('agent')).toBeInTheDocument()
|
||||
// # prefixes
|
||||
const hashmarks = screen.getAllByText('#')
|
||||
expect(hashmarks).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('renders download count when provided', () => {
|
||||
render(<CardMoreInfo downloadCount={1000} tags={[]} />)
|
||||
expect(screen.getByTestId('download-count')).toHaveTextContent('1000')
|
||||
})
|
||||
|
||||
it('does not render download count when undefined', () => {
|
||||
render(<CardMoreInfo tags={['tag1']} />)
|
||||
expect(screen.queryByTestId('download-count')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders separator between download count and tags', () => {
|
||||
render(<CardMoreInfo downloadCount={500} tags={['test']} />)
|
||||
expect(screen.getByText('·')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render separator when no tags', () => {
|
||||
render(<CardMoreInfo downloadCount={500} tags={[]} />)
|
||||
expect(screen.queryByText('·')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render separator when no download count', () => {
|
||||
render(<CardMoreInfo tags={['tag1']} />)
|
||||
expect(screen.queryByText('·')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles empty tags array', () => {
|
||||
const { container } = render(<CardMoreInfo tags={[]} />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
589
web/app/components/plugins/card/__tests__/index.spec.tsx
Normal file
589
web/app/components/plugins/card/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,589 @@
|
||||
import type { Plugin } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
import Card from '../index'
|
||||
|
||||
let mockTheme = 'light'
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: mockTheme }),
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n-config', () => ({
|
||||
renderI18nObject: (obj: Record<string, string>, locale: string) => {
|
||||
return obj?.[locale] || obj?.['en-US'] || ''
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n-config/language', () => ({
|
||||
getLanguage: (locale: string) => locale || 'en-US',
|
||||
}))
|
||||
|
||||
const mockCategoriesMap: Record<string, { label: string }> = {
|
||||
'tool': { label: 'Tool' },
|
||||
'model': { label: 'Model' },
|
||||
'extension': { label: 'Extension' },
|
||||
'agent-strategy': { label: 'Agent' },
|
||||
'datasource': { label: 'Datasource' },
|
||||
'trigger': { label: 'Trigger' },
|
||||
'bundle': { label: 'Bundle' },
|
||||
}
|
||||
|
||||
vi.mock('../../hooks', () => ({
|
||||
useCategories: () => ({
|
||||
categoriesMap: mockCategoriesMap,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/format', () => ({
|
||||
formatNumber: (num: number) => num.toLocaleString(),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/mcp', () => ({
|
||||
shouldUseMcpIcon: (src: unknown) => typeof src === 'object' && src !== null && (src as { content?: string })?.content === '🔗',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ icon, background, innerIcon, size, iconType }: {
|
||||
icon?: string
|
||||
background?: string
|
||||
innerIcon?: React.ReactNode
|
||||
size?: string
|
||||
iconType?: string
|
||||
}) => (
|
||||
<div
|
||||
data-testid="app-icon"
|
||||
data-icon={icon}
|
||||
data-background={background}
|
||||
data-size={size}
|
||||
data-icon-type={iconType}
|
||||
>
|
||||
{!!innerIcon && <div data-testid="inner-icon">{innerIcon}</div>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/other', () => ({
|
||||
Mcp: ({ className }: { className?: string }) => (
|
||||
<div data-testid="mcp-icon" className={className}>MCP</div>
|
||||
),
|
||||
Group: ({ className }: { className?: string }) => (
|
||||
<div data-testid="group-icon" className={className}>Group</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../base/icons/src/vender/plugin', () => ({
|
||||
LeftCorner: ({ className }: { className?: string }) => (
|
||||
<div data-testid="left-corner" className={className}>LeftCorner</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/badges/partner', () => ({
|
||||
default: ({ className, text }: { className?: string, text?: string }) => (
|
||||
<div data-testid="partner-badge" className={className} title={text}>Partner</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../base/badges/verified', () => ({
|
||||
default: ({ className, text }: { className?: string, text?: string }) => (
|
||||
<div data-testid="verified-badge" className={className} title={text}>Verified</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/skeleton', () => ({
|
||||
SkeletonContainer: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="skeleton-container">{children}</div>
|
||||
),
|
||||
SkeletonPoint: () => <div data-testid="skeleton-point" />,
|
||||
SkeletonRectangle: ({ className }: { className?: string }) => (
|
||||
<div data-testid="skeleton-rectangle" className={className} />
|
||||
),
|
||||
SkeletonRow: ({ children, className }: { children: React.ReactNode, className?: string }) => (
|
||||
<div data-testid="skeleton-row" className={className}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockPlugin = (overrides?: Partial<Plugin>): Plugin => ({
|
||||
type: 'plugin',
|
||||
org: 'test-org',
|
||||
name: 'test-plugin',
|
||||
plugin_id: 'plugin-123',
|
||||
version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
latest_package_identifier: 'test-org/test-plugin:1.0.0',
|
||||
icon: '/test-icon.png',
|
||||
verified: false,
|
||||
label: { 'en-US': 'Test Plugin' },
|
||||
brief: { 'en-US': 'Test plugin description' },
|
||||
description: { 'en-US': 'Full test plugin description' },
|
||||
introduction: 'Test plugin introduction',
|
||||
repository: 'https://github.com/test/plugin',
|
||||
category: PluginCategoryEnum.tool,
|
||||
install_count: 1000,
|
||||
endpoint: { settings: [] },
|
||||
tags: [{ name: 'search' }],
|
||||
badges: [],
|
||||
verification: { authorized_category: 'community' },
|
||||
from: 'marketplace',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('Card', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin title from label', () => {
|
||||
const plugin = createMockPlugin({
|
||||
label: { 'en-US': 'My Plugin Title' },
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('My Plugin Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin description from brief', () => {
|
||||
const plugin = createMockPlugin({
|
||||
brief: { 'en-US': 'This is a brief description' },
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('This is a brief description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render organization info with org name and package name', () => {
|
||||
const plugin = createMockPlugin({
|
||||
org: 'my-org',
|
||||
name: 'my-plugin',
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('my-org')).toBeInTheDocument()
|
||||
expect(screen.getByText('my-plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plugin icon', () => {
|
||||
const plugin = createMockPlugin({
|
||||
icon: '/custom-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Check for background image style on icon element
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use icon_dark when theme is dark and icon_dark is provided', () => {
|
||||
// Set theme to dark
|
||||
mockTheme = 'dark'
|
||||
|
||||
const plugin = createMockPlugin({
|
||||
icon: '/light-icon.png',
|
||||
icon_dark: '/dark-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Check that icon uses dark icon
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
expect(iconElement).toHaveStyle({ backgroundImage: 'url(/dark-icon.png)' })
|
||||
|
||||
// Reset theme
|
||||
mockTheme = 'light'
|
||||
})
|
||||
|
||||
it('should use icon when theme is dark but icon_dark is not provided', () => {
|
||||
mockTheme = 'dark'
|
||||
|
||||
const plugin = createMockPlugin({
|
||||
icon: '/light-icon.png',
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Should fallback to light icon
|
||||
const iconElement = container.querySelector('[style*="background-image"]')
|
||||
expect(iconElement).toBeInTheDocument()
|
||||
expect(iconElement).toHaveStyle({ backgroundImage: 'url(/light-icon.png)' })
|
||||
|
||||
mockTheme = 'light'
|
||||
})
|
||||
|
||||
it('should render corner mark with category label', () => {
|
||||
const plugin = createMockPlugin({
|
||||
category: PluginCategoryEnum.tool,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('Tool')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Props Testing
|
||||
// ================================
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const { container } = render(
|
||||
<Card payload={plugin} className="custom-class" />,
|
||||
)
|
||||
|
||||
expect(container.querySelector('.custom-class')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide corner mark when hideCornerMark is true', () => {
|
||||
const plugin = createMockPlugin({
|
||||
category: PluginCategoryEnum.tool,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} hideCornerMark={true} />)
|
||||
|
||||
expect(screen.queryByTestId('left-corner')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show corner mark by default', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('left-corner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass installed prop to Icon component', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const { container } = render(<Card payload={plugin} installed={true} />)
|
||||
|
||||
expect(container.querySelector('.bg-state-success-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass installFailed prop to Icon component', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const { container } = render(<Card payload={plugin} installFailed={true} />)
|
||||
|
||||
expect(container.querySelector('.bg-state-destructive-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render footer when provided', () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(
|
||||
<Card payload={plugin} footer={<div data-testid="custom-footer">Footer Content</div>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('custom-footer')).toBeInTheDocument()
|
||||
expect(screen.getByText('Footer Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render titleLeft when provided', () => {
|
||||
const plugin = createMockPlugin()
|
||||
render(
|
||||
<Card payload={plugin} titleLeft={<span data-testid="title-left">v1.0</span>} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('title-left')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use custom descriptionLineRows', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(
|
||||
<Card payload={plugin} descriptionLineRows={1} />,
|
||||
)
|
||||
|
||||
// Check for h-4 truncate class when descriptionLineRows is 1
|
||||
expect(container.querySelector('.h-4.truncate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use default descriptionLineRows of 2', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Check for h-8 line-clamp-2 class when descriptionLineRows is 2 (default)
|
||||
expect(container.querySelector('.h-8.line-clamp-2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Loading State Tests
|
||||
// ================================
|
||||
describe('Loading State', () => {
|
||||
it('should render Placeholder when isLoading is true', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} isLoading={true} loadingFileName="loading.txt" />)
|
||||
|
||||
// Should render skeleton elements
|
||||
expect(screen.getByTestId('skeleton-container')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loadingFileName in Placeholder', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} isLoading={true} loadingFileName="my-plugin.zip" />)
|
||||
|
||||
expect(screen.getByText('my-plugin.zip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render card content when loading', () => {
|
||||
const plugin = createMockPlugin({
|
||||
label: { 'en-US': 'Plugin Title' },
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} isLoading={true} loadingFileName="file.txt" />)
|
||||
|
||||
// Plugin content should not be visible during loading
|
||||
expect(screen.queryByText('Plugin Title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render loading state by default', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('skeleton-container')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Badges Tests
|
||||
// ================================
|
||||
describe('Badges', () => {
|
||||
it('should render Partner badge when badges includes partner', () => {
|
||||
const plugin = createMockPlugin({
|
||||
badges: ['partner'],
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Verified badge when verified is true', () => {
|
||||
const plugin = createMockPlugin({
|
||||
verified: true,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render both Partner and Verified badges', () => {
|
||||
const plugin = createMockPlugin({
|
||||
badges: ['partner'],
|
||||
verified: true,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByTestId('partner-badge')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('verified-badge')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render Partner badge when badges is empty', () => {
|
||||
const plugin = createMockPlugin({
|
||||
badges: [],
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('partner-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render Verified badge when verified is false', () => {
|
||||
const plugin = createMockPlugin({
|
||||
verified: false,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('verified-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined badges gracefully', () => {
|
||||
const plugin = createMockPlugin()
|
||||
// @ts-expect-error - Testing undefined badges
|
||||
plugin.badges = undefined
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.queryByTestId('partner-badge')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Limited Install Warning Tests
|
||||
// ================================
|
||||
describe('Limited Install Warning', () => {
|
||||
it('should render warning when limitedInstall is true', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} limitedInstall={true} />)
|
||||
|
||||
expect(container.querySelector('.text-text-warning-secondary')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render warning by default', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
expect(container.querySelector('.text-text-warning-secondary')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply limited padding when limitedInstall is true', () => {
|
||||
const plugin = createMockPlugin()
|
||||
|
||||
const { container } = render(<Card payload={plugin} limitedInstall={true} />)
|
||||
|
||||
expect(container.querySelector('.pb-1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Category Type Tests
|
||||
// ================================
|
||||
describe('Category Types', () => {
|
||||
it('should display bundle label for bundle type', () => {
|
||||
const plugin = createMockPlugin({
|
||||
type: 'bundle',
|
||||
category: PluginCategoryEnum.tool,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
// For bundle type, should show 'Bundle' instead of category
|
||||
expect(screen.getByText('Bundle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display category label for non-bundle types', () => {
|
||||
const plugin = createMockPlugin({
|
||||
type: 'plugin',
|
||||
category: PluginCategoryEnum.model,
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('Model')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Memoization Tests
|
||||
// ================================
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized with React.memo', () => {
|
||||
// Card is wrapped with React.memo
|
||||
expect(Card).toBeDefined()
|
||||
// The component should have the memo display name characteristic
|
||||
expect(typeof Card).toBe('object')
|
||||
})
|
||||
|
||||
it('should not re-render when props are the same', () => {
|
||||
const plugin = createMockPlugin()
|
||||
const renderCount = vi.fn()
|
||||
|
||||
const TestWrapper = ({ p }: { p: Plugin }) => {
|
||||
renderCount()
|
||||
return <Card payload={p} />
|
||||
}
|
||||
|
||||
const { rerender } = render(<TestWrapper p={plugin} />)
|
||||
expect(renderCount).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Re-render with same plugin reference
|
||||
rerender(<TestWrapper p={plugin} />)
|
||||
expect(renderCount).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Edge Cases Tests
|
||||
// ================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty label object', () => {
|
||||
const plugin = createMockPlugin({
|
||||
label: {},
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
// Should render without crashing
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty brief object', () => {
|
||||
const plugin = createMockPlugin({
|
||||
brief: {},
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined label', () => {
|
||||
const plugin = createMockPlugin()
|
||||
// @ts-expect-error - Testing undefined label
|
||||
plugin.label = undefined
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(document.body).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in plugin name', () => {
|
||||
const plugin = createMockPlugin({
|
||||
name: 'plugin-with-special-chars!@#$%',
|
||||
org: 'org<script>alert(1)</script>',
|
||||
})
|
||||
|
||||
render(<Card payload={plugin} />)
|
||||
|
||||
expect(screen.getByText('plugin-with-special-chars!@#$%')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long title', () => {
|
||||
const longTitle = 'A'.repeat(500)
|
||||
const plugin = createMockPlugin({
|
||||
label: { 'en-US': longTitle },
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Should have truncate class for long text
|
||||
expect(container.querySelector('.truncate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long description', () => {
|
||||
const longDescription = 'B'.repeat(1000)
|
||||
const plugin = createMockPlugin({
|
||||
brief: { 'en-US': longDescription },
|
||||
})
|
||||
|
||||
const { container } = render(<Card payload={plugin} />)
|
||||
|
||||
// Should have line-clamp class for long text
|
||||
expect(container.querySelector('.line-clamp-2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,61 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Icon from '../card-icon'
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ icon, background }: { icon: string, background: string }) => (
|
||||
<div data-testid="app-icon" data-icon={icon} data-bg={background} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/other', () => ({
|
||||
Mcp: () => <span data-testid="mcp-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/mcp', () => ({
|
||||
shouldUseMcpIcon: () => false,
|
||||
}))
|
||||
|
||||
describe('Icon', () => {
|
||||
it('renders string src as background image', () => {
|
||||
const { container } = render(<Icon src="https://example.com/icon.png" />)
|
||||
const el = container.firstChild as HTMLElement
|
||||
expect(el.style.backgroundImage).toContain('https://example.com/icon.png')
|
||||
})
|
||||
|
||||
it('renders emoji src using AppIcon', () => {
|
||||
render(<Icon src={{ content: '🔍', background: '#fff' }} />)
|
||||
expect(screen.getByTestId('app-icon')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-icon')).toHaveAttribute('data-icon', '🔍')
|
||||
expect(screen.getByTestId('app-icon')).toHaveAttribute('data-bg', '#fff')
|
||||
})
|
||||
|
||||
it('shows check icon when installed', () => {
|
||||
const { container } = render(<Icon src="icon.png" installed />)
|
||||
expect(container.querySelector('.bg-state-success-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows close icon when installFailed', () => {
|
||||
const { container } = render(<Icon src="icon.png" installFailed />)
|
||||
expect(container.querySelector('.bg-state-destructive-solid')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not show status icons by default', () => {
|
||||
const { container } = render(<Icon src="icon.png" />)
|
||||
expect(container.querySelector('.bg-state-success-solid')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('.bg-state-destructive-solid')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Icon src="icon.png" className="my-class" />)
|
||||
const el = container.firstChild as HTMLElement
|
||||
expect(el.className).toContain('my-class')
|
||||
})
|
||||
|
||||
it('applies correct size class', () => {
|
||||
const { container } = render(<Icon src="icon.png" size="small" />)
|
||||
const el = container.firstChild as HTMLElement
|
||||
expect(el.className).toContain('w-8')
|
||||
expect(el.className).toContain('h-8')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,27 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import CornerMark from '../corner-mark'
|
||||
|
||||
vi.mock('../../../../base/icons/src/vender/plugin', () => ({
|
||||
LeftCorner: ({ className }: { className: string }) => <svg data-testid="left-corner" className={className} />,
|
||||
}))
|
||||
|
||||
describe('CornerMark', () => {
|
||||
it('renders the text content', () => {
|
||||
render(<CornerMark text="NEW" />)
|
||||
expect(screen.getByText('NEW')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the LeftCorner icon', () => {
|
||||
render(<CornerMark text="BETA" />)
|
||||
expect(screen.getByTestId('left-corner')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with absolute positioning', () => {
|
||||
const { container } = render(<CornerMark text="TAG" />)
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper.className).toContain('absolute')
|
||||
expect(wrapper.className).toContain('right-0')
|
||||
expect(wrapper.className).toContain('top-0')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,37 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Description from '../description'
|
||||
|
||||
describe('Description', () => {
|
||||
it('renders description text', () => {
|
||||
render(<Description text="A great plugin" descriptionLineRows={1} />)
|
||||
expect(screen.getByText('A great plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies truncate class for 1 line', () => {
|
||||
render(<Description text="Single line" descriptionLineRows={1} />)
|
||||
const el = screen.getByText('Single line')
|
||||
expect(el.className).toContain('truncate')
|
||||
expect(el.className).toContain('h-4')
|
||||
})
|
||||
|
||||
it('applies line-clamp-2 class for 2 lines', () => {
|
||||
render(<Description text="Two lines" descriptionLineRows={2} />)
|
||||
const el = screen.getByText('Two lines')
|
||||
expect(el.className).toContain('line-clamp-2')
|
||||
expect(el.className).toContain('h-8')
|
||||
})
|
||||
|
||||
it('applies line-clamp-3 class for 3 lines', () => {
|
||||
render(<Description text="Three lines" descriptionLineRows={3} />)
|
||||
const el = screen.getByText('Three lines')
|
||||
expect(el.className).toContain('line-clamp-3')
|
||||
expect(el.className).toContain('h-12')
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<Description text="test" descriptionLineRows={1} className="mt-2" />)
|
||||
const el = screen.getByText('test')
|
||||
expect(el.className).toContain('mt-2')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,28 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import DownloadCount from '../download-count'
|
||||
|
||||
vi.mock('@/utils/format', () => ({
|
||||
formatNumber: (n: number) => {
|
||||
if (n >= 1000)
|
||||
return `${(n / 1000).toFixed(1)}k`
|
||||
return String(n)
|
||||
},
|
||||
}))
|
||||
|
||||
describe('DownloadCount', () => {
|
||||
it('renders formatted download count', () => {
|
||||
render(<DownloadCount downloadCount={1500} />)
|
||||
expect(screen.getByText('1.5k')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders small numbers directly', () => {
|
||||
render(<DownloadCount downloadCount={42} />)
|
||||
expect(screen.getByText('42')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders zero download count', () => {
|
||||
render(<DownloadCount downloadCount={0} />)
|
||||
expect(screen.getByText('0')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,34 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import OrgInfo from '../org-info'
|
||||
|
||||
describe('OrgInfo', () => {
|
||||
it('renders package name', () => {
|
||||
render(<OrgInfo packageName="my-plugin" />)
|
||||
expect(screen.getByText('my-plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders org name with separator when provided', () => {
|
||||
render(<OrgInfo orgName="dify" packageName="search-tool" />)
|
||||
expect(screen.getByText('dify')).toBeInTheDocument()
|
||||
expect(screen.getByText('/')).toBeInTheDocument()
|
||||
expect(screen.getByText('search-tool')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render org name or separator when orgName is not provided', () => {
|
||||
render(<OrgInfo packageName="standalone" />)
|
||||
expect(screen.queryByText('/')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('standalone')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<OrgInfo packageName="pkg" className="custom-class" />)
|
||||
expect((container.firstChild as HTMLElement).className).toContain('custom-class')
|
||||
})
|
||||
|
||||
it('applies packageNameClassName to package name element', () => {
|
||||
render(<OrgInfo packageName="pkg" packageNameClassName="w-auto" />)
|
||||
const pkgEl = screen.getByText('pkg')
|
||||
expect(pkgEl.className).toContain('w-auto')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,71 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../title', () => ({
|
||||
default: ({ title }: { title: string }) => <span data-testid="title">{title}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../base/icons/src/vender/other', () => ({
|
||||
Group: ({ className }: { className: string }) => <span data-testid="group-icon" className={className} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
describe('Placeholder', () => {
|
||||
let Placeholder: (typeof import('../placeholder'))['default']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../placeholder')
|
||||
Placeholder = mod.default
|
||||
})
|
||||
|
||||
it('should render skeleton rows', () => {
|
||||
const { container } = render(<Placeholder wrapClassName="w-full" />)
|
||||
|
||||
expect(container.querySelectorAll('.gap-2').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('should render group icon placeholder', () => {
|
||||
render(<Placeholder wrapClassName="w-full" />)
|
||||
|
||||
expect(screen.getByTestId('group-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render loading filename when provided', () => {
|
||||
render(<Placeholder wrapClassName="w-full" loadingFileName="test-plugin.zip" />)
|
||||
|
||||
expect(screen.getByTestId('title')).toHaveTextContent('test-plugin.zip')
|
||||
})
|
||||
|
||||
it('should render skeleton rectangles when no filename', () => {
|
||||
const { container } = render(<Placeholder wrapClassName="w-full" />)
|
||||
|
||||
expect(container.querySelectorAll('.bg-text-quaternary').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('LoadingPlaceholder', () => {
|
||||
let LoadingPlaceholder: (typeof import('../placeholder'))['LoadingPlaceholder']
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
const mod = await import('../placeholder')
|
||||
LoadingPlaceholder = mod.LoadingPlaceholder
|
||||
})
|
||||
|
||||
it('should render as a simple div with background', () => {
|
||||
const { container } = render(<LoadingPlaceholder />)
|
||||
|
||||
expect(container.firstChild).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should accept className prop', () => {
|
||||
const { container } = render(<LoadingPlaceholder className="mt-3 w-[420px]" />)
|
||||
|
||||
expect(container.firstChild).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,21 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Title from '../title'
|
||||
|
||||
describe('Title', () => {
|
||||
it('renders the title text', () => {
|
||||
render(<Title title="Test Plugin" />)
|
||||
expect(screen.getByText('Test Plugin')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with truncate class for long text', () => {
|
||||
render(<Title title="A very long title that should be truncated" />)
|
||||
const el = screen.getByText('A very long title that should be truncated')
|
||||
expect(el.className).toContain('truncate')
|
||||
})
|
||||
|
||||
it('renders empty string without error', () => {
|
||||
const { container } = render(<Title title="" />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user