I have a simple directive that works fine (written with TypeScript but it does not really matter) that allows to get the content of files uploaded via <input type="file">
and HTML5 File API.
The use is simple:
<body ng-controller="MyCtrl as ctrl">
<input type="file" file-read="ctrl.readInputFile(data)">
</body>
callback readInputFile() is being called with the files content.
interface IFileReadScope extends angular.IScope {
fileRead(data: any): void;
}
class FileRead implements angular.IDirective {
restrict = 'A';
scope = {
fileRead: '&'
};
link = (scope: IFileReadScope, element: angular.IAugmentedJQuery) => {
element.on('change', (e: Event) => {
const files: FileList = (<HTMLInputElement> e.target).files;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const reader = new FileReader();
reader.onload = (e: Event) => {
scope.$apply(() => {
scope.fileRead({data: (<FileReader> e.target).result});
});
};
reader.readAsText(file);
}
});
};
static factory(): angular.IDirectiveFactory {
const directive = () => new FileRead();
return directive;
}
}
app.directive('fileRead', FileRead.factory());
And here the test for it:
describe('fileRead', () => {
beforeEach(module('app'));
let $compile: angular.ICompileService;
let scope: any;
beforeEach(inject((_$compile_, _$rootScope_) => {
$compile = _$compile_;
scope = _$rootScope_.$new();
}));
function compileTemplate(template: string): angular.IAugmentedJQuery {
const el = $compile(angular.element(template))(scope);
scope.$digest();
return el;
}
it('should call a callback each time a file has been read', () => {
const el = compileTemplate(
'<input type="file" file-read="readInputFile(data)">'
);
const files = new Array<string>();
scope.readInputFile = (data: any) => {
files.push(data);
};
el.triggerHandler({
type: 'change',
target: {
files: [
new Blob(['data0']),
new Blob(['data1'])
]
}
});
scope.$digest();
// Fails because it does not wait for scope.readInputFile() to be called
expect(files.length).toEqual(2);
expect(files[0]).toEqual('data0');
expect(files[1]).toEqual('data1');
});
});
The problem is that I don't know how to wait for scope.readInputFile()
callback to be executed.
I've tried scope.$digest(), $rootScope.$digest(), $apply, Jasmine spyOn... I've also checked how popular directives do that. It seems like nobody do "that" :/
Any idea?
Edit:
Thanks to Hooray Im Helping and Matho, here the 2 possible solutions using Jasmine 2:
let files: string[];
beforeEach(done => {
const el = compileTemplate(
'<input type="file" file-read="readInputFile(data)">'
);
files = new Array<string>();
scope.readInputFile = (data: any) => {
files.push(data);
if (files.length === 2) done();
};
el.triggerHandler({
type: 'change',
target: {
files: [
new Blob(['data0']),
new Blob(['data1'])
]
}
});
scope.$digest();
});
it('should call a callback each time a file has been read', () => {
expect(files.length).toEqual(2);
expect(files[0]).toEqual('data0');
expect(files[1]).toEqual('data1');
});
And the more elegant one in this case:
it('should call a callback each time a file has been read', done => {
const el = compileTemplate(
'<input type="file" file-read="readInputFile(data)">'
);
const files = new Array<string>();
scope.readInputFile = (data: any) => {
files.push(data);
if (files.length === 2) {
expect(files[0]).toEqual('data0');
expect(files[1]).toEqual('data1');
done();
}
};
el.triggerHandler({
type: 'change',
target: {
files: [
new Blob(['data0']),
new Blob(['data1'])
]
}
});
scope.$digest();
});
If scope.fileRead()
is not executed inside the directive, then the test will fail:
FAILED fileRead should call a callback each time a file has been read
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
So if the code breaks the unit test will fail => job done :)