diff --git a/webapp/package.json b/webapp/package.json index ad807a7194d..a1479ed1a2d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -14,7 +14,7 @@ }, "scripts": { "postinstall": "patch-package && ng cache clean", - "unit:mocha": "UNIT_TEST_ENV=1 mocha 'tests/mocha/**/*.spec.js'", + "unit:mocha": "UNIT_TEST_ENV=1 mocha 'tests/mocha/**/*.spec.js' && mocha --config tests/mocha/ts/.mocharc.js", "unit:mocha:tz": "TZ=Canada/Pacific npm run unit:mocha && TZ=Africa/Monrovia npm run unit:mocha && TZ=Pacific/Auckland npm run unit:mocha", "unit:cht-form": "ng test cht-form", "unit": "UNIT_TEST_ENV=1 ng test webapp", diff --git a/webapp/src/ts/libs/schema.ts b/webapp/src/ts/libs/schema.ts new file mode 100644 index 00000000000..2f939228da9 --- /dev/null +++ b/webapp/src/ts/libs/schema.ts @@ -0,0 +1,10 @@ +export const hasProperty = (obj: unknown, prop: T): obj is Record => { + return typeof obj === 'object' && obj !== null && prop in obj; +}; + +export const getProperty = (obj: unknown, prop: T): unknown => { + if (hasProperty(obj, prop)) { + return obj[prop]; + } + return undefined; +}; diff --git a/webapp/src/ts/modals/edit-user/update-password.component.ts b/webapp/src/ts/modals/edit-user/update-password.component.ts index fece50288c0..913137c774c 100644 --- a/webapp/src/ts/modals/edit-user/update-password.component.ts +++ b/webapp/src/ts/modals/edit-user/update-password.component.ts @@ -8,6 +8,7 @@ import { UserSettingsService } from '@mm-services/user-settings.service'; import { UpdatePasswordService } from '@mm-services/update-password.service'; import { UserLoginService } from '@mm-services/user-login.service'; import { TranslateService } from '@mm-services/translate.service'; +import { getProperty } from '../../libs/schema'; const PASSWORD_MINIMUM_LENGTH = 8; const PASSWORD_MINIMUM_SCORE = 50; @@ -64,7 +65,7 @@ export class UpdatePasswordComponent { try { await this.userLoginService.login(username, newPassword); } catch (err) { - if (err.status === 302) { + if (getProperty(err, 'status') === 302) { this.close(); const snackText = await this.translateService.get('password.updated'); this.globalActions.setSnackbarContent(snackText); @@ -73,12 +74,13 @@ export class UpdatePasswordComponent { } } } catch (error) { - if (error.status === 0) { // Offline status + const status = getProperty(error, 'status'); + if (status === 0) { // Offline status const message = await this.translateService.get('online.action.message'); this.setError(ErrorType.SUBMIT, message); return; } - if (error.status === 401) { + if (status === 401) { const message = await this.translateService.get('password.incorrect'); this.setError(ErrorType.CURRENT_PASSWORD, message); return; diff --git a/webapp/src/ts/modules/contacts/contacts-edit.component.ts b/webapp/src/ts/modules/contacts/contacts-edit.component.ts index e0407b7caf4..2ab9eb7a948 100644 --- a/webapp/src/ts/modules/contacts/contacts-edit.component.ts +++ b/webapp/src/ts/modules/contacts/contacts-edit.component.ts @@ -13,6 +13,7 @@ import { Selectors } from '@mm-selectors/index'; import { GlobalActions } from '@mm-actions/global'; import { PerformanceService } from '@mm-services/performance.service'; import { TranslateService } from '@mm-services/translate.service'; +import { getProperty } from '../../libs/schema'; @Component({ templateUrl: './contacts-edit.component.html' @@ -173,7 +174,7 @@ export class ContactsEditComponent implements OnInit, OnDestroy, AfterViewInit { this.globalActions.setLoadingContent(false); } catch (error) { - this.errorTranslationKey = error.translationKey || 'error.loading.form'; + this.errorTranslationKey = getProperty(error, 'translationKey') || 'error.loading.form'; this.globalActions.setLoadingContent(false); this.contentError = true; console.error('Error loading contact form.', error); diff --git a/webapp/src/ts/modules/reports/reports.component.ts b/webapp/src/ts/modules/reports/reports.component.ts index 12ec12282c6..d88b2d97713 100644 --- a/webapp/src/ts/modules/reports/reports.component.ts +++ b/webapp/src/ts/modules/reports/reports.component.ts @@ -25,6 +25,7 @@ import { XmlFormsService } from '@mm-services/xml-forms.service'; import { PerformanceService } from '@mm-services/performance.service'; import { ExtractLineageService } from '@mm-services/extract-lineage.service'; import { ButtonType } from '@mm-components/fast-action-button/fast-action-button.component'; +import { getProperty } from '../../libs/schema'; const PAGE_SIZE = 50; const CAN_DEFAULT_FACILITY_FILTER = 'can_default_facility_filter'; @@ -335,7 +336,7 @@ export class ReportsComponent implements OnInit, AfterViewInit, OnDestroy { const userContact = await this.userContactService.get(); return userContact?.parent; } catch (error) { - console.error(error.message, error); + console.error(getProperty(error, 'message'), error); } } diff --git a/webapp/src/ts/modules/tasks/tasks.component.ts b/webapp/src/ts/modules/tasks/tasks.component.ts index a68c342b8dd..d80bc012261 100644 --- a/webapp/src/ts/modules/tasks/tasks.component.ts +++ b/webapp/src/ts/modules/tasks/tasks.component.ts @@ -13,6 +13,7 @@ import { GlobalActions } from '@mm-actions/global'; import { LineageModelGeneratorService } from '@mm-services/lineage-model-generator.service'; import { PerformanceService } from '@mm-services/performance.service'; import { ExtractLineageService } from '@mm-services/extract-lineage.service'; +import { getProperty } from '../../libs/schema'; @Component({ templateUrl: './tasks.component.html', @@ -154,7 +155,7 @@ export class TasksComponent implements OnInit, OnDestroy { } catch (exception) { console.error('Error getting tasks for all contacts', exception); - this.errorStack = exception.stack; + this.errorStack = getProperty(exception, 'stack'); this.hasTasks = false; this.tasksActions.setTasksList([]); } finally { diff --git a/webapp/src/ts/services/android-api.service.ts b/webapp/src/ts/services/android-api.service.ts index 2327ce87d17..eaa8a8b0256 100644 --- a/webapp/src/ts/services/android-api.service.ts +++ b/webapp/src/ts/services/android-api.service.ts @@ -5,6 +5,7 @@ import { GeolocationService } from '@mm-services/geolocation.service'; import { MRDTService } from '@mm-services/mrdt.service'; import { SessionService } from '@mm-services/session.service'; import { NavigationService } from '@mm-services/navigation.service'; +import { getProperty } from '../libs/schema'; /** * An API to provide integration with the medic-android app. @@ -176,9 +177,9 @@ export class AndroidApiService { try { this.mrdtService.respond(JSON.parse(response)); } catch (e) { - return console.error( - new Error(`Unable to parse JSON response from android app: "${response}", error message: "${e.message}"`) - ); + return console.error(new Error( + `Unable to parse JSON response from android app: "${response}", error message: "${getProperty(e, 'message')}"` + )); } } @@ -190,9 +191,9 @@ export class AndroidApiService { try { this.mrdtService.respondTimeTaken(JSON.parse(response)); } catch (e) { - return console.error( - new Error(`Unable to parse JSON response from android app: "${response}", error message: "${e.message}"`) - ); + return console.error(new Error( + `Unable to parse JSON response from android app: "${response}", error message: "${getProperty(e, 'message')}"` + )); } } diff --git a/webapp/src/ts/services/form.service.ts b/webapp/src/ts/services/form.service.ts index 2329c820407..c17b38414e3 100644 --- a/webapp/src/ts/services/form.service.ts +++ b/webapp/src/ts/services/form.service.ts @@ -26,6 +26,7 @@ import { reduce as _reduce } from 'lodash-es'; import { ContactTypesService } from '@mm-services/contact-types.service'; import { TargetAggregatesService } from '@mm-services/target-aggregates.service'; import { ContactViewModelGeneratorService } from '@mm-services/contact-view-model-generator.service'; +import { getProperty } from '../libs/schema'; /** * Service for interacting with forms. This is the primary entry-point for CHT code to render forms and save the @@ -176,11 +177,11 @@ export class FormService { } return await this.enketoService.renderForm(formContext, doc, userSettings); } catch (error) { - if (error.translationKey) { + if (getProperty(error, 'translationKey')) { throw error; } const errorMessage = `Failed during the form "${formDoc.internalId}" rendering : `; - throw new Error(errorMessage + error.message); + throw new Error(errorMessage + getProperty(error, 'message')); } } diff --git a/webapp/src/ts/services/indexed-db.service.ts b/webapp/src/ts/services/indexed-db.service.ts index 680fdb7e293..806b02b96cc 100644 --- a/webapp/src/ts/services/indexed-db.service.ts +++ b/webapp/src/ts/services/indexed-db.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { DbService } from '@mm-services/db.service'; +import { getProperty } from '../libs/schema'; @Injectable({ providedIn: 'root' @@ -84,7 +85,7 @@ export class IndexedDbService { try { localDoc = await this.loadingLocalDoc; } catch (error) { - if (error.status !== 404) { + if (getProperty(error, 'status') !== 404) { throw error; } console.debug('IndexedDbService :: Local doc not created yet. Ignoring error.'); diff --git a/webapp/src/ts/services/migrations/target-checkpointer.migration.ts b/webapp/src/ts/services/migrations/target-checkpointer.migration.ts index 2a6e118840f..5cb2d440b5d 100644 --- a/webapp/src/ts/services/migrations/target-checkpointer.migration.ts +++ b/webapp/src/ts/services/migrations/target-checkpointer.migration.ts @@ -3,6 +3,7 @@ import { default as generateReplicationId } from 'pouchdb-generate-replication-i import { Migration } from './migration'; import { DbService } from '@mm-services/db.service'; +import { getProperty } from '../../libs/schema'; @Injectable({ providedIn: 'root' @@ -30,7 +31,7 @@ export class TargetCheckpointerMigration extends Migration { try { return await this.dbService.get().get(replicationId); } catch (err) { - if (err?.status === 404) { + if (getProperty(err, 'status') === 404) { return; } throw err; @@ -48,7 +49,7 @@ export class TargetCheckpointerMigration extends Migration { await this.dbService.get({ remote: true }).put(localDoc); return true; } catch (err) { - if (err?.status === 409) { + if (getProperty(err, 'status') === 409) { // dont fail on conflicts return true; } diff --git a/webapp/src/ts/services/telemetry.service.ts b/webapp/src/ts/services/telemetry.service.ts index f566e377c11..6ecfa63ac4e 100644 --- a/webapp/src/ts/services/telemetry.service.ts +++ b/webapp/src/ts/services/telemetry.service.ts @@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid'; import { DbService } from '@mm-services/db.service'; import { SessionService } from '@mm-services/session.service'; import { IndexedDbService } from '@mm-services/indexed-db.service'; +import { getProperty } from '../libs/schema'; @Injectable({ providedIn: 'root' @@ -196,7 +197,7 @@ export class TelemetryService { .get({ meta: true }) .put(aggregateDoc); } catch (error) { - if (error.status === 409) { + if (getProperty(error, 'status') === 409) { return this.storeConflictedAggregate(aggregateDoc); } throw error; diff --git a/webapp/src/ts/services/user-contact.service.ts b/webapp/src/ts/services/user-contact.service.ts index 98ae8da1a5d..a446736870e 100644 --- a/webapp/src/ts/services/user-contact.service.ts +++ b/webapp/src/ts/services/user-contact.service.ts @@ -3,6 +3,7 @@ import { Person, Qualifier } from '@medic/cht-datasource'; import { UserSettingsService } from '@mm-services/user-settings.service'; import { CHTDatasourceService } from '@mm-services/cht-datasource.service'; +import { getProperty } from '../libs/schema'; @Injectable({ providedIn: 'root' @@ -31,7 +32,7 @@ export class UserContactService { try { return await this.userSettingsService.get(); } catch (err) { - if (err.code === 404) { + if (getProperty(err, 'code') === 404) { return null; } throw err; diff --git a/webapp/tests/karma/js/enketo/medic-xpath-extensions.spec.ts b/webapp/tests/karma/js/enketo/medic-xpath-extensions.spec.ts index d8735d9c357..4e40bda4ecd 100644 --- a/webapp/tests/karma/js/enketo/medic-xpath-extensions.spec.ts +++ b/webapp/tests/karma/js/enketo/medic-xpath-extensions.spec.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import * as medicXpathExtensions from '../../../../src/js/enketo/medic-xpath-extensions.js'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('Medic XPath Extensions', () => { it('should have expected attributes', () => { @@ -78,7 +79,7 @@ describe('Medic XPath Extensions', () => { try { extensionLib({ v: 'myfunc' }, { t: 'string', v: 'hello' }); } catch (e) { - expect(e.message).to.equal('Form configuration error: no extension-lib with ID "myfunc" found'); + expect(getProperty(e, 'message')).to.equal('Form configuration error: no extension-lib with ID "myfunc" found'); return; } throw new Error('Expected exception to be thrown.'); diff --git a/webapp/tests/karma/ts/providers/parse.provider.spec.ts b/webapp/tests/karma/ts/providers/parse.provider.spec.ts index 094fff3448c..46860809b77 100644 --- a/webapp/tests/karma/ts/providers/parse.provider.spec.ts +++ b/webapp/tests/karma/ts/providers/parse.provider.spec.ts @@ -11,6 +11,7 @@ import { PhonePipe } from '@mm-pipes/phone.pipe'; import { FormatDateService } from '@mm-services/format-date.service'; import { RelativeDateService } from '@mm-services/relative-date.service'; import { XmlFormsContextUtilsService } from '@mm-services/xml-forms-context-utils.service'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('Parse provider', () => { let provider:ParseProvider; @@ -47,7 +48,7 @@ describe('Parse provider', () => { result = parse('2 ===== 3'); assert.fail('should have thrown'); } catch (e) { - expect(e.message.startsWith('Parser Error: Unexpected token')).to.equal(true); + expect((getProperty(e, 'message') as string).startsWith('Parser Error: Unexpected token')).to.equal(true); expect(result).to.equal(undefined); } }); diff --git a/webapp/tests/karma/ts/services/delete-docs.service.spec.ts b/webapp/tests/karma/ts/services/delete-docs.service.spec.ts index 4225f074194..1c72e96bdd0 100644 --- a/webapp/tests/karma/ts/services/delete-docs.service.spec.ts +++ b/webapp/tests/karma/ts/services/delete-docs.service.spec.ts @@ -7,6 +7,7 @@ import { SessionService } from '@mm-services/session.service'; import { ChangesService } from '@mm-services/changes.service'; import { DeleteDocsService } from '@mm-services/delete-docs.service'; import { ExtractLineageService } from '@mm-services/extract-lineage.service'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('DeleteDocs service', () => { @@ -266,7 +267,7 @@ describe('DeleteDocs service', () => { try { JSON.stringify(report); } catch (e) { - if (e.message.startsWith('Converting circular structure to JSON')) { + if ((getProperty(e, 'message') as string).startsWith('Converting circular structure to JSON')) { isCircularBefore = true; } } @@ -277,7 +278,7 @@ describe('DeleteDocs service', () => { try { JSON.stringify(bulkDocs.args[0][0][0]); } catch (e) { - if (e.message.startsWith('Converting circular structure to JSON')) { + if ((getProperty(e, 'message') as string).startsWith('Converting circular structure to JSON')) { isCircularAfter = true; } } diff --git a/webapp/tests/karma/ts/services/enketo.service.spec.ts b/webapp/tests/karma/ts/services/enketo.service.spec.ts index 5386a298f11..ec7db515e70 100644 --- a/webapp/tests/karma/ts/services/enketo.service.spec.ts +++ b/webapp/tests/karma/ts/services/enketo.service.spec.ts @@ -12,6 +12,7 @@ import { TranslateService } from '@mm-services/translate.service'; import { EnketoFormContext, EnketoService } from '@mm-services/enketo.service'; import { ExtractLineageService } from '@mm-services/extract-lineage.service'; import * as FileManager from '../../../../src/js/enketo/file-manager.js'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('Enketo service', () => { // return a mock form ready for putting in #dbContent @@ -130,7 +131,7 @@ describe('Enketo service', () => { expect.fail('Should throw error'); } catch (error) { expect(enketoInit.callCount).to.equal(1); - expect(error.message).to.equal('["nope","still nope"]'); + expect(getProperty(error, 'message')).to.equal('["nope","still nope"]'); } })); diff --git a/webapp/tests/karma/ts/services/form.service.spec.ts b/webapp/tests/karma/ts/services/form.service.spec.ts index 5b8599081a5..d88889fd16b 100644 --- a/webapp/tests/karma/ts/services/form.service.spec.ts +++ b/webapp/tests/karma/ts/services/form.service.spec.ts @@ -36,6 +36,7 @@ import { EnketoTranslationService } from '@mm-services/enketo-translation.servic import * as FileManager from '../../../../src/js/enketo/file-manager.js'; import { TargetAggregatesService } from '@mm-services/target-aggregates.service'; import { ContactViewModelGeneratorService } from '@mm-services/contact-view-model-generator.service'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('Form service', () => { // return a mock form ready for putting in #dbContent @@ -297,7 +298,7 @@ describe('Form service', () => { { doc: { _id: '123-patient-contact' }, contactSummary: { pregnant: false }, shouldEvaluateExpression: true }, ]); expect(enketoInit.callCount).to.equal(1); - expect(error.message).to.equal(expectedErrorMessage); + expect(getProperty(error, 'message')).to.equal(expectedErrorMessage); expect(consoleErrorMock.callCount).to.equal(0); } })); @@ -585,7 +586,7 @@ describe('Form service', () => { flush(); expect.fail('Should throw error'); } catch (error) { - expect(error.message).to.equal('Failed during the form "myform" rendering : invalid user'); + expect(getProperty(error, 'message')).to.equal('Failed during the form "myform" rendering : invalid user'); expect(UserContact.calledOnce).to.be.true; expect(renderForm.notCalled).to.be.true; expect(enketoInit.notCalled).to.be.true; diff --git a/webapp/tests/karma/ts/services/replication.service.spec.ts b/webapp/tests/karma/ts/services/replication.service.spec.ts index 9fcb44ea1c2..a9965d792e4 100644 --- a/webapp/tests/karma/ts/services/replication.service.spec.ts +++ b/webapp/tests/karma/ts/services/replication.service.spec.ts @@ -7,6 +7,7 @@ import { ReplicationService } from '@mm-services/replication.service'; import { DbService } from '@mm-services/db.service'; import { of, throwError } from 'rxjs'; import { RulesEngineService } from '@mm-services/rules-engine.service'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('ContactTypes service', () => { @@ -330,7 +331,7 @@ describe('ContactTypes service', () => { await service.replicateFrom(); expect.fail('Should have thrown'); } catch (err) { - expect(err.message).to.equal('omg'); + expect(getProperty(err, 'message')).to.equal('omg'); expect(localDb.allDocs.callCount).to.equal(0); expect(localDb.bulkDocs.callCount).to.equal(0); expect(remoteDb.bulkGet.callCount).to.equal(0); @@ -351,7 +352,7 @@ describe('ContactTypes service', () => { await service.replicateFrom(); expect.fail('Should have thrown'); } catch (err) { - expect(err.message).to.equal('alldocsfail'); + expect(getProperty(err, 'message')).to.equal('alldocsfail'); expect(localDb.bulkDocs.callCount).to.equal(0); expect(remoteDb.bulkGet.callCount).to.equal(0); expect(http.post.callCount).to.equal(0); @@ -381,7 +382,7 @@ describe('ContactTypes service', () => { await service.replicateFrom(); expect.fail('Should have thrown'); } catch (err) { - expect(err.message).to.equal('bulkgeterror'); + expect(getProperty(err, 'message')).to.equal('bulkgeterror'); expect(localDb.bulkDocs.callCount).to.equal(0); expect(http.post.callCount).to.equal(0); } @@ -416,7 +417,7 @@ describe('ContactTypes service', () => { await service.replicateFrom(); expect.fail('Should have thrown'); } catch (err) { - expect(err.message).to.equal('bulkdocserr'); + expect(getProperty(err, 'message')).to.equal('bulkdocserr'); expect(http.post.callCount).to.equal(0); } }); @@ -444,7 +445,7 @@ describe('ContactTypes service', () => { await service.replicateFrom(); expect.fail('Should have thrown'); } catch (err) { - expect(err.message).to.equal('getdeleteserror'); + expect(getProperty(err, 'message')).to.equal('getdeleteserror'); expect(localDb.bulkDocs.callCount).to.equal(0); } }); @@ -474,7 +475,7 @@ describe('ContactTypes service', () => { await service.replicateFrom(); expect.fail('Should have thrown'); } catch (err) { - expect(err.message).to.equal('bulkdocserror2'); + expect(getProperty(err, 'message')).to.equal('bulkdocserror2'); } }); }); diff --git a/webapp/tests/karma/ts/services/rules-engine.service.spec.ts b/webapp/tests/karma/ts/services/rules-engine.service.spec.ts index a3dbc7e793d..22254e0d65b 100644 --- a/webapp/tests/karma/ts/services/rules-engine.service.spec.ts +++ b/webapp/tests/karma/ts/services/rules-engine.service.spec.ts @@ -18,6 +18,7 @@ import { TranslateFromService } from '@mm-services/translate-from.service'; import { RulesEngineCoreFactoryService, RulesEngineService } from '@mm-services/rules-engine.service'; import { PipesService } from '@mm-services/pipes.service'; import { CHTDatasourceService } from '@mm-services/cht-datasource.service'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('RulesEngineService', () => { let service: RulesEngineService; @@ -215,7 +216,7 @@ describe('RulesEngineService', () => { await func(); assert.fail('Should throw'); } catch (err) { - expect(err.name).to.include(include); + expect(getProperty(err, 'name')).to.include(include); } }; diff --git a/webapp/tests/karma/ts/services/training-cards.service.spec.ts b/webapp/tests/karma/ts/services/training-cards.service.spec.ts index e7a67de3c7d..18ae37fdc5c 100644 --- a/webapp/tests/karma/ts/services/training-cards.service.spec.ts +++ b/webapp/tests/karma/ts/services/training-cards.service.spec.ts @@ -11,6 +11,7 @@ import { TrainingCardsService } from '@mm-services/training-cards.service'; import { SessionService } from '@mm-services/session.service'; import { RouteSnapshotService } from '@mm-services/route-snapshot.service'; import { FeedbackService } from '@mm-services/feedback.service'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('TrainingCardsService', () => { let service: TrainingCardsService; @@ -767,7 +768,7 @@ describe('TrainingCardsService', () => { service.getTrainingCardDocId().startsWith('training:ronald:'); assert.fail('should have thrown'); } catch (error) { - expect(error.message) + expect(getProperty(error, 'message')) .to.equal('Training Cards :: Cannot create document ID, user context does not have the "name" property.'); } }); diff --git a/webapp/tests/karma/ts/services/transitions.service.spec.ts b/webapp/tests/karma/ts/services/transitions.service.spec.ts index 56bac18ef76..f48ce2a118a 100644 --- a/webapp/tests/karma/ts/services/transitions.service.spec.ts +++ b/webapp/tests/karma/ts/services/transitions.service.spec.ts @@ -8,6 +8,7 @@ import { SettingsService } from '@mm-services/settings.service'; import { MutingTransition } from '@mm-services/transitions/muting.transition'; import { ValidationService } from '@mm-services/validation.service'; import { CreateUserForContactsTransition } from '@mm-services/transitions/create-user-for-contacts.transition'; +import { getProperty } from '../../../../src/ts/libs/schema'; describe('Transitions Service', () => { let settingsService; @@ -264,7 +265,7 @@ describe('Transitions Service', () => { await service.applyTransitions(docs); expect.fail('should have thrown an error'); } catch (err) { - expect(err.message).to.equal(`An array of valid doc objects must be provided.`); + expect(getProperty(err, 'message')).to.equal(`An array of valid doc objects must be provided.`); } expect(mutingTransition.filter.callCount).to.equal(0); diff --git a/webapp/tests/karma/ts/services/transitions/create-user-for-contacts.transition.spec.ts b/webapp/tests/karma/ts/services/transitions/create-user-for-contacts.transition.spec.ts index 2ff76b6eab9..1266cd42290 100644 --- a/webapp/tests/karma/ts/services/transitions/create-user-for-contacts.transition.spec.ts +++ b/webapp/tests/karma/ts/services/transitions/create-user-for-contacts.transition.spec.ts @@ -8,6 +8,7 @@ import { expect } from 'chai'; import { UserContactService } from '@mm-services/user-contact.service'; import { ExtractLineageService } from '@mm-services/extract-lineage.service'; import { CHTDatasourceService } from '@mm-services/cht-datasource.service'; +import { getProperty } from '../../../../../src/ts/libs/schema'; const deepFreeze = obj => { Object @@ -395,7 +396,7 @@ describe('Create User for Contacts Transition', () => { await transition.run([replaceUserDoc]); expect(true).to.equal('should have thrown an error'); } catch (err) { - expect(err.message).to.equal( + expect(getProperty(err, 'message')).to.equal( 'Only the contact associated with the currently logged in user can be replaced.' ); } @@ -417,8 +418,10 @@ describe('Create User for Contacts Transition', () => { await transition.run([replaceUserDoc]); expect(true).to.equal('should have thrown an error'); } catch (err) { - expect(err.message).to.equal('The form for replacing a user must include a replacement_contact_id field ' + - 'containing the id of the new contact.'); + expect(getProperty(err, 'message')).to.equal( + 'The form for replacing a user must include a replacement_contact_id field ' + + 'containing the id of the new contact.' + ); } expect(userContactService.get.callCount).to.equal(1); @@ -437,7 +440,7 @@ describe('Create User for Contacts Transition', () => { await transition.run([REPLACE_USER_DOC]); expect(true).to.equal('should have thrown an error'); } catch (err) { - expect(err.message).to.equal(`The new contact could not be found [${NEW_CONTACT._id}].`); + expect(getProperty(err, 'message')).to.equal(`The new contact could not be found [${NEW_CONTACT._id}].`); } expect(userContactService.get.callCount).to.equal(1); @@ -455,7 +458,7 @@ describe('Create User for Contacts Transition', () => { await transition.run([REPLACE_USER_DOC]); expect(true).to.equal('should have thrown an error'); } catch (err) { - expect(err.message).to.equal(`Server Error`); + expect(getProperty(err, 'message')).to.equal(`Server Error`); } expect(userContactService.get.callCount).to.equal(1); @@ -475,7 +478,9 @@ describe('Create User for Contacts Transition', () => { await transition.run([REPLACE_USER_DOC, NEW_CONTACT, REPLACE_USER_DOC]); expect(true).to.equal('should have thrown an error'); } catch (err) { - expect(err.message).to.equal(`Only one user replace form is allowed to be submitted per transaction.`); + expect(getProperty(err, 'message')).to.equal( + `Only one user replace form is allowed to be submitted per transaction.` + ); } expect(userContactService.get.callCount).to.equal(1); diff --git a/webapp/tests/mocha/ts/.mocharc.js b/webapp/tests/mocha/ts/.mocharc.js new file mode 100644 index 00000000000..96b1e599f3e --- /dev/null +++ b/webapp/tests/mocha/ts/.mocharc.js @@ -0,0 +1,8 @@ +const chaiAsPromised = require('chai-as-promised'); +const chai = require('chai'); +chai.use(chaiAsPromised); + +module.exports = { + spec: 'tests/mocha/ts/**/*.spec.ts', + require: 'ts-node/register', +}; diff --git a/webapp/tests/mocha/ts/libs/schema.spec.ts b/webapp/tests/mocha/ts/libs/schema.spec.ts new file mode 100644 index 00000000000..7a74da08ecb --- /dev/null +++ b/webapp/tests/mocha/ts/libs/schema.spec.ts @@ -0,0 +1,46 @@ +import { expect } from 'chai'; +import { getProperty, hasProperty } from '../../../../src/ts/libs/schema'; + +describe('Schema', () => { + describe('hasProperty', () => { + it('returns true if the object has the property', () => { + expect(hasProperty({ foo: 'bar' }, 'foo')).to.be.true; + }); + + [ + null, + undefined, + 'foo', + 1, + true, + [], + {}, + { bar: 'foo' } + ].forEach((value) => { + it(`returns false if the object does not have the property: ${value}`, () => { + expect(hasProperty(value, 'foo')).to.be.false; + }); + }); + }); + + describe('getProperty', () => { + it('returns the property value if the object has the property', () => { + expect(getProperty({ foo: 'bar' }, 'foo')).to.equal('bar'); + }); + + [ + null, + undefined, + 'foo', + 1, + true, + [], + {}, + { bar: 'foo' } + ].forEach((value) => { + it(`returns undefined if the object does not have the property: ${value}`, () => { + expect(getProperty(value, 'foo')).to.be.undefined; + }); + }); + }); +}); diff --git a/webapp/tsconfig.base.json b/webapp/tsconfig.base.json index b41cb20ee68..6863da9512f 100755 --- a/webapp/tsconfig.base.json +++ b/webapp/tsconfig.base.json @@ -36,6 +36,7 @@ "noImplicitThis": true, "strictBindCallApply": true, "strictPropertyInitialization": true, + "useUnknownInCatchVariables": true }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, diff --git a/webapp/tsconfig.spec.json b/webapp/tsconfig.spec.json index 6301e1bcc3c..50cecc0021d 100755 --- a/webapp/tsconfig.spec.json +++ b/webapp/tsconfig.spec.json @@ -16,6 +16,7 @@ ], "include": [ "tests/karma/**/*.spec.ts", - "tests/karma/**/*.d.ts" + "tests/karma/**/*.d.ts", + "tests/mocha/ts/**/*.spec.ts" ] }