dify/web/app/components/workflow-app/components/workflow-onboarding-modal/start-node-option.spec.tsx

349 lines
10 KiB
TypeScript

import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import StartNodeOption from './start-node-option'
describe('StartNodeOption', () => {
const mockOnClick = jest.fn()
const defaultProps = {
icon: <div data-testid="test-icon">Icon</div>,
title: 'Test Title',
description: 'Test description for the option',
onClick: mockOnClick,
}
beforeEach(() => {
jest.clearAllMocks()
})
// Helper function to render component
const renderComponent = (props = {}) => {
return render(<StartNodeOption {...defaultProps} {...props} />)
}
// Rendering tests (REQUIRED)
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
renderComponent()
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument()
})
it('should render icon correctly', () => {
// Arrange & Act
renderComponent()
// Assert
expect(screen.getByTestId('test-icon')).toBeInTheDocument()
expect(screen.getByText('Icon')).toBeInTheDocument()
})
it('should render title correctly', () => {
// Arrange & Act
renderComponent()
// Assert
const title = screen.getByText('Test Title')
expect(title).toBeInTheDocument()
expect(title).toHaveClass('system-md-semi-bold')
expect(title).toHaveClass('text-text-primary')
})
it('should render description correctly', () => {
// Arrange & Act
renderComponent()
// Assert
const description = screen.getByText('Test description for the option')
expect(description).toBeInTheDocument()
expect(description).toHaveClass('system-xs-regular')
expect(description).toHaveClass('text-text-tertiary')
})
it('should be rendered as a clickable card', () => {
// Arrange & Act
const { container } = renderComponent()
// Assert
const card = container.querySelector('.cursor-pointer')
expect(card).toBeInTheDocument()
// Check that it has cursor-pointer class to indicate clickability
expect(card).toHaveClass('cursor-pointer')
})
})
// Props tests (REQUIRED)
describe('Props', () => {
it('should render with subtitle when provided', () => {
// Arrange & Act
renderComponent({ subtitle: 'Optional Subtitle' })
// Assert
expect(screen.getByText('Optional Subtitle')).toBeInTheDocument()
})
it('should not render subtitle when not provided', () => {
// Arrange & Act
renderComponent()
// Assert
const titleElement = screen.getByText('Test Title').parentElement
expect(titleElement).not.toHaveTextContent('Optional Subtitle')
})
it('should render subtitle with correct styling', () => {
// Arrange & Act
renderComponent({ subtitle: 'Subtitle Text' })
// Assert
const subtitle = screen.getByText('Subtitle Text')
expect(subtitle).toHaveClass('system-md-regular')
expect(subtitle).toHaveClass('text-text-quaternary')
})
it('should render custom icon component', () => {
// Arrange
const customIcon = <svg data-testid="custom-svg">Custom</svg>
// Act
renderComponent({ icon: customIcon })
// Assert
expect(screen.getByTestId('custom-svg')).toBeInTheDocument()
})
it('should render long title correctly', () => {
// Arrange
const longTitle = 'This is a very long title that should still render correctly'
// Act
renderComponent({ title: longTitle })
// Assert
expect(screen.getByText(longTitle)).toBeInTheDocument()
})
it('should render long description correctly', () => {
// Arrange
const longDescription = 'This is a very long description that explains the option in great detail and should still render correctly within the component layout'
// Act
renderComponent({ description: longDescription })
// Assert
expect(screen.getByText(longDescription)).toBeInTheDocument()
})
it('should render with proper layout structure', () => {
// Arrange & Act
renderComponent()
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument()
expect(screen.getByText('Test description for the option')).toBeInTheDocument()
expect(screen.getByTestId('test-icon')).toBeInTheDocument()
})
})
// User Interactions
describe('User Interactions', () => {
it('should call onClick when card is clicked', async () => {
// Arrange
const user = userEvent.setup()
renderComponent()
// Act
const card = screen.getByText('Test Title').closest('div[class*="cursor-pointer"]')
await user.click(card!)
// Assert
expect(mockOnClick).toHaveBeenCalledTimes(1)
})
it('should call onClick when icon is clicked', async () => {
// Arrange
const user = userEvent.setup()
renderComponent()
// Act
const icon = screen.getByTestId('test-icon')
await user.click(icon)
// Assert
expect(mockOnClick).toHaveBeenCalledTimes(1)
})
it('should call onClick when title is clicked', async () => {
// Arrange
const user = userEvent.setup()
renderComponent()
// Act
const title = screen.getByText('Test Title')
await user.click(title)
// Assert
expect(mockOnClick).toHaveBeenCalledTimes(1)
})
it('should call onClick when description is clicked', async () => {
// Arrange
const user = userEvent.setup()
renderComponent()
// Act
const description = screen.getByText('Test description for the option')
await user.click(description)
// Assert
expect(mockOnClick).toHaveBeenCalledTimes(1)
})
it('should handle multiple rapid clicks', async () => {
// Arrange
const user = userEvent.setup()
renderComponent()
// Act
const card = screen.getByText('Test Title').closest('div[class*="cursor-pointer"]')
await user.click(card!)
await user.click(card!)
await user.click(card!)
// Assert
expect(mockOnClick).toHaveBeenCalledTimes(3)
})
it('should not throw error if onClick is undefined', async () => {
// Arrange
const user = userEvent.setup()
renderComponent({ onClick: undefined })
// Act & Assert
const card = screen.getByText('Test Title').closest('div[class*="cursor-pointer"]')
await expect(user.click(card!)).resolves.not.toThrow()
})
})
// Edge Cases (REQUIRED)
describe('Edge Cases', () => {
it('should handle empty string title', () => {
// Arrange & Act
renderComponent({ title: '' })
// Assert
const titleContainer = screen.getByText('Test description for the option').parentElement?.parentElement
expect(titleContainer).toBeInTheDocument()
})
it('should handle empty string description', () => {
// Arrange & Act
renderComponent({ description: '' })
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument()
})
it('should handle undefined subtitle gracefully', () => {
// Arrange & Act
renderComponent({ subtitle: undefined })
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument()
})
it('should handle empty string subtitle', () => {
// Arrange & Act
renderComponent({ subtitle: '' })
// Assert
// Empty subtitle should still render but be empty
expect(screen.getByText('Test Title')).toBeInTheDocument()
})
it('should handle null subtitle', () => {
// Arrange & Act
renderComponent({ subtitle: null })
// Assert
expect(screen.getByText('Test Title')).toBeInTheDocument()
})
it('should render with subtitle containing special characters', () => {
// Arrange
const specialSubtitle = '(optional) - [Beta]'
// Act
renderComponent({ subtitle: specialSubtitle })
// Assert
expect(screen.getByText(specialSubtitle)).toBeInTheDocument()
})
it('should render with title and subtitle together', () => {
// Arrange & Act
const { container } = renderComponent({
title: 'Main Title',
subtitle: 'Secondary Text',
})
// Assert
expect(screen.getByText('Main Title')).toBeInTheDocument()
expect(screen.getByText('Secondary Text')).toBeInTheDocument()
// Both should be in the same heading element
const heading = container.querySelector('h3')
expect(heading).toHaveTextContent('Main Title')
expect(heading).toHaveTextContent('Secondary Text')
})
})
// Accessibility Tests
describe('Accessibility', () => {
it('should have semantic heading structure', () => {
// Arrange & Act
const { container } = renderComponent()
// Assert
const heading = container.querySelector('h3')
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent('Test Title')
})
it('should have semantic paragraph for description', () => {
// Arrange & Act
const { container } = renderComponent()
// Assert
const paragraph = container.querySelector('p')
expect(paragraph).toBeInTheDocument()
expect(paragraph).toHaveTextContent('Test description for the option')
})
it('should have proper cursor style for accessibility', () => {
// Arrange & Act
const { container } = renderComponent()
// Assert
const card = container.querySelector('.cursor-pointer')
expect(card).toBeInTheDocument()
expect(card).toHaveClass('cursor-pointer')
})
})
// Additional Edge Cases
describe('Additional Edge Cases', () => {
it('should handle click when onClick handler is missing', async () => {
// Arrange
const user = userEvent.setup()
renderComponent({ onClick: undefined })
// Act & Assert - Should not throw error
const card = screen.getByText('Test Title').closest('div[class*="cursor-pointer"]')
await expect(user.click(card!)).resolves.not.toThrow()
})
})
})