import { Injectable } from '@angular/core'; import { GCMockModule } from '@core/mocks/gc-module.mock'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { AdHocReportingUI } from '@core/typings/ui/ad-hoc-reporting.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { FormAudience } from '@features/configure-forms/form.typing'; import { SimpleStringMap } from '@yourcause/common'; import { AfterEach, BeforeEach, Spec, TestCase } from '@yourcause/test-decorators'; import { DescribeAngularService } from '@yourcause/test-decorators/angular'; import { expect, spy } from 'chai'; import { AddEditDataTableOptionModalResponse } from '../add-edit-custom-option-modal/add-edit-custom-option-modal.component'; import { CustomDataTablesService } from '../custom-data-table.service'; import { CustomDataTablesResources } from '../custom-data-tables.resources'; import { CustomDataTable, CustomDataTableDetailViewOption, CustomDataTableOption, KeyValue, KeyValueFromAPI, PicklistConflict, PicklistDataType, UpdateSortOrderPayload } from '../custom-data-tables.typing'; @Injectable({ providedIn: 'root' }) @DescribeAngularService(CustomDataTablesService, { imports: [ GCMockModule ] }) export class CustomDataTablesServiceSpec implements Spec< CustomDataTablesServiceSpec, CustomDataTablesService > { id = 1; guid = '5236fd96-d448-4e1e-b8d9-db5df84c2aa0'; sampleKey1 = 'Sample Key 1'; sampleKey2 = 'Sample Key 2'; sampleValue1 = 'Sample Value 1'; sampleValue2 = 'Sample Value 2'; keyValues: KeyValueFromAPI[] = [{ key: this.sampleKey1, value: this.sampleValue1, sortOrder: 1, parentKeys: [], parentKeysString: '', inUse: true, picklistGuid: this.guid }, { key: this.sampleKey2, value: this.sampleValue2, sortOrder: 2, parentKeys: [], parentKeysString: '', inUse: false, picklistGuid: this.guid }]; rootObject: AdHocReportingUI.RootObject = { chartEndpoint: 'GetApplicationFormsChart', columns: [], display: 'Custom form responses', editEndpoint: 'ApplicationForms', i18nKey: 'reporting:textCustomFormResponses', isRootObject: true, omitRootObject: true, previewChartEndpoint: 'GetApplicationFormsChartPreview', property: 'formData', readOnlyEndpoint: 'ApplicationForm', relatedObjects: [ 'application', 'organization', 'applicant', 'program', 'assignedBudget', 'assignedFundingSource', 'workflowLevel', 'cycle', 'nominee', 'form' ], supportsSummaryWidgets: false, type: 2 }; customDataTables: CustomDataTable[] = [{ id: this.id, name: 'Sample Data Table', guid: this.guid, createdDate: new Date().toISOString(), updatedDate: new Date().toISOString(), createdBy: 'Tina Conway', updatedBy: 'Tina Conway', isSystem: false, hasOptions: true, defaultLanguageId: 'en-US', dataType: PicklistDataType.Text, parentPicklistId: null }, { id: 2, name: 'Sample Data Table', guid: '5236fd96-d448-4e1e-b8d9-db5df84c2aa1', createdDate: new Date().toISOString(), updatedDate: new Date().toISOString(), createdBy: 'System', updatedBy: 'System', isSystem: true, hasOptions: true, dataType: PicklistDataType.Text, defaultLanguageId: 'en-US', parentPicklistId: null }]; option1Id = 1; option2Id = 2; customDataTableOptions: CustomDataTableOption[] = [{ id: this.option1Id, key: 'Sample Key', values: [{ languageId: 'en-US', text: 'Sample Text' }], inUse: true, sortOrder: 1, createdDate: new Date().toISOString(), updatedDate: new Date().toISOString(), picklistOptionDependentPicklists: [] }, { id: this.option2Id, key: 'Sample Key 2', values: [{ languageId: 'en-US', text: 'Sample Text 2' }], inUse: true, sortOrder: 2, createdDate: new Date().toISOString(), updatedDate: new Date().toISOString(), picklistOptionDependentPicklists: [] }]; invalidFile = [{ key: 'Sample Key 1' }, { key: 'Sample Key 2', value: 'Sample Value 2' }]; categoryId = 1; categoryId2 = 2; textFieldKey = 'textFieldKey'; picklistKey = 'picklistKey'; cdtTableKey = 'tableFileUploadKey'; cdtTableField: ReferenceFieldAPI.ReferenceFieldDisplayModel = { customDataTableGuid: this.guid, name: 'CDT Table Field', description: '', type: ReferenceFieldsUI.ReferenceFieldTypes.CustomDataTable, key: this.cdtTableKey, supportsMultiple: false, categoryId: this.categoryId, formAudience: FormAudience.APPLICANT, referenceFieldId: 165, formCount: 1, usedOnReports: false, createdBy: '', createDate: '', updatedBy: '', aggregateType: null, updateDate: '', parentReferenceFieldId: null, isSingleResponse: true, isEncrypted: false, isMasked: false, tableAllowsImport: false, isTableField: true, aggregateTableReferenceFieldId: null, referenceFieldTableId: null, standardComponentIsPublished: false, isStandardProductField: false, subsetCollectionType: null }; referenceFields: ReferenceFieldAPI.ReferenceFieldDisplayModel[] = [{ customDataTableGuid: '', name: 'Text', description: '', type: ReferenceFieldsUI.ReferenceFieldTypes.TextField, key: this.textFieldKey, supportsMultiple: false, categoryId: this.categoryId, formAudience: FormAudience.APPLICANT, referenceFieldId: 1, formCount: 1, usedOnReports: false, createdBy: '', createDate: '', updatedBy: '', aggregateType: null, updateDate: '', parentReferenceFieldId: null, isSingleResponse: true, isEncrypted: false, isMasked: false, tableAllowsImport: false, isTableField: false, aggregateTableReferenceFieldId: null, referenceFieldTableId: null, standardComponentIsPublished: false, isStandardProductField: false, subsetCollectionType: null }, { customDataTableGuid: this.guid, name: 'Picklist', description: '', type: ReferenceFieldsUI.ReferenceFieldTypes.CustomDataTable, aggregateType: null, key: this.picklistKey, supportsMultiple: false, categoryId: this.categoryId2, formAudience: FormAudience.APPLICANT, referenceFieldId: 1, formCount: 1, usedOnReports: false, createdBy: '', createDate: '', updatedBy: '', updateDate: '', parentReferenceFieldId: null, isSingleResponse: true, isEncrypted: false, isMasked: false, tableAllowsImport: false, isTableField: false, aggregateTableReferenceFieldId: null, referenceFieldTableId: null, standardComponentIsPublished: false, isStandardProductField: false, subsetCollectionType: null }, this.cdtTableField ]; conflictInfo: Record = { 1: { picklistOption: { picklistId: 1, id: 1, key: 'one', value: 'One' }, conflictResolutionRequired: true, picklistOptionWithSameKey: { picklistId: 2, id: 2, key: 'one', value: '1' }, picklistOptionWithSameValue: null, picklistOptionWithSameKeyAndValue: null }, 2: { picklistOption: { picklistId: 1, id: 2, key: 'two', value: 'Two' }, conflictResolutionRequired: false, picklistOptionWithSameKey: null, picklistOptionWithSameValue: null, picklistOptionWithSameKeyAndValue: null }, 3: { picklistOption: { picklistId: 1, id: 3, key: 'three', value: 'Three' }, conflictResolutionRequired: true, picklistOptionWithSameKey: null, picklistOptionWithSameValue: { picklistId: 2, id: 2, key: '3', value: 'Three' }, picklistOptionWithSameKeyAndValue: null } }; tableColumns: ReferenceFieldsUI.TableFieldForUi[] = [{ referenceFieldId: this.cdtTableField.referenceFieldId, referenceFieldName: this.cdtTableField.name, label: this.cdtTableField.name, isRequired: false, showInTable: true, columnOrder: 1, aggregateColumnReferenceFieldId: null, summarizeData: false, summarizeLabel: '', referenceField: this.cdtTableField }]; constructor ( private customDataTablesResources: CustomDataTablesResources ) { } @BeforeEach() async mock (service: CustomDataTablesService) { service['customDataTableResources'] = this.customDataTablesResources; this.customDataTablesResources.getKeyValuesByGuid = async () => { return this.keyValues; }; this.customDataTablesResources.getBulkCustomDataTables = async () => { return this.keyValues; }; this.customDataTablesResources.getCustomDataTableOptions = async () => { return this.customDataTableOptions; }; this.customDataTablesResources.getCustomTableDataList = async () => { return this.customDataTables; }; this.customDataTablesResources.getMergeConflictsForPicklists = async () => { return { conflictInfo: this.conflictInfo, conflictResolutionRequiredCount: 2 }; }; await service.setCustomDataTables(); } @AfterEach() restore () { spy.restore(); } @TestCase('should be able get and set customDataTableOptionsMap') getCustomDataTableOptionsMap ( service: CustomDataTablesService ) { service.setCustomDataTableOptionsMap(this.guid, this.keyValues); const options = service.customDataTableOptionsMap[this.guid]; expect(options.length).to.be.equal(2); } @TestCase('should be able to set customDataTables') async setCustomDataTables ( service: CustomDataTablesService ) { await service.setCustomDataTables(); const dataTables = service.customDataTables; expect(dataTables.length).to.be.equal(2); } @TestCase('should be able to reset customDataTables') async resetCustomDataTables ( service: CustomDataTablesService ) { await service.setCustomDataTables(); await service.resetCustomDataTables(); const dataTables = service.customDataTables; expect(dataTables.length).to.be.equal(2); } @TestCase('should be able to set customDataTableOptions') async setCustomDataTableOptions ( service: CustomDataTablesService ) { await service.setCustomDataTableOptions(this.id); const options = service.customDataTableMap[this.id]; expect(options[0].inUse).to.be.equal(true); } @TestCase('should be able to set setCustomDataTableOptionsForPdf') async setCustomDataTableOptionsForPdf ( service: CustomDataTablesService ) { await service.setCustomDataTableOptionsForPdf([], 'en-US', [], []); const options = service.customDataTableOptionsMap[this.guid]; expect(options.length).to.be.equal(this.keyValues.length); } @TestCase('should be able to check if a file is valid for import') isFileValid ( service: CustomDataTablesService ) { const valid = service.isFileValid(this.keyValues); expect(valid).to.be.equal(true); } @TestCase('should be able to check if a file is invalid for import') isFileInvalid ( service: CustomDataTablesService ) { const valid = service.isFileValid(this.invalidFile as any); expect(valid).to.be.equal(false); } @TestCase('should be able to get most common default lang from array') async getMostCommonDefaultLangFromArray ( service: CustomDataTablesService ) { await service.setCustomDataTables(); const lang = service.getMostCommonDefaultLangFromArray([this.guid]); expect(lang).to.be.equal('en-US'); } @TestCase('should be able to remove empty rows from import') removeEmptyRows (service: CustomDataTablesService) { const options: KeyValue[] = [{ key: '', value: '', inUse: true, parentKeys: [], sortOrder: 1, parentKeysString: '' }, { key: 'Sample Key 2', value: 'Sample Value 2', inUse: true, parentKeys: [], sortOrder: 2, parentKeysString: '' }]; const filtered = service.removeEmptyRows(options); expect(filtered.length).to.be.equal(1); expect(filtered[0].key).to.be.equal('Sample Key 2'); } @TestCase('should be able to get picklistId from guid') getDataTableIdFromGuid (service: CustomDataTablesService) { const dataTableId = service.getCDTIdFromGuid(this.guid); expect(dataTableId).to.be.equal(this.id); } @TestCase('should be able to et options list from category map') async setCustomDataTableOptionsMap (service: CustomDataTablesService) { const map: SimpleStringMap = { ['' + this.categoryId]: [ this.referenceFields[0] ], ['' + this.categoryId2]: [ this.referenceFields[1] ] }; await service.setOptionsListFromCategoryMap(map, [], this.rootObject); expect(service.customDataTableOptionsMap[this.guid]).to.not.be.undefined; expect(service.customDataTableOptionsMap[this.guid].length).to.be.equal( this.keyValues.length ); } @TestCase('should be able to get typeahead options for cdt') async getTypeaheadOptionsForCdt (service: CustomDataTablesService) { service.setCustomDataTableOptionsMap(this.guid, this.keyValues); const options = service.getTypeaheadOptionsForCdt( this.guid, null, false ); const hasSampleKey1 = options.some((opt) => { return opt.value === this.sampleKey1; }); expect(hasSampleKey1).to.be.true; const hasSampleKey2 = options.some((opt) => { return opt.value === this.sampleKey2; }); expect(hasSampleKey2).to.be.false; } @TestCase('should be able to get options for cdt with old value no longer in use') getTypeaheadOptionsForCdtOldValue (service: CustomDataTablesService) { service.setCustomDataTableOptionsMap(this.guid, this.keyValues); const options = service.getTypeaheadOptionsForCdt( this.guid, this.sampleKey2, false ); const hasSampleKey2 = options.some((opt) => { return opt.value === this.sampleKey2; }); expect(hasSampleKey2).to.be.true; } @TestCase('should be able to get all CDT options per form') async getAllCdtOptionsPerForm (service: CustomDataTablesService) { await service.getAllCdtOptionsPerForm([this.guid], true, 'en-US', [123]); const options = service.customDataTableOptionsMap[this.guid]; expect(options).to.not.be.undefined; } @TestCase('should be able to check for duplicate option values - false') async checkForDuplicateOptionValuesFalse (service: CustomDataTablesService) { const result = await service.checkForDuplicateOptionValues( this.id ); // No dupes exist expect(result).to.be.false; } @TestCase('should be able to check for duplicate option values - true') async checkForDuplicateOptionValuesTrue (service: CustomDataTablesService) { service.resetCustomDataTableOptionsMap(); service['customDataTableResources'].getKeyValuesByGuid = async () => { return [{ key: this.sampleKey1, value: 'Sample Value 1', sortOrder: 1, parentKeys: [], parentKeysString: '', inUse: true, picklistGuid: this.guid }, { key: this.sampleKey2, value: 'Sample Value 1', sortOrder: 2, parentKeys: [], parentKeysString: '', inUse: false, picklistGuid: this.guid }]; }; const result = await service.checkForDuplicateOptionValues( this.id ); // Has dupes expect(result).to.be.true; } @TestCase('should be able to get merge conflicts for picklist') async getMergeConflictsForPicklist (service: CustomDataTablesService) { const result = await service.getMergeConflictsForPicklists(1, 2); expect(result.numberOfConflicts).to.be.equal(2); const hasAdaptedConflicts = result.results.every((item) => { let passes = false; switch (item.id) { case '1': passes = item.conflictResolutionRequired === this.conflictInfo['1'].conflictResolutionRequired; break; case '2': passes = item.conflictResolutionRequired === this.conflictInfo['2'].conflictResolutionRequired; break; case '3': passes = item.conflictResolutionRequired === this.conflictInfo['3'].conflictResolutionRequired; break; } return passes; }); expect(hasAdaptedConflicts).to.be.true; } @TestCase('should be able to get options for key or value to resolve merge') getOptionsForKeyOrValueToResolveMerge (service: CustomDataTablesService) { const options = service.getOptionsForKeyOrValueToResolveMerge( { id: '1', ...this.conflictInfo['1'] } ); const conflict = this.conflictInfo['1']; const hasCorrectOptions = options.every((opt) => { return ( opt.label === conflict.picklistOption.value && opt.value === conflict.picklistOption.id ) || ( opt.label === conflict.picklistOptionWithSameKey.value && opt.value === conflict.picklistOptionWithSameKey.id ); }); expect(hasCorrectOptions).to.be.true; } @TestCase('should be able to adapt modal info for merge payload') adaptModalInfoForMergePayload (service: CustomDataTablesService) { const newName = 'new name'; const picklistToKeepId = 1; const picklistToMergeId = 2; const conflictResolution1 = 1; const conflictResolution2 = 2; const adapted = service.adaptModalInfoForMergePayload( newName, picklistToKeepId, picklistToMergeId, { 1: { items: [], picklistOptionId: conflictResolution1 }, 2: { items: [], picklistOptionId: conflictResolution2 } } ); expect(adapted.newPicklistName).to.be.equal(newName); expect(adapted.picklistToKeepId).to.be.equal(picklistToKeepId); expect(adapted.picklistToMergeId).to.be.equal(picklistToMergeId); expect(adapted.conflictResolutions['1']).to.be.equal(conflictResolution1); expect(adapted.conflictResolutions['2']).to.be.equal(conflictResolution2); } @TestCase('should be able to get options for detail') getOptionsForCustomDataTableDetail (service: CustomDataTablesService) { const options = service.getOptionsForCustomDataTableDetail(this.id); expect(options.length).to.be.equal(this.customDataTableOptions.length); const option = options[0]; const expectedOption = this.customDataTableOptions[0]; expect(option.key).to.be.equal(expectedOption.key); expect(option.inUse).to.be.equal(expectedOption.inUse); } @TestCase('should be able to get required picklist options map') getRequiredPicklistOptionsMap (service: CustomDataTablesService) { const requiredPicklistOptionsMap = service.getRequiredPicklistOptionsMap(this.id); const options = this.customDataTableOptions; expect(Object.keys(requiredPicklistOptionsMap).length).to.be.equal( options.length ); const requiredOption = this.customDataTableOptions[0]; const requiredValue = requiredPicklistOptionsMap[ requiredOption.key ]; const mappedValue = requiredOption.values.find((value) => { return value.languageId === this.customDataTables[0].defaultLanguageId; })?.text; expect(requiredValue).to.be.equal(mappedValue); } @TestCase('should be able to validate required rows fail') async validateRequiredRowsFail (service: CustomDataTablesService) { const contents = [{ key: 'one', value: 'One', inactive: false, sortOrder: 1 }]; // This should fail because I have not imported the required option const errors = await service.validateRequiredRows(this.id, contents); const isInvalid = errors.length > 0; expect(isInvalid).to.be.true; } @TestCase('should be able to validate required rows pass') async validateRequiredRowsPass (service: CustomDataTablesService) { const requiredOption1 = this.customDataTableOptions[0]; const requiredOption2 = this.customDataTableOptions[1]; const contents = [{ key: requiredOption1.key, value: '', inactive: false, sortOrder: 1 }, { key: requiredOption2.key, value: '', inactive: false, sortOrder: 1 }]; // This should pass because I have imported the required option const errors = await service.validateRequiredRows(this.id, contents); const isInvalid = errors.length > 0; expect(isInvalid).to.be.false; } @TestCase('should be able to get cdt items map for row - filtered') getCdtItemsMapForRowFiltered (service: CustomDataTablesService) { const itemsMap = service.getCdtItemsMapForRow( this.tableColumns, { rowId: 1, columns: [{ referenceFieldId: this.cdtTableField.referenceFieldId, referenceFieldKey: this.cdtTableField.key, value: this.sampleKey1, dateValue: null, currencyValue: null, numericValue: null, file: null, files: [], applicationId: 1, applicationFormId: 2 }] }, false ); const itemsForColumn = itemsMap[this.cdtTableField.referenceFieldId]; // Should only return in use items expect(itemsForColumn.length).to.be.equal(1); const hasCorrectItems = itemsForColumn.every((item) => { return item.value === this.sampleKey1; }); expect(hasCorrectItems).to.be.true; } @TestCase('should be able to get cdt items map for row - not filtered') getCdtItemsMapForRowNotFiltered (service: CustomDataTablesService) { const itemsMap = service.getCdtItemsMapForRow( this.tableColumns, { rowId: 1, columns: [{ referenceFieldId: this.cdtTableField.referenceFieldId, referenceFieldKey: this.cdtTableField.key, value: this.sampleKey1, dateValue: null, currencyValue: null, numericValue: null, file: null, files: [], applicationId: 1, applicationFormId: 2 }] }, true ); const itemsForColumn = itemsMap[this.cdtTableField.referenceFieldId]; // Should return all items expect(itemsForColumn.length).to.be.equal(this.keyValues.length); const expectedKeys = this.keyValues.map((keyValue) => { return keyValue.key; }); const hasCorrectItems = itemsForColumn.every((item) => { return expectedKeys.includes(item.value); }); expect(hasCorrectItems).to.be.true; } @TestCase('should be able to handle add option') async handleAddOption (service: CustomDataTablesService) { spy.on(service['customDataTableResources'], 'updateOptionValue', async () => {}); spy.on(service['customDataTableResources'], 'addDataTableOption', async () => 42143); spy.on(service, 'handleAdjustSortOrder', () => {}); const modalResponse: AddEditDataTableOptionModalResponse = { value: 'Cheese', key: 'cheese', sortOrder: 1, parentKeys: [] }; await service.handleAddOrEditOption( this.id, 2, [1], modalResponse, null, 'en-US' ); expect(service['customDataTableResources']['updateOptionValue']).to.have.not.been.called; expect(service['customDataTableResources']['addDataTableOption']).to.have.been.called.once; expect(service['handleAdjustSortOrder']).to.have.been.called.once; } @TestCase('should be able to handle edit option') async handleEditOption (service: CustomDataTablesService) { spy.on(service['customDataTableResources'], 'updateOptionValue', async () => {}); spy.on(service['customDataTableResources'], 'addDataTableOption', async () => 42143); spy.on(service, 'handleAdjustSortOrder', () => {}); const modalResponse: AddEditDataTableOptionModalResponse = { value: 'Cheese', key: 'cheese', sortOrder: 3, parentKeys: [] }; const option: CustomDataTableDetailViewOption = { id: 435, key: 'test', value: 'test', inUse: true, sortOrder: 1, createdDate: '', updatedDate: '', parentKeys: [] }; await service.handleAddOrEditOption( this.id, 1, [2], modalResponse, option, 'en-US' ); expect(service['customDataTableResources']['addDataTableOption']).to.have.not.been.called; expect(service['customDataTableResources']['updateOptionValue']).to.have.been.called.once; expect(service['handleAdjustSortOrder']).to.not.have.been.called; } @TestCase('should be able to handle add option - fail') async handleAddOptionFail (service: CustomDataTablesService) { spy.on(service['customDataTableResources'], 'addDataTableOption', async () => { // eslint-disable-next-line no-throw-literal throw { error: { message: 'fail' } }; }); spy.on(service['notifierService'], 'error'); spy.on(service['notifierService'], 'success'); spy.on(service['logger'], 'error'); const modalResponse: AddEditDataTableOptionModalResponse = { value: 'Cheese', key: 'cheese', sortOrder: 1, parentKeys: [] }; await service.handleAddOrEditOption( this.id, 2, [1], modalResponse, null, 'en-US' ); expect(service['notifierService']['error']).to.have.been.called.once; expect(service['logger']['error']).to.have.been.called.once; expect(service['notifierService']['success']).to.not.have.been.called; } @TestCase('should be able to handle adjust sort order') async handleAdjustSortOrder (service: CustomDataTablesService) { let payload: UpdateSortOrderPayload; spy.on(service['customDataTableResources'], 'updateSortOrder', async (result: UpdateSortOrderPayload) => { payload = result; }); await service.handleAdjustSortOrder( this.id, this.option2Id, 1 ); expect(service['customDataTableResources']['updateSortOrder']).to.have.been.called.once; const hasNewSortOrder = payload.picklistOptionsWithSortOrder.some((item) => { return item.picklistOptionId === this.option2Id && item.sortOrder === 1; }); const adjustedSortOrder = payload.picklistOptionsWithSortOrder.some((item) => { return item.picklistOptionId === this.option1Id && item.sortOrder === 2; }); expect(hasNewSortOrder && adjustedSortOrder).to.be.true; } }