import { fireEvent, render, screen } from '@testing-library/react' import React from 'react' import Header from './header' import type { DataSourceCredential } from '@/types/pipeline' // Mock CredentialTypeEnum to avoid deep import chain issues enum MockCredentialTypeEnum { OAUTH2 = 'oauth2', API_KEY = 'api_key', } // Mock plugin-auth module to avoid deep import chain issues jest.mock('@/app/components/plugins/plugin-auth', () => ({ CredentialTypeEnum: { OAUTH2: 'oauth2', API_KEY: 'api_key', }, })) // Mock portal-to-follow-elem - required for CredentialSelector jest.mock('@/app/components/base/portal-to-follow-elem', () => { const MockPortalToFollowElem = ({ children, open }: any) => { return (
{React.Children.map(children, (child: any) => { if (!child) return null return React.cloneElement(child, { __portalOpen: open }) })}
) } const MockPortalToFollowElemTrigger = ({ children, onClick, className, __portalOpen }: any) => (
{children}
) const MockPortalToFollowElemContent = ({ children, className, __portalOpen }: any) => { if (!__portalOpen) return null return (
{children}
) } return { PortalToFollowElem: MockPortalToFollowElem, PortalToFollowElemTrigger: MockPortalToFollowElemTrigger, PortalToFollowElemContent: MockPortalToFollowElemContent, } }) // ========================================== // Test Data Builders // ========================================== const createMockCredential = (overrides?: Partial): DataSourceCredential => ({ id: 'cred-1', name: 'Test Credential', avatar_url: 'https://example.com/avatar.png', credential: { key: 'value' }, is_default: false, type: MockCredentialTypeEnum.OAUTH2 as unknown as DataSourceCredential['type'], ...overrides, }) const createMockCredentials = (count: number = 3): DataSourceCredential[] => Array.from({ length: count }, (_, i) => createMockCredential({ id: `cred-${i + 1}`, name: `Credential ${i + 1}`, avatar_url: `https://example.com/avatar-${i + 1}.png`, is_default: i === 0, }), ) type HeaderProps = React.ComponentProps const createDefaultProps = (overrides?: Partial): HeaderProps => ({ docTitle: 'Documentation', docLink: 'https://docs.example.com', pluginName: 'Test Plugin', currentCredentialId: 'cred-1', onCredentialChange: jest.fn(), credentials: createMockCredentials(), ...overrides, }) describe('Header', () => { beforeEach(() => { jest.clearAllMocks() }) // ========================================== // Rendering Tests // ========================================== describe('Rendering', () => { it('should render without crashing', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert expect(screen.getByText('Documentation')).toBeInTheDocument() }) it('should render documentation link with correct attributes', () => { // Arrange const props = createDefaultProps({ docTitle: 'API Docs', docLink: 'https://api.example.com/docs', }) // Act render(
) // Assert const link = screen.getByRole('link', { name: /API Docs/i }) expect(link).toHaveAttribute('href', 'https://api.example.com/docs') expect(link).toHaveAttribute('target', '_blank') expect(link).toHaveAttribute('rel', 'noopener noreferrer') }) it('should render document title with title attribute', () => { // Arrange const props = createDefaultProps({ docTitle: 'My Documentation' }) // Act render(
) // Assert const titleSpan = screen.getByText('My Documentation') expect(titleSpan).toHaveAttribute('title', 'My Documentation') }) it('should render CredentialSelector with correct props', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert - CredentialSelector should render current credential name expect(screen.getByText('Credential 1')).toBeInTheDocument() }) it('should render configuration button', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert expect(screen.getByRole('button')).toBeInTheDocument() }) it('should render book icon in documentation link', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert - RiBookOpenLine renders as SVG const link = screen.getByRole('link') const svg = link.querySelector('svg') expect(svg).toBeInTheDocument() }) it('should render divider between credential selector and configuration button', () => { // Arrange const props = createDefaultProps() // Act const { container } = render(
) // Assert - Divider component should be rendered // Divider typically renders as a div with specific styling const divider = container.querySelector('[class*="divider"]') || container.querySelector('.mx-1.h-3\\.5') expect(divider).toBeInTheDocument() }) }) // ========================================== // Props Testing // ========================================== describe('Props', () => { describe('docTitle prop', () => { it('should display the document title', () => { // Arrange const props = createDefaultProps({ docTitle: 'Getting Started Guide' }) // Act render(
) // Assert expect(screen.getByText('Getting Started Guide')).toBeInTheDocument() }) it.each([ 'Quick Start', 'API Reference', 'Configuration Guide', 'Plugin Documentation', ])('should display "%s" as document title', (title) => { // Arrange const props = createDefaultProps({ docTitle: title }) // Act render(
) // Assert expect(screen.getByText(title)).toBeInTheDocument() }) }) describe('docLink prop', () => { it('should set correct href on documentation link', () => { // Arrange const props = createDefaultProps({ docLink: 'https://custom.docs.com/guide' }) // Act render(
) // Assert const link = screen.getByRole('link') expect(link).toHaveAttribute('href', 'https://custom.docs.com/guide') }) it.each([ 'https://docs.dify.ai', 'https://example.com/api', '/local/docs', ])('should accept "%s" as docLink', (link) => { // Arrange const props = createDefaultProps({ docLink: link }) // Act render(
) // Assert expect(screen.getByRole('link')).toHaveAttribute('href', link) }) }) describe('pluginName prop', () => { it('should pass pluginName to translation function', () => { // Arrange const props = createDefaultProps({ pluginName: 'MyPlugin' }) // Act render(
) // Assert - The translation mock returns the key with options // Tooltip uses the translated content expect(screen.getByRole('button')).toBeInTheDocument() }) }) describe('onClickConfiguration prop', () => { it('should call onClickConfiguration when configuration icon is clicked', () => { // Arrange const mockOnClick = jest.fn() const props = createDefaultProps({ onClickConfiguration: mockOnClick }) render(
) // Act - Find the configuration button and click the icon inside // The button contains the RiEqualizer2Line icon with onClick handler const configButton = screen.getByRole('button') const configIcon = configButton.querySelector('svg') expect(configIcon).toBeInTheDocument() fireEvent.click(configIcon!) // Assert expect(mockOnClick).toHaveBeenCalledTimes(1) }) it('should not crash when onClickConfiguration is undefined', () => { // Arrange const props = createDefaultProps({ onClickConfiguration: undefined }) render(
) // Act - Find the configuration button and click the icon inside const configButton = screen.getByRole('button') const configIcon = configButton.querySelector('svg') expect(configIcon).toBeInTheDocument() fireEvent.click(configIcon!) // Assert - Component should still be rendered (no crash) expect(screen.getByRole('button')).toBeInTheDocument() }) }) describe('CredentialSelector props passthrough', () => { it('should pass currentCredentialId to CredentialSelector', () => { // Arrange const props = createDefaultProps({ currentCredentialId: 'cred-2' }) // Act render(
) // Assert - Should display the second credential expect(screen.getByText('Credential 2')).toBeInTheDocument() }) it('should pass credentials to CredentialSelector', () => { // Arrange const customCredentials = [ createMockCredential({ id: 'custom-1', name: 'Custom Credential' }), ] const props = createDefaultProps({ credentials: customCredentials, currentCredentialId: 'custom-1', }) // Act render(
) // Assert expect(screen.getByText('Custom Credential')).toBeInTheDocument() }) it('should pass onCredentialChange to CredentialSelector', () => { // Arrange const mockOnChange = jest.fn() const props = createDefaultProps({ onCredentialChange: mockOnChange }) render(
) // Act - Open dropdown and select a credential // Use getAllByTestId and select the first one (CredentialSelector's trigger) const triggers = screen.getAllByTestId('portal-trigger') fireEvent.click(triggers[0]) const credential2 = screen.getByText('Credential 2') fireEvent.click(credential2) // Assert expect(mockOnChange).toHaveBeenCalledWith('cred-2') }) }) }) // ========================================== // User Interactions // ========================================== describe('User Interactions', () => { it('should open external link in new tab when clicking documentation link', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert - Link has target="_blank" for new tab const link = screen.getByRole('link') expect(link).toHaveAttribute('target', '_blank') }) it('should allow credential selection through CredentialSelector', () => { // Arrange const mockOnChange = jest.fn() const props = createDefaultProps({ onCredentialChange: mockOnChange }) render(
) // Act - Open dropdown (use first trigger which is CredentialSelector's) const triggers = screen.getAllByTestId('portal-trigger') fireEvent.click(triggers[0]) // Assert - Dropdown should be open expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) it('should trigger configuration callback when clicking config icon', () => { // Arrange const mockOnConfig = jest.fn() const props = createDefaultProps({ onClickConfiguration: mockOnConfig }) const { container } = render(
) // Act const configIcon = container.querySelector('.h-4.w-4') fireEvent.click(configIcon!) // Assert expect(mockOnConfig).toHaveBeenCalled() }) }) // ========================================== // Component Memoization // ========================================== describe('Component Memoization', () => { it('should be wrapped with React.memo', () => { // Assert expect(Header.$$typeof).toBe(Symbol.for('react.memo')) }) it('should not re-render when props remain the same', () => { // Arrange const props = createDefaultProps() const renderSpy = jest.fn() const TrackedHeader: React.FC = (trackedProps) => { renderSpy() return
} const MemoizedTracked = React.memo(TrackedHeader) // Act const { rerender } = render() rerender() // Assert - Should only render once due to same props expect(renderSpy).toHaveBeenCalledTimes(1) }) it('should re-render when docTitle changes', () => { // Arrange const props = createDefaultProps({ docTitle: 'Original Title' }) const { rerender } = render(
) // Assert initial expect(screen.getByText('Original Title')).toBeInTheDocument() // Act rerender(
) // Assert expect(screen.getByText('Updated Title')).toBeInTheDocument() }) it('should re-render when currentCredentialId changes', () => { // Arrange const props = createDefaultProps({ currentCredentialId: 'cred-1' }) const { rerender } = render(
) // Assert initial expect(screen.getByText('Credential 1')).toBeInTheDocument() // Act rerender(
) // Assert expect(screen.getByText('Credential 2')).toBeInTheDocument() }) }) // ========================================== // Edge Cases // ========================================== describe('Edge Cases', () => { it('should handle empty docTitle', () => { // Arrange const props = createDefaultProps({ docTitle: '' }) // Act render(
) // Assert - Should render without crashing const link = screen.getByRole('link') expect(link).toBeInTheDocument() }) it('should handle very long docTitle', () => { // Arrange const longTitle = 'A'.repeat(200) const props = createDefaultProps({ docTitle: longTitle }) // Act render(
) // Assert expect(screen.getByText(longTitle)).toBeInTheDocument() }) it('should handle special characters in docTitle', () => { // Arrange const specialTitle = 'Docs & Guide "Special"' const props = createDefaultProps({ docTitle: specialTitle }) // Act render(
) // Assert expect(screen.getByText(specialTitle)).toBeInTheDocument() }) it('should handle empty credentials array', () => { // Arrange const props = createDefaultProps({ credentials: [], currentCredentialId: '', }) // Act render(
) // Assert - Should render without crashing expect(screen.getByRole('link')).toBeInTheDocument() }) it('should handle special characters in pluginName', () => { // Arrange const props = createDefaultProps({ pluginName: 'Plugin & Tool ' }) // Act render(
) // Assert - Should render without crashing expect(screen.getByRole('button')).toBeInTheDocument() }) it('should handle unicode characters in docTitle', () => { // Arrange const props = createDefaultProps({ docTitle: '文档说明 📚' }) // Act render(
) // Assert expect(screen.getByText('文档说明 📚')).toBeInTheDocument() }) }) // ========================================== // Styling // ========================================== describe('Styling', () => { it('should apply correct classes to container', () => { // Arrange const props = createDefaultProps() // Act const { container } = render(
) // Assert const rootDiv = container.firstChild as HTMLElement expect(rootDiv).toHaveClass('flex', 'items-center', 'justify-between', 'gap-x-2') }) it('should apply correct classes to documentation link', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert const link = screen.getByRole('link') expect(link).toHaveClass('system-xs-medium', 'text-text-accent') }) it('should apply shrink-0 to documentation link', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert const link = screen.getByRole('link') expect(link).toHaveClass('shrink-0') }) }) // ========================================== // Integration Tests // ========================================== describe('Integration', () => { it('should work with full credential workflow', () => { // Arrange const mockOnCredentialChange = jest.fn() const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange, currentCredentialId: 'cred-1', }) render(
) // Assert initial state expect(screen.getByText('Credential 1')).toBeInTheDocument() // Act - Open dropdown and select different credential // Use first trigger which is CredentialSelector's const triggers = screen.getAllByTestId('portal-trigger') fireEvent.click(triggers[0]) const credential3 = screen.getByText('Credential 3') fireEvent.click(credential3) // Assert expect(mockOnCredentialChange).toHaveBeenCalledWith('cred-3') }) it('should display all components together correctly', () => { // Arrange const mockOnConfig = jest.fn() const props = createDefaultProps({ docTitle: 'Integration Test Docs', docLink: 'https://test.com/docs', pluginName: 'TestPlugin', onClickConfiguration: mockOnConfig, }) // Act render(
) // Assert - All main elements present expect(screen.getByText('Credential 1')).toBeInTheDocument() // CredentialSelector expect(screen.getByRole('button')).toBeInTheDocument() // Config button expect(screen.getByText('Integration Test Docs')).toBeInTheDocument() // Doc link expect(screen.getByRole('link')).toHaveAttribute('href', 'https://test.com/docs') }) }) // ========================================== // Accessibility // ========================================== describe('Accessibility', () => { it('should have accessible link', () => { // Arrange const props = createDefaultProps({ docTitle: 'Accessible Docs' }) // Act render(
) // Assert const link = screen.getByRole('link', { name: /Accessible Docs/i }) expect(link).toBeInTheDocument() }) it('should have accessible button for configuration', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert const button = screen.getByRole('button') expect(button).toBeInTheDocument() }) it('should have noopener noreferrer for security on external links', () => { // Arrange const props = createDefaultProps() // Act render(
) // Assert const link = screen.getByRole('link') expect(link).toHaveAttribute('rel', 'noopener noreferrer') }) }) })