diff --git a/web/__tests__/plugins/plugin-card-rendering.test.tsx b/web/__tests__/plugins/plugin-card-rendering.test.tsx index 5bd7f0c8bf..09ee3c98be 100644 --- a/web/__tests__/plugins/plugin-card-rendering.test.tsx +++ b/web/__tests__/plugins/plugin-card-rendering.test.tsx @@ -75,11 +75,10 @@ vi.mock('@/app/components/plugins/card/base/description', () => ({ })) vi.mock('@/app/components/plugins/card/base/org-info', () => ({ - default: ({ orgName, packageName }: { orgName: string, packageName: string }) => ( + default: ({ orgName, downloadCount }: { orgName: string, downloadCount?: number }) => (
{orgName} - / - {packageName} + {typeof downloadCount === 'number' ? ` ยท ${downloadCount}` : null}
), })) @@ -124,7 +123,7 @@ describe('Plugin Card Rendering Integration', () => { expect(screen.getByTestId('card-icon')).toBeInTheDocument() expect(screen.getByTestId('title')).toHaveTextContent('Google Search') - expect(screen.getByTestId('org-info')).toHaveTextContent('langgenius/google-search') + expect(screen.getByTestId('org-info')).toHaveTextContent('langgenius') expect(screen.getByTestId('description')).toHaveTextContent('Search the web using Google') }) diff --git a/web/app/components/plugins/card/__tests__/index.spec.tsx b/web/app/components/plugins/card/__tests__/index.spec.tsx index 37cdd58985..0073612c56 100644 --- a/web/app/components/plugins/card/__tests__/index.spec.tsx +++ b/web/app/components/plugins/card/__tests__/index.spec.tsx @@ -181,7 +181,7 @@ describe('Card', () => { render() expect(screen.getByText('my-org')).toBeInTheDocument() - expect(screen.getByText('my-plugin')).toBeInTheDocument() + expect(screen.queryByText('my-plugin')).not.toBeInTheDocument() }) it('should render plugin icon', () => { @@ -596,7 +596,7 @@ describe('Card', () => { render() - expect(screen.getByText('plugin-with-special-chars!@#$%')).toBeInTheDocument() + expect(screen.getByText('org')).toBeInTheDocument() }) it('should handle very long title', () => { diff --git a/web/app/components/plugins/card/base/__tests__/download-count.spec.tsx b/web/app/components/plugins/card/base/__tests__/download-count.spec.tsx index 6bb52f8528..8a888af4c2 100644 --- a/web/app/components/plugins/card/base/__tests__/download-count.spec.tsx +++ b/web/app/components/plugins/card/base/__tests__/download-count.spec.tsx @@ -2,6 +2,12 @@ import { render, screen } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' import DownloadCount from '../download-count' +vi.mock('#i18n', () => ({ + useTranslation: () => ({ + t: (key: string) => key === 'marketplace.installs' ? 'installs' : key, + }), +})) + vi.mock('@/utils/format', () => ({ formatNumber: (n: number) => { if (n >= 1000) @@ -13,16 +19,16 @@ vi.mock('@/utils/format', () => ({ describe('DownloadCount', () => { it('renders formatted download count', () => { render() - expect(screen.getByText('1.5k')).toBeInTheDocument() + expect(screen.getByText('1.5k installs')).toBeInTheDocument() }) it('renders small numbers directly', () => { render() - expect(screen.getByText('42')).toBeInTheDocument() + expect(screen.getByText('42 installs')).toBeInTheDocument() }) it('renders zero download count', () => { render() - expect(screen.getByText('0')).toBeInTheDocument() + expect(screen.getByText('0 installs')).toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/marketplace/__tests__/atoms.spec.tsx b/web/app/components/plugins/marketplace/__tests__/atoms.spec.tsx index 16cb6aef56..61b4ec2343 100644 --- a/web/app/components/plugins/marketplace/__tests__/atoms.spec.tsx +++ b/web/app/components/plugins/marketplace/__tests__/atoms.spec.tsx @@ -15,6 +15,22 @@ import { } from '../atoms' import { DEFAULT_PLUGIN_SORT } from '../constants' +const { mockRouterPush, mockNavigation } = vi.hoisted(() => ({ + mockRouterPush: vi.fn(), + mockNavigation: { + pathname: '/plugins', + params: {} as Record, + }, +})) + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: mockRouterPush, + }), + usePathname: () => mockNavigation.pathname, + useParams: () => mockNavigation.params, +})) + const createWrapper = (searchParams = '') => { const { wrapper: NuqsWrapper } = createNuqsTestWrapper({ searchParams }) const wrapper = ({ children }: { children: ReactNode }) => ( @@ -30,6 +46,8 @@ const createWrapper = (searchParams = '') => { describe('Marketplace sort atoms', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} }) it('should return default sort value from useMarketplaceSort', () => { @@ -76,6 +94,8 @@ describe('Marketplace sort atoms', () => { describe('useSearchText', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} }) it('should return empty string as default', () => { @@ -108,6 +128,8 @@ describe('useSearchText', () => { describe('useActivePluginCategory', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} }) it('should return "all" as default category', () => { @@ -128,6 +150,8 @@ describe('useActivePluginCategory', () => { describe('useFilterPluginTags', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} }) it('should return empty array as default', () => { @@ -148,6 +172,8 @@ describe('useFilterPluginTags', () => { describe('useMarketplaceSearchMode', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} }) it('should return false when no search text, no tags, and category has collections (all)', () => { @@ -161,7 +187,7 @@ describe('useMarketplaceSearchMode', () => { const { wrapper } = createWrapper('?q=test&category=all') const { result } = renderHook(() => useMarketplaceSearchMode(), { wrapper }) - expect(result.current).toBe(true) + expect(result.current).toBeTruthy() }) it('should return true when tags are present', () => { @@ -189,6 +215,8 @@ describe('useMarketplaceSearchMode', () => { describe('useMarketplaceMoreClick', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} }) it('should return a callback function', () => { diff --git a/web/app/components/plugins/marketplace/__tests__/hooks-integration.spec.tsx b/web/app/components/plugins/marketplace/__tests__/hooks-integration.spec.tsx index e7655877a4..920652a0f0 100644 --- a/web/app/components/plugins/marketplace/__tests__/hooks-integration.spec.tsx +++ b/web/app/components/plugins/marketplace/__tests__/hooks-integration.spec.tsx @@ -42,8 +42,10 @@ const mockCollectionPlugins = vi.fn() vi.mock('@/service/client', () => ({ marketplaceClient: { - collections: (...args: unknown[]) => mockCollections(...args), - collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), + plugins: { + collections: (...args: unknown[]) => mockCollections(...args), + collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), + }, }, })) diff --git a/web/app/components/plugins/marketplace/__tests__/hydration-server.spec.tsx b/web/app/components/plugins/marketplace/__tests__/hydration-server.spec.tsx index ad1e208a2f..1994cf0a0d 100644 --- a/web/app/components/plugins/marketplace/__tests__/hydration-server.spec.tsx +++ b/web/app/components/plugins/marketplace/__tests__/hydration-server.spec.tsx @@ -2,6 +2,12 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +vi.mock('next/headers', () => ({ + headers: async () => ({ + get: (name: string) => name === 'sec-fetch-dest' ? 'document' : null, + }), +})) + vi.mock('@/config', () => ({ API_PREFIX: '/api', APP_VERSION: '1.0.0', @@ -15,15 +21,24 @@ vi.mock('@/utils/var', () => ({ const mockCollections = vi.fn() const mockCollectionPlugins = vi.fn() +const mockSearchAdvanced = vi.fn() vi.mock('@/service/client', () => ({ marketplaceClient: { - collections: (...args: unknown[]) => mockCollections(...args), - collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), + plugins: { + collections: (...args: unknown[]) => mockCollections(...args), + collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), + searchAdvanced: (...args: unknown[]) => mockSearchAdvanced(...args), + }, }, marketplaceQuery: { - collections: { - queryKey: (params: unknown) => ['marketplace', 'collections', params], + plugins: { + collections: { + queryKey: (params: unknown) => ['marketplace', 'plugins', 'collections', params], + }, + searchAdvanced: { + queryKey: (params: unknown) => ['marketplace', 'plugins', 'searchAdvanced', params], + }, }, }, })) @@ -46,6 +61,9 @@ describe('HydrateQueryClient', () => { mockCollectionPlugins.mockResolvedValue({ data: { plugins: [] }, }) + mockSearchAdvanced.mockResolvedValue({ + data: { plugins: [], total: 0 }, + }) }) it('should render children within HydrationBoundary', async () => { @@ -81,6 +99,7 @@ describe('HydrateQueryClient', () => { await HydrateQueryClient({ searchParams: Promise.resolve({ category: 'all' }), + isMarketplacePlatform: true, children:
Child
, }) @@ -92,31 +111,36 @@ describe('HydrateQueryClient', () => { await HydrateQueryClient({ searchParams: Promise.resolve({ category: 'tool' }), + isMarketplacePlatform: true, children:
Child
, }) expect(mockCollections).toHaveBeenCalled() }) - it('should not prefetch when category does not have collections (model)', async () => { + it('should prefetch search results when category does not have collections (model)', async () => { const { HydrateQueryClient } = await import('../hydration-server') await HydrateQueryClient({ searchParams: Promise.resolve({ category: 'model' }), + isMarketplacePlatform: true, children:
Child
, }) - expect(mockCollections).not.toHaveBeenCalled() + expect(mockCollections).toHaveBeenCalled() + expect(mockSearchAdvanced).toHaveBeenCalled() }) - it('should not prefetch when category does not have collections (bundle)', async () => { + it('should prefetch search results when category does not have collections (bundle)', async () => { const { HydrateQueryClient } = await import('../hydration-server') await HydrateQueryClient({ searchParams: Promise.resolve({ category: 'bundle' }), + isMarketplacePlatform: true, children:
Child
, }) - expect(mockCollections).not.toHaveBeenCalled() + expect(mockCollections).toHaveBeenCalled() + expect(mockSearchAdvanced).toHaveBeenCalled() }) }) diff --git a/web/app/components/plugins/marketplace/__tests__/plugin-type-switch.spec.tsx b/web/app/components/plugins/marketplace/__tests__/plugin-type-switch.spec.tsx index d950c30993..399ee91dc1 100644 --- a/web/app/components/plugins/marketplace/__tests__/plugin-type-switch.spec.tsx +++ b/web/app/components/plugins/marketplace/__tests__/plugin-type-switch.spec.tsx @@ -5,6 +5,22 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createNuqsTestWrapper } from '@/test/nuqs-testing' import { PluginCategorySwitch } from '../category-switch/plugin' +const { mockRouterPush, mockNavigation } = vi.hoisted(() => ({ + mockRouterPush: vi.fn(), + mockNavigation: { + pathname: '/plugins', + params: {} as Record, + }, +})) + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: mockRouterPush, + }), + usePathname: () => mockNavigation.pathname, + useParams: () => mockNavigation.params, +})) + vi.mock('#i18n', () => ({ useTranslation: () => ({ t: (key: string) => { @@ -38,6 +54,8 @@ const createWrapper = (searchParams = '') => { describe('PluginCategorySwitch', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} }) it('should render all category options', () => { diff --git a/web/app/components/plugins/marketplace/__tests__/query.spec.tsx b/web/app/components/plugins/marketplace/__tests__/query.spec.tsx index 80d8e6a932..c5d840024f 100644 --- a/web/app/components/plugins/marketplace/__tests__/query.spec.tsx +++ b/web/app/components/plugins/marketplace/__tests__/query.spec.tsx @@ -20,16 +20,20 @@ const mockSearchAdvanced = vi.fn() vi.mock('@/service/client', () => ({ marketplaceClient: { - collections: (...args: unknown[]) => mockCollections(...args), - collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), - searchAdvanced: (...args: unknown[]) => mockSearchAdvanced(...args), + plugins: { + collections: (...args: unknown[]) => mockCollections(...args), + collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), + searchAdvanced: (...args: unknown[]) => mockSearchAdvanced(...args), + }, }, marketplaceQuery: { - collections: { - queryKey: (params: unknown) => ['marketplace', 'collections', params], - }, - searchAdvanced: { - queryKey: (params: unknown) => ['marketplace', 'searchAdvanced', params], + plugins: { + collections: { + queryKey: (params: unknown) => ['marketplace', 'plugins', 'collections', params], + }, + searchAdvanced: { + queryKey: (params: unknown) => ['marketplace', 'plugins', 'searchAdvanced', params], + }, }, }, })) diff --git a/web/app/components/plugins/marketplace/__tests__/state.spec.tsx b/web/app/components/plugins/marketplace/__tests__/state.spec.tsx index 348b6d1ce0..08dacc63b9 100644 --- a/web/app/components/plugins/marketplace/__tests__/state.spec.tsx +++ b/web/app/components/plugins/marketplace/__tests__/state.spec.tsx @@ -5,6 +5,10 @@ import { Provider as JotaiProvider } from 'jotai' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createNuqsTestWrapper } from '@/test/nuqs-testing' +vi.mock('ahooks', () => ({ + useDebounce: (value: T) => value, +})) + vi.mock('@/config', () => ({ API_PREFIX: '/api', APP_VERSION: '1.0.0', @@ -19,19 +23,38 @@ vi.mock('@/utils/var', () => ({ const mockCollections = vi.fn() const mockCollectionPlugins = vi.fn() const mockSearchAdvanced = vi.fn() +const { mockRouterPush, mockNavigation } = vi.hoisted(() => ({ + mockRouterPush: vi.fn(), + mockNavigation: { + pathname: '/plugins', + params: {} as Record, + }, +})) + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: mockRouterPush, + }), + usePathname: () => mockNavigation.pathname, + useParams: () => mockNavigation.params, +})) vi.mock('@/service/client', () => ({ marketplaceClient: { - collections: (...args: unknown[]) => mockCollections(...args), - collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), - searchAdvanced: (...args: unknown[]) => mockSearchAdvanced(...args), + plugins: { + collections: (...args: unknown[]) => mockCollections(...args), + collectionPlugins: (...args: unknown[]) => mockCollectionPlugins(...args), + searchAdvanced: (...args: unknown[]) => mockSearchAdvanced(...args), + }, }, marketplaceQuery: { - collections: { - queryKey: (params: unknown) => ['marketplace', 'collections', params], - }, - searchAdvanced: { - queryKey: (params: unknown) => ['marketplace', 'searchAdvanced', params], + plugins: { + collections: { + queryKey: (params: unknown) => ['marketplace', 'plugins', 'collections', params], + }, + searchAdvanced: { + queryKey: (params: unknown) => ['marketplace', 'plugins', 'searchAdvanced', params], + }, }, }, })) @@ -58,6 +81,8 @@ const createWrapper = (searchParams = '') => { describe('usePluginsMarketplaceData', () => { beforeEach(() => { vi.clearAllMocks() + mockNavigation.pathname = '/plugins' + mockNavigation.params = {} mockCollections.mockResolvedValue({ data: { diff --git a/web/app/components/plugins/marketplace/__tests__/utils.spec.ts b/web/app/components/plugins/marketplace/__tests__/utils.spec.ts index 6514b01031..5e5f4d0ac4 100644 --- a/web/app/components/plugins/marketplace/__tests__/utils.spec.ts +++ b/web/app/components/plugins/marketplace/__tests__/utils.spec.ts @@ -109,7 +109,7 @@ describe('getPluginLinkInMarketplace', () => { const { getPluginLinkInMarketplace } = await import('../utils') const plugin = createMockPlugin({ org: 'test-org', name: 'test-plugin', type: 'plugin' }) const link = getPluginLinkInMarketplace(plugin) - expect(link).toBe('https://marketplace.dify.ai/plugins/test-org/test-plugin') + expect(link).toBe('https://marketplace.dify.ai/plugin/test-org/test-plugin') }) it('should return correct link for bundle', async () => { @@ -125,7 +125,7 @@ describe('getPluginDetailLinkInMarketplace', () => { const { getPluginDetailLinkInMarketplace } = await import('../utils') const plugin = createMockPlugin({ org: 'test-org', name: 'test-plugin', type: 'plugin' }) const link = getPluginDetailLinkInMarketplace(plugin) - expect(link).toBe('/plugins/test-org/test-plugin') + expect(link).toBe('/plugin/test-org/test-plugin') }) it('should return correct detail link for bundle', async () => { @@ -149,7 +149,7 @@ describe('getPluginCondition', () => { it('should return category condition for agent', async () => { const { getPluginCondition } = await import('../utils') - expect(getPluginCondition(PluginCategoryEnum.agent)).toBe('category=agent') + expect(getPluginCondition(PluginCategoryEnum.agent)).toBe('category=agent-strategy') }) it('should return category condition for datasource', async () => { diff --git a/web/app/components/plugins/marketplace/description/__tests__/index.spec.tsx b/web/app/components/plugins/marketplace/description/__tests__/index.spec.tsx index 6761fab7b6..cc6dbde037 100644 --- a/web/app/components/plugins/marketplace/description/__tests__/index.spec.tsx +++ b/web/app/components/plugins/marketplace/description/__tests__/index.spec.tsx @@ -2,648 +2,100 @@ 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 = { - '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 = { - 'operation.in': 'in', -} - -// Mock i18n hooks vi.mock('#i18n', () => ({ - useLocale: vi.fn(() => mockDefaultLocale), - useTranslation: vi.fn((ns: string) => ({ + useTranslation: () => ({ t: (key: string) => { - if (ns === 'plugin') - return pluginTranslations[key] || key - if (ns === 'common') - return commonTranslations[key] || key - return key + const translations: Record = { + 'marketplace.pluginsHeroTitle': 'Build with plugins', + 'marketplace.pluginsHeroSubtitle': 'Discover and install marketplace plugins.', + 'marketplace.templatesHeroTitle': 'Build with templates', + 'marketplace.templatesHeroSubtitle': 'Explore reusable templates.', + } + return translations[key] || key }, - })), + }), })) -// ================================ -// Description Component Tests -// ================================ +let mockCreationType = 'plugins' + +vi.mock('../../atoms', () => ({ + useCreationType: () => mockCreationType, +})) + +vi.mock('../../search-params', () => ({ + CREATION_TYPE: { + plugins: 'plugins', + templates: 'templates', + }, +})) + +vi.mock('../../category-switch', () => ({ + PluginCategorySwitch: ({ variant }: { variant?: string }) =>
{variant}
, + TemplateCategorySwitch: ({ variant }: { variant?: string }) =>
{variant}
, +})) + +vi.mock('motion/react', () => ({ + motion: { + div: ({ children, ...props }: React.HTMLAttributes) =>
{children}
, + }, + useMotionValue: (value: number) => ({ set: vi.fn(), get: () => value }), + useSpring: (value: unknown) => value, + useTransform: (...args: unknown[]) => { + const values = args[0] + if (Array.isArray(values)) + return 0 + return values + }, +})) + +class ResizeObserverMock { + observe() {} + disconnect() {} +} + describe('Description', () => { beforeEach(() => { vi.clearAllMocks() - mockDefaultLocale = 'en-US' + mockCreationType = 'plugins' + vi.stubGlobal('ResizeObserver', ResizeObserverMock) + vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { + cb(0) + return 1 + }) + vi.stubGlobal('cancelAnimationFrame', vi.fn()) }) - // ================================ - // Rendering Tests - // ================================ describe('Rendering', () => { - it('should render without crashing', () => { - const { container } = render() - - expect(container.firstChild).toBeInTheDocument() - }) - - it('should render h1 heading with empower text', () => { + it('should render plugin hero content by default', () => { render() - const heading = screen.getByRole('heading', { level: 1 }) - expect(heading).toBeInTheDocument() - expect(heading).toHaveTextContent('Empower your AI development') + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Build with plugins') + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Discover and install marketplace plugins.') + expect(screen.getByTestId('plugin-category-switch')).toHaveTextContent('hero') }) - it('should render h2 subheading', () => { + it('should render template hero content when creationType is templates', () => { + mockCreationType = 'templates' + render() - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading).toBeInTheDocument() - }) - - it('should apply correct CSS classes to h1', () => { - render() - - 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() - - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading).toHaveClass('body-md-regular') - expect(subheading).toHaveClass('text-center') - expect(subheading).toHaveClass('text-text-tertiary') + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Build with templates') + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Explore reusable templates.') + expect(screen.getByTestId('template-category-switch')).toHaveTextContent('hero') }) }) - // ================================ - // Non-Chinese Locale Rendering Tests - // ================================ - describe('Non-Chinese Locale Rendering', () => { - beforeEach(() => { - mockDefaultLocale = 'en-US' + describe('Props', () => { + it('should render marketplace nav content when provided', () => { + render(Nav} />) + + expect(screen.getByTestId('marketplace-nav')).toBeInTheDocument() }) - it('should render discover text for en-US locale', () => { - render() + it('should apply custom className to the sticky wrapper', () => { + const { container } = render() - expect(screen.getByText(/Discover/)).toBeInTheDocument() - }) - - it('should render all category names', () => { - render() - - 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() - - 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() - - expect(screen.getByText('in')).toBeInTheDocument() - }) - - it('should render Dify Marketplace text at the end for non-Chinese locales', () => { - render() - - 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() - - 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() - - 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() - - // 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() - - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading.textContent).toContain('Dify Marketplace') - }) - - it('should render discover text for zh-Hans locale', () => { - render() - - expect(screen.getByText(/Discover/)).toBeInTheDocument() - }) - - it('should render all categories for zh-Hans locale', () => { - render() - - 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() - - // 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() - - expect(screen.getByText('Empower your AI development')).toBeInTheDocument() - }) - - it('should handle ja-JP locale as non-Chinese', () => { - mockDefaultLocale = 'ja-JP' - render() - - // 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() - - // 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() - - expect(screen.getByText('Empower your AI development')).toBeInTheDocument() - }) - - it('should handle fr-FR locale as non-Chinese', () => { - mockDefaultLocale = 'fr-FR' - render() - - expect(screen.getByText('Empower your AI development')).toBeInTheDocument() - }) - - it('should handle pt-BR locale as non-Chinese', () => { - mockDefaultLocale = 'pt-BR' - render() - - expect(screen.getByText('Empower your AI development')).toBeInTheDocument() - }) - - it('should handle es-ES locale as non-Chinese', () => { - mockDefaultLocale = 'es-ES' - render() - - 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() - - // 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() - - // 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() - - // 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() - - 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() - - const categorySpan = container.querySelector('.after\\:absolute') - expect(categorySpan).toBeInTheDocument() - }) - - it('should apply correct after pseudo-element classes', () => { - const { container } = render() - - // 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() - - const categorySpans = container.querySelectorAll('.after\\:w-full') - expect(categorySpans.length).toBe(7) - }) - - it('should apply correct height to after element', () => { - const { container } = render() - - 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() - - 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() - - const categorySpans = container.querySelectorAll('.z-\\[1\\]') - expect(categorySpans.length).toBe(7) - }) - - it('should apply left margin to category spans', () => { - const { container } = render() - - 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() - - // 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() - - // 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() - - // 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() - - 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() - - 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() - - 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() - - 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() - - const heading = screen.getByRole('heading', { level: 1 }) - expect(heading).toHaveClass('shrink-0') - }) - - it('should have shrink-0 on h2 subheading', () => { - render() - - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading).toHaveClass('shrink-0') - }) - - it('should have flex layout on h2', () => { - render() - - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading).toHaveClass('flex') - }) - - it('should have items-center on h2', () => { - render() - - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading).toHaveClass('items-center') - }) - - it('should have justify-center on h2', () => { - render() - - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading).toHaveClass('justify-center') - }) - }) - - // ================================ - // Accessibility Tests - // ================================ - describe('Accessibility', () => { - it('should have proper heading hierarchy', () => { - render() - - 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() - - const h1 = screen.getByRole('heading', { level: 1 }) - expect(h1.textContent).not.toBe('') - }) - - it('should have visible h1 heading', () => { - render() - - const heading = screen.getByRole('heading', { level: 1 }) - expect(heading).toBeVisible() - }) - - it('should have visible h2 heading', () => { - render() - - const subheading = screen.getByRole('heading', { level: 2 }) - expect(subheading).toBeVisible() + expect(container.querySelector('.custom-hero-class')).toBeInTheDocument() }) }) }) - -// ================================ -// Integration Tests -// ================================ -describe('Description Integration', () => { - beforeEach(() => { - vi.clearAllMocks() - mockDefaultLocale = 'en-US' - }) - - it('should render complete component structure', () => { - const { container } = render() - - // 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() - - // 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() - const enContent = enContainer.querySelector('h2')?.textContent || '' - unmountEn() - - // Render zh-Hans - mockDefaultLocale = 'zh-Hans' - const { container: zhContainer } = render() - 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() - const enCategoryCount = enContainer.querySelectorAll('.body-md-medium').length - unmountEn() - - // Render zh-Hans - mockDefaultLocale = 'zh-Hans' - const { container: zhContainer } = render() - 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) - }) -}) diff --git a/web/app/components/plugins/marketplace/list/__tests__/index.spec.tsx b/web/app/components/plugins/marketplace/list/__tests__/index.spec.tsx index 8bd930f210..fe25d57555 100644 --- a/web/app/components/plugins/marketplace/list/__tests__/index.spec.tsx +++ b/web/app/components/plugins/marketplace/list/__tests__/index.spec.tsx @@ -43,6 +43,7 @@ const { mockMarketplaceData, mockMoreClick } = vi.hoisted(() => { mockMoreClick: vi.fn(), } }) +let mockSearchMode = false vi.mock('../../state', () => ({ useMarketplaceData: () => mockMarketplaceData, @@ -51,7 +52,11 @@ vi.mock('../../state', () => ({ vi.mock('../../atoms', () => ({ useMarketplaceMoreClick: () => mockMoreClick, - useMarketplaceSearchMode: () => false, + useMarketplaceSearchMode: () => mockSearchMode, + useCreationType: () => 'plugins', + useFilterPluginTags: () => [[]], + useActivePluginCategory: () => ['all'], + useActiveTemplateCategory: () => ['all'], })) vi.mock('@/context/i18n', () => ({ @@ -148,7 +153,7 @@ vi.mock('../../sort-dropdown', () => ({ })) // Mock Empty component -vi.mock('../empty', () => ({ +vi.mock('../../empty', () => ({ default: ({ className, text }: { className?: string, text?: string }) => (
{text || 'No plugins found'} @@ -234,6 +239,7 @@ describe('List', () => { beforeEach(() => { vi.clearAllMocks() + mockSearchMode = false }) // ================================ @@ -872,6 +878,7 @@ describe('ListWithCollection', () => { describe('ListWrapper', () => { beforeEach(() => { vi.clearAllMocks() + mockSearchMode = false // Reset mock data mockMarketplaceData.plugins = undefined mockMarketplaceData.pluginsTotal = 0 @@ -917,6 +924,7 @@ describe('ListWrapper', () => { }) it('should render template empty state with flex content wrapper when templates are empty', () => { + mockSearchMode = true delete (mockMarketplaceData as Record).pluginCollections delete (mockMarketplaceData as Record).pluginCollectionPluginsMap ;(mockMarketplaceData as Record).templateCollections = [] @@ -931,6 +939,7 @@ describe('ListWrapper', () => { }) it('should keep plugin empty text when plugins are empty', () => { + mockSearchMode = true mockMarketplaceData.plugins = [] mockMarketplaceData.pluginsTotal = 0 mockMarketplaceData.pluginCollections = [] @@ -947,16 +956,18 @@ describe('ListWrapper', () => { // Plugins Header Tests // ================================ describe('Plugins Header', () => { - it('should render plugins result count when plugins are present', () => { + it('should render list top info when search mode is enabled', () => { + mockSearchMode = true mockMarketplaceData.plugins = createMockPluginList(5) mockMarketplaceData.pluginsTotal = 5 render() - expect(screen.getByText('5 plugins found')).toBeInTheDocument() + expect(screen.getByTestId('sort-dropdown')).toBeInTheDocument() }) it('should render SortDropdown when plugins are present', () => { + mockSearchMode = true mockMarketplaceData.plugins = createMockPluginList(1) render() @@ -1039,25 +1050,28 @@ describe('ListWrapper', () => { // ================================ describe('Edge Cases', () => { it('should handle empty plugins array', () => { + mockSearchMode = true mockMarketplaceData.plugins = [] mockMarketplaceData.pluginsTotal = 0 render() - expect(screen.getByText('0 plugins found')).toBeInTheDocument() expect(screen.getByTestId('empty-component')).toBeInTheDocument() }) - it('should handle large pluginsTotal', () => { + it('should handle many plugin results', () => { + mockSearchMode = true mockMarketplaceData.plugins = createMockPluginList(10) mockMarketplaceData.pluginsTotal = 10000 render() - expect(screen.getByText('10000 plugins found')).toBeInTheDocument() + expect(screen.getByTestId('card-plugin-0')).toBeInTheDocument() + expect(screen.getByTestId('card-plugin-9')).toBeInTheDocument() }) it('should handle both loading and has plugins', () => { + mockSearchMode = true mockMarketplaceData.isLoading = true mockMarketplaceData.page = 2 mockMarketplaceData.plugins = createMockPluginList(5) @@ -1065,9 +1079,7 @@ describe('ListWrapper', () => { render() - // Should show plugins header and list - expect(screen.getByText('50 plugins found')).toBeInTheDocument() - // Should not show loading because page > 1 + expect(screen.getByTestId('card-plugin-0')).toBeInTheDocument() expect(screen.queryByTestId('loading-component')).not.toBeInTheDocument() }) }) @@ -1405,6 +1417,7 @@ describe('Combined Workflows', () => { }) it('should transition from collections to search results', async () => { + mockSearchMode = true mockMarketplaceData.pluginCollections = createMockCollectionList(1) mockMarketplaceData.pluginCollectionPluginsMap = { 'collection-0': createMockPluginList(1), @@ -1421,20 +1434,21 @@ describe('Combined Workflows', () => { rerender() expect(screen.queryByText('Collection 0')).not.toBeInTheDocument() - expect(screen.getByText('5 plugins found')).toBeInTheDocument() + expect(screen.getByTestId('card-plugin-0')).toBeInTheDocument() }) it('should handle empty search results', () => { + mockSearchMode = true mockMarketplaceData.plugins = [] mockMarketplaceData.pluginsTotal = 0 render() expect(screen.getByTestId('empty-component')).toBeInTheDocument() - expect(screen.getByText('0 plugins found')).toBeInTheDocument() }) it('should support pagination (page > 1)', () => { + mockSearchMode = true mockMarketplaceData.plugins = createMockPluginList(40) mockMarketplaceData.pluginsTotal = 80 mockMarketplaceData.isLoading = true @@ -1442,9 +1456,7 @@ describe('Combined Workflows', () => { render() - // Should show existing results while loading more - expect(screen.getByText('80 plugins found')).toBeInTheDocument() - // Should not show loading spinner for pagination + expect(screen.getByTestId('card-plugin-0')).toBeInTheDocument() expect(screen.queryByTestId('loading-component')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/marketplace/list/list-wrapper-flat.spec.tsx b/web/app/components/plugins/marketplace/list/list-wrapper-flat.spec.tsx index 2657d74044..5d7c961e81 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper-flat.spec.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper-flat.spec.tsx @@ -21,6 +21,11 @@ const { mockMarketplaceData } = vi.hoisted(() => ({ vi.mock('../state', () => ({ useMarketplaceData: () => mockMarketplaceData, + isPluginsData: (data: typeof mockMarketplaceData) => data.creationType === 'plugins', +})) + +vi.mock('../atoms', () => ({ + useMarketplaceSearchMode: () => false, })) vi.mock('@/app/components/base/loading', () => ({ diff --git a/web/app/components/plugins/marketplace/list/template-card.spec.tsx b/web/app/components/plugins/marketplace/list/template-card.spec.tsx index 29c7c58136..5002cd12f5 100644 --- a/web/app/components/plugins/marketplace/list/template-card.spec.tsx +++ b/web/app/components/plugins/marketplace/list/template-card.spec.tsx @@ -44,9 +44,8 @@ vi.mock('next/link', () => ({ ), })) -// Mock next-themes -vi.mock('next-themes', () => ({ - useTheme: () => ({ theme: 'light' }), +vi.mock('@/hooks/use-theme', () => ({ + default: () => ({ theme: 'light' }), })) // Mock marketplace utils @@ -66,16 +65,20 @@ vi.mock('@/app/components/plugins/card/base/corner-mark', () => ({ default: ({ text }: { text: string }) => {text}, })) -// Mock marketplace utils (getTemplateIconUrl) -vi.mock('../utils', () => ({ - getTemplateIconUrl: (template: { id: string, icon?: string, icon_file_key?: string }): string => { - if (template.icon?.startsWith('http')) - return template.icon - if (template.icon_file_key) - return `https://marketplace.dify.ai/api/v1/templates/${template.id}/icon` - return '' - }, -})) +// Mock marketplace utils (getTemplateIconUrl) while keeping other exports intact. +vi.mock('../utils', async () => { + const actual = await vi.importActual('../utils') + return { + ...actual, + getTemplateIconUrl: (template: { id: string, icon?: string, icon_file_key?: string }): string => { + if (template.icon?.startsWith('http')) + return template.icon + if (template.icon_file_key) + return `https://marketplace.dify.ai/api/v1/templates/${template.id}/icon` + return '' + }, + } +}) // ================================ // Test Data Factories diff --git a/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx b/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx index fcafc9e748..0624504409 100644 --- a/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx +++ b/web/app/components/plugins/marketplace/search-box/__tests__/index.spec.tsx @@ -41,10 +41,18 @@ vi.mock('ahooks', () => ({ useDebounce: (value: string) => value, })) +const mockRouterPush = vi.fn() +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: mockRouterPush, + }), +})) + vi.mock('jotai', async () => { const actual = await vi.importActual('jotai') return { ...actual, + useAtomValue: () => false, useSetAtom: () => vi.fn(), } }) @@ -61,6 +69,7 @@ vi.mock('@/hooks/use-i18n', () => ({ const { mockSearchText, mockHandleSearchTextChange, + mockSetSearchTab, mockFilterPluginTags, mockHandleFilterPluginTagsChange, mockActivePluginCategory, @@ -69,6 +78,7 @@ const { return { mockSearchText: '', mockHandleSearchTextChange: vi.fn(), + mockSetSearchTab: vi.fn(), mockFilterPluginTags: [] as string[], mockHandleFilterPluginTagsChange: vi.fn(), mockActivePluginCategory: 'all', @@ -87,8 +97,9 @@ vi.mock('../../atoms', () => ({ useMarketplaceTemplateSortValue: () => ({ sortBy: 'usage_count', sortOrder: 'DESC' }), useActiveSort: () => [mockSortValue, vi.fn()], useActiveSortValue: () => mockSortValue, - useCreationType: () => ['plugins', vi.fn()], - useSearchTab: () => ['', vi.fn()], + useCreationType: () => 'plugins', + useSearchTab: () => ['', mockSetSearchTab], + isMarketplacePlatformAtom: {}, searchModeAtom: {}, })) @@ -135,7 +146,7 @@ vi.mock('@/app/components/plugins/hooks', () => ({ })) let mockDropdownPlugins: Plugin[] = [] -vi.mock('../query', () => ({ +vi.mock('../../query', () => ({ useMarketplaceUnifiedSearch: () => ({ data: { plugins: { items: mockDropdownPlugins, total: mockDropdownPlugins.length }, @@ -233,6 +244,7 @@ describe('SearchBox', () => { vi.clearAllMocks() mockPortalOpenState = false mockDropdownPlugins = [] + mockRouterPush.mockReset() }) // ================================ @@ -630,6 +642,7 @@ describe('SearchBoxWrapper', () => { vi.clearAllMocks() mockPortalOpenState = false mockDropdownPlugins = [] + mockRouterPush.mockReset() }) describe('Rendering', () => { @@ -639,17 +652,10 @@ describe('SearchBoxWrapper', () => { expect(screen.getByRole('textbox')).toBeInTheDocument() }) - it('should render in marketplace mode', () => { - const { container } = render() + it('should apply custom wrapper class to the input wrapper', () => { + const { container } = render() - expect(container.querySelector('.rounded-xl')).toBeInTheDocument() - }) - - it('should apply correct wrapper classes', () => { - const { container } = render() - - // Check for z-[11] class from wrapper - expect(container.querySelector('.z-\\[11\\]')).toBeInTheDocument() + expect(container.querySelector('.custom-wrapper')).toBeInTheDocument() }) }) @@ -671,6 +677,7 @@ describe('SearchBoxWrapper', () => { fireEvent.keyDown(input, { key: 'Enter' }) expect(mockHandleSearchTextChange).toHaveBeenCalledWith('new search') + expect(mockSetSearchTab).toHaveBeenCalledWith('all') }) it('should clear committed search when input is emptied and blurred', () => { @@ -702,7 +709,7 @@ describe('SearchBoxWrapper', () => { it('should use translation for placeholder', () => { render() - expect(screen.getByPlaceholderText('Search plugins')).toBeInTheDocument() + expect(screen.getByPlaceholderText('searchInMarketplace')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/plugins/marketplace/sort-dropdown/__tests__/index.spec.tsx b/web/app/components/plugins/marketplace/sort-dropdown/__tests__/index.spec.tsx index 0f2a55f8e3..a3c9647e50 100644 --- a/web/app/components/plugins/marketplace/sort-dropdown/__tests__/index.spec.tsx +++ b/web/app/components/plugins/marketplace/sort-dropdown/__tests__/index.spec.tsx @@ -1,15 +1,8 @@ import { fireEvent, render, screen, within } from '@testing-library/react' -import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import SortDropdown from '../index' -// ================================ -// Mock external dependencies only -// ================================ - -// Mock i18n translation hook const mockTranslation = vi.fn((key: string, options?: { ns?: string }) => { - // Build full key with namespace prefix if provided const fullKey = options?.ns ? `${options.ns}.${key}` : key const translations: Record = { 'plugin.marketplace.sortBy': 'Sort by', @@ -27,73 +20,36 @@ vi.mock('#i18n', () => ({ }), })) -// Mock marketplace atoms with controllable values let mockSort: { sortBy: string, sortOrder: string } = { sortBy: 'install_count', sortOrder: 'DESC' } const mockHandleSortChange = vi.fn() let mockCreationType = 'plugins' -vi.mock('../atoms', () => ({ +vi.mock('../../atoms', () => ({ useActiveSort: () => [mockSort, mockHandleSortChange], - useCreationType: () => [mockCreationType, vi.fn()], - useMarketplaceSort: () => [mockSort, mockHandleSortChange], + useCreationType: () => mockCreationType, })) -vi.mock('../search-params', () => ({ +vi.mock('../../search-params', () => ({ CREATION_TYPE: { plugins: 'plugins', templates: 'templates' }, })) -// Mock portal component with controllable open state let mockPortalOpenState = false vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ - PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: { - children: React.ReactNode - open: boolean - onOpenChange: (open: boolean) => void - }) => { + PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => { mockPortalOpenState = open - return ( -
- {children} -
- ) + return
{children}
}, - PortalToFollowElemTrigger: ({ children, onClick }: { - children: React.ReactNode - onClick: () => void - }) => ( -
- {children} -
+ PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => ( +
{children}
), PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => { - // Match actual behavior: only render when portal is open if (!mockPortalOpenState) return null return
{children}
}, })) -// ================================ -// Test Factory Functions -// ================================ - -type SortOption = { - value: string - order: string - text: string -} - -const createSortOptions = (): SortOption[] => [ - { value: 'install_count', order: 'DESC', text: 'Most Popular' }, - { value: 'version_updated_at', order: 'DESC', text: 'Recently Updated' }, - { value: 'created_at', order: 'DESC', text: 'Newly Released' }, - { value: 'created_at', order: 'ASC', text: 'First Released' }, -] - -// ================================ -// SortDropdown Component Tests -// ================================ describe('SortDropdown', () => { beforeEach(() => { vi.clearAllMocks() @@ -102,149 +58,40 @@ describe('SortDropdown', () => { mockPortalOpenState = false }) - // ================================ - // Rendering Tests - // ================================ describe('Rendering', () => { - it('should render without crashing', () => { - render() - - expect(screen.getByTestId('portal-wrapper')).toBeInTheDocument() - }) - - it('should render sort by label', () => { + it('should render selected plugin sort label by default', () => { render() expect(screen.getByText('Sort by')).toBeInTheDocument() - }) - - it('should render selected option text', () => { - render() - expect(screen.getByText('Most Popular')).toBeInTheDocument() + expect(screen.getByTestId('portal-wrapper')).toHaveAttribute('data-open', 'false') }) - it('should render arrow down icon', () => { - const { container } = render() + it('should render template sort label when creationType is templates', () => { + mockCreationType = 'templates' + mockSort = { sortBy: 'updated_at', sortOrder: 'DESC' } - const arrowIcon = container.querySelector('.h-4.w-4.text-text-tertiary') - expect(arrowIcon).toBeInTheDocument() - }) - - it('should render trigger element with correct styles', () => { - const { container } = render() - - const trigger = container.querySelector('.cursor-pointer') - expect(trigger).toBeInTheDocument() - expect(trigger).toHaveClass('h-8', 'rounded-lg', 'bg-state-base-hover-alt') - }) - - it('should not render dropdown content when closed', () => { - render() - - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() - }) - }) - - // ================================ - // State Management Tests - // ================================ - describe('State Management', () => { - it('should initialize with closed state', () => { - render() - - const wrapper = screen.getByTestId('portal-wrapper') - expect(wrapper).toHaveAttribute('data-open', 'false') - }) - - it('should display correct selected option for install_count DESC', () => { - mockSort = { sortBy: 'install_count', sortOrder: 'DESC' } - render() - - expect(screen.getByText('Most Popular')).toBeInTheDocument() - }) - - it('should display correct selected option for version_updated_at DESC', () => { - mockSort = { sortBy: 'version_updated_at', sortOrder: 'DESC' } render() expect(screen.getByText('Recently Updated')).toBeInTheDocument() }) - - it('should display correct selected option for created_at DESC', () => { - mockSort = { sortBy: 'created_at', sortOrder: 'DESC' } - render() - - expect(screen.getByText('Newly Released')).toBeInTheDocument() - }) - - it('should display correct selected option for created_at ASC', () => { - mockSort = { sortBy: 'created_at', sortOrder: 'ASC' } - render() - - expect(screen.getByText('First Released')).toBeInTheDocument() - }) - - it('should toggle open state when trigger clicked', () => { - render() - - const trigger = screen.getByTestId('portal-trigger') - fireEvent.click(trigger) - - // After click, portal content should be visible - expect(screen.getByTestId('portal-content')).toBeInTheDocument() - }) - - it('should close dropdown when trigger clicked again', () => { - render() - - const trigger = screen.getByTestId('portal-trigger') - - // Open - fireEvent.click(trigger) - expect(screen.getByTestId('portal-content')).toBeInTheDocument() - - // Close - fireEvent.click(trigger) - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() - }) }) - // ================================ - // User Interactions Tests - // ================================ - describe('User Interactions', () => { - it('should open dropdown on trigger click', () => { + describe('Interactions', () => { + it('should open the dropdown when trigger is clicked', () => { render() - const trigger = screen.getByTestId('portal-trigger') - fireEvent.click(trigger) + fireEvent.click(screen.getByTestId('portal-trigger')) expect(screen.getByTestId('portal-content')).toBeInTheDocument() + expect(within(screen.getByTestId('portal-content')).getByText('Recently Updated')).toBeInTheDocument() }) - it('should render all sort options when open', () => { + it('should call handleSortChange with plugin sort option values', () => { render() - // Open dropdown fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - expect(within(content).getByText('Most Popular')).toBeInTheDocument() - expect(within(content).getByText('Recently Updated')).toBeInTheDocument() - expect(within(content).getByText('Newly Released')).toBeInTheDocument() - expect(within(content).getByText('First Released')).toBeInTheDocument() - }) - - it('should call handleSortChange when option clicked', () => { - render() - - // Open dropdown - fireEvent.click(screen.getByTestId('portal-trigger')) - - // Click on "Recently Updated" - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText('Recently Updated')) + fireEvent.click(within(screen.getByTestId('portal-content')).getByText('Recently Updated')) expect(mockHandleSortChange).toHaveBeenCalledWith({ sortBy: 'version_updated_at', @@ -252,455 +99,28 @@ describe('SortDropdown', () => { }) }) - it('should call handleSortChange with correct params for Most Popular', () => { - mockSort = { sortBy: 'created_at', sortOrder: 'DESC' } + it('should call handleSortChange with template sort option values', () => { + mockCreationType = 'templates' + render() fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText('Most Popular')) + fireEvent.click(within(screen.getByTestId('portal-content')).getByText('Most Popular')) expect(mockHandleSortChange).toHaveBeenCalledWith({ - sortBy: 'install_count', + sortBy: 'usage_count', sortOrder: 'DESC', }) }) - - it('should call handleSortChange with correct params for Newly Released', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText('Newly Released')) - - expect(mockHandleSortChange).toHaveBeenCalledWith({ - sortBy: 'created_at', - sortOrder: 'DESC', - }) - }) - - it('should call handleSortChange with correct params for First Released', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText('First Released')) - - expect(mockHandleSortChange).toHaveBeenCalledWith({ - sortBy: 'created_at', - sortOrder: 'ASC', - }) - }) - - it('should allow selecting currently selected option', () => { - mockSort = { sortBy: 'install_count', sortOrder: 'DESC' } - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText('Most Popular')) - - expect(mockHandleSortChange).toHaveBeenCalledWith({ - sortBy: 'install_count', - sortOrder: 'DESC', - }) - }) - - it('should support userEvent for trigger click', async () => { - const user = userEvent.setup() - render() - - const trigger = screen.getByTestId('portal-trigger') - await user.click(trigger) - - expect(screen.getByTestId('portal-content')).toBeInTheDocument() - }) }) - // ================================ - // Check Icon Tests - // ================================ - describe('Check Icon', () => { - it('should show check icon for selected option', () => { - mockSort = { sortBy: 'install_count', sortOrder: 'DESC' } - const { container } = render() - - // Open dropdown - fireEvent.click(screen.getByTestId('portal-trigger')) - - // Check icon should be present in the dropdown - const checkIcon = container.querySelector('.text-text-accent') - expect(checkIcon).toBeInTheDocument() - }) - - it('should show check icon only for matching sortBy AND sortOrder', () => { - mockSort = { sortBy: 'created_at', sortOrder: 'DESC' } - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const options = content.querySelectorAll('.cursor-pointer') - - // "Newly Released" (created_at DESC) should have check icon - // "First Released" (created_at ASC) should NOT have check icon - expect(options.length).toBe(4) - }) - - it('should not show check icon for different sortOrder with same sortBy', () => { - mockSort = { sortBy: 'created_at', sortOrder: 'DESC' } - const { container } = render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - // Only one check icon should be visible (for Newly Released, not First Released) - const checkIcons = container.querySelectorAll('.text-text-accent') - expect(checkIcons.length).toBe(1) - }) - }) - - // ================================ - // Dropdown Options Structure Tests - // ================================ - describe('Dropdown Options Structure', () => { - const sortOptions = createSortOptions() - - it('should render 4 sort options', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const options = content.querySelectorAll('.cursor-pointer') - expect(options.length).toBe(4) - }) - - it.each(sortOptions)('should render option: $text', ({ text }) => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - expect(within(content).getByText(text)).toBeInTheDocument() - }) - - it('should render options with unique keys', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const options = content.querySelectorAll('.cursor-pointer') - - // All options should be rendered (no key conflicts) - expect(options.length).toBe(4) - }) - - it('should render dropdown container with correct styles', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const container = content.firstChild as HTMLElement - expect(container).toHaveClass('rounded-xl', 'shadow-lg') - }) - - it('should render option items with hover styles', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const option = content.querySelector('.cursor-pointer') - expect(option).toHaveClass('hover:bg-components-panel-on-panel-item-bg-hover') - }) - }) - - // ================================ - // Edge Cases Tests - // ================================ - describe('Edge Cases', () => { - // The component falls back to the first option (Most Popular) when sort values are invalid - - it('should fallback to default option when sortBy is unknown', () => { - mockSort = { sortBy: 'unknown_field', sortOrder: 'DESC' } - - render() - - // Should fallback to first option "Most Popular" - expect(screen.getByText('Most Popular')).toBeInTheDocument() - }) - - it('should fallback to default option when sortBy is empty', () => { - mockSort = { sortBy: '', sortOrder: 'DESC' } + describe('Selection', () => { + it('should fall back to the first option when the sort does not match any option', () => { + mockSort = { sortBy: 'unknown', sortOrder: 'DESC' } render() expect(screen.getByText('Most Popular')).toBeInTheDocument() }) - - it('should fallback to default option when sortOrder is unknown', () => { - mockSort = { sortBy: 'install_count', sortOrder: 'UNKNOWN' } - - render() - - expect(screen.getByText('Most Popular')).toBeInTheDocument() - }) - - it('should render correctly when handleSortChange is a no-op', () => { - mockHandleSortChange.mockImplementation(() => {}) - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText('Recently Updated')) - - expect(mockHandleSortChange).toHaveBeenCalled() - }) - - it('should handle rapid toggle clicks', () => { - render() - - const trigger = screen.getByTestId('portal-trigger') - - // Rapid clicks - fireEvent.click(trigger) - fireEvent.click(trigger) - fireEvent.click(trigger) - - // Final state should be open (odd number of clicks) - expect(screen.getByTestId('portal-content')).toBeInTheDocument() - }) - - it('should handle multiple option selections', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - - // Click multiple options - fireEvent.click(within(content).getByText('Recently Updated')) - fireEvent.click(within(content).getByText('Newly Released')) - fireEvent.click(within(content).getByText('First Released')) - - expect(mockHandleSortChange).toHaveBeenCalledTimes(3) - }) - }) - - // ================================ - // Context Integration Tests - // ================================ - describe('Context Integration', () => { - it('should read sort value from context', () => { - mockSort = { sortBy: 'version_updated_at', sortOrder: 'DESC' } - render() - - expect(screen.getByText('Recently Updated')).toBeInTheDocument() - }) - - it('should call context handleSortChange on selection', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText('First Released')) - - expect(mockHandleSortChange).toHaveBeenCalledWith({ - sortBy: 'created_at', - sortOrder: 'ASC', - }) - }) - - it('should update display when context sort changes', () => { - const { rerender } = render() - - expect(screen.getByText('Most Popular')).toBeInTheDocument() - - // Simulate context change - mockSort = { sortBy: 'created_at', sortOrder: 'ASC' } - rerender() - - expect(screen.getByText('First Released')).toBeInTheDocument() - }) - - it('should use selector pattern correctly', () => { - render() - - // Component should have called useMarketplaceContext with selector functions - expect(screen.getByTestId('portal-wrapper')).toBeInTheDocument() - }) - }) - - // ================================ - // Accessibility Tests - // ================================ - describe('Accessibility', () => { - it('should have cursor pointer on trigger', () => { - const { container } = render() - - const trigger = container.querySelector('.cursor-pointer') - expect(trigger).toBeInTheDocument() - }) - - it('should have cursor pointer on options', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const options = content.querySelectorAll('.cursor-pointer') - expect(options.length).toBeGreaterThan(0) - }) - - it('should have visible focus indicators via hover styles', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const option = content.querySelector('.hover\\:bg-components-panel-on-panel-item-bg-hover') - expect(option).toBeInTheDocument() - }) - }) - - // ================================ - // Translation Tests - // ================================ - describe('Translations', () => { - it('should call translation for sortBy label', () => { - render() - - expect(mockTranslation).toHaveBeenCalledWith('marketplace.sortBy', { ns: 'plugin' }) - }) - - it('should call translation for all sort options', () => { - render() - - expect(mockTranslation).toHaveBeenCalledWith('marketplace.sortOption.mostPopular', { ns: 'plugin' }) - expect(mockTranslation).toHaveBeenCalledWith('marketplace.sortOption.recentlyUpdated', { ns: 'plugin' }) - expect(mockTranslation).toHaveBeenCalledWith('marketplace.sortOption.newlyReleased', { ns: 'plugin' }) - expect(mockTranslation).toHaveBeenCalledWith('marketplace.sortOption.firstReleased', { ns: 'plugin' }) - }) - }) - - // ================================ - // Portal Component Integration Tests - // ================================ - describe('Portal Component Integration', () => { - it('should pass open state to PortalToFollowElem', () => { - render() - - const wrapper = screen.getByTestId('portal-wrapper') - expect(wrapper).toHaveAttribute('data-open', 'false') - - fireEvent.click(screen.getByTestId('portal-trigger')) - - expect(wrapper).toHaveAttribute('data-open', 'true') - }) - - it('should render trigger content inside PortalToFollowElemTrigger', () => { - render() - - const trigger = screen.getByTestId('portal-trigger') - expect(within(trigger).getByText('Sort by')).toBeInTheDocument() - expect(within(trigger).getByText('Most Popular')).toBeInTheDocument() - }) - - it('should render options inside PortalToFollowElemContent', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - expect(within(content).getByText('Most Popular')).toBeInTheDocument() - }) - }) - - // ================================ - // Visual Style Tests - // ================================ - describe('Visual Styles', () => { - it('should apply correct trigger container styles', () => { - const { container } = render() - - const triggerDiv = container.querySelector('.flex.h-8.cursor-pointer.items-center.rounded-lg') - expect(triggerDiv).toBeInTheDocument() - }) - - it('should apply secondary text color to sort by label', () => { - const { container } = render() - - const label = container.querySelector('.text-text-secondary') - expect(label).toBeInTheDocument() - expect(label?.textContent).toBe('Sort by') - }) - - it('should apply primary text color to selected option', () => { - const { container } = render() - - const selected = container.querySelector('.text-text-primary.system-sm-medium') - expect(selected).toBeInTheDocument() - }) - - it('should apply tertiary text color to arrow icon', () => { - const { container } = render() - - const arrow = container.querySelector('.text-text-tertiary') - expect(arrow).toBeInTheDocument() - }) - - it('should apply accent text color to check icon when option selected', () => { - mockSort = { sortBy: 'install_count', sortOrder: 'DESC' } - const { container } = render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const checkIcon = container.querySelector('.text-text-accent') - expect(checkIcon).toBeInTheDocument() - }) - - it('should apply blur backdrop to dropdown container', () => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - const container = content.querySelector('.backdrop-blur-sm') - expect(container).toBeInTheDocument() - }) - }) - - // ================================ - // All Sort Options Click Tests - // ================================ - describe('All Sort Options Click Handlers', () => { - const testCases = [ - { text: 'Most Popular', sortBy: 'install_count', sortOrder: 'DESC' }, - { text: 'Recently Updated', sortBy: 'version_updated_at', sortOrder: 'DESC' }, - { text: 'Newly Released', sortBy: 'created_at', sortOrder: 'DESC' }, - { text: 'First Released', sortBy: 'created_at', sortOrder: 'ASC' }, - ] - - it.each(testCases)( - 'should call handleSortChange with { sortBy: "$sortBy", sortOrder: "$sortOrder" } when clicking "$text"', - ({ text, sortBy, sortOrder }) => { - render() - - fireEvent.click(screen.getByTestId('portal-trigger')) - - const content = screen.getByTestId('portal-content') - fireEvent.click(within(content).getByText(text)) - - expect(mockHandleSortChange).toHaveBeenCalledWith({ sortBy, sortOrder }) - }, - ) }) }) diff --git a/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx index d36cf12f11..f267b9eaf1 100644 --- a/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-mutation-model/__tests__/index.spec.tsx @@ -610,7 +610,7 @@ describe('PluginMutationModal', () => { render() expect(screen.getByText('my-organization')).toBeInTheDocument() - expect(screen.getByText('my-plugin-name')).toBeInTheDocument() + expect(screen.queryByText('my-plugin-name')).not.toBeInTheDocument() }) it('should display plugin category', () => { @@ -750,7 +750,7 @@ describe('PluginMutationModal', () => { render() - expect(screen.getByText('plugin-with-special!@#$%')).toBeInTheDocument() + expect(screen.getByText('org')).toBeInTheDocument() }) it('should handle very long title', () => { diff --git a/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx index e02a2bcb57..ca09e78481 100644 --- a/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx @@ -104,6 +104,10 @@ vi.mock('../install-plugin-dropdown', () => ({ ), })) +vi.mock('../../marketplace/search-box/search-box-wrapper', () => ({ + default: () =>
SearchBoxWrapper
, +})) + vi.mock('../../install-plugin/install-from-local-package', () => ({ default: ({ onClose }: { onClose: () => void }) => (
@@ -180,11 +184,7 @@ describe('PluginPage Component', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() - // The marketplace content should be visible when enable_marketplace is true and on discover tab - const container = document.getElementById('marketplace-container') - expect(container).toBeInTheDocument() - // Check that marketplace-specific links are shown - expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() + expect(screen.getByTestId('marketplace-content')).toBeInTheDocument() }) it('should render TabSlider', () => { @@ -225,9 +225,7 @@ describe('PluginPage Component', () => { vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()]) render() - // Check for marketplace-specific buttons - expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() - expect(screen.getByText(/publishPlugins/i)).toBeInTheDocument() + expect(screen.getByText(/requestSubmit/i)).toBeInTheDocument() }) it('should not show marketplace links when on plugins tab', () => { @@ -548,12 +546,11 @@ describe('PluginPage Component', () => { const { rerender } = render() - // Should show marketplace links when on discover tab - expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() + expect(screen.getByTestId('marketplace-content')).toBeInTheDocument() // Rerender with same props rerender() - expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() + expect(screen.getByTestId('marketplace-content')).toBeInTheDocument() }) it('should recognize plugin type tabs as marketplace', () => { @@ -562,9 +559,8 @@ describe('PluginPage Component', () => { render() - // Should show marketplace links when on a plugin type tab - expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() - expect(screen.getByText(/publishPlugins/i)).toBeInTheDocument() + expect(screen.getByTestId('marketplace-content')).toBeInTheDocument() + expect(screen.getByText(/requestSubmit/i)).toBeInTheDocument() }) it('should render marketplace content when isExploringMarketplace and enable_marketplace are true', () => { @@ -572,11 +568,7 @@ describe('PluginPage Component', () => { render() - // The marketplace prop content should be rendered - // Since we mock the marketplace as a div, check it's not hidden - const container = document.getElementById('marketplace-container') - expect(container).toBeInTheDocument() - expect(container).toHaveClass('bg-background-body') + expect(screen.getByTestId('marketplace-content')).toBeInTheDocument() }) }) @@ -616,6 +608,7 @@ describe('PluginPage Component', () => { render() expect(document.getElementById('marketplace-container')).toBeInTheDocument() + expect(screen.queryByTestId('marketplace-content')).not.toBeInTheDocument() }) it('should handle rapid tab switches', async () => { @@ -638,8 +631,8 @@ describe('PluginPage Component', () => { render() - // Component should still render but without marketplace content when disabled expect(document.getElementById('marketplace-container')).toBeInTheDocument() + expect(screen.queryByTestId('marketplace-content')).not.toBeInTheDocument() }) it('should handle file with empty name', async () => { @@ -731,7 +724,7 @@ describe('PluginPage Component', () => { render() const container = document.getElementById('marketplace-container') - expect(container).toHaveClass('bg-background-body') + expect(container).toHaveClass('bg-components-panel-bg') }) it('should have scrollbar-gutter stable style', () => { @@ -1027,11 +1020,10 @@ describe('PluginPage Integration', () => { const { rerender } = render() - // With enable_marketplace: true (default mock), marketplace links should show - expect(screen.getByText(/requestAPlugin/i)).toBeInTheDocument() + expect(screen.getByTestId('marketplace-content')).toBeInTheDocument() // Rerender to verify consistent behavior rerender() - expect(screen.getByText(/publishPlugins/i)).toBeInTheDocument() + expect(screen.getByTestId('marketplace-content')).toBeInTheDocument() }) })