mirror of https://github.com/langgenius/dify.git
perf: improve Jest caching and configuration in web tests (#29881)
This commit is contained in:
parent
95a2b3d088
commit
80f11471ae
|
|
@ -35,6 +35,14 @@ jobs:
|
|||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
- name: Restore Jest cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: web/.cache/jest
|
||||
key: ${{ runner.os }}-jest-${{ hashFiles('web/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-jest-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
|
@ -45,7 +53,7 @@ jobs:
|
|||
run: |
|
||||
pnpm exec jest \
|
||||
--ci \
|
||||
--runInBand \
|
||||
--maxWorkers=100% \
|
||||
--coverage \
|
||||
--passWithNoTests
|
||||
|
||||
|
|
|
|||
|
|
@ -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,9 +52,15 @@ const EditItem: FC<Props> = ({
|
|||
}, [content])
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await onSave(newContent)
|
||||
setIsEdit(false)
|
||||
}
|
||||
catch {
|
||||
// Keep edit mode open when save fails
|
||||
// Error notification is handled by the parent component
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setNewContent('')
|
||||
|
|
@ -96,9 +102,16 @@ const EditItem: FC<Props> = ({
|
|||
<div className='mr-2'>·</div>
|
||||
<div
|
||||
className='flex cursor-pointer items-center space-x-1'
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
try {
|
||||
await onSave(content)
|
||||
// Only update UI state after successful delete
|
||||
setNewContent(content)
|
||||
onSave(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,8 +420,7 @@ describe('EditAnnotationModal', () => {
|
|||
// Mock API failure
|
||||
mockAddAnnotation.mockRejectedValueOnce(new Error('API Error'))
|
||||
|
||||
// Act & Assert - Should handle API error without crashing
|
||||
expect(async () => {
|
||||
// Act
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
|
||||
// Find and click edit link for query
|
||||
|
|
@ -437,12 +436,59 @@ describe('EditAnnotationModal', () => {
|
|||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
|
||||
// Should not call onAdded on error
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'API Error',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
expect(mockOnAdded).not.toHaveBeenCalled()
|
||||
}).not.toThrow()
|
||||
|
||||
// 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,8 +502,7 @@ describe('EditAnnotationModal', () => {
|
|||
// Mock API failure
|
||||
mockEditAnnotation.mockRejectedValueOnce(new Error('API Error'))
|
||||
|
||||
// Act & Assert - Should handle API error without crashing
|
||||
expect(async () => {
|
||||
// Act
|
||||
render(<EditAnnotationModal {...props} />)
|
||||
|
||||
// Edit query content
|
||||
|
|
@ -471,9 +516,58 @@ describe('EditAnnotationModal', () => {
|
|||
const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveButton)
|
||||
|
||||
// Should not call onEdited on error
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
message: 'API Error',
|
||||
type: 'error',
|
||||
})
|
||||
})
|
||||
expect(mockOnEdited).not.toHaveBeenCalled()
|
||||
}).not.toThrow()
|
||||
|
||||
// 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,6 +53,7 @@ const EditAnnotationModal: FC<Props> = ({
|
|||
postQuery = editedContent
|
||||
else
|
||||
postAnswer = editedContent
|
||||
try {
|
||||
if (!isAdd) {
|
||||
await editAnnotation(appId, annotationId, {
|
||||
message_id: messageId,
|
||||
|
|
@ -62,12 +63,12 @@ const EditAnnotationModal: FC<Props> = ({
|
|||
onEdited(postQuery, postAnswer)
|
||||
}
|
||||
else {
|
||||
const res: any = await addAnnotation(appId, {
|
||||
const res = await addAnnotation(appId, {
|
||||
question: postQuery,
|
||||
answer: postAnswer,
|
||||
message_id: messageId,
|
||||
})
|
||||
onAdded(res.id, res.account?.name, postQuery, postAnswer)
|
||||
onAdded(res.id, res.account?.name ?? '', postQuery, postAnswer)
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
|
|
@ -75,6 +76,17 @@ const EditAnnotationModal: FC<Props> = ({
|
|||
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)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const translation = {
|
|||
saved: 'تم الحفظ',
|
||||
create: 'تم الإنشاء',
|
||||
remove: 'تمت الإزالة',
|
||||
actionFailed: 'فشل الإجراء',
|
||||
},
|
||||
operation: {
|
||||
create: 'إنشاء',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Gespeichert',
|
||||
create: 'Erstellt',
|
||||
remove: 'Entfernt',
|
||||
actionFailed: 'Aktion fehlgeschlagen',
|
||||
},
|
||||
operation: {
|
||||
create: 'Erstellen',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const translation = {
|
|||
api: {
|
||||
success: 'Success',
|
||||
actionSuccess: 'Action succeeded',
|
||||
actionFailed: 'Action failed',
|
||||
saved: 'Saved',
|
||||
create: 'Created',
|
||||
remove: 'Removed',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Guardado',
|
||||
create: 'Creado',
|
||||
remove: 'Eliminado',
|
||||
actionFailed: 'Acción fallida',
|
||||
},
|
||||
operation: {
|
||||
create: 'Crear',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'ذخیره شد',
|
||||
create: 'ایجاد شد',
|
||||
remove: 'حذف شد',
|
||||
actionFailed: 'عمل شکست خورد',
|
||||
},
|
||||
operation: {
|
||||
create: 'ایجاد',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Sauvegardé',
|
||||
create: 'Créé',
|
||||
remove: 'Supprimé',
|
||||
actionFailed: 'Action échouée',
|
||||
},
|
||||
operation: {
|
||||
create: 'Créer',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'सहेजा गया',
|
||||
create: 'बनाया गया',
|
||||
remove: 'हटाया गया',
|
||||
actionFailed: 'क्रिया विफल',
|
||||
},
|
||||
operation: {
|
||||
create: 'बनाएं',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const translation = {
|
|||
remove: 'Dihapus',
|
||||
actionSuccess: 'Aksi berhasil',
|
||||
create: 'Dibuat',
|
||||
actionFailed: 'Tindakan gagal',
|
||||
},
|
||||
operation: {
|
||||
setup: 'Setup',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Salvato',
|
||||
create: 'Creato',
|
||||
remove: 'Rimosso',
|
||||
actionFailed: 'Azione non riuscita',
|
||||
},
|
||||
operation: {
|
||||
create: 'Crea',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const translation = {
|
|||
saved: '保存済み',
|
||||
create: '作成済み',
|
||||
remove: '削除済み',
|
||||
actionFailed: 'アクションに失敗しました',
|
||||
},
|
||||
operation: {
|
||||
create: '作成',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: '저장됨',
|
||||
create: '생성됨',
|
||||
remove: '삭제됨',
|
||||
actionFailed: '작업 실패',
|
||||
},
|
||||
operation: {
|
||||
create: '생성',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Zapisane',
|
||||
create: 'Utworzono',
|
||||
remove: 'Usunięto',
|
||||
actionFailed: 'Akcja nie powiodła się',
|
||||
},
|
||||
operation: {
|
||||
create: 'Utwórz',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Salvo',
|
||||
create: 'Criado',
|
||||
remove: 'Removido',
|
||||
actionFailed: 'Ação falhou',
|
||||
},
|
||||
operation: {
|
||||
create: 'Criar',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Salvat',
|
||||
create: 'Creat',
|
||||
remove: 'Eliminat',
|
||||
actionFailed: 'Acțiunea a eșuat',
|
||||
},
|
||||
operation: {
|
||||
create: 'Creează',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Сохранено',
|
||||
create: 'Создано',
|
||||
remove: 'Удалено',
|
||||
actionFailed: 'Действие не удалось',
|
||||
},
|
||||
operation: {
|
||||
create: 'Создать',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Shranjeno',
|
||||
create: 'Ustvarjeno',
|
||||
remove: 'Odstranjeno',
|
||||
actionFailed: 'Dejanje ni uspelo',
|
||||
},
|
||||
operation: {
|
||||
create: 'Ustvari',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'บันทึก',
|
||||
create: 'สร้าง',
|
||||
remove: 'ถูก เอา ออก',
|
||||
actionFailed: 'การดำเนินการล้มเหลว',
|
||||
},
|
||||
operation: {
|
||||
create: 'สร้าง',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Kaydedildi',
|
||||
create: 'Oluşturuldu',
|
||||
remove: 'Kaldırıldı',
|
||||
actionFailed: 'İşlem başarısız',
|
||||
},
|
||||
operation: {
|
||||
create: 'Oluştur',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Збережено',
|
||||
create: 'Створено',
|
||||
remove: 'Видалено',
|
||||
actionFailed: 'Не вдалося виконати дію',
|
||||
},
|
||||
operation: {
|
||||
create: 'Створити',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: 'Đã lưu',
|
||||
create: 'Tạo',
|
||||
remove: 'Xóa',
|
||||
actionFailed: 'Thao tác thất bại',
|
||||
},
|
||||
operation: {
|
||||
create: 'Tạo mới',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const translation = {
|
|||
saved: '已保存',
|
||||
create: '已创建',
|
||||
remove: '已移除',
|
||||
actionFailed: '操作失败',
|
||||
},
|
||||
operation: {
|
||||
create: '创建',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const translation = {
|
|||
saved: '已儲存',
|
||||
create: '已建立',
|
||||
remove: '已移除',
|
||||
actionFailed: '操作失敗',
|
||||
},
|
||||
operation: {
|
||||
create: '建立',
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const config: Config = {
|
|||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/9c/7gly5yl90qxdjljqsvkk758h0000gn/T/jest_dx",
|
||||
cacheDirectory: '<rootDir>/.cache/jest',
|
||||
|
||||
// Automatically clear mock calls, instances, contexts and results before every test
|
||||
clearMocks: true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Fetcher } from 'swr'
|
||||
import { del, get, post } from './base'
|
||||
import type { AnnotationEnableStatus, AnnotationItemBasic, EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
import type { AnnotationCreateResponse, AnnotationEnableStatus, AnnotationItemBasic, EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
import { ANNOTATION_DEFAULT } from '@/config'
|
||||
|
||||
export const fetchAnnotationConfig = (appId: string) => {
|
||||
|
|
@ -41,7 +41,7 @@ export const fetchExportAnnotationList = (appId: string) => {
|
|||
}
|
||||
|
||||
export const addAnnotation = (appId: string, body: AnnotationItemBasic) => {
|
||||
return post(`apps/${appId}/annotations`, { body })
|
||||
return post<AnnotationCreateResponse>(`apps/${appId}/annotations`, { body })
|
||||
}
|
||||
|
||||
export const annotationBatchImport: Fetcher<{ job_id: string; job_status: string }, { url: string; body: FormData }> = ({ url, body }) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue