Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions src/app/shared/upload/uploader/uploader-complete-event.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* An interface that represents a completed single-file upload, carrying both the
* parsed response body and the client-side file name of the file that completed.
*/
export interface UploaderCompleteEvent {
/**
* The parsed response body (e.g. the submission object returned by REST)
*/
response: any;

/**
* The client-side name of the file that completed uploading. Present only when a
* non-empty file name is known — an empty file name is never emitted, so the presence
* of this key means a usable name is available. Whitespace-only names are not trimmed.
*/
fileName?: string;
}
229 changes: 228 additions & 1 deletion src/app/shared/upload/uploader/uploader.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DragService } from '../../../core/drag.service';
import { UploaderOptions } from './uploader-options.model';
import { UploaderComponent } from './uploader.component';
import { FileUploadModule } from 'ng2-file-upload';
import { TranslateModule } from '@ngx-translate/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { createTestComponent } from '../../testing/utils.test';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { CookieService } from '../../../core/services/cookie.service';
Expand Down Expand Up @@ -69,6 +69,233 @@ describe('Chips component', () => {
expect(app).toBeDefined();
}));

it('should emit both onCompleteItem and onCompleteItemWithFile on a completed upload', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItem, 'emit');
spyOn(app.onCompleteItemWithFile, 'emit');

const parsed = { foo: 'bar' };
app.uploader.onCompleteItem({ file: { name: 'test.pdf' } } as any, JSON.stringify(parsed), 200, {});

expect(app.onCompleteItem.emit).toHaveBeenCalledWith(parsed);
expect(app.onCompleteItem.emit).toHaveBeenCalledTimes(1);
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed, fileName: 'test.pdf' });
}));

it('should not emit either completion output when the response body is empty', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItem, 'emit');
spyOn(app.onCompleteItemWithFile, 'emit');

app.uploader.onCompleteItem({ file: { name: 'test.pdf' } } as any, '', 204, {});

expect(app.onCompleteItem.emit).not.toHaveBeenCalled();
expect(app.onCompleteItemWithFile.emit).not.toHaveBeenCalled();
}));

it('should omit fileName from the completion event when the item is undefined', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItemWithFile, 'emit');

const parsed = { foo: 'bar' };
app.uploader.onCompleteItem(undefined, JSON.stringify(parsed), 200, {});

const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
expect(Object.keys(arg)).toEqual(['response']);
expect('fileName' in arg).toBeFalse();
}));

it('should omit fileName from the completion event when the item has no file', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItemWithFile, 'emit');

const parsed = { foo: 'bar' };
app.uploader.onCompleteItem({} as any, JSON.stringify(parsed), 200, {});

const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
expect(Object.keys(arg)).toEqual(['response']);
expect('fileName' in arg).toBeFalse();
}));

it('should omit fileName from the completion event when the file has no name', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItemWithFile, 'emit');

const parsed = { foo: 'bar' };
app.uploader.onCompleteItem({ file: {} } as any, JSON.stringify(parsed), 200, {});

const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
expect(Object.keys(arg)).toEqual(['response']);
expect('fileName' in arg).toBeFalse();
}));

it('should omit fileName from the completion event when the file name is an empty string', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItemWithFile, 'emit');

const parsed = { foo: 'bar' };
app.uploader.onCompleteItem({ file: { name: '' } } as any, JSON.stringify(parsed), 200, {});

const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
expect(Object.keys(arg)).toEqual(['response']);
expect('fileName' in arg).toBeFalse();
}));

it('should keep a whitespace-only file name on the completion event', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItemWithFile, 'emit');

const parsed = { foo: 'bar' };
app.uploader.onCompleteItem({ file: { name: ' ' } } as any, JSON.stringify(parsed), 200, {});

expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed, fileName: ' ' });
}));

it('should emit a distinct file name for each of two sequential completed uploads', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onCompleteItemWithFile, 'emit');

app.uploader.onCompleteItem({ file: { name: 'first.pdf' } } as any, JSON.stringify({ n: 1 }), 200, {});
app.uploader.onCompleteItem({ file: { name: 'second.pdf' } } as any, JSON.stringify({ n: 2 }), 200, {});

const emitSpy = app.onCompleteItemWithFile.emit as jasmine.Spy;
expect(emitSpy).toHaveBeenCalledTimes(2);
expect(emitSpy.calls.argsFor(0)[0]).toEqual({ response: { n: 1 }, fileName: 'first.pdf' });
expect(emitSpy.calls.argsFor(1)[0]).toEqual({ response: { n: 2 }, fileName: 'second.pdf' });
}));

it('should emit onUploadError with the item, response, status and headers of the failed upload', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

spyOn(app.onUploadError, 'emit');

app.uploader.onErrorItem({ file: { name: 'broken.zip' } } as any, 'boom', 500, {});

expect(app.onUploadError.emit).toHaveBeenCalledWith({
item: { file: { name: 'broken.zip' } },
response: 'boom',
status: 500,
headers: {},
});
}));

it('should emit the un-interpolated size-limit message when a file exceeds the maximum upload size', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

const instantSpy = spyOn(TestBed.inject(TranslateService), 'instant').and.returnValue('SIZE-LIMIT-MSG');
spyOn(app.onUploadError, 'emit');

app.uploader.options.maxFileSize = 1024;
app.uploader.onWhenAddingFileFailed({ name: 'big.zip', size: 2048 } as any, null, app.uploader.options);

expect(instantSpy).toHaveBeenCalledWith('submission.sections.upload.upload-failed.size-limit-exceeded');
expect(app.onUploadError.emit).toHaveBeenCalledWith(jasmine.objectContaining({
status: 400,
response: 'SIZE-LIMIT-MSG',
}));
}));

it('should not pass interpolation params to the size-limit instant() call', inject([UploaderComponent], (app: UploaderComponent) => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();

const instantSpy = spyOn(TestBed.inject(TranslateService), 'instant').and.returnValue('SIZE-LIMIT-MSG');

app.uploader.options.maxFileSize = 1024;
app.uploader.onWhenAddingFileFailed({ name: 'big.zip', size: 2048 } as any, null, app.uploader.options);

expect(instantSpy.calls.count()).toBe(1);
expect(instantSpy.calls.mostRecent().args.length).toBe(1);
}));

});

// declare a test component
Expand Down
12 changes: 11 additions & 1 deletion src/app/shared/upload/uploader/uploader.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { FileUploader } from 'ng2-file-upload';
import uniqueId from 'lodash/uniqueId';
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';

import { UploaderCompleteEvent } from './uploader-complete-event.model';
import { UploaderError } from './uploader-error.model';
import { UploaderOptions } from './uploader-options.model';
import { hasValue, isNotEmpty, isUndefined } from '../../empty.util';
import { UploaderProperties } from './uploader-properties.model';
Expand Down Expand Up @@ -89,10 +91,16 @@ export class UploaderComponent implements OnInit, AfterViewInit {
*/
@Output() onCompleteItem: EventEmitter<any> = new EventEmitter<any>();

/**
* The function to call when upload is completed, carrying the parsed response together with the
* client-side file name. Emitted alongside {@link onCompleteItem} so existing consumers are unaffected.
*/
@Output() onCompleteItemWithFile: EventEmitter<UploaderCompleteEvent> = new EventEmitter<UploaderCompleteEvent>();

/**
* The function to call on error occurred
*/
@Output() onUploadError: EventEmitter<any> = new EventEmitter<any>();
@Output() onUploadError: EventEmitter<UploaderError> = new EventEmitter<UploaderError>();

/**
* The function to call when a file is selected
Expand Down Expand Up @@ -201,6 +209,8 @@ export class UploaderComponent implements OnInit, AfterViewInit {
if (isNotEmpty(response)) {
const responsePath = JSON.parse(response);
this.onCompleteItem.emit(responsePath);
const fileName = item?.file?.name;
this.onCompleteItemWithFile.emit(isNotEmpty(fileName) ? { response: responsePath, fileName } : { response: responsePath });
}
};
this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
[enableDragOverDocument]="enableDragOverDocument"
[onBeforeUpload]="onBeforeUpload"
[uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem($event)"
(onCompleteItemWithFile)="onCompleteItem($event)"
(onUploadError)="onUploadError($event)"></ds-uploader>
Loading
Loading