Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add unit tests for DE polling #3231

Merged
merged 3 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading