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
33 changed files with 496 additions and 111 deletions

View File

@@ -35,6 +35,14 @@ jobs:
cache: pnpm cache: pnpm
cache-dependency-path: ./web/pnpm-lock.yaml 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 - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -45,7 +53,7 @@ jobs:
run: | run: |
pnpm exec jest \ pnpm exec jest \
--ci \ --ci \
--runInBand \ --maxWorkers=100% \
--coverage \ --coverage \
--passWithNoTests --passWithNoTests

View File

@@ -245,7 +245,7 @@ describe('EditItem', () => {
expect(mockSave).toHaveBeenCalledWith('Test save content') 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 // Arrange
const mockSave = jest.fn().mockResolvedValue(undefined) const mockSave = jest.fn().mockResolvedValue(undefined)
const props = { const props = {
@@ -267,7 +267,13 @@ describe('EditItem', () => {
await user.click(screen.getByRole('button', { name: 'common.operation.save' })) await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
// Assert // 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 () => { it('should handle keyboard interactions in edit mode', async () => {
@@ -393,5 +399,68 @@ describe('EditItem', () => {
expect(screen.queryByRole('textbox')).not.toBeInTheDocument() expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
expect(screen.getByText('Test content')).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]) }, [content])
const handleSave = async () => { const handleSave = async () => {
await onSave(newContent) try {
setIsEdit(false) await onSave(newContent)
setIsEdit(false)
}
catch {
// Keep edit mode open when save fails
// Error notification is handled by the parent component
}
} }
const handleCancel = () => { const handleCancel = () => {
@@ -96,9 +102,16 @@ const EditItem: FC<Props> = ({
<div className='mr-2'>·</div> <div className='mr-2'>·</div>
<div <div
className='flex cursor-pointer items-center space-x-1' className='flex cursor-pointer items-center space-x-1'
onClick={() => { onClick={async () => {
setNewContent(content) try {
onSave(content) 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'> <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 userEvent from '@testing-library/user-event'
import Toast, { type IToastProps, type ToastHandle } from '@/app/components/base/toast' import Toast, { type IToastProps, type ToastHandle } from '@/app/components/base/toast'
import EditAnnotationModal from './index' import EditAnnotationModal from './index'
@@ -408,7 +408,7 @@ describe('EditAnnotationModal', () => {
// Error Handling (CRITICAL for coverage) // Error Handling (CRITICAL for coverage)
describe('Error Handling', () => { describe('Error Handling', () => {
it('should handle addAnnotation API failure gracefully', async () => { it('should show error toast and skip callbacks when addAnnotation fails', async () => {
// Arrange // Arrange
const mockOnAdded = jest.fn() const mockOnAdded = jest.fn()
const props = { const props = {
@@ -420,29 +420,75 @@ describe('EditAnnotationModal', () => {
// Mock API failure // Mock API failure
mockAddAnnotation.mockRejectedValueOnce(new Error('API Error')) mockAddAnnotation.mockRejectedValueOnce(new Error('API Error'))
// Act & Assert - Should handle API error without crashing // Act
expect(async () => { render(<EditAnnotationModal {...props} />)
render(<EditAnnotationModal {...props} />)
// Find and click edit link for query // Find and click edit link for query
const editLinks = screen.getAllByText(/common\.operation\.edit/i) const editLinks = screen.getAllByText(/common\.operation\.edit/i)
await user.click(editLinks[0]) await user.click(editLinks[0])
// Find textarea and enter new content // Find textarea and enter new content
const textarea = screen.getByRole('textbox') const textarea = screen.getByRole('textbox')
await user.clear(textarea) await user.clear(textarea)
await user.type(textarea, 'New query content') await user.type(textarea, 'New query content')
// Click save button // Click save button
const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
await user.click(saveButton) await user.click(saveButton)
// Should not call onAdded on error // Assert
expect(mockOnAdded).not.toHaveBeenCalled() await waitFor(() => {
}).not.toThrow() 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 // Arrange
const mockOnEdited = jest.fn() const mockOnEdited = jest.fn()
const props = { const props = {
@@ -456,24 +502,72 @@ describe('EditAnnotationModal', () => {
// Mock API failure // Mock API failure
mockEditAnnotation.mockRejectedValueOnce(new Error('API Error')) mockEditAnnotation.mockRejectedValueOnce(new Error('API Error'))
// Act & Assert - Should handle API error without crashing // Act
expect(async () => { render(<EditAnnotationModal {...props} />)
render(<EditAnnotationModal {...props} />)
// Edit query content // Edit query content
const editLinks = screen.getAllByText(/common\.operation\.edit/i) const editLinks = screen.getAllByText(/common\.operation\.edit/i)
await user.click(editLinks[0]) await user.click(editLinks[0])
const textarea = screen.getByRole('textbox') const textarea = screen.getByRole('textbox')
await user.clear(textarea) await user.clear(textarea)
await user.type(textarea, 'Modified query') await user.type(textarea, 'Modified query')
const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) const saveButton = screen.getByRole('button', { name: 'common.operation.save' })
await user.click(saveButton) await user.click(saveButton)
// Should not call onEdited on error // Assert
expect(mockOnEdited).not.toHaveBeenCalled() await waitFor(() => {
}).not.toThrow() 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', () => { describe('Toast Notifications', () => {
it('should trigger success notification when save operation completes', async () => { it('should show success notification when save operation completes', async () => {
// Arrange // Arrange
const mockOnAdded = jest.fn() const props = { ...defaultProps }
const props = { const user = userEvent.setup()
...defaultProps,
onAdded: mockOnAdded,
}
// Act // Act
render(<EditAnnotationModal {...props} />) render(<EditAnnotationModal {...props} />)
// Simulate successful save by calling handleSave indirectly const editLinks = screen.getAllByText(/common\.operation\.edit/i)
const mockSave = jest.fn() await user.click(editLinks[0])
expect(mockSave).not.toHaveBeenCalled()
// Assert - Toast spy is available and will be called during real save operations const textarea = screen.getByRole('textbox')
expect(toastNotifySpy).toBeDefined() 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 postQuery = editedContent
else else
postAnswer = editedContent postAnswer = editedContent
if (!isAdd) { try {
await editAnnotation(appId, annotationId, { if (!isAdd) {
message_id: messageId, await editAnnotation(appId, annotationId, {
question: postQuery, message_id: messageId,
answer: postAnswer, question: postQuery,
}) answer: postAnswer,
onEdited(postQuery, postAnswer) })
} onEdited(postQuery, postAnswer)
else { }
const res: any = await addAnnotation(appId, { else {
question: postQuery, const res = await addAnnotation(appId, {
answer: postAnswer, question: postQuery,
message_id: messageId, answer: postAnswer,
}) message_id: messageId,
onAdded(res.id, res.account?.name, postQuery, postAnswer) })
} onAdded(res.id, res.account?.name ?? '', postQuery, postAnswer)
}
Toast.notify({ Toast.notify({
message: t('common.api.actionSuccess') as string, message: t('common.api.actionSuccess') as string,
type: 'success', 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) 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 { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import type { ComponentProps } from 'react' import type { ComponentProps } from 'react'
@@ -7,6 +8,120 @@ import { LanguagesSupported } from '@/i18n-config/language'
import type { AnnotationItemBasic } from '../type' import type { AnnotationItemBasic } from '../type'
import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation' 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 let lastCSVDownloaderProps: Record<string, unknown> | undefined
const mockCSVDownloader = jest.fn(({ children, ...props }) => { const mockCSVDownloader = jest.fn(({ children, ...props }) => {
lastCSVDownloaderProps = props lastCSVDownloaderProps = props
@@ -121,6 +236,7 @@ const mockedClearAllAnnotations = jest.mocked(clearAllAnnotations)
describe('HeaderOptions', () => { describe('HeaderOptions', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
jest.useRealTimers()
mockCSVDownloader.mockClear() mockCSVDownloader.mockClear()
lastCSVDownloaderProps = undefined lastCSVDownloaderProps = undefined
mockedFetchAnnotations.mockResolvedValue({ data: [] }) mockedFetchAnnotations.mockResolvedValue({ data: [] })

View File

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

View File

@@ -1,5 +1,6 @@
import * as React from 'react' 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 ParamsConfig from './index'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import type { DatasetConfigs } from '@/models/debug' import type { DatasetConfigs } from '@/models/debug'
@@ -11,6 +12,37 @@ import {
useModelListAndDefaultModelAndCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel,
} from '@/app/components/header/account-setting/model-provider-page/hooks' } 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', () => ({ jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useModelListAndDefaultModelAndCurrentProviderAndModel: jest.fn(), useModelListAndDefaultModelAndCurrentProviderAndModel: jest.fn(),
useCurrentProviderAndModel: jest.fn(), useCurrentProviderAndModel: jest.fn(),
@@ -74,9 +106,6 @@ const renderParamsConfig = ({
initialModalOpen?: boolean initialModalOpen?: boolean
disabled?: boolean disabled?: boolean
} = {}) => { } = {}) => {
const setDatasetConfigsSpy = jest.fn<void, [DatasetConfigs]>()
const setModalOpenSpy = jest.fn<void, [boolean]>()
const Wrapper = ({ children }: { children: React.ReactNode }) => { const Wrapper = ({ children }: { children: React.ReactNode }) => {
const [datasetConfigsState, setDatasetConfigsState] = React.useState(datasetConfigs) const [datasetConfigsState, setDatasetConfigsState] = React.useState(datasetConfigs)
const [modalOpen, setModalOpen] = React.useState(initialModalOpen) const [modalOpen, setModalOpen] = React.useState(initialModalOpen)
@@ -84,12 +113,10 @@ const renderParamsConfig = ({
const contextValue = { const contextValue = {
datasetConfigs: datasetConfigsState, datasetConfigs: datasetConfigsState,
setDatasetConfigs: (next: DatasetConfigs) => { setDatasetConfigs: (next: DatasetConfigs) => {
setDatasetConfigsSpy(next)
setDatasetConfigsState(next) setDatasetConfigsState(next)
}, },
rerankSettingModalOpen: modalOpen, rerankSettingModalOpen: modalOpen,
setRerankSettingModalOpen: (open: boolean) => { setRerankSettingModalOpen: (open: boolean) => {
setModalOpenSpy(open)
setModalOpen(open) setModalOpen(open)
}, },
} as unknown as React.ComponentProps<typeof ConfigContext.Provider>['value'] } as unknown as React.ComponentProps<typeof ConfigContext.Provider>['value']
@@ -101,18 +128,13 @@ const renderParamsConfig = ({
) )
} }
render( return render(
<ParamsConfig <ParamsConfig
disabled={disabled} disabled={disabled}
selectedDatasets={[]} selectedDatasets={[]}
/>, />,
{ wrapper: Wrapper }, { wrapper: Wrapper },
) )
return {
setDatasetConfigsSpy,
setModalOpenSpy,
}
} }
describe('dataset-config/params-config', () => { describe('dataset-config/params-config', () => {
@@ -151,77 +173,92 @@ describe('dataset-config/params-config', () => {
describe('User Interactions', () => { describe('User Interactions', () => {
it('should open modal and persist changes when save is clicked', async () => { it('should open modal and persist changes when save is clicked', async () => {
// Arrange // Arrange
const { setDatasetConfigsSpy } = renderParamsConfig() renderParamsConfig()
const user = userEvent.setup()
// Act // 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 dialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
const dialogScope = within(dialog) const dialogScope = within(dialog)
// Change top_k via the first number input increment control.
const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' }) 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' }) await waitFor(() => {
fireEvent.click(saveButton) 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(() => { await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument() 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 () => { it('should discard changes when cancel is clicked', async () => {
// Arrange // Arrange
const { setDatasetConfigsSpy } = renderParamsConfig() renderParamsConfig()
const user = userEvent.setup()
// Act // 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 dialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
const dialogScope = within(dialog) const dialogScope = within(dialog)
const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' }) 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' }) const cancelButton = await dialogScope.findByRole('button', { name: 'common.operation.cancel' })
fireEvent.click(cancelButton) await user.click(cancelButton)
await waitFor(() => { await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument() expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
}) })
// Re-open and save without changes. // Re-open and verify the original value remains.
fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
const reopenedDialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) const reopenedDialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
const reopenedScope = within(reopenedDialog) const reopenedScope = within(reopenedDialog)
const reopenedSave = await reopenedScope.findByRole('button', { name: 'common.operation.save' }) const [reopenedTopKInput] = reopenedScope.getAllByRole('spinbutton')
fireEvent.click(reopenedSave)
// Assert - should save original top_k rather than the canceled change. // Assert
expect(setDatasetConfigsSpy).toHaveBeenCalledWith(expect.objectContaining({ top_k: 4 })) expect(reopenedTopKInput).toHaveValue(4)
}) })
it('should prevent saving when rerank model is required but invalid', async () => { it('should prevent saving when rerank model is required but invalid', async () => {
// Arrange // Arrange
const { setDatasetConfigsSpy } = renderParamsConfig({ renderParamsConfig({
datasetConfigs: createDatasetConfigs({ datasetConfigs: createDatasetConfigs({
reranking_enable: true, reranking_enable: true,
reranking_mode: RerankingModeEnum.RerankingModel, reranking_mode: RerankingModeEnum.RerankingModel,
}), }),
initialModalOpen: true, initialModalOpen: true,
}) })
const user = userEvent.setup()
// Act // Act
const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 })
const dialogScope = within(dialog) const dialogScope = within(dialog)
fireEvent.click(dialogScope.getByRole('button', { name: 'common.operation.save' })) await user.click(dialogScope.getByRole('button', { name: 'common.operation.save' }))
// Assert // Assert
expect(toastNotifySpy).toHaveBeenCalledWith({ expect(toastNotifySpy).toHaveBeenCalledWith({
type: 'error', type: 'error',
message: 'appDebug.datasetConfig.rerankModelRequired', message: 'appDebug.datasetConfig.rerankModelRequired',
}) })
expect(setDatasetConfigsSpy).not.toHaveBeenCalled()
expect(screen.getByRole('dialog')).toBeInTheDocument() expect(screen.getByRole('dialog')).toBeInTheDocument()
}) })
}) })

View File

@@ -41,7 +41,7 @@ const AnnotationCtrlButton: FC<Props> = ({
setShowAnnotationFullModal() setShowAnnotationFullModal()
return return
} }
const res: any = await addAnnotation(appId, { const res = await addAnnotation(appId, {
message_id: messageId, message_id: messageId,
question: query, question: query,
answer, answer,
@@ -50,7 +50,7 @@ const AnnotationCtrlButton: FC<Props> = ({
message: t('common.api.actionSuccess') as string, message: t('common.api.actionSuccess') as string,
type: 'success', type: 'success',
}) })
onAdded(res.id, res.account?.name) onAdded(res.id, res.account?.name ?? '')
} }
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ const translation = {
saved: 'Kaydedildi', saved: 'Kaydedildi',
create: 'Oluşturuldu', create: 'Oluşturuldu',
remove: 'Kaldırıldı', remove: 'Kaldırıldı',
actionFailed: 'İşlem başarısız',
}, },
operation: { operation: {
create: 'Oluştur', create: 'Oluştur',

View File

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

View File

@@ -5,6 +5,7 @@ const translation = {
saved: 'Đã lưu', saved: 'Đã lưu',
create: 'Tạo', create: 'Tạo',
remove: 'Xóa', remove: 'Xóa',
actionFailed: 'Thao tác thất bại',
}, },
operation: { operation: {
create: 'Tạo mới', create: 'Tạo mới',

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ const config: Config = {
// bail: 0, // bail: 0,
// The directory where Jest should store its cached dependency information // 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 // Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true, clearMocks: true,

View File

@@ -1,6 +1,6 @@
import type { Fetcher } from 'swr' import type { Fetcher } from 'swr'
import { del, get, post } from './base' 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' import { ANNOTATION_DEFAULT } from '@/config'
export const fetchAnnotationConfig = (appId: string) => { export const fetchAnnotationConfig = (appId: string) => {
@@ -41,7 +41,7 @@ export const fetchExportAnnotationList = (appId: string) => {
} }
export const addAnnotation = (appId: string, body: AnnotationItemBasic) => { 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 }) => { export const annotationBatchImport: Fetcher<{ job_id: string; job_status: string }, { url: string; body: FormData }> = ({ url, body }) => {