mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
Merge branch 'main' into chore/jest1
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import EditItem, { EditItemType } from './index'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('AddAnnotationModal/EditItem', () => {
|
||||
test('should render query inputs with user avatar and placeholder strings', () => {
|
||||
render(
|
||||
<EditItem
|
||||
type={EditItemType.Query}
|
||||
content="Why?"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('appAnnotation.addModal.queryName')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('appAnnotation.addModal.queryPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByText('Why?')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should render answer name and placeholder text', () => {
|
||||
render(
|
||||
<EditItem
|
||||
type={EditItemType.Answer}
|
||||
content="Existing answer"
|
||||
onChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('appAnnotation.addModal.answerName')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('appAnnotation.addModal.answerPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByDisplayValue('Existing answer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should propagate changes when answer content updates', () => {
|
||||
const handleChange = jest.fn()
|
||||
render(
|
||||
<EditItem
|
||||
type={EditItemType.Answer}
|
||||
content=""
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('appAnnotation.addModal.answerPlaceholder'), { target: { value: 'Because' } })
|
||||
expect(handleChange).toHaveBeenCalledWith('Because')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,155 @@
|
||||
import React from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import AddAnnotationModal from './index'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockToastNotify = jest.fn()
|
||||
jest.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: jest.fn(args => mockToastNotify(args)),
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/billing/annotation-full', () => () => <div data-testid="annotation-full" />)
|
||||
|
||||
const mockUseProviderContext = useProviderContext as jest.Mock
|
||||
|
||||
const getProviderContext = ({ usage = 0, total = 10, enableBilling = false } = {}) => ({
|
||||
plan: {
|
||||
usage: { annotatedResponse: usage },
|
||||
total: { annotatedResponse: total },
|
||||
},
|
||||
enableBilling,
|
||||
})
|
||||
|
||||
describe('AddAnnotationModal', () => {
|
||||
const baseProps = {
|
||||
isShow: true,
|
||||
onHide: jest.fn(),
|
||||
onAdd: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockUseProviderContext.mockReturnValue(getProviderContext())
|
||||
})
|
||||
|
||||
const typeQuestion = (value: string) => {
|
||||
fireEvent.change(screen.getByPlaceholderText('appAnnotation.addModal.queryPlaceholder'), {
|
||||
target: { value },
|
||||
})
|
||||
}
|
||||
|
||||
const typeAnswer = (value: string) => {
|
||||
fireEvent.change(screen.getByPlaceholderText('appAnnotation.addModal.answerPlaceholder'), {
|
||||
target: { value },
|
||||
})
|
||||
}
|
||||
|
||||
test('should render modal title when drawer is visible', () => {
|
||||
render(<AddAnnotationModal {...baseProps} />)
|
||||
|
||||
expect(screen.getByText('appAnnotation.addModal.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should capture query input text when typing', () => {
|
||||
render(<AddAnnotationModal {...baseProps} />)
|
||||
typeQuestion('Sample question')
|
||||
expect(screen.getByPlaceholderText('appAnnotation.addModal.queryPlaceholder')).toHaveValue('Sample question')
|
||||
})
|
||||
|
||||
test('should capture answer input text when typing', () => {
|
||||
render(<AddAnnotationModal {...baseProps} />)
|
||||
typeAnswer('Sample answer')
|
||||
expect(screen.getByPlaceholderText('appAnnotation.addModal.answerPlaceholder')).toHaveValue('Sample answer')
|
||||
})
|
||||
|
||||
test('should show annotation full notice and disable submit when quota exceeded', () => {
|
||||
mockUseProviderContext.mockReturnValue(getProviderContext({ usage: 10, total: 10, enableBilling: true }))
|
||||
render(<AddAnnotationModal {...baseProps} />)
|
||||
|
||||
expect(screen.getByTestId('annotation-full')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.add' })).toBeDisabled()
|
||||
})
|
||||
|
||||
test('should call onAdd with form values when create next enabled', async () => {
|
||||
const onAdd = jest.fn().mockResolvedValue(undefined)
|
||||
render(<AddAnnotationModal {...baseProps} onAdd={onAdd} />)
|
||||
|
||||
typeQuestion('Question value')
|
||||
typeAnswer('Answer value')
|
||||
fireEvent.click(screen.getByTestId('checkbox-create-next-checkbox'))
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
||||
})
|
||||
|
||||
expect(onAdd).toHaveBeenCalledWith({ question: 'Question value', answer: 'Answer value' })
|
||||
})
|
||||
|
||||
test('should reset fields after saving when create next enabled', async () => {
|
||||
const onAdd = jest.fn().mockResolvedValue(undefined)
|
||||
render(<AddAnnotationModal {...baseProps} onAdd={onAdd} />)
|
||||
|
||||
typeQuestion('Question value')
|
||||
typeAnswer('Answer value')
|
||||
const createNextToggle = screen.getByText('appAnnotation.addModal.createNext').previousElementSibling as HTMLElement
|
||||
fireEvent.click(createNextToggle)
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText('appAnnotation.addModal.queryPlaceholder')).toHaveValue('')
|
||||
expect(screen.getByPlaceholderText('appAnnotation.addModal.answerPlaceholder')).toHaveValue('')
|
||||
})
|
||||
})
|
||||
|
||||
test('should show toast when validation fails for missing question', () => {
|
||||
render(<AddAnnotationModal {...baseProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
message: 'appAnnotation.errorMessage.queryRequired',
|
||||
}))
|
||||
})
|
||||
|
||||
test('should show toast when validation fails for missing answer', () => {
|
||||
render(<AddAnnotationModal {...baseProps} />)
|
||||
typeQuestion('Filled question')
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
||||
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'error',
|
||||
message: 'appAnnotation.errorMessage.answerRequired',
|
||||
}))
|
||||
})
|
||||
|
||||
test('should close modal when save completes and create next unchecked', async () => {
|
||||
const onAdd = jest.fn().mockResolvedValue(undefined)
|
||||
render(<AddAnnotationModal {...baseProps} onAdd={onAdd} />)
|
||||
|
||||
typeQuestion('Q')
|
||||
typeAnswer('A')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
||||
})
|
||||
|
||||
expect(baseProps.onHide).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should allow cancel button to close the drawer', () => {
|
||||
render(<AddAnnotationModal {...baseProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
expect(baseProps.onHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -101,7 +101,7 @@ const AddAnnotationModal: FC<Props> = ({
|
||||
<div
|
||||
className='flex items-center space-x-2'
|
||||
>
|
||||
<Checkbox checked={isCreateNext} onCheck={() => setIsCreateNext(!isCreateNext)} />
|
||||
<Checkbox id='create-next-checkbox' checked={isCreateNext} onCheck={() => setIsCreateNext(!isCreateNext)} />
|
||||
<div>{t('appAnnotation.addModal.createNext')}</div>
|
||||
</div>
|
||||
<div className='mt-2 flex space-x-2'>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import CSVDownload from './index'
|
||||
|
||||
const mockType = { Link: 'mock-link' }
|
||||
let capturedProps: Record<string, unknown> | undefined
|
||||
|
||||
jest.mock('react-papaparse', () => ({
|
||||
useCSVDownloader: () => {
|
||||
const CSVDownloader = ({ children, ...props }: React.PropsWithChildren<Record<string, unknown>>) => {
|
||||
capturedProps = props
|
||||
return <div data-testid="csv-downloader" className={props.className as string}>{children}</div>
|
||||
}
|
||||
return {
|
||||
CSVDownloader,
|
||||
Type: mockType,
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
describe('CSVDownload', () => {
|
||||
const vars = [{ name: 'prompt' }, { name: 'context' }]
|
||||
|
||||
beforeEach(() => {
|
||||
capturedProps = undefined
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('should render table headers and sample row for each variable', () => {
|
||||
render(<CSVDownload vars={vars} />)
|
||||
|
||||
expect(screen.getByText('share.generation.csvStructureTitle')).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('row')[0].children).toHaveLength(2)
|
||||
expect(screen.getByText('prompt share.generation.field')).toBeInTheDocument()
|
||||
expect(screen.getByText('context share.generation.field')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should configure CSV downloader with template data', () => {
|
||||
render(<CSVDownload vars={vars} />)
|
||||
|
||||
expect(capturedProps?.filename).toBe('template')
|
||||
expect(capturedProps?.type).toBe(mockType.Link)
|
||||
expect(capturedProps?.bom).toBe(true)
|
||||
expect(capturedProps?.data).toEqual([
|
||||
{ prompt: '', context: '' },
|
||||
])
|
||||
expect(screen.getByText('share.generation.downloadTemplate')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import CSVReader from './index'
|
||||
|
||||
let mockAcceptedFile: { name: string } | null = null
|
||||
let capturedHandlers: Record<string, (payload: any) => void> = {}
|
||||
|
||||
jest.mock('react-papaparse', () => ({
|
||||
useCSVReader: () => ({
|
||||
CSVReader: ({ children, ...handlers }: any) => {
|
||||
capturedHandlers = handlers
|
||||
return (
|
||||
<div data-testid="csv-reader-wrapper">
|
||||
{children({
|
||||
getRootProps: () => ({ 'data-testid': 'drop-zone' }),
|
||||
acceptedFile: mockAcceptedFile,
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('CSVReader', () => {
|
||||
beforeEach(() => {
|
||||
mockAcceptedFile = null
|
||||
capturedHandlers = {}
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('should display upload instructions when no file selected', async () => {
|
||||
const onParsed = jest.fn()
|
||||
render(<CSVReader onParsed={onParsed} />)
|
||||
|
||||
expect(screen.getByText('share.generation.csvUploadTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('share.generation.browse')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
capturedHandlers.onUploadAccepted?.({ data: [['row1']] })
|
||||
})
|
||||
expect(onParsed).toHaveBeenCalledWith([['row1']])
|
||||
})
|
||||
|
||||
test('should show accepted file name without extension', () => {
|
||||
mockAcceptedFile = { name: 'batch.csv' }
|
||||
render(<CSVReader onParsed={jest.fn()} />)
|
||||
|
||||
expect(screen.getByText('batch')).toBeInTheDocument()
|
||||
expect(screen.getByText('.csv')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should toggle hover styling on drag events', async () => {
|
||||
render(<CSVReader onParsed={jest.fn()} />)
|
||||
const dragEvent = { preventDefault: jest.fn() } as unknown as DragEvent
|
||||
|
||||
await act(async () => {
|
||||
capturedHandlers.onDragOver?.(dragEvent)
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drop-zone')).toHaveClass('border-components-dropzone-border-accent')
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
capturedHandlers.onDragLeave?.(dragEvent)
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('drop-zone')).not.toHaveClass('border-components-dropzone-border-accent')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import RunBatch from './index'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
jest.mock('@/hooks/use-breakpoints', () => {
|
||||
const actual = jest.requireActual('@/hooks/use-breakpoints')
|
||||
return {
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
MediaType: actual.MediaType,
|
||||
}
|
||||
})
|
||||
|
||||
let latestOnParsed: ((data: string[][]) => void) | undefined
|
||||
let receivedCSVDownloadProps: Record<string, unknown> | undefined
|
||||
|
||||
jest.mock('./csv-reader', () => (props: { onParsed: (data: string[][]) => void }) => {
|
||||
latestOnParsed = props.onParsed
|
||||
return <div data-testid="csv-reader" />
|
||||
})
|
||||
|
||||
jest.mock('./csv-download', () => (props: { vars: { name: string }[] }) => {
|
||||
receivedCSVDownloadProps = props
|
||||
return <div data-testid="csv-download" />
|
||||
})
|
||||
|
||||
const mockUseBreakpoints = useBreakpoints as jest.Mock
|
||||
|
||||
describe('RunBatch', () => {
|
||||
const vars = [{ name: 'prompt' }]
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseBreakpoints.mockReturnValue(MediaType.pc)
|
||||
latestOnParsed = undefined
|
||||
receivedCSVDownloadProps = undefined
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('should enable run button after CSV parsed and send data', async () => {
|
||||
const onSend = jest.fn()
|
||||
render(
|
||||
<RunBatch
|
||||
vars={vars}
|
||||
onSend={onSend}
|
||||
isAllFinished
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(receivedCSVDownloadProps?.vars).toEqual(vars)
|
||||
await act(async () => {
|
||||
latestOnParsed?.([['row1']])
|
||||
})
|
||||
|
||||
const runButton = screen.getByRole('button', { name: 'share.generation.run' })
|
||||
await waitFor(() => {
|
||||
expect(runButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
fireEvent.click(runButton)
|
||||
expect(onSend).toHaveBeenCalledWith([['row1']])
|
||||
})
|
||||
|
||||
test('should keep button disabled and show spinner when results still running on mobile', async () => {
|
||||
mockUseBreakpoints.mockReturnValue(MediaType.mobile)
|
||||
const onSend = jest.fn()
|
||||
const { container } = render(
|
||||
<RunBatch
|
||||
vars={vars}
|
||||
onSend={onSend}
|
||||
isAllFinished={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
latestOnParsed?.([['row']])
|
||||
})
|
||||
|
||||
const runButton = screen.getByRole('button', { name: 'share.generation.run' })
|
||||
await waitFor(() => {
|
||||
expect(runButton).toBeDisabled()
|
||||
})
|
||||
expect(runButton).toHaveClass('grow')
|
||||
const icon = container.querySelector('svg')
|
||||
expect(icon).toHaveClass('animate-spin')
|
||||
expect(onSend).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user