mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 22:28:46 +00:00
test: add comprehensive Jest tests for CustomPage and WorkflowOnboardingModal components (#29714)
This commit is contained in:
500
web/app/components/custom/custom-page/index.spec.tsx
Normal file
500
web/app/components/custom/custom-page/index.spec.tsx
Normal file
@@ -0,0 +1,500 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import CustomPage from './index'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||
import { contactSalesUrl } from '@/app/components/billing/config'
|
||||
|
||||
// Mock external dependencies only
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/context/modal-context', () => ({
|
||||
useModalContext: jest.fn(),
|
||||
}))
|
||||
|
||||
// Mock the complex CustomWebAppBrand component to avoid dependency issues
|
||||
// This is acceptable because it has complex dependencies (fetch, APIs)
|
||||
jest.mock('../custom-web-app-brand', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="custom-web-app-brand">CustomWebAppBrand</div>,
|
||||
}))
|
||||
|
||||
// Get the mocked functions
|
||||
const { useProviderContext } = jest.requireMock('@/context/provider-context')
|
||||
const { useModalContext } = jest.requireMock('@/context/modal-context')
|
||||
|
||||
describe('CustomPage', () => {
|
||||
const mockSetShowPricingModal = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
// Default mock setup
|
||||
useModalContext.mockReturnValue({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
})
|
||||
})
|
||||
|
||||
// Helper function to render with different provider contexts
|
||||
const renderWithContext = (overrides = {}) => {
|
||||
useProviderContext.mockReturnValue(
|
||||
createMockProviderContextValue(overrides),
|
||||
)
|
||||
return render(<CustomPage />)
|
||||
}
|
||||
|
||||
// Rendering tests (REQUIRED)
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should always render CustomWebAppBrand component', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct layout structure', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderWithContext()
|
||||
|
||||
// Assert
|
||||
const mainContainer = container.querySelector('.flex.flex-col')
|
||||
expect(mainContainer).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Conditional Rendering - Billing Tip
|
||||
describe('Billing Tip Banner', () => {
|
||||
it('should show billing tip when enableBilling is true and plan is sandbox', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('custom.upgradeTip.des')).toBeInTheDocument()
|
||||
expect(screen.getByText('billing.upgradeBtn.encourageShort')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show billing tip when enableBilling is false', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: false,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.upgradeTip.des')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show billing tip when plan is professional', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.upgradeTip.des')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show billing tip when plan is team', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.team },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.upgradeTip.des')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct gradient styling for billing tip banner', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
const banner = container.querySelector('.bg-gradient-to-r')
|
||||
expect(banner).toBeInTheDocument()
|
||||
expect(banner).toHaveClass('from-components-input-border-active-prompt-1')
|
||||
expect(banner).toHaveClass('to-components-input-border-active-prompt-2')
|
||||
expect(banner).toHaveClass('p-4')
|
||||
expect(banner).toHaveClass('pl-6')
|
||||
expect(banner).toHaveClass('shadow-lg')
|
||||
})
|
||||
})
|
||||
|
||||
// Conditional Rendering - Contact Sales
|
||||
describe('Contact Sales Section', () => {
|
||||
it('should show contact section when enableBilling is true and plan is professional', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert - Check that contact section exists with all parts
|
||||
const contactSection = container.querySelector('.absolute.bottom-0')
|
||||
expect(contactSection).toBeInTheDocument()
|
||||
expect(contactSection).toHaveTextContent('custom.customize.prefix')
|
||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
||||
expect(contactSection).toHaveTextContent('custom.customize.suffix')
|
||||
})
|
||||
|
||||
it('should show contact section when enableBilling is true and plan is team', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.team },
|
||||
})
|
||||
|
||||
// Assert - Check that contact section exists with all parts
|
||||
const contactSection = container.querySelector('.absolute.bottom-0')
|
||||
expect(contactSection).toBeInTheDocument()
|
||||
expect(contactSection).toHaveTextContent('custom.customize.prefix')
|
||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
||||
expect(contactSection).toHaveTextContent('custom.customize.suffix')
|
||||
})
|
||||
|
||||
it('should not show contact section when enableBilling is false', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: false,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.customize.prefix')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show contact section when plan is sandbox', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.customize.prefix')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render contact link with correct URL', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('custom.customize.contactUs').closest('a')
|
||||
expect(link).toHaveAttribute('href', contactSalesUrl)
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should have correct positioning for contact section', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert
|
||||
const contactSection = container.querySelector('.absolute.bottom-0')
|
||||
expect(contactSection).toBeInTheDocument()
|
||||
expect(contactSection).toHaveClass('h-[50px]')
|
||||
expect(contactSection).toHaveClass('text-xs')
|
||||
expect(contactSection).toHaveClass('leading-[50px]')
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions
|
||||
describe('User Interactions', () => {
|
||||
it('should call setShowPricingModal when upgrade button is clicked', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Act
|
||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
||||
await user.click(upgradeButton)
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call setShowPricingModal without arguments', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Act
|
||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
||||
await user.click(upgradeButton)
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalledWith()
|
||||
})
|
||||
|
||||
it('should handle multiple clicks on upgrade button', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Act
|
||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
||||
await user.click(upgradeButton)
|
||||
await user.click(upgradeButton)
|
||||
await user.click(upgradeButton)
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should have correct button styling for upgrade button', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
||||
expect(upgradeButton).toHaveClass('cursor-pointer')
|
||||
expect(upgradeButton).toHaveClass('bg-white')
|
||||
expect(upgradeButton).toHaveClass('text-text-accent')
|
||||
expect(upgradeButton).toHaveClass('rounded-3xl')
|
||||
})
|
||||
})
|
||||
|
||||
// Edge Cases (REQUIRED)
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined plan type gracefully', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: undefined },
|
||||
})
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle plan without type property', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: null },
|
||||
})
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show any banners when both conditions are false', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: false,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.customize.prefix')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle enableBilling undefined', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderWithContext({
|
||||
enableBilling: undefined,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show only billing tip for sandbox plan, not contact section', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show only contact section for professional plan, not billing tip', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show only contact section for team plan, not billing tip', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.team },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty plan object', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: {},
|
||||
})
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Accessibility Tests
|
||||
describe('Accessibility', () => {
|
||||
it('should have clickable upgrade button', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
const upgradeButton = screen.getByText('billing.upgradeBtn.encourageShort')
|
||||
expect(upgradeButton).toBeInTheDocument()
|
||||
expect(upgradeButton).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('should have proper external link attributes on contact link', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('custom.customize.contactUs').closest('a')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
})
|
||||
|
||||
it('should have proper text hierarchy in billing tip', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
const title = screen.getByText('custom.upgradeTip.title')
|
||||
const description = screen.getByText('custom.upgradeTip.des')
|
||||
|
||||
expect(title).toHaveClass('title-xl-semi-bold')
|
||||
expect(description).toHaveClass('system-sm-regular')
|
||||
})
|
||||
|
||||
it('should use semantic color classes', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert - Check that the billing tip has text content (which implies semantic colors)
|
||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Integration Tests
|
||||
describe('Integration', () => {
|
||||
it('should render both CustomWebAppBrand and billing tip together', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render both CustomWebAppBrand and contact section together', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: true,
|
||||
plan: { type: Plan.professional },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
expect(screen.getByText('custom.customize.contactUs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render only CustomWebAppBrand when no billing conditions met', () => {
|
||||
// Arrange & Act
|
||||
renderWithContext({
|
||||
enableBilling: false,
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('custom-web-app-brand')).toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.upgradeTip.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,13 +5,6 @@ import type { ParentMode, SimpleDocumentDetail } from '@/models/datasets'
|
||||
import { ChunkingMode, DataSourceType } from '@/models/datasets'
|
||||
import DocumentPicker from './index'
|
||||
|
||||
// Mock react-i18next
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock portal-to-follow-elem - always render content for testing
|
||||
jest.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open }: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import type { DocumentItem } from '@/models/datasets'
|
||||
import PreviewDocumentPicker from './preview-document-picker'
|
||||
|
||||
// Mock react-i18next
|
||||
// Override shared i18n mock for custom translations
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, params?: Record<string, unknown>) => {
|
||||
|
||||
@@ -9,13 +9,6 @@ import {
|
||||
} from '@/models/datasets'
|
||||
import RetrievalMethodConfig from './index'
|
||||
|
||||
// Mock react-i18next
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock provider context with controllable supportRetrievalMethods
|
||||
let mockSupportRetrievalMethods: RETRIEVE_METHOD[] = [
|
||||
RETRIEVE_METHOD.semantic,
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import WorkflowOnboardingModal from './index'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
// Mock Modal component
|
||||
jest.mock('@/app/components/base/modal', () => {
|
||||
return function MockModal({
|
||||
isShow,
|
||||
onClose,
|
||||
children,
|
||||
closable,
|
||||
}: any) {
|
||||
if (!isShow)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div data-testid="modal" role="dialog">
|
||||
{closable && (
|
||||
<button data-testid="modal-close-button" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Mock useDocLink hook
|
||||
jest.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
||||
}))
|
||||
|
||||
// Mock StartNodeSelectionPanel (using real component would be better for integration,
|
||||
// but for this test we'll mock to control behavior)
|
||||
jest.mock('./start-node-selection-panel', () => {
|
||||
return function MockStartNodeSelectionPanel({
|
||||
onSelectUserInput,
|
||||
onSelectTrigger,
|
||||
}: any) {
|
||||
return (
|
||||
<div data-testid="start-node-selection-panel">
|
||||
<button data-testid="select-user-input" onClick={onSelectUserInput}>
|
||||
Select User Input
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-trigger-schedule"
|
||||
onClick={() => onSelectTrigger(BlockEnum.TriggerSchedule)}
|
||||
>
|
||||
Select Trigger Schedule
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-trigger-webhook"
|
||||
onClick={() => onSelectTrigger(BlockEnum.TriggerWebhook, { config: 'test' })}
|
||||
>
|
||||
Select Trigger Webhook
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('WorkflowOnboardingModal', () => {
|
||||
const mockOnClose = jest.fn()
|
||||
const mockOnSelectStartNode = jest.fn()
|
||||
|
||||
const defaultProps = {
|
||||
isShow: true,
|
||||
onClose: mockOnClose,
|
||||
onSelectStartNode: mockOnSelectStartNode,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// Helper function to render component
|
||||
const renderComponent = (props = {}) => {
|
||||
return render(<WorkflowOnboardingModal {...defaultProps} {...props} />)
|
||||
}
|
||||
|
||||
// Rendering tests (REQUIRED)
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render modal when isShow is true', () => {
|
||||
// Arrange & Act
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render modal when isShow is false', () => {
|
||||
// Arrange & Act
|
||||
renderComponent({ isShow: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render modal title', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render modal description', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderComponent()
|
||||
|
||||
// Assert - Check both parts of description (separated by link)
|
||||
const descriptionDiv = container.querySelector('.body-xs-regular.leading-4')
|
||||
expect(descriptionDiv).toBeInTheDocument()
|
||||
expect(descriptionDiv).toHaveTextContent('workflow.onboarding.description')
|
||||
expect(descriptionDiv).toHaveTextContent('workflow.onboarding.aboutStartNode')
|
||||
})
|
||||
|
||||
it('should render learn more link', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const learnMoreLink = screen.getByText('workflow.onboarding.learnMore')
|
||||
expect(learnMoreLink).toBeInTheDocument()
|
||||
expect(learnMoreLink.closest('a')).toHaveAttribute('href', 'https://docs.example.com/guides/workflow/node/start')
|
||||
expect(learnMoreLink.closest('a')).toHaveAttribute('target', '_blank')
|
||||
expect(learnMoreLink.closest('a')).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should render StartNodeSelectionPanel', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ESC tip when modal is shown', () => {
|
||||
// Arrange & Act
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render ESC tip when modal is hidden', () => {
|
||||
// Arrange & Act
|
||||
renderComponent({ isShow: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('workflow.onboarding.escTip.press')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct styling for title', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const title = screen.getByText('workflow.onboarding.title')
|
||||
expect(title).toHaveClass('title-2xl-semi-bold')
|
||||
expect(title).toHaveClass('text-text-primary')
|
||||
})
|
||||
|
||||
it('should have modal close button', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal-close-button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props tests (REQUIRED)
|
||||
describe('Props', () => {
|
||||
it('should accept isShow prop', () => {
|
||||
// Arrange & Act
|
||||
const { rerender } = renderComponent({ isShow: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
|
||||
// Act
|
||||
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should accept onClose prop', () => {
|
||||
// Arrange
|
||||
const customOnClose = jest.fn()
|
||||
|
||||
// Act
|
||||
renderComponent({ onClose: customOnClose })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should accept onSelectStartNode prop', () => {
|
||||
// Arrange
|
||||
const customHandler = jest.fn()
|
||||
|
||||
// Act
|
||||
renderComponent({ onSelectStartNode: customHandler })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined onClose gracefully', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderComponent({ onClose: undefined })
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined onSelectStartNode gracefully', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderComponent({ onSelectStartNode: undefined })
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions - Start Node Selection
|
||||
describe('User Interactions - Start Node Selection', () => {
|
||||
it('should call onSelectStartNode with Start block when user input is selected', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const userInputButton = screen.getByTestId('select-user-input')
|
||||
await user.click(userInputButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start)
|
||||
})
|
||||
|
||||
it('should call onClose after selecting user input', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const userInputButton = screen.getByTestId('select-user-input')
|
||||
await user.click(userInputButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSelectStartNode with trigger type when trigger is selected', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const triggerButton = screen.getByTestId('select-trigger-schedule')
|
||||
await user.click(triggerButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined)
|
||||
})
|
||||
|
||||
it('should call onClose after selecting trigger', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const triggerButton = screen.getByTestId('select-trigger-schedule')
|
||||
await user.click(triggerButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should pass tool config when selecting trigger with config', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const webhookButton = screen.getByTestId('select-trigger-webhook')
|
||||
await user.click(webhookButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, { config: 'test' })
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions - Modal Close
|
||||
describe('User Interactions - Modal Close', () => {
|
||||
it('should call onClose when close button is clicked', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const closeButton = screen.getByTestId('modal-close-button')
|
||||
await user.click(closeButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onSelectStartNode when closing without selection', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const closeButton = screen.getByTestId('modal-close-button')
|
||||
await user.click(closeButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectStartNode).not.toHaveBeenCalled()
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// Keyboard Event Handling
|
||||
describe('Keyboard Event Handling', () => {
|
||||
it('should call onClose when ESC key is pressed', () => {
|
||||
// Arrange
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Act
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onClose when other keys are pressed', () => {
|
||||
// Arrange
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Act
|
||||
fireEvent.keyDown(document, { key: 'Enter', code: 'Enter' })
|
||||
fireEvent.keyDown(document, { key: 'Tab', code: 'Tab' })
|
||||
fireEvent.keyDown(document, { key: 'a', code: 'KeyA' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call onClose when ESC is pressed but modal is hidden', () => {
|
||||
// Arrange
|
||||
renderComponent({ isShow: false })
|
||||
|
||||
// Act
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clean up event listener on unmount', () => {
|
||||
// Arrange
|
||||
const { unmount } = renderComponent({ isShow: true })
|
||||
|
||||
// Act
|
||||
unmount()
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update event listener when isShow changes', () => {
|
||||
// Arrange
|
||||
const { rerender } = renderComponent({ isShow: true })
|
||||
|
||||
// Act - Press ESC when shown
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Act - Hide modal and clear mock
|
||||
mockOnClose.mockClear()
|
||||
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={false} />)
|
||||
|
||||
// Act - Press ESC when hidden
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle multiple ESC key presses', () => {
|
||||
// Arrange
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Act
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
// Edge Cases (REQUIRED)
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle rapid modal show/hide toggling', async () => {
|
||||
// Arrange
|
||||
const { rerender } = renderComponent({ isShow: false })
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
|
||||
// Act
|
||||
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
|
||||
// Act
|
||||
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={false} />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle selecting multiple nodes in sequence', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
const { rerender } = renderComponent()
|
||||
|
||||
// Act - Select user input
|
||||
await user.click(screen.getByTestId('select-user-input'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start)
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Act - Re-show modal and select trigger
|
||||
mockOnClose.mockClear()
|
||||
mockOnSelectStartNode.mockClear()
|
||||
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} />)
|
||||
|
||||
await user.click(screen.getByTestId('select-trigger-schedule'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined)
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle prop updates correctly', () => {
|
||||
// Arrange
|
||||
const { rerender } = renderComponent({ isShow: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
|
||||
// Act - Update props
|
||||
const newOnClose = jest.fn()
|
||||
const newOnSelectStartNode = jest.fn()
|
||||
rerender(
|
||||
<WorkflowOnboardingModal
|
||||
isShow={true}
|
||||
onClose={newOnClose}
|
||||
onSelectStartNode={newOnSelectStartNode}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - Modal still renders with new props
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle onClose being called multiple times', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByTestId('modal-close-button'))
|
||||
await user.click(screen.getByTestId('modal-close-button'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should maintain modal state when props change', () => {
|
||||
// Arrange
|
||||
const { rerender } = renderComponent({ isShow: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
|
||||
// Act - Change onClose handler
|
||||
const newOnClose = jest.fn()
|
||||
rerender(<WorkflowOnboardingModal {...defaultProps} isShow={true} onClose={newOnClose} />)
|
||||
|
||||
// Assert - Modal should still be visible
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Accessibility Tests
|
||||
describe('Accessibility', () => {
|
||||
it('should have dialog role', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have proper heading hierarchy', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderComponent()
|
||||
|
||||
// Assert
|
||||
const heading = container.querySelector('h3')
|
||||
expect(heading).toBeInTheDocument()
|
||||
expect(heading).toHaveTextContent('workflow.onboarding.title')
|
||||
})
|
||||
|
||||
it('should have external link with proper attributes', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('workflow.onboarding.learnMore').closest('a')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should have keyboard navigation support via ESC key', () => {
|
||||
// Arrange
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Act
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should have visible ESC key hint', () => {
|
||||
// Arrange & Act
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Assert
|
||||
const escKey = screen.getByText('workflow.onboarding.escTip.key')
|
||||
expect(escKey.closest('kbd')).toBeInTheDocument()
|
||||
expect(escKey.closest('kbd')).toHaveClass('system-kbd')
|
||||
})
|
||||
|
||||
it('should have descriptive text for ESC functionality', () => {
|
||||
// Arrange & Act
|
||||
renderComponent({ isShow: true })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.escTip.press')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.escTip.toDismiss')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have proper text color classes', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const title = screen.getByText('workflow.onboarding.title')
|
||||
expect(title).toHaveClass('text-text-primary')
|
||||
})
|
||||
|
||||
it('should have underlined learn more link', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
const link = screen.getByText('workflow.onboarding.learnMore').closest('a')
|
||||
expect(link).toHaveClass('underline')
|
||||
expect(link).toHaveClass('cursor-pointer')
|
||||
})
|
||||
})
|
||||
|
||||
// Integration Tests
|
||||
describe('Integration', () => {
|
||||
it('should complete full flow of selecting user input node', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Assert - Initial state
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.title')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument()
|
||||
|
||||
// Act - Select user input
|
||||
await user.click(screen.getByTestId('select-user-input'))
|
||||
|
||||
// Assert - Callbacks called
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.Start)
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should complete full flow of selecting trigger node', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Assert - Initial state
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
|
||||
// Act - Select trigger
|
||||
await user.click(screen.getByTestId('select-trigger-webhook'))
|
||||
|
||||
// Assert - Callbacks called with config
|
||||
expect(mockOnSelectStartNode).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, { config: 'test' })
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render all components in correct hierarchy', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderComponent()
|
||||
|
||||
// Assert - Modal is the root
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
|
||||
// Assert - Header elements
|
||||
const heading = container.querySelector('h3')
|
||||
expect(heading).toBeInTheDocument()
|
||||
|
||||
// Assert - Description with link
|
||||
expect(screen.getByText('workflow.onboarding.learnMore').closest('a')).toBeInTheDocument()
|
||||
|
||||
// Assert - Selection panel
|
||||
expect(screen.getByTestId('start-node-selection-panel')).toBeInTheDocument()
|
||||
|
||||
// Assert - ESC tip
|
||||
expect(screen.getByText('workflow.onboarding.escTip.key')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should coordinate between keyboard and click interactions', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Click close button
|
||||
await user.click(screen.getByTestId('modal-close-button'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Act - Clear and try ESC key
|
||||
mockOnClose.mockClear()
|
||||
fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' })
|
||||
|
||||
// Assert
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,348 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,586 @@
|
||||
import React from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import StartNodeSelectionPanel from './start-node-selection-panel'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
// Mock NodeSelector component
|
||||
jest.mock('@/app/components/workflow/block-selector', () => {
|
||||
return function MockNodeSelector({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelect,
|
||||
trigger,
|
||||
}: any) {
|
||||
// trigger is a function that returns a React element
|
||||
const triggerElement = typeof trigger === 'function' ? trigger() : trigger
|
||||
|
||||
return (
|
||||
<div data-testid="node-selector">
|
||||
{triggerElement}
|
||||
{open && (
|
||||
<div data-testid="node-selector-content">
|
||||
<button
|
||||
data-testid="select-schedule"
|
||||
onClick={() => onSelect(BlockEnum.TriggerSchedule)}
|
||||
>
|
||||
Select Schedule
|
||||
</button>
|
||||
<button
|
||||
data-testid="select-webhook"
|
||||
onClick={() => onSelect(BlockEnum.TriggerWebhook)}
|
||||
>
|
||||
Select Webhook
|
||||
</button>
|
||||
<button
|
||||
data-testid="close-selector"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Mock icons
|
||||
jest.mock('@/app/components/base/icons/src/vender/workflow', () => ({
|
||||
Home: () => <div data-testid="home-icon">Home</div>,
|
||||
TriggerAll: () => <div data-testid="trigger-all-icon">TriggerAll</div>,
|
||||
}))
|
||||
|
||||
describe('StartNodeSelectionPanel', () => {
|
||||
const mockOnSelectUserInput = jest.fn()
|
||||
const mockOnSelectTrigger = jest.fn()
|
||||
|
||||
const defaultProps = {
|
||||
onSelectUserInput: mockOnSelectUserInput,
|
||||
onSelectTrigger: mockOnSelectTrigger,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// Helper function to render component
|
||||
const renderComponent = (props = {}) => {
|
||||
return render(<StartNodeSelectionPanel {...defaultProps} {...props} />)
|
||||
}
|
||||
|
||||
// Rendering tests (REQUIRED)
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.userInputFull')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render user input option', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.userInputFull')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.userInputDescription')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('home-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render trigger option', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.trigger')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.triggerDescription')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('trigger-all-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render node selector component', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('node-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have correct grid layout', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderComponent()
|
||||
|
||||
// Assert
|
||||
const grid = container.querySelector('.grid')
|
||||
expect(grid).toBeInTheDocument()
|
||||
expect(grid).toHaveClass('grid-cols-2')
|
||||
expect(grid).toHaveClass('gap-4')
|
||||
})
|
||||
|
||||
it('should not show trigger selector initially', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('node-selector-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Props tests (REQUIRED)
|
||||
describe('Props', () => {
|
||||
it('should accept onSelectUserInput prop', () => {
|
||||
// Arrange
|
||||
const customHandler = jest.fn()
|
||||
|
||||
// Act
|
||||
renderComponent({ onSelectUserInput: customHandler })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.userInputFull')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should accept onSelectTrigger prop', () => {
|
||||
// Arrange
|
||||
const customHandler = jest.fn()
|
||||
|
||||
// Act
|
||||
renderComponent({ onSelectTrigger: customHandler })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.trigger')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle missing onSelectUserInput gracefully', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderComponent({ onSelectUserInput: undefined })
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.userInputFull')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle missing onSelectTrigger gracefully', () => {
|
||||
// Arrange & Act
|
||||
expect(() => {
|
||||
renderComponent({ onSelectTrigger: undefined })
|
||||
}).not.toThrow()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.trigger')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions - User Input Option
|
||||
describe('User Interactions - User Input', () => {
|
||||
it('should call onSelectUserInput when user input option is clicked', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const userInputOption = screen.getByText('workflow.onboarding.userInputFull')
|
||||
await user.click(userInputOption)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectUserInput).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call onSelectTrigger when user input option is clicked', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const userInputOption = screen.getByText('workflow.onboarding.userInputFull')
|
||||
await user.click(userInputOption)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectTrigger).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle multiple clicks on user input option', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const userInputOption = screen.getByText('workflow.onboarding.userInputFull')
|
||||
await user.click(userInputOption)
|
||||
await user.click(userInputOption)
|
||||
await user.click(userInputOption)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectUserInput).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions - Trigger Option
|
||||
describe('User Interactions - Trigger', () => {
|
||||
it('should show trigger selector when trigger option is clicked', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('node-selector-content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call onSelectTrigger immediately when trigger option is clicked', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectTrigger).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onSelectTrigger when a trigger is selected from selector', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Open trigger selector
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Act - Select a trigger
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-schedule')).toBeInTheDocument()
|
||||
})
|
||||
const scheduleButton = screen.getByTestId('select-schedule')
|
||||
await user.click(scheduleButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectTrigger).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelectTrigger).toHaveBeenCalledWith(BlockEnum.TriggerSchedule, undefined)
|
||||
})
|
||||
|
||||
it('should call onSelectTrigger with correct node type for webhook', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Open trigger selector
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Act - Select webhook trigger
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-webhook')).toBeInTheDocument()
|
||||
})
|
||||
const webhookButton = screen.getByTestId('select-webhook')
|
||||
await user.click(webhookButton)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectTrigger).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelectTrigger).toHaveBeenCalledWith(BlockEnum.TriggerWebhook, undefined)
|
||||
})
|
||||
|
||||
it('should hide trigger selector after selection', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Open trigger selector
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Act - Select a trigger
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-schedule')).toBeInTheDocument()
|
||||
})
|
||||
const scheduleButton = screen.getByTestId('select-schedule')
|
||||
await user.click(scheduleButton)
|
||||
|
||||
// Assert - Selector should be hidden
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('node-selector-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass tool config parameter through onSelectTrigger', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Open trigger selector
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Act - Select a trigger (our mock doesn't pass toolConfig, but real NodeSelector would)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-schedule')).toBeInTheDocument()
|
||||
})
|
||||
const scheduleButton = screen.getByTestId('select-schedule')
|
||||
await user.click(scheduleButton)
|
||||
|
||||
// Assert - Verify handler was called
|
||||
// In real usage, NodeSelector would pass toolConfig as second parameter
|
||||
expect(mockOnSelectTrigger).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// State Management
|
||||
describe('State Management', () => {
|
||||
it('should toggle trigger selector visibility', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Assert - Initially hidden
|
||||
expect(screen.queryByTestId('node-selector-content')).not.toBeInTheDocument()
|
||||
|
||||
// Act - Show selector
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Assert - Now visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('node-selector-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Act - Close selector
|
||||
const closeButton = screen.getByTestId('close-selector')
|
||||
await user.click(closeButton)
|
||||
|
||||
// Assert - Hidden again
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('node-selector-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should maintain state across user input selections', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Click user input multiple times
|
||||
const userInputOption = screen.getByText('workflow.onboarding.userInputFull')
|
||||
await user.click(userInputOption)
|
||||
await user.click(userInputOption)
|
||||
|
||||
// Assert - Trigger selector should remain hidden
|
||||
expect(screen.queryByTestId('node-selector-content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should reset trigger selector visibility after selection', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Open and select trigger
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-schedule')).toBeInTheDocument()
|
||||
})
|
||||
const scheduleButton = screen.getByTestId('select-schedule')
|
||||
await user.click(scheduleButton)
|
||||
|
||||
// Assert - Selector should be closed
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('node-selector-content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Act - Click trigger option again
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Assert - Selector should open again
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('node-selector-content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Edge Cases (REQUIRED)
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle rapid clicks on trigger option', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
await user.click(triggerOption)
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Assert - Should still be open (last click)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('node-selector-content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle selecting different trigger types in sequence', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Open and select schedule
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-schedule')).toBeInTheDocument()
|
||||
})
|
||||
await user.click(screen.getByTestId('select-schedule'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectTrigger).toHaveBeenNthCalledWith(1, BlockEnum.TriggerSchedule, undefined)
|
||||
|
||||
// Act - Open again and select webhook
|
||||
await user.click(triggerOption)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('select-webhook')).toBeInTheDocument()
|
||||
})
|
||||
await user.click(screen.getByTestId('select-webhook'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectTrigger).toHaveBeenNthCalledWith(2, BlockEnum.TriggerWebhook, undefined)
|
||||
expect(mockOnSelectTrigger).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should not crash with undefined callbacks', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent({
|
||||
onSelectUserInput: undefined,
|
||||
onSelectTrigger: undefined,
|
||||
})
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
const userInputOption = screen.getByText('workflow.onboarding.userInputFull')
|
||||
await expect(user.click(userInputOption)).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle opening and closing selector without selection', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Open selector
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Act - Close without selecting
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('close-selector')).toBeInTheDocument()
|
||||
})
|
||||
await user.click(screen.getByTestId('close-selector'))
|
||||
|
||||
// Assert - No selection callback should be called
|
||||
expect(mockOnSelectTrigger).not.toHaveBeenCalled()
|
||||
|
||||
// Assert - Selector should be closed
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('node-selector-content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Accessibility Tests
|
||||
describe('Accessibility', () => {
|
||||
it('should have both options visible and accessible', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.userInputFull')).toBeVisible()
|
||||
expect(screen.getByText('workflow.onboarding.trigger')).toBeVisible()
|
||||
})
|
||||
|
||||
it('should have descriptive text for both options', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.onboarding.userInputDescription')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.triggerDescription')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have icons for visual identification', () => {
|
||||
// Arrange & Act
|
||||
renderComponent()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('home-icon')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('trigger-all-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain focus after interactions', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act
|
||||
const userInputOption = screen.getByText('workflow.onboarding.userInputFull')
|
||||
await user.click(userInputOption)
|
||||
|
||||
// Assert - Component should still be in document
|
||||
expect(screen.getByText('workflow.onboarding.userInputFull')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.trigger')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Integration Tests
|
||||
describe('Integration', () => {
|
||||
it('should coordinate between both options correctly', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
// Act - Click user input
|
||||
const userInputOption = screen.getByText('workflow.onboarding.userInputFull')
|
||||
await user.click(userInputOption)
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectUserInput).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelectTrigger).not.toHaveBeenCalled()
|
||||
|
||||
// Act - Click trigger
|
||||
const triggerOption = screen.getByText('workflow.onboarding.trigger')
|
||||
await user.click(triggerOption)
|
||||
|
||||
// Assert - Trigger selector should open
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('node-selector-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Act - Select trigger
|
||||
await user.click(screen.getByTestId('select-schedule'))
|
||||
|
||||
// Assert
|
||||
expect(mockOnSelectTrigger).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnSelectUserInput).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render all components in correct hierarchy', () => {
|
||||
// Arrange & Act
|
||||
const { container } = renderComponent()
|
||||
|
||||
// Assert
|
||||
const grid = container.querySelector('.grid')
|
||||
expect(grid).toBeInTheDocument()
|
||||
|
||||
// Both StartNodeOption components should be rendered
|
||||
expect(screen.getByText('workflow.onboarding.userInputFull')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.onboarding.trigger')).toBeInTheDocument()
|
||||
|
||||
// NodeSelector should be rendered
|
||||
expect(screen.getByTestId('node-selector')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user