Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
49b8aee
refactor(upload): add the upload engine as a queue of files with per-…
xabg2 Oct 29, 2026
db2ee21
refactor(upload): upload small files before medium ones and adjust co…
xabg2 Oct 29, 2026
9e2d9da
refactor(upload): run every file upload on the new engine
xabg2 Oct 29, 2026
038286c
refactor(upload): remove the legacy upload manager
xabg2 Oct 29, 2026
946d375
refactor(upload): upload folder trees through the engine, creating ea…
xabg2 Oct 29, 2026
00a77fa
refactor(upload): mark the queue push as not awaited and simplify the…
xabg2 Oct 30, 2026
bf41be8
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
93fcd9c
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
2a627b0
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
28ac216
refactor(upload): return the cancel promise without an empty async
xabg2 Oct 30, 2026
0fd64f4
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
6eb17a9
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
1a6ef19
refactor(upload): retry an upload by calling it again instead of looping
xabg2 Oct 30, 2026
e17640c
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
a11b4e0
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
c46ffab
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
14c11da
fix(upload): start every run with uploads not cancelled and the full …
xabg2 Oct 30, 2026
ecac697
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
ca42785
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
f535e7b
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
53d1bbe
test(upload): name uploaded items with the shared helper
xabg2 Oct 30, 2026
e134060
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
5e5951d
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
a8aafd2
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
99a8e27
refactor(upload): remove cancelling everything from the engine, uploa…
xabg2 Oct 30, 2026
97486d5
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
00797b0
fix(upload): cancel every file of a folder upload even when it is can…
xabg2 Oct 30, 2026
2d3b5c6
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
3636102
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
302d7c7
test(upload): show which files are uploading and which are waiting wh…
xabg2 Oct 30, 2026
dd73509
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
cb9901e
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
e749f0c
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
17874fd
test(upload): state each upload scenario plainly and count started up…
xabg2 Oct 30, 2026
d0443ab
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
a649f64
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
3dfcd00
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
614ec04
refactor(upload): save the upload state where it changes instead of t…
xabg2 Oct 30, 2026
f21a72b
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
3b3ce0c
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
d65eba2
fix(upload): do not start a file upload that was cancelled while it w…
xabg2 Oct 30, 2026
3d7b02b
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Oct 30, 2026
90aa298
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
4173f23
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
cac6c49
test(upload): a cancelled folder upload no longer starts the files th…
xabg2 Oct 30, 2026
c598793
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Oct 30, 2026
bfb332c
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Oct 30, 2026
407e562
fix(upload): do not create a folder that was cancelled while its uplo…
xabg2 Oct 30, 2026
7017fcf
test(upload): state each folder upload scenario plainly instead of re…
xabg2 Oct 30, 2026
212bf47
refactor(upload): keep the upload engine types and failures next to t…
xabg2 Nov 2, 2026
a07d75c
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Nov 2, 2026
582e822
refactor(upload): import the upload types from their new place
xabg2 Nov 2, 2026
3e862ca
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Nov 2, 2026
eb1c841
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Nov 2, 2026
3e92a9f
refactor(upload): classify plan limit failures once and retry automat…
xabg2 Nov 2, 2026
b42f89e
Merge branch 'refactor/upload-engine' into refactor/upload-engine-switch
xabg2 Nov 2, 2026
5448c61
fix(upload): keep showing the upload failed message when the plan lim…
xabg2 Nov 2, 2026
7781ee2
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Nov 2, 2026
f9dc148
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Nov 2, 2026
f8bb497
fix(upload): show the storage full and file too large dialogs on the …
xabg2 Nov 2, 2026
374816a
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Nov 2, 2026
07f3ce3
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Nov 2, 2026
2b5ad5b
refactor(upload): add Upload to track each user upload with its tasks…
xabg2 Nov 2, 2026
c150853
refactor(upload): add the upload engine as a queue of files with per-…
xabg2 Oct 29, 2026
b6ec3d4
refactor(upload): upload small files before medium ones and adjust co…
xabg2 Oct 29, 2026
fb0f9bd
refactor(upload): mark the queue push as not awaited and simplify the…
xabg2 Oct 30, 2026
9bb6b2e
refactor(upload): retry an upload by calling it again instead of looping
xabg2 Oct 30, 2026
50eb199
fix(upload): start every run with uploads not cancelled and the full …
xabg2 Oct 30, 2026
24944d2
test(upload): name uploaded items with the shared helper
xabg2 Oct 30, 2026
e6cb85f
refactor(upload): remove cancelling everything from the engine, uploa…
xabg2 Oct 30, 2026
7bfb76b
test(upload): show which files are uploading and which are waiting wh…
xabg2 Oct 30, 2026
b872483
test(upload): state each upload scenario plainly and count started up…
xabg2 Oct 30, 2026
60fd915
fix(upload): do not start a file upload that was cancelled while it w…
xabg2 Oct 30, 2026
560de20
refactor(upload): keep the upload engine types and failures next to t…
xabg2 Nov 2, 2026
e230473
refactor(upload): classify plan limit failures once and retry automat…
xabg2 Nov 2, 2026
b6e3256
refactor(upload): run every file upload on the new engine
xabg2 Oct 29, 2026
bd9908e
refactor(upload): return the cancel promise without an empty async
xabg2 Oct 30, 2026
9f571ba
fix(upload): cancel every file of a folder upload even when it is can…
xabg2 Oct 30, 2026
9d55c50
refactor(upload): save the upload state where it changes instead of t…
xabg2 Oct 30, 2026
51110c5
test(upload): a cancelled folder upload no longer starts the files th…
xabg2 Oct 30, 2026
6e0ded3
refactor(upload): import the upload types from their new place
xabg2 Nov 2, 2026
3a2e56a
fix(upload): keep showing the upload failed message when the plan lim…
xabg2 Nov 2, 2026
245674d
fix(upload): show the storage full and file too large dialogs on the …
xabg2 Nov 2, 2026
4fbd278
Merge branch 'refactor/upload-engine-switch' into refactor/upload-eng…
xabg2 Nov 3, 2026
fe250ba
Merge branch 'refactor/upload-engine-legacy-removal' into refactor/up…
xabg2 Nov 3, 2026
9005f34
Merge branch 'refactor/upload-engine-folders' into refactor/upload-ac…
xabg2 Nov 3, 2026
84a04ee
Merge branch 'master' into refactor/upload-action
xabg2 Nov 4, 2026
133e94e
fix(upload): let a cancelled folder finish being created so it can be…
xabg2 Nov 4, 2026
1f3c95a
Merge branch 'master' into refactor/upload-action
xabg2 Nov 4, 2026
31d41fd
refactor(upload): move the upload item tree helpers to their own file
xabg2 Nov 6, 2026
81c28fd
Merge branch 'master' into refactor/upload-action
xabg2 Nov 6, 2026
cf235b7
fix(upload): update task before notifying
xabg2 Nov 6, 2026
690c24f
fix(upload): await cancelled folder deletion
xabg2 Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/app/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "Upload fehlgeschlagen"
"upload-failed": "Upload fehlgeschlagen",
"folder-removal-failed": "Der hochgeladene Ordner konnte nicht entfernt werden"
},
"generalErrorMessages": {
"retryUploadFailed": "Beim erneuten Hochladen ist etwas schiefgegangen. Versuche es erneut",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "Upload failed"
"upload-failed": "Upload failed",
"folder-removal-failed": "Couldn't remove the uploaded folder"
},
"generalErrorMessages": {
"retryUploadFailed": "Something went wrong when retrying the upload. Try uploading again",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1377,7 +1377,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "Error al subir"
"upload-failed": "Error al subir",
"folder-removal-failed": "No se pudo eliminar la carpeta subida"
},
"generalErrorMessages": {
"retryUploadFailed": "Algo salió mal al reintentar la subida. Intenta subirlo de nuevo",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1376,7 +1376,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "Échec du téléchargement"
"upload-failed": "Échec du téléchargement",
"folder-removal-failed": "Impossible de supprimer le dossier importé"
},
"generalErrorMessages": {
"retryUploadFailed": "Quelque chose s'est mal passé en réessayant de télécharger. Essayez de télécharger à nouveau",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "Caricamento fallito"
"upload-failed": "Caricamento fallito",
"folder-removal-failed": "Impossibile rimuovere la cartella caricata"
},
"generalErrorMessages": {
"retryUploadFailed": "Qualcosa è andato storto nel riprovare a caricare. Prova a caricare di nuovo",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "Falha no envio"
"upload-failed": "Falha no envio",
"folder-removal-failed": "Não foi possível remover a pasta enviada"
},
"generalErrorMessages": {
"retryUploadFailed": "Algo deu errado ao tentar enviar novamente. Tente fazer o envio de novo",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1376,7 +1376,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "Не удалось загрузить"
"upload-failed": "Не удалось загрузить",
"folder-removal-failed": "Не удалось удалить загруженную папку"
},
"generalErrorMessages": {
"retryUploadFailed": "Что-то пошло не так при повторной попытке загрузки. Попробуйте загрузить снова",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "上傳失敗"
"upload-failed": "上傳失敗",
"folder-removal-failed": "無法移除已上傳的資料夾"
},
"generalErrorMessages": {
"retryUploadFailed": "重試上傳時出現問題。請重新上傳",
Expand Down
3 changes: 2 additions & 1 deletion src/app/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1397,7 +1397,8 @@
"errors": {}
},
"subtitles": {
"upload-failed": "上传失败"
"upload-failed": "上传失败",
"folder-removal-failed": "无法删除已上传的文件夹"
},
"generalErrorMessages": {
"retryUploadFailed": "重试上传时出了问题。请再次上传",
Expand Down
5 changes: 5 additions & 0 deletions src/app/tasks/upload/uploadTaskErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ export const errorMerge = (reason?: UploadFailureReason) => {
const subtitle = errorSubtitle(reason);
return subtitle !== undefined ? { status: TaskStatus.Error, subtitle } : { status: TaskStatus.Error };
};

export const folderRemovalFailedMerge = () => ({
status: TaskStatus.Error,
subtitle: t('tasks.subtitles.folder-removal-failed') as string,
});
286 changes: 286 additions & 0 deletions src/app/upload/Upload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import { beforeEach, describe, expect, Mock, test, vi } from 'vitest';
import uploadFile from 'app/drive/services/file.service/uploadFile';
import folderService from 'app/drive/services/folder.service';
import newStorageService from 'app/drive/services/new-storage.service';
import { IRoot } from 'app/store/slices/storage/types';
import tasksService from 'app/tasks/services/tasks.service';
import { TaskStatus, TaskType } from 'app/tasks/types';
import errorService from 'services/error.service';
import referralService from 'services/referral.service';
import { AppError } from '@internxt/sdk';
import { HTTP_STATUS_CODES } from 'app/core/constants';
import {
getCreatedFolder,
getFileToUpload,
getFolderToUpload,
getUploadRepository,
} from 'testUtils/fixtures/upload.fixtures';
import { Upload } from './Upload';

vi.mock('app/drive/services/file.service/uploadFile', () => ({ default: vi.fn() }));
vi.mock('app/drive/services/folder.service', () => ({ default: { createFolderByUuid: vi.fn() } }));
vi.mock('services/workspace.service', () => ({ default: { createFolder: vi.fn() } }));
vi.mock('app/drive/services/new-storage.service', () => ({ default: { deleteFolderByUuid: vi.fn() } }));
vi.mock('services/referral.service', () => ({ default: { trackFileUpload: vi.fn(), trackFolderUpload: vi.fn() } }));
vi.mock('app/network/networkInformation', () => ({ logNetworkInfoForUpload: vi.fn() }));
vi.mock('i18next', () => ({ t: (key: string) => key }));

const DESTINATION_UUID = 'destination-uuid';

const uploadRepository = getUploadRepository();

const fakeFolderCreationTaking = (delayMs = 0) =>
vi.mocked(folderService.createFolderByUuid).mockImplementation((_parentUuid, name) => {
let abortCreation: () => void = () => undefined;
const creation = new Promise((resolve, reject) => {
setTimeout(() => resolve(getCreatedFolder(name)), delayMs);
abortCreation = () => reject(new Error('aborted'));
});
return [creation, { cancel: () => abortCreation() }] as never;
});

const fakeUploadsTaking = (delayMs: number) =>
(uploadFile as Mock).mockImplementation(
(file: { name: string }, _progress, options: { abortCallback?: (abort: () => void) => void }) =>
new Promise((resolve, reject) => {
const timeout = setTimeout(() => resolve({ uuid: `${file.name}-uuid` }), delayMs);
options.abortCallback?.(() => {
clearTimeout(timeout);
reject(new Error('aborted'));
});
}),
);

const createUpload = (
roots: IRoot[],
files: File[],
callbacks: Partial<ConstructorParameters<typeof Upload>[0]> = {},
) => new Upload({ destinationFolderUuid: DESTINATION_UUID, roots, files, uploadRepository, ...callbacks });

const tasksOf = (action: TaskType) => tasksService.getTasks().filter((task) => task.action === action);

const failUploadsOf = (failingNames: string[], error: unknown = new Error('Server error')) =>
(uploadFile as Mock).mockImplementation(async (file: { name: string }) => {
if (failingNames.includes(file.name)) throw error;
return { uuid: `${file.name}-uuid` };
});

describe('Uploading what the user picked', () => {
beforeEach(() => {
vi.clearAllMocks();
tasksService.clearTasks();
vi.spyOn(errorService, 'reportError').mockReturnValue();
vi.mocked(newStorageService.deleteFolderByUuid).mockResolvedValue(undefined as never);
(uploadFile as Mock).mockImplementation(async (file: { name: string }) => ({ uuid: `${file.name}-uuid` }));
fakeFolderCreationTaking();
});

test('When a folder and a file are uploaded, then each one is tracked by its own task until it succeeds', async () => {
const upload = createUpload(
[getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('beach.png'), getFileToUpload('city.png')] })],
[getFileToUpload('report.pdf')],
);

await upload.run();

const [folderTask] = tasksOf(TaskType.UploadFolder);
const [fileTask] = tasksOf(TaskType.UploadFile);
expect(tasksService.getTasks()).toHaveLength(2);
expect(folderTask).toEqual(
expect.objectContaining({ status: TaskStatus.Success, progress: 1, itemUUID: { rootFolderUUID: 'Photos-uuid' } }),
);
expect(fileTask).toEqual(
expect.objectContaining({ status: TaskStatus.Success, itemUUID: { fileUUID: 'report-uuid' } }),
);
});

test('When the items are uploaded, then each one is reported with the folder it was uploaded into', async () => {
const onItemUploaded = vi.fn();
const upload = createUpload(
[getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('beach.png')] })],
[getFileToUpload('report.pdf')],
{
onItemUploaded,
},
);

await upload.run();

expect(onItemUploaded).toHaveBeenCalledWith(
expect.objectContaining({ uuid: 'Photos-uuid' }),
DESTINATION_UUID,
true,
);
expect(onItemUploaded).toHaveBeenCalledWith(expect.objectContaining({ name: 'beach' }), 'Photos-uuid', false);
expect(onItemUploaded).toHaveBeenCalledWith(expect.objectContaining({ name: 'report' }), DESTINATION_UUID, false);
});

test('When reporting an uploaded item fails, then the upload still finishes and each task succeeds', async () => {
const onItemUploaded = vi.fn(() => {
throw new Error('Listener error');
});
vi.mocked(referralService.trackFileUpload).mockImplementation(() => {
throw new Error('Corrupt banner state');
});
const upload = createUpload(
[getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('beach.png')] })],
[getFileToUpload('report.pdf')],
{ onItemUploaded },
);

await upload.run();

expect(tasksOf(TaskType.UploadFolder)[0]).toEqual(
expect.objectContaining({ status: TaskStatus.Success, progress: 1, itemUUID: { rootFolderUUID: 'Photos-uuid' } }),
);
expect(tasksOf(TaskType.UploadFile)[0]).toEqual(
expect.objectContaining({ status: TaskStatus.Success, itemUUID: { fileUUID: 'report-uuid' } }),
);
});

test('When a file inside a folder fails, then the folder still succeeds with what could be uploaded', async () => {
(uploadFile as Mock).mockImplementation(async (file: { name: string }) => {
if (file.name === 'broken') throw new Error('Server error');
return { uuid: `${file.name}-uuid` };
});
const upload = createUpload(
[getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('broken.png'), getFileToUpload('beach.png')] })],
[],
);

const { failed } = await upload.run();

expect(failed).toHaveLength(1);
expect(tasksOf(TaskType.UploadFolder)[0]).toEqual(
expect.objectContaining({ status: TaskStatus.Success, progress: 2 / 3 }),
);
});

test('When a folder cannot be created, then its task shows the error', async () => {
vi.mocked(folderService.createFolderByUuid).mockImplementation(
() => [Promise.reject(new Error('Server error')), { cancel: vi.fn() }] as never,
);

await createUpload([getFolderToUpload('Photos')], []).run();

expect(tasksOf(TaskType.UploadFolder)[0]).toEqual(
expect.objectContaining({ status: TaskStatus.Error, subtitle: 'tasks.subtitles.upload-failed' }),
);
});

test('When a file fails, then its task shows the reason', async () => {
(uploadFile as Mock).mockRejectedValue(new Error('Server error'));

await createUpload([], [getFileToUpload('report.pdf')]).run();

expect(tasksOf(TaskType.UploadFile)[0]).toEqual(
expect.objectContaining({ status: TaskStatus.Error, subtitle: 'tasks.subtitles.upload-failed' }),
);
});

test('When the user cancels a file, then only that file stops', async () => {
fakeUploadsTaking(500);
const upload = createUpload([], [getFileToUpload('report.pdf'), getFileToUpload('invoice.pdf')]);

const running = upload.run();
await vi.waitFor(() => expect(uploadFile).toHaveBeenCalledTimes(2));
const [reportTask, invoiceTask] = tasksOf(TaskType.UploadFile);
await tasksService.cancelTask(reportTask.id);
await running;

expect(tasksService.findTask(reportTask.id)?.status).toBe(TaskStatus.Cancelled);
expect(tasksService.findTask(invoiceTask.id)?.status).toBe(TaskStatus.Success);
});

test('When the user cancels a file, then its saved upload state is removed', async () => {
fakeUploadsTaking(500);
const upload = createUpload([], [getFileToUpload('report.pdf')]);

const running = upload.run();
await vi.waitFor(() => expect(uploadFile).toHaveBeenCalled());
const [reportTask] = tasksOf(TaskType.UploadFile);
await tasksService.cancelTask(reportTask.id);
await running;

expect(uploadRepository.removeUploadState).toHaveBeenCalledWith(reportTask.id);
});

test('When the user cancels a folder, then its upload stops and the folder already created is removed', async () => {
fakeUploadsTaking(500);
const onItemRemoved = vi.fn();
const upload = createUpload([getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('beach.png')] })], [], {
onItemRemoved,
});

const running = upload.run();
await vi.waitFor(() => expect(uploadFile).toHaveBeenCalled());
await tasksService.cancelTask(tasksOf(TaskType.UploadFolder)[0].id);
await running;

expect(newStorageService.deleteFolderByUuid).toHaveBeenCalledWith('Photos-uuid');
expect(onItemRemoved).toHaveBeenCalledWith(expect.objectContaining({ uuid: 'Photos-uuid' }), DESTINATION_UUID);
expect(tasksOf(TaskType.UploadFolder)[0].status).toBe(TaskStatus.Cancelled);
});

test('When the user cancels a folder, then the upload finishes only once the folder is removed', async () => {
fakeUploadsTaking(500);
let finishRemoval: () => void = () => undefined;
vi.mocked(newStorageService.deleteFolderByUuid).mockReturnValue(
new Promise<void>((resolve) => (finishRemoval = resolve)) as never,
);
const upload = createUpload([getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('beach.png')] })], []);
const onFinished = vi.fn();

const running = upload.run().then(onFinished);
await vi.waitFor(() => expect(uploadFile).toHaveBeenCalled());
await tasksService.cancelTask(tasksOf(TaskType.UploadFolder)[0].id);
await vi.waitFor(() => expect(newStorageService.deleteFolderByUuid).toHaveBeenCalled());
await new Promise((resolve) => setTimeout(resolve, 600));

expect(onFinished).not.toHaveBeenCalled();
finishRemoval();
await running;
expect(onFinished).toHaveBeenCalled();
});

test('When the cancelled folder cannot be removed, then its task shows the error', async () => {
fakeUploadsTaking(500);
vi.mocked(newStorageService.deleteFolderByUuid).mockRejectedValue(new Error('Server error'));
const onItemRemoved = vi.fn();
const upload = createUpload([getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('beach.png')] })], [], {
onItemRemoved,
});

const running = upload.run();
await vi.waitFor(() => expect(uploadFile).toHaveBeenCalled());
await tasksService.cancelTask(tasksOf(TaskType.UploadFolder)[0].id);
await running;

expect(tasksOf(TaskType.UploadFolder)[0]).toEqual(
expect.objectContaining({ status: TaskStatus.Error, subtitle: 'tasks.subtitles.folder-removal-failed' }),
);
expect(onItemRemoved).not.toHaveBeenCalled();
});

test('When the user cancels a folder while it is being created, then it is removed once it exists', async () => {
fakeFolderCreationTaking(300);
const upload = createUpload([getFolderToUpload('Photos', { childrenFiles: [getFileToUpload('beach.png')] })], []);

const running = upload.run();
await vi.waitFor(() => expect(folderService.createFolderByUuid).toHaveBeenCalled());
await tasksService.cancelTask(tasksOf(TaskType.UploadFolder)[0].id);
await running;

expect(newStorageService.deleteFolderByUuid).toHaveBeenCalledWith('Photos-uuid');
expect(uploadFile).not.toHaveBeenCalled();
});

test('When the storage fills up while uploading, then the user is told once, however many items fail', async () => {
const onStorageFull = vi.fn();
failUploadsOf(['report', 'invoice'], new AppError('No space left', HTTP_STATUS_CODES.MAX_SPACE_USED));

await createUpload([], [getFileToUpload('report.pdf'), getFileToUpload('invoice.pdf')], { onStorageFull }).run();

expect(onStorageFull).toHaveBeenCalledOnce();
});
});
Loading
Loading