Angular & Jasmine Unit Test change event for input[type="file"]
Asked Answered
T

2

14

I am trying to go for 100% test coverage, but i can't seem to test the stuff inside this onUploadFile function.

html template

<input type="file" formControlName="theUpload" id="upload" (change)="onUploadFile($event, i, true)">

filing.ts file

onUploadFile(evt: Event, index: number, isReq: boolean = false): void {
  const reader = new FileReader();
  const target = <HTMLInputElement>evt.target;

  if (target.files && target.files.length) {
    const file = target.files[0];
    reader.readAsDataURL(file);
    reader.onload = () => {
      this.getUploadFormGroup(index, isReq).patchValue({
        filename: file.name,
        filetype: file.type,
        value: reader.result.split(',')[1],
        dateUploaded: new Date()
      });

      console.log(
        `getUploadFormArray (${isReq ? 'required' : 'other'} forms)`,
        this.getUploadFormArray(isReq)
      );
    };
  }
}

filing.spec.ts file

it('should call onUploadFile when input is changed for required files', () => {
  spyOn(component, 'onUploadFile').and.callThrough();

  const fakeChangeEvent = new Event('change');

  const targ = <HTMLInputElement>de.nativeElement.querySelector('input#upload');
  targ.dispatchEvent(fakeChangeEvent);

  fixture.whenStable().then(() => {
    expect(component.onUploadFile).toHaveBeenCalledWith(fakeChangeEvent, 0, true);
    expect(targ.files.length).toBeGreaterThan(0); //  this is always zero because i can't add to targ.files (readonly FileList)
  });
});

I am open to mocking whatever can be mocked, but can someone please show me how I might test the console.log function (requiring that i have at least one item in the target.files array?

Teletypewriter answered 22/8, 2018 at 15:52 Comment(0)
V
15

Let's follow the divide and conquer rule - since the aim of Unit Test is to test the parts of component logic separately, I would slightly change the component logic in order to make it easier to Unit Test.

You're setting the anonymous callback function for onload inside your onUploadFile method, so there is no way to spyOn it. As a trade off - you can refactor out the callback method:

export class TestComponent {
  ...
  getLoadCallback(fg: FormGroup, file: File, reader: FileReader): () => void {
    return () => {
      fg.patchValue({
        filename: file.name,
        filetype: file.type,
        value: reader.result.split(',')[1],
        dateUploaded: new Date()
      });

      // do some other stuff here
    };
  }

  onUploadFile(evt: Event, index: number, isReq: boolean = false): void {
    const reader = new FileReader();
    const target = <HTMLInputElement>evt.target;

    if (target.files && target.files.length) {
      const file = target.files[0];
      reader.readAsDataURL(file);
      const fg = this.getUploadFormGroup(index, isReq);
      reader.onload = this.getLoadCallback(fg, file, reader);
    }
  }
  ...
}

So, now we can create at least three Unit Tests: one for triggering onUploadFile on input event (your existing test is good for this), another one for testing getLoadCallback method and lastly - for onUploadFile method:

  it('getLoadCallback', () => {
    const mockValue = ['a', 'b'];
    const result = jasmine.createSpyObj('result', ['split']);
    result.split.and.callFake(() => mockValue);
    const mockReader = { result } as FileReader;
    const mockFormGroup: FormGroup = jasmine.createSpyObj('FormGroup', ['patchValue']);
    const mockFile = new File([''], 'filename', { type: 'text/html' });
    const callback: () => void = component.getLoadCallback(mockFormGroup, mockFile, mockReader);

    callback();

    const obj = {
      filename: mockFile.name,
      filetype: mockFile.type,
      value: mockValue[1],
      dateUploaded: new Date()
    }
    expect(mockFormGroup.patchValue).toHaveBeenCalledWith(obj);
  });

  it('onUploadFile', () => {
    const mockIndex = 1;
    const mockIsReq = false;
    const mockFile = new File([''], 'filename', { type: 'text/html' });
    const mockFormGroup = new FormGroup({});
    const mockEvt = { target: { files: [mockFile] } };
    const mockReader: FileReader = jasmine.createSpyObj('FileReader', ['readAsDataURL', 'onload']);
    spyOn(window as any, 'FileReader').and.returnValue(mockReader);
    spyOn(component, 'getUploadFormGroup').and.returnValue(mockFormGroup);
    spyOn(component, 'getLoadCallback').and.callThrough();

    component.onUploadFile(mockEvt as any, mockIndex, mockIsReq);

    expect((window as any).FileReader).toHaveBeenCalled();
    expect(mockReader.readAsDataURL).toHaveBeenCalledWith(mockFile);
    expect(component.getUploadFormGroup).toHaveBeenCalledWith(mockIndex, mockIsReq);
    expect(component.getLoadCallback).toHaveBeenCalledWith(mockFormGroup, mockFile, mockReader);
  });

Of course you can change mock values and create more unit tests...

Vintage answered 22/8, 2018 at 22:8 Comment(2)
I've created a stackblitz example as well.Vintage
Terrific answer! Thank you for the detail!Teletypewriter
S
0

I ran into this today as well, and I found this simple solution that works

<input type="file" multiple (change)="onFileSelect($event)">
it('should call onFileSelect() when files manually selected with the file input', () => {
  spyOn(component, 'onFileSelect').and.callThrough();
  fixture.detectChanges();

  const file = new File(['this is file content!'], 'dummy.txt');
  const dt = new DataTransfer();
  dt.items.add(file);
  dt.items.add(file);

  const inputElDebug = fixture.debugElement.query(By.css('input[type="file"]'));
  const inputEl: HTMLInputElement = inputElDebug.nativeElement;
  inputEl.files = dt.files;

  const changeEvent = new Event('change');
  inputEl.dispatchEvent(changeEvent);

  fixture.detectChanges();

  expect(component.onFileSelect).toHaveBeenCalled();
  //or
  //expect(component.onFileSelect).toHaveBeenCalledWith(changeEvent);
});

And of course you can leave out the actual files if you don't need to do anything with them, but if you do they are easily made available

Sidero answered 24/5, 2023 at 18:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.