mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
perf: improve Jest caching and configuration in web tests (#29881)
This commit is contained in:
@@ -245,7 +245,7 @@ describe('EditItem', () => {
|
||||
expect(mockSave).toHaveBeenCalledWith('Test save content')
|
||||
})
|
||||
|
||||
it('should show delete option when content changes', async () => {
|
||||
it('should show delete option and restore original content when delete is clicked', async () => {
|
||||
// Arrange
|
||||
const mockSave = jest.fn().mockResolvedValue(undefined)
|
||||
const props = {
|
||||
@@ -267,7 +267,13 @@ describe('EditItem', () => {
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
// Assert
|
||||
expect(mockSave).toHaveBeenCalledWith('Modified content')
|
||||
expect(mockSave).toHaveBeenNthCalledWith(1, 'Modified content')
|
||||
expect(await screen.findByText('common.operation.delete')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('common.operation.delete'))
|
||||
|
||||
expect(mockSave).toHaveBeenNthCalledWith(2, 'Test content')
|
||||
expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle keyboard interactions in edit mode', async () => {
|
||||
@@ -393,5 +399,68 @@ describe('EditItem', () => {
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle save failure gracefully in edit mode', async () => {
|
||||
// Arrange
|
||||
const mockSave = jest.fn().mockRejectedValueOnce(new Error('Save failed'))
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onSave: mockSave,
|
||||
}
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
render(<EditItem {...props} />)
|
||||
|
||||
// Enter edit mode and save (should fail)
|
||||
await user.click(screen.getByText('common.operation.edit'))
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.type(textarea, 'New content')
|
||||
|
||||
// Save should fail but not throw
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
// Assert - Should remain in edit mode when save fails
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument()
|
||||
expect(mockSave).toHaveBeenCalledWith('New content')
|
||||
})
|
||||
|
||||
it('should handle delete action failure gracefully', async () => {
|
||||
// Arrange
|
||||
const mockSave = jest.fn()
|
||||
.mockResolvedValueOnce(undefined) // First save succeeds
|
||||
.mockRejectedValueOnce(new Error('Delete failed')) // Delete fails
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onSave: mockSave,
|
||||
}
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
render(<EditItem {...props} />)
|
||||
|
||||
// Edit content to show delete button
|
||||
await user.click(screen.getByText('common.operation.edit'))
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'Modified content')
|
||||
|
||||
// Save to create new content
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
await screen.findByText('common.operation.delete')
|
||||
|
||||
// Click delete (should fail but not throw)
|
||||
await user.click(screen.getByText('common.operation.delete'))
|
||||
|
||||
// Assert - Delete action should handle error gracefully
|
||||
expect(mockSave).toHaveBeenCalledTimes(2)
|
||||
expect(mockSave).toHaveBeenNthCalledWith(1, 'Modified content')
|
||||
expect(mockSave).toHaveBeenNthCalledWith(2, 'Test content')
|
||||
|
||||
// When delete fails, the delete button should still be visible (state not changed)
|
||||
expect(screen.getByText('common.operation.delete')).toBeInTheDocument()
|
||||
expect(screen.getByText('Modified content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,8 +52,14 @@ const EditItem: FC<Props> = ({
|
||||
}, [content])
|
||||
|
||||
const handleSave = async () => {
|
||||
await onSave(newContent)
|
||||
setIsEdit(false)
|
||||
try {
|
||||
await onSave(newContent)
|
||||
setIsEdit(false)
|
||||
}
|
||||
catch {
|
||||
// Keep edit mode open when save fails
|
||||
// Error notification is handled by the parent component
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
@@ -96,9 +102,16 @@ const EditItem: FC<Props> = ({
|
||||
<div className='mr-2'>·</div>
|
||||
<div
|
||||
className='flex cursor-pointer items-center space-x-1'
|
||||
onClick={() => {
|
||||
setNewContent(content)
|
||||
onSave(content)
|
||||
onClick={async () => {
|
||||
try {
|
||||
await onSave(content)
|
||||
// Only update UI state after successful delete
|
||||
setNewContent(content)
|
||||
}
|
||||
catch {
|
||||
// Delete action failed - error is already handled by parent
|
||||
// UI state remains unchanged, user can retry
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Toast, { type IToastProps, type ToastHandle } from '@/app/components/base/toast'
|
||||
import EditAnnotationModal from './index'
|
||||
@@ -408,7 +408,7 @@ describe('EditAnnotationModal', () => {
|
||||
|
||||
// Error Handling (CRITICAL for coverage)
|
||||
describe('Error Handling', () => {
|
||||
it('should handle addAnnotation API failure gracefully', async () => {
|
||||
it('should show error toast and skip callbacks when addAnnotation fails', async () => {
|
||||
// Arrange
|
||||
const mockOnAdded = jest.fn()
|
||||
const props = {
|
||||
@@ -420,29 +420,75 @@ describe('EditAnnotationModal', () => {
|
||||
// Mock API failure
|
||||
mockAddAnnotation.mockRejectedValueOnce(new Error('API Error'))
|
||||
|
||||
// Act & Assert - Should handle API error without crashing
|
||||
expect(async () => {
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
// Act
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
|
||||
// Find and click edit link for query
|
||||
const editLinks = screen.getAllByText(/common\.operation\.edit/i)
|
||||
await user.click(editLinks[0])
|
||||
// Find and click edit link for query
|
||||
const editLinks = screen.getAllByText(/common\.operation\.edit/i)
|
||||
await user.click(editLinks[0])
|
||||
|
||||
// Find textarea and enter new content
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'New query content')
|
||||
// Find textarea and enter new content
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'New query content')
|
||||
|
||||
// Click save button
|
||||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
// Click save button
|
||||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
|
||||
// Should not call onAdded on error
|
||||
expect(mockOnAdded).not.toHaveBeenCalled()
|
||||
}).not.toThrow()
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'API Error',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
expect(mockOnAdded).not.toHaveBeenCalled()
|
||||
|
||||
// Verify edit mode remains open (textarea should still be visible)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle editAnnotation API failure gracefully', async () => {
|
||||
it('should show fallback error message when addAnnotation error has no message', async () => {
|
||||
// Arrange
|
||||
const mockOnAdded = jest.fn()
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onAdded: mockOnAdded,
|
||||
}
|
||||
const user = userEvent.setup()
|
||||
|
||||
mockAddAnnotation.mockRejectedValueOnce({})
|
||||
|
||||
// Act
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
|
||||
const editLinks = screen.getAllByText(/common\.operation\.edit/i)
|
||||
await user.click(editLinks[0])
|
||||
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'New query content')
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'common.api.actionFailed',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
expect(mockOnAdded).not.toHaveBeenCalled()
|
||||
|
||||
// Verify edit mode remains open (textarea should still be visible)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show error toast and skip callbacks when editAnnotation fails', async () => {
|
||||
// Arrange
|
||||
const mockOnEdited = jest.fn()
|
||||
const props = {
|
||||
@@ -456,24 +502,72 @@ describe('EditAnnotationModal', () => {
|
||||
// Mock API failure
|
||||
mockEditAnnotation.mockRejectedValueOnce(new Error('API Error'))
|
||||
|
||||
// Act & Assert - Should handle API error without crashing
|
||||
expect(async () => {
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
// Act
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
|
||||
// Edit query content
|
||||
const editLinks = screen.getAllByText(/common\.operation\.edit/i)
|
||||
await user.click(editLinks[0])
|
||||
// Edit query content
|
||||
const editLinks = screen.getAllByText(/common\.operation\.edit/i)
|
||||
await user.click(editLinks[0])
|
||||
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'Modified query')
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'Modified query')
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
|
||||
// Should not call onEdited on error
|
||||
expect(mockOnEdited).not.toHaveBeenCalled()
|
||||
}).not.toThrow()
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'API Error',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
expect(mockOnEdited).not.toHaveBeenCalled()
|
||||
|
||||
// Verify edit mode remains open (textarea should still be visible)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show fallback error message when editAnnotation error is not an Error instance', async () => {
|
||||
// Arrange
|
||||
const mockOnEdited = jest.fn()
|
||||
const props = {
|
||||
...defaultProps,
|
||||
annotationId: 'test-annotation-id',
|
||||
messageId: 'test-message-id',
|
||||
onEdited: mockOnEdited,
|
||||
}
|
||||
const user = userEvent.setup()
|
||||
|
||||
mockEditAnnotation.mockRejectedValueOnce('oops')
|
||||
|
||||
// Act
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
|
||||
const editLinks = screen.getAllByText(/common\.operation\.edit/i)
|
||||
await user.click(editLinks[0])
|
||||
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'Modified query')
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'common.api.actionFailed',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
expect(mockOnEdited).not.toHaveBeenCalled()
|
||||
|
||||
// Verify edit mode remains open (textarea should still be visible)
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -526,25 +620,33 @@ describe('EditAnnotationModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Toast Notifications (Simplified)
|
||||
// Toast Notifications (Success)
|
||||
describe('Toast Notifications', () => {
|
||||
it('should trigger success notification when save operation completes', async () => {
|
||||
it('should show success notification when save operation completes', async () => {
|
||||
// Arrange
|
||||
const mockOnAdded = jest.fn()
|
||||
const props = {
|
||||
...defaultProps,
|
||||
onAdded: mockOnAdded,
|
||||
}
|
||||
const props = { ...defaultProps }
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
|
||||
// Simulate successful save by calling handleSave indirectly
|
||||
const mockSave = jest.fn()
|
||||
expect(mockSave).not.toHaveBeenCalled()
|
||||
const editLinks = screen.getAllByText(/common\.operation\.edit/i)
|
||||
await user.click(editLinks[0])
|
||||
|
||||
// Assert - Toast spy is available and will be called during real save operations
|
||||
expect(toastNotifySpy).toBeDefined()
|
||||
const textarea = screen.getByRole('textbox')
|
||||
await user.clear(textarea)
|
||||
await user.type(textarea, 'Updated query')
|
||||
|
||||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'common.api.actionSuccess',
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -53,27 +53,39 @@ const EditAnnotationModal: FC<Props> = ({
|
||||
postQuery = editedContent
|
||||
else
|
||||
postAnswer = editedContent
|
||||
if (!isAdd) {
|
||||
await editAnnotation(appId, annotationId, {
|
||||
message_id: messageId,
|
||||
question: postQuery,
|
||||
answer: postAnswer,
|
||||
})
|
||||
onEdited(postQuery, postAnswer)
|
||||
}
|
||||
else {
|
||||
const res: any = await addAnnotation(appId, {
|
||||
question: postQuery,
|
||||
answer: postAnswer,
|
||||
message_id: messageId,
|
||||
})
|
||||
onAdded(res.id, res.account?.name, postQuery, postAnswer)
|
||||
}
|
||||
try {
|
||||
if (!isAdd) {
|
||||
await editAnnotation(appId, annotationId, {
|
||||
message_id: messageId,
|
||||
question: postQuery,
|
||||
answer: postAnswer,
|
||||
})
|
||||
onEdited(postQuery, postAnswer)
|
||||
}
|
||||
else {
|
||||
const res = await addAnnotation(appId, {
|
||||
question: postQuery,
|
||||
answer: postAnswer,
|
||||
message_id: messageId,
|
||||
})
|
||||
onAdded(res.id, res.account?.name ?? '', postQuery, postAnswer)
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess') as string,
|
||||
type: 'success',
|
||||
})
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess') as string,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
const fallbackMessage = t('common.api.actionFailed') as string
|
||||
const message = error instanceof Error && error.message ? error.message : fallbackMessage
|
||||
Toast.notify({
|
||||
message,
|
||||
type: 'error',
|
||||
})
|
||||
// Re-throw to preserve edit mode behavior for UI components
|
||||
throw error
|
||||
}
|
||||
}
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as React from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import type { ComponentProps } from 'react'
|
||||
@@ -7,6 +8,120 @@ import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation'
|
||||
|
||||
jest.mock('@headlessui/react', () => {
|
||||
type PopoverContextValue = { open: boolean; setOpen: (open: boolean) => void }
|
||||
type MenuContextValue = { open: boolean; setOpen: (open: boolean) => void }
|
||||
const PopoverContext = React.createContext<PopoverContextValue | null>(null)
|
||||
const MenuContext = React.createContext<MenuContextValue | null>(null)
|
||||
|
||||
const Popover = ({ children }: { children: React.ReactNode | ((props: { open: boolean }) => React.ReactNode) }) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const value = React.useMemo(() => ({ open, setOpen }), [open])
|
||||
return (
|
||||
<PopoverContext.Provider value={value}>
|
||||
{typeof children === 'function' ? children({ open }) : children}
|
||||
</PopoverContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const PopoverButton = React.forwardRef(({ onClick, children, ...props }: { onClick?: () => void; children?: React.ReactNode }, ref: React.Ref<HTMLButtonElement>) => {
|
||||
const context = React.useContext(PopoverContext)
|
||||
const handleClick = () => {
|
||||
context?.setOpen(!context.open)
|
||||
onClick?.()
|
||||
}
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
aria-expanded={context?.open ?? false}
|
||||
onClick={handleClick}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
const PopoverPanel = React.forwardRef(({ children, ...props }: { children: React.ReactNode | ((props: { close: () => void }) => React.ReactNode) }, ref: React.Ref<HTMLDivElement>) => {
|
||||
const context = React.useContext(PopoverContext)
|
||||
if (!context?.open) return null
|
||||
const content = typeof children === 'function' ? children({ close: () => context.setOpen(false) }) : children
|
||||
return (
|
||||
<div ref={ref} {...props}>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const Menu = ({ children }: { children: React.ReactNode }) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const value = React.useMemo(() => ({ open, setOpen }), [open])
|
||||
return (
|
||||
<MenuContext.Provider value={value}>
|
||||
{children}
|
||||
</MenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const MenuButton = ({ onClick, children, ...props }: { onClick?: () => void; children?: React.ReactNode }) => {
|
||||
const context = React.useContext(MenuContext)
|
||||
const handleClick = () => {
|
||||
context?.setOpen(!context.open)
|
||||
onClick?.()
|
||||
}
|
||||
return (
|
||||
<button type="button" aria-expanded={context?.open ?? false} onClick={handleClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const MenuItems = ({ children, ...props }: { children: React.ReactNode }) => {
|
||||
const context = React.useContext(MenuContext)
|
||||
if (!context?.open) return null
|
||||
return (
|
||||
<div {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
Dialog: ({ open, children, className }: { open?: boolean; children: React.ReactNode; className?: string }) => {
|
||||
if (open === false) return null
|
||||
return (
|
||||
<div role="dialog" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
DialogBackdrop: ({ children, className, onClick }: { children?: React.ReactNode; className?: string; onClick?: () => void }) => (
|
||||
<div className={className} onClick={onClick}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogPanel: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogTitle: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItems,
|
||||
Transition: ({ show = true, children }: { show?: boolean; children: React.ReactNode }) => (show ? <>{children}</> : null),
|
||||
TransitionChild: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}
|
||||
})
|
||||
|
||||
let lastCSVDownloaderProps: Record<string, unknown> | undefined
|
||||
const mockCSVDownloader = jest.fn(({ children, ...props }) => {
|
||||
lastCSVDownloaderProps = props
|
||||
@@ -121,6 +236,7 @@ const mockedClearAllAnnotations = jest.mocked(clearAllAnnotations)
|
||||
describe('HeaderOptions', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.useRealTimers()
|
||||
mockCSVDownloader.mockClear()
|
||||
lastCSVDownloaderProps = undefined
|
||||
mockedFetchAnnotations.mockResolvedValue({ data: [] })
|
||||
|
||||
@@ -12,6 +12,12 @@ export type AnnotationItem = {
|
||||
hit_count: number
|
||||
}
|
||||
|
||||
export type AnnotationCreateResponse = AnnotationItem & {
|
||||
account?: {
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type HitHistoryItem = {
|
||||
id: string
|
||||
question: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ParamsConfig from './index'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import type { DatasetConfigs } from '@/models/debug'
|
||||
@@ -11,6 +12,37 @@ import {
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
jest.mock('@headlessui/react', () => ({
|
||||
Dialog: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
||||
<div role="dialog" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogPanel: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DialogTitle: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => (
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Transition: ({ show, children }: { show: boolean; children: React.ReactNode }) => (show ? <>{children}</> : null),
|
||||
TransitionChild: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
Switch: ({ checked, onChange, children, ...props }: { checked: boolean; onChange?: (value: boolean) => void; children?: React.ReactNode }) => (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => onChange?.(!checked)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel: jest.fn(),
|
||||
useCurrentProviderAndModel: jest.fn(),
|
||||
@@ -74,9 +106,6 @@ const renderParamsConfig = ({
|
||||
initialModalOpen?: boolean
|
||||
disabled?: boolean
|
||||
} = {}) => {
|
||||
const setDatasetConfigsSpy = jest.fn<void, [DatasetConfigs]>()
|
||||
const setModalOpenSpy = jest.fn<void, [boolean]>()
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const [datasetConfigsState, setDatasetConfigsState] = React.useState(datasetConfigs)
|
||||
const [modalOpen, setModalOpen] = React.useState(initialModalOpen)
|
||||
@@ -84,12 +113,10 @@ const renderParamsConfig = ({
|
||||
const contextValue = {
|
||||
datasetConfigs: datasetConfigsState,
|
||||
setDatasetConfigs: (next: DatasetConfigs) => {
|
||||
setDatasetConfigsSpy(next)
|
||||
setDatasetConfigsState(next)
|
||||
},
|
||||
rerankSettingModalOpen: modalOpen,
|
||||
setRerankSettingModalOpen: (open: boolean) => {
|
||||
setModalOpenSpy(open)
|
||||
setModalOpen(open)
|
||||
},
|
||||
} as unknown as React.ComponentProps<typeof ConfigContext.Provider>['value']
|
||||
@@ -101,18 +128,13 @@ const renderParamsConfig = ({
|
||||
)
|
||||
}
|
||||
|
||||
render(
|
||||
return render(
|
||||
<ParamsConfig
|
||||
disabled={disabled}
|
||||
selectedDatasets={[]}
|
||||
/>,
|
||||
{ wrapper: Wrapper },
|
||||
)
|
||||
|
||||
return {
|
||||
setDatasetConfigsSpy,
|
||||
setModalOpenSpy,
|
||||
}
|
||||
}
|
||||
|
||||
describe('dataset-config/params-config', () => {
|
||||
@@ -151,77 +173,92 @@ describe('dataset-config/params-config', () => {
|
||||
describe('User Interactions', () => {
|
||||
it('should open modal and persist changes when save is clicked', async () => {
|
||||
// Arrange
|
||||
const { setDatasetConfigsSpy } = renderParamsConfig()
|
||||
renderParamsConfig()
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
|
||||
const dialogScope = within(dialog)
|
||||
|
||||
// Change top_k via the first number input increment control.
|
||||
const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' })
|
||||
fireEvent.click(incrementButtons[0])
|
||||
await user.click(incrementButtons[0])
|
||||
|
||||
const saveButton = await dialogScope.findByRole('button', { name: 'common.operation.save' })
|
||||
fireEvent.click(saveButton)
|
||||
await waitFor(() => {
|
||||
const [topKInput] = dialogScope.getAllByRole('spinbutton')
|
||||
expect(topKInput).toHaveValue(5)
|
||||
})
|
||||
|
||||
await user.click(dialogScope.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
// Assert
|
||||
expect(setDatasetConfigsSpy).toHaveBeenCalledWith(expect.objectContaining({ top_k: 5 }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
const reopenedDialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
|
||||
const reopenedScope = within(reopenedDialog)
|
||||
const [reopenedTopKInput] = reopenedScope.getAllByRole('spinbutton')
|
||||
|
||||
// Assert
|
||||
expect(reopenedTopKInput).toHaveValue(5)
|
||||
})
|
||||
|
||||
it('should discard changes when cancel is clicked', async () => {
|
||||
// Arrange
|
||||
const { setDatasetConfigsSpy } = renderParamsConfig()
|
||||
renderParamsConfig()
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
|
||||
const dialogScope = within(dialog)
|
||||
|
||||
const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' })
|
||||
fireEvent.click(incrementButtons[0])
|
||||
await user.click(incrementButtons[0])
|
||||
|
||||
await waitFor(() => {
|
||||
const [topKInput] = dialogScope.getAllByRole('spinbutton')
|
||||
expect(topKInput).toHaveValue(5)
|
||||
})
|
||||
|
||||
const cancelButton = await dialogScope.findByRole('button', { name: 'common.operation.cancel' })
|
||||
fireEvent.click(cancelButton)
|
||||
await user.click(cancelButton)
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Re-open and save without changes.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
// Re-open and verify the original value remains.
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
const reopenedDialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
|
||||
const reopenedScope = within(reopenedDialog)
|
||||
const reopenedSave = await reopenedScope.findByRole('button', { name: 'common.operation.save' })
|
||||
fireEvent.click(reopenedSave)
|
||||
const [reopenedTopKInput] = reopenedScope.getAllByRole('spinbutton')
|
||||
|
||||
// Assert - should save original top_k rather than the canceled change.
|
||||
expect(setDatasetConfigsSpy).toHaveBeenCalledWith(expect.objectContaining({ top_k: 4 }))
|
||||
// Assert
|
||||
expect(reopenedTopKInput).toHaveValue(4)
|
||||
})
|
||||
|
||||
it('should prevent saving when rerank model is required but invalid', async () => {
|
||||
// Arrange
|
||||
const { setDatasetConfigsSpy } = renderParamsConfig({
|
||||
renderParamsConfig({
|
||||
datasetConfigs: createDatasetConfigs({
|
||||
reranking_enable: true,
|
||||
reranking_mode: RerankingModeEnum.RerankingModel,
|
||||
}),
|
||||
initialModalOpen: true,
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
|
||||
// Act
|
||||
const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
|
||||
const dialogScope = within(dialog)
|
||||
fireEvent.click(dialogScope.getByRole('button', { name: 'common.operation.save' }))
|
||||
await user.click(dialogScope.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
// Assert
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'appDebug.datasetConfig.rerankModelRequired',
|
||||
})
|
||||
expect(setDatasetConfigsSpy).not.toHaveBeenCalled()
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ const AnnotationCtrlButton: FC<Props> = ({
|
||||
setShowAnnotationFullModal()
|
||||
return
|
||||
}
|
||||
const res: any = await addAnnotation(appId, {
|
||||
const res = await addAnnotation(appId, {
|
||||
message_id: messageId,
|
||||
question: query,
|
||||
answer,
|
||||
@@ -50,7 +50,7 @@ const AnnotationCtrlButton: FC<Props> = ({
|
||||
message: t('common.api.actionSuccess') as string,
|
||||
type: 'success',
|
||||
})
|
||||
onAdded(res.id, res.account?.name)
|
||||
onAdded(res.id, res.account?.name ?? '')
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user