Skip to content

Commit

Permalink
test: Add unit tests for DE polling (#3231)
Browse files Browse the repository at this point in the history
* Add unit tests for DE polling

* minor

* fix lint
  • Loading branch information
arjunaj5 authored Oct 22, 2024
1 parent 3e8eb82 commit c3deaff
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 117 deletions.
221 changes: 200 additions & 21 deletions src/app/fyle/my-expenses/my-expenses.page.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, TestBed, discardPeriodicTasks, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
import { ActionSheetController, IonicModule, ModalController, NavController, PopoverController } from '@ionic/angular';

import * as dayjs from 'dayjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MatBottomSheet, MatBottomSheetRef } from '@angular/material/bottom-sheet';
Expand All @@ -18,22 +19,14 @@ import {
} from 'src/app/core/mock-data/action-sheet-options.data';
import { allowedExpenseTypes } from 'src/app/core/mock-data/allowed-expense-types.data';
import { apiAuthRes } from 'src/app/core/mock-data/auth-reponse.data';
import { cardAggregateStatParam } from 'src/app/core/mock-data/card-aggregate-stats.data';
import { expectedAssignedCCCStats } from 'src/app/core/mock-data/ccc-expense.details.data';
import {
expectedCriticalPolicyViolationPopoverParams,
expectedCriticalPolicyViolationPopoverParams2,
expectedCriticalPolicyViolationPopoverParams3,
} from 'src/app/core/mock-data/critical-policy-violation-popover.data';
import {
expenseFiltersData1,
expenseFiltersData2,
expenseWithPotentialDuplicateFilterData,
} from 'src/app/core/mock-data/expense-filters.data';
import {
apiExpenseRes,
expectedFormattedTransaction,
expenseData1,
expenseData2,
expenseList4,
expenseListwithoutID,
Expand Down Expand Up @@ -82,8 +75,6 @@ import {
mileageExpenseWithDistance,
perDiemExpenseWithSingleNumDays,
} from 'src/app/core/mock-data/platform/v1/expense.data';
import { reportUnflattenedData } from 'src/app/core/mock-data/report-v1.data';
import { expectedReportSingleResponse } from 'src/app/core/mock-data/report.data';
import { selectedFilters1, selectedFilters2 } from 'src/app/core/mock-data/selected-filters.data';
import {
snackbarPropertiesRes,
Expand All @@ -92,15 +83,11 @@ import {
snackbarPropertiesRes4,
} from 'src/app/core/mock-data/snackbar-properties.data';
import { txnList } from 'src/app/core/mock-data/transaction.data';
import { unflattenedTxnData } from 'src/app/core/mock-data/unflattened-txn.data';
import { unformattedTxnData } from 'src/app/core/mock-data/unformatted-transaction.data';
import { expectedUniqueCardStats } from 'src/app/core/mock-data/unique-cards-stats.data';
import { uniqueCardsData } from 'src/app/core/mock-data/unique-cards.data';
import { AdvancesStates } from 'src/app/core/models/advances-states.model';
import { BackButtonActionPriority } from 'src/app/core/models/back-button-action-priority.enum';
import { Expense } from 'src/app/core/models/expense.model';
import { ExtendedReport } from 'src/app/core/models/report.model';
import { ApiV2Service } from 'src/app/core/services/api-v2.service';
import { CategoriesService } from 'src/app/core/services/categories.service';
import { CorporateCreditCardExpenseService } from 'src/app/core/services/corporate-credit-card-expense.service';
import { CurrencyService } from 'src/app/core/services/currency.service';
Expand Down Expand Up @@ -143,13 +130,14 @@ import { UtilityService } from 'src/app/core/services/utility.service';
import { AuthService } from 'src/app/core/services/auth.service';
import { apiEouRes } from 'src/app/core/mock-data/extended-org-user.data';
import { properties } from 'src/app/core/mock-data/modal-properties.data';
import { ExpensesQueryParams } from 'src/app/core/models/platform/v1/expenses-query-params.model';
import { Expense } from 'src/app/core/models/platform/v1/expense.model';

describe('MyExpensesPage', () => {
let component: MyExpensesPage;
let fixture: ComponentFixture<MyExpensesPage>;
let tasksService: jasmine.SpyObj<TasksService>;
let currencyService: jasmine.SpyObj<CurrencyService>;
let apiV2Service: jasmine.SpyObj<ApiV2Service>;
let transactionService: jasmine.SpyObj<TransactionService>;
let orgSettingsService: jasmine.SpyObj<OrgSettingsService>;
let activatedRoute: jasmine.SpyObj<ActivatedRoute>;
Expand Down Expand Up @@ -185,7 +173,6 @@ describe('MyExpensesPage', () => {
beforeEach(waitForAsync(() => {
const tasksServiceSpy = jasmine.createSpyObj('TasksService', ['getReportsTaskCount', 'getExpensesTaskCount']);
const currencyServiceSpy = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']);
const apiV2ServiceSpy = jasmine.createSpyObj('ApiV2Service', ['extendQueryParamsForTextSearch']);
const transactionServiceSpy = jasmine.createSpyObj('TransactionService', [
'getMyExpensesCount',
'getMyExpenses',
Expand Down Expand Up @@ -322,7 +309,6 @@ describe('MyExpensesPage', () => {
providers: [
{ provide: TasksService, useValue: tasksServiceSpy },
{ provide: CurrencyService, useValue: currencyServiceSpy },
{ provide: ApiV2Service, useValue: apiV2ServiceSpy },
{ provide: TransactionService, useValue: transactionServiceSpy },
{ provide: OrgSettingsService, useValue: orgSettingsServiceSpy },
{ provide: ActivatedRoute, useValue: activatedRouteSpy },
Expand Down Expand Up @@ -449,7 +435,6 @@ describe('MyExpensesPage', () => {
tasksService = TestBed.inject(TasksService) as jasmine.SpyObj<TasksService>;
orgSettingsService = TestBed.inject(OrgSettingsService) as jasmine.SpyObj<OrgSettingsService>;
categoriesService = TestBed.inject(CategoriesService) as jasmine.SpyObj<CategoriesService>;
apiV2Service = TestBed.inject(ApiV2Service) as jasmine.SpyObj<ApiV2Service>;
transactionService = TestBed.inject(TransactionService) as jasmine.SpyObj<TransactionService>;
networkService = TestBed.inject(NetworkService) as jasmine.SpyObj<NetworkService>;
transactionOutboxService = TestBed.inject(TransactionsOutboxService) as jasmine.SpyObj<TransactionsOutboxService>;
Expand Down Expand Up @@ -493,7 +478,7 @@ describe('MyExpensesPage', () => {

describe('ionViewWillEnter(): ', () => {
let backButtonSubscription: Subscription;

const dEincompleteExpenseIds = ['txfCdl3TEZ7K', 'txfCdl3TEZ7l', 'txfCdl3TEZ7m'];
beforeEach(() => {
component.isConnected$ = of(true);
backButtonSubscription = new Subscription();
Expand All @@ -508,6 +493,8 @@ describe('MyExpensesPage', () => {
spyOn(component, 'setAllExpensesCountAndAmount');
spyOn(component, 'clearFilters');
spyOn(component, 'setupActionSheet');
//@ts-ignore
spyOn(component, 'pollDEIncompleteExpenses').and.returnValue(of(apiExpenses1));
tokenService.getClusterDomain.and.resolveTo(apiAuthRes.cluster_domain);
currencyService.getHomeCurrency.and.returnValue(of('USD'));
expensesService.getExpenseStats.and.returnValue(of(completeStats));
Expand Down Expand Up @@ -988,12 +975,204 @@ describe('MyExpensesPage', () => {

expect(expensesService.getExpenses).not.toHaveBeenCalled();
}));

it('should call pollDEIncompleteExpenses if expenses not completed DE scan', () => {
//@ts-ignore
spyOn(component, 'filterDEIncompleteExpenses').and.returnValue(dEincompleteExpenseIds);
component.ionViewWillEnter();

//@ts-ignore
expect(component.pollDEIncompleteExpenses).toHaveBeenCalledWith(dEincompleteExpenseIds, apiExpenses1);
});
});

it('HeaderState(): should return the headerState', () => {
expect(component.HeaderState).toEqual(HeaderState);
});

describe('pollDEIncompleteExpenses()', () => {
beforeEach(() => {
expensesService.getExpenses.and.returnValue(of(apiExpenses1));
//@ts-ignore
spyOn(component, 'updateExpensesList').and.returnValue(apiExpenses1);
});

it('should call expenseService.getExpenses for dE incomplete expenses and return updated expenses', fakeAsync(() => {
const dEincompleteExpenseIds = ['txfCdl3TEZ7K', 'txfCdl3TEZ7l', 'txfCdl3TEZ7m'];
const dEincompleteExpenseIdParams: ExpensesQueryParams = {
queryParams: { id: `in.(${dEincompleteExpenseIds.join(',')})` },
};

//@ts-ignore
component.pollDEIncompleteExpenses(dEincompleteExpenseIds, apiExpenses1).subscribe((result) => {
expect(expensesService.getExpenses).toHaveBeenCalledOnceWith({ ...dEincompleteExpenseIdParams.queryParams });
expect(result).toEqual(apiExpenses1);
});
tick(5000);
discardPeriodicTasks();
}));

it('should call expensesService.getExpenses 5 times and stop polling after 30 seconds', fakeAsync(() => {
const dEincompleteExpenseIds = ['txfCdl3TEZ7K', 'txfCdl3TEZ7l', 'txfCdl3TEZ7m'];
//@ts-ignore
spyOn(component, 'filterDEIncompleteExpenses').and.returnValue(dEincompleteExpenseIds);
//@ts-ignore
component.pollDEIncompleteExpenses(dEincompleteExpenseIds, apiExpenses1).subscribe(() => {});

// Simulate 30 seconds of time passing (the polling interval is 5 seconds)
tick(30000);

// After 30 seconds, polling should stop, so no further calls to getAllExpenses
expect(expensesService.getExpenses).toHaveBeenCalledTimes(5); // If called every 5 seconds after the first 5 seconds

// Cleanup
discardPeriodicTasks();
}));
});

describe('updateExpensesList', () => {
beforeEach(() => {
//@ts-ignore
spyOn(component, 'isExpenseScanComplete').and.callThrough();
});

it('should update expenses with completed scans', () => {
const updatedExpenses: Expense[] = [
{ ...apiExpenses1[0], extracted_data: { ...apiExpenses1[0].extracted_data, amount: 200 } },
];
const dEincompleteExpenseIds = [apiExpenses1[0].id];

//@ts-ignore
component.isExpenseScanComplete.and.returnValue(true);

//@ts-ignore
const result = component.updateExpensesList(apiExpenses1, updatedExpenses, dEincompleteExpenseIds);

expect(result).toEqual([updatedExpenses[0], apiExpenses1[1]]);
});

it('should not update expenses if scan is incomplete', () => {
const updatedExpenses: Expense[] = [
{ ...apiExpenses1[0], extracted_data: { ...apiExpenses1[0].extracted_data, amount: 200 } },
];
const dEincompleteExpenseIds = [apiExpenses1[0].id];

// Mock isExpenseScanComplete to return false for the updated expense
//@ts-ignore
(component.isExpenseScanComplete as jasmine.Spy).and.returnValue(false);

//@ts-ignore
const result = component.updateExpensesList(apiExpenses1, updatedExpenses, dEincompleteExpenseIds);

// Assert
expect(result).toEqual(apiExpenses1); // No changes should occur
});
});

describe('checkIfScanIsCompleted():', () => {
it('should check if scan is complete and return true if the expense amount is not null and no other data is present', () => {
const expense = {
...expenseData,
amount: 100,
claim_amount: null,
extracted_data: null,
};
//@ts-ignore
const result = component.isExpenseScanComplete(expense);
expect(result).toBeTrue();
});

it('should check if scan is complete and return true if the expense user amount is present and no extracted data is available', () => {
const expense = {
...expenseData,
amount: null,
claim_amount: 7500,
extracted_data: null,
};
//@ts-ignore
const result = component.isExpenseScanComplete(expense);
expect(result).toBeTrue();
});

it('should check if scan is complete and return true if the required extracted data is present', () => {
const expense = {
...expenseData,
amount: null,
claim_amount: null,
extracted_data: {
amount: 84.12,
currency: 'USD',
category: 'Professional Services',
date: null,
vendor_name: null,
invoice_dt: null,
},
};
//@ts-ignore
const result = component.isExpenseScanComplete(expense);
expect(result).toBeTrue();
});

it('should return true if the scan has expired', () => {
const expense = {
...expenseData,
amount: null,
claim_amount: null,
extracted_data: null,
};
const oneDaysAfter = dayjs(expense.created_at).add(1, 'day').toDate();
jasmine.clock().mockDate(oneDaysAfter);

//@ts-ignore
const result = component.isExpenseScanComplete(expense);
expect(result).toBeTrue();
});
});

describe('isZeroAmountPerDiemOrMileage():', () => {
it('should check if scan is complete and return true if it is per diem expense with amount 0', () => {
const expense = {
...cloneDeep(expenseData),
amount: 0,
};
expense.category.name = 'Per Diem';
//@ts-ignore
const result = component.isZeroAmountPerDiemOrMileage(expense);
expect(result).toBeTrue();
});

it('should check if scan is complete and return true if it is per diem expense with user amount 0', () => {
const expense = {
...cloneDeep(expenseData),
amount: null,
claim_amount: 0,
};
expense.category.name = 'Per Diem';
//@ts-ignore
const result = component.isZeroAmountPerDiemOrMileage(expense);
expect(result).toBeTrue();
});

it('should check if scan is complete and return true if it is mileage expense with amount 0', () => {
const expense = {
...cloneDeep(expenseData),
amount: 0,
};
expense.category.name = 'Mileage';
//@ts-ignore
const result = component.isZeroAmountPerDiemOrMileage(expense);
expect(result).toBeTrue();
});

it('should return false if org category is null', () => {
const expense = cloneDeep(expenseData);
expense.category.name = null;
//@ts-ignore
const result = component.isZeroAmountPerDiemOrMileage(expense);
expect(result).toBeFalse();
});
});

describe('clearText', () => {
let dispatchEventSpy: jasmine.Spy;
beforeEach(() => {
Expand Down Expand Up @@ -2947,7 +3126,7 @@ describe('MyExpensesPage', () => {
expect(component.isReportableExpensesSelected).toBeTrue();
});

it('should update selectedElements, allExpensesCount and call apiV2Service if checked is true', () => {
it('should update selectedElements, allExpensesCount and call expensesService if checked is true', () => {
expensesService.getAllExpenses.and.returnValue(of(cloneDeep(apiExpenses1)));
component.outboxExpensesToBeDeleted = apiExpenseRes;
component.pendingTransactions = cloneDeep([]);
Expand Down
Loading

0 comments on commit c3deaff

Please sign in to comment.