-
Notifications
You must be signed in to change notification settings - Fork 56
Bugfix cmgoz1sdq00009mh3txngg3fm #1094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d336b2f
6ba8deb
50a8c64
ab79176
c3c5491
4570956
f439f79
1ab3fb4
e27258e
7b5472e
0896bc4
0777098
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -86,6 +86,7 @@ interface SelectedFile { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadError?: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| preview?: string // URL for image preview | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileType?: FileType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadAborted?: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const getFileIcon = (fileType: FileType | string | undefined) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -417,6 +418,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollPositionRef = useRef<number>(0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const navigate = useNavigate() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileInputRef = useRef<HTMLInputElement | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadControllers = useRef<Map<string, AbortController>>(new Map()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [showReferenceBox, setShowReferenceBox] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [searchMode, setSearchMode] = useState<"citations" | "global">( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -836,12 +838,16 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedFiles((prev) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev.map((f) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| files.some((file) => file.id === f.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { ...f, uploading: true, uploadError: undefined } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { ...f, uploading: true, uploadError: undefined, uploadAborted: false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : f, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uploadPromises = files.map(async (selectedFile) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create AbortController for this file | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const abortController = new AbortController() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadControllers.current.set(selectedFile.id, abortController) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const formData = new FormData() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| formData.append("attachment", selectedFile.file) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -850,6 +856,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: formData, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signal: abortController.signal, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -874,6 +881,19 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("No document ID returned from upload") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if the error is an AbortError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error instanceof Error && error.name === 'AbortError') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle aborted upload - don't show error toast | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedFiles((prev) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prev.map((f) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f.id === selectedFile.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? { ...f, uploading: false, uploadAborted: true } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : f, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errorMessage = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error instanceof Error ? error.message : "Upload failed" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedFiles((prev) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -889,6 +909,8 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up the controller | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadControllers.current.delete(selectedFile.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadingFilesCount((prev) => prev - 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -980,6 +1002,25 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const removeFile = useCallback(async (id: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileToRemove = selectedFiles.find((f) => f.id === id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If the file is currently uploading, abort the upload | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fileToRemove?.uploading) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const controller = uploadControllers.current.get(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (controller) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| controller.abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadControllers.current.delete(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Remove from UI immediately for aborted uploads | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedFiles((prev) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fileToRemove?.preview) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| URL.revokeObjectURL(fileToRemove.preview) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return prev.filter((f) => f.id !== id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setUploadingFilesCount((prev) => Math.max(0, prev - 1)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1005
to
+1022
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: double-decrement of When canceling an in-flight upload, Fix by removing the early decrement and relying on the promise if (controller) {
controller.abort()
uploadControllers.current.delete(id)
}
// Remove from UI immediately for aborted uploads
setSelectedFiles((prev) => {
if (fileToRemove?.preview) {
URL.revokeObjectURL(fileToRemove.preview)
}
return prev.filter((f) => f.id !== id)
})
- setUploadingFilesCount((prev) => Math.max(0, prev - 1))
return📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If the file has metadata with fileId (meaning it's already uploaded), delete it from the server | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fileToRemove?.metadata?.fileId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2213,6 +2254,12 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((f) => f.preview) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(Boolean) as string[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanupPreviewUrls(previewUrls) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Abort any ongoing uploads | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadControllers.current.forEach((controller) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| controller.abort() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadControllers.current.clear() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2861,8 +2908,12 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => removeFile(selectedFile.id)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="absolute top-1 right-1 bg-black bg-opacity-60 text-white rounded-full p-1 hover:bg-opacity-80 transition-opacity" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={selectedFile.uploading} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`absolute top-1 right-1 rounded-full p-1 transition-opacity ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedFile.uploading | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "bg-red-500 bg-opacity-80 text-white hover:bg-opacity-100" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "bg-black bg-opacity-60 text-white hover:bg-opacity-80" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title={selectedFile.uploading ? "Cancel upload" : "Remove file"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <X size={10} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2917,8 +2968,12 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => removeFile(selectedFile.id)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="text-gray-400 hover:text-red-500 transition-colors p-1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={selectedFile.uploading} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className={`transition-colors p-1 ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedFile.uploading | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "text-red-500 hover:text-red-600" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "text-gray-400 hover:text-red-500" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title={selectedFile.uploading ? "Cancel upload" : "Remove file"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <X size={12} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2931,7 +2986,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {uploadingFilesCount > 0 && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-2 text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Loader2 size={12} className="animate-spin" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uploading files... | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Uploading files... (click X to cancel) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {selectedFiles.length >= MAX_ATTACHMENTS && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||||||||
| import { renderHook, act } from '@testing-library/react' | ||||||||||||
| import { describe, it, expect, vi, beforeEach } from 'vitest' | ||||||||||||
| import { ChatBox } from '../ChatBox' | ||||||||||||
|
Comment on lines
+1
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix TS6133: remove unused imports Build fails due to unused Apply: -import { renderHook, act } from '@testing-library/react'
+import { act } from '@testing-library/react'
-import { ChatBox } from '../ChatBox'📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: TypeScript Build Check[error] 1-1: bunx tsc -b: TS6133: 'renderHook' is declared but its value is never read. [error] 3-3: bunx tsc -b: TS6133: 'ChatBox' is declared but its value is never read. 🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| // Mock the necessary dependencies | ||||||||||||
| vi.mock('@/utils/authFetch', () => ({ | ||||||||||||
| authFetch: vi.fn() | ||||||||||||
| })) | ||||||||||||
|
|
||||||||||||
| vi.mock('@/hooks/use-toast', () => ({ | ||||||||||||
| useToast: () => ({ | ||||||||||||
| toast: { | ||||||||||||
| error: vi.fn(), | ||||||||||||
| success: vi.fn() | ||||||||||||
| } | ||||||||||||
| }) | ||||||||||||
| })) | ||||||||||||
|
|
||||||||||||
| vi.mock('@/api', () => ({ | ||||||||||||
| api: { | ||||||||||||
| files: { | ||||||||||||
| delete: { | ||||||||||||
| $post: vi.fn() | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| })) | ||||||||||||
|
|
||||||||||||
| describe('ChatBox Upload Cancellation', () => { | ||||||||||||
| let mockAuthFetch: any | ||||||||||||
|
|
||||||||||||
| beforeEach(() => { | ||||||||||||
| vi.clearAllMocks() | ||||||||||||
| mockAuthFetch = vi.fn() | ||||||||||||
| vi.doMock('@/utils/authFetch', () => ({ | ||||||||||||
| authFetch: mockAuthFetch | ||||||||||||
| })) | ||||||||||||
| }) | ||||||||||||
|
|
||||||||||||
| it('should handle upload cancellation correctly', async () => { | ||||||||||||
| // Create a mock AbortController | ||||||||||||
| const abortController = new AbortController() | ||||||||||||
|
|
||||||||||||
| // Mock a file upload that can be aborted | ||||||||||||
| const uploadPromise = new Promise((resolve, reject) => { | ||||||||||||
| abortController.signal.addEventListener('abort', () => { | ||||||||||||
| const error = new Error('Upload aborted') | ||||||||||||
| error.name = 'AbortError' | ||||||||||||
| reject(error) | ||||||||||||
| }) | ||||||||||||
| }) | ||||||||||||
|
|
||||||||||||
| mockAuthFetch.mockReturnValue(uploadPromise) | ||||||||||||
|
|
||||||||||||
| // Test that aborting the upload triggers the correct behavior | ||||||||||||
| act(() => { | ||||||||||||
| abortController.abort() | ||||||||||||
| }) | ||||||||||||
|
|
||||||||||||
| // Verify the upload was cancelled | ||||||||||||
| await expect(uploadPromise).rejects.toThrow('Upload aborted') | ||||||||||||
| }) | ||||||||||||
|
|
||||||||||||
| it('should clean up upload controllers on component unmount', () => { | ||||||||||||
| // This test would require more complex setup with actual component rendering | ||||||||||||
| // For now, we'll just verify the concept | ||||||||||||
| const controllers = new Map<string, AbortController>() | ||||||||||||
| const controller1 = new AbortController() | ||||||||||||
| const controller2 = new AbortController() | ||||||||||||
|
|
||||||||||||
| controllers.set('file1', controller1) | ||||||||||||
| controllers.set('file2', controller2) | ||||||||||||
|
|
||||||||||||
| // Simulate cleanup | ||||||||||||
| controllers.forEach((controller) => { | ||||||||||||
| controller.abort() | ||||||||||||
| }) | ||||||||||||
| controllers.clear() | ||||||||||||
|
|
||||||||||||
| expect(controllers.size).toBe(0) | ||||||||||||
| expect(controller1.signal.aborted).toBe(true) | ||||||||||||
| expect(controller2.signal.aborted).toBe(true) | ||||||||||||
| }) | ||||||||||||
|
|
||||||||||||
| it('should handle AbortError gracefully in upload completion', async () => { | ||||||||||||
| const abortError = new Error('Request aborted') | ||||||||||||
| abortError.name = 'AbortError' | ||||||||||||
|
|
||||||||||||
| // Mock the upload function to handle AbortError | ||||||||||||
| const mockUploadFile = async (file: File, signal: AbortSignal) => { | ||||||||||||
| if (signal.aborted) { | ||||||||||||
| throw abortError | ||||||||||||
| } | ||||||||||||
| return { fileId: 'test-id', fileName: file.name } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const testFile = new File(['test'], 'test.txt', { type: 'text/plain' }) | ||||||||||||
| const abortController = new AbortController() | ||||||||||||
|
|
||||||||||||
| // Abort before upload | ||||||||||||
| abortController.abort() | ||||||||||||
|
|
||||||||||||
| // Verify AbortError is thrown | ||||||||||||
| await expect(mockUploadFile(testFile, abortController.signal)).rejects.toThrow('Request aborted') | ||||||||||||
| }) | ||||||||||||
| }) | ||||||||||||
|
Comment on lines
+29
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests verify the concepts of upload cancellation (like how To make these tests more effective and ensure the cancellation feature works as expected from a user's perspective, I recommend using Here's an example of how such a test could be structured: import { render, screen, fireEvent, waitFor } from '@testing-library/react';
it('should cancel an ongoing upload when the cancel button is clicked', async () => {
// 1. Mock authFetch to simulate a pending upload that can be aborted
const abortSpy = vi.fn();
mockAuthFetch.mockImplementation((url, options) => {
options.signal.addEventListener('abort', () => {
abortSpy();
const error = new Error('Upload aborted');
error.name = 'AbortError';
// This would normally be handled by fetch, but for test we reject a promise
});
// Return a promise that never resolves to simulate a long upload
return new Promise(() => {});
});
// 2. Render the ChatBox component with necessary props
render(<ChatBox {...mockProps} />);
// 3. Simulate file selection
const fileInput = screen.getByTitle(/Attach files/); // Use a more accessible selector
const file = new File(['content'], 'test.png', { type: 'image/png' });
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } });
});
// 4. Find and click the cancel button
const cancelButton = await screen.findByTitle('Cancel upload');
fireEvent.click(cancelButton);
// 5. Assert that the abort function was called and the file is removed from UI
await waitFor(() => {
expect(abortSpy).toHaveBeenCalled();
expect(screen.queryByText('test.png')).not.toBeInTheDocument();
});
}); |
||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||
| import { describe, it, expect, beforeEach, vi } from 'vitest' | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix TS6133: remove unused Build fails due to unused import. Apply: -import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { describe, it, expect, beforeEach } from 'vitest'📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: TypeScript Build Check[error] 1-1: bunx tsc -b: TS6133: 'vi' is declared but its value is never read. 🤖 Prompt for AI Agents |
||||||
| import { useUploadProgress } from '../useUploadProgressStore' | ||||||
|
|
||||||
| // Mock File API for testing | ||||||
| global.File = class MockFile { | ||||||
| name: string | ||||||
| size: number | ||||||
| type: string | ||||||
| lastModified: number | ||||||
|
|
||||||
| constructor(bits: any[], name: string, options?: { type?: string }) { | ||||||
| this.name = name | ||||||
| this.type = options?.type || 'text/plain' | ||||||
| this.size = bits.reduce((acc, bit) => acc + (typeof bit === 'string' ? bit.length : 0), 0) | ||||||
| this.lastModified = Date.now() | ||||||
| } | ||||||
| } as any | ||||||
|
|
||||||
| describe('useUploadProgressStore', () => { | ||||||
| beforeEach(() => { | ||||||
| // Reset the store before each test by getting the state and clearing it | ||||||
| const store = useUploadProgress.getState() | ||||||
| if (store.currentUpload) { | ||||||
| store.finishUpload(store.currentUpload.id) | ||||||
| } | ||||||
| }) | ||||||
|
|
||||||
| it('should start upload with abort controller', () => { | ||||||
| const store = useUploadProgress.getState() | ||||||
|
|
||||||
| const files = [ | ||||||
| { file: new File(['test'], 'test.txt'), id: 'file1' } | ||||||
| ] | ||||||
|
|
||||||
| const result = store.startUpload('test-collection', files, 1, true) | ||||||
|
|
||||||
| expect(result.uploadId).toBeDefined() | ||||||
| expect(result.abortController).toBeInstanceOf(AbortController) | ||||||
| expect(useUploadProgress.getState().currentUpload?.id).toBe(result.uploadId) | ||||||
| }) | ||||||
|
|
||||||
| it('should cancel upload and abort controller', () => { | ||||||
| const store = useUploadProgress.getState() | ||||||
|
|
||||||
| const files = [ | ||||||
| { file: new File(['test'], 'test.txt'), id: 'file1' } | ||||||
| ] | ||||||
|
|
||||||
| const { uploadId, abortController } = store.startUpload('test-collection', files, 1, true) | ||||||
|
|
||||||
| // Verify controller is not aborted initially | ||||||
| expect(abortController.signal.aborted).toBe(false) | ||||||
|
|
||||||
| // Cancel the upload | ||||||
| store.cancelUpload(uploadId) | ||||||
|
|
||||||
| // Verify controller is aborted and upload is removed | ||||||
| expect(abortController.signal.aborted).toBe(true) | ||||||
| expect(useUploadProgress.getState().currentUpload).toBeNull() | ||||||
| }) | ||||||
|
|
||||||
| it('should remove progress', () => { | ||||||
| const store = useUploadProgress.getState() | ||||||
|
|
||||||
| const files = [ | ||||||
| { file: new File(['test'], 'test.txt'), id: 'file1' } | ||||||
| ] | ||||||
|
|
||||||
| const { uploadId } = store.startUpload('test-collection', files, 1, true) | ||||||
|
|
||||||
| // Verify upload exists | ||||||
| expect(useUploadProgress.getState().currentUpload?.id).toBe(uploadId) | ||||||
|
|
||||||
| // Remove progress | ||||||
| store.removeProgress(uploadId) | ||||||
|
|
||||||
| // Verify upload is removed | ||||||
| expect(useUploadProgress.getState().currentUpload).toBeNull() | ||||||
| }) | ||||||
|
|
||||||
| it('should handle file status updates', () => { | ||||||
| const store = useUploadProgress.getState() | ||||||
|
|
||||||
| const files = [ | ||||||
| { file: new File(['test'], 'test.txt'), id: 'file1' } | ||||||
| ] | ||||||
|
|
||||||
| const { uploadId } = store.startUpload('test-collection', files, 1, true) | ||||||
|
|
||||||
| // Update file status to uploading | ||||||
| store.updateFileStatus(uploadId, 'test.txt', 'file1', 'uploading') | ||||||
|
|
||||||
| let upload = useUploadProgress.getState().currentUpload | ||||||
| expect(upload?.files[0].status).toBe('uploading') | ||||||
|
|
||||||
| // Update file status to uploaded | ||||||
| store.updateFileStatus(uploadId, 'test.txt', 'file1', 'uploaded') | ||||||
|
|
||||||
| upload = useUploadProgress.getState().currentUpload | ||||||
| expect(upload?.files[0].status).toBe('uploaded') | ||||||
| }) | ||||||
|
|
||||||
| it('should handle abort controller cleanup', () => { | ||||||
| const store = useUploadProgress.getState() | ||||||
|
|
||||||
| const files = [ | ||||||
| { file: new File(['test'], 'test.txt'), id: 'file1' } | ||||||
| ] | ||||||
|
|
||||||
| const { abortController } = store.startUpload('test-collection', files, 1, true) | ||||||
|
|
||||||
| // Manually abort the controller | ||||||
| abortController.abort() | ||||||
|
|
||||||
| // Verify the signal is aborted | ||||||
| expect(abortController.signal.aborted).toBe(true) | ||||||
| }) | ||||||
| }) | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a bug here where
uploadingFilesCountis decremented twice for a cancelled upload. It's decremented here, and also in thefinallyblock of theuploadFilesfunction (line 914), which will execute after the upload promise is rejected due to the abort signal.This can lead to an incorrect count and potential UI bugs. The
finallyblock is a more reliable place to handle the decrement as it covers success, failure, and abort cases. I suggest removing this line to fix the double-decrement issue.