Skip to content

Commit b3708e1

Browse files
authored
Merge pull request #2753 from pbugnion/tests-and-tidy-file-upload
Tests and tidy file upload
2 parents 28ab143 + 6168249 commit b3708e1

File tree

3 files changed

+173
-31
lines changed

3 files changed

+173
-31
lines changed

packages/controls/src/widget_upload.ts

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
import { CoreDOMWidgetModel } from './widget_core';
55
import { DOMWidgetView } from '@jupyter-widgets/base';
66

7+
interface IFileUploaded {
8+
content: ArrayBuffer;
9+
name: string;
10+
size: number;
11+
type: string;
12+
lastModified: number;
13+
error: string;
14+
}
15+
716
export class FileUploadModel extends CoreDOMWidgetModel {
817
defaults(): Backbone.ObjectHash {
918
return {
@@ -16,7 +25,7 @@ export class FileUploadModel extends CoreDOMWidgetModel {
1625
icon: 'upload',
1726
button_style: '',
1827
multiple: false,
19-
value: [],
28+
value: [], // has type Array<IFileUploaded>
2029
error: '',
2130
style: null
2231
};
@@ -32,7 +41,6 @@ export class FileUploadModel extends CoreDOMWidgetModel {
3241
export class FileUploadView extends DOMWidgetView {
3342
el: HTMLButtonElement;
3443
fileInput: HTMLInputElement;
35-
fileReader: FileReader;
3644

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

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

6168
this.fileInput.addEventListener('change', () => {
62-
const promisesFile: Promise<{
63-
buffer: any;
64-
metadata: any;
65-
error: string;
66-
}>[] = [];
69+
const promisesFile: Array<Promise<IFileUploaded>> = [];
6770

68-
Array.from(this.fileInput.files ?? []).forEach(file => {
71+
Array.from(this.fileInput.files ?? []).forEach((file: File) => {
6972
promisesFile.push(
7073
new Promise((resolve, reject) => {
71-
const metadata = {
72-
name: file.name,
73-
type: file.type,
74-
size: file.size,
75-
lastModified: file.lastModified
76-
};
77-
this.fileReader = new FileReader();
78-
this.fileReader.onload = (event): any => {
79-
const buffer = (event as any).target.result;
74+
const fileReader = new FileReader();
75+
fileReader.onload = (): void => {
76+
// We know we can read the result as an array buffer since
77+
// we use the `.readAsArrayBuffer` method
78+
const content: ArrayBuffer = fileReader.result as ArrayBuffer;
8079
resolve({
81-
buffer,
82-
metadata,
80+
content,
81+
name: file.name,
82+
type: file.type,
83+
size: file.size,
84+
lastModified: file.lastModified,
8385
error: ''
8486
});
8587
};
86-
this.fileReader.onerror = (): any => {
88+
fileReader.onerror = (): void => {
8789
reject();
8890
};
89-
this.fileReader.onabort = this.fileReader.onerror;
90-
this.fileReader.readAsArrayBuffer(file);
91+
fileReader.onabort = fileReader.onerror;
92+
fileReader.readAsArrayBuffer(file);
9193
})
9294
);
9395
});
9496

9597
Promise.all(promisesFile)
96-
.then(contents => {
97-
const value = contents.map(c => {
98-
return {
99-
...c.metadata,
100-
content: c.buffer
101-
};
102-
});
98+
.then((files: Array<IFileUploaded>) => {
10399
this.model.set({
104-
value,
100+
value: files,
105101
error: ''
106102
});
107103
this.touch();

packages/controls/test/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33

44
import './widget_date_test';
55
import './widget_string_test';
6+
import './widget_upload_test';
67
import './lumino/currentselection_test';
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { DummyManager } from './dummy-manager';
2+
3+
import { expect } from 'chai';
4+
5+
import * as widgets from '../../lib';
6+
7+
function getFileInput(view: widgets.FileUploadView): HTMLInputElement {
8+
const elem = view.fileInput;
9+
return elem as HTMLInputElement;
10+
}
11+
12+
function getProxyButton(view: widgets.FileUploadView): HTMLButtonElement {
13+
const elem = view.el;
14+
return elem as HTMLButtonElement;
15+
}
16+
17+
function fileInputForModel(model: widgets.FileUploadModel): HTMLInputElement {
18+
// For a given model, create and render a view and return the
19+
// view's input.
20+
const options = { model };
21+
const view = new widgets.FileUploadView(options);
22+
view.render();
23+
return getFileInput(view);
24+
}
25+
26+
function proxyButtonForModel(
27+
model: widgets.FileUploadModel
28+
): HTMLButtonElement {
29+
const options = { model };
30+
const view = new widgets.FileUploadView(options);
31+
view.render();
32+
return getProxyButton(view);
33+
}
34+
35+
function simulateUpload(fileInput: HTMLInputElement, files: Array<File>): void {
36+
// The 'files' property on an input element is normally not writeable
37+
// programmatically, so we explicitly overwrite it.
38+
39+
// The type of fileInput.files is FileList, an Array with an
40+
// extra `.item` method.
41+
const fileList: any = files;
42+
fileList.item = (index: number): File => files[index];
43+
Object.defineProperty(fileInput, 'files', {
44+
value: fileList,
45+
writable: false
46+
});
47+
fileInput.dispatchEvent(new Event('change', { bubbles: true }));
48+
}
49+
50+
describe('FileUploadView', function() {
51+
beforeEach(async function() {
52+
this.manager = new DummyManager();
53+
const modelId = 'u-u-i-d';
54+
this.model = await this.manager.new_model(
55+
{
56+
model_name: 'FileUploadModel',
57+
model_module: '@jupyter-widgets/controls',
58+
model_module_version: '1.0.0',
59+
model_id: modelId
60+
},
61+
{}
62+
);
63+
});
64+
65+
it('construction', function() {
66+
const options = { model: this.model };
67+
const view = new widgets.FileUploadView(options);
68+
expect(view).to.not.be.undefined;
69+
});
70+
71+
it('default options', function() {
72+
const options = { model: this.model };
73+
const view = new widgets.FileUploadView(options);
74+
view.render();
75+
const fileInput = getFileInput(view);
76+
const proxyButton = getProxyButton(view);
77+
expect(fileInput.disabled).to.be.false;
78+
expect(fileInput.multiple).to.be.false;
79+
expect(proxyButton.innerText).to.equal('Upload (0)');
80+
expect(proxyButton.querySelector('i')).to.not.be.null;
81+
expect(proxyButton.querySelector('i')!.className).to.equal('fa fa-upload');
82+
});
83+
84+
it('multiple', function() {
85+
this.model.set('multiple', true);
86+
const fileInput = fileInputForModel(this.model);
87+
expect(fileInput.multiple).to.be.true;
88+
});
89+
90+
it('accept', function() {
91+
this.model.set('accept', 'text/csv');
92+
const fileInput = fileInputForModel(this.model);
93+
expect(fileInput.accept).to.equal('text/csv');
94+
});
95+
96+
it('disabled', function() {
97+
this.model.set('disabled', true);
98+
const proxyButton = proxyButtonForModel(this.model);
99+
expect(proxyButton.disabled).to.be.true;
100+
});
101+
102+
it('no icon', function() {
103+
this.model.set('icon', '');
104+
const proxyButton = proxyButtonForModel(this.model);
105+
expect(proxyButton.querySelector('i')).to.be.null;
106+
});
107+
108+
it('other icon', function() {
109+
this.model.set('icon', 'check');
110+
const proxyButton = proxyButtonForModel(this.model);
111+
expect(proxyButton.querySelector('i')).to.not.be.null;
112+
expect(proxyButton.querySelector('i')!.className).to.equal('fa fa-check');
113+
});
114+
115+
it('description', function() {
116+
this.model.set('description', 'some text');
117+
const proxyButton = proxyButtonForModel(this.model);
118+
expect(proxyButton.innerText).to.equal('some text (0)');
119+
});
120+
121+
it('set model value on upload', function(done) {
122+
const fileInput = fileInputForModel(this.model);
123+
const lastModified = Date.UTC(2019, 0, 0);
124+
125+
const uploadedFile = new File(['some file content'], 'some-name', {
126+
type: 'text/plain',
127+
lastModified
128+
});
129+
130+
simulateUpload(fileInput, [uploadedFile]);
131+
setTimeout(() => {
132+
expect(this.model.get('value')).to.have.length(1);
133+
const [fileInModel] = this.model.get('value');
134+
expect(fileInModel.name).to.equal('some-name');
135+
expect(fileInModel.type).to.equal('text/plain');
136+
expect(fileInModel.lastModified).to.equal(lastModified);
137+
138+
const contentInModel = new TextDecoder('utf-8').decode(
139+
fileInModel.content
140+
);
141+
expect(contentInModel).to.equal('some file content');
142+
done();
143+
}, 100);
144+
});
145+
});

0 commit comments

Comments
 (0)