perf: improve Jest caching and configuration in web tests (#29881)

This commit is contained in:
yyh 2025-12-19 12:00:46 +08:00 committed by GitHub
parent 95a2b3d088
commit 80f11471ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 496 additions and 111 deletions

View File

@ -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

View File

@ -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()
})
})
})

View File

@ -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'>

View File

@ -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',
})
})
})
})

View File

@ -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)

View File

@ -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: [] })

View File

@ -12,6 +12,12 @@ export type AnnotationItem = {
hit_count: number
}
export type AnnotationCreateResponse = AnnotationItem & {
account?: {
name?: string
}
}
export type HitHistoryItem = {
id: string
question: string

View File

@ -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()
})
})

View File

@ -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 (

View File

@ -11,6 +11,7 @@ const translation = {
saved: 'تم الحفظ',
create: 'تم الإنشاء',
remove: 'تمت الإزالة',
actionFailed: 'فشل الإجراء',
},
operation: {
create: 'إنشاء',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Gespeichert',
create: 'Erstellt',
remove: 'Entfernt',
actionFailed: 'Aktion fehlgeschlagen',
},
operation: {
create: 'Erstellen',

View File

@ -8,6 +8,7 @@ const translation = {
api: {
success: 'Success',
actionSuccess: 'Action succeeded',
actionFailed: 'Action failed',
saved: 'Saved',
create: 'Created',
remove: 'Removed',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Guardado',
create: 'Creado',
remove: 'Eliminado',
actionFailed: 'Acción fallida',
},
operation: {
create: 'Crear',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'ذخیره شد',
create: 'ایجاد شد',
remove: 'حذف شد',
actionFailed: 'عمل شکست خورد',
},
operation: {
create: 'ایجاد',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Sauvegardé',
create: 'Créé',
remove: 'Supprimé',
actionFailed: 'Action échouée',
},
operation: {
create: 'Créer',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'सहेजा गया',
create: 'बनाया गया',
remove: 'हटाया गया',
actionFailed: 'क्रिया विफल',
},
operation: {
create: 'बनाएं',

View File

@ -11,6 +11,7 @@ const translation = {
remove: 'Dihapus',
actionSuccess: 'Aksi berhasil',
create: 'Dibuat',
actionFailed: 'Tindakan gagal',
},
operation: {
setup: 'Setup',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Salvato',
create: 'Creato',
remove: 'Rimosso',
actionFailed: 'Azione non riuscita',
},
operation: {
create: 'Crea',

View File

@ -11,6 +11,7 @@ const translation = {
saved: '保存済み',
create: '作成済み',
remove: '削除済み',
actionFailed: 'アクションに失敗しました',
},
operation: {
create: '作成',

View File

@ -5,6 +5,7 @@ const translation = {
saved: '저장됨',
create: '생성됨',
remove: '삭제됨',
actionFailed: '작업 실패',
},
operation: {
create: '생성',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Zapisane',
create: 'Utworzono',
remove: 'Usunięto',
actionFailed: 'Akcja nie powiodła się',
},
operation: {
create: 'Utwórz',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Salvo',
create: 'Criado',
remove: 'Removido',
actionFailed: 'Ação falhou',
},
operation: {
create: 'Criar',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Salvat',
create: 'Creat',
remove: 'Eliminat',
actionFailed: 'Acțiunea a eșuat',
},
operation: {
create: 'Creează',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Сохранено',
create: 'Создано',
remove: 'Удалено',
actionFailed: 'Действие не удалось',
},
operation: {
create: 'Создать',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Shranjeno',
create: 'Ustvarjeno',
remove: 'Odstranjeno',
actionFailed: 'Dejanje ni uspelo',
},
operation: {
create: 'Ustvari',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'บันทึก',
create: 'สร้าง',
remove: 'ถูก เอา ออก',
actionFailed: 'การดำเนินการล้มเหลว',
},
operation: {
create: 'สร้าง',

View File

@ -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',

View File

@ -5,6 +5,7 @@ const translation = {
saved: 'Збережено',
create: 'Створено',
remove: 'Видалено',
actionFailed: 'Не вдалося виконати дію',
},
operation: {
create: 'Створити',

View File

@ -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',

View File

@ -11,6 +11,7 @@ const translation = {
saved: '已保存',
create: '已创建',
remove: '已移除',
actionFailed: '操作失败',
},
operation: {
create: '创建',

View File

@ -5,6 +5,7 @@ const translation = {
saved: '已儲存',
create: '已建立',
remove: '已移除',
actionFailed: '操作失敗',
},
operation: {
create: '建立',

View File

@ -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,

View File

@ -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 }) => {