Skip to content
Open
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;
}
122 changes: 122 additions & 0 deletions src/app/shared/upload/uploader/uploader.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ describe('UploaderComponent', () => {
let testFixture: ComponentFixture<TestComponent>;
let html;

/**
* Bring an injected UploaderComponent instance to the state in which its ng2-file-upload
* callbacks are installed, so that `app.uploader.onCompleteItem(...)` runs the component's code.
*/
const driveUploader = (app: UploaderComponent): void => {
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
url: 'http://test',
authToken: null,
disableMultipart: false,
itemAlias: null,
});
app.ngOnInit();
app.ngAfterViewInit();
};

// waitForAsync beforeEach
beforeEach(waitForAsync(() => {

Expand Down Expand Up @@ -68,6 +83,113 @@ describe('UploaderComponent', () => {
expect(app).toBeDefined();
}));

it('should emit both the legacy completion output and the new completion event on a completed upload', inject([UploaderComponent], (app: UploaderComponent) => {
driveUploader(app);
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 emit a distinct file name for each of two sequential completed uploads', inject([UploaderComponent], (app: UploaderComponent) => {
driveUploader(app);
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 calls = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls;
expect(calls.count()).toBe(2);
expect(calls.argsFor(0)[0]).toEqual({ response: { n: 1 }, fileName: 'first.pdf' });
expect(calls.argsFor(1)[0]).toEqual({ response: { n: 2 }, fileName: 'second.pdf' });
}));

it('should omit fileName from the completion event when the item is undefined', inject([UploaderComponent], (app: UploaderComponent) => {
driveUploader(app);
spyOn(app.onCompleteItemWithFile, 'emit');

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

const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: { foo: 'bar' } });
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) => {
driveUploader(app);
spyOn(app.onCompleteItemWithFile, 'emit');

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

const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: { foo: 'bar' } });
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) => {
driveUploader(app);
spyOn(app.onCompleteItemWithFile, 'emit');

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

const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: { foo: 'bar' } });
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) => {
driveUploader(app);
spyOn(app.onCompleteItemWithFile, 'emit');

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

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

it('should keep a whitespace-only file name on the completion event', inject([UploaderComponent], (app: UploaderComponent) => {
driveUploader(app);
spyOn(app.onCompleteItemWithFile, 'emit');

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

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

it('should not emit either completion output when the response body is empty', inject([UploaderComponent], (app: UploaderComponent) => {
driveUploader(app);
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 emit onUploadError with the item, response, status and headers of the failed upload', inject([UploaderComponent], (app: UploaderComponent) => {
driveUploader(app);
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: {},
});
}));

});

// 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 @@ -35,6 +35,8 @@ import { of } from 'rxjs';

import { BtnDisabledDirective } from '../../btn-disabled.directive';
import { LiveRegionService } from '../../live-region/live-region.service';
import { UploaderCompleteEvent } from './uploader-complete-event.model';
import { UploaderError } from './uploader-error.model';
import { UploaderOptions } from './uploader-options.model';
import { UploaderProperties } from './uploader-properties.model';

Expand Down Expand Up @@ -103,10 +105,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 @@ -227,6 +235,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 @@ -5,6 +5,6 @@
[enableDragOverDocument]="enableDragOverDocument"
[onBeforeUpload]="onBeforeUpload"
[uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem($event)"
(onUploadError)="onUploadError()"></ds-uploader>
(onCompleteItemWithFile)="onCompleteItem($event)"
(onUploadError)="onUploadError($event)"></ds-uploader>
}
Loading
Loading