Skip to content

Commit

Permalink
Merge pull request #2753 from pbugnion/tests-and-tidy-file-upload
Browse files Browse the repository at this point in the history
Tests and tidy file upload
  • Loading branch information
jasongrout authored Jan 26, 2020
2 parents 28ab143 + 6168249 commit b3708e1
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 31 deletions.
58 changes: 27 additions & 31 deletions packages/controls/src/widget_upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
import { CoreDOMWidgetModel } from './widget_core';
import { DOMWidgetView } from '@jupyter-widgets/base';

interface IFileUploaded {
content: ArrayBuffer;
name: string;
size: number;
type: string;
lastModified: number;
error: string;
}

export class FileUploadModel extends CoreDOMWidgetModel {
defaults(): Backbone.ObjectHash {
return {
Expand All @@ -16,7 +25,7 @@ export class FileUploadModel extends CoreDOMWidgetModel {
icon: 'upload',
button_style: '',
multiple: false,
value: [],
value: [], // has type Array<IFileUploaded>
error: '',
style: null
};
Expand All @@ -32,7 +41,6 @@ export class FileUploadModel extends CoreDOMWidgetModel {
export class FileUploadView extends DOMWidgetView {
el: HTMLButtonElement;
fileInput: HTMLInputElement;
fileReader: FileReader;

get tagName(): string {
return 'button';
Expand All @@ -48,7 +56,6 @@ export class FileUploadView extends DOMWidgetView {
this.fileInput = document.createElement('input');
this.fileInput.type = 'file';
this.fileInput.style.display = 'none';
this.el.appendChild(this.fileInput);

this.el.addEventListener('click', () => {
this.fileInput.click();
Expand All @@ -59,49 +66,38 @@ export class FileUploadView extends DOMWidgetView {
});

this.fileInput.addEventListener('change', () => {
const promisesFile: Promise<{
buffer: any;
metadata: any;
error: string;
}>[] = [];
const promisesFile: Array<Promise<IFileUploaded>> = [];

Array.from(this.fileInput.files ?? []).forEach(file => {
Array.from(this.fileInput.files ?? []).forEach((file: File) => {
promisesFile.push(
new Promise((resolve, reject) => {
const metadata = {
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified
};
this.fileReader = new FileReader();
this.fileReader.onload = (event): any => {
const buffer = (event as any).target.result;
const fileReader = new FileReader();
fileReader.onload = (): void => {
// We know we can read the result as an array buffer since
// we use the `.readAsArrayBuffer` method
const content: ArrayBuffer = fileReader.result as ArrayBuffer;
resolve({
buffer,
metadata,
content,
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified,
error: ''
});
};
this.fileReader.onerror = (): any => {
fileReader.onerror = (): void => {
reject();
};
this.fileReader.onabort = this.fileReader.onerror;
this.fileReader.readAsArrayBuffer(file);
fileReader.onabort = fileReader.onerror;
fileReader.readAsArrayBuffer(file);
})
);
});

Promise.all(promisesFile)
.then(contents => {
const value = contents.map(c => {
return {
...c.metadata,
content: c.buffer
};
});
.then((files: Array<IFileUploaded>) => {
this.model.set({
value,
value: files,
error: ''
});
this.touch();
Expand Down
1 change: 1 addition & 0 deletions packages/controls/test/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@

import './widget_date_test';
import './widget_string_test';
import './widget_upload_test';
import './lumino/currentselection_test';
145 changes: 145 additions & 0 deletions packages/controls/test/src/widget_upload_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { DummyManager } from './dummy-manager';

import { expect } from 'chai';

import * as widgets from '../../lib';

function getFileInput(view: widgets.FileUploadView): HTMLInputElement {
const elem = view.fileInput;
return elem as HTMLInputElement;
}

function getProxyButton(view: widgets.FileUploadView): HTMLButtonElement {
const elem = view.el;
return elem as HTMLButtonElement;
}

function fileInputForModel(model: widgets.FileUploadModel): HTMLInputElement {
// For a given model, create and render a view and return the
// view's input.
const options = { model };
const view = new widgets.FileUploadView(options);
view.render();
return getFileInput(view);
}

function proxyButtonForModel(
model: widgets.FileUploadModel
): HTMLButtonElement {
const options = { model };
const view = new widgets.FileUploadView(options);
view.render();
return getProxyButton(view);
}

function simulateUpload(fileInput: HTMLInputElement, files: Array<File>): void {
// The 'files' property on an input element is normally not writeable
// programmatically, so we explicitly overwrite it.

// The type of fileInput.files is FileList, an Array with an
// extra `.item` method.
const fileList: any = files;
fileList.item = (index: number): File => files[index];
Object.defineProperty(fileInput, 'files', {
value: fileList,
writable: false
});
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
}

describe('FileUploadView', function() {
beforeEach(async function() {
this.manager = new DummyManager();
const modelId = 'u-u-i-d';
this.model = await this.manager.new_model(
{
model_name: 'FileUploadModel',
model_module: '@jupyter-widgets/controls',
model_module_version: '1.0.0',
model_id: modelId
},
{}
);
});

it('construction', function() {
const options = { model: this.model };
const view = new widgets.FileUploadView(options);
expect(view).to.not.be.undefined;
});

it('default options', function() {
const options = { model: this.model };
const view = new widgets.FileUploadView(options);
view.render();
const fileInput = getFileInput(view);
const proxyButton = getProxyButton(view);
expect(fileInput.disabled).to.be.false;
expect(fileInput.multiple).to.be.false;
expect(proxyButton.innerText).to.equal('Upload (0)');
expect(proxyButton.querySelector('i')).to.not.be.null;
expect(proxyButton.querySelector('i')!.className).to.equal('fa fa-upload');
});

it('multiple', function() {
this.model.set('multiple', true);
const fileInput = fileInputForModel(this.model);
expect(fileInput.multiple).to.be.true;
});

it('accept', function() {
this.model.set('accept', 'text/csv');
const fileInput = fileInputForModel(this.model);
expect(fileInput.accept).to.equal('text/csv');
});

it('disabled', function() {
this.model.set('disabled', true);
const proxyButton = proxyButtonForModel(this.model);
expect(proxyButton.disabled).to.be.true;
});

it('no icon', function() {
this.model.set('icon', '');
const proxyButton = proxyButtonForModel(this.model);
expect(proxyButton.querySelector('i')).to.be.null;
});

it('other icon', function() {
this.model.set('icon', 'check');
const proxyButton = proxyButtonForModel(this.model);
expect(proxyButton.querySelector('i')).to.not.be.null;
expect(proxyButton.querySelector('i')!.className).to.equal('fa fa-check');
});

it('description', function() {
this.model.set('description', 'some text');
const proxyButton = proxyButtonForModel(this.model);
expect(proxyButton.innerText).to.equal('some text (0)');
});

it('set model value on upload', function(done) {
const fileInput = fileInputForModel(this.model);
const lastModified = Date.UTC(2019, 0, 0);

const uploadedFile = new File(['some file content'], 'some-name', {
type: 'text/plain',
lastModified
});

simulateUpload(fileInput, [uploadedFile]);
setTimeout(() => {
expect(this.model.get('value')).to.have.length(1);
const [fileInModel] = this.model.get('value');
expect(fileInModel.name).to.equal('some-name');
expect(fileInModel.type).to.equal('text/plain');
expect(fileInModel.lastModified).to.equal(lastModified);

const contentInModel = new TextDecoder('utf-8').decode(
fileInModel.content
);
expect(contentInModel).to.equal('some file content');
done();
}, 100);
});
});

0 comments on commit b3708e1

Please sign in to comment.