Skip to content

Commit ef8d744

Browse files
committed
fix: Remove some duplicate code in file and util service (#4087)
1 parent 4b5acb3 commit ef8d744

15 files changed

+82
-170
lines changed

src/app/core/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const MAX_FILE_SIZE = 11 * 1024 * 1024;
1+
export const MAX_FILE_SIZE = 11 * 1024 * 1024; // 11MB
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export type ReceiptDetail = {
22
dataUrl: string;
33
type: string;
4-
actionSource: string;
54
};

src/app/core/services/file.service.spec.ts

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe('FileService', () => {
9393
expect(res.content).toBe('dGVzdCBjb250ZW50');
9494
expect(spenderFileService.generateUrlsBulk).toHaveBeenCalledOnceWith([fileId]);
9595
expect(globalThis.fetch).toHaveBeenCalledOnceWith(mockDownloadUrl);
96+
expect(fileService.getDataUrlFromBlob).toHaveBeenCalledOnceWith(mockBlob);
9697
done();
9798
});
9899
});
@@ -422,65 +423,52 @@ describe('FileService', () => {
422423
const mockBlob = new Blob(['test content'], { type: 'image/jpeg' });
423424
const mockDataUrl = 'data:image/jpeg;base64,dGVzdCBjb250ZW50';
424425

425-
// Mock the FileReader to work properly
426-
const mockFileReader = {
427-
readAsDataURL: jasmine.createSpy('readAsDataURL'),
428-
result: mockDataUrl,
429-
onload: null as any,
430-
onerror: null as any,
431-
};
432-
433-
spyOn(window, 'FileReader').and.returnValue(mockFileReader as any);
434426
spyOn(fileService, 'getDataUrlFromBlob').and.resolveTo(mockDataUrl);
435427

436-
const resultPromise = fileService.readFile(mockBlob);
437-
438-
// Simulate the FileReader success
439-
mockFileReader.onload();
428+
const result = await fileService.readFile(mockBlob);
440429

441-
const result = await resultPromise;
442430
expect(result).toBe(mockDataUrl);
443-
expect(mockFileReader.readAsDataURL).toHaveBeenCalledWith(mockBlob);
431+
expect(fileService.getDataUrlFromBlob).toHaveBeenCalledWith(mockBlob);
444432
});
445433

446-
it('should read a HEIC file and convert to JPEG', async () => {
447-
const mockHeicBlob = new Blob(['heic content'], { type: 'image/heic' });
434+
it('should reject when getDataUrlFromBlob fails', async () => {
435+
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
436+
const mockError = new Error('Read error');
448437

449-
// Since heic2any is an external library that's hard to mock in tests,
450-
// let's test the method structure and behavior without triggering the actual conversion
451-
// We'll verify that the method exists and can handle HEIC files
438+
spyOn(fileService, 'getDataUrlFromBlob').and.rejectWith(mockError);
452439

453-
// Mock the getDataUrlFromBlob method to avoid the actual conversion
440+
await expectAsync(fileService.readFile(mockBlob)).toBeRejectedWith(mockError);
441+
expect(fileService.getDataUrlFromBlob).toHaveBeenCalledWith(mockBlob);
442+
});
443+
444+
it('should read a HEIC file and return data URL via heic2any and getDataUrlFromBlob', async () => {
445+
const mockHeicBlob = new Blob(['heic content'], { type: 'image/heic' });
446+
const mockJpegBlob = new Blob(['jpeg content'], { type: 'image/jpeg' });
454447
const mockDataUrl = 'data:image/jpeg;base64,Y29udmVydGVkIGNvbnRlbnQ=';
448+
449+
spyOn(fileService as any, 'heic2any').and.resolveTo(mockJpegBlob);
455450
spyOn(fileService, 'getDataUrlFromBlob').and.resolveTo(mockDataUrl);
456451

457-
// Test that the method exists and is callable
458-
expect(typeof fileService.readFile).toBe('function');
452+
const result = await fileService.readFile(mockHeicBlob);
459453

460-
// The actual heic2any call will fail in tests due to missing native libraries,
461-
// but we can verify the method structure and that it's designed to handle HEIC files
462-
expect(mockHeicBlob.type).toBe('image/heic');
454+
expect(result).toBe(mockDataUrl);
455+
expect((fileService as any).heic2any).toHaveBeenCalledWith({
456+
blob: mockHeicBlob,
457+
toType: 'image/jpeg',
458+
quality: 50,
459+
});
460+
expect(fileService.getDataUrlFromBlob).toHaveBeenCalledWith(mockJpegBlob);
463461
});
464462

465-
it('should handle FileReader errors', async () => {
466-
const mockBlob = new Blob(['test content'], { type: 'text/plain' });
467-
const mockError = new Error('FileReader error');
468-
469-
// Create a mock FileReader that will trigger the error
470-
const mockFileReader = {
471-
readAsDataURL: jasmine.createSpy('readAsDataURL'),
472-
onload: null as any,
473-
onerror: null as any,
474-
};
475-
476-
spyOn(window, 'FileReader').and.returnValue(mockFileReader as any);
477-
478-
const resultPromise = fileService.readFile(mockBlob);
463+
it('should reject when heic2any fails for HEIC image', async () => {
464+
const mockHeicBlob = new Blob(['heic content'], { type: 'image/heic' });
465+
const heicError = new Error('HEIC conversion failed');
479466

480-
// Simulate the error
481-
mockFileReader.onerror(mockError);
467+
spyOn(fileService as any, 'heic2any').and.rejectWith(heicError);
468+
spyOn(fileService, 'getDataUrlFromBlob');
482469

483-
await expectAsync(resultPromise).toBeRejectedWith(mockError);
470+
await expectAsync(fileService.readFile(mockHeicBlob)).toBeRejectedWith(heicError);
471+
expect(fileService.getDataUrlFromBlob).not.toHaveBeenCalled();
484472
});
485473
});
486474
});

src/app/core/services/file.service.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export class FileService {
2323

2424
private approverService = inject(ApproverService);
2525

26+
private heic2any = heic2any;
27+
2628
private findByAdvanceRequestIdWithService(
2729
advanceRequestId: string,
2830
service: SpenderService | ApproverService,
@@ -177,26 +179,14 @@ export class FileService {
177179
readFile(file: Blob): Promise<string> {
178180
return new Promise((resolve, reject) => {
179181
if (file.type === 'image/heic') {
180-
heic2any({
182+
this.heic2any({
181183
blob: file,
182184
toType: 'image/jpeg',
183185
quality: 50,
184186
})
185-
.then((result) => {
186-
this.getDataUrlFromBlob(result as Blob).then((dataUrl) => {
187-
resolve(dataUrl);
188-
});
189-
})
190-
.catch((err) => {
191-
reject(err);
192-
});
187+
.then((result: Blob) => this.getDataUrlFromBlob(result)).then(resolve).catch(reject);
193188
} else {
194-
const fileReader = new FileReader();
195-
fileReader.onload = async (): Promise<void> => {
196-
return resolve(fileReader.result.toString());
197-
};
198-
fileReader.readAsDataURL(file);
199-
fileReader.onerror = (error): void => reject(error);
189+
this.getDataUrlFromBlob(file).then(resolve).catch(reject);
200190
}
201191
});
202192
}

src/app/core/services/utility.service.spec.ts

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { cloneDeep } from 'lodash';
1515
import { TokenService } from './token.service';
1616
import { AuthService } from './auth.service';
1717
import { FeatureConfigService } from './platform/v1/spender/feature-config.service';
18-
import { BehaviorSubject, of, throwError } from 'rxjs';
18+
import { FileService } from './file.service';
19+
import { BehaviorSubject, of } from 'rxjs';
1920
import { apiEouRes } from '../mock-data/extended-org-user.data';
2021
import { featureConfigOptInData } from '../mock-data/feature-config.data';
2122
import { txnCustomPropertiesData } from '../mock-data/txn-custom-properties.data';
@@ -26,11 +27,13 @@ describe('UtilityService', () => {
2627
let tokenService: jasmine.SpyObj<TokenService>;
2728
let authService: jasmine.SpyObj<AuthService>;
2829
let featureConfigService: jasmine.SpyObj<FeatureConfigService>;
30+
let fileService: jasmine.SpyObj<FileService>;
2931

3032
beforeEach(() => {
3133
const tokenServiceSpy = jasmine.createSpyObj('TokenService', ['getClusterDomain']);
3234
const authServiceSpy = jasmine.createSpyObj('AuthService', ['getEou']);
3335
const featureConfigServiceSpy = jasmine.createSpyObj('FeatureConfigService', ['getConfiguration']);
36+
const fileServiceSpy = jasmine.createSpyObj('FileService', ['getDataUrlFromBlob']);
3437

3538
TestBed.configureTestingModule({
3639
providers: [
@@ -47,12 +50,17 @@ describe('UtilityService', () => {
4750
provide: FeatureConfigService,
4851
useValue: featureConfigServiceSpy,
4952
},
53+
{
54+
provide: FileService,
55+
useValue: fileServiceSpy,
56+
},
5057
],
5158
});
5259
utilityService = TestBed.inject(UtilityService);
5360
tokenService = TestBed.inject(TokenService) as jasmine.SpyObj<TokenService>;
5461
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
5562
featureConfigService = TestBed.inject(FeatureConfigService) as jasmine.SpyObj<FeatureConfigService>;
63+
fileService = TestBed.inject(FileService) as jasmine.SpyObj<FileService>;
5664
});
5765

5866
it('should be created', () => {
@@ -610,16 +618,18 @@ describe('UtilityService', () => {
610618
describe('webPathToBase64():', () => {
611619
it('should convert webPath to base64 string', async () => {
612620
const mockBlob = new Blob(['test content'], { type: 'image/png' });
621+
const mockDataUrl = 'data:image/png;base64,dGVzdCBjb250ZW50';
613622
spyOn(window, 'fetch').and.resolveTo({
614623
blob: () => Promise.resolve(mockBlob),
615624
ok: true,
616625
} as Response);
626+
fileService.getDataUrlFromBlob.and.resolveTo(mockDataUrl);
617627

618628
const result = await utilityService.webPathToBase64('https://example.com/image.png');
619629

620-
expect(result).toContain('data:');
621-
expect(result).toContain('base64,');
630+
expect(result).toBe(mockDataUrl);
622631
expect(window.fetch).toHaveBeenCalledWith('https://example.com/image.png');
632+
expect(fileService.getDataUrlFromBlob).toHaveBeenCalledWith(mockBlob);
623633
});
624634

625635
it('should reject on fetch error', async () => {
@@ -629,34 +639,16 @@ describe('UtilityService', () => {
629639
await expectAsync(utilityService.webPathToBase64('https://example.com/image.png')).toBeRejectedWith(error);
630640
});
631641

632-
it('should reject on FileReader error', async () => {
642+
it('should reject when getDataUrlFromBlob fails', async () => {
633643
const mockBlob = new Blob(['test'], { type: 'image/png' });
644+
const readError = new Error('Read error');
634645
spyOn(window, 'fetch').and.resolveTo({
635646
blob: () => Promise.resolve(mockBlob),
636647
ok: true,
637648
} as Response);
649+
fileService.getDataUrlFromBlob.and.rejectWith(readError);
638650

639-
// Mock FileReader to simulate error
640-
const originalFileReader = window.FileReader;
641-
const mockFileReader = jasmine.createSpy('FileReader').and.returnValue({
642-
readAsDataURL: function (blob: Blob) {
643-
setTimeout(() => {
644-
if (this.onerror) {
645-
this.onerror(new Error('Read error') as unknown as ProgressEvent);
646-
}
647-
}, 0);
648-
},
649-
} as unknown as FileReader);
650-
(window as unknown as { FileReader: typeof FileReader }).FileReader = mockFileReader as unknown as typeof FileReader;
651-
652-
try {
653-
await utilityService.webPathToBase64('https://example.com/image.png');
654-
fail('Should have thrown an error');
655-
} catch (error) {
656-
expect(error).toBeDefined();
657-
} finally {
658-
(window as unknown as { FileReader: typeof FileReader }).FileReader = originalFileReader;
659-
}
651+
await expectAsync(utilityService.webPathToBase64('https://example.com/image.png')).toBeRejectedWith(readError);
660652
});
661653
});
662654

-216 Bytes
Binary file not shown.

src/app/fyle/add-edit-expense/add-edit-expense-4.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ export function TestCases4(getTestBed) {
252252
expect(component.attachReceipts).toHaveBeenCalledOnceWith({
253253
type: file.type,
254254
dataUrl: 'base',
255-
actionSource: 'gallery_upload',
256255
});
257256
}));
258257

@@ -360,7 +359,6 @@ export function TestCases4(getTestBed) {
360359
expect(component.attachReceipts).toHaveBeenCalledOnceWith({
361360
dataUrl: 'data-url',
362361
type: 'png',
363-
actionSource: 'camera',
364362
});
365363
expect(component.showSnackBarToast).toHaveBeenCalledOnceWith(
366364
{ message: 'Receipt added to expense successfully' },

src/app/fyle/add-edit-expense/add-edit-expense.page.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4924,7 +4924,7 @@ export class AddEditExpensePage implements OnInit {
49244924
});
49254925
}
49264926

4927-
attachReceipts(data: { type: string; dataUrl: string | ArrayBuffer; actionSource?: string }): void {
4927+
attachReceipts(data: { type: string; dataUrl: string | ArrayBuffer }): void {
49284928
if (data) {
49294929
const fileInfo = {
49304930
type: data.type,
@@ -5033,7 +5033,7 @@ export class AddEditExpensePage implements OnInit {
50335033
}
50345034

50355035
async uploadFileCallback(file: File): Promise<void> {
5036-
let fileData: { type: string; dataUrl: string | ArrayBuffer; actionSource: string };
5036+
let fileData: { type: string; dataUrl: string | ArrayBuffer };
50375037
if (file) {
50385038
if (file.size < MAX_FILE_SIZE) {
50395039
const fileRead$ = from(this.fileService.readFile(file));
@@ -5049,7 +5049,6 @@ export class AddEditExpensePage implements OnInit {
50495049
fileData = {
50505050
type: file.type,
50515051
dataUrl,
5052-
actionSource: 'gallery_upload',
50535052
};
50545053
this.attachReceipts(fileData);
50555054
this.trackingService.addAttachment({ type: file.type });
@@ -5095,7 +5094,6 @@ export class AddEditExpensePage implements OnInit {
50955094
option?: string;
50965095
type: string;
50975096
dataUrl: string;
5098-
actionSource?: string;
50995097
};
51005098
};
51015099

@@ -5124,7 +5122,6 @@ export class AddEditExpensePage implements OnInit {
51245122
receiptDetails = {
51255123
type: this.fileService.getImageTypeFromDataUrl(data.dataUrl),
51265124
dataUrl: data.dataUrl,
5127-
actionSource: 'camera',
51285125
};
51295126
}
51305127
}

0 commit comments

Comments
 (0)