mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 09:17:19 -05:00
chore: tests for annotation (#29851)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
42
web/app/components/app/annotation/batch-action.spec.tsx
Normal file
42
web/app/components/app/annotation/batch-action.spec.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import BatchAction from './batch-action'
|
||||
|
||||
describe('BatchAction', () => {
|
||||
const baseProps = {
|
||||
selectedIds: ['1', '2', '3'],
|
||||
onBatchDelete: jest.fn(),
|
||||
onCancel: jest.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should show the selected count and trigger cancel action', () => {
|
||||
render(<BatchAction {...baseProps} className='custom-class' />)
|
||||
|
||||
expect(screen.getByText('3')).toBeInTheDocument()
|
||||
expect(screen.getByText('appAnnotation.batchAction.selected')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
|
||||
expect(baseProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should confirm before running batch delete', async () => {
|
||||
const onBatchDelete = jest.fn().mockResolvedValue(undefined)
|
||||
render(<BatchAction {...baseProps} onBatchDelete={onBatchDelete} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.delete' }))
|
||||
await screen.findByText('appAnnotation.list.delete.title')
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'common.operation.delete' })[1])
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onBatchDelete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import CSVDownload from './csv-downloader'
|
||||
import I18nContext from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
|
||||
const downloaderProps: any[] = []
|
||||
|
||||
jest.mock('react-papaparse', () => ({
|
||||
useCSVDownloader: jest.fn(() => ({
|
||||
CSVDownloader: ({ children, ...props }: any) => {
|
||||
downloaderProps.push(props)
|
||||
return <div data-testid="mock-csv-downloader">{children}</div>
|
||||
},
|
||||
Type: { Link: 'link' },
|
||||
})),
|
||||
}))
|
||||
|
||||
const renderWithLocale = (locale: Locale) => {
|
||||
return render(
|
||||
<I18nContext.Provider value={{
|
||||
locale,
|
||||
i18n: {},
|
||||
setLocaleOnClient: jest.fn().mockResolvedValue(undefined),
|
||||
}}
|
||||
>
|
||||
<CSVDownload />
|
||||
</I18nContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('CSVDownload', () => {
|
||||
const englishTemplate = [
|
||||
['question', 'answer'],
|
||||
['question1', 'answer1'],
|
||||
['question2', 'answer2'],
|
||||
]
|
||||
const chineseTemplate = [
|
||||
['问题', '答案'],
|
||||
['问题 1', '答案 1'],
|
||||
['问题 2', '答案 2'],
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
downloaderProps.length = 0
|
||||
})
|
||||
|
||||
it('should render the structure preview and pass English template data by default', () => {
|
||||
renderWithLocale('en-US' as Locale)
|
||||
|
||||
expect(screen.getByText('share.generation.csvStructureTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('appAnnotation.batchModal.template')).toBeInTheDocument()
|
||||
|
||||
expect(downloaderProps[0]).toMatchObject({
|
||||
filename: 'template-en-US',
|
||||
type: 'link',
|
||||
bom: true,
|
||||
data: englishTemplate,
|
||||
})
|
||||
})
|
||||
|
||||
it('should switch to the Chinese template when locale matches the secondary language', () => {
|
||||
const locale = LanguagesSupported[1] as Locale
|
||||
renderWithLocale(locale)
|
||||
|
||||
expect(downloaderProps[0]).toMatchObject({
|
||||
filename: `template-${locale}`,
|
||||
data: chineseTemplate,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,164 @@
|
||||
import React from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import BatchModal, { ProcessStatus } from './index'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation'
|
||||
import type { IBatchModalProps } from './index'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
jest.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
notify: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/service/annotation', () => ({
|
||||
annotationBatchImport: jest.fn(),
|
||||
checkAnnotationBatchImportProgress: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('./csv-downloader', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="csv-downloader-stub" />,
|
||||
}))
|
||||
|
||||
let lastUploadedFile: File | undefined
|
||||
|
||||
jest.mock('./csv-uploader', () => ({
|
||||
__esModule: true,
|
||||
default: ({ file, updateFile }: { file?: File; updateFile: (file?: File) => void }) => (
|
||||
<div>
|
||||
<button
|
||||
data-testid="mock-uploader"
|
||||
onClick={() => {
|
||||
lastUploadedFile = new File(['question,answer'], 'batch.csv', { type: 'text/csv' })
|
||||
updateFile(lastUploadedFile)
|
||||
}}
|
||||
>
|
||||
upload
|
||||
</button>
|
||||
{file && <span data-testid="selected-file">{file.name}</span>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/billing/annotation-full', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
const mockNotify = Toast.notify as jest.Mock
|
||||
const useProviderContextMock = useProviderContext as jest.Mock
|
||||
const annotationBatchImportMock = annotationBatchImport as jest.Mock
|
||||
const checkAnnotationBatchImportProgressMock = checkAnnotationBatchImportProgress as jest.Mock
|
||||
|
||||
const renderComponent = (props: Partial<IBatchModalProps> = {}) => {
|
||||
const mergedProps: IBatchModalProps = {
|
||||
appId: 'app-id',
|
||||
isShow: true,
|
||||
onCancel: jest.fn(),
|
||||
onAdded: jest.fn(),
|
||||
...props,
|
||||
}
|
||||
return {
|
||||
...render(<BatchModal {...mergedProps} />),
|
||||
props: mergedProps,
|
||||
}
|
||||
}
|
||||
|
||||
describe('BatchModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
lastUploadedFile = undefined
|
||||
useProviderContextMock.mockReturnValue({
|
||||
plan: {
|
||||
usage: { annotatedResponse: 0 },
|
||||
total: { annotatedResponse: 10 },
|
||||
},
|
||||
enableBilling: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable run action and show billing hint when annotation quota is full', () => {
|
||||
useProviderContextMock.mockReturnValue({
|
||||
plan: {
|
||||
usage: { annotatedResponse: 10 },
|
||||
total: { annotatedResponse: 10 },
|
||||
},
|
||||
enableBilling: true,
|
||||
})
|
||||
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByTestId('annotation-full')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'appAnnotation.batchModal.run' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should reset uploader state when modal closes and allow manual cancellation', () => {
|
||||
const { rerender, props } = renderComponent()
|
||||
|
||||
fireEvent.click(screen.getByTestId('mock-uploader'))
|
||||
expect(screen.getByTestId('selected-file')).toHaveTextContent('batch.csv')
|
||||
|
||||
rerender(<BatchModal {...props} isShow={false} />)
|
||||
rerender(<BatchModal {...props} isShow />)
|
||||
|
||||
expect(screen.queryByTestId('selected-file')).toBeNull()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appAnnotation.batchModal.cancel' }))
|
||||
expect(props.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should submit the csv file, poll status, and notify when import completes', async () => {
|
||||
jest.useFakeTimers()
|
||||
const { props } = renderComponent()
|
||||
const fileTrigger = screen.getByTestId('mock-uploader')
|
||||
fireEvent.click(fileTrigger)
|
||||
|
||||
const runButton = screen.getByRole('button', { name: 'appAnnotation.batchModal.run' })
|
||||
expect(runButton).not.toBeDisabled()
|
||||
|
||||
annotationBatchImportMock.mockResolvedValue({ job_id: 'job-1', job_status: ProcessStatus.PROCESSING })
|
||||
checkAnnotationBatchImportProgressMock
|
||||
.mockResolvedValueOnce({ job_id: 'job-1', job_status: ProcessStatus.PROCESSING })
|
||||
.mockResolvedValueOnce({ job_id: 'job-1', job_status: ProcessStatus.COMPLETED })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(runButton)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(annotationBatchImportMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
const formData = annotationBatchImportMock.mock.calls[0][0].body as FormData
|
||||
expect(formData.get('file')).toBe(lastUploadedFile)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkAnnotationBatchImportProgressMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
jest.runOnlyPendingTimers()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkAnnotationBatchImportProgressMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'appAnnotation.batchModal.completed',
|
||||
})
|
||||
expect(props.onAdded).toHaveBeenCalledTimes(1)
|
||||
expect(props.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
jest.useRealTimers()
|
||||
})
|
||||
})
|
||||
13
web/app/components/app/annotation/empty-element.spec.tsx
Normal file
13
web/app/components/app/annotation/empty-element.spec.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import EmptyElement from './empty-element'
|
||||
|
||||
describe('EmptyElement', () => {
|
||||
it('should render the empty state copy and supporting icon', () => {
|
||||
const { container } = render(<EmptyElement />)
|
||||
|
||||
expect(screen.getByText('appAnnotation.noData.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('appAnnotation.noData.description')).toBeInTheDocument()
|
||||
expect(container.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
})
|
||||
70
web/app/components/app/annotation/filter.spec.tsx
Normal file
70
web/app/components/app/annotation/filter.spec.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import Filter, { type QueryParam } from './filter'
|
||||
import useSWR from 'swr'
|
||||
|
||||
jest.mock('swr', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/log', () => ({
|
||||
fetchAnnotationsCount: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockUseSWR = useSWR as unknown as jest.Mock
|
||||
|
||||
describe('Filter', () => {
|
||||
const appId = 'app-1'
|
||||
const childContent = 'child-content'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render nothing until annotation count is fetched', () => {
|
||||
mockUseSWR.mockReturnValue({ data: undefined })
|
||||
|
||||
const { container } = render(
|
||||
<Filter
|
||||
appId={appId}
|
||||
queryParams={{ keyword: '' }}
|
||||
setQueryParams={jest.fn()}
|
||||
>
|
||||
<div>{childContent}</div>
|
||||
</Filter>,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toBeNull()
|
||||
expect(mockUseSWR).toHaveBeenCalledWith(
|
||||
{ url: `/apps/${appId}/annotations/count` },
|
||||
expect.any(Function),
|
||||
)
|
||||
})
|
||||
|
||||
it('should propagate keyword changes and clearing behavior', () => {
|
||||
mockUseSWR.mockReturnValue({ data: { total: 20 } })
|
||||
const queryParams: QueryParam = { keyword: 'prefill' }
|
||||
const setQueryParams = jest.fn()
|
||||
|
||||
const { container } = render(
|
||||
<Filter
|
||||
appId={appId}
|
||||
queryParams={queryParams}
|
||||
setQueryParams={setQueryParams}
|
||||
>
|
||||
<div>{childContent}</div>
|
||||
</Filter>,
|
||||
)
|
||||
|
||||
const input = screen.getByPlaceholderText('common.operation.search') as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: 'updated' } })
|
||||
expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: 'updated' })
|
||||
|
||||
const clearButton = input.parentElement?.querySelector('div.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(clearButton)
|
||||
expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: '' })
|
||||
|
||||
expect(container).toHaveTextContent(childContent)
|
||||
})
|
||||
})
|
||||
323
web/app/components/app/annotation/header-opts/index.spec.tsx
Normal file
323
web/app/components/app/annotation/header-opts/index.spec.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import type { ComponentProps } from 'react'
|
||||
import HeaderOptions from './index'
|
||||
import I18NContext from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||
|
||||
let lastCSVDownloaderProps: Record<string, unknown> | undefined
|
||||
const mockCSVDownloader = jest.fn(({ children, ...props }) => {
|
||||
lastCSVDownloaderProps = props
|
||||
return (
|
||||
<div data-testid="csv-downloader">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
jest.mock('react-papaparse', () => ({
|
||||
useCSVDownloader: () => ({
|
||||
CSVDownloader: (props: any) => mockCSVDownloader(props),
|
||||
Type: { Link: 'link' },
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/annotation', () => ({
|
||||
fetchExportAnnotationList: jest.fn(),
|
||||
clearAllAnnotations: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
plan: {
|
||||
usage: { annotatedResponse: 0 },
|
||||
total: { annotatedResponse: 10 },
|
||||
},
|
||||
enableBilling: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/billing/annotation-full', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="annotation-full" />,
|
||||
}))
|
||||
|
||||
type HeaderOptionsProps = ComponentProps<typeof HeaderOptions>
|
||||
|
||||
const renderComponent = (
|
||||
props: Partial<HeaderOptionsProps> = {},
|
||||
locale: string = LanguagesSupported[0] as string,
|
||||
) => {
|
||||
const defaultProps: HeaderOptionsProps = {
|
||||
appId: 'test-app-id',
|
||||
onAdd: jest.fn(),
|
||||
onAdded: jest.fn(),
|
||||
controlUpdateList: 0,
|
||||
...props,
|
||||
}
|
||||
|
||||
return render(
|
||||
<I18NContext.Provider
|
||||
value={{
|
||||
locale,
|
||||
i18n: {},
|
||||
setLocaleOnClient: jest.fn(),
|
||||
}}
|
||||
>
|
||||
<HeaderOptions {...defaultProps} />
|
||||
</I18NContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
const openOperationsPopover = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
const trigger = document.querySelector('button.btn.btn-secondary') as HTMLButtonElement
|
||||
expect(trigger).toBeTruthy()
|
||||
await user.click(trigger)
|
||||
}
|
||||
|
||||
const expandExportMenu = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
await openOperationsPopover(user)
|
||||
const exportLabel = await screen.findByText('appAnnotation.table.header.bulkExport')
|
||||
const exportButton = exportLabel.closest('button') as HTMLButtonElement
|
||||
expect(exportButton).toBeTruthy()
|
||||
await user.click(exportButton)
|
||||
}
|
||||
|
||||
const getExportButtons = async () => {
|
||||
const csvLabel = await screen.findByText('CSV')
|
||||
const jsonLabel = await screen.findByText('JSONL')
|
||||
const csvButton = csvLabel.closest('button') as HTMLButtonElement
|
||||
const jsonButton = jsonLabel.closest('button') as HTMLButtonElement
|
||||
expect(csvButton).toBeTruthy()
|
||||
expect(jsonButton).toBeTruthy()
|
||||
return {
|
||||
csvButton,
|
||||
jsonButton,
|
||||
}
|
||||
}
|
||||
|
||||
const clickOperationAction = async (
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
translationKey: string,
|
||||
) => {
|
||||
const label = await screen.findByText(translationKey)
|
||||
const button = label.closest('button') as HTMLButtonElement
|
||||
expect(button).toBeTruthy()
|
||||
await user.click(button)
|
||||
}
|
||||
|
||||
const mockAnnotations: AnnotationItemBasic[] = [
|
||||
{
|
||||
question: 'Question 1',
|
||||
answer: 'Answer 1',
|
||||
},
|
||||
]
|
||||
|
||||
const mockedFetchAnnotations = jest.mocked(fetchExportAnnotationList)
|
||||
const mockedClearAllAnnotations = jest.mocked(clearAllAnnotations)
|
||||
|
||||
describe('HeaderOptions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockCSVDownloader.mockClear()
|
||||
lastCSVDownloaderProps = undefined
|
||||
mockedFetchAnnotations.mockResolvedValue({ data: [] })
|
||||
})
|
||||
|
||||
it('should fetch annotations on mount and render enabled export actions when data exist', async () => {
|
||||
mockedFetchAnnotations.mockResolvedValue({ data: mockAnnotations })
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedFetchAnnotations).toHaveBeenCalledWith('test-app-id')
|
||||
})
|
||||
|
||||
await expandExportMenu(user)
|
||||
|
||||
const { csvButton, jsonButton } = await getExportButtons()
|
||||
|
||||
expect(csvButton).not.toBeDisabled()
|
||||
expect(jsonButton).not.toBeDisabled()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastCSVDownloaderProps).toMatchObject({
|
||||
bom: true,
|
||||
filename: 'annotations-en-US',
|
||||
type: 'link',
|
||||
data: [
|
||||
['Question', 'Answer'],
|
||||
['Question 1', 'Answer 1'],
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable export actions when there are no annotations', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
await expandExportMenu(user)
|
||||
|
||||
const { csvButton, jsonButton } = await getExportButtons()
|
||||
|
||||
expect(csvButton).toBeDisabled()
|
||||
expect(jsonButton).toBeDisabled()
|
||||
|
||||
expect(lastCSVDownloaderProps).toMatchObject({
|
||||
data: [['Question', 'Answer']],
|
||||
})
|
||||
})
|
||||
|
||||
it('should open the add annotation modal and forward the onAdd callback', async () => {
|
||||
mockedFetchAnnotations.mockResolvedValue({ data: mockAnnotations })
|
||||
const user = userEvent.setup()
|
||||
const onAdd = jest.fn().mockResolvedValue(undefined)
|
||||
renderComponent({ onAdd })
|
||||
|
||||
await waitFor(() => expect(mockedFetchAnnotations).toHaveBeenCalled())
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'appAnnotation.table.header.addAnnotation' }),
|
||||
)
|
||||
|
||||
await screen.findByText('appAnnotation.addModal.title')
|
||||
const questionInput = screen.getByPlaceholderText('appAnnotation.addModal.queryPlaceholder')
|
||||
const answerInput = screen.getByPlaceholderText('appAnnotation.addModal.answerPlaceholder')
|
||||
|
||||
await user.type(questionInput, 'Integration question')
|
||||
await user.type(answerInput, 'Integration answer')
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onAdd).toHaveBeenCalledWith({
|
||||
question: 'Integration question',
|
||||
answer: 'Integration answer',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should allow bulk import through the batch modal', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onAdded = jest.fn()
|
||||
renderComponent({ onAdded })
|
||||
|
||||
await openOperationsPopover(user)
|
||||
await clickOperationAction(user, 'appAnnotation.table.header.bulkImport')
|
||||
|
||||
expect(await screen.findByText('appAnnotation.batchModal.title')).toBeInTheDocument()
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'appAnnotation.batchModal.cancel' }),
|
||||
)
|
||||
expect(onAdded).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should trigger JSONL download with locale-specific filename', async () => {
|
||||
mockedFetchAnnotations.mockResolvedValue({ data: mockAnnotations })
|
||||
const user = userEvent.setup()
|
||||
const originalCreateElement = document.createElement.bind(document)
|
||||
const anchor = originalCreateElement('a') as HTMLAnchorElement
|
||||
const clickSpy = jest.spyOn(anchor, 'click').mockImplementation(jest.fn())
|
||||
const createElementSpy = jest
|
||||
.spyOn(document, 'createElement')
|
||||
.mockImplementation((tagName: Parameters<Document['createElement']>[0]) => {
|
||||
if (tagName === 'a')
|
||||
return anchor
|
||||
return originalCreateElement(tagName)
|
||||
})
|
||||
const objectURLSpy = jest
|
||||
.spyOn(URL, 'createObjectURL')
|
||||
.mockReturnValue('blob://mock-url')
|
||||
const revokeSpy = jest.spyOn(URL, 'revokeObjectURL').mockImplementation(jest.fn())
|
||||
|
||||
renderComponent({}, LanguagesSupported[1] as string)
|
||||
|
||||
await expandExportMenu(user)
|
||||
|
||||
await waitFor(() => expect(mockCSVDownloader).toHaveBeenCalled())
|
||||
|
||||
const { jsonButton } = await getExportButtons()
|
||||
await user.click(jsonButton)
|
||||
|
||||
expect(createElementSpy).toHaveBeenCalled()
|
||||
expect(anchor.download).toBe(`annotations-${LanguagesSupported[1]}.jsonl`)
|
||||
expect(clickSpy).toHaveBeenCalled()
|
||||
expect(revokeSpy).toHaveBeenCalledWith('blob://mock-url')
|
||||
|
||||
const blobArg = objectURLSpy.mock.calls[0][0] as Blob
|
||||
await expect(blobArg.text()).resolves.toContain('"Question 1"')
|
||||
|
||||
clickSpy.mockRestore()
|
||||
createElementSpy.mockRestore()
|
||||
objectURLSpy.mockRestore()
|
||||
revokeSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should clear all annotations when confirmation succeeds', async () => {
|
||||
mockedClearAllAnnotations.mockResolvedValue(undefined)
|
||||
const user = userEvent.setup()
|
||||
const onAdded = jest.fn()
|
||||
renderComponent({ onAdded })
|
||||
|
||||
await openOperationsPopover(user)
|
||||
await clickOperationAction(user, 'appAnnotation.table.header.clearAll')
|
||||
|
||||
await screen.findByText('appAnnotation.table.header.clearAllConfirm')
|
||||
const confirmButton = screen.getByRole('button', { name: 'common.operation.confirm' })
|
||||
await user.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedClearAllAnnotations).toHaveBeenCalledWith('test-app-id')
|
||||
expect(onAdded).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle clear all failures gracefully', async () => {
|
||||
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
||||
mockedClearAllAnnotations.mockRejectedValue(new Error('network'))
|
||||
const user = userEvent.setup()
|
||||
const onAdded = jest.fn()
|
||||
renderComponent({ onAdded })
|
||||
|
||||
await openOperationsPopover(user)
|
||||
await clickOperationAction(user, 'appAnnotation.table.header.clearAll')
|
||||
await screen.findByText('appAnnotation.table.header.clearAllConfirm')
|
||||
const confirmButton = screen.getByRole('button', { name: 'common.operation.confirm' })
|
||||
await user.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedClearAllAnnotations).toHaveBeenCalled()
|
||||
expect(onAdded).not.toHaveBeenCalled()
|
||||
expect(consoleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should refetch annotations when controlUpdateList changes', async () => {
|
||||
const view = renderComponent({ controlUpdateList: 0 })
|
||||
|
||||
await waitFor(() => expect(mockedFetchAnnotations).toHaveBeenCalledTimes(1))
|
||||
|
||||
view.rerender(
|
||||
<I18NContext.Provider
|
||||
value={{
|
||||
locale: LanguagesSupported[0] as string,
|
||||
i18n: {},
|
||||
setLocaleOnClient: jest.fn(),
|
||||
}}
|
||||
>
|
||||
<HeaderOptions
|
||||
appId="test-app-id"
|
||||
onAdd={jest.fn()}
|
||||
onAdded={jest.fn()}
|
||||
controlUpdateList={1}
|
||||
/>
|
||||
</I18NContext.Provider>,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(mockedFetchAnnotations).toHaveBeenCalledTimes(2))
|
||||
})
|
||||
})
|
||||
233
web/app/components/app/annotation/index.spec.tsx
Normal file
233
web/app/components/app/annotation/index.spec.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import Annotation from './index'
|
||||
import type { AnnotationItem } from './type'
|
||||
import { JobStatus } from './type'
|
||||
import { type App, AppModeEnum } from '@/types/app'
|
||||
import {
|
||||
addAnnotation,
|
||||
delAnnotation,
|
||||
delAnnotations,
|
||||
fetchAnnotationConfig,
|
||||
fetchAnnotationList,
|
||||
queryAnnotationJobStatus,
|
||||
} from '@/service/annotation'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
jest.mock('@/app/components/base/toast', () => ({
|
||||
__esModule: true,
|
||||
default: { notify: jest.fn() },
|
||||
}))
|
||||
|
||||
jest.mock('ahooks', () => ({
|
||||
useDebounce: (value: any) => value,
|
||||
}))
|
||||
|
||||
jest.mock('@/service/annotation', () => ({
|
||||
addAnnotation: jest.fn(),
|
||||
delAnnotation: jest.fn(),
|
||||
delAnnotations: jest.fn(),
|
||||
fetchAnnotationConfig: jest.fn(),
|
||||
editAnnotation: jest.fn(),
|
||||
fetchAnnotationList: jest.fn(),
|
||||
queryAnnotationJobStatus: jest.fn(),
|
||||
updateAnnotationScore: jest.fn(),
|
||||
updateAnnotationStatus: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('./filter', () => ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="filter">{children}</div>
|
||||
))
|
||||
|
||||
jest.mock('./empty-element', () => () => <div data-testid="empty-element" />)
|
||||
|
||||
jest.mock('./header-opts', () => (props: any) => (
|
||||
<div data-testid="header-opts">
|
||||
<button data-testid="trigger-add" onClick={() => props.onAdd({ question: 'new question', answer: 'new answer' })}>
|
||||
add
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
|
||||
let latestListProps: any
|
||||
|
||||
jest.mock('./list', () => (props: any) => {
|
||||
latestListProps = props
|
||||
if (!props.list.length)
|
||||
return <div data-testid="list-empty" />
|
||||
return (
|
||||
<div data-testid="list">
|
||||
<button data-testid="list-view" onClick={() => props.onView(props.list[0])}>view</button>
|
||||
<button data-testid="list-remove" onClick={() => props.onRemove(props.list[0].id)}>remove</button>
|
||||
<button data-testid="list-batch-delete" onClick={() => props.onBatchDelete()}>batch-delete</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
jest.mock('./view-annotation-modal', () => (props: any) => {
|
||||
if (!props.isShow)
|
||||
return null
|
||||
return (
|
||||
<div data-testid="view-modal">
|
||||
<div>{props.item.question}</div>
|
||||
<button data-testid="view-modal-remove" onClick={props.onRemove}>remove</button>
|
||||
<button data-testid="view-modal-close" onClick={props.onHide}>close</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/base/pagination', () => () => <div data-testid="pagination" />)
|
||||
jest.mock('@/app/components/base/loading', () => () => <div data-testid="loading" />)
|
||||
jest.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => (props: any) => props.isShow ? <div data-testid="config-modal" /> : null)
|
||||
jest.mock('@/app/components/billing/annotation-full/modal', () => (props: any) => props.show ? <div data-testid="annotation-full-modal" /> : null)
|
||||
|
||||
const mockNotify = Toast.notify as jest.Mock
|
||||
const addAnnotationMock = addAnnotation as jest.Mock
|
||||
const delAnnotationMock = delAnnotation as jest.Mock
|
||||
const delAnnotationsMock = delAnnotations as jest.Mock
|
||||
const fetchAnnotationConfigMock = fetchAnnotationConfig as jest.Mock
|
||||
const fetchAnnotationListMock = fetchAnnotationList as jest.Mock
|
||||
const queryAnnotationJobStatusMock = queryAnnotationJobStatus as jest.Mock
|
||||
const useProviderContextMock = useProviderContext as jest.Mock
|
||||
|
||||
const appDetail = {
|
||||
id: 'app-id',
|
||||
mode: AppModeEnum.CHAT,
|
||||
} as App
|
||||
|
||||
const createAnnotation = (overrides: Partial<AnnotationItem> = {}): AnnotationItem => ({
|
||||
id: overrides.id ?? 'annotation-1',
|
||||
question: overrides.question ?? 'Question 1',
|
||||
answer: overrides.answer ?? 'Answer 1',
|
||||
created_at: overrides.created_at ?? 1700000000,
|
||||
hit_count: overrides.hit_count ?? 0,
|
||||
})
|
||||
|
||||
const renderComponent = () => render(<Annotation appDetail={appDetail} />)
|
||||
|
||||
describe('Annotation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
latestListProps = undefined
|
||||
fetchAnnotationConfigMock.mockResolvedValue({
|
||||
id: 'config-id',
|
||||
enabled: false,
|
||||
embedding_model: {
|
||||
embedding_model_name: 'model',
|
||||
embedding_provider_name: 'provider',
|
||||
},
|
||||
score_threshold: 0.5,
|
||||
})
|
||||
fetchAnnotationListMock.mockResolvedValue({ data: [], total: 0 })
|
||||
queryAnnotationJobStatusMock.mockResolvedValue({ job_status: JobStatus.completed })
|
||||
useProviderContextMock.mockReturnValue({
|
||||
plan: {
|
||||
usage: { annotatedResponse: 0 },
|
||||
total: { annotatedResponse: 10 },
|
||||
},
|
||||
enableBilling: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should render empty element when no annotations are returned', async () => {
|
||||
renderComponent()
|
||||
|
||||
expect(await screen.findByTestId('empty-element')).toBeInTheDocument()
|
||||
expect(fetchAnnotationListMock).toHaveBeenCalledWith(appDetail.id, expect.objectContaining({
|
||||
page: 1,
|
||||
keyword: '',
|
||||
}))
|
||||
})
|
||||
|
||||
it('should handle annotation creation and refresh list data', async () => {
|
||||
const annotation = createAnnotation()
|
||||
fetchAnnotationListMock.mockResolvedValue({ data: [annotation], total: 1 })
|
||||
addAnnotationMock.mockResolvedValue(undefined)
|
||||
|
||||
renderComponent()
|
||||
|
||||
await screen.findByTestId('list')
|
||||
fireEvent.click(screen.getByTestId('trigger-add'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addAnnotationMock).toHaveBeenCalledWith(appDetail.id, { question: 'new question', answer: 'new answer' })
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
message: 'common.api.actionSuccess',
|
||||
type: 'success',
|
||||
}))
|
||||
})
|
||||
expect(fetchAnnotationListMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should support viewing items and running batch deletion success flow', async () => {
|
||||
const annotation = createAnnotation()
|
||||
fetchAnnotationListMock.mockResolvedValue({ data: [annotation], total: 1 })
|
||||
delAnnotationsMock.mockResolvedValue(undefined)
|
||||
delAnnotationMock.mockResolvedValue(undefined)
|
||||
|
||||
renderComponent()
|
||||
await screen.findByTestId('list')
|
||||
|
||||
await act(async () => {
|
||||
latestListProps.onSelectedIdsChange([annotation.id])
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(latestListProps.selectedIds).toEqual([annotation.id])
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await latestListProps.onBatchDelete()
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(delAnnotationsMock).toHaveBeenCalledWith(appDetail.id, [annotation.id])
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'success',
|
||||
}))
|
||||
expect(latestListProps.selectedIds).toEqual([])
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('list-view'))
|
||||
expect(screen.getByTestId('view-modal')).toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('view-modal-remove'))
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(delAnnotationMock).toHaveBeenCalledWith(appDetail.id, annotation.id)
|
||||
})
|
||||
})
|
||||
|
||||
it('should show an error notification when batch deletion fails', async () => {
|
||||
const annotation = createAnnotation()
|
||||
fetchAnnotationListMock.mockResolvedValue({ data: [annotation], total: 1 })
|
||||
const error = new Error('failed')
|
||||
delAnnotationsMock.mockRejectedValue(error)
|
||||
|
||||
renderComponent()
|
||||
await screen.findByTestId('list')
|
||||
|
||||
await act(async () => {
|
||||
latestListProps.onSelectedIdsChange([annotation.id])
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(latestListProps.selectedIds).toEqual([annotation.id])
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await latestListProps.onBatchDelete()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: error.message,
|
||||
})
|
||||
expect(latestListProps.selectedIds).toEqual([annotation.id])
|
||||
})
|
||||
})
|
||||
})
|
||||
116
web/app/components/app/annotation/list.spec.tsx
Normal file
116
web/app/components/app/annotation/list.spec.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react'
|
||||
import List from './list'
|
||||
import type { AnnotationItem } from './type'
|
||||
|
||||
const mockFormatTime = jest.fn(() => 'formatted-time')
|
||||
|
||||
jest.mock('@/hooks/use-timestamp', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
formatTime: mockFormatTime,
|
||||
}),
|
||||
}))
|
||||
|
||||
const createAnnotation = (overrides: Partial<AnnotationItem> = {}): AnnotationItem => ({
|
||||
id: overrides.id ?? 'annotation-id',
|
||||
question: overrides.question ?? 'question 1',
|
||||
answer: overrides.answer ?? 'answer 1',
|
||||
created_at: overrides.created_at ?? 1700000000,
|
||||
hit_count: overrides.hit_count ?? 2,
|
||||
})
|
||||
|
||||
const getCheckboxes = (container: HTMLElement) => container.querySelectorAll('[data-testid^="checkbox"]')
|
||||
|
||||
describe('List', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render annotation rows and call onView when clicking a row', () => {
|
||||
const item = createAnnotation()
|
||||
const onView = jest.fn()
|
||||
|
||||
render(
|
||||
<List
|
||||
list={[item]}
|
||||
onView={onView}
|
||||
onRemove={jest.fn()}
|
||||
selectedIds={[]}
|
||||
onSelectedIdsChange={jest.fn()}
|
||||
onBatchDelete={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText(item.question))
|
||||
|
||||
expect(onView).toHaveBeenCalledWith(item)
|
||||
expect(mockFormatTime).toHaveBeenCalledWith(item.created_at, 'appLog.dateTimeFormat')
|
||||
})
|
||||
|
||||
it('should toggle single and bulk selection states', () => {
|
||||
const list = [createAnnotation({ id: 'a', question: 'A' }), createAnnotation({ id: 'b', question: 'B' })]
|
||||
const onSelectedIdsChange = jest.fn()
|
||||
const { container, rerender } = render(
|
||||
<List
|
||||
list={list}
|
||||
onView={jest.fn()}
|
||||
onRemove={jest.fn()}
|
||||
selectedIds={[]}
|
||||
onSelectedIdsChange={onSelectedIdsChange}
|
||||
onBatchDelete={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const checkboxes = getCheckboxes(container)
|
||||
fireEvent.click(checkboxes[1])
|
||||
expect(onSelectedIdsChange).toHaveBeenCalledWith(['a'])
|
||||
|
||||
rerender(
|
||||
<List
|
||||
list={list}
|
||||
onView={jest.fn()}
|
||||
onRemove={jest.fn()}
|
||||
selectedIds={['a']}
|
||||
onSelectedIdsChange={onSelectedIdsChange}
|
||||
onBatchDelete={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const updatedCheckboxes = getCheckboxes(container)
|
||||
fireEvent.click(updatedCheckboxes[1])
|
||||
expect(onSelectedIdsChange).toHaveBeenCalledWith([])
|
||||
|
||||
fireEvent.click(updatedCheckboxes[0])
|
||||
expect(onSelectedIdsChange).toHaveBeenCalledWith(['a', 'b'])
|
||||
})
|
||||
|
||||
it('should confirm before removing an annotation and expose batch actions', async () => {
|
||||
const item = createAnnotation({ id: 'to-delete', question: 'Delete me' })
|
||||
const onRemove = jest.fn()
|
||||
render(
|
||||
<List
|
||||
list={[item]}
|
||||
onView={jest.fn()}
|
||||
onRemove={onRemove}
|
||||
selectedIds={[item.id]}
|
||||
onSelectedIdsChange={jest.fn()}
|
||||
onBatchDelete={jest.fn()}
|
||||
onCancel={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const row = screen.getByText(item.question).closest('tr') as HTMLTableRowElement
|
||||
const actionButtons = within(row).getAllByRole('button')
|
||||
fireEvent.click(actionButtons[1])
|
||||
|
||||
expect(await screen.findByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument()
|
||||
const confirmButton = await screen.findByRole('button', { name: 'common.operation.confirm' })
|
||||
fireEvent.click(confirmButton)
|
||||
expect(onRemove).toHaveBeenCalledWith(item.id)
|
||||
|
||||
expect(screen.getByText('appAnnotation.batchAction.selected')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import ViewAnnotationModal from './index'
|
||||
import type { AnnotationItem, HitHistoryItem } from '../type'
|
||||
import { fetchHitHistoryList } from '@/service/annotation'
|
||||
|
||||
const mockFormatTime = jest.fn(() => 'formatted-time')
|
||||
|
||||
jest.mock('@/hooks/use-timestamp', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
formatTime: mockFormatTime,
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/annotation', () => ({
|
||||
fetchHitHistoryList: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('../edit-annotation-modal/edit-item', () => {
|
||||
const EditItemType = {
|
||||
Query: 'query',
|
||||
Answer: 'answer',
|
||||
}
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({ type, content, onSave }: { type: string; content: string; onSave: (value: string) => void }) => (
|
||||
<div>
|
||||
<div data-testid={`content-${type}`}>{content}</div>
|
||||
<button data-testid={`edit-${type}`} onClick={() => onSave(`${type}-updated`)}>edit-{type}</button>
|
||||
</div>
|
||||
),
|
||||
EditItemType,
|
||||
}
|
||||
})
|
||||
|
||||
const fetchHitHistoryListMock = fetchHitHistoryList as jest.Mock
|
||||
|
||||
const createAnnotationItem = (overrides: Partial<AnnotationItem> = {}): AnnotationItem => ({
|
||||
id: overrides.id ?? 'annotation-id',
|
||||
question: overrides.question ?? 'question',
|
||||
answer: overrides.answer ?? 'answer',
|
||||
created_at: overrides.created_at ?? 1700000000,
|
||||
hit_count: overrides.hit_count ?? 0,
|
||||
})
|
||||
|
||||
const createHitHistoryItem = (overrides: Partial<HitHistoryItem> = {}): HitHistoryItem => ({
|
||||
id: overrides.id ?? 'hit-id',
|
||||
question: overrides.question ?? 'query',
|
||||
match: overrides.match ?? 'match',
|
||||
response: overrides.response ?? 'response',
|
||||
source: overrides.source ?? 'source',
|
||||
score: overrides.score ?? 0.42,
|
||||
created_at: overrides.created_at ?? 1700000000,
|
||||
})
|
||||
|
||||
const renderComponent = (props?: Partial<React.ComponentProps<typeof ViewAnnotationModal>>) => {
|
||||
const item = createAnnotationItem()
|
||||
const mergedProps: React.ComponentProps<typeof ViewAnnotationModal> = {
|
||||
appId: 'app-id',
|
||||
isShow: true,
|
||||
onHide: jest.fn(),
|
||||
item,
|
||||
onSave: jest.fn().mockResolvedValue(undefined),
|
||||
onRemove: jest.fn().mockResolvedValue(undefined),
|
||||
...props,
|
||||
}
|
||||
return {
|
||||
...render(<ViewAnnotationModal {...mergedProps} />),
|
||||
props: mergedProps,
|
||||
}
|
||||
}
|
||||
|
||||
describe('ViewAnnotationModal', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
fetchHitHistoryListMock.mockResolvedValue({ data: [], total: 0 })
|
||||
})
|
||||
|
||||
it('should render annotation tab and allow saving updated content', async () => {
|
||||
const { props } = renderComponent()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchHitHistoryListMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-query'))
|
||||
await waitFor(() => {
|
||||
expect(props.onSave).toHaveBeenCalledWith('query-updated', props.item.answer)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-answer'))
|
||||
await waitFor(() => {
|
||||
expect(props.onSave).toHaveBeenCalledWith(props.item.question, 'answer-updated')
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('appAnnotation.viewModal.hitHistory'))
|
||||
expect(await screen.findByText('appAnnotation.viewModal.noHitHistory')).toBeInTheDocument()
|
||||
expect(mockFormatTime).toHaveBeenCalledWith(props.item.created_at, 'appLog.dateTimeFormat')
|
||||
})
|
||||
|
||||
it('should render hit history entries with pagination badge when data exists', async () => {
|
||||
const hits = [createHitHistoryItem({ question: 'user input' }), createHitHistoryItem({ id: 'hit-2', question: 'second' })]
|
||||
fetchHitHistoryListMock.mockResolvedValue({ data: hits, total: 15 })
|
||||
|
||||
renderComponent()
|
||||
|
||||
fireEvent.click(await screen.findByText('appAnnotation.viewModal.hitHistory'))
|
||||
|
||||
expect(await screen.findByText('user input')).toBeInTheDocument()
|
||||
expect(screen.getByText('15 appAnnotation.viewModal.hits')).toBeInTheDocument()
|
||||
expect(mockFormatTime).toHaveBeenCalledWith(hits[0].created_at, 'appLog.dateTimeFormat')
|
||||
})
|
||||
|
||||
it('should confirm before removing the annotation and hide on success', async () => {
|
||||
const { props } = renderComponent()
|
||||
|
||||
fireEvent.click(screen.getByText('appAnnotation.editModal.removeThisCache'))
|
||||
expect(await screen.findByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument()
|
||||
|
||||
const confirmButton = await screen.findByRole('button', { name: 'common.operation.confirm' })
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onRemove).toHaveBeenCalledTimes(1)
|
||||
expect(props.onHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user