dify/web/app/components/plugins/marketplace/description/index.spec.tsx

650 lines
22 KiB
TypeScript

import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Description from './index'
// ================================
// Mock external dependencies
// ================================
// Track mock locale for testing
let mockDefaultLocale = 'en-US'
// Mock translations with realistic values
const pluginTranslations: Record<string, string> = {
'marketplace.empower': 'Empower your AI development',
'marketplace.discover': 'Discover',
'marketplace.difyMarketplace': 'Dify Marketplace',
'marketplace.and': 'and',
'category.models': 'Models',
'category.tools': 'Tools',
'category.datasources': 'Data Sources',
'category.triggers': 'Triggers',
'category.agents': 'Agent Strategies',
'category.extensions': 'Extensions',
'category.bundles': 'Bundles',
}
const commonTranslations: Record<string, string> = {
'operation.in': 'in',
}
// Mock i18n hooks
vi.mock('#i18n', () => ({
useLocale: vi.fn(() => mockDefaultLocale),
useTranslation: vi.fn((ns: string) => ({
t: (key: string) => {
if (ns === 'plugin')
return pluginTranslations[key] || key
if (ns === 'common')
return commonTranslations[key] || key
return key
},
})),
}))
// ================================
// Description Component Tests
// ================================
describe('Description', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDefaultLocale = 'en-US'
})
// ================================
// Rendering Tests
// ================================
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(<Description />)
expect(container.firstChild).toBeInTheDocument()
})
it('should render h1 heading with empower text', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent('Empower your AI development')
})
it('should render h2 subheading', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toBeInTheDocument()
})
it('should apply correct CSS classes to h1', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveClass('title-4xl-semi-bold')
expect(heading).toHaveClass('mb-2')
expect(heading).toHaveClass('text-center')
expect(heading).toHaveClass('text-text-primary')
})
it('should apply correct CSS classes to h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('body-md-regular')
expect(subheading).toHaveClass('text-center')
expect(subheading).toHaveClass('text-text-tertiary')
})
})
// ================================
// Non-Chinese Locale Rendering Tests
// ================================
describe('Non-Chinese Locale Rendering', () => {
beforeEach(() => {
mockDefaultLocale = 'en-US'
})
it('should render discover text for en-US locale', () => {
render(<Description />)
expect(screen.getByText(/Discover/)).toBeInTheDocument()
})
it('should render all category names', () => {
render(<Description />)
expect(screen.getByText('Models')).toBeInTheDocument()
expect(screen.getByText('Tools')).toBeInTheDocument()
expect(screen.getByText('Data Sources')).toBeInTheDocument()
expect(screen.getByText('Triggers')).toBeInTheDocument()
expect(screen.getByText('Agent Strategies')).toBeInTheDocument()
expect(screen.getByText('Extensions')).toBeInTheDocument()
expect(screen.getByText('Bundles')).toBeInTheDocument()
})
it('should render "and" conjunction text', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('and')
})
it('should render "in" preposition at the end for non-Chinese locales', () => {
render(<Description />)
expect(screen.getByText('in')).toBeInTheDocument()
})
it('should render Dify Marketplace text at the end for non-Chinese locales', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should render category spans with styled underline effect', () => {
const { container } = render(<Description />)
const styledSpans = container.querySelectorAll('.body-md-medium.relative.z-\\[1\\]')
// 7 category spans (models, tools, datasources, triggers, agents, extensions, bundles)
expect(styledSpans.length).toBe(7)
})
it('should apply text-text-secondary class to category spans', () => {
const { container } = render(<Description />)
const styledSpans = container.querySelectorAll('.text-text-secondary')
expect(styledSpans.length).toBeGreaterThanOrEqual(7)
})
})
// ================================
// Chinese (zh-Hans) Locale Rendering Tests
// ================================
describe('Chinese (zh-Hans) Locale Rendering', () => {
beforeEach(() => {
mockDefaultLocale = 'zh-Hans'
})
it('should render "in" text at the beginning for zh-Hans locale', () => {
render(<Description />)
// In zh-Hans mode, "in" appears at the beginning
const inElements = screen.getAllByText('in')
expect(inElements.length).toBeGreaterThanOrEqual(1)
})
it('should render Dify Marketplace text for zh-Hans locale', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should render discover text for zh-Hans locale', () => {
render(<Description />)
expect(screen.getByText(/Discover/)).toBeInTheDocument()
})
it('should render all categories for zh-Hans locale', () => {
render(<Description />)
expect(screen.getByText('Models')).toBeInTheDocument()
expect(screen.getByText('Tools')).toBeInTheDocument()
expect(screen.getByText('Data Sources')).toBeInTheDocument()
expect(screen.getByText('Triggers')).toBeInTheDocument()
expect(screen.getByText('Agent Strategies')).toBeInTheDocument()
expect(screen.getByText('Extensions')).toBeInTheDocument()
expect(screen.getByText('Bundles')).toBeInTheDocument()
})
it('should render both zh-Hans specific elements and shared elements', () => {
render(<Description />)
// zh-Hans has specific element order: "in" -> Dify Marketplace -> Discover
// then the same category list with "and" -> Bundles
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('and')
})
})
// ================================
// Locale Variations Tests
// ================================
describe('Locale Variations', () => {
it('should use en-US locale by default', () => {
mockDefaultLocale = 'en-US'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle ja-JP locale as non-Chinese', () => {
mockDefaultLocale = 'ja-JP'
render(<Description />)
// Should render in non-Chinese format (discover first, then "in Dify Marketplace" at end)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should handle ko-KR locale as non-Chinese', () => {
mockDefaultLocale = 'ko-KR'
render(<Description />)
// Should render in non-Chinese format
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle de-DE locale as non-Chinese', () => {
mockDefaultLocale = 'de-DE'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle fr-FR locale as non-Chinese', () => {
mockDefaultLocale = 'fr-FR'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle pt-BR locale as non-Chinese', () => {
mockDefaultLocale = 'pt-BR'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle es-ES locale as non-Chinese', () => {
mockDefaultLocale = 'es-ES'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
})
// ================================
// Conditional Rendering Tests
// ================================
describe('Conditional Rendering', () => {
it('should render zh-Hans specific content when locale is zh-Hans', () => {
mockDefaultLocale = 'zh-Hans'
const { container } = render(<Description />)
// zh-Hans has additional span with mr-1 before "in" text at the start
const mrSpan = container.querySelector('span.mr-1')
expect(mrSpan).toBeInTheDocument()
})
it('should render non-Chinese specific content when locale is not zh-Hans', () => {
mockDefaultLocale = 'en-US'
render(<Description />)
// Non-Chinese has "in" and "Dify Marketplace" at the end
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should not render zh-Hans intro content for non-Chinese locales', () => {
mockDefaultLocale = 'en-US'
render(<Description />)
// For en-US, the order should be Discover ... in Dify Marketplace
// The "in" text should only appear once at the end
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
// "in" should appear after "Bundles" and before "Dify Marketplace"
const bundlesIndex = content.indexOf('Bundles')
const inIndex = content.indexOf('in')
const marketplaceIndex = content.indexOf('Dify Marketplace')
expect(bundlesIndex).toBeLessThan(inIndex)
expect(inIndex).toBeLessThan(marketplaceIndex)
})
it('should render zh-Hans with proper word order', () => {
mockDefaultLocale = 'zh-Hans'
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
// zh-Hans order: in -> Dify Marketplace -> Discover -> categories
const inIndex = content.indexOf('in')
const marketplaceIndex = content.indexOf('Dify Marketplace')
const discoverIndex = content.indexOf('Discover')
expect(inIndex).toBeLessThan(marketplaceIndex)
expect(marketplaceIndex).toBeLessThan(discoverIndex)
})
})
// ================================
// Category Styling Tests
// ================================
describe('Category Styling', () => {
it('should apply underline effect with after pseudo-element styling', () => {
const { container } = render(<Description />)
const categorySpan = container.querySelector('.after\\:absolute')
expect(categorySpan).toBeInTheDocument()
})
it('should apply correct after pseudo-element classes', () => {
const { container } = render(<Description />)
// Check for the specific after pseudo-element classes
const categorySpans = container.querySelectorAll('.after\\:bottom-\\[1\\.5px\\]')
expect(categorySpans.length).toBe(7)
})
it('should apply full width to after element', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.after\\:w-full')
expect(categorySpans.length).toBe(7)
})
it('should apply correct height to after element', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.after\\:h-2')
expect(categorySpans.length).toBe(7)
})
it('should apply bg-text-text-selected to after element', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.after\\:bg-text-text-selected')
expect(categorySpans.length).toBe(7)
})
it('should have z-index 1 on category spans', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.z-\\[1\\]')
expect(categorySpans.length).toBe(7)
})
it('should apply left margin to category spans', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.ml-1')
expect(categorySpans.length).toBeGreaterThanOrEqual(7)
})
it('should apply both left and right margin to specific spans', () => {
const { container } = render(<Description />)
// Extensions and Bundles spans have both ml-1 and mr-1
const extensionsBundlesSpans = container.querySelectorAll('.ml-1.mr-1')
expect(extensionsBundlesSpans.length).toBe(2)
})
})
// ================================
// Edge Cases Tests
// ================================
describe('Edge Cases', () => {
it('should render fragment as root element', () => {
const { container } = render(<Description />)
// Fragment renders h1 and h2 as direct children
expect(container.querySelector('h1')).toBeInTheDocument()
expect(container.querySelector('h2')).toBeInTheDocument()
})
it('should handle zh-Hant as non-Chinese simplified', () => {
mockDefaultLocale = 'zh-Hant'
render(<Description />)
// zh-Hant is different from zh-Hans, should use non-Chinese format
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
// Check that "Dify Marketplace" appears at the end (non-Chinese format)
const discoverIndex = content.indexOf('Discover')
const marketplaceIndex = content.indexOf('Dify Marketplace')
// For non-Chinese locales, Discover should come before Dify Marketplace
expect(discoverIndex).toBeLessThan(marketplaceIndex)
})
})
// ================================
// Content Structure Tests
// ================================
describe('Content Structure', () => {
it('should have comma separators between categories', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
// Commas should exist between categories
expect(content).toMatch(/Models[^\n\r,\u2028\u2029]*,.*Tools[^\n\r,\u2028\u2029]*,.*Data Sources[^\n\r,\u2028\u2029]*,.*Triggers[^\n\r,\u2028\u2029]*,.*Agent Strategies[^\n\r,\u2028\u2029]*,.*Extensions/)
})
it('should have "and" before last category (Bundles)', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
// "and" should appear before Bundles
const andIndex = content.indexOf('and')
const bundlesIndex = content.indexOf('Bundles')
expect(andIndex).toBeLessThan(bundlesIndex)
})
it('should render all text elements in correct order for en-US', () => {
mockDefaultLocale = 'en-US'
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
const expectedOrder = [
'Discover',
'Models',
'Tools',
'Data Sources',
'Triggers',
'Agent Strategies',
'Extensions',
'and',
'Bundles',
'in',
'Dify Marketplace',
]
let lastIndex = -1
for (const text of expectedOrder) {
const currentIndex = content.indexOf(text)
expect(currentIndex).toBeGreaterThan(lastIndex)
lastIndex = currentIndex
}
})
it('should render all text elements in correct order for zh-Hans', () => {
mockDefaultLocale = 'zh-Hans'
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
// zh-Hans order: in -> Dify Marketplace -> Discover -> categories -> and -> Bundles
const inIndex = content.indexOf('in')
const marketplaceIndex = content.indexOf('Dify Marketplace')
const discoverIndex = content.indexOf('Discover')
const modelsIndex = content.indexOf('Models')
expect(inIndex).toBeLessThan(marketplaceIndex)
expect(marketplaceIndex).toBeLessThan(discoverIndex)
expect(discoverIndex).toBeLessThan(modelsIndex)
})
})
// ================================
// Layout Tests
// ================================
describe('Layout', () => {
it('should have shrink-0 on h1 heading', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveClass('shrink-0')
})
it('should have shrink-0 on h2 subheading', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('shrink-0')
})
it('should have flex layout on h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('flex')
})
it('should have items-center on h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('items-center')
})
it('should have justify-center on h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('justify-center')
})
})
// ================================
// Accessibility Tests
// ================================
describe('Accessibility', () => {
it('should have proper heading hierarchy', () => {
render(<Description />)
const h1 = screen.getByRole('heading', { level: 1 })
const h2 = screen.getByRole('heading', { level: 2 })
expect(h1).toBeInTheDocument()
expect(h2).toBeInTheDocument()
})
it('should have readable text content', () => {
render(<Description />)
const h1 = screen.getByRole('heading', { level: 1 })
expect(h1.textContent).not.toBe('')
})
it('should have visible h1 heading', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeVisible()
})
it('should have visible h2 heading', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toBeVisible()
})
})
})
// ================================
// Integration Tests
// ================================
describe('Description Integration', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDefaultLocale = 'en-US'
})
it('should render complete component structure', () => {
const { container } = render(<Description />)
// Main headings
expect(container.querySelector('h1')).toBeInTheDocument()
expect(container.querySelector('h2')).toBeInTheDocument()
// All category spans
const categorySpans = container.querySelectorAll('.body-md-medium')
expect(categorySpans.length).toBe(7)
})
it('should render complete zh-Hans structure', () => {
mockDefaultLocale = 'zh-Hans'
const { container } = render(<Description />)
// Main headings
expect(container.querySelector('h1')).toBeInTheDocument()
expect(container.querySelector('h2')).toBeInTheDocument()
// All category spans
const categorySpans = container.querySelectorAll('.body-md-medium')
expect(categorySpans.length).toBe(7)
})
it('should correctly differentiate between zh-Hans and en-US layouts', () => {
// Render en-US
mockDefaultLocale = 'en-US'
const { container: enContainer, unmount: unmountEn } = render(<Description />)
const enContent = enContainer.querySelector('h2')?.textContent || ''
unmountEn()
// Render zh-Hans
mockDefaultLocale = 'zh-Hans'
const { container: zhContainer } = render(<Description />)
const zhContent = zhContainer.querySelector('h2')?.textContent || ''
// Both should have all categories
expect(enContent).toContain('Models')
expect(zhContent).toContain('Models')
// But order should differ
const enMarketplaceIndex = enContent.indexOf('Dify Marketplace')
const enDiscoverIndex = enContent.indexOf('Discover')
const zhMarketplaceIndex = zhContent.indexOf('Dify Marketplace')
const zhDiscoverIndex = zhContent.indexOf('Discover')
// en-US: Discover comes before Dify Marketplace
expect(enDiscoverIndex).toBeLessThan(enMarketplaceIndex)
// zh-Hans: Dify Marketplace comes before Discover
expect(zhMarketplaceIndex).toBeLessThan(zhDiscoverIndex)
})
it('should maintain consistent styling across locales', () => {
// Render en-US
mockDefaultLocale = 'en-US'
const { container: enContainer, unmount: unmountEn } = render(<Description />)
const enCategoryCount = enContainer.querySelectorAll('.body-md-medium').length
unmountEn()
// Render zh-Hans
mockDefaultLocale = 'zh-Hans'
const { container: zhContainer } = render(<Description />)
const zhCategoryCount = zhContainer.querySelectorAll('.body-md-medium').length
// Both should have same number of styled category spans
expect(enCategoryCount).toBe(zhCategoryCount)
expect(enCategoryCount).toBe(7)
})
})