diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 8b871403cc..b1f32f96c2 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -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 diff --git a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx index 1f32e55928..95a5586292 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx @@ -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() + + // 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() + + // 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() + }) }) }) diff --git a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx index e808d0b48a..37b5ab0686 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx @@ -52,8 +52,14 @@ const EditItem: FC = ({ }, [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 = ({
·
{ - 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 + } }} >
diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx index b48f8a2a4a..bdc991116c 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx @@ -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() + // Act + render() - // 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() + + 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() + // Act + render() - // 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() + + 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() - // 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', + }) + }) }) }) diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index 2961ce393c..6172a215e4 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -53,27 +53,39 @@ const EditAnnotationModal: FC = ({ 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) diff --git a/web/app/components/app/annotation/header-opts/index.spec.tsx b/web/app/components/app/annotation/header-opts/index.spec.tsx index 8c640c2790..3d8a1fd4ef 100644 --- a/web/app/components/app/annotation/header-opts/index.spec.tsx +++ b/web/app/components/app/annotation/header-opts/index.spec.tsx @@ -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(null) + const MenuContext = React.createContext(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 ( + + {typeof children === 'function' ? children({ open }) : children} + + ) + } + + const PopoverButton = React.forwardRef(({ onClick, children, ...props }: { onClick?: () => void; children?: React.ReactNode }, ref: React.Ref) => { + const context = React.useContext(PopoverContext) + const handleClick = () => { + context?.setOpen(!context.open) + onClick?.() + } + return ( + + ) + }) + + const PopoverPanel = React.forwardRef(({ children, ...props }: { children: React.ReactNode | ((props: { close: () => void }) => React.ReactNode) }, ref: React.Ref) => { + const context = React.useContext(PopoverContext) + if (!context?.open) return null + const content = typeof children === 'function' ? children({ close: () => context.setOpen(false) }) : children + return ( +
+ {content} +
+ ) + }) + + const Menu = ({ children }: { children: React.ReactNode }) => { + const [open, setOpen] = React.useState(false) + const value = React.useMemo(() => ({ open, setOpen }), [open]) + return ( + + {children} + + ) + } + + const MenuButton = ({ onClick, children, ...props }: { onClick?: () => void; children?: React.ReactNode }) => { + const context = React.useContext(MenuContext) + const handleClick = () => { + context?.setOpen(!context.open) + onClick?.() + } + return ( + + ) + } + + const MenuItems = ({ children, ...props }: { children: React.ReactNode }) => { + const context = React.useContext(MenuContext) + if (!context?.open) return null + return ( +
+ {children} +
+ ) + } + + return { + Dialog: ({ open, children, className }: { open?: boolean; children: React.ReactNode; className?: string }) => { + if (open === false) return null + return ( +
+ {children} +
+ ) + }, + DialogBackdrop: ({ children, className, onClick }: { children?: React.ReactNode; className?: string; onClick?: () => void }) => ( +
+ {children} +
+ ), + DialogPanel: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + DialogTitle: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + 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 | 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: [] }) diff --git a/web/app/components/app/annotation/type.ts b/web/app/components/app/annotation/type.ts index 5df6f51ace..e2f2264f07 100644 --- a/web/app/components/app/annotation/type.ts +++ b/web/app/components/app/annotation/type.ts @@ -12,6 +12,12 @@ export type AnnotationItem = { hit_count: number } +export type AnnotationCreateResponse = AnnotationItem & { + account?: { + name?: string + } +} + export type HitHistoryItem = { id: string question: string diff --git a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx index b666a6cb5b..c432ca68e2 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx @@ -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 }) => ( +
+ {children} +
+ ), + DialogPanel: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + DialogTitle: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + 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 }) => ( + + ), +})) + 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() - const setModalOpenSpy = jest.fn() - 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['value'] @@ -101,18 +128,13 @@ const renderParamsConfig = ({ ) } - render( + return render( , { 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() }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx index 3050249bb6..ef1fb183c8 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx @@ -41,7 +41,7 @@ const AnnotationCtrlButton: FC = ({ 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 = ({ message: t('common.api.actionSuccess') as string, type: 'success', }) - onAdded(res.id, res.account?.name) + onAdded(res.id, res.account?.name ?? '') } return ( diff --git a/web/i18n/ar-TN/common.ts b/web/i18n/ar-TN/common.ts index 8437c0643f..b1f4f46f22 100644 --- a/web/i18n/ar-TN/common.ts +++ b/web/i18n/ar-TN/common.ts @@ -11,6 +11,7 @@ const translation = { saved: 'تم الحفظ', create: 'تم الإنشاء', remove: 'تمت الإزالة', + actionFailed: 'فشل الإجراء', }, operation: { create: 'إنشاء', diff --git a/web/i18n/de-DE/common.ts b/web/i18n/de-DE/common.ts index 479348ef43..d9ebfd60e0 100644 --- a/web/i18n/de-DE/common.ts +++ b/web/i18n/de-DE/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Gespeichert', create: 'Erstellt', remove: 'Entfernt', + actionFailed: 'Aktion fehlgeschlagen', }, operation: { create: 'Erstellen', diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index d78520cf1f..92d24b1351 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -8,6 +8,7 @@ const translation = { api: { success: 'Success', actionSuccess: 'Action succeeded', + actionFailed: 'Action failed', saved: 'Saved', create: 'Created', remove: 'Removed', diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts index 38d4402fd2..8f56c7e668 100644 --- a/web/i18n/es-ES/common.ts +++ b/web/i18n/es-ES/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Guardado', create: 'Creado', remove: 'Eliminado', + actionFailed: 'Acción fallida', }, operation: { create: 'Crear', diff --git a/web/i18n/fa-IR/common.ts b/web/i18n/fa-IR/common.ts index 62a2e2cec8..afd6f760aa 100644 --- a/web/i18n/fa-IR/common.ts +++ b/web/i18n/fa-IR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'ذخیره شد', create: 'ایجاد شد', remove: 'حذف شد', + actionFailed: 'عمل شکست خورد', }, operation: { create: 'ایجاد', diff --git a/web/i18n/fr-FR/common.ts b/web/i18n/fr-FR/common.ts index da72b0497c..4b0deb4a8c 100644 --- a/web/i18n/fr-FR/common.ts +++ b/web/i18n/fr-FR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Sauvegardé', create: 'Créé', remove: 'Supprimé', + actionFailed: 'Action échouée', }, operation: { create: 'Créer', diff --git a/web/i18n/hi-IN/common.ts b/web/i18n/hi-IN/common.ts index fa25074b9c..88f8f814e6 100644 --- a/web/i18n/hi-IN/common.ts +++ b/web/i18n/hi-IN/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'सहेजा गया', create: 'बनाया गया', remove: 'हटाया गया', + actionFailed: 'क्रिया विफल', }, operation: { create: 'बनाएं', diff --git a/web/i18n/id-ID/common.ts b/web/i18n/id-ID/common.ts index 0c70b0341e..4cce24e76a 100644 --- a/web/i18n/id-ID/common.ts +++ b/web/i18n/id-ID/common.ts @@ -11,6 +11,7 @@ const translation = { remove: 'Dihapus', actionSuccess: 'Aksi berhasil', create: 'Dibuat', + actionFailed: 'Tindakan gagal', }, operation: { setup: 'Setup', diff --git a/web/i18n/it-IT/common.ts b/web/i18n/it-IT/common.ts index d5793bb902..b52b93b1a5 100644 --- a/web/i18n/it-IT/common.ts +++ b/web/i18n/it-IT/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Salvato', create: 'Creato', remove: 'Rimosso', + actionFailed: 'Azione non riuscita', }, operation: { create: 'Crea', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index d4647fbc12..bde00cb66b 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -11,6 +11,7 @@ const translation = { saved: '保存済み', create: '作成済み', remove: '削除済み', + actionFailed: 'アクションに失敗しました', }, operation: { create: '作成', diff --git a/web/i18n/ko-KR/common.ts b/web/i18n/ko-KR/common.ts index 805b9f9840..531aa29054 100644 --- a/web/i18n/ko-KR/common.ts +++ b/web/i18n/ko-KR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: '저장됨', create: '생성됨', remove: '삭제됨', + actionFailed: '작업 실패', }, operation: { create: '생성', diff --git a/web/i18n/pl-PL/common.ts b/web/i18n/pl-PL/common.ts index 2ecf18c7e6..10f566258a 100644 --- a/web/i18n/pl-PL/common.ts +++ b/web/i18n/pl-PL/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Zapisane', create: 'Utworzono', remove: 'Usunięto', + actionFailed: 'Akcja nie powiodła się', }, operation: { create: 'Utwórz', diff --git a/web/i18n/pt-BR/common.ts b/web/i18n/pt-BR/common.ts index d0838b4f09..b739561ca4 100644 --- a/web/i18n/pt-BR/common.ts +++ b/web/i18n/pt-BR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Salvo', create: 'Criado', remove: 'Removido', + actionFailed: 'Ação falhou', }, operation: { create: 'Criar', diff --git a/web/i18n/ro-RO/common.ts b/web/i18n/ro-RO/common.ts index 8b8ab9ac26..df3cf01b6c 100644 --- a/web/i18n/ro-RO/common.ts +++ b/web/i18n/ro-RO/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Salvat', create: 'Creat', remove: 'Eliminat', + actionFailed: 'Acțiunea a eșuat', }, operation: { create: 'Creează', diff --git a/web/i18n/ru-RU/common.ts b/web/i18n/ru-RU/common.ts index afc9368e9e..ae8b2e558f 100644 --- a/web/i18n/ru-RU/common.ts +++ b/web/i18n/ru-RU/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Сохранено', create: 'Создано', remove: 'Удалено', + actionFailed: 'Действие не удалось', }, operation: { create: 'Создать', diff --git a/web/i18n/sl-SI/common.ts b/web/i18n/sl-SI/common.ts index b024ace3be..697d06eb8b 100644 --- a/web/i18n/sl-SI/common.ts +++ b/web/i18n/sl-SI/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Shranjeno', create: 'Ustvarjeno', remove: 'Odstranjeno', + actionFailed: 'Dejanje ni uspelo', }, operation: { create: 'Ustvari', diff --git a/web/i18n/th-TH/common.ts b/web/i18n/th-TH/common.ts index 9c325e3781..dc82c71c78 100644 --- a/web/i18n/th-TH/common.ts +++ b/web/i18n/th-TH/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'บันทึก', create: 'สร้าง', remove: 'ถูก เอา ออก', + actionFailed: 'การดำเนินการล้มเหลว', }, operation: { create: 'สร้าง', diff --git a/web/i18n/tr-TR/common.ts b/web/i18n/tr-TR/common.ts index 8b0a7cba69..5e7f2182c7 100644 --- a/web/i18n/tr-TR/common.ts +++ b/web/i18n/tr-TR/common.ts @@ -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', diff --git a/web/i18n/uk-UA/common.ts b/web/i18n/uk-UA/common.ts index bd0f55c2f5..70b8aaa862 100644 --- a/web/i18n/uk-UA/common.ts +++ b/web/i18n/uk-UA/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Збережено', create: 'Створено', remove: 'Видалено', + actionFailed: 'Не вдалося виконати дію', }, operation: { create: 'Створити', diff --git a/web/i18n/vi-VN/common.ts b/web/i18n/vi-VN/common.ts index 8b1b69163e..666dc7a133 100644 --- a/web/i18n/vi-VN/common.ts +++ b/web/i18n/vi-VN/common.ts @@ -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', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 8e7103564f..bd0e0e3ba4 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -11,6 +11,7 @@ const translation = { saved: '已保存', create: '已创建', remove: '已移除', + actionFailed: '操作失败', }, operation: { create: '创建', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 1a72a083d8..8ed1e336ef 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -5,6 +5,7 @@ const translation = { saved: '已儲存', create: '已建立', remove: '已移除', + actionFailed: '操作失敗', }, operation: { create: '建立', diff --git a/web/jest.config.ts b/web/jest.config.ts index e86ec5af74..434b19270f 100644 --- a/web/jest.config.ts +++ b/web/jest.config.ts @@ -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: '/.cache/jest', // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, diff --git a/web/service/annotation.ts b/web/service/annotation.ts index 58efb7b976..af708fe174 100644 --- a/web/service/annotation.ts +++ b/web/service/annotation.ts @@ -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(`apps/${appId}/annotations`, { body }) } export const annotationBatchImport: Fetcher<{ job_id: string; job_status: string }, { url: string; body: FormData }> = ({ url, body }) => {