Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d006e5c

Browse files
committedMay 19, 2025··
🚸(frontend) let loader until resource ready
The backend can analyze the upload file, this take time, so we need to show a loader until the backend finish the analysis.
1 parent 8df1b26 commit d006e5c

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed
 
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import fetchMock from 'fetch-mock';
3+
4+
import { useMediaUrl } from '@/core/config';
5+
import { AppWrapper } from '@/tests/utils';
6+
import { sleep } from '@/utils';
7+
8+
import { useUploadFile } from '../hook/useUploadFile';
9+
10+
// Mock dependencies
11+
jest.mock('@/core/config', () => ({
12+
useMediaUrl: jest.fn(),
13+
}));
14+
15+
jest.mock('@/utils', () => ({
16+
sleep: jest.fn(),
17+
}));
18+
19+
describe('useUploadFile', () => {
20+
const mockDocId = 'doc-123';
21+
const mockMediaUrl = 'https://media.example.com/';
22+
const mockFile = new File(['test content'], 'test.jpg', {
23+
type: 'image/jpeg',
24+
});
25+
const mockFilePath = 'uploads/test.jpg';
26+
const mockFullUrl = `${mockMediaUrl}${mockFilePath}`;
27+
const mockUploadAttachment = `http://test.jest/api/v1.0/documents/${mockDocId}/attachment-upload/`;
28+
29+
beforeEach(() => {
30+
jest.clearAllMocks();
31+
fetchMock.reset();
32+
33+
// Setup mocks
34+
(useMediaUrl as jest.Mock).mockReturnValue(mockMediaUrl);
35+
(sleep as jest.Mock).mockResolvedValue(undefined);
36+
});
37+
38+
test('should upload file successfully', async () => {
39+
fetchMock
40+
.postOnce(mockUploadAttachment, {
41+
body: {
42+
file: mockFilePath,
43+
},
44+
})
45+
.head(mockFullUrl, 200);
46+
47+
const { result } = renderHook(() => useUploadFile(mockDocId), {
48+
wrapper: AppWrapper,
49+
});
50+
51+
let fileUrl;
52+
await act(async () => {
53+
fileUrl = await result.current.uploadFile(mockFile);
54+
});
55+
56+
expect(fetchMock.called(mockUploadAttachment)).toBeTruthy();
57+
expect(fetchMock.called(mockFullUrl)).toBeTruthy();
58+
expect(fetchMock.calls().length).toBe(2);
59+
expect(fileUrl).toBe(mockFullUrl);
60+
});
61+
62+
test('should retry when file is not yet analyzed (status 403)', async () => {
63+
fetchMock
64+
.postOnce(mockUploadAttachment, {
65+
body: {
66+
file: mockFilePath,
67+
},
68+
})
69+
.headOnce(mockFullUrl, 403, {
70+
overwriteRoutes: true,
71+
})
72+
.headOnce(mockFullUrl, 200, {
73+
overwriteRoutes: false,
74+
});
75+
76+
const { result } = renderHook(() => useUploadFile(mockDocId), {
77+
wrapper: AppWrapper,
78+
});
79+
80+
let fileUrl;
81+
await act(async () => {
82+
fileUrl = await result.current.uploadFile(mockFile);
83+
});
84+
85+
expect(sleep).toHaveBeenCalledWith(4000);
86+
expect(fetchMock.calls().length).toBe(3);
87+
expect(fileUrl).toBe(mockFullUrl);
88+
});
89+
90+
test('should handle max retries exceeded', async () => {
91+
fetchMock
92+
.postOnce(mockUploadAttachment, {
93+
body: {
94+
file: mockFilePath,
95+
},
96+
})
97+
.head(mockFullUrl, 403);
98+
99+
const { result } = renderHook(() => useUploadFile(mockDocId), {
100+
wrapper: AppWrapper,
101+
});
102+
103+
let fileUrl;
104+
await act(async () => {
105+
fileUrl = await result.current.uploadFile(mockFile);
106+
});
107+
108+
expect(fetchMock.calls().length).toBe(13);
109+
expect(sleep).toHaveBeenCalledTimes(11);
110+
expect(fileUrl).toBe(mockFullUrl);
111+
});
112+
113+
test('should handle non-200 successful responses', async () => {
114+
fetchMock
115+
.postOnce(mockUploadAttachment, {
116+
body: {
117+
file: mockFilePath,
118+
},
119+
})
120+
.head(mockFullUrl, 204);
121+
122+
const { result } = renderHook(() => useUploadFile(mockDocId), {
123+
wrapper: AppWrapper,
124+
});
125+
126+
let fileUrl;
127+
await act(async () => {
128+
fileUrl = await result.current.uploadFile(mockFile);
129+
});
130+
131+
expect(fetchMock.calls().length).toBe(2);
132+
expect(fileUrl).toBe(mockFullUrl);
133+
});
134+
135+
test('should handle attachment creation error', async () => {
136+
fetchMock.postOnce(mockUploadAttachment, {
137+
throws: new Error('Attachment creation error'),
138+
});
139+
140+
const { result } = renderHook(() => useUploadFile(mockDocId), {
141+
wrapper: AppWrapper,
142+
});
143+
144+
await expect(result.current.uploadFile(mockFile)).rejects.toThrow();
145+
});
146+
});

‎src/frontend/apps/impress/src/features/docs/doc-editor/hook/useUploadFile.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
import { useCallback } from 'react';
22

33
import { useMediaUrl } from '@/core/config';
4+
import { sleep } from '@/utils';
45

56
import { useCreateDocAttachment } from '../api';
67

8+
/**
9+
* Upload file can be analyzed on the server side,
10+
* we had this function to wait for the analysis to be done
11+
* before returning the file url. It will keep the loader
12+
* on the upload button until the analysis is done.
13+
* @param url
14+
* @param retry
15+
* @returns boolean
16+
* @description Waits for the upload to be analyzed by checking the status of the file.
17+
*/
18+
const waitForUploadAnalyze = async (url: string, retry = 0) => {
19+
const RETRY_LIMIT = 10;
20+
const SLEEP_TIME = 4000;
21+
const { status } = await fetch(url, {
22+
method: 'HEAD',
23+
credentials: 'include',
24+
});
25+
26+
if (status !== 403 || retry > RETRY_LIMIT) {
27+
return retry > RETRY_LIMIT || status !== 200 ? false : true;
28+
} else {
29+
await sleep(SLEEP_TIME);
30+
return await waitForUploadAnalyze(url, retry + 1);
31+
}
32+
};
33+
734
export const useUploadFile = (docId: string) => {
835
const mediaUrl = useMediaUrl();
936
const {
@@ -22,6 +49,8 @@ export const useUploadFile = (docId: string) => {
2249
body,
2350
});
2451

52+
await waitForUploadAnalyze(`${mediaUrl}${ret.file}`);
53+
2554
return `${mediaUrl}${ret.file}`;
2655
},
2756
[createDocAttachment, docId, mediaUrl],

0 commit comments

Comments
 (0)
Please sign in to comment.